tambah jalan, tambah delete jalan rusak

This commit is contained in:
Farha
2026-04-16 09:58:45 +07:00
parent aa3fe296c1
commit 607aabf2e0
7 changed files with 1963 additions and 76 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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
View File
@@ -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