|
|
|
@@ -0,0 +1,911 @@
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="id">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
|
<title>WebGIS — SPBU · 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" />
|
|
|
|
|
<style>
|
|
|
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
:root {
|
|
|
|
|
--bg:#0f1923; --surface:#1a2535; --surface2:#243044; --border:#2e3f57;
|
|
|
|
|
--accent:#3b9eff; --accent2:#56d4a0; --danger:#ff5c5c; --warn:#ffb84d;
|
|
|
|
|
--text:#e2eaf5; --muted:#7a90aa; --radius:8px; --shadow:0 4px 20px rgba(0,0,0,.45);
|
|
|
|
|
--font:'Segoe UI',system-ui,sans-serif;
|
|
|
|
|
}
|
|
|
|
|
html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--text);}
|
|
|
|
|
#app{display:flex;flex-direction:column;height:100vh;}
|
|
|
|
|
|
|
|
|
|
header{
|
|
|
|
|
background:var(--surface);border-bottom:1px solid var(--border);
|
|
|
|
|
padding:10px 18px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;
|
|
|
|
|
z-index:1000;position:relative;
|
|
|
|
|
}
|
|
|
|
|
header h1{font-size:.95rem;font-weight:700;color:var(--accent);white-space:nowrap;}
|
|
|
|
|
header h1 span{color:var(--text);}
|
|
|
|
|
|
|
|
|
|
.tabs{display:flex;gap:5px;}
|
|
|
|
|
.tab-btn{
|
|
|
|
|
padding:6px 14px;border-radius:20px;border:1px solid var(--border);
|
|
|
|
|
background:transparent;color:var(--muted);cursor:pointer;font-size:.8rem;transition:all .2s;
|
|
|
|
|
}
|
|
|
|
|
.tab-btn.active{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600;}
|
|
|
|
|
.tab-btn:hover:not(.active){border-color:var(--accent);color:var(--accent);}
|
|
|
|
|
|
|
|
|
|
#mode-badge{
|
|
|
|
|
margin-left:auto;padding:5px 12px;border-radius:20px;
|
|
|
|
|
background:var(--surface2);border:1px solid var(--border);font-size:.75rem;color:var(--muted);
|
|
|
|
|
}
|
|
|
|
|
#mode-badge.active{border-color:var(--accent2);color:var(--accent2);}
|
|
|
|
|
|
|
|
|
|
#map{flex:1;}
|
|
|
|
|
|
|
|
|
|
/* Sidebar */
|
|
|
|
|
#sidebar{
|
|
|
|
|
position:absolute;right:12px;top:70px;width:290px;
|
|
|
|
|
max-height:calc(100vh - 90px);background:var(--surface);
|
|
|
|
|
border:1px solid var(--border);border-radius:var(--radius);
|
|
|
|
|
box-shadow:var(--shadow);overflow-y:auto;z-index:900;
|
|
|
|
|
display:none;flex-direction:column;
|
|
|
|
|
}
|
|
|
|
|
#sidebar.open{display:flex;}
|
|
|
|
|
.sidebar-header{
|
|
|
|
|
padding:12px 14px;border-bottom:1px solid var(--border);
|
|
|
|
|
display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;
|
|
|
|
|
background:var(--surface);z-index:1;
|
|
|
|
|
}
|
|
|
|
|
.sidebar-header h3{font-size:.85rem;font-weight:600;}
|
|
|
|
|
.sidebar-close{background:none;border:none;color:var(--muted);cursor:pointer;font-size:1rem;}
|
|
|
|
|
.record-list{padding:8px;}
|
|
|
|
|
.record-item{
|
|
|
|
|
padding:9px 10px;border-radius:6px;border:1px solid var(--border);
|
|
|
|
|
margin-bottom:6px;transition:background .15s;
|
|
|
|
|
display:flex;justify-content:space-between;align-items:flex-start;gap:6px;
|
|
|
|
|
}
|
|
|
|
|
.record-item:hover{background:var(--surface2);}
|
|
|
|
|
.record-item .info{flex:1;cursor:pointer;}
|
|
|
|
|
.record-item .name{font-size:.82rem;font-weight:600;}
|
|
|
|
|
.record-item .sub{font-size:.72rem;color:var(--muted);margin-top:2px;}
|
|
|
|
|
.record-actions{display:flex;gap:4px;flex-shrink:0;align-items:flex-start;}
|
|
|
|
|
.btn-icon{
|
|
|
|
|
background:var(--surface2);border:1px solid var(--border);
|
|
|
|
|
color:var(--muted);cursor:pointer;border-radius:5px;
|
|
|
|
|
padding:4px 7px;font-size:.72rem;transition:all .15s;
|
|
|
|
|
}
|
|
|
|
|
.btn-icon:hover{border-color:var(--accent);color:var(--accent);}
|
|
|
|
|
.btn-icon.danger:hover{border-color:var(--danger);color:var(--danger);}
|
|
|
|
|
|
|
|
|
|
.badge{font-size:.65rem;padding:2px 7px;border-radius:10px;font-weight:700;white-space:nowrap;}
|
|
|
|
|
.badge-green {background:#1a4a30;color:var(--accent2);}
|
|
|
|
|
.badge-red {background:#4a1a1a;color:var(--danger);}
|
|
|
|
|
.badge-blue {background:#1a2e4a;color:var(--accent);}
|
|
|
|
|
.badge-yellow{background:#4a3a10;color:var(--warn);}
|
|
|
|
|
.badge-nat {background:#1a3a4a;color:#7fd4ff;}
|
|
|
|
|
.badge-prov {background:#2e1a4a;color:#c07fff;}
|
|
|
|
|
.badge-kab {background:#1a4a2a;color:#7fffc0;}
|
|
|
|
|
|
|
|
|
|
/* Modal */
|
|
|
|
|
.modal-overlay{
|
|
|
|
|
position:fixed;inset:0;background:rgba(0,0,0,.6);
|
|
|
|
|
z-index:9000;display:none;align-items:center;justify-content:center;
|
|
|
|
|
}
|
|
|
|
|
.modal-overlay.open{display:flex;}
|
|
|
|
|
.modal{
|
|
|
|
|
background:var(--surface);border:1px solid var(--border);
|
|
|
|
|
border-radius:var(--radius);box-shadow:var(--shadow);width:360px;max-width:95vw;
|
|
|
|
|
}
|
|
|
|
|
.modal-header{
|
|
|
|
|
padding:14px 16px;border-bottom:1px solid var(--border);
|
|
|
|
|
display:flex;justify-content:space-between;align-items:center;
|
|
|
|
|
}
|
|
|
|
|
.modal-header h3{font-size:.9rem;font-weight:700;}
|
|
|
|
|
.modal-close{background:none;border:none;color:var(--muted);cursor:pointer;font-size:1.1rem;}
|
|
|
|
|
.modal-body{padding:16px;}
|
|
|
|
|
.field{margin-bottom:13px;}
|
|
|
|
|
.field label{
|
|
|
|
|
display:block;font-size:.72rem;color:var(--muted);margin-bottom:4px;
|
|
|
|
|
font-weight:600;text-transform:uppercase;letter-spacing:.05em;
|
|
|
|
|
}
|
|
|
|
|
.field input,.field select{
|
|
|
|
|
width:100%;padding:8px 10px;border-radius:6px;border:1px solid var(--border);
|
|
|
|
|
background:var(--surface2);color:var(--text);font-size:.85rem;outline:none;
|
|
|
|
|
}
|
|
|
|
|
.field input:focus,.field select:focus{border-color:var(--accent);}
|
|
|
|
|
.radio-group{display:flex;gap:12px;}
|
|
|
|
|
.radio-group label{
|
|
|
|
|
color:var(--text);font-size:.83rem;display:flex;align-items:center;gap:5px;
|
|
|
|
|
text-transform:none;letter-spacing:0;font-weight:400;
|
|
|
|
|
}
|
|
|
|
|
.field-info{
|
|
|
|
|
font-size:.75rem;color:var(--accent2);background:var(--surface2);
|
|
|
|
|
padding:7px 10px;border-radius:5px;border:1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
/* redraw hint box */
|
|
|
|
|
.redraw-hint{
|
|
|
|
|
font-size:.72rem;color:var(--warn);background:#2a2010;
|
|
|
|
|
padding:7px 10px;border-radius:5px;border:1px solid #4a3a10;
|
|
|
|
|
margin-top:6px;display:none;
|
|
|
|
|
}
|
|
|
|
|
.modal-footer{
|
|
|
|
|
padding:12px 16px;border-top:1px solid var(--border);
|
|
|
|
|
display:flex;gap:8px;justify-content:flex-end;
|
|
|
|
|
}
|
|
|
|
|
.btn{padding:7px 16px;border-radius:6px;border:none;cursor:pointer;font-size:.82rem;font-weight:600;transition:opacity .15s;}
|
|
|
|
|
.btn:hover{opacity:.85;}
|
|
|
|
|
.btn-primary{background:var(--accent);color:#fff;}
|
|
|
|
|
.btn-danger {background:var(--danger);color:#fff;}
|
|
|
|
|
.btn-muted {background:var(--surface2);color:var(--muted);border:1px solid var(--border);}
|
|
|
|
|
|
|
|
|
|
/* Toast */
|
|
|
|
|
#toast{
|
|
|
|
|
position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(60px);
|
|
|
|
|
background:var(--surface);border:1px solid var(--border);color:var(--text);
|
|
|
|
|
padding:10px 22px;border-radius:20px;font-size:.82rem;box-shadow:var(--shadow);
|
|
|
|
|
z-index:9999;transition:transform .3s;pointer-events:none;
|
|
|
|
|
}
|
|
|
|
|
#toast.show{transform:translateX(-50%) translateY(0);}
|
|
|
|
|
#toast.success{border-color:var(--accent2);color:var(--accent2);}
|
|
|
|
|
#toast.error {border-color:var(--danger);color:var(--danger);}
|
|
|
|
|
|
|
|
|
|
.leaflet-draw-toolbar a{background-color:var(--surface)!important;color:var(--text)!important;}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div id="app">
|
|
|
|
|
|
|
|
|
|
<header>
|
|
|
|
|
<h1>🗺 <span>WebGIS</span> Spasial</h1>
|
|
|
|
|
<div class="tabs">
|
|
|
|
|
<button class="tab-btn active" data-layer="spbu">📍 SPBU</button>
|
|
|
|
|
<button class="tab-btn" data-layer="jalan">🛣 Jalan</button>
|
|
|
|
|
<button class="tab-btn" data-layer="parsil">🏡 Parsil</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="mode-badge">Mode: Lihat</div>
|
|
|
|
|
<button class="btn btn-muted" id="btn-sidebar">☰ Daftar Data</button>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div id="map"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Sidebar -->
|
|
|
|
|
<div id="sidebar">
|
|
|
|
|
<div class="sidebar-header">
|
|
|
|
|
<h3 id="sidebar-title">Data</h3>
|
|
|
|
|
<button class="sidebar-close" id="sidebar-close-btn">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="record-list" id="record-list"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Modal SPBU -->
|
|
|
|
|
<div class="modal-overlay" id="modal-spbu">
|
|
|
|
|
<div class="modal">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h3 id="spbu-modal-title">Tambah SPBU</h3>
|
|
|
|
|
<button class="modal-close" data-close="modal-spbu">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<input type="hidden" id="spbu-id" />
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Nama SPBU</label>
|
|
|
|
|
<input type="text" id="spbu-nama" placeholder="cth. SPBU 64.762.01" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Nomor WhatsApp</label>
|
|
|
|
|
<input type="text" id="spbu-wa" placeholder="cth. 08123456789" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Buka 24 Jam?</label>
|
|
|
|
|
<div class="radio-group">
|
|
|
|
|
<label><input type="radio" name="buka24" value="1" /> Ya</label>
|
|
|
|
|
<label><input type="radio" name="buka24" value="0" checked /> Tidak</label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<div class="field-info" id="spbu-coords-info">—</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button class="btn btn-danger" id="btn-spbu-delete" style="display:none;margin-right:auto">🗑 Hapus</button>
|
|
|
|
|
<button class="btn btn-muted" data-close="modal-spbu">Batal</button>
|
|
|
|
|
<button class="btn btn-primary" id="btn-spbu-save">💾 Simpan</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Modal Jalan -->
|
|
|
|
|
<div class="modal-overlay" id="modal-jalan">
|
|
|
|
|
<div class="modal">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h3 id="jalan-modal-title">Tambah Data Jalan</h3>
|
|
|
|
|
<button class="modal-close" data-close="modal-jalan">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<input type="hidden" id="jalan-id" />
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Nama Jalan</label>
|
|
|
|
|
<input type="text" id="jalan-nama" placeholder="cth. Jl. Ahmad Yani" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Status Jalan</label>
|
|
|
|
|
<select id="jalan-status">
|
|
|
|
|
<option value="nasional">Jalan Nasional</option>
|
|
|
|
|
<option value="provinsi">Jalan Provinsi</option>
|
|
|
|
|
<option value="kabupaten">Jalan Kabupaten</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Panjang Jalan (otomatis)</label>
|
|
|
|
|
<div class="field-info" id="jalan-panjang-info">—</div>
|
|
|
|
|
<div class="redraw-hint" id="jalan-redraw-hint">
|
|
|
|
|
✏️ Tutup form ini, gambar ulang garis di peta, lalu simpan untuk mengubah geometri.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button class="btn btn-danger" id="btn-jalan-delete" style="display:none;margin-right:auto">🗑 Hapus</button>
|
|
|
|
|
<button class="btn btn-muted" data-close="modal-jalan">Batal</button>
|
|
|
|
|
<button class="btn btn-primary" id="btn-jalan-save">💾 Simpan</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Modal Parsil -->
|
|
|
|
|
<div class="modal-overlay" id="modal-parsil">
|
|
|
|
|
<div class="modal">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h3 id="parsil-modal-title">Tambah Parsil Tanah</h3>
|
|
|
|
|
<button class="modal-close" data-close="modal-parsil">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<input type="hidden" id="parsil-id" />
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Nama Pemilik</label>
|
|
|
|
|
<input type="text" id="parsil-nama" placeholder="cth. Budi Santoso" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Status Sertifikat</label>
|
|
|
|
|
<select id="parsil-status">
|
|
|
|
|
<option value="SHM">SHM — Sertifikat 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="field">
|
|
|
|
|
<label>Luas Tanah (otomatis)</label>
|
|
|
|
|
<div class="field-info" id="parsil-luas-info">—</div>
|
|
|
|
|
<div class="redraw-hint" id="parsil-redraw-hint">
|
|
|
|
|
✏️ Tutup form ini, gambar ulang polygon di peta, lalu simpan untuk mengubah geometri.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button class="btn btn-danger" id="btn-parsil-delete" style="display:none;margin-right:auto">🗑 Hapus</button>
|
|
|
|
|
<button class="btn btn-muted" data-close="modal-parsil">Batal</button>
|
|
|
|
|
<button class="btn btn-primary" id="btn-parsil-save">💾 Simpan</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="toast"></div>
|
|
|
|
|
</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>
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
const API = { spbu:'api/spbu.php', jalan:'api/jalan.php', parsil:'api/parsil.php' };
|
|
|
|
|
|
|
|
|
|
// ── Map ─────────────────────────────────────────────────
|
|
|
|
|
const map = L.map('map').setView([-0.0263, 109.3425], 13);
|
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
|
|
|
|
|
attribution:'© OpenStreetMap contributors', maxZoom:19
|
|
|
|
|
}).addTo(map);
|
|
|
|
|
|
|
|
|
|
const layers = { spbu:L.layerGroup().addTo(map), jalan:L.layerGroup().addTo(map), parsil:L.layerGroup().addTo(map) };
|
|
|
|
|
|
|
|
|
|
// ── State ────────────────────────────────────────────────
|
|
|
|
|
let activeLayer = 'spbu';
|
|
|
|
|
let pendingLatLng = null;
|
|
|
|
|
let pendingGeojson= null; // geometri dari draw / dari data existing saat edit
|
|
|
|
|
let pendingLength = 0;
|
|
|
|
|
let pendingArea = 0;
|
|
|
|
|
let editingMarker = null;
|
|
|
|
|
let isEditMode = false; // true = edit atribut saja, false = tambah baru / redraw
|
|
|
|
|
|
|
|
|
|
// ── Helpers ──────────────────────────────────────────────
|
|
|
|
|
const JALAN_COLOR = { nasional:'#7fd4ff', provinsi:'#c07fff', kabupaten:'#7fffc0' };
|
|
|
|
|
const PARSIL_COLOR = { SHM:'#ffb84d', HGB:'#3b9eff', HGU:'#56d4a0', HP:'#ff9fdf' };
|
|
|
|
|
|
|
|
|
|
function spbuIcon(buka24){
|
|
|
|
|
const is24 = Number(buka24) === 1;
|
|
|
|
|
const c = is24 ? '#56d4a0' : '#ff5c5c';
|
|
|
|
|
return L.divIcon({
|
|
|
|
|
className:'',
|
|
|
|
|
html:`<svg width="22" height="30" viewBox="0 0 22 30" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
<path d="M11 0C5 0 0 5 0 11c0 8 11 19 11 19s11-11 11-19C22 5 17 0 11 0z"
|
|
|
|
|
fill="${c}" stroke="#fff" stroke-width="1.5"/>
|
|
|
|
|
<circle cx="11" cy="11" r="5" fill="#fff" fill-opacity=".85"/>
|
|
|
|
|
</svg>`,
|
|
|
|
|
iconSize:[22,30], iconAnchor:[11,30], popupAnchor:[0,-32]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toast(msg, type='success'){
|
|
|
|
|
const el=document.getElementById('toast');
|
|
|
|
|
el.textContent=msg; el.className=`show ${type}`;
|
|
|
|
|
setTimeout(()=>el.className='',3000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openModal(id) { document.getElementById(id).classList.add('open'); }
|
|
|
|
|
function closeModal(id){ document.getElementById(id).classList.remove('open'); }
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('[data-close]').forEach(b=>
|
|
|
|
|
b.addEventListener('click',()=>closeModal(b.dataset.close))
|
|
|
|
|
);
|
|
|
|
|
document.querySelectorAll('.modal-overlay').forEach(m=>
|
|
|
|
|
m.addEventListener('click',e=>{ if(e.target===m) closeModal(m.id); })
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const modeBadge = document.getElementById('mode-badge');
|
|
|
|
|
function setMode(label, active=false){
|
|
|
|
|
modeBadge.textContent=`Mode: ${label}`;
|
|
|
|
|
modeBadge.classList.toggle('active',active);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Leaflet.Draw ─────────────────────────────────────────
|
|
|
|
|
const drawnItems = new L.FeatureGroup();
|
|
|
|
|
map.addLayer(drawnItems);
|
|
|
|
|
|
|
|
|
|
const drawControl = new L.Control.Draw({
|
|
|
|
|
edit:{ featureGroup:drawnItems, edit:false, remove:false },
|
|
|
|
|
draw:{
|
|
|
|
|
marker:false, circle:false, rectangle:false, circlemarker:false,
|
|
|
|
|
polyline:{ shapeOptions:{ color:'#3b9eff', weight:4 } },
|
|
|
|
|
polygon :{ shapeOptions:{ color:'#ffb84d', weight:2, fillOpacity:.35 } },
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function updateDrawControl(){
|
|
|
|
|
try{ map.removeControl(drawControl); }catch(e){}
|
|
|
|
|
if(activeLayer==='jalan'){
|
|
|
|
|
drawControl.options.draw.polyline = { shapeOptions:{ color:'#3b9eff', weight:4 } };
|
|
|
|
|
drawControl.options.draw.polygon = false;
|
|
|
|
|
map.addControl(drawControl);
|
|
|
|
|
} else if(activeLayer==='parsil'){
|
|
|
|
|
drawControl.options.draw.polyline = false;
|
|
|
|
|
drawControl.options.draw.polygon = { shapeOptions:{ color:'#ffb84d', weight:2, fillOpacity:.35 } };
|
|
|
|
|
map.addControl(drawControl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
map.on(L.Draw.Event.DRAWSTART, ()=>setMode('Menggambar…',true));
|
|
|
|
|
map.on(L.Draw.Event.DRAWSTOP, ()=>setMode('Lihat'));
|
|
|
|
|
|
|
|
|
|
map.on(L.Draw.Event.CREATED, function(e){
|
|
|
|
|
drawnItems.clearLayers();
|
|
|
|
|
drawnItems.addLayer(e.layer);
|
|
|
|
|
|
|
|
|
|
if(e.layerType==='polyline'){
|
|
|
|
|
const ll = e.layer.getLatLngs();
|
|
|
|
|
const m = calcPolylineLength(ll);
|
|
|
|
|
pendingGeojson = JSON.stringify(e.layer.toGeoJSON());
|
|
|
|
|
pendingLength = m;
|
|
|
|
|
isEditMode = false;
|
|
|
|
|
|
|
|
|
|
document.getElementById('jalan-id').value = '';
|
|
|
|
|
document.getElementById('jalan-nama').value = '';
|
|
|
|
|
document.getElementById('jalan-panjang-info').textContent =
|
|
|
|
|
`${m.toFixed(1)} m (${(m/1000).toFixed(3)} km)`;
|
|
|
|
|
document.getElementById('jalan-redraw-hint').style.display='none';
|
|
|
|
|
document.getElementById('jalan-modal-title').textContent='Tambah Data Jalan';
|
|
|
|
|
document.getElementById('btn-jalan-delete').style.display='none';
|
|
|
|
|
openModal('modal-jalan');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(e.layerType==='polygon'){
|
|
|
|
|
const ll = e.layer.getLatLngs()[0];
|
|
|
|
|
const a = calcPolygonArea(ll);
|
|
|
|
|
pendingGeojson = JSON.stringify(e.layer.toGeoJSON());
|
|
|
|
|
pendingArea = a;
|
|
|
|
|
isEditMode = false;
|
|
|
|
|
|
|
|
|
|
document.getElementById('parsil-id').value = '';
|
|
|
|
|
document.getElementById('parsil-nama').value = '';
|
|
|
|
|
document.getElementById('parsil-luas-info').textContent =
|
|
|
|
|
`${a.toFixed(2)} m² (${(a/10000).toFixed(4)} ha)`;
|
|
|
|
|
document.getElementById('parsil-redraw-hint').style.display='none';
|
|
|
|
|
document.getElementById('parsil-modal-title').textContent='Tambah Parsil Tanah';
|
|
|
|
|
document.getElementById('btn-parsil-delete').style.display='none';
|
|
|
|
|
openModal('modal-parsil');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Geometry calc ────────────────────────────────────────
|
|
|
|
|
function calcPolylineLength(latlngs){
|
|
|
|
|
const flat = latlngs.flat ? latlngs.flat(Infinity) : latlngs;
|
|
|
|
|
let t=0;
|
|
|
|
|
for(let i=0;i<flat.length-1;i++) t+=flat[i].distanceTo(flat[i+1]);
|
|
|
|
|
return t;
|
|
|
|
|
}
|
|
|
|
|
function calcPolygonArea(latlngs){
|
|
|
|
|
const R=6371000;
|
|
|
|
|
const pts=latlngs.map(ll=>({lat:ll.lat*Math.PI/180,lng:ll.lng*Math.PI/180}));
|
|
|
|
|
let a=0,n=pts.length;
|
|
|
|
|
for(let i=0;i<n;i++){const j=(i+1)%n;a+=pts[i].lng*pts[j].lat-pts[j].lng*pts[i].lat;}
|
|
|
|
|
return Math.abs(a/2)*R*R;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Tabs ─────────────────────────────────────────────────
|
|
|
|
|
document.querySelectorAll('.tab-btn').forEach(btn=>{
|
|
|
|
|
btn.addEventListener('click',()=>{
|
|
|
|
|
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
|
|
|
|
btn.classList.add('active');
|
|
|
|
|
activeLayer=btn.dataset.layer;
|
|
|
|
|
updateDrawControl();
|
|
|
|
|
setMode('Lihat');
|
|
|
|
|
if(document.getElementById('sidebar').classList.contains('open')) renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Map click → new SPBU ─────────────────────────────────
|
|
|
|
|
map.on('click',function(e){
|
|
|
|
|
if(activeLayer!=='spbu') return;
|
|
|
|
|
pendingLatLng=e.latlng;
|
|
|
|
|
document.getElementById('spbu-id').value='';
|
|
|
|
|
document.getElementById('spbu-nama').value='';
|
|
|
|
|
document.getElementById('spbu-wa').value='';
|
|
|
|
|
document.querySelectorAll('input[name="buka24"]').forEach(r=>r.checked=r.value==='0');
|
|
|
|
|
document.getElementById('spbu-coords-info').textContent=
|
|
|
|
|
`Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}`;
|
|
|
|
|
document.getElementById('spbu-modal-title').textContent='Tambah SPBU Baru';
|
|
|
|
|
document.getElementById('btn-spbu-delete').style.display='none';
|
|
|
|
|
openModal('modal-spbu');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
// SPBU CRUD
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
async function loadSPBU(){
|
|
|
|
|
layers.spbu.clearLayers();
|
|
|
|
|
const data=await apiFetch(API.spbu,'GET');
|
|
|
|
|
data.forEach(row=>addSpbuMarker(row));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addSpbuMarker(row){
|
|
|
|
|
const marker=L.marker([row.latitude,row.longitude],{
|
|
|
|
|
icon:spbuIcon(row.buka_24jam), draggable:true
|
|
|
|
|
}).addTo(layers.spbu);
|
|
|
|
|
marker._spbuData=row;
|
|
|
|
|
marker.bindTooltip(row.nama_spbu,{permanent:false,direction:'top'});
|
|
|
|
|
marker.on('click',()=>openEditSpbu(marker));
|
|
|
|
|
marker.on('dragend',()=>{
|
|
|
|
|
const ll=marker.getLatLng();
|
|
|
|
|
marker._spbuData.latitude =ll.lat;
|
|
|
|
|
marker._spbuData.longitude=ll.lng;
|
|
|
|
|
apiFetch(API.spbu+'?id='+row.id,'PUT',marker._spbuData)
|
|
|
|
|
.then(()=>toast('Posisi SPBU diperbarui'));
|
|
|
|
|
});
|
|
|
|
|
return marker;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEditSpbu(marker){
|
|
|
|
|
const d=marker._spbuData;
|
|
|
|
|
editingMarker=marker;
|
|
|
|
|
document.getElementById('spbu-id').value =d.id;
|
|
|
|
|
document.getElementById('spbu-nama').value =d.nama_spbu;
|
|
|
|
|
document.getElementById('spbu-wa').value =d.nomor_wa;
|
|
|
|
|
document.querySelectorAll('input[name="buka24"]').forEach(r=>r.checked=r.value==d.buka_24jam);
|
|
|
|
|
document.getElementById('spbu-coords-info').textContent=
|
|
|
|
|
`Lat: ${(+d.latitude).toFixed(6)}, Lng: ${(+d.longitude).toFixed(6)}`;
|
|
|
|
|
document.getElementById('spbu-modal-title').textContent='✏️ Edit SPBU';
|
|
|
|
|
document.getElementById('btn-spbu-delete').style.display='inline-block';
|
|
|
|
|
openModal('modal-spbu');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-spbu-save').addEventListener('click',async()=>{
|
|
|
|
|
const id =document.getElementById('spbu-id').value;
|
|
|
|
|
const nama =document.getElementById('spbu-nama').value.trim();
|
|
|
|
|
const wa =document.getElementById('spbu-wa').value.trim();
|
|
|
|
|
const b24 =+document.querySelector('input[name="buka24"]:checked').value;
|
|
|
|
|
if(!nama||!wa){ toast('Nama dan nomor WA wajib diisi','error'); return; }
|
|
|
|
|
|
|
|
|
|
let lat,lng;
|
|
|
|
|
if(id){ lat=editingMarker._spbuData.latitude; lng=editingMarker._spbuData.longitude; }
|
|
|
|
|
else { lat=pendingLatLng.lat; lng=pendingLatLng.lng; }
|
|
|
|
|
|
|
|
|
|
const body={id:+id||undefined,nama_spbu:nama,nomor_wa:wa,buka_24jam:b24,latitude:lat,longitude:lng};
|
|
|
|
|
|
|
|
|
|
if(id){
|
|
|
|
|
await apiFetch(API.spbu+'?id='+id,'PUT',body);
|
|
|
|
|
editingMarker._spbuData={...editingMarker._spbuData,...body};
|
|
|
|
|
editingMarker.setIcon(spbuIcon(b24));
|
|
|
|
|
editingMarker.bindTooltip(nama,{permanent:false,direction:'top'});
|
|
|
|
|
toast('Data SPBU diperbarui ✓');
|
|
|
|
|
} else {
|
|
|
|
|
const res=await apiFetch(API.spbu,'POST',body);
|
|
|
|
|
body.id=res.id;
|
|
|
|
|
addSpbuMarker(body);
|
|
|
|
|
toast('SPBU berhasil ditambahkan ✓');
|
|
|
|
|
}
|
|
|
|
|
closeModal('modal-spbu');
|
|
|
|
|
if(document.getElementById('sidebar').classList.contains('open')) renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-spbu-delete').addEventListener('click',async()=>{
|
|
|
|
|
const id=document.getElementById('spbu-id').value;
|
|
|
|
|
if(!id) return;
|
|
|
|
|
if(!confirm('Hapus data SPBU ini?')) return;
|
|
|
|
|
await apiFetch(API.spbu+'?id='+id,'DELETE');
|
|
|
|
|
layers.spbu.eachLayer(m=>{ if(m._spbuData?.id==id) layers.spbu.removeLayer(m); });
|
|
|
|
|
closeModal('modal-spbu');
|
|
|
|
|
toast('SPBU dihapus','error');
|
|
|
|
|
if(document.getElementById('sidebar').classList.contains('open')) renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
// JALAN CRUD
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
async function loadJalan(){
|
|
|
|
|
layers.jalan.clearLayers();
|
|
|
|
|
const data=await apiFetch(API.jalan,'GET');
|
|
|
|
|
data.forEach(row=>addJalanLayer(row));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addJalanLayer(row){
|
|
|
|
|
const gj =JSON.parse(row.geojson);
|
|
|
|
|
const col=JALAN_COLOR[row.status_jalan]||'#fff';
|
|
|
|
|
const line=L.geoJSON(gj,{style:{color:col,weight:4,opacity:.85}}).addTo(layers.jalan);
|
|
|
|
|
line._jalanData=row;
|
|
|
|
|
line.bindTooltip(
|
|
|
|
|
`<b>${row.nama_jalan}</b><br>${row.status_jalan} · ${(+row.panjang_m).toFixed(1)} m`,
|
|
|
|
|
{sticky:true}
|
|
|
|
|
);
|
|
|
|
|
line.on('click',()=>openEditJalan(line));
|
|
|
|
|
return line;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEditJalan(line){
|
|
|
|
|
const d=line._jalanData;
|
|
|
|
|
// Saat edit: pakai geojson yang sudah ada, tidak perlu redraw
|
|
|
|
|
pendingGeojson=d.geojson;
|
|
|
|
|
pendingLength =+d.panjang_m;
|
|
|
|
|
isEditMode=true;
|
|
|
|
|
|
|
|
|
|
document.getElementById('jalan-id').value =d.id;
|
|
|
|
|
document.getElementById('jalan-nama').value =d.nama_jalan;
|
|
|
|
|
document.getElementById('jalan-status').value=d.status_jalan;
|
|
|
|
|
document.getElementById('jalan-panjang-info').textContent=
|
|
|
|
|
`${(+d.panjang_m).toFixed(1)} m (${(+d.panjang_m/1000).toFixed(3)} km)`;
|
|
|
|
|
document.getElementById('jalan-redraw-hint').style.display='block';
|
|
|
|
|
document.getElementById('jalan-modal-title').textContent='✏️ Edit Data Jalan';
|
|
|
|
|
document.getElementById('btn-jalan-delete').style.display='inline-block';
|
|
|
|
|
openEditJalan._layer=line;
|
|
|
|
|
openModal('modal-jalan');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-jalan-save').addEventListener('click',async()=>{
|
|
|
|
|
const id =document.getElementById('jalan-id').value;
|
|
|
|
|
const nama =document.getElementById('jalan-nama').value.trim();
|
|
|
|
|
const status=document.getElementById('jalan-status').value;
|
|
|
|
|
if(!nama){ toast('Nama jalan wajib diisi','error'); return; }
|
|
|
|
|
// Saat mode tambah baru (bukan edit), wajib ada geojson dari draw
|
|
|
|
|
if(!id && !pendingGeojson){ toast('Gambar ruas jalan terlebih dahulu','error'); return; }
|
|
|
|
|
|
|
|
|
|
const body={
|
|
|
|
|
id:+id||undefined, nama_jalan:nama, status_jalan:status,
|
|
|
|
|
panjang_m:pendingLength, geojson:pendingGeojson
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if(id){
|
|
|
|
|
await apiFetch(API.jalan+'?id='+id,'PUT',body);
|
|
|
|
|
const old=openEditJalan._layer;
|
|
|
|
|
if(old) layers.jalan.removeLayer(old);
|
|
|
|
|
// Merge geojson lama jika tidak digambar ulang
|
|
|
|
|
body.geojson=pendingGeojson;
|
|
|
|
|
addJalanLayer(body);
|
|
|
|
|
toast('Data jalan diperbarui ✓');
|
|
|
|
|
} else {
|
|
|
|
|
const res=await apiFetch(API.jalan,'POST',body);
|
|
|
|
|
body.id=res.id;
|
|
|
|
|
addJalanLayer(body);
|
|
|
|
|
toast('Jalan berhasil ditambahkan ✓');
|
|
|
|
|
}
|
|
|
|
|
drawnItems.clearLayers();
|
|
|
|
|
pendingGeojson=null;
|
|
|
|
|
closeModal('modal-jalan');
|
|
|
|
|
if(document.getElementById('sidebar').classList.contains('open')) renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-jalan-delete').addEventListener('click',async()=>{
|
|
|
|
|
const id=document.getElementById('jalan-id').value;
|
|
|
|
|
if(!id) return;
|
|
|
|
|
if(!confirm('Hapus data jalan ini?')) return;
|
|
|
|
|
await apiFetch(API.jalan+'?id='+id,'DELETE');
|
|
|
|
|
const old=openEditJalan._layer;
|
|
|
|
|
if(old) layers.jalan.removeLayer(old);
|
|
|
|
|
closeModal('modal-jalan');
|
|
|
|
|
toast('Jalan dihapus','error');
|
|
|
|
|
if(document.getElementById('sidebar').classList.contains('open')) renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
// PARSIL CRUD
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
async function loadParsil(){
|
|
|
|
|
layers.parsil.clearLayers();
|
|
|
|
|
const data=await apiFetch(API.parsil,'GET');
|
|
|
|
|
data.forEach(row=>addParsilLayer(row));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addParsilLayer(row){
|
|
|
|
|
const gj =JSON.parse(row.geojson);
|
|
|
|
|
const col=PARSIL_COLOR[row.status_sertifikat]||'#fff';
|
|
|
|
|
const poly=L.geoJSON(gj,{style:{color:col,weight:2,fillColor:col,fillOpacity:.35}}).addTo(layers.parsil);
|
|
|
|
|
poly._parsilData=row;
|
|
|
|
|
poly.bindTooltip(
|
|
|
|
|
`<b>${row.nama_pemilik}</b><br>${row.status_sertifikat} · ${(+row.luas_m2).toFixed(2)} m²`,
|
|
|
|
|
{sticky:true}
|
|
|
|
|
);
|
|
|
|
|
poly.on('click',()=>openEditParsil(poly));
|
|
|
|
|
return poly;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEditParsil(poly){
|
|
|
|
|
const d=poly._parsilData;
|
|
|
|
|
pendingGeojson=d.geojson;
|
|
|
|
|
pendingArea =+d.luas_m2;
|
|
|
|
|
isEditMode=true;
|
|
|
|
|
|
|
|
|
|
document.getElementById('parsil-id').value =d.id;
|
|
|
|
|
document.getElementById('parsil-nama').value =d.nama_pemilik;
|
|
|
|
|
document.getElementById('parsil-status').value=d.status_sertifikat;
|
|
|
|
|
document.getElementById('parsil-luas-info').textContent=
|
|
|
|
|
`${(+d.luas_m2).toFixed(2)} m² (${(+d.luas_m2/10000).toFixed(4)} ha)`;
|
|
|
|
|
document.getElementById('parsil-redraw-hint').style.display='block';
|
|
|
|
|
document.getElementById('parsil-modal-title').textContent='✏️ Edit Parsil Tanah';
|
|
|
|
|
document.getElementById('btn-parsil-delete').style.display='inline-block';
|
|
|
|
|
openEditParsil._layer=poly;
|
|
|
|
|
openModal('modal-parsil');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-parsil-save').addEventListener('click',async()=>{
|
|
|
|
|
const id =document.getElementById('parsil-id').value;
|
|
|
|
|
const nama =document.getElementById('parsil-nama').value.trim();
|
|
|
|
|
const status=document.getElementById('parsil-status').value;
|
|
|
|
|
if(!nama){ toast('Nama pemilik wajib diisi','error'); return; }
|
|
|
|
|
if(!id && !pendingGeojson){ toast('Gambar polygon terlebih dahulu','error'); return; }
|
|
|
|
|
|
|
|
|
|
const body={
|
|
|
|
|
id:+id||undefined, nama_pemilik:nama, status_sertifikat:status,
|
|
|
|
|
luas_m2:pendingArea, geojson:pendingGeojson
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if(id){
|
|
|
|
|
await apiFetch(API.parsil+'?id='+id,'PUT',body);
|
|
|
|
|
const old=openEditParsil._layer;
|
|
|
|
|
if(old) layers.parsil.removeLayer(old);
|
|
|
|
|
addParsilLayer(body);
|
|
|
|
|
toast('Parsil diperbarui ✓');
|
|
|
|
|
} else {
|
|
|
|
|
const res=await apiFetch(API.parsil,'POST',body);
|
|
|
|
|
body.id=res.id;
|
|
|
|
|
addParsilLayer(body);
|
|
|
|
|
toast('Parsil berhasil ditambahkan ✓');
|
|
|
|
|
}
|
|
|
|
|
drawnItems.clearLayers();
|
|
|
|
|
pendingGeojson=null;
|
|
|
|
|
closeModal('modal-parsil');
|
|
|
|
|
if(document.getElementById('sidebar').classList.contains('open')) renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-parsil-delete').addEventListener('click',async()=>{
|
|
|
|
|
const id=document.getElementById('parsil-id').value;
|
|
|
|
|
if(!id) return;
|
|
|
|
|
if(!confirm('Hapus data parsil ini?')) return;
|
|
|
|
|
await apiFetch(API.parsil+'?id='+id,'DELETE');
|
|
|
|
|
const old=openEditParsil._layer;
|
|
|
|
|
if(old) layers.parsil.removeLayer(old);
|
|
|
|
|
closeModal('modal-parsil');
|
|
|
|
|
toast('Parsil dihapus','error');
|
|
|
|
|
if(document.getElementById('sidebar').classList.contains('open')) renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
// SIDEBAR
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
document.getElementById('btn-sidebar').addEventListener('click',()=>{
|
|
|
|
|
document.getElementById('sidebar').classList.toggle('open');
|
|
|
|
|
renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
document.getElementById('sidebar-close-btn').addEventListener('click',()=>
|
|
|
|
|
document.getElementById('sidebar').classList.remove('open')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
async function renderSidebar(){
|
|
|
|
|
const list =document.getElementById('record-list');
|
|
|
|
|
const title=document.getElementById('sidebar-title');
|
|
|
|
|
list.innerHTML='<p style="padding:10px;color:var(--muted);font-size:.8rem">Memuat…</p>';
|
|
|
|
|
|
|
|
|
|
if(activeLayer==='spbu'){
|
|
|
|
|
title.textContent='Daftar SPBU';
|
|
|
|
|
const data=await apiFetch(API.spbu,'GET');
|
|
|
|
|
list.innerHTML='';
|
|
|
|
|
data.forEach(row=>{
|
|
|
|
|
const div=document.createElement('div');
|
|
|
|
|
div.className='record-item';
|
|
|
|
|
div.innerHTML=`
|
|
|
|
|
<div class="info">
|
|
|
|
|
<div class="name">${row.nama_spbu}</div>
|
|
|
|
|
<div class="sub">📞 ${row.nomor_wa}</div>
|
|
|
|
|
<div class="sub">📍 ${(+row.latitude).toFixed(5)}, ${(+row.longitude).toFixed(5)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:5px">
|
|
|
|
|
<span class="badge ${Number(row.buka_24jam) === 1 ? 'badge-green' : 'badge-red'}">
|
|
|
|
|
${Number(row.buka_24jam) === 1 ? '24 Jam' : 'Terbatas'}
|
|
|
|
|
</span>
|
|
|
|
|
<div class="record-actions">
|
|
|
|
|
<button class="btn-icon" data-act="zoom-spbu" data-id="${row.id}"
|
|
|
|
|
data-lat="${row.latitude}" data-lng="${row.longitude}">🔍</button>
|
|
|
|
|
<button class="btn-icon" data-act="edit-spbu" data-id="${row.id}">✏️</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
list.appendChild(div);
|
|
|
|
|
});
|
|
|
|
|
if(!data.length) list.innerHTML='<p style="padding:10px;color:var(--muted);font-size:.8rem">Belum ada data SPBU.</p>';
|
|
|
|
|
|
|
|
|
|
} else if(activeLayer==='jalan'){
|
|
|
|
|
title.textContent='Daftar Jalan';
|
|
|
|
|
const data=await apiFetch(API.jalan,'GET');
|
|
|
|
|
list.innerHTML='';
|
|
|
|
|
const bc={nasional:'badge-nat',provinsi:'badge-prov',kabupaten:'badge-kab'};
|
|
|
|
|
data.forEach(row=>{
|
|
|
|
|
const div=document.createElement('div');
|
|
|
|
|
div.className='record-item';
|
|
|
|
|
div.innerHTML=`
|
|
|
|
|
<div class="info">
|
|
|
|
|
<div class="name">${row.nama_jalan}</div>
|
|
|
|
|
<div class="sub">📏 ${(+row.panjang_m).toFixed(1)} m</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:5px">
|
|
|
|
|
<span class="badge ${bc[row.status_jalan]||''}">${row.status_jalan}</span>
|
|
|
|
|
<div class="record-actions">
|
|
|
|
|
<button class="btn-icon" data-act="zoom-jalan" data-id="${row.id}">🔍</button>
|
|
|
|
|
<button class="btn-icon" data-act="edit-jalan" data-id="${row.id}">✏️</button>
|
|
|
|
|
<button class="btn-icon danger" data-act="del-jalan" data-id="${row.id}">🗑</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
list.appendChild(div);
|
|
|
|
|
});
|
|
|
|
|
if(!data.length) list.innerHTML='<p style="padding:10px;color:var(--muted);font-size:.8rem">Belum ada data jalan.</p>';
|
|
|
|
|
|
|
|
|
|
} else if(activeLayer==='parsil'){
|
|
|
|
|
title.textContent='Daftar Parsil Tanah';
|
|
|
|
|
const data=await apiFetch(API.parsil,'GET');
|
|
|
|
|
list.innerHTML='';
|
|
|
|
|
const bc={SHM:'badge-yellow',HGB:'badge-blue',HGU:'badge-green',HP:'badge-red'};
|
|
|
|
|
data.forEach(row=>{
|
|
|
|
|
const div=document.createElement('div');
|
|
|
|
|
div.className='record-item';
|
|
|
|
|
div.innerHTML=`
|
|
|
|
|
<div class="info">
|
|
|
|
|
<div class="name">${row.nama_pemilik}</div>
|
|
|
|
|
<div class="sub">📐 ${(+row.luas_m2).toFixed(2)} m²</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:5px">
|
|
|
|
|
<span class="badge ${bc[row.status_sertifikat]||''}">${row.status_sertifikat}</span>
|
|
|
|
|
<div class="record-actions">
|
|
|
|
|
<button class="btn-icon" data-act="zoom-parsil" data-id="${row.id}">🔍</button>
|
|
|
|
|
<button class="btn-icon" data-act="edit-parsil" data-id="${row.id}">✏️</button>
|
|
|
|
|
<button class="btn-icon danger" data-act="del-parsil" data-id="${row.id}">🗑</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
list.appendChild(div);
|
|
|
|
|
});
|
|
|
|
|
if(!data.length) list.innerHTML='<p style="padding:10px;color:var(--muted);font-size:.8rem">Belum ada data parsil.</p>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sidebar action delegate
|
|
|
|
|
list.onclick=function(e){
|
|
|
|
|
const btn=e.target.closest('[data-act]');
|
|
|
|
|
if(!btn) return;
|
|
|
|
|
const act=btn.dataset.act, id=btn.dataset.id;
|
|
|
|
|
|
|
|
|
|
if(act==='zoom-spbu'){
|
|
|
|
|
map.setView([+btn.dataset.lat,+btn.dataset.lng],17);
|
|
|
|
|
}
|
|
|
|
|
if(act==='edit-spbu'){
|
|
|
|
|
layers.spbu.eachLayer(m=>{ if(m._spbuData?.id==id) openEditSpbu(m); });
|
|
|
|
|
}
|
|
|
|
|
if(act==='zoom-jalan'){
|
|
|
|
|
layers.jalan.eachLayer(l=>{ if(l._jalanData?.id==id) map.fitBounds(l.getBounds(),{padding:[30,30]}); });
|
|
|
|
|
}
|
|
|
|
|
if(act==='edit-jalan'){
|
|
|
|
|
layers.jalan.eachLayer(l=>{ if(l._jalanData?.id==id) openEditJalan(l); });
|
|
|
|
|
}
|
|
|
|
|
if(act==='del-jalan'){
|
|
|
|
|
if(!confirm('Hapus jalan ini?')) return;
|
|
|
|
|
apiFetch(API.jalan+'?id='+id,'DELETE').then(()=>{
|
|
|
|
|
layers.jalan.eachLayer(l=>{ if(l._jalanData?.id==id) layers.jalan.removeLayer(l); });
|
|
|
|
|
toast('Jalan dihapus','error'); renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if(act==='zoom-parsil'){
|
|
|
|
|
layers.parsil.eachLayer(l=>{ if(l._parsilData?.id==id) map.fitBounds(l.getBounds(),{padding:[30,30]}); });
|
|
|
|
|
}
|
|
|
|
|
if(act==='edit-parsil'){
|
|
|
|
|
layers.parsil.eachLayer(l=>{ if(l._parsilData?.id==id) openEditParsil(l); });
|
|
|
|
|
}
|
|
|
|
|
if(act==='del-parsil'){
|
|
|
|
|
if(!confirm('Hapus parsil ini?')) return;
|
|
|
|
|
apiFetch(API.parsil+'?id='+id,'DELETE').then(()=>{
|
|
|
|
|
layers.parsil.eachLayer(l=>{ if(l._parsilData?.id==id) layers.parsil.removeLayer(l); });
|
|
|
|
|
toast('Parsil dihapus','error'); renderSidebar();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
// API helper
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
async function apiFetch(url,method='GET',body=null){
|
|
|
|
|
const opts={method,headers:{'Content-Type':'application/json'}};
|
|
|
|
|
if(body) opts.body=JSON.stringify(body);
|
|
|
|
|
try{
|
|
|
|
|
const res =await fetch(url,opts);
|
|
|
|
|
const data=await res.json();
|
|
|
|
|
if(data.error) throw new Error(data.error);
|
|
|
|
|
return data;
|
|
|
|
|
}catch(e){ toast('Error: '+e.message,'error'); return []; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
// LEGEND
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
const legend=L.control({position:'bottomleft'});
|
|
|
|
|
legend.onAdd=function(){
|
|
|
|
|
const d=L.DomUtil.create('div');
|
|
|
|
|
d.style.cssText='background:rgba(26,37,53,.93);border:1px solid #2e3f57;border-radius:8px;padding:10px 14px;font-size:.71rem;color:#e2eaf5;min-width:155px;line-height:1.8;';
|
|
|
|
|
d.innerHTML=`
|
|
|
|
|
<b style="display:block;margin-bottom:4px;color:#3b9eff">Legenda</b>
|
|
|
|
|
<div><span style="background:#56d4a0;border-radius:50%;display:inline-block;width:10px;height:10px;margin-right:5px"></span>SPBU 24 Jam</div>
|
|
|
|
|
<div><span style="background:#ff5c5c;border-radius:50%;display:inline-block;width:10px;height:10px;margin-right:5px"></span>SPBU Terbatas</div>
|
|
|
|
|
<hr style="border-color:#2e3f57;margin:5px 0">
|
|
|
|
|
<div><span style="background:#7fd4ff;display:inline-block;width:18px;height:4px;margin-right:5px;vertical-align:middle"></span>Jalan Nasional</div>
|
|
|
|
|
<div><span style="background:#c07fff;display:inline-block;width:18px;height:4px;margin-right:5px;vertical-align:middle"></span>Jalan Provinsi</div>
|
|
|
|
|
<div><span style="background:#7fffc0;display:inline-block;width:18px;height:4px;margin-right:5px;vertical-align:middle"></span>Jalan Kabupaten</div>
|
|
|
|
|
<hr style="border-color:#2e3f57;margin:5px 0">
|
|
|
|
|
<div><span style="background:#ffb84d55;border:2px solid #ffb84d;display:inline-block;width:14px;height:10px;margin-right:5px"></span>SHM</div>
|
|
|
|
|
<div><span style="background:#3b9eff55;border:2px solid #3b9eff;display:inline-block;width:14px;height:10px;margin-right:5px"></span>HGB</div>
|
|
|
|
|
<div><span style="background:#56d4a055;border:2px solid #56d4a0;display:inline-block;width:14px;height:10px;margin-right:5px"></span>HGU</div>
|
|
|
|
|
<div><span style="background:#ff9fdf55;border:2px solid #ff9fdf;display:inline-block;width:14px;height:10px;margin-right:5px"></span>HP</div>`;
|
|
|
|
|
return d;
|
|
|
|
|
};
|
|
|
|
|
legend.addTo(map);
|
|
|
|
|
|
|
|
|
|
fetch('data/Admin_Kecamatan_lengkap.geojson')
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
.then(data => {
|
|
|
|
|
L.geoJSON(data, {
|
|
|
|
|
style: {
|
|
|
|
|
color: '#ffff00',
|
|
|
|
|
weight: 2,
|
|
|
|
|
fillOpacity: 0.12
|
|
|
|
|
},
|
|
|
|
|
onEachFeature: (feature, layer) => {
|
|
|
|
|
layer.bindTooltip(feature.properties.Ket || 'Kecamatan');
|
|
|
|
|
}
|
|
|
|
|
}).addTo(map);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
// INIT
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
|
(async function(){
|
|
|
|
|
await Promise.all([loadSPBU(),loadJalan(),loadParsil()]);
|
|
|
|
|
updateDrawControl();
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|