Files
2026-06-03 05:16:02 +00:00

1062 lines
54 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php include 'koneksi.php'; ?>
<!DOCTYPE html>
<html lang="id">
<head>
<script src="https://cdn.jsdelivr.net/npm/leaflet-path-transform@1.1.3/dist/L.Path.Transform.js"></script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>WebGIS — Manajemen Jalan & Parsil Tanah</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Space+Mono:wght@700&display=swap" rel="stylesheet"/>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0d1117;--surface:#161b22;--border:#30363d;
--text:#e6edf3;--muted:#8b949e;
--nasional:#ef4444;--provinsi:#f97316;--kabupaten:#eab308;
--shm:#22c55e;--hgb:#3b82f6;--hgu:#a855f7;--hp:#ec4899;
--point:#58a6ff;
--radius:10px;--shadow:0 8px 32px rgba(251, 245, 245, 0.5);
}
body{font-family:'Plus Jakarta Sans',sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden;}
#app{display:flex;height:100vh;}
/* ── Sidebar ── */
#sidebar{width:290px;min-width:290px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;z-index:1000;}
.sb-head{padding:16px 16px 12px;background:linear-gradient(135deg,#1a2332,#161b22);border-bottom:1px solid var(--border);}
.sb-head h1{font-family:'Space Mono',monospace;font-size:14px;color:#58a6ff;letter-spacing:-0.3px;}
.sb-head p{font-size:10px;color:var(--muted);margin-top:3px;}
/* Layer tabs */
.layer-tabs{display:flex;border-bottom:1px solid var(--border);background:#0d1117;}
.ltab{flex:1;padding:9px 4px;font-size:10px;font-weight:700;text-align:center;cursor:pointer;border:none;background:transparent;color:var(--muted);border-bottom:2px solid transparent;transition:all .15s;}
.ltab.active{color:var(--text);border-bottom-color:#58a6ff;}
.ltab:hover:not(.active){color:var(--text);}
/* Legend */
.legend-box{padding:8px 12px;border-bottom:1px solid var(--border);background:#0d1117;}
.legend-title{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--muted);margin-bottom:5px;}
.legend-items{display:flex;flex-wrap:wrap;gap:4px;}
.leg{display:flex;align-items:center;gap:4px;font-size:10px;font-weight:600;}
.leg-dot{width:10px;height:10px;border-radius:2px;flex-shrink:0;}
/* Stats */
.stats-bar{display:flex;padding:8px 10px;gap:5px;border-bottom:1px solid var(--border);}
.stat-chip{flex:1;text-align:center;background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:5px 3px;}
.stat-chip .val{font-family:'Space Mono',monospace;font-size:17px;font-weight:700;line-height:1;}
.stat-chip .lbl{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;margin-top:1px;}
/* List */
.list-hdr{padding:8px 12px 4px;font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--muted);}
#data-list{flex:1;overflow-y:auto;padding:0 6px 6px;}
#data-list::-webkit-scrollbar{width:3px;}
#data-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px;}
.data-item{display:flex;align-items:center;gap:8px;padding:8px 9px;border-radius:7px;border:1px solid transparent;cursor:pointer;transition:all .15s;margin-bottom:2px;}
.data-item:hover{background:rgba(88,166,255,.08);border-color:rgba(88,166,255,.2);}
.data-item.active{background:rgba(88,166,255,.12);border-color:rgba(88,166,255,.35);}
.di-stripe{width:4px;height:32px;border-radius:2px;flex-shrink:0;}
.di-info{flex:1;min-width:0;}
.di-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.di-meta{font-size:10px;color:var(--muted);margin-top:1px;}
.di-badge{font-size:9px;padding:2px 6px;border-radius:4px;font-weight:700;flex-shrink:0;}
/* Map */
#map{flex:1;height:100%;}
/* Popup */
.leaflet-popup-content-wrapper{background:var(--surface)!important;border:1px solid var(--border)!important;border-radius:var(--radius)!important;box-shadow:var(--shadow)!important;padding:0!important;color:var(--text)!important;}
.leaflet-popup-tip{background:var(--surface)!important;}
.leaflet-popup-content{margin:0!important;width:auto!important;}
.leaflet-popup-close-button{color:var(--muted)!important;font-size:18px!important;top:8px!important;right:8px!important;}
.leaflet-popup-close-button:hover{color:var(--text)!important;}
/* Pop panel */
.pop-panel{width:250px;}
.pop-head{padding:12px 14px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;}
.ph-stripe{width:5px;height:36px;border-radius:3px;flex-shrink:0;}
.ph-title{font-size:13px;font-weight:700;}
.ph-sub{font-size:10px;color:var(--muted);margin-top:1px;}
.pop-body{padding:12px 14px;}
/* Menu buttons */
.menu-choices{display:flex;flex-direction:column;gap:5px;}
.menu-btn{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px solid var(--border);border-radius:7px;background:transparent;color:var(--text);cursor:pointer;font-family:'Plus Jakarta Sans',sans-serif;font-size:12px;font-weight:600;transition:all .15s;width:100%;text-align:left;}
.menu-btn:hover{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.2);}
.mb-desc{font-size:10px;color:var(--muted);margin-top:1px;}
/* Form */
.form-group{margin-bottom:9px;}
.form-group label{display:block;font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--muted);margin-bottom:3px;}
.form-group input,.form-group select{width:100%;padding:6px 9px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-family:'Plus Jakarta Sans',sans-serif;font-size:12px;outline:none;transition:border-color .15s;}
.form-group input:focus,.form-group select:focus{border-color:#58a6ff;}
.form-group input::placeholder{color:var(--muted);}
.form-group select option{background:var(--surface);}
.calc-field{background:rgba(88,166,255,.08)!important;border-color:rgba(88,166,255,.3)!important;color:#58a6ff!important;font-weight:700;}
/* Buttons */
.btn-row{display:flex;gap:5px;margin-top:10px;}
.btn{flex:1;padding:7px;border:none;border-radius:6px;font-family:'Plus Jakarta Sans',sans-serif;font-size:11px;font-weight:700;cursor:pointer;transition:opacity .15s;}
.btn:hover{opacity:.88;}
.btn-primary{background:#58a6ff;color:#0d1117;}
.btn-success{background:#3fb950;color:#0d1117;}
.btn-danger{background:#f85149;color:#fff;}
.btn-muted{background:var(--border);color:var(--text);}
/* Info rows */
.info-row{display:flex;align-items:flex-start;gap:8px;padding:5px 0;border-bottom:1px solid rgba(48,54,61,.7);font-size:12px;}
.info-row:last-child{border-bottom:none;}
.ir-label{color:var(--muted);font-size:10px;width:60px;flex-shrink:0;padding-top:1px;}
.ir-val{font-weight:500;}
/* Toast */
#toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(16px);background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:9px 16px;font-size:12px;font-weight:600;box-shadow:var(--shadow);z-index:9999;opacity:0;transition:all .2s ease;pointer-events:none;}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
#toast.ok{border-color:#3fb950;color:#3fb950;}
#toast.err{border-color:#f85149;color:#f85149;}
#toast.info{border-color:#58a6ff;color:#58a6ff;}
/* Draw hint */
#draw-hint{position:fixed;top:12px;left:50%;transform:translateX(-50%);background:var(--surface);border:1px solid #d29922;border-radius:8px;padding:8px 16px;font-size:11px;color:#d29922;font-weight:600;z-index:9000;display:none;box-shadow:var(--shadow);}
#draw-hint button{background:#f85149;color:#fff;border:none;border-radius:5px;padding:3px 8px;font-size:10px;cursor:pointer;margin-left:10px;}
.empty-state{text-align:center;padding:28px 16px;color:var(--muted);font-size:11px;}
.empty-state .ei{font-size:26px;margin-bottom:5px;}
.leaflet-editing-icon {
width: 8px !important; height: 8px !important; margin-left: -4px !important; margin-top: -4px !important;
border-radius: 50% !important; background: #ffffff !important; border: 1px solid #000 !important; box-shadow: 0 0 5px rgba(0,0,0,0.5);
}
/* Label nama kecamatan permanent */
.geojson-label {
background: transparent !important;
border: none !important;
box-shadow: none !important;
color: #ffffff !important;
font-family: 'Plus Jakarta Sans', sans-serif !important;
font-size: 10px !important;
font-weight: 700 !important;
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000, 0 0 6px rgba(0,0,0,0.9) !important;
text-transform: uppercase;
pointer-events: none !important;
white-space: nowrap;
}
.geojson-label::before { display: none !important; }
/* ── Layer Control Checkbox Panel ── */
.layer-control {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
background: #0d1117;
}
.lc-title {
font-size: 9px; font-weight: 700; letter-spacing: 1px;
text-transform: uppercase; color: var(--muted); margin-bottom: 6px;
}
.lc-item {
display: flex; align-items: center; gap: 8px;
padding: 6px 0; cursor: pointer;
border-bottom: 1px solid rgba(48,54,61,.5);
}
.lc-item:last-child { border-bottom: none; }
.lc-check {
width: 16px; height: 16px; border-radius: 4px;
border: 2px solid var(--border);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; transition: all .15s; cursor: pointer;
}
.lc-check.checked { border-color: transparent; }
.lc-check.checked::after { content:'✓'; font-size:11px; font-weight:700; color:#0d1117; }
.lc-dot { width:10px; height:10px; border-radius:2px; flex-shrink:0; }
.lc-label { font-size:12px; font-weight:600; color:var(--text); flex:1; }
.lc-desc { font-size:9px; color:var(--muted); }
</style>
</head>
<body>
<div id="app">
<aside id="sidebar">
<div class="sb-head">
<center><h1> WEBGIS</h1></center>
<center><p>Manajemen Jalan & Parsil Tanah — Pontianak</p></center>
</div>
<!-- ── LAYER CONTROL CHECKBOX ── -->
<div class="layer-control">
<div class="lc-title">🗂️ Tampilkan Layer</div>
<!-- Batas Kecamatan -->
<div class="lc-item" onclick="toggleLayer('kecamatan')">
<div class="lc-check checked" id="chk-kecamatan" style="background:#58a6ff"></div>
<div class="lc-dot" style="background:#58a6ff"></div>
<div>
<div class="lc-label">Batas Kecamatan</div>
<div class="lc-desc">Populasi & info wilayah</div>
</div>
</div>
<!-- Manajemen Objek -->
<div class="lc-item" onclick="toggleLayer('objek')">
<div class="lc-check checked" id="chk-objek" style="background:#3fb950"></div>
<div class="lc-dot" style="background:#3fb950"></div>
<div>
<div class="lc-label">Manajemen Objek</div>
<div class="lc-desc">Jalan, parsil tanah & titik</div>
</div>
</div>
</div>
<div class="layer-tabs">
<button class="ltab active" data-layer="jalan" onclick="switchLayer('jalan')">Jalan</button>
<button class="ltab" data-layer="parsil" onclick="switchLayer('parsil')">Tanah</button>
<button class="ltab" data-layer="point" onclick="switchLayer('point')">Titik</button>
</div>
<div class="legend-box" id="legend-jalan">
<div class="legend-title">Legenda Jalan</div>
<div class="legend-items">
<div class="leg"><div class="leg-dot" style="background:var(--nasional)"></div>Nasional</div>
<div class="leg"><div class="leg-dot" style="background:var(--provinsi)"></div>Provinsi</div>
<div class="leg"><div class="leg-dot" style="background:var(--kabupaten)"></div>Kabupaten</div>
</div>
</div>
<div class="legend-box" id="legend-parsil" style="display:none">
<div class="legend-title">Legenda Sertifikat</div>
<div class="legend-items">
<div class="leg"><div class="leg-dot" style="background:var(--shm)"></div>SHM</div>
<div class="leg"><div class="leg-dot" style="background:var(--hgb)"></div>HGB</div>
<div class="leg"><div class="leg-dot" style="background:var(--hgu)"></div>HGU</div>
<div class="leg"><div class="leg-dot" style="background:var(--hp)"></div>HP</div>
</div>
</div>
<div class="legend-box" id="legend-point" style="display:none">
<div class="legend-title">Legenda Titik</div>
<div class="legend-items">
<div class="leg"><div class="leg-dot" style="background:var(--shm)"></div>24 Jam</div>
<div class="leg"><div class="leg-dot" style="background:#f85149"></div>Tidak 24 Jam</div>
</div>
</div>
<div class="stats-bar">
<div class="stat-chip"><div class="val" id="cnt-total">0</div><div class="lbl">Total</div></div>
<div class="stat-chip"><div class="val" id="cnt-a">0</div><div class="lbl" id="lbl-a">Nasional</div></div>
<div class="stat-chip"><div class="val" id="cnt-b">0</div><div class="lbl" id="lbl-b">Provinsi</div></div>
<div class="stat-chip"><div class="val" id="cnt-c">0</div><div class="lbl" id="lbl-c">Kabupaten</div></div>
</div>
<div class="list-hdr">Daftar Objek</div>
<div id="data-list"></div>
</aside>
<div id="map"></div>
</div>
<div id="toast"></div>
<div id="draw-hint">
✏️ <span id="hint-text">Klik untuk menggambar. Dobel-klik untuk selesai.</span>
<button onclick="cancelDraw()">Batal</button>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script>
var JALAN_COLOR = {
'Jalan Nasional' : '#ef4444',
'Jalan Provinsi' : '#f97316',
'Jalan Kabupaten': '#eab308'
};
var PARSIL_COLOR = { SHM:'#22c55e', HGB:'#3b82f6', HGU:'#a855f7', HP:'#ec4899' };
var POINT_COLOR = { Ya:'green', Tidak:'red' };
var map = L.map('map').setView([-0.0263, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
attribution:'© OpenStreetMap', maxZoom:19
}).addTo(map);
var drawnLayers = {};
var activeDrawTool = null;
var pendingLayer = null;
var pendingCoords = null;
var pendingType = null;
var currentLayer = 'jalan';
var allData = { jalan:[], parsil:[], point:[] };
function makeIcon(color) {
return L.icon({
iconUrl:`https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-${color}.png`,
shadowUrl:'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize:[25,41],iconAnchor:[12,41],popupAnchor:[1,-34],shadowSize:[41,41]
});
}
function toast(msg, type='ok', ms=2500) {
var el=document.getElementById('toast');
el.textContent=msg; el.className='show '+type;
clearTimeout(el._t); el._t=setTimeout(()=>{el.className='';},ms);
}
function showHint(msg){document.getElementById('hint-text').textContent=msg;document.getElementById('draw-hint').style.display='block';}
function hideHint(){document.getElementById('draw-hint').style.display='none';}
function switchLayer(layer) {
currentLayer = layer;
document.querySelectorAll('.ltab').forEach(t => t.classList.toggle('active', t.dataset.layer===layer));
['jalan','parsil','point'].forEach(l => {
document.getElementById('legend-'+l).style.display = (l===layer)?'':'none';
});
renderSidebar(layer);
updateStats(layer);
}
// 1. GABUNGAN LOGIKA KLIK (MATIKAN EDIT + MUNCULKAN MENU TAMBAH)
map.on('click', function(e) {
let modeEditDimatikan = false;
Object.values(drawnLayers).forEach(l => {
if (l.editing && l.editing.enabled()) {
l.editing.disable();
if(l.transform) l.transform.disable();
modeEditDimatikan = true;
}
});
if (modeEditDimatikan) {
toast('Mode Edit Dimatikan', 'info');
return; // Jangan muncul menu tambah kalau niatnya cuma matiin edit
}
if (activeDrawTool) return;
var lat = e.latlng.lat.toFixed(5), lng = e.latlng.lng.toFixed(5);
var html = `
<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:#58a6ff"></div>
<div><div class="ph-title">Tambah Objek</div><div class="ph-sub">${lat}, ${lng}</div></div>
</div>
<div class="pop-body">
<div class="menu-choices">
<button class="menu-btn" onclick="startDraw('point',${e.latlng.lat},${e.latlng.lng})">
<div><div>Buat Titik</div><div class="mb-desc">Tandai lokasi (Point)</div></div>
</button>
<button class="menu-btn" onclick="startDraw('jalan')">
<div><div>Buat Jalan</div><div class="mb-desc">Gambar polyline jalan</div></div>
</button>
<button class="menu-btn" onclick="startDraw('parsil')">
<div><div>Buat Parsil Tanah</div><div class="mb-desc">Gambar polygon area</div></div>
</button>
</div>
</div>
</div>`;
L.popup().setLatLng(e.latlng).setContent(html).openOn(map);
});
function startDraw(type, lat, lng) {
map.closePopup();
pendingType = type;
if (type === 'point') {
pendingCoords = {lat, lng};
if (pendingLayer) map.removeLayer(pendingLayer);
pendingLayer = L.marker([lat,lng],{draggable:true,icon:makeIcon('blue'),opacity:0.7}).addTo(map);
pendingLayer.on('dragend', ev => { pendingCoords={lat:ev.target.getLatLng().lat,lng:ev.target.getLatLng().lng}; });
openFormPopup('point', [lat,lng]);
} else if (type === 'jalan') {
activeDrawTool = new L.Draw.Polyline(map,{shapeOptions:{color:'#58a6ff',weight:4,opacity:.9}});
activeDrawTool.enable();
showHint('Klik untuk menambah titik jalan. Dobel-klik untuk selesai.');
} else if (type === 'parsil') {
activeDrawTool = new L.Draw.Polygon(map,{shapeOptions:{color:'#a855f7',fillOpacity:.25,weight:3}});
activeDrawTool.enable();
showHint('Klik untuk menambah sudut parsil. Klik titik awal atau dobel-klik untuk menutup.');
}
}
function cancelDraw() {
if (activeDrawTool){activeDrawTool.disable();activeDrawTool=null;}
if (pendingLayer){map.removeLayer(pendingLayer);pendingLayer=null;}
pendingCoords=null; pendingType=null; hideHint();
toast('Drawing dibatalkan','info');
}
map.on(L.Draw.Event.CREATED, function(e) {
activeDrawTool=null; hideHint();
var layer=e.layer;
if (pendingLayer) map.removeLayer(pendingLayer);
pendingLayer=layer; map.addLayer(pendingLayer);
var center = layer.getBounds ? layer.getBounds().getCenter() : layer.getLatLng();
openFormPopup(pendingType, center, layer);
});
function hitungPanjang(layer) {
var latlngs = layer.getLatLngs();
var total = 0;
var pts = latlngs[0] && Array.isArray(latlngs[0]) ? latlngs[0] : latlngs;
for (var i=0; i<pts.length-1; i++) {
total += pts[i].distanceTo(pts[i+1]);
}
return Math.round(total * 100) / 100;
}
function hitungLuas(layer) {
var latlngs = layer.getLatLngs()[0];
if (!latlngs || latlngs.length < 3) return 0;
var R = 6378137;
var area = 0;
var n = latlngs.length;
for (var i=0; i<n; i++) {
var j = (i+1) % n;
var xi = latlngs[i].lng * Math.PI/180;
var yi = latlngs[i].lat * Math.PI/180;
var xj = latlngs[j].lng * Math.PI/180;
var yj = latlngs[j].lat * Math.PI/180;
area += (xj - xi) * (2 + Math.sin(yi) + Math.sin(yj));
}
area = Math.abs(area * R * R / 2);
return Math.round(area * 100) / 100;
}
function openFormPopup(type, latlng, layer) {
var html = '';
if (type === 'jalan') {
var panjang = layer ? hitungPanjang(layer) : 0;
html = `
<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:#58a6ff"></div>
<div><div class="ph-title">Data Jalan Baru</div><div class="ph-sub">Isi atribut jalan</div></div>
</div>
<div class="pop-body">
<div class="form-group"><label>Nama Jalan *</label><input type="text" id="f-nama" placeholder="Jl. Ahmad Yani"/></div>
<div class="form-group">
<label>Status Jalan *</label>
<select id="f-status">
<option value="Jalan Nasional">Jalan Nasional</option>
<option value="Jalan Provinsi">Jalan Provinsi</option>
<option value="Jalan Kabupaten">Jalan Kabupaten</option>
</select>
</div>
<div class="form-group"><label>Panjang (meter) — otomatis</label>
<input type="text" id="f-panjang" class="calc-field" value="${panjang}" readonly/>
</div>
<div class="btn-row">
<button class="btn btn-muted" onclick="cancelDraw()">Batal</button>
<button class="btn btn-success" onclick="simpanData('jalan')">Simpan</button>
</div>
</div>
</div>`;
} else if (type === 'parsil') {
var luas = layer ? hitungLuas(layer) : 0;
html = `
<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:#a855f7"></div>
<div><div class="ph-title">Data Parsil Tanah</div><div class="ph-sub">Isi atribut parsil</div></div>
</div>
<div class="pop-body">
<div class="form-group"><label>Nama Parsil *</label><input type="text" id="f-nama" placeholder="Parsil A-01"/></div>
<div class="form-group">
<label>Status Sertifikat *</label>
<select id="f-status">
<option value="SHM">SHM — Hak Milik</option>
<option value="HGB">HGB — Hak Guna Bangunan</option>
<option value="HGU">HGU — Hak Guna Usaha</option>
<option value="HP">HP — Hak Pakai</option>
</select>
</div>
<div class="form-group"><label>Luas (m²) — otomatis</label>
<input type="text" id="f-luas" class="calc-field" value="${luas}" readonly/>
</div>
<div class="btn-row">
<button class="btn btn-muted" onclick="cancelDraw()">Batal</button>
<button class="btn btn-success" onclick="simpanData('parsil')">Simpan</button>
</div>
</div>
</div>`;
} else {
html = `
<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:#22c55e"></div>
<div><div class="ph-title">Data Titik Baru</div><div class="ph-sub">Isi detail objek</div></div>
</div>
<div class="pop-body">
<div class="form-group"><label>Nama Objek *</label><input type="text" id="f-nama" placeholder="SPBU, Rumah Sakit..."/></div>
<div class="form-group"><label>No. WhatsApp</label><input type="text" id="f-wa" placeholder="0812xxxxxxxx"/></div>
<div class="form-group"><label>Buka 24 Jam?</label>
<select id="f-jam"><option value="Ya">Ya</option><option value="Tidak" selected>Tidak</option></select>
</div>
<div class="btn-row">
<button class="btn btn-muted" onclick="cancelDraw()">Batal</button>
<button class="btn btn-success" onclick="simpanData('point')">Simpan</button>
</div>
</div>
</div>`;
}
L.popup({closeButton:true,closeOnClick:false}).setLatLng(latlng).setContent(html).openOn(map);
setTimeout(()=>{var el=document.getElementById('f-nama');if(el)el.focus();},100);
}
function simpanData(type) {
var nama = (document.getElementById('f-nama')?.value||'').trim();
if (!nama) { toast('⚠️ Nama wajib diisi!','err'); return; }
if (!pendingCoords && !pendingLayer) { toast('⚠️ Gambar dulu di peta!','err'); return; }
var geojson;
if (type === 'point') {
geojson = JSON.stringify({type:'Point',coordinates:[pendingCoords.lng,pendingCoords.lat]});
} else if (type === 'jalan') {
var pts = pendingLayer.getLatLngs();
var arr = (pts[0]&&Array.isArray(pts[0])) ? pts[0] : pts;
geojson = JSON.stringify({type:'LineString',coordinates:arr.map(p=>[p.lng,p.lat])});
} else {
var pts = pendingLayer.getLatLngs()[0];
geojson = JSON.stringify({type:'Polygon',coordinates:[pts.map(p=>[p.lng,p.lat])]});
}
var fd = new FormData();
fd.append('aksi','simpan'); fd.append('layer',type); fd.append('nama',nama); fd.append('geojson',geojson);
if (type==='jalan') {
fd.append('status', document.getElementById('f-status').value);
fd.append('panjang', document.getElementById('f-panjang').value);
} else if (type==='parsil') {
fd.append('status', document.getElementById('f-status').value);
fd.append('luas', document.getElementById('f-luas').value);
} else {
fd.append('wa', document.getElementById('f-wa')?.value||'');
fd.append('jam', document.getElementById('f-jam').value);
}
fetch('proses.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='ok') {
map.closePopup();
if (pendingLayer) map.removeLayer(pendingLayer);
pendingLayer=null; pendingCoords=null; pendingType=null;
toast('✅ '+res.message,'ok');
loadAll();
} else { toast('❌ '+res.message,'err'); }
}).catch(()=>toast('❌ Gagal terhubung ke server','err'));
}
function loadAll() {
Object.values(drawnLayers).forEach(l=>{ try{map.removeLayer(l);}catch(e){} });
drawnLayers={};
var layers = ['jalan','parsil','point'];
var done = 0;
layers.forEach(l=>{
fetch(`proses.php?aksi=baca&layer=${l}`).then(r=>r.json()).then(res=>{
allData[l] = res.status==='ok' ? res.data : [];
allData[l].forEach(row=>addToMap(l,row));
done++;
if (done===3) {
renderSidebar(currentLayer);
updateStats(currentLayer);
}
});
});
}
function addToMap(layer, row) {
var gj = JSON.parse(row.geojson);
var leafLayer;
if (layer === 'jalan') {
var color = JALAN_COLOR[row.status_jalan] || '#fff';
var coords = gj.coordinates.map(c => ([c[1], c[0]]));
leafLayer = L.polyline(coords, {
color: color, weight: 5, opacity: .9,
draggable: true, transform: true
});
// 2. KEMBALIKAN FUNGSI POPUP AGAR BISA KLIK HAPUS
leafLayer.bindPopup(buildPopupJalan(row));
} else if (layer === 'parsil') {
var color = PARSIL_COLOR[row.status_sertifikat] || '#fff';
var coords = gj.coordinates[0].map(c => ([c[1], c[0]]));
leafLayer = L.polygon(coords, {
color: color, fillColor: color, fillOpacity: .25, weight: 3,
draggable: true, transform: true
});
// 2. KEMBALIKAN FUNGSI POPUP AGAR BISA KLIK HAPUS
leafLayer.bindPopup(buildPopupParsil(row));
} else {
var iconColor = row.buka_24jam === 'Ya' ? 'green' : 'red';
var coord = [gj.coordinates[1], gj.coordinates[0]];
leafLayer = L.marker(coord, { draggable: true, icon: makeIcon(iconColor) });
leafLayer.bindPopup(buildPopupPoint(row));
}
if (layer !== 'point') {
// 3. UBAH KLIK JADI KLIK KANAN (CONTEXTMENU) UNTUK MODE EDIT BENTUK
leafLayer.on('contextmenu', function (e) {
L.DomEvent.stopPropagation(e);
if (!leafLayer.editing.enabled()) {
Object.values(drawnLayers).forEach(l => { if(l.editing) l.editing.disable(); });
leafLayer.editing.enable();
if(leafLayer.transform) leafLayer.transform.enable();
toast('Mode Edit Aktif (Geser titik/garis)', 'info');
}
});
leafLayer.on('dragend edit transformsave', function () {
updateGeometriOtomatis(layer, row, leafLayer);
});
} else {
leafLayer.on('dragend', function () {
updateGeometriOtomatis('point', row, leafLayer);
});
}
leafLayer.addTo(objekGroup || map);
drawnLayers[layer + '_' + row.id] = leafLayer;
}
function buildPopupJalan(row) {
var color = JALAN_COLOR[row.status_jalan]||'#fff';
return `<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:${color}"></div>
<div><div class="ph-title">${esc(row.nama_jalan)}</div><div class="ph-sub">Data Jalan</div></div>
</div>
<div class="pop-body">
<div class="info-row"><span class="ir-label">Status</span><span class="ir-val">${esc(row.status_jalan)}</span></div>
<div class="info-row"><span class="ir-label">Panjang</span><span class="ir-val">${parseFloat(row.panjang_meter).toLocaleString('id')} m</span></div>
<div class="info-row"><span class="ir-label">ID</span><span class="ir-val">#${row.id}</span></div>
<div class="btn-row"><button class="btn btn-primary" onclick="openEdit('jalan',${row.id})">✏️ Edit</button>
<button class="btn btn-danger" onclick="hapus('jalan',${row.id})">🗑️ Hapus</button></div>
</div></div>`;
}
function buildPopupParsil(row) {
var color = PARSIL_COLOR[row.status_sertifikat]||'#fff';
var label = {SHM:'Hak Milik',HGB:'Hak Guna Bangunan',HGU:'Hak Guna Usaha',HP:'Hak Pakai'};
return `<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:${color}"></div>
<div><div class="ph-title">${esc(row.nama_parsil)}</div><div class="ph-sub">Parsil Tanah</div></div>
</div>
<div class="pop-body">
<div class="info-row"><span class="ir-label">Sertifikat</span><span class="ir-val">${row.status_sertifikat} — ${label[row.status_sertifikat]||''}</span></div>
<div class="info-row"><span class="ir-label">Luas</span><span class="ir-val">${parseFloat(row.luas_m2).toLocaleString('id')} m²</span></div>
<div class="info-row"><span class="ir-label">ID</span><span class="ir-val">#${row.id}</span></div>
<div class="btn-row"><button class="btn btn-primary" onclick="openEdit('parsil',${row.id})">✏️ Edit</button>
<button class="btn btn-danger" onclick="hapus('parsil',${row.id})">🗑️ Hapus</button></div>
</div></div>`;
}
function buildPopupPoint(row) {
return `<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:#58a6ff"></div>
<div><div class="ph-title">${esc(row.nama_objek)}</div><div class="ph-sub">Titik / Point</div></div>
</div>
<div class="pop-body">
<div class="info-row"><span class="ir-label">WA</span><span class="ir-val">${row.no_wa||'—'}</span></div>
<div class="info-row"><span class="ir-label">24 Jam</span><span class="ir-val">${row.buka_24jam}</span></div>
<div class="info-row"><span class="ir-label">ID</span><span class="ir-val">#${row.id}</span></div>
<div class="btn-row"><button class="btn btn-primary" onclick="openEdit('point',${row.id})">✏️ Edit</button>
<button class="btn btn-danger" onclick="hapus('point',${row.id})">🗑️ Hapus</button></div>
</div></div>`;
}
function openEdit(layer, id) {
fetch(`proses.php?aksi=detail&layer=${layer}&id=${id}`).then(r=>r.json()).then(res=>{
if (res.status!=='ok') { toast('❌ Data tidak ditemukan','err'); return; }
var row = res.data;
var leafLayer = drawnLayers[layer+'_'+id];
var center = leafLayer
? (leafLayer.getBounds ? leafLayer.getBounds().getCenter() : leafLayer.getLatLng())
: map.getCenter();
var html='';
if (layer==='jalan') {
html=`<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:#58a6ff"></div>
<div><div class="ph-title">Edit Jalan #${id}</div></div>
</div>
<div class="pop-body">
<div class="form-group"><label>Nama Jalan *</label><input type="text" id="e-nama" value="${esc(row.nama_jalan)}"/></div>
<div class="form-group"><label>Status</label>
<select id="e-status">
<option ${row.status_jalan==='Jalan Nasional'?'selected':''}>Jalan Nasional</option>
<option ${row.status_jalan==='Jalan Provinsi'?'selected':''}>Jalan Provinsi</option>
<option ${row.status_jalan==='Jalan Kabupaten'?'selected':''}>Jalan Kabupaten</option>
</select>
</div>
<div class="form-group"><label>Panjang (m)</label><input class="calc-field" id="e-panjang" value="${row.panjang_meter}" readonly/></div>
<div class="btn-row"><button class="btn btn-muted" onclick="map.closePopup()">Batal</button>
<button class="btn btn-primary" onclick="updateData('jalan',${id})">Simpan</button></div>
</div></div>`;
} else if (layer==='parsil') {
html=`<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:#a855f7"></div>
<div><div class="ph-title">Edit Parsil #${id}</div></div>
</div>
<div class="pop-body">
<div class="form-group"><label>Nama Parsil *</label><input type="text" id="e-nama" value="${esc(row.nama_parsil)}"/></div>
<div class="form-group"><label>Status Sertifikat</label>
<select id="e-status">
<option value="SHM" ${row.status_sertifikat==='SHM'?'selected':''}>SHM — Hak Milik</option>
<option value="HGB" ${row.status_sertifikat==='HGB'?'selected':''}>HGB — Hak Guna Bangunan</option>
<option value="HGU" ${row.status_sertifikat==='HGU'?'selected':''}>HGU — Hak Guna Usaha</option>
<option value="HP" ${row.status_sertifikat==='HP'?'selected':''}>HP — Hak Pakai</option>
</select>
</div>
<div class="form-group"><label>Luas (m²)</label><input class="calc-field" id="e-luas" value="${row.luas_m2}" readonly/></div>
<div class="btn-row"><button class="btn btn-muted" onclick="map.closePopup()">Batal</button>
<button class="btn btn-primary" onclick="updateData('parsil',${id})">Simpan</button></div>
</div></div>`;
} else {
html=`<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:#22c55e"></div>
<div><div class="ph-title">Edit Titik #${id}</div></div>
</div>
<div class="pop-body">
<div class="form-group"><label>Nama *</label><input type="text" id="e-nama" value="${esc(row.nama_objek)}"/></div>
<div class="form-group"><label>No. WA</label><input type="text" id="e-wa" value="${esc(row.no_wa||'')}"/></div>
<div class="form-group"><label>24 Jam?</label>
<select id="e-jam">
<option value="Ya" ${row.buka_24jam==='Ya'?'selected':''}>Ya</option>
<option value="Tidak" ${row.buka_24jam==='Tidak'?'selected':''}>Tidak</option>
</select>
</div>
<div class="btn-row"><button class="btn btn-muted" onclick="map.closePopup()">Batal</button>
<button class="btn btn-primary" onclick="updateData('point',${id})">Simpan</button></div>
</div></div>`;
}
L.popup({closeButton:true,closeOnClick:false}).setLatLng(center).setContent(html).openOn(map);
setTimeout(()=>{var el=document.getElementById('e-nama');if(el)el.focus();},100);
}).catch(()=>toast('❌ Gagal memuat data','err'));
}
function updateData(layer, id) {
var nama = (document.getElementById('e-nama')?.value||'').trim();
if (!nama) { toast('⚠️ Nama wajib diisi!','err'); return; }
var fd = new FormData();
fd.append('aksi','update'); fd.append('layer',layer); fd.append('id',id); fd.append('nama',nama);
if (layer==='jalan') { fd.append('status',document.getElementById('e-status').value); fd.append('panjang',document.getElementById('e-panjang').value); }
if (layer==='parsil') { fd.append('status',document.getElementById('e-status').value); fd.append('luas',document.getElementById('e-luas').value); }
if (layer==='point') { fd.append('wa',document.getElementById('e-wa').value||''); fd.append('jam',document.getElementById('e-jam').value); }
fetch('proses.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='ok') { map.closePopup(); toast('✅ '+res.message,'ok'); loadAll(); }
else toast('❌ '+res.message,'err');
}).catch(()=>toast('❌ Gagal terhubung','err'));
}
function hapus(layer, id) {
var leafLayer = drawnLayers[layer+'_'+id];
var center = leafLayer
? (leafLayer.getBounds ? leafLayer.getBounds().getCenter() : leafLayer.getLatLng())
: map.getCenter();
var html=`<div class="pop-panel">
<div class="pop-head"><div class="ph-stripe" style="background:#f85149"></div>
<div><div class="ph-title" style="color:#f85149">Konfirmasi Hapus</div><div class="ph-sub">Tidak bisa dibatalkan</div></div>
</div>
<div class="pop-body">
<p style="font-size:12px;color:var(--muted);margin-bottom:10px;">Yakin ingin menghapus objek ini?</p>
<div class="btn-row">
<button class="btn btn-muted" onclick="map.closePopup()">Batal</button>
<button class="btn btn-danger" onclick="konfirmasiHapus('${layer}',${id})">Ya, Hapus</button>
</div>
</div></div>`;
L.popup({closeButton:true,closeOnClick:false}).setLatLng(center).setContent(html).openOn(map);
}
function konfirmasiHapus(layer, id) {
var fd=new FormData(); fd.append('aksi','hapus'); fd.append('layer',layer); fd.append('id',id);
fetch('proses.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='ok') { map.closePopup(); toast('🗑️ '+res.message,'ok'); loadAll(); }
else toast('❌ '+res.message,'err');
}).catch(()=>toast('❌ Gagal terhubung','err'));
}
function renderSidebar(layer) {
var el = document.getElementById('data-list');
el.innerHTML='';
var data = allData[layer]||[];
if (!data.length) {
el.innerHTML='<div class="empty-state"><div class="ei">🗺️</div><div>Belum ada data</div></div>';
return;
}
data.forEach(row=>{
var item=document.createElement('div');
item.className='data-item'; item.dataset.id=row.id;
var stripe='', name='', meta='', badge='', badgeCls='';
if (layer==='jalan') {
var c=JALAN_COLOR[row.status_jalan]||'#fff';
stripe=`style="background:${c}"`;
name=row.nama_jalan; meta=row.status_jalan+' · '+parseFloat(row.panjang_meter).toLocaleString('id')+' m';
badge=row.status_jalan.split(' ')[1]; badgeCls=`background:${c}20;color:${c}`;
} else if (layer==='parsil') {
var c=PARSIL_COLOR[row.status_sertifikat]||'#fff';
stripe=`style="background:${c}"`;
name=row.nama_parsil; meta=row.status_sertifikat+' · '+parseFloat(row.luas_m2).toLocaleString('id')+' m²';
badge=row.status_sertifikat; badgeCls=`background:${c}20;color:${c}`;
} else {
var c=row.buka_24jam==='Ya'?'#22c55e':'#f85149';
stripe=`style="background:${c}"`;
name=row.nama_objek; meta=(row.no_wa||'—')+' · '+row.buka_24jam;
badge=row.buka_24jam==='Ya'?'24J':'—'; badgeCls=`background:${c}20;color:${c}`;
}
item.innerHTML=`
<div class="di-stripe" ${stripe}></div>
<div class="di-info"><div class="di-name">${esc(name)}</div><div class="di-meta">${meta}</div></div>
<span class="di-badge" style="${badgeCls}">${badge}</span>`;
item.addEventListener('click',()=>{
document.querySelectorAll('.data-item').forEach(i=>i.classList.remove('active'));
item.classList.add('active');
var ll = drawnLayers[layer+'_'+row.id];
if (!ll) return;
if (ll.getBounds) map.flyToBounds(ll.getBounds(),{padding:[60,60],duration:.6});
else map.flyTo(ll.getLatLng(),16,{duration:.6});
setTimeout(()=>ll.openPopup(),700);
});
el.appendChild(item);
});
}
function updateGeometriOtomatis(layer, row, leafLayer) {
var geojson;
var fd = new FormData();
fd.append('aksi', 'update');
fd.append('layer', layer);
fd.append('id', row.id);
fd.append('nama', (layer === 'jalan' ? row.nama_jalan : (layer === 'parsil' ? row.nama_parsil : row.nama_objek)));
if (layer === 'point') {
var ll = leafLayer.getLatLng();
geojson = JSON.stringify({type:'Point', coordinates:[ll.lng, ll.lat]});
fd.append('wa', row.no_wa || '');
fd.append('jam', row.buka_24jam);
}
else if (layer === 'jalan') {
var pts = leafLayer.getLatLngs();
geojson = JSON.stringify({type:'LineString', coordinates: pts.map(p => [p.lng, p.lat])});
fd.append('status', row.status_jalan);
fd.append('panjang', hitungPanjang(leafLayer));
}
else if (layer === 'parsil') {
var pts = leafLayer.getLatLngs()[0];
geojson = JSON.stringify({type:'Polygon', coordinates: [pts.map(p => [p.lng, p.lat])]});
fd.append('status', row.status_sertifikat);
fd.append('luas', hitungLuas(leafLayer));
}
fd.append('geojson', geojson);
fetch('proses.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(res => {
if (res.status === 'ok') {
toast('💾 Perubahan geometri disimpan', 'ok');
if(layer==='jalan') row.panjang_meter = fd.get('panjang');
if(layer==='parsil') row.luas_m2 = fd.get('luas');
updateStats(currentLayer);
}
});
}
function updateStats(layer) {
var data = allData[layer]||[];
document.getElementById('cnt-total').textContent = data.length;
if (layer==='jalan') {
document.getElementById('lbl-a').textContent='Nasional';
document.getElementById('lbl-b').textContent='Provinsi';
document.getElementById('lbl-c').textContent='Kabupaten';
document.getElementById('cnt-a').textContent=data.filter(d=>d.status_jalan==='Jalan Nasional').length;
document.getElementById('cnt-b').textContent=data.filter(d=>d.status_jalan==='Jalan Provinsi').length;
document.getElementById('cnt-c').textContent=data.filter(d=>d.status_jalan==='Jalan Kabupaten').length;
} else if (layer==='parsil') {
document.getElementById('lbl-a').textContent='SHM';
document.getElementById('lbl-b').textContent='HGB';
document.getElementById('lbl-c').textContent='HGU/HP';
document.getElementById('cnt-a').textContent=data.filter(d=>d.status_sertifikat==='SHM').length;
document.getElementById('cnt-b').textContent=data.filter(d=>d.status_sertifikat==='HGB').length;
document.getElementById('cnt-c').textContent=data.filter(d=>d.status_sertifikat==='HGU'||d.status_sertifikat==='HP').length;
} else {
document.getElementById('lbl-a').textContent='24 Jam';
document.getElementById('lbl-b').textContent='Tidak';
document.getElementById('lbl-c').textContent='Total WA';
document.getElementById('cnt-a').textContent=data.filter(d=>d.buka_24jam==='Ya').length;
document.getElementById('cnt-b').textContent=data.filter(d=>d.buka_24jam==='Tidak').length;
document.getElementById('cnt-c').textContent=data.filter(d=>d.no_wa).length;
}
}
function esc(s){if(!s)return'';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
// objekGroup harus ada sebelum loadAll() dipanggil
var objekGroup = L.layerGroup().addTo(map);
loadAll();
// ─────────────────────────────────────────────────────
// LAYER REFERENCES untuk toggle
// ─────────────────────────────────────────────────────
var kecamatanLayer = null; // Layer batas kecamatan (GeoJSON)
var layerVisible = { kecamatan: true, objek: true };
// ─────────────────────────────────────────────────────
// TOGGLE LAYER via checkbox
// ─────────────────────────────────────────────────────
function toggleLayer(nama) {
var chk = document.getElementById('chk-' + nama);
layerVisible[nama] = !layerVisible[nama];
if (layerVisible[nama]) {
chk.classList.add('checked');
if (nama === 'kecamatan' && kecamatanLayer) map.addLayer(kecamatanLayer);
if (nama === 'objek' && objekGroup) map.addLayer(objekGroup);
} else {
chk.classList.remove('checked');
chk.style.background = 'transparent';
if (nama === 'kecamatan' && kecamatanLayer) map.removeLayer(kecamatanLayer);
if (nama === 'objek' && objekGroup) map.removeLayer(objekGroup);
}
}
// ─────────────────────────────────────────────────────
// WARNA PER KECAMATAN
// ─────────────────────────────────────────────────────
var KEC_COLORS = [
'#ef4444','#f97316','#eab308','#22c55e',
'#3b82f6','#a855f7','#ec4899','#14b8a6'
];
var colorIdx = 0;
var kecColorMap = {};
function getFieldNama(props) {
var kandidat = ['KECAMATAN','Kecamatan','kecamatan','NAMA','Nama','nama',
'NAMOBJ','namobj','NAME','name','KEC','kec'];
for (var f of kandidat) {
if (props[f] !== undefined && props[f] !== null && String(props[f]).trim() !== '')
return String(props[f]).trim();
}
for (var k in props) {
if (typeof props[k] === 'string' && props[k].trim() !== '') return props[k].trim();
}
return 'Kecamatan';
}
function formatAngka(val) {
var n = parseFloat(val);
if (isNaN(n)) return val;
return n % 1 === 0 ? n.toLocaleString('id-ID') : n.toLocaleString('id-ID', {maximumFractionDigits:2});
}
function shouldShow(key, val) {
var skip = ['FID','fid','OBJECTID','objectid'];
if (skip.includes(key)) return false;
if (val === null || val === '' || val === undefined) return false;
return true;
}
function labelField(key) {
var map = {
'KECAMATAN':'Kecamatan','Kecamatan':'Kecamatan','kecamatan':'Kecamatan',
'NAMA':'Nama','Nama':'Nama','nama':'Nama',
'POPULASI':'Populasi','Populasi':'Populasi','populasi':'Populasi',
'PENDUDUK':'Jumlah Penduduk','penduduk':'Jumlah Penduduk',
'LUAS':'Luas (Ha)','luas':'Luas (Ha)','LUAS_HA':'Luas (Ha)',
'LUAS_KM2':'Luas (km²)','luas_km2':'Luas (km²)',
'PLAN_AREA':'Luas Area','Plan_Area':'Luas Area',
'KET':'Keterangan','Ket':'Keterangan','ket':'Keterangan',
'Shape_Area':'Luas (m²)','Shape_Leng':'Panjang (m)',
'KEPADATAN':'Kepadatan','kepadatan':'Kepadatan',
};
return map[key] || key;
}
// ─────────────────────────────────────────────────────
// LOAD BATAS KECAMATAN (GeoJSON)
// ─────────────────────────────────────────────────────
function loadAllLayers() {
fetch('geojson.php?file=Populasi_Kecamatan.json')
.then(res => {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
})
.then(data => {
if (!data || !data.features) {
console.error('❌ Format GeoJSON tidak valid');
return;
}
kecamatanLayer = L.geoJSON(data, {
style: function(feature) {
var props = feature.properties || {};
var nama = getFieldNama(props);
if (!kecColorMap[nama]) {
kecColorMap[nama] = KEC_COLORS[colorIdx % KEC_COLORS.length];
colorIdx++;
}
var warna = kecColorMap[nama];
return { color:warna, weight:2, opacity:1, fillColor:warna, fillOpacity:0.18 };
},
onEachFeature: function(feature, layer) {
var props = feature.properties || {};
var nama = getFieldNama(props);
var warna = kecColorMap[nama] || '#58a6ff';
// Popup detail — baca SEMUA field otomatis
var popHtml = `
<div style="font-family:'Plus Jakarta Sans',sans-serif;width:230px;">
<div style="background:${warna}18;border-left:4px solid ${warna};
padding:10px 12px;margin-bottom:8px;border-radius:0 6px 6px 0;">
<div style="font-weight:700;font-size:14px;color:${warna};">${esc(nama)}</div>
<div style="font-size:10px;color:#8b949e;margin-top:2px;">Kota Pontianak</div>
</div>
<div style="padding:0 12px 10px;">`;
for (var key in props) {
if (!shouldShow(key, props[key])) continue;
var label = labelField(key);
var val = props[key];
if (typeof val === 'number') val = formatAngka(val);
var icon = '•';
var lk = key.toLowerCase();
if (lk.includes('popul') || lk.includes('penduduk') || lk.includes('jumlah')) icon = '👥';
else if (lk.includes('luas') || lk.includes('area')) icon = '📐';
else if (lk.includes('kec') || lk.includes('nama') || lk.includes('name')) icon = '🏘️';
else if (lk.includes('kepadat')) icon = '📊';
else if (lk.includes('ket')) icon = '️';
popHtml += `
<div style="display:flex;justify-content:space-between;align-items:center;
padding:5px 0;border-bottom:1px solid #30363d;font-size:11px;">
<span style="color:#8b949e;font-weight:600;">${icon} ${label}</span>
<span style="color:#e6edf3;font-weight:700;text-align:right;max-width:130px;">${esc(String(val))}</span>
</div>`;
}
popHtml += `</div></div>`;
layer.bindPopup(popHtml, { maxWidth: 260 });
// Hover effect
layer.on('mouseover', function() { this.setStyle({ fillOpacity:0.4, weight:3 }); });
layer.on('mouseout', function() { this.setStyle({ fillOpacity:0.18, weight:2 }); });
}
}).addTo(map);
console.log(`✅ Loaded: Populasi_Kecamatan.json (${data.features.length} kecamatan)`);
})
.catch(err => console.error('❌ Gagal memuat Populasi_Kecamatan.json:', err.message));
}
loadAllLayers();
</script>
</body>
</html>