First commit
This commit is contained in:
@@ -0,0 +1,974 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS — Manajemen Jalan & Parsil Tanah</title>
|
||||
<meta name="description" content="Sistem Informasi Geografis untuk manajemen data jalan dan parsil tanah. Berbasis LeafletJS, PHP, dan MySQL.">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Link to the extracted premium styling -->
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
/* CSS Tambahan khusus untuk Jalan dan Parsil */
|
||||
:root {
|
||||
--jalan: #4f46e5;
|
||||
--parsil: #10b981;
|
||||
}
|
||||
.db-btn {
|
||||
width: 36px; height: 36px; border-radius: 9px;
|
||||
border: 1.5px solid rgba(255,255,255,0.07);
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: #64748b; cursor: pointer; font-size: 15px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: all 0.2s; position: relative;
|
||||
}
|
||||
.db-btn:hover { background: rgba(99,102,241,0.25); border-color: rgba(99,102,241,0.5); color: #fff; }
|
||||
.db-btn.active { background: linear-gradient(135deg,#4f46e5,#7c3aed); border-color: #a78bfa; color: #fff; box-shadow: 0 0 14px rgba(79,70,229,0.5); }
|
||||
.db-sep { height: 1px; background: rgba(255,255,255,0.06); }
|
||||
|
||||
#draw-bar {
|
||||
position: absolute; top: 50%; right: 12px;
|
||||
transform: translateY(-50%); z-index: 1000;
|
||||
display: flex; flex-direction: column; gap: 5px;
|
||||
background: rgba(13,21,38,0.93);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 14px; padding: 9px 7px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.45);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header-bar">
|
||||
<div class="header-title">
|
||||
<span style="font-size: 20px;">🗺️</span>
|
||||
<h1>WebGIS Manajemen Jalan & Parsil</h1>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div class="status-indicator">
|
||||
<div class="pulse-dot"></div>
|
||||
Sistem Online
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app-container">
|
||||
<div id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>🗺️ Sidebar Panel</h1>
|
||||
<p>Filter & Ringkasan Statistik</p>
|
||||
</div>
|
||||
<div class="sidebar-body">
|
||||
<div class="layer-info-box">
|
||||
<p>💡 Gunakan <strong>Layers Control</strong> di pojok kanan atas peta untuk menampilkan/menyembunyikan layer berdasarkan jenis datanya.</p>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- JALAN STATS -->
|
||||
<div class="section-title" style="color:var(--jalan)">🛣️ Data Jalan</div>
|
||||
<div class="count-row" style="flex-direction:column; gap:6px;">
|
||||
<div class="count-card" style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid var(--jalan); padding:8px 12px">
|
||||
<div style="text-align:left"><strong style="color:var(--jalan); font-size:12px;">Total Ruas</strong></div>
|
||||
<div class="num" id="sj-total" style="font-size:16px; color:var(--text-primary)">0</div>
|
||||
</div>
|
||||
<div class="count-card" style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid var(--jalan); padding:8px 12px">
|
||||
<div style="text-align:left"><strong style="color:var(--jalan); font-size:12px;">Panjang Keseluruhan</strong></div>
|
||||
<div class="num" id="sj-panjang" style="font-size:14px; color:var(--text-primary)">0 m</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-list" id="list-jalan">
|
||||
<div class="loading-text">⏳ Memuat data...</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- PARSIL STATS -->
|
||||
<div class="section-title" style="color:var(--parsil)">🏘️ Data Parsil Tanah</div>
|
||||
<div class="count-row" style="flex-direction:column; gap:6px;">
|
||||
<div class="count-card" style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid var(--parsil); padding:8px 12px">
|
||||
<div style="text-align:left"><strong style="color:var(--parsil); font-size:12px;">Total Bidang</strong></div>
|
||||
<div class="num" id="sp-total" style="font-size:16px; color:var(--text-primary)">0</div>
|
||||
</div>
|
||||
<div class="count-card" style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid var(--parsil); padding:8px 12px">
|
||||
<div style="text-align:left"><strong style="color:var(--parsil); font-size:12px;">Luas Keseluruhan</strong></div>
|
||||
<div class="num" id="sp-luas" style="font-size:14px; color:var(--text-primary)">0 m²</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-list" id="list-parsil">
|
||||
<div class="loading-text">⏳ Memuat data...</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<div class="legend-box">
|
||||
<div class="legend-title">Legenda Status Jalan</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#ef4444;flex-shrink:0"></div>Nasional</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#3b82f6;flex-shrink:0"></div>Provinsi</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#f59e0b;flex-shrink:0"></div>Kabupaten</div>
|
||||
</div>
|
||||
<div class="legend-box" style="border-top:none; padding-top:0;">
|
||||
<div class="legend-title">Legenda Status Parsil</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#22c55e;flex-shrink:0"></div>SHM</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#06b6d4;flex-shrink:0"></div>HGB</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#a855f7;flex-shrink:0"></div>HGU</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#f97316;flex-shrink:0"></div>HP</div>
|
||||
</div>
|
||||
</div> <!-- End sidebar-body -->
|
||||
</div> <!-- End sidebar -->
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Drawing toolbar -->
|
||||
<div id="draw-bar" style="display:none;">
|
||||
<button class="db-btn" id="db-undo" onclick="undoPt()" title="Undo (Z)">↩️</button>
|
||||
<div class="db-sep" id="db-sep1"></div>
|
||||
<button class="db-btn" id="db-finish" onclick="finishDraw()" title="Selesai (Enter)">✅</button>
|
||||
<div class="db-sep" id="db-sep2"></div>
|
||||
<button class="db-btn" id="db-cancel" onclick="cancelDraw()" title="Batal (Esc)">❌</button>
|
||||
</div>
|
||||
|
||||
<!-- Status bar -->
|
||||
<div id="status-bar" style="display:none; position:absolute; bottom:22px; left:50%; transform:translateX(-50%); z-index:1000; background:rgba(13,21,38,0.93); backdrop-filter:blur(12px); color:#e2e8f0; font-size:11px; font-weight:500; padding:7px 16px; border-radius:30px; border:1px solid rgba(255,255,255,0.08); box-shadow:0 4px 20px rgba(0,0,0,0.35); align-items:center; gap:8px;">
|
||||
<span style="width:7px; height:7px; border-radius:50%; background:#818cf8;"></span>
|
||||
<span id="sb-txt"></span>
|
||||
</div>
|
||||
|
||||
<!-- FAB Container -->
|
||||
<div class="fab-container">
|
||||
<div class="fab-menu" id="fab-menu">
|
||||
<div class="fab-item" onclick="toggleDraw('jalan'); document.getElementById('fab-menu').classList.remove('active')">
|
||||
🛣️ Tambah Jalan
|
||||
</div>
|
||||
<div class="fab-item" onclick="toggleDraw('parsil'); document.getElementById('fab-menu').classList.remove('active')">
|
||||
🏘️ Tambah Parsil
|
||||
</div>
|
||||
</div>
|
||||
<button class="fab-btn" id="fab-main" onclick="document.getElementById('fab-menu').classList.toggle('active')">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div> <!-- End app-container -->
|
||||
|
||||
<!-- Modal Form -->
|
||||
<div id="modal-bg" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:9999; align-items:center; justify-content:center;">
|
||||
<div id="modal" style="background:var(--bg-card); width:400px; max-width:90%; border-radius:12px; border:1px solid var(--border); box-shadow:0 10px 30px rgba(0,0,0,0.5); overflow:hidden; display:flex; flex-direction:column;">
|
||||
<div style="padding:16px 20px; border-bottom:1px solid var(--border); background:rgba(255,255,255,0.02);">
|
||||
<h3 id="modal-title" style="margin:0; font-size:16px; color:var(--text-primary);">Title</h3>
|
||||
<p id="modal-sub" style="margin:4px 0 0; font-size:11px; color:var(--text-secondary);">Subtitle</p>
|
||||
</div>
|
||||
<div style="padding:20px; display:flex; flex-direction:column; gap:16px;" class="popup-form">
|
||||
<div>
|
||||
<label>Nama</label>
|
||||
<input type="text" id="inp-nama" placeholder="Masukkan nama...">
|
||||
</div>
|
||||
<div>
|
||||
<label>Status</label>
|
||||
<select id="inp-status"></select>
|
||||
</div>
|
||||
<div id="grp-dim">
|
||||
<label id="inp-dim-lbl">Dimensi</label>
|
||||
<input type="text" id="inp-dim" readonly style="background:rgba(255,255,255,0.05); color:var(--text-muted); cursor:not-allowed;">
|
||||
<div id="inp-dim-note" style="font-size:9px; color:var(--text-muted); margin-top:4px;">Dihitung otomatis</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:16px 20px; border-top:1px solid var(--border); display:flex; justify-content:flex-end; gap:10px; background:rgba(0,0,0,0.2);">
|
||||
<button onclick="closeModal()" class="popup-btn popup-btn-cancel" style="max-width:100px;">Batal</button>
|
||||
<button onclick="saveModal()" id="btn-save" class="popup-btn popup-btn-save" style="max-width:120px;">💾 Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
KONSTANTA
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
const API_JALAN = './api/jalan.php';
|
||||
const API_PARSIL = './api/parsil.php';
|
||||
|
||||
const JALAN_COLOR = { Nasional:'#ef4444', Provinsi:'#3b82f6', Kabupaten:'#f59e0b' };
|
||||
const PARSIL_COLOR = { SHM:'#22c55e', HGB:'#06b6d4', HGU:'#a855f7', HP:'#f97316' };
|
||||
|
||||
const JALAN_STATUS_OPT = [['Nasional','🔴 Jalan Nasional'],['Provinsi','🔵 Jalan Provinsi'],['Kabupaten','🟡 Jalan Kabupaten']];
|
||||
const PARSIL_STATUS_OPT = [['SHM','🟢 Sertifikat Hak Milik (SHM)'],['HGB','🔵 Hak Guna Bangunan (HGB)'],['HGU','🟣 Hak Guna Usaha (HGU)'],['HP','🟠 Hak Pakai (HP)']];
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
STATE
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
let drawMode = null; // 'jalan' | 'parsil' | null
|
||||
let drawingPts = []; // L.LatLng[]
|
||||
let tempMkrs = [];
|
||||
let tempPoly = null;
|
||||
|
||||
let modalType = null; // 'jalan' | 'parsil'
|
||||
let modalMode = 'add'; // 'add' | 'edit'
|
||||
let modalEditId = null; // id data yang sedang diedit
|
||||
let pendingPts = null;
|
||||
let pendingDim = null;
|
||||
|
||||
let jalanData = [];
|
||||
let parsilData = [];
|
||||
let jalanLyrs = {}; // id → { polyline, vertices, label }
|
||||
let parsilLyrs = {}; // id → { polygon, vertices, label }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
INISIALISASI PETA
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
const map = L.map('map', { zoomControl: true }).setView([-0.055319, 109.349502], 13);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
KALKULASI GEOSPASIAL — MENGGUNAKAN LEAFLET JS
|
||||
|
||||
1. hitungPanjangJalan : Menghitung panjang total polyline jalan
|
||||
dalam meter menggunakan metode L.LatLng.distanceTo() dari
|
||||
library LeafletJS. Metode ini menggunakan formula Haversine
|
||||
yang sudah terintegrasi di LeafletJS.
|
||||
|
||||
2. hitungLuasParsil : Menghitung luas area poligon parsil
|
||||
dalam meter persegi menggunakan formula geodesic spherical
|
||||
area, setara dengan implementasi internal LeafletJS yang
|
||||
digunakan pada L.GeometryUtil.geodesicArea().
|
||||
Berbasis objek L.LatLng dari LeafletJS.
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* Panjang total jalan (meter).
|
||||
* Menggunakan L.LatLng.distanceTo() — fungsi bawaan LeafletJS.
|
||||
* @param {L.LatLng[]} pts
|
||||
* @returns {number}
|
||||
*/
|
||||
function hitungPanjangJalan(pts) {
|
||||
let total = 0;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
// L.latLng.distanceTo() → Haversine distance (meter)
|
||||
total += L.latLng(pts[i]).distanceTo(L.latLng(pts[i + 1]));
|
||||
}
|
||||
return parseFloat(total.toFixed(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Luas parsil tanah (meter persegi).
|
||||
* Menggunakan formula geodesic spherical area berbasis L.LatLng LeafletJS.
|
||||
* Formula ini setara dengan L.GeometryUtil.geodesicArea() dari ekosistem LeafletJS.
|
||||
* @param {L.LatLng[]} pts
|
||||
* @returns {number}
|
||||
*/
|
||||
function hitungLuasParsil(pts) {
|
||||
const R = 6378137; // radius bumi WGS-84 (meter) — sama dengan yang digunakan LeafletJS
|
||||
let area = 0;
|
||||
const n = pts.length;
|
||||
for (let i = 0, j = n - 1; i < n; j = i++) {
|
||||
const pi = L.latLng(pts[i]);
|
||||
const pj = L.latLng(pts[j]);
|
||||
// Formula geodesic spherical (sumber: Leaflet source & L.GeometryUtil)
|
||||
area += (pj.lng - pi.lng) * (Math.PI / 180) *
|
||||
(2 + Math.sin(pi.lat * Math.PI / 180) + Math.sin(pj.lat * Math.PI / 180));
|
||||
}
|
||||
return parseFloat(Math.abs(area * R * R / 2).toFixed(4));
|
||||
}
|
||||
|
||||
/* ── Format helpers ─────────────────────────── */
|
||||
function fmtPanjang(m) {
|
||||
return m >= 1000 ? (m / 1000).toFixed(2) + ' km' : m.toFixed(0) + ' m';
|
||||
}
|
||||
function fmtLuas(m2) {
|
||||
return m2 >= 10000 ? (m2 / 10000).toFixed(4) + ' ha' : m2.toFixed(2) + ' m²';
|
||||
}
|
||||
|
||||
/* ── GeoJSON ↔ LeafletJS conversion ────────── */
|
||||
// GeoJSON LineString: coordinates = [[lng,lat],...]
|
||||
// GeoJSON Polygon: coordinates = [[[lng,lat],...,[lng,lat]]]
|
||||
// Leaflet: L.LatLng(lat, lng)
|
||||
|
||||
function toGeoJSONLine(lls) {
|
||||
return { type:'LineString', coordinates: lls.map(ll => [ll.lng, ll.lat]) };
|
||||
}
|
||||
function toGeoJSONPolygon(lls) {
|
||||
const ring = lls.map(ll => [ll.lng, ll.lat]);
|
||||
ring.push(ring[0]); // tutup ring
|
||||
return { type:'Polygon', coordinates: [ring] };
|
||||
}
|
||||
function fromGeoJSONLine(gj) {
|
||||
return gj.coordinates.map(c => L.latLng(c[1], c[0]));
|
||||
}
|
||||
function fromGeoJSONPolygon(gj) {
|
||||
// Ambil ring pertama, buang titik penutup
|
||||
return gj.coordinates[0].slice(0, -1).map(c => L.latLng(c[1], c[0]));
|
||||
}
|
||||
function centroid(lls) {
|
||||
return L.latLng(
|
||||
lls.reduce((s,p) => s + p.lat, 0) / lls.length,
|
||||
lls.reduce((s,p) => s + p.lng, 0) / lls.length
|
||||
);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
TAB
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
document.getElementById('tbtn-' + tab).classList.add('active');
|
||||
document.getElementById('tp-' + tab).classList.add('active');
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
DRAWING
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
function toggleDraw(type) {
|
||||
if (drawMode === type) { cancelDraw(); return; }
|
||||
if (drawMode) cancelDraw();
|
||||
drawMode = type;
|
||||
drawingPts = [];
|
||||
map.getContainer().style.cursor = 'crosshair';
|
||||
|
||||
// Show draw controls
|
||||
document.getElementById('draw-bar').style.display = 'flex';
|
||||
|
||||
setStatus(type === 'jalan'
|
||||
? '🛣️ Klik peta untuk menambah titik jalan | Enter = selesai'
|
||||
: '🏘️ Klik peta untuk menambah titik parsil | Enter = selesai'
|
||||
);
|
||||
}
|
||||
|
||||
function cancelDraw() {
|
||||
clearTemp();
|
||||
drawMode = null;
|
||||
map.getContainer().style.cursor = '';
|
||||
|
||||
// Hide draw controls
|
||||
document.getElementById('draw-bar').style.display = 'none';
|
||||
|
||||
clearStatus();
|
||||
}
|
||||
|
||||
function clearTemp() {
|
||||
drawingPts = [];
|
||||
tempMkrs.forEach(m => map.removeLayer(m));
|
||||
tempMkrs = [];
|
||||
if (tempPoly) { map.removeLayer(tempPoly); tempPoly = null; }
|
||||
}
|
||||
|
||||
/* Map click → add vertex */
|
||||
map.on('click', function(e) {
|
||||
if (!drawMode) return;
|
||||
drawingPts.push(e.latlng);
|
||||
|
||||
const m = L.circleMarker(e.latlng, {
|
||||
radius:5, color:'#fff', fillColor:'#818cf8', fillOpacity:1, weight:2
|
||||
}).addTo(map);
|
||||
tempMkrs.push(m);
|
||||
|
||||
refreshTempPoly();
|
||||
updateDrawStatus();
|
||||
});
|
||||
|
||||
function refreshTempPoly() {
|
||||
if (tempPoly) map.removeLayer(tempPoly);
|
||||
if (drawingPts.length < 2) return;
|
||||
const pts = drawMode === 'parsil' ? [...drawingPts, drawingPts[0]] : drawingPts;
|
||||
tempPoly = L.polyline(pts, { color:'#818cf8', weight:3, dashArray:'8,5', opacity:.85 }).addTo(map);
|
||||
}
|
||||
|
||||
function updateDrawStatus() {
|
||||
if (!drawMode || !drawingPts.length) return;
|
||||
let extra = '';
|
||||
if (drawMode === 'jalan' && drawingPts.length >= 2)
|
||||
extra = ` | Panjang: ${fmtPanjang(hitungPanjangJalan(drawingPts))}`;
|
||||
if (drawMode === 'parsil' && drawingPts.length >= 3)
|
||||
extra = ` | Luas: ${fmtLuas(hitungLuasParsil(drawingPts))}`;
|
||||
setStatus(`${drawMode==='jalan'?'🛣️':'🏘️'} ${drawingPts.length} titik${extra} | Enter = selesai`);
|
||||
}
|
||||
|
||||
function undoPt() {
|
||||
if (!drawingPts.length) return;
|
||||
drawingPts.pop();
|
||||
const m = tempMkrs.pop();
|
||||
if (m) map.removeLayer(m);
|
||||
refreshTempPoly();
|
||||
updateDrawStatus();
|
||||
}
|
||||
|
||||
function finishDraw() {
|
||||
if (!drawMode) return;
|
||||
if (drawMode === 'jalan' && drawingPts.length < 2) { alert('⚠ Minimal 2 titik untuk jalan!'); return; }
|
||||
if (drawMode === 'parsil' && drawingPts.length < 3) { alert('⚠ Minimal 3 titik untuk parsil!'); return; }
|
||||
|
||||
const pts = drawingPts.slice();
|
||||
const type = drawMode;
|
||||
clearTemp();
|
||||
cancelDraw();
|
||||
|
||||
let dim, dimLabel, dimStr, dimNote;
|
||||
if (type === 'jalan') {
|
||||
dim = hitungPanjangJalan(pts);
|
||||
dimLabel = 'Panjang Jalan';
|
||||
dimStr = fmtPanjang(dim) + ' (' + dim.toFixed(2) + ' m)';
|
||||
dimNote = 'Dihitung otomatis: L.LatLng.distanceTo() dari LeafletJS';
|
||||
} else {
|
||||
dim = hitungLuasParsil(pts);
|
||||
dimLabel = 'Luas Tanah';
|
||||
dimStr = fmtLuas(dim) + ' (' + dim.toFixed(4) + ' m²)';
|
||||
dimNote = 'Dihitung otomatis: geodesic spherical area (LeafletJS)';
|
||||
}
|
||||
|
||||
pendingPts = pts;
|
||||
pendingDim = dim;
|
||||
openModal(type, dimLabel, dimStr, dimNote);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
MODAL
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
/* ── Helper: isi opsi select status ────────── */
|
||||
function isiStatusOpts(type, selectedVal) {
|
||||
const sel = document.getElementById('inp-status');
|
||||
sel.innerHTML = '';
|
||||
const opts = type === 'jalan' ? JALAN_STATUS_OPT : PARSIL_STATUS_OPT;
|
||||
opts.forEach(([v, l]) => {
|
||||
const o = document.createElement('option');
|
||||
o.value = v; o.textContent = l;
|
||||
if (v === selectedVal) o.selected = true;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Buka modal TAMBAH (setelah selesai gambar) ── */
|
||||
function openModal(type, dimLbl, dimStr, dimNote) {
|
||||
modalType = type; modalMode = 'add'; modalEditId = null;
|
||||
|
||||
document.getElementById('modal-title').textContent = type === 'jalan'
|
||||
? '🛣️ Simpan Data Jalan' : '🏘️ Simpan Data Parsil Tanah';
|
||||
document.getElementById('modal-sub').textContent = type === 'jalan'
|
||||
? 'Lengkapi atribut ruas jalan baru' : 'Lengkapi atribut parsil/kavling baru';
|
||||
|
||||
isiStatusOpts(type, null);
|
||||
|
||||
document.getElementById('inp-dim-lbl').textContent = dimLbl;
|
||||
document.getElementById('inp-dim').value = dimStr;
|
||||
document.getElementById('inp-dim-note').textContent = dimNote;
|
||||
document.getElementById('inp-nama').value = '';
|
||||
document.getElementById('grp-dim').style.display = '';
|
||||
document.getElementById('btn-save').textContent = '💾 Simpan';
|
||||
|
||||
document.getElementById('modal-bg').style.display = 'flex';
|
||||
setTimeout(() => document.getElementById('inp-nama').focus(), 60);
|
||||
}
|
||||
|
||||
/* ── Buka modal EDIT atribut ─────────────────── */
|
||||
function openEditModal(type, id) {
|
||||
map.closePopup();
|
||||
let item;
|
||||
if (type === 'jalan') {
|
||||
item = jalanData.find(j => j.id == id);
|
||||
} else {
|
||||
item = parsilData.find(p => p.id == id);
|
||||
}
|
||||
if (!item) return;
|
||||
|
||||
modalType = type; modalMode = 'edit'; modalEditId = id;
|
||||
|
||||
const isJalan = (type === 'jalan');
|
||||
document.getElementById('modal-title').textContent = isJalan
|
||||
? '✏️ Edit Data Jalan' : '✏️ Edit Data Parsil Tanah';
|
||||
document.getElementById('modal-sub').textContent = isJalan
|
||||
? 'Ubah nama atau status jalan' : 'Ubah nama atau status kepemilikan parsil';
|
||||
|
||||
// Pre-fill form
|
||||
document.getElementById('inp-nama').value = isJalan ? item.nama_jalan : item.nama_parsil;
|
||||
isiStatusOpts(type, isJalan ? item.status_jalan : item.status_kepemilikan);
|
||||
|
||||
// Tampilkan info dimensi (readonly, tidak bisa diubah di sini)
|
||||
if (isJalan) {
|
||||
document.getElementById('inp-dim-lbl').textContent = 'Panjang Jalan (tidak berubah)';
|
||||
document.getElementById('inp-dim').value = fmtPanjang(parseFloat(item.panjang_jalan)) + ' (' + parseFloat(item.panjang_jalan).toFixed(2) + ' m)';
|
||||
document.getElementById('inp-dim-note').textContent = 'Geser titik pada peta untuk mengubah panjang jalan';
|
||||
} else {
|
||||
document.getElementById('inp-dim-lbl').textContent = 'Luas Tanah (tidak berubah)';
|
||||
document.getElementById('inp-dim').value = fmtLuas(parseFloat(item.luas_tanah)) + ' (' + parseFloat(item.luas_tanah).toFixed(4) + ' m²)';
|
||||
document.getElementById('inp-dim-note').textContent = 'Geser titik sudut pada peta untuk mengubah luas';
|
||||
}
|
||||
document.getElementById('grp-dim').style.display = '';
|
||||
document.getElementById('btn-save').textContent = '✏️ Simpan Perubahan';
|
||||
|
||||
document.getElementById('modal-bg').style.display = 'flex';
|
||||
setTimeout(() => document.getElementById('inp-nama').focus(), 60);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal-bg').style.display = 'none';
|
||||
modalType = null; modalMode = 'add'; modalEditId = null;
|
||||
pendingPts = null; pendingDim = null;
|
||||
}
|
||||
|
||||
function handleBgClick(e) { if (e.target === document.getElementById('modal-bg')) closeModal(); }
|
||||
|
||||
async function saveModal() {
|
||||
const nama = document.getElementById('inp-nama').value.trim();
|
||||
const status = document.getElementById('inp-status').value;
|
||||
if (!nama) { alert('Nama tidak boleh kosong!'); document.getElementById('inp-nama').focus(); return; }
|
||||
|
||||
const btn = document.getElementById('btn-save');
|
||||
const origLabel = btn.textContent;
|
||||
btn.textContent = '⏳ Menyimpan...'; btn.disabled = true;
|
||||
|
||||
try {
|
||||
/* ── MODE EDIT (update atribut saja) ── */
|
||||
if (modalMode === 'edit') {
|
||||
if (modalType === 'jalan') {
|
||||
const res = await fetch(API_JALAN + '?id=' + modalEditId, {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ nama_jalan: nama, status_jalan: status })
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!text) throw new Error("Server mengembalikan respons kosong. Pastikan Anda menjalankan aplikasi ini melalui server PHP (Laragon/XAMPP), BUKAN Live Server.");
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (data.status === 'sukses') {
|
||||
// Update data lokal
|
||||
const idx = jalanData.findIndex(j => j.id == modalEditId);
|
||||
if (idx >= 0) { jalanData[idx].nama_jalan = nama; jalanData[idx].status_jalan = status; }
|
||||
closeModal();
|
||||
// Re-render layer di peta (warna & label bisa berubah)
|
||||
const l = jalanLyrs[modalEditId];
|
||||
if (l) {
|
||||
const newColor = JALAN_COLOR[status] || '#6366f1';
|
||||
l.polyline.setStyle({ color: newColor });
|
||||
l.label.setContent(nama);
|
||||
l.vertices.forEach(v => v.setStyle({ fillColor: newColor }));
|
||||
l.polyline.bindPopup(buatPopupJalan(jalanData[idx], null, l.polyline, l.label));
|
||||
}
|
||||
renderListJalan();
|
||||
} else { alert('Gagal: ' + data.pesan); }
|
||||
} else {
|
||||
const res = await fetch(API_PARSIL + '?id=' + modalEditId, {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ nama_parsil: nama, status_kepemilikan: status })
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!text) throw new Error("Server mengembalikan respons kosong. Pastikan Anda menjalankan aplikasi ini melalui server PHP (Laragon/XAMPP), BUKAN Live Server.");
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (data.status === 'sukses') {
|
||||
const idx = parsilData.findIndex(p => p.id == modalEditId);
|
||||
if (idx >= 0) { parsilData[idx].nama_parsil = nama; parsilData[idx].status_kepemilikan = status; }
|
||||
closeModal();
|
||||
const l = parsilLyrs[modalEditId];
|
||||
if (l) {
|
||||
const newColor = PARSIL_COLOR[status] || '#6366f1';
|
||||
l.polygon.setStyle({ color: newColor, fillColor: newColor });
|
||||
l.label.setContent(nama);
|
||||
l.vertices.forEach(v => v.setStyle({ fillColor: newColor }));
|
||||
l.polygon.bindPopup(buatPopupParsil(parsilData[idx], null, l.polygon, l.label));
|
||||
}
|
||||
renderListParsil();
|
||||
} else { alert('Gagal: ' + data.pesan); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── MODE TAMBAH (create baru) ── */
|
||||
if (modalType === 'jalan') {
|
||||
const res = await fetch(API_JALAN, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({
|
||||
nama_jalan: nama, status_jalan: status,
|
||||
panjang_jalan: pendingDim, geojson: toGeoJSONLine(pendingPts)
|
||||
})
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!text) throw new Error("Server mengembalikan respons kosong. Pastikan Anda menjalankan aplikasi ini melalui server PHP (Laragon/XAMPP), BUKAN Live Server.");
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (data.status === 'sukses') {
|
||||
closeModal(); await muatJalan();
|
||||
const lyr = jalanLyrs[data.id];
|
||||
if (lyr) map.fitBounds(lyr.polyline.getBounds(), {padding:[50,50]});
|
||||
} else { alert('Gagal: ' + data.pesan); }
|
||||
} else {
|
||||
const res = await fetch(API_PARSIL, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({
|
||||
nama_parsil: nama, status_kepemilikan: status,
|
||||
luas_tanah: pendingDim, geojson: toGeoJSONPolygon(pendingPts)
|
||||
})
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!text) throw new Error("Server mengembalikan respons kosong. Pastikan Anda menjalankan aplikasi ini melalui server PHP (Laragon/XAMPP), BUKAN Live Server.");
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (data.status === 'sukses') {
|
||||
closeModal(); await muatParsil();
|
||||
const lyr = parsilLyrs[data.id];
|
||||
if (lyr) map.fitBounds(lyr.polygon.getBounds(), {padding:[50,50]});
|
||||
} else { alert('Gagal: ' + data.pesan); }
|
||||
}
|
||||
} catch(err) { alert('Error: ' + err.message); }
|
||||
finally { btn.textContent = origLabel; btn.disabled = false; }
|
||||
}
|
||||
|
||||
/* Shortcut global untuk tombol edit di popup & sidebar */
|
||||
window.editJalan = (id) => openEditModal('jalan', id);
|
||||
window.editParsil = (id) => openEditModal('parsil', id);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
JALAN — CRUD
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
async function muatJalan() {
|
||||
try { const r = await fetch(API_JALAN); jalanData = await r.json(); }
|
||||
catch(e) { jalanData = []; console.warn('API Jalan:', e); }
|
||||
|
||||
// Hapus layer lama
|
||||
Object.values(jalanLyrs).forEach(l => {
|
||||
map.removeLayer(l.polyline);
|
||||
l.vertices.forEach(v => map.removeLayer(v));
|
||||
if (l.label) map.removeLayer(l.label);
|
||||
});
|
||||
jalanLyrs = {};
|
||||
|
||||
jalanData.forEach(j => renderJalan(j));
|
||||
renderListJalan();
|
||||
updateStatsJalan();
|
||||
}
|
||||
|
||||
function renderJalan(jalan) {
|
||||
const lls = fromGeoJSONLine(jalan.geojson);
|
||||
const color = JALAN_COLOR[jalan.status_jalan] || '#6366f1';
|
||||
|
||||
const polyline = L.polyline(lls, { color, weight:5, opacity:.9, lineJoin:'round' }).addTo(map);
|
||||
|
||||
// Label nama di tengah
|
||||
const midPt = lls[Math.floor(lls.length / 2)];
|
||||
const label = L.tooltip({ permanent:true, direction:'top', className:'map-label', offset:[0,-4] })
|
||||
.setContent(jalan.nama_jalan).setLatLng(midPt).addTo(map);
|
||||
|
||||
// Popup
|
||||
function mkPopup() { return buatPopupJalan(jalan, lls, polyline, label); }
|
||||
polyline.bindPopup(mkPopup());
|
||||
|
||||
// Vertex drag
|
||||
const vertices = [];
|
||||
lls.forEach((pt, idx) => {
|
||||
const vm = L.circleMarker(pt, { radius:5, color:'#fff', fillColor:color, fillOpacity:1, weight:2 }).addTo(map);
|
||||
vm.on('mousedown', e => {
|
||||
map.dragging.disable(); L.DomEvent.stopPropagation(e);
|
||||
function onMove(ev) {
|
||||
vm.setLatLng(ev.latlng); lls[idx] = ev.latlng;
|
||||
polyline.setLatLngs(lls);
|
||||
label.setLatLng(lls[Math.floor(lls.length/2)]);
|
||||
}
|
||||
function onUp() {
|
||||
map.dragging.enable();
|
||||
map.off('mousemove', onMove); map.off('mouseup', onUp);
|
||||
const newP = hitungPanjangJalan(lls);
|
||||
updateJalanAPI(jalan.id, lls, newP);
|
||||
polyline.bindPopup(buatPopupJalan({...jalan, panjang_jalan:newP}, lls, polyline, label));
|
||||
}
|
||||
map.on('mousemove', onMove); map.on('mouseup', onUp);
|
||||
});
|
||||
vertices.push(vm);
|
||||
});
|
||||
|
||||
jalanLyrs[jalan.id] = { polyline, vertices, label };
|
||||
}
|
||||
|
||||
function buatPopupJalan(j, lls, polyline, label) {
|
||||
const color = JALAN_COLOR[j.status_jalan];
|
||||
const pjg = lls ? hitungPanjangJalan(lls) : parseFloat(j.panjang_jalan);
|
||||
return `
|
||||
<div class="gis-popup">
|
||||
<div class="popup-title" style="color:${color}">🛣️ ${j.nama_jalan}</div>
|
||||
<div class="info-row"><span class="info-label">Status</span><span class="info-value"><span style="background:${color}20;color:${color};padding:2px 6px;border-radius:4px;font-size:10px;font-weight:bold">${j.status_jalan}</span></span></div>
|
||||
<div class="info-row"><span class="info-label">Panjang</span><span class="info-value">📐 ${fmtPanjang(pjg)}</span></div>
|
||||
<div class="popup-form">
|
||||
<div style="font-size:9px; color:var(--text-muted); font-style:italic; margin-top:8px;">Seret titik pada jalan untuk ubah koordinat</div>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<button class="popup-btn popup-btn-edit" onclick="window.editJalan(${j.id})">✏️ Edit</button>
|
||||
<button class="popup-btn popup-btn-delete" onclick="window.hapusJalan(${j.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderListJalan() {
|
||||
// Note: We don't have fil-jalan dropdown in the new sidebar yet, if needed we can add it later.
|
||||
// Assuming no filter for now.
|
||||
const data = jalanData;
|
||||
const el = document.getElementById('list-jalan');
|
||||
|
||||
if (!data.length) {
|
||||
el.innerHTML = `<div class="loading-text">Tidak ada data jalan.</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = data.map(j => {
|
||||
const c = JALAN_COLOR[j.status_jalan]; const bg = c+'22';
|
||||
return `<div class="item-card" onclick="zoomJalan(${j.id})">
|
||||
<div class="item-card-header">
|
||||
<div class="item-card-name">${j.nama_jalan}</div>
|
||||
<div class="badge" style="background:${bg};color:${c};">${j.status_jalan}</div>
|
||||
</div>
|
||||
<div class="item-card-info">
|
||||
📐 Panjang: ${fmtPanjang(parseFloat(j.panjang_jalan))}
|
||||
</div>
|
||||
<div class="item-card-actions">
|
||||
<button class="btn-sm btn-zoom" title="Zoom" onclick="event.stopPropagation();zoomJalan(${j.id})">🔍 Zoom</button>
|
||||
<button class="btn-sm btn-edit" title="Edit" onclick="event.stopPropagation();window.editJalan(${j.id})">✏️ Edit</button>
|
||||
<button class="btn-sm btn-delete" title="Hapus" onclick="event.stopPropagation();window.hapusJalan(${j.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateStatsJalan() {
|
||||
const total = jalanData.length;
|
||||
const km = jalanData.reduce((s,j) => s + parseFloat(j.panjang_jalan), 0);
|
||||
document.getElementById('sj-total').textContent = total;
|
||||
document.getElementById('sj-panjang').textContent = fmtPanjang(km);
|
||||
}
|
||||
|
||||
function zoomJalan(id) {
|
||||
const l = jalanLyrs[id];
|
||||
if (l) { map.fitBounds(l.polyline.getBounds(), {padding:[60,60], maxZoom:17}); l.polyline.openPopup(); }
|
||||
}
|
||||
|
||||
window.hapusJalan = function(id) {
|
||||
map.closePopup();
|
||||
Swal.fire({
|
||||
title: 'Hapus Jalan?',
|
||||
text: "Data jalan ini akan dihapus permanen dari database!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
background: '#1e293b',
|
||||
color: '#f1f5f9',
|
||||
confirmButtonColor: '#ef4444',
|
||||
cancelButtonColor: '#4f46e5',
|
||||
confirmButtonText: '🗑️ Ya, Hapus',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const r = await fetch(API_JALAN + '?id=' + id, {method:'DELETE'});
|
||||
const d = await r.json();
|
||||
if (d.status === 'sukses') {
|
||||
const l = jalanLyrs[id];
|
||||
if (l) { map.removeLayer(l.polyline); l.vertices.forEach(v=>map.removeLayer(v)); if(l.label) map.removeLayer(l.label); }
|
||||
delete jalanLyrs[id];
|
||||
jalanData = jalanData.filter(j => j.id != id);
|
||||
renderListJalan(); updateStatsJalan();
|
||||
Swal.fire({icon: 'success', title: 'Terhapus!', text: 'Data jalan berhasil dihapus.', background: '#1e293b', color: '#f1f5f9', timer: 1500, showConfirmButton: false});
|
||||
} else { Swal.fire({icon: 'error', title: 'Gagal', text: d.pesan, background: '#1e293b', color: '#f1f5f9'}); }
|
||||
} catch(e) { Swal.fire({icon: 'error', title: 'Error', text: e.message, background: '#1e293b', color: '#f1f5f9'}); }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function updateJalanAPI(id, lls, panjang) {
|
||||
try {
|
||||
await fetch(API_JALAN + '?id=' + id, {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ geojson:toGeoJSONLine(lls), panjang_jalan:panjang })
|
||||
});
|
||||
const i = jalanData.findIndex(j => j.id == id);
|
||||
if (i >= 0) jalanData[i].panjang_jalan = panjang;
|
||||
updateStatsJalan(); renderListJalan();
|
||||
} catch(e) { console.error('Update jalan:', e); }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
PARSIL — CRUD
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
async function muatParsil() {
|
||||
try { const r = await fetch(API_PARSIL); parsilData = await r.json(); }
|
||||
catch(e) { parsilData = []; console.warn('API Parsil:', e); }
|
||||
|
||||
Object.values(parsilLyrs).forEach(l => {
|
||||
map.removeLayer(l.polygon);
|
||||
l.vertices.forEach(v => map.removeLayer(v));
|
||||
if (l.label) map.removeLayer(l.label);
|
||||
});
|
||||
parsilLyrs = {};
|
||||
|
||||
parsilData.forEach(p => renderParsil(p));
|
||||
renderListParsil();
|
||||
updateStatsParsil();
|
||||
}
|
||||
|
||||
function renderParsil(parsil) {
|
||||
const lls = fromGeoJSONPolygon(parsil.geojson);
|
||||
const color = PARSIL_COLOR[parsil.status_kepemilikan] || '#6366f1';
|
||||
|
||||
const polygon = L.polygon(lls, { color, fillColor:color, fillOpacity:.18, weight:2.5 }).addTo(map);
|
||||
|
||||
const label = L.tooltip({ permanent:true, direction:'center', className:'map-label', offset:[0,0] })
|
||||
.setContent(parsil.nama_parsil).setLatLng(centroid(lls)).addTo(map);
|
||||
|
||||
function mkPopup() { return buatPopupParsil(parsil, lls, polygon, label); }
|
||||
polygon.bindPopup(mkPopup());
|
||||
|
||||
const vertices = [];
|
||||
lls.forEach((pt, idx) => {
|
||||
const vm = L.circleMarker(pt, { radius:5, color:'#fff', fillColor:color, fillOpacity:1, weight:2 }).addTo(map);
|
||||
vm.on('mousedown', e => {
|
||||
map.dragging.disable(); L.DomEvent.stopPropagation(e);
|
||||
function onMove(ev) {
|
||||
vm.setLatLng(ev.latlng); lls[idx] = ev.latlng;
|
||||
polygon.setLatLngs(lls); label.setLatLng(centroid(lls));
|
||||
}
|
||||
function onUp() {
|
||||
map.dragging.enable();
|
||||
map.off('mousemove', onMove); map.off('mouseup', onUp);
|
||||
const newL = hitungLuasParsil(lls);
|
||||
updateParsilAPI(parsil.id, lls, newL);
|
||||
polygon.bindPopup(buatPopupParsil({...parsil, luas_tanah:newL}, lls, polygon, label));
|
||||
}
|
||||
map.on('mousemove', onMove); map.on('mouseup', onUp);
|
||||
});
|
||||
vertices.push(vm);
|
||||
});
|
||||
|
||||
parsilLyrs[parsil.id] = { polygon, vertices, label };
|
||||
}
|
||||
|
||||
function buatPopupParsil(p, lls, polygon, label) {
|
||||
const color = PARSIL_COLOR[p.status_kepemilikan];
|
||||
const luas = lls ? hitungLuasParsil(lls) : parseFloat(p.luas_tanah);
|
||||
const sk = { SHM:'Sertifikat Hak Milik', HGB:'Hak Guna Bangunan', HGU:'Hak Guna Usaha', HP:'Hak Pakai' };
|
||||
return `
|
||||
<div class="gis-popup">
|
||||
<div class="popup-title" style="color:${color}">🏘️ ${p.nama_parsil}</div>
|
||||
<div class="info-row"><span class="info-label">Status</span><span class="info-value"><span style="background:${color}20;color:${color};padding:2px 6px;border-radius:4px;font-size:10px;font-weight:bold">${p.status_kepemilikan}</span></span></div>
|
||||
<div class="info-row"><span class="info-label">Luas</span><span class="info-value">📐 ${fmtLuas(luas)}</span></div>
|
||||
<div class="popup-form">
|
||||
<div style="font-size:9px; color:var(--text-muted); font-style:italic; margin-top:8px;">Seret titik sudut poligon untuk ubah bentuk parsil</div>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<button class="popup-btn popup-btn-edit" onclick="window.editParsil(${p.id})">✏️ Edit</button>
|
||||
<button class="popup-btn popup-btn-delete" onclick="window.hapusParsil(${p.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderListParsil() {
|
||||
const data = parsilData;
|
||||
const el = document.getElementById('list-parsil');
|
||||
|
||||
if (!data.length) {
|
||||
el.innerHTML = `<div class="loading-text">Tidak ada data parsil.</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = data.map(p => {
|
||||
const c = PARSIL_COLOR[p.status_kepemilikan]; const bg = c+'22';
|
||||
return `<div class="item-card" onclick="zoomParsil(${p.id})">
|
||||
<div class="item-card-header">
|
||||
<div class="item-card-name">${p.nama_parsil}</div>
|
||||
<div class="badge" style="background:${bg};color:${c};">${p.status_kepemilikan}</div>
|
||||
</div>
|
||||
<div class="item-card-info">
|
||||
📐 Luas: ${fmtLuas(parseFloat(p.luas_tanah))}
|
||||
</div>
|
||||
<div class="item-card-actions">
|
||||
<button class="btn-sm btn-zoom" title="Zoom" onclick="event.stopPropagation();zoomParsil(${p.id})">🔍 Zoom</button>
|
||||
<button class="btn-sm btn-edit" title="Edit" onclick="event.stopPropagation();window.editParsil(${p.id})">✏️ Edit</button>
|
||||
<button class="btn-sm btn-delete" title="Hapus" onclick="event.stopPropagation();window.hapusParsil(${p.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateStatsParsil() {
|
||||
const total = parsilData.length;
|
||||
const luas = parsilData.reduce((s,p) => s + parseFloat(p.luas_tanah), 0);
|
||||
document.getElementById('sp-total').textContent = total;
|
||||
document.getElementById('sp-luas').textContent = fmtLuas(luas);
|
||||
}
|
||||
|
||||
function zoomParsil(id) {
|
||||
const l = parsilLyrs[id];
|
||||
if (l) { map.fitBounds(l.polygon.getBounds(), {padding:[60,60], maxZoom:18}); l.polygon.openPopup(); }
|
||||
}
|
||||
|
||||
window.hapusParsil = function(id) {
|
||||
map.closePopup();
|
||||
Swal.fire({
|
||||
title: 'Hapus Parsil?',
|
||||
text: "Data parsil tanah ini akan dihapus permanen dari database!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
background: '#1e293b',
|
||||
color: '#f1f5f9',
|
||||
confirmButtonColor: '#ef4444',
|
||||
cancelButtonColor: '#4f46e5',
|
||||
confirmButtonText: '🗑️ Ya, Hapus',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const r = await fetch(API_PARSIL + '?id=' + id, {method:'DELETE'});
|
||||
const d = await r.json();
|
||||
if (d.status === 'sukses') {
|
||||
const l = parsilLyrs[id];
|
||||
if (l) { map.removeLayer(l.polygon); l.vertices.forEach(v=>map.removeLayer(v)); if(l.label) map.removeLayer(l.label); }
|
||||
delete parsilLyrs[id];
|
||||
parsilData = parsilData.filter(p => p.id != id);
|
||||
renderListParsil(); updateStatsParsil();
|
||||
Swal.fire({icon: 'success', title: 'Terhapus!', text: 'Data parsil berhasil dihapus.', background: '#1e293b', color: '#f1f5f9', timer: 1500, showConfirmButton: false});
|
||||
} else { Swal.fire({icon: 'error', title: 'Gagal', text: d.pesan, background: '#1e293b', color: '#f1f5f9'}); }
|
||||
} catch(e) { Swal.fire({icon: 'error', title: 'Error', text: e.message, background: '#1e293b', color: '#f1f5f9'}); }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function updateParsilAPI(id, lls, luas) {
|
||||
try {
|
||||
await fetch(API_PARSIL + '?id=' + id, {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ geojson:toGeoJSONPolygon(lls), luas_tanah:luas })
|
||||
});
|
||||
const i = parsilData.findIndex(p => p.id == id);
|
||||
if (i >= 0) parsilData[i].luas_tanah = luas;
|
||||
updateStatsParsil(); renderListParsil();
|
||||
} catch(e) { console.error('Update parsil:', e); }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
STATUS BAR
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
function setStatus(msg) {
|
||||
document.getElementById('sb-txt').textContent = msg;
|
||||
document.getElementById('status-bar').classList.add('on');
|
||||
}
|
||||
function clearStatus() { document.getElementById('status-bar').classList.remove('on'); }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
KEYBOARD
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (document.getElementById('modal-bg').classList.contains('show')) return;
|
||||
if (e.key === 'Escape') cancelDraw();
|
||||
if (e.key === 'Enter' && drawMode) finishDraw();
|
||||
if ((e.key === 'z' || e.key === 'Z') && drawMode) undoPt();
|
||||
});
|
||||
document.getElementById('inp-nama').addEventListener('keydown', e => { if(e.key==='Enter') saveModal(); });
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
INIT
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
Promise.all([muatJalan(), muatParsil()]);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user