tambah jalan, tambah delete jalan rusak
This commit is contained in:
File diff suppressed because one or more lines are too long
+258
-23
@@ -18,13 +18,13 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
#map {
|
||||
height: calc(100vh - 2rem);
|
||||
height: calc(100vh - 1rem);
|
||||
width: 100%;
|
||||
border-radius: 1rem;
|
||||
border-radius: .75rem;
|
||||
z-index: 1;
|
||||
}
|
||||
.custom-msg { display: none; position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 250px; }
|
||||
@@ -35,7 +35,7 @@
|
||||
/* CSS Tambahan agar ikon SVG tidak terpotong atau memiliki background */
|
||||
.custom-marker { background: transparent !important; border: none !important; }
|
||||
.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.active { background: #4f46e5; color: white; border-color: #4f46e5; }
|
||||
.registry-card { box-shadow: 0 18px 45px rgba(15, 23, 42, 0.14); }
|
||||
@@ -50,27 +50,31 @@
|
||||
<div class="app-shell">
|
||||
<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>
|
||||
<h1 class="text-lg 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>
|
||||
<h1 class="text-base font-black tracking-tight text-indigo-700">GIS</h1>
|
||||
<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 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">
|
||||
<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 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="connectionStatus" class="mt-2 text-[10px] font-bold uppercase tracking-wider text-amber-500">Mengecek koneksi ke database...</div>
|
||||
<div class="mt-3 flex items-center justify-between rounded-xl bg-slate-50 px-3 py-2 text-xs text-slate-600">
|
||||
<span>Total SPBU</span>
|
||||
<span id="totalCount" class="text-base font-black text-indigo-600">0</span>
|
||||
<div id="mainPanelBody">
|
||||
<div id="connectionStatus" class="mt-1 text-[9px] font-bold uppercase tracking-wide text-amber-500">Mengecek koneksi...</div>
|
||||
<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>SPBU</span>
|
||||
<span id="totalCount" class="text-sm font-black text-indigo-600">0</span>
|
||||
</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">
|
||||
<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>
|
||||
@@ -105,9 +109,7 @@
|
||||
</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 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>
|
||||
@@ -160,6 +162,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</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 class="flex items-start justify-between gap-3">
|
||||
@@ -330,6 +335,7 @@
|
||||
<!-- Leaflet JS -->
|
||||
<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://cdn.jsdelivr.net/npm/proj4@2.12.1/dist/proj4.js"></script>
|
||||
|
||||
<script>
|
||||
// Inisialisasi Peta (Default Pontianak)
|
||||
@@ -355,8 +361,13 @@
|
||||
let editingFeatureId = null;
|
||||
let choroplethLayer = null;
|
||||
let roadDamageLayer = L.featureGroup().addTo(map);
|
||||
let roadNetworkLayer = L.featureGroup().addTo(map);
|
||||
let roadDamageRecords = [];
|
||||
let pendingRoadDamageCoords = null;
|
||||
let isMainPanelCollapsed = false;
|
||||
|
||||
const ROAD_NETWORK_GEOJSON_PATH = 'data/Export_Output_2.json';
|
||||
let roadNetworkLoaded = false;
|
||||
|
||||
const FEATURE_STATUS_OPTIONS = {
|
||||
polyline: [
|
||||
@@ -480,11 +491,29 @@
|
||||
function sanitizeChoroplethFeature(feature) {
|
||||
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)) {
|
||||
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) }))
|
||||
.filter(entry => entry.area > 0)
|
||||
.sort((left, right) => right.area - left.area);
|
||||
@@ -542,8 +571,8 @@
|
||||
|
||||
choroplethLayer = L.geoJSON(geoJsonData, {
|
||||
style: feature => ({
|
||||
color: '#ffffff',
|
||||
weight: 2,
|
||||
color: 'transparent',
|
||||
weight: 0,
|
||||
fillColor: getPopulationFill(Number(getChoroplethValue(feature.properties || {}).value || 0)),
|
||||
fillOpacity: 0.72
|
||||
}),
|
||||
@@ -551,9 +580,10 @@
|
||||
const props = feature.properties || {};
|
||||
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.setStyle({ weight: 3, fillOpacity: 0.86, color: '#1f2937' });
|
||||
layer.setStyle({ weight: 0, fillOpacity: 0.86, color: 'transparent' });
|
||||
});
|
||||
layer.on('mouseout', () => {
|
||||
choroplethLayer.resetStyle(layer);
|
||||
@@ -562,7 +592,8 @@
|
||||
const mainValue = getChoroplethValue(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);
|
||||
@@ -576,16 +607,25 @@
|
||||
|
||||
panel.classList.toggle('hidden', !enabled);
|
||||
if (enabled) {
|
||||
if (map.hasLayer(featureLayer)) {
|
||||
map.removeLayer(featureLayer);
|
||||
}
|
||||
try {
|
||||
await buildChoroplethLayer();
|
||||
} catch (error) {
|
||||
showMessage(error.message || 'Gagal memuat peta choropleth', 'bg-red-600 text-white');
|
||||
panel.classList.add('hidden');
|
||||
if (!map.hasLayer(featureLayer)) {
|
||||
map.addLayer(featureLayer);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
clearChoroplethLayer();
|
||||
if (!map.hasLayer(featureLayer)) {
|
||||
map.addLayer(featureLayer);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!confirm('Hapus laporan jalan rusak ini?')) return;
|
||||
|
||||
@@ -1688,7 +1906,22 @@
|
||||
function setConnectionStatus(message, colorClass) {
|
||||
const statusEl = document.getElementById('connectionStatus');
|
||||
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() {
|
||||
@@ -2016,6 +2249,7 @@
|
||||
enforceRegistryDateRange(false);
|
||||
loadMarkers({ force: true });
|
||||
loadFeatures(true);
|
||||
loadExternalRoadNetwork(true);
|
||||
startAutoRefresh();
|
||||
};
|
||||
|
||||
@@ -2025,6 +2259,7 @@
|
||||
document.getElementById('modePolygonBtn').addEventListener('click', () => setDataMode('polygon'));
|
||||
document.getElementById('modeChoroplethBtn').addEventListener('click', () => setDataMode('choropleth'));
|
||||
document.getElementById('modeRoadDamageBtn').addEventListener('click', () => setDataMode('roadDamage'));
|
||||
document.getElementById('toggleMainPanelBtn').addEventListener('click', () => toggleMainPanel());
|
||||
document.getElementById('drawStatus').addEventListener('change', () => {
|
||||
const type = document.getElementById('featureType').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