1062 lines
54 KiB
PHP
1062 lines
54 KiB
PHP
<?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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||
|
||
// 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>
|