tambah jalan, tambah delete jalan rusak
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -43,4 +43,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
http_response_code(405);
|
http_response_code(405);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||||
$conn->close();
|
$conn->close();
|
||||||
?>
|
?>
|
||||||
+310
-75
@@ -18,24 +18,24 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: calc(1rem + env(safe-area-inset-top)) 1rem 1rem 1rem;
|
padding: calc(.5rem + env(safe-area-inset-top)) .5rem .5rem .5rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
#map {
|
#map {
|
||||||
height: calc(100vh - 2rem);
|
height: calc(100vh - 1rem);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 1rem;
|
border-radius: .75rem;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.custom-msg { display: none; position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 250px; }
|
.custom-msg { display: none; position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 250px; }
|
||||||
.leaflet-popup-content-wrapper { border-radius: 12px; padding: 0; overflow: hidden; }
|
.leaflet-popup-content-wrapper { border-radius: 12px; padding: 0; overflow: hidden; }
|
||||||
.leaflet-popup-content { margin: 0; width: 280px !important; }
|
.leaflet-popup-content { margin: 0; width: 280px !important; }
|
||||||
.leaflet-draw-toolbar a { background-color: white; }
|
.leaflet-draw-toolbar a { background-color: white; }
|
||||||
|
|
||||||
/* CSS Tambahan agar ikon SVG tidak terpotong atau memiliki background */
|
/* CSS Tambahan agar ikon SVG tidak terpotong atau memiliki background */
|
||||||
.custom-marker { background: transparent !important; border: none !important; }
|
.custom-marker { background: transparent !important; border: none !important; }
|
||||||
.feature-card { box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08); }
|
.feature-card { box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08); }
|
||||||
.icon-btn { width: 2.75rem; height: 2.75rem; display: inline-flex; align-items: center; justify-content: center; border-radius: 0.9rem; border: 1px solid rgba(203,213,225,.9); background: rgba(255,255,255,.92); backdrop-filter: blur(14px); transition: .18s ease; }
|
.icon-btn { width: 2.45rem; height: 2.45rem; display: inline-flex; align-items: center; justify-content: center; border-radius: 0.8rem; border: 1px solid rgba(203,213,225,.9); background: rgba(255,255,255,.92); backdrop-filter: blur(14px); transition: .18s ease; }
|
||||||
.icon-btn:hover { transform: translateY(-1px); box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12); }
|
.icon-btn:hover { transform: translateY(-1px); box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12); }
|
||||||
.icon-btn.active { background: #4f46e5; color: white; border-color: #4f46e5; }
|
.icon-btn.active { background: #4f46e5; color: white; border-color: #4f46e5; }
|
||||||
.registry-card { box-shadow: 0 18px 45px rgba(15, 23, 42, 0.14); }
|
.registry-card { box-shadow: 0 18px 45px rgba(15, 23, 42, 0.14); }
|
||||||
@@ -50,27 +50,31 @@
|
|||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
|
|
||||||
<div class="absolute left-6 top-[calc(1rem+env(safe-area-inset-top))] z-[2000] w-[300px] max-w-[calc(100vw-3rem)] rounded-2xl border border-slate-200 bg-white/90 p-3 shadow-2xl backdrop-blur-md md:w-[340px]">
|
<div id="mainPanel" class="absolute left-6 top-[calc(1rem+env(safe-area-inset-top))] z-[2000] w-[260px] max-w-[calc(100vw-3rem)] rounded-2xl border border-slate-200 bg-white/90 p-2.5 shadow-2xl backdrop-blur-md md:w-[290px]">
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-lg font-black tracking-tight text-indigo-700">GIS</h1>
|
<h1 class="text-base font-black tracking-tight text-indigo-700">GIS</h1>
|
||||||
<p id="modeHint" class="mt-1 text-xs text-slate-500">Mode default hanya menampilkan peta dan atribut data yang sudah dibuat.</p>
|
<p id="modeHint" class="hidden mt-1 text-xs text-slate-500">Mode default hanya menampilkan peta dan atribut data yang sudah dibuat.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<button id="toggleMainPanelBtn" class="icon-btn text-slate-700" title="Ciutkan panel">
|
||||||
|
<svg id="toggleMainPanelIcon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||||
|
</button>
|
||||||
<button onclick="toggleInputRegistry()" class="icon-btn text-slate-700" title="Buka registry input">
|
<button onclick="toggleInputRegistry()" class="icon-btn text-slate-700" title="Buka registry input">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 4v16"/><path d="M8 8h11"/><path d="M8 12h11"/><path d="M8 16h7"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 4v16"/><path d="M8 8h11"/><path d="M8 12h11"/><path d="M8 16h7"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="loadMarkers({ force: true }); loadFeatures(true);" class="icon-btn text-indigo-600" title="Refresh data">
|
<button onclick="loadMarkers({ force: true }); loadFeatures(true); loadExternalRoadNetwork(true);" class="icon-btn text-indigo-600" title="Refresh data">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 21h5v-5"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 21h5v-5"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="connectionStatus" class="mt-2 text-[10px] font-bold uppercase tracking-wider text-amber-500">Mengecek koneksi ke database...</div>
|
<div id="mainPanelBody">
|
||||||
<div class="mt-3 flex items-center justify-between rounded-xl bg-slate-50 px-3 py-2 text-xs text-slate-600">
|
<div id="connectionStatus" class="mt-1 text-[9px] font-bold uppercase tracking-wide text-amber-500">Mengecek koneksi...</div>
|
||||||
<span>Total SPBU</span>
|
<div class="mt-2 flex items-center justify-between rounded-xl bg-slate-50 px-2.5 py-1.5 text-xs text-slate-600">
|
||||||
<span id="totalCount" class="text-base font-black text-indigo-600">0</span>
|
<span>SPBU</span>
|
||||||
|
<span id="totalCount" class="text-sm font-black text-indigo-600">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||||
<button class="icon-btn" id="modeViewBtn" title="Lihat data">
|
<button class="icon-btn" id="modeViewBtn" title="Lihat data">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -104,63 +108,64 @@
|
|||||||
<button id="clearDraftBtn" class="rounded-lg bg-slate-100 px-4 py-2 text-sm font-bold text-slate-700 transition hover:bg-slate-200">Bersih</button>
|
<button id="clearDraftBtn" class="rounded-lg bg-slate-100 px-4 py-2 text-sm font-bold text-slate-700 transition hover:bg-slate-200">Bersih</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="inputRegistryPanel" class="mt-3 hidden rounded-2xl border border-slate-200 bg-white/95 p-3 shadow-sm registry-card">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-black tracking-tight text-slate-800">Registry Input</h2>
|
||||||
|
<p class="mt-1 text-[11px] text-slate-500">Daftar input yang bisa difilter.</p>
|
||||||
|
</div>
|
||||||
|
<button id="closeInputRegistryBtn" class="icon-btn h-9 w-9 text-slate-600" title="Tutup registry">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex items-center justify-between rounded-xl bg-slate-50 px-3 py-2">
|
||||||
|
<span class="text-[11px] font-bold uppercase tracking-widest text-slate-500">Tercatat</span>
|
||||||
|
<span id="inputRegistryCount" class="text-sm font-black text-indigo-600">0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 grid gap-2">
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Cari Nama / Atribut</label>
|
||||||
|
<input id="inputSearchQuery" type="text" placeholder="Cari nama, kategori, nomor..." class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Jenis</label>
|
||||||
|
<select id="inputTypeFilter" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500">
|
||||||
|
<option value="all">Semua</option>
|
||||||
|
<option value="point">Poin</option>
|
||||||
|
<option value="polyline">Polyline</option>
|
||||||
|
<option value="polygon">Polygon</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Dari</label>
|
||||||
|
<input id="inputDateFrom" type="date" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Sampai</label>
|
||||||
|
<input id="inputDateTo" type="date" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex gap-2">
|
||||||
|
<button id="applyInputFilterBtn" class="flex-1 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-bold text-white transition hover:bg-indigo-700">Terapkan</button>
|
||||||
|
<button id="clearInputFilterBtn" class="rounded-lg bg-slate-100 px-4 py-2 text-sm font-bold text-slate-700 transition hover:bg-slate-200">Reset</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="inputRegistryList" class="registry-list mt-3 space-y-2 pr-1">
|
||||||
|
<div class="rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-sm text-slate-500">
|
||||||
|
Memuat data input...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="inputRegistryPanel" class="absolute right-6 top-[calc(1rem+env(safe-area-inset-top))] z-[2000] hidden w-[320px] max-w-[calc(100vw-3rem)] rounded-2xl border border-slate-200 bg-white/95 p-3 shadow-2xl backdrop-blur-md registry-card">
|
|
||||||
<div class="flex items-start justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-sm font-black tracking-tight text-slate-800">Registry Input</h2>
|
|
||||||
<p class="mt-1 text-[11px] text-slate-500">Daftar input yang bisa difilter.</p>
|
|
||||||
</div>
|
|
||||||
<button id="closeInputRegistryBtn" class="icon-btn h-9 w-9 text-slate-600" title="Tutup registry">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3 flex items-center justify-between rounded-xl bg-slate-50 px-3 py-2">
|
|
||||||
<span class="text-[11px] font-bold uppercase tracking-widest text-slate-500">Tercatat</span>
|
|
||||||
<span id="inputRegistryCount" class="text-sm font-black text-indigo-600">0</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3 grid gap-2">
|
|
||||||
<div class="sm:col-span-2">
|
|
||||||
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Cari Nama / Atribut</label>
|
|
||||||
<input id="inputSearchQuery" type="text" placeholder="Cari nama, kategori, nomor..." class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500">
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1">
|
|
||||||
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Jenis</label>
|
|
||||||
<select id="inputTypeFilter" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500">
|
|
||||||
<option value="all">Semua</option>
|
|
||||||
<option value="point">Poin</option>
|
|
||||||
<option value="polyline">Polyline</option>
|
|
||||||
<option value="polygon">Polygon</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Dari</label>
|
|
||||||
<input id="inputDateFrom" type="date" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Sampai</label>
|
|
||||||
<input id="inputDateTo" type="date" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3 flex gap-2">
|
|
||||||
<button id="applyInputFilterBtn" class="flex-1 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-bold text-white transition hover:bg-indigo-700">Terapkan</button>
|
|
||||||
<button id="clearInputFilterBtn" class="rounded-lg bg-slate-100 px-4 py-2 text-sm font-bold text-slate-700 transition hover:bg-slate-200">Reset</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="inputRegistryList" class="registry-list mt-3 space-y-2 pr-1">
|
|
||||||
<div class="rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-sm text-slate-500">
|
|
||||||
Memuat data input...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="choroplethPanel" class="absolute right-6 bottom-6 z-[2000] hidden w-[320px] max-w-[calc(100vw-3rem)] rounded-2xl border border-slate-200 bg-white/95 p-3 shadow-2xl backdrop-blur-md">
|
<div id="choroplethPanel" class="absolute right-6 bottom-6 z-[2000] hidden w-[320px] max-w-[calc(100vw-3rem)] rounded-2xl border border-slate-200 bg-white/95 p-3 shadow-2xl backdrop-blur-md">
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -330,6 +335,7 @@
|
|||||||
<!-- Leaflet JS -->
|
<!-- Leaflet JS -->
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/proj4@2.12.1/dist/proj4.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Inisialisasi Peta (Default Pontianak)
|
// Inisialisasi Peta (Default Pontianak)
|
||||||
@@ -355,8 +361,13 @@
|
|||||||
let editingFeatureId = null;
|
let editingFeatureId = null;
|
||||||
let choroplethLayer = null;
|
let choroplethLayer = null;
|
||||||
let roadDamageLayer = L.featureGroup().addTo(map);
|
let roadDamageLayer = L.featureGroup().addTo(map);
|
||||||
|
let roadNetworkLayer = L.featureGroup().addTo(map);
|
||||||
let roadDamageRecords = [];
|
let roadDamageRecords = [];
|
||||||
let pendingRoadDamageCoords = null;
|
let pendingRoadDamageCoords = null;
|
||||||
|
let isMainPanelCollapsed = false;
|
||||||
|
|
||||||
|
const ROAD_NETWORK_GEOJSON_PATH = 'data/Export_Output_2.json';
|
||||||
|
let roadNetworkLoaded = false;
|
||||||
|
|
||||||
const FEATURE_STATUS_OPTIONS = {
|
const FEATURE_STATUS_OPTIONS = {
|
||||||
polyline: [
|
polyline: [
|
||||||
@@ -480,11 +491,29 @@
|
|||||||
function sanitizeChoroplethFeature(feature) {
|
function sanitizeChoroplethFeature(feature) {
|
||||||
const geometry = feature.geometry || {};
|
const geometry = feature.geometry || {};
|
||||||
|
|
||||||
|
if (geometry.type === 'Polygon' && Array.isArray(geometry.coordinates)) {
|
||||||
|
const outerRing = geometry.coordinates[0];
|
||||||
|
if (!Array.isArray(outerRing)) {
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...feature,
|
||||||
|
geometry: {
|
||||||
|
...geometry,
|
||||||
|
coordinates: [outerRing]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (geometry.type !== 'MultiPolygon' || !Array.isArray(geometry.coordinates)) {
|
if (geometry.type !== 'MultiPolygon' || !Array.isArray(geometry.coordinates)) {
|
||||||
return feature;
|
return feature;
|
||||||
}
|
}
|
||||||
|
|
||||||
const partsWithArea = geometry.coordinates
|
const normalizedParts = geometry.coordinates
|
||||||
|
.map(part => (Array.isArray(part) && Array.isArray(part[0]) ? [part[0]] : part));
|
||||||
|
|
||||||
|
const partsWithArea = normalizedParts
|
||||||
.map(part => ({ part, area: getPolygonPartArea(part) }))
|
.map(part => ({ part, area: getPolygonPartArea(part) }))
|
||||||
.filter(entry => entry.area > 0)
|
.filter(entry => entry.area > 0)
|
||||||
.sort((left, right) => right.area - left.area);
|
.sort((left, right) => right.area - left.area);
|
||||||
@@ -542,8 +571,8 @@
|
|||||||
|
|
||||||
choroplethLayer = L.geoJSON(geoJsonData, {
|
choroplethLayer = L.geoJSON(geoJsonData, {
|
||||||
style: feature => ({
|
style: feature => ({
|
||||||
color: '#ffffff',
|
color: 'transparent',
|
||||||
weight: 2,
|
weight: 0,
|
||||||
fillColor: getPopulationFill(Number(getChoroplethValue(feature.properties || {}).value || 0)),
|
fillColor: getPopulationFill(Number(getChoroplethValue(feature.properties || {}).value || 0)),
|
||||||
fillOpacity: 0.72
|
fillOpacity: 0.72
|
||||||
}),
|
}),
|
||||||
@@ -551,9 +580,10 @@
|
|||||||
const props = feature.properties || {};
|
const props = feature.properties || {};
|
||||||
const districtName = props.name || props.NAME || 'Tanpa nama';
|
const districtName = props.name || props.NAME || 'Tanpa nama';
|
||||||
|
|
||||||
layer.bindTooltip(districtName, { sticky: true });
|
// Diagnostic: Disabled tooltip
|
||||||
|
// layer.bindTooltip(districtName, { sticky: true });
|
||||||
layer.on('mouseover', () => {
|
layer.on('mouseover', () => {
|
||||||
layer.setStyle({ weight: 3, fillOpacity: 0.86, color: '#1f2937' });
|
layer.setStyle({ weight: 0, fillOpacity: 0.86, color: 'transparent' });
|
||||||
});
|
});
|
||||||
layer.on('mouseout', () => {
|
layer.on('mouseout', () => {
|
||||||
choroplethLayer.resetStyle(layer);
|
choroplethLayer.resetStyle(layer);
|
||||||
@@ -562,7 +592,8 @@
|
|||||||
const mainValue = getChoroplethValue(props);
|
const mainValue = getChoroplethValue(props);
|
||||||
|
|
||||||
updateChoroplethPanel(props);
|
updateChoroplethPanel(props);
|
||||||
layer.bindPopup(`<div class="p-1"><strong>${districtName}</strong><br>${mainValue.label}: ${mainValue.value.toLocaleString('id-ID')} ${mainValue.unit}</div>`).openPopup();
|
// Diagnostic: Disabled popup to check if black rectangle comes from popup element
|
||||||
|
// layer.bindPopup(`<div class="p-1"><strong>${districtName}</strong><br>${mainValue.label}: ${mainValue.value.toLocaleString('id-ID')} ${mainValue.unit}</div>`).openPopup();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
@@ -576,16 +607,25 @@
|
|||||||
|
|
||||||
panel.classList.toggle('hidden', !enabled);
|
panel.classList.toggle('hidden', !enabled);
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
if (map.hasLayer(featureLayer)) {
|
||||||
|
map.removeLayer(featureLayer);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await buildChoroplethLayer();
|
await buildChoroplethLayer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage(error.message || 'Gagal memuat peta choropleth', 'bg-red-600 text-white');
|
showMessage(error.message || 'Gagal memuat peta choropleth', 'bg-red-600 text-white');
|
||||||
panel.classList.add('hidden');
|
panel.classList.add('hidden');
|
||||||
|
if (!map.hasLayer(featureLayer)) {
|
||||||
|
map.addLayer(featureLayer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearChoroplethLayer();
|
clearChoroplethLayer();
|
||||||
|
if (!map.hasLayer(featureLayer)) {
|
||||||
|
map.addLayer(featureLayer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFeatureStatusColor(type, status) {
|
function getFeatureStatusColor(type, status) {
|
||||||
@@ -1063,6 +1103,184 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRoadNetworkColor(status) {
|
||||||
|
const key = String(status || '').toLowerCase().trim();
|
||||||
|
if (key.includes('nasional')) return '#0f766e';
|
||||||
|
if (key.includes('propinsi') || key.includes('provinsi')) return '#1d4ed8';
|
||||||
|
if (key.includes('kota')) return '#dc2626';
|
||||||
|
if (key.includes('kabupaten')) return '#b45309';
|
||||||
|
return '#475569';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeRoadCoordinates(coords) {
|
||||||
|
if (!Array.isArray(coords)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
for (const coord of coords) {
|
||||||
|
const x = Number(coord && coord[0]);
|
||||||
|
const y = Number(coord && coord[1]);
|
||||||
|
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data jalan berada pada UTM 49S (easting normal sekitar 100k-900k).
|
||||||
|
if (x < 100000 || x > 900000 || Math.abs(y) > 11000000) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push([x, y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeRoadFeature(feature) {
|
||||||
|
if (!feature || !feature.geometry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const geometry = feature.geometry;
|
||||||
|
const type = geometry.type;
|
||||||
|
|
||||||
|
if (type === 'LineString') {
|
||||||
|
const cleaned = sanitizeRoadCoordinates(geometry.coordinates);
|
||||||
|
if (cleaned.length < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...feature,
|
||||||
|
geometry: {
|
||||||
|
...geometry,
|
||||||
|
coordinates: cleaned
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'MultiLineString') {
|
||||||
|
const cleanedLines = (Array.isArray(geometry.coordinates) ? geometry.coordinates : [])
|
||||||
|
.map(line => sanitizeRoadCoordinates(line))
|
||||||
|
.filter(line => line.length >= 2);
|
||||||
|
|
||||||
|
if (!cleanedLines.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...feature,
|
||||||
|
geometry: {
|
||||||
|
...geometry,
|
||||||
|
coordinates: cleanedLines
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeRoadGeoJson(data) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
features: (Array.isArray(data.features) ? data.features : [])
|
||||||
|
.map(sanitizeRoadFeature)
|
||||||
|
.filter(Boolean)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertExportRoadCoord(coords) {
|
||||||
|
const x = Number(coords && coords[0]);
|
||||||
|
const y = Number(coords && coords[1]);
|
||||||
|
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||||
|
return L.latLng(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If coordinates are already lon/lat, use them directly.
|
||||||
|
if (Math.abs(x) <= 180 && Math.abs(y) <= 90) {
|
||||||
|
return L.latLng(y, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof proj4 !== 'function') {
|
||||||
|
return L.latLng(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export data uses UTM 49S with false northing removed (small +/- values).
|
||||||
|
const adjustedNorthing = Math.abs(y) < 1000000 ? y + 10000000 : y;
|
||||||
|
const projected = proj4('EPSG:32749', 'EPSG:4326', [x, adjustedNorthing]);
|
||||||
|
return L.latLng(projected[1], projected[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadExternalRoadNetwork(force = false) {
|
||||||
|
if (roadNetworkLoaded && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof proj4 === 'function' && !proj4.defs('EPSG:32749')) {
|
||||||
|
proj4.defs('EPSG:32749', '+proj=utm +zone=49 +south +datum=WGS84 +units=m +no_defs +type=crs');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
roadNetworkLayer.clearLayers();
|
||||||
|
roadNetworkLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(ROAD_NETWORK_GEOJSON_PATH, { cache: 'no-store' });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Gagal memuat ${ROAD_NETWORK_GEOJSON_PATH} (HTTP ${response.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data || data.type !== 'FeatureCollection' || !Array.isArray(data.features)) {
|
||||||
|
throw new Error('Format data jalan tidak valid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedRoadData = sanitizeRoadGeoJson(data);
|
||||||
|
|
||||||
|
const roadGeoJsonLayer = L.geoJSON(sanitizedRoadData, {
|
||||||
|
coordsToLatLng: convertExportRoadCoord,
|
||||||
|
filter: feature => {
|
||||||
|
const type = feature && feature.geometry && feature.geometry.type;
|
||||||
|
return type === 'LineString' || type === 'MultiLineString';
|
||||||
|
},
|
||||||
|
style: feature => {
|
||||||
|
const props = feature.properties || {};
|
||||||
|
return {
|
||||||
|
color: getRoadNetworkColor(props.status_jal),
|
||||||
|
weight: 2.8,
|
||||||
|
opacity: 0.92
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onEachFeature: (feature, layer) => {
|
||||||
|
const props = feature.properties || {};
|
||||||
|
const namaJalan = String(props.nama_jalan || '').trim() || 'Tanpa nama';
|
||||||
|
const status = String(props.status_jal || '').trim() || '-';
|
||||||
|
const fungsi = String(props.Fungsi_Jal || '').trim() || '-';
|
||||||
|
const panjang = Number(props.Plan_Len);
|
||||||
|
const panjangText = Number.isFinite(panjang) ? `${panjang.toFixed(1)} m` : '-';
|
||||||
|
|
||||||
|
layer.bindPopup(`
|
||||||
|
<div class="p-2">
|
||||||
|
<strong>${namaJalan}</strong><br>
|
||||||
|
Status: ${status}<br>
|
||||||
|
Fungsi: ${fungsi}<br>
|
||||||
|
Panjang: ${panjangText}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
roadNetworkLayer.addLayer(roadGeoJsonLayer);
|
||||||
|
roadNetworkLoaded = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal memuat layer jalan eksternal:', error);
|
||||||
|
showMessage(error.message || 'Gagal memuat data jalan eksternal', 'bg-red-600 text-white');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function deleteRoadDamage(id) {
|
function deleteRoadDamage(id) {
|
||||||
if (!confirm('Hapus laporan jalan rusak ini?')) return;
|
if (!confirm('Hapus laporan jalan rusak ini?')) return;
|
||||||
|
|
||||||
@@ -1688,7 +1906,22 @@
|
|||||||
function setConnectionStatus(message, colorClass) {
|
function setConnectionStatus(message, colorClass) {
|
||||||
const statusEl = document.getElementById('connectionStatus');
|
const statusEl = document.getElementById('connectionStatus');
|
||||||
statusEl.innerText = message;
|
statusEl.innerText = message;
|
||||||
statusEl.className = `mt-2 text-[10px] font-bold uppercase tracking-wider ${colorClass}`;
|
statusEl.className = `mt-1 text-[9px] font-bold uppercase tracking-wide ${colorClass}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMainPanel(forceState = null) {
|
||||||
|
const panelBody = document.getElementById('mainPanelBody');
|
||||||
|
const toggleIcon = document.getElementById('toggleMainPanelIcon');
|
||||||
|
const nextState = typeof forceState === 'boolean' ? forceState : !isMainPanelCollapsed;
|
||||||
|
|
||||||
|
isMainPanelCollapsed = nextState;
|
||||||
|
panelBody.classList.toggle('hidden', isMainPanelCollapsed);
|
||||||
|
|
||||||
|
if (isMainPanelCollapsed) {
|
||||||
|
toggleIcon.innerHTML = '<path d="m9 18 6-6-6-6"/>';
|
||||||
|
} else {
|
||||||
|
toggleIcon.innerHTML = '<path d="m15 18-6-6 6-6"/>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAllMarkers() {
|
function clearAllMarkers() {
|
||||||
@@ -2016,6 +2249,7 @@
|
|||||||
enforceRegistryDateRange(false);
|
enforceRegistryDateRange(false);
|
||||||
loadMarkers({ force: true });
|
loadMarkers({ force: true });
|
||||||
loadFeatures(true);
|
loadFeatures(true);
|
||||||
|
loadExternalRoadNetwork(true);
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2025,6 +2259,7 @@
|
|||||||
document.getElementById('modePolygonBtn').addEventListener('click', () => setDataMode('polygon'));
|
document.getElementById('modePolygonBtn').addEventListener('click', () => setDataMode('polygon'));
|
||||||
document.getElementById('modeChoroplethBtn').addEventListener('click', () => setDataMode('choropleth'));
|
document.getElementById('modeChoroplethBtn').addEventListener('click', () => setDataMode('choropleth'));
|
||||||
document.getElementById('modeRoadDamageBtn').addEventListener('click', () => setDataMode('roadDamage'));
|
document.getElementById('modeRoadDamageBtn').addEventListener('click', () => setDataMode('roadDamage'));
|
||||||
|
document.getElementById('toggleMainPanelBtn').addEventListener('click', () => toggleMainPanel());
|
||||||
document.getElementById('drawStatus').addEventListener('change', () => {
|
document.getElementById('drawStatus').addEventListener('change', () => {
|
||||||
const type = document.getElementById('featureType').value;
|
const type = document.getElementById('featureType').value;
|
||||||
document.getElementById('featureStatus').value = document.getElementById('drawStatus').value;
|
document.getElementById('featureStatus').value = document.getElementById('drawStatus').value;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Reference in New Issue
Block a user