Merge pull request #1 from Mhanif26/baru

pembaruan fitur layer
This commit is contained in:
2026-04-20 09:26:08 +07:00
committed by GitHub
3 changed files with 479 additions and 139 deletions
+479 -139
View File
@@ -43,6 +43,8 @@
.registry-item { transition: .18s ease; }
.registry-item:hover { transform: translateY(-1px); box-shadow: 0 12px 28px rgba(15, 23, 42, 0.1); }
.registry-chip { display: inline-flex; align-items: center; gap: .35rem; border-radius: 9999px; padding: .25rem .6rem; font-size: .7rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
#registryDrawer { transition: transform .24s ease, opacity .24s ease; }
#registryDrawer.hidden-drawer { transform: translateX(-12px); opacity: 0; pointer-events: none; }
</style>
</head>
<body class="bg-slate-50 text-slate-800 font-sans">
@@ -50,23 +52,10 @@
<div class="app-shell">
<div id="map"></div>
<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-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); 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 id="mainPanel" class="hidden 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>
<h1 class="text-base font-black tracking-tight text-indigo-700">GIS</h1>
<p id="modeHint" class="hidden"></p>
</div>
<div id="mainPanelBody">
<div id="connectionStatus" class="mt-1 text-[9px] font-bold uppercase tracking-wide text-amber-500">Mengecek koneksi...</div>
@@ -74,25 +63,30 @@
<span>SPBU</span>
<span id="totalCount" class="text-sm font-black text-indigo-600">0</span>
</div>
<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>
<button class="icon-btn" id="modePointBtn" title="Tambah point SPBU">
<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="M12 2C8.13 2 5 5.13 5 9c0 5 7 13 7 13s7-8 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg>
</button>
<button class="icon-btn" id="modePolylineBtn" title="Tambah polyline jalan">
<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="M4 18l6-6 4 4 6-8"/></svg>
</button>
<button class="icon-btn" id="modePolygonBtn" title="Tambah polygon parsil">
<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="M12 2l8 5v10l-8 5-8-5V7z"/></svg>
</button>
<button class="icon-btn" id="modeRoadDamageBtn" title="Lapor jalan rusak">
<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="M6 12h12M12 6v12M2 12h20M12 2v20"/><circle cx="12" cy="12" r="10"/></svg>
</button>
<button class="icon-btn" id="modeChoroplethBtn" title="Lihat peta choropleth kecamatan">
<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="M3 6l6-2 6 2 6-2v14l-6 2-6-2-6 2z"/><path d="M9 4v14"/><path d="M15 6v14"/></svg>
<div class="mt-2 flex flex-wrap items-center gap-1.5">
<button class="icon-btn" id="toggleEditModeBtn" title="Aktifkan mode edit">
<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="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
</button>
<div id="editModeButtons" class="hidden 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>
<button class="icon-btn" id="modePointBtn" title="Tambah point SPBU">
<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="M12 2C8.13 2 5 5.13 5 9c0 5 7 13 7 13s7-8 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg>
</button>
<button class="icon-btn" id="modePolylineBtn" title="Tambah polyline jalan">
<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="M4 18l6-6 4 4 6-8"/></svg>
</button>
<button class="icon-btn" id="modePolygonBtn" title="Tambah polygon parsil">
<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="M12 2l8 5v10l-8 5-8-5V7z"/></svg>
</button>
<button class="icon-btn" id="modeRoadDamageBtn" title="Lapor jalan rusak">
<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="M6 12h12M12 6v12M2 12h20M12 2v20"/><circle cx="12" cy="12" r="10"/></svg>
</button>
<button class="icon-btn" id="modeChoroplethBtn" title="Lihat peta choropleth kecamatan">
<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="M3 6l6-2 6 2 6-2v14l-6 2-6-2-6 2z"/><path d="M9 4v14"/><path d="M15 6v14"/></svg>
</button>
</div>
</div>
<div id="spatialControls" class="mt-3 hidden rounded-2xl border border-slate-200 bg-white p-3 shadow-sm">
<div id="statusControlWrap">
@@ -166,6 +160,76 @@
</div>
<div id="minimalToolbar" class="hidden absolute right-6 top-[calc(1rem+env(safe-area-inset-top))] z-[2100] flex items-center gap-2 rounded-2xl border border-slate-200 bg-white/90 p-2 shadow-xl backdrop-blur-md">
<button class="icon-btn" id="miniModePointBtn" title="Tambah point SPBU">
<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="M12 2C8.13 2 5 5.13 5 9c0 5 7 13 7 13s7-8 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg>
</button>
<button class="icon-btn" id="miniModePolylineBtn" title="Tambah polyline jalan">
<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="M4 18l6-6 4 4 6-8"/></svg>
</button>
<button class="icon-btn" id="miniModePolygonBtn" title="Tambah polygon parsil">
<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="M12 2l8 5v10l-8 5-8-5V7z"/></svg>
</button>
<button class="icon-btn" id="toggleRegistryDrawerBtn" title="Buka registry & filter">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="6" x2="20" y2="6"/><line x1="7" y1="12" x2="17" y2="12"/><line x1="10" y1="18" x2="14" y2="18"/></svg>
</button>
</div>
<div id="registryDrawer" class="hidden-drawer absolute right-6 top-[calc(1rem+env(safe-area-inset-top)+4.2rem)] z-[2100] w-[360px] 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>
<h2 class="text-sm font-black tracking-tight text-slate-800">Registry</h2>
<p class="mt-1 text-[11px] text-slate-500">Filter dan pencarian data input.</p>
</div>
<button id="registryDrawerCloseBtn" 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="registryCount" class="text-sm font-black text-indigo-600">0</span>
</div>
<div class="mt-3 grid gap-2">
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Cari Nama / Atribut</label>
<input id="registrySearchQuery" 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>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Jenis</label>
<select id="registryTypeFilter" 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>
<option value="roadDamage">Jalan Rusak</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="registryDateFrom" 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="registryDateTo" 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="registryApplyFilterBtn" 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="registryResetFilterBtn" 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="registryList" 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 class="flex items-start justify-between gap-3">
<div>
@@ -247,7 +311,7 @@
<div class="mt-4 grid gap-3 sm:grid-cols-2">
<div class="sm:col-span-2">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Nama Objek</label>
<input id="newFeatureName" type="text" placeholder="Contoh: Jl. Ahmad Yani" class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500">
<input id="newFeatureName" type="text" placeholder="Tambahkan nama atribut" class="w-full rounded-lg border border-slate-300 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">Kategori</label>
@@ -352,6 +416,7 @@
let activeDrawHandler = null;
let currentDrawMode = 'polyline';
let appMode = 'view';
let isEditModeEnabled = true;
let pendingShape = null;
let pendingNewFeatureLayer = null;
let featureLayerMap = {};
@@ -360,6 +425,10 @@
let featureRecords = [];
let editingFeatureId = null;
let choroplethLayer = null;
let spbuBuka24JamLayer = L.layerGroup().addTo(map);
let spbuTidak24JamLayer = L.layerGroup().addTo(map);
let spbuLayerControl = null;
let utilityControl = null;
let roadDamageLayer = L.featureGroup().addTo(map);
let roadNetworkLayer = L.featureGroup().addTo(map);
let roadDamageRecords = [];
@@ -670,9 +739,38 @@
return `${year}-${month}-${day}`;
}
function getRegistryUi() {
const drawerPanel = document.getElementById('registryDrawer');
const drawerSearch = document.getElementById('registrySearchQuery');
if (drawerPanel && drawerSearch) {
return {
panel: drawerPanel,
count: document.getElementById('registryCount'),
list: document.getElementById('registryList'),
search: drawerSearch,
type: document.getElementById('registryTypeFilter'),
from: document.getElementById('registryDateFrom'),
to: document.getElementById('registryDateTo')
};
}
return {
panel: document.getElementById('inputRegistryPanel'),
count: document.getElementById('inputRegistryCount'),
list: document.getElementById('inputRegistryList'),
search: document.getElementById('inputSearchQuery'),
type: document.getElementById('inputTypeFilter'),
from: document.getElementById('inputDateFrom'),
to: document.getElementById('inputDateTo')
};
}
function enforceRegistryDateRange(showFeedback = false) {
const fromEl = document.getElementById('inputDateFrom');
const toEl = document.getElementById('inputDateTo');
const ui = getRegistryUi();
const fromEl = ui.from;
const toEl = ui.to;
if (!fromEl || !toEl) return;
const from = fromEl.value;
const to = toEl.value;
@@ -688,11 +786,12 @@
}
function getRegistryFilters() {
const ui = getRegistryUi();
return {
search: document.getElementById('inputSearchQuery').value.toLowerCase().trim(),
type: document.getElementById('inputTypeFilter').value,
from: document.getElementById('inputDateFrom').value,
to: document.getElementById('inputDateTo').value
search: String(ui.search && ui.search.value ? ui.search.value : '').toLowerCase().trim(),
type: ui.type && ui.type.value ? ui.type.value : 'all',
from: ui.from && ui.from.value ? ui.from.value : '',
to: ui.to && ui.to.value ? ui.to.value : ''
};
}
@@ -754,7 +853,18 @@
created_at: feature.created_at || null
}));
return [...pointRecords, ...spatialRecords].sort((left, right) => {
const roadDamageRegistryRecords = roadDamageRecords.map(record => ({
id: Number(record.id),
type: 'roadDamage',
title: `Jalan Rusak - ${record.jenis_kerusakan || 'Tanpa jenis'}`,
subtitle: record.deskripsi || '-',
status: record.severity || '-',
created_at: record.created_at || null,
latitude: Number(record.latitude),
longitude: Number(record.longitude)
}));
return [...pointRecords, ...spatialRecords, ...roadDamageRegistryRecords].sort((left, right) => {
const leftDate = parseDateOnly(left.created_at);
const rightDate = parseDateOnly(right.created_at);
@@ -769,6 +879,7 @@
if (type === 'point') return 'Poin';
if (type === 'polyline') return 'Polyline';
if (type === 'polygon') return 'Polygon';
if (type === 'roadDamage') return 'Jalan Rusak';
return 'Data';
}
@@ -776,6 +887,7 @@
if (type === 'point') return 'bg-emerald-100 text-emerald-700';
if (type === 'polyline') return 'bg-indigo-100 text-indigo-700';
if (type === 'polygon') return 'bg-amber-100 text-amber-700';
if (type === 'roadDamage') return 'bg-red-100 text-red-700';
return 'bg-slate-100 text-slate-700';
}
@@ -788,6 +900,27 @@
return;
}
if (type === 'roadDamage') {
let selectedMarker = null;
roadDamageLayer.eachLayer(layer => {
if (layer && layer.roadDamageId === id) {
selectedMarker = layer;
}
});
if (selectedMarker) {
map.setView(selectedMarker.getLatLng(), Math.max(map.getZoom(), 16));
selectedMarker.openPopup();
return;
}
const record = roadDamageRecords.find(item => Number(item.id) === Number(id));
if (record && Number.isFinite(Number(record.latitude)) && Number.isFinite(Number(record.longitude))) {
map.setView([Number(record.latitude), Number(record.longitude)], Math.max(map.getZoom(), 16));
}
return;
}
const layer = featureLayerMap[id];
if (!layer) return;
if (layer.getBounds) {
@@ -797,8 +930,10 @@
}
function renderInputRegistry() {
const listEl = document.getElementById('inputRegistryList');
const countEl = document.getElementById('inputRegistryCount');
const ui = getRegistryUi();
const listEl = ui.list;
const countEl = ui.count;
if (!listEl || !countEl) return;
const filters = getRegistryFilters();
const records = buildRegistryRecords().filter(record => recordMatchesFilters(record, filters));
@@ -819,10 +954,14 @@
const dateLabel = formatInputDate(record.created_at);
const measure = record.type === 'point'
? `Telp: ${record.subtitle}`
: `Kategori: ${record.subtitle}`;
: record.type === 'roadDamage'
? `Deskripsi: ${record.subtitle}`
: `Kategori: ${record.subtitle}`;
const statusLabel = record.type === 'point'
? `24 Jam: ${record.status}`
: `Status: ${record.status}`;
: record.type === 'roadDamage'
? `Tingkat: ${record.status}`
: `Status: ${record.status}`;
return `
<button type="button" onclick="focusRegistryItem('${record.type}', ${record.id})" class="registry-item w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-left shadow-sm">
@@ -847,10 +986,28 @@
}
function toggleInputRegistry(forceState = null) {
const panel = document.getElementById('inputRegistryPanel');
const shouldOpen = forceState === null ? panel.classList.contains('hidden') : !!forceState;
const ui = getRegistryUi();
const panel = ui.panel;
if (!panel) return;
const useDrawer = panel.id === 'registryDrawer';
const isHidden = useDrawer ? panel.classList.contains('hidden-drawer') : panel.classList.contains('hidden');
const shouldOpen = forceState === null ? isHidden : !!forceState;
if (useDrawer) {
panel.classList.toggle('hidden-drawer', !shouldOpen);
const triggerBtn = document.getElementById('toggleRegistryDrawerBtn');
if (triggerBtn) {
triggerBtn.classList.toggle('active', shouldOpen);
}
const leafletTriggerBtn = document.getElementById('leafletRegistryBtn');
if (leafletTriggerBtn) {
leafletTriggerBtn.classList.toggle('active', shouldOpen);
}
} else {
panel.classList.toggle('hidden', !shouldOpen);
}
panel.classList.toggle('hidden', !shouldOpen);
if (shouldOpen) {
renderInputRegistry();
}
@@ -888,7 +1045,6 @@
function setDataMode(mode) {
appMode = mode;
const hint = document.getElementById('modeHint');
const statusWrap = document.getElementById('statusControlWrap');
const measureWrap = document.getElementById('measureControlWrap');
const runActionBtn = document.getElementById('runActionBtn');
@@ -900,10 +1056,27 @@
polyline: document.getElementById('modePolylineBtn'),
polygon: document.getElementById('modePolygonBtn'),
choropleth: document.getElementById('modeChoroplethBtn'),
roadDamage: document.getElementById('modeRoadDamageBtn')
roadDamage: document.getElementById('leafletRoadDamageBtn') || document.getElementById('modeRoadDamageBtn'),
miniPoint: document.getElementById('miniModePointBtn'),
miniPolyline: document.getElementById('miniModePolylineBtn'),
miniPolygon: document.getElementById('miniModePolygonBtn')
};
Object.values(modeButtons).forEach(button => button.classList.remove('active'));
Object.values(modeButtons).forEach(button => {
if (button) button.classList.remove('active');
});
if (mode === 'point') {
if (modeButtons.point) modeButtons.point.classList.add('active');
if (modeButtons.miniPoint) modeButtons.miniPoint.classList.add('active');
}
if (mode === 'polyline') {
if (modeButtons.polyline) modeButtons.polyline.classList.add('active');
if (modeButtons.miniPolyline) modeButtons.miniPolyline.classList.add('active');
}
if (mode === 'polygon') {
if (modeButtons.polygon) modeButtons.polygon.classList.add('active');
if (modeButtons.miniPolygon) modeButtons.miniPolygon.classList.add('active');
}
if (modeButtons[mode]) {
modeButtons[mode].classList.add('active');
}
@@ -911,7 +1084,6 @@
toggleChoroplethMode(mode === 'choropleth');
if (mode === 'view') {
hint.innerText = 'Mode default: hanya melihat peta dan atribut data yang sudah dibuat.';
spatialControls.classList.add('hidden');
runActionBtn.innerText = 'Mode Lihat';
runActionBtn.disabled = true;
@@ -922,7 +1094,6 @@
}
if (mode === 'choropleth') {
hint.innerText = 'Mode choropleth aktif: klik kecamatan untuk melihat populasi.';
spatialControls.classList.add('hidden');
runActionBtn.innerText = 'Choropleth Aktif';
runActionBtn.disabled = true;
@@ -933,7 +1104,6 @@
}
if (mode === 'roadDamage') {
hint.innerText = 'Mode laporan jalan rusak aktif: klik peta untuk menandai lokasi kerusakan.';
spatialControls.classList.add('hidden');
runActionBtn.innerText = 'Klik Peta';
runActionBtn.disabled = true;
@@ -948,7 +1118,6 @@
clearDraftBtn.disabled = false;
if (mode === 'point') {
hint.innerText = 'Mode point aktif: klik peta untuk menambah SPBU baru.';
spatialControls.classList.add('hidden');
runActionBtn.innerText = 'Klik Peta';
clearDraftBtn.disabled = true;
@@ -958,13 +1127,27 @@
}
spatialControls.classList.remove('hidden');
hint.innerText = mode === 'polyline'
? 'Mode polyline aktif: gambar jalan lalu isi nama pada modal.'
: 'Mode polygon aktif: gambar parsil lalu isi nama pada modal.';
runActionBtn.innerText = mode === 'polyline' ? 'Gambar Jalan' : 'Gambar Polygon';
setSpatialType(mode);
}
function setEditModeEnabled(enabled) {
isEditModeEnabled = !!enabled;
const toggleBtn = document.getElementById('toggleEditModeBtn');
const editButtonsWrap = document.getElementById('editModeButtons');
if (!toggleBtn || !editButtonsWrap) return;
editButtonsWrap.classList.toggle('hidden', !isEditModeEnabled);
toggleBtn.classList.toggle('active', isEditModeEnabled);
toggleBtn.title = isEditModeEnabled ? 'Nonaktifkan mode edit' : 'Aktifkan mode edit';
if (!isEditModeEnabled) {
setDataMode('view');
return;
}
}
function openRoadDamageModal(lat, lng) {
pendingRoadDamageCoords = { lat, lng };
document.getElementById('roadDamageCoords').innerText = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
@@ -1095,8 +1278,12 @@
icon: createRoadDamageIcon(record.severity)
}).addTo(roadDamageLayer);
marker.roadDamageId = Number(record.id);
marker.bindPopup(getRoadDamagePopup(record));
});
renderInputRegistry();
})
.catch(err => {
console.error('Gagal memuat data jalan rusak:', err);
@@ -1322,38 +1509,78 @@
map.removeControl(drawControl);
drawControl = null;
}
if (type !== 'polyline' && type !== 'polygon') {
return;
}
drawControl = new L.Control.Draw({
edit: false,
position: 'topleft',
edit: {
featureGroup: featureLayer,
edit: true,
remove: true
},
draw: {
polygon: type === 'polygon' ? {
polygon: {
allowIntersection: false,
showArea: true,
shapeOptions: {
color: getFeatureStatusColor('polygon', document.getElementById('featureStatus').value),
color: '#3b82f6',
weight: 3
}
} : false,
polyline: type === 'polyline' ? {
},
polyline: {
shapeOptions: {
color: getFeatureStatusColor('polyline', document.getElementById('featureStatus').value),
color: '#16a34a',
weight: 5
}
} : false,
rectangle: false,
circle: false,
},
rectangle: true,
circle: true,
circlemarker: false,
marker: false
marker: true
}
});
map.addControl(drawControl);
}
function openNewLocationPopup(lat, lng) {
const content = `
<div class="p-4 w-full">
<h3 class="font-bold text-indigo-700 border-b pb-2 mb-3 uppercase text-[10px] tracking-widest">Input SPBU Baru</h3>
<div class="space-y-3">
<input type="text" id="new_nama" placeholder="Nama SPBU" class="w-full border rounded p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm">
<input type="text" id="new_nomor" placeholder="Nomor SPBU" class="w-full border rounded p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm">
<select id="new_status" class="w-full border rounded p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm">
<option value="Ya">Buka 24 Jam</option>
<option value="Tidak">Tidak</option>
</select>
<div class="text-[10px] text-slate-400 bg-slate-50 p-2 rounded border border-dashed text-center">
Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}
</div>
<button onclick="saveNew(${lat}, ${lng})" class="w-full bg-indigo-600 text-white py-2 rounded font-bold text-sm shadow-lg hover:bg-indigo-700 transition">Simpan</button>
</div>
</div>
`;
L.popup().setLatLng([lat, lng]).setContent(content).openOn(map);
}
function convertCircleToPolygon(circleLayer, vertices = 48) {
const center = circleLayer.getLatLng();
const radius = Number(circleLayer.getRadius() || 0);
const points = [];
const latFactor = 111320;
const lngFactor = latFactor * Math.cos((center.lat * Math.PI) / 180);
for (let index = 0; index < vertices; index += 1) {
const angle = (index / vertices) * Math.PI * 2;
const dx = radius * Math.cos(angle);
const dy = radius * Math.sin(angle);
const lat = center.lat + (dy / latFactor);
const lng = center.lng + (dx / (lngFactor || 1));
points.push([lat, lng]);
}
return L.polygon(points);
}
function calculatePolylineLength(layer) {
const latlngs = layer.getLatLngs();
const points = Array.isArray(latlngs[0]) ? latlngs.flat() : latlngs;
@@ -1864,15 +2091,34 @@
});
map.on(L.Draw.Event.CREATED, function(event) {
const { layerType, layer } = event;
const activeType = document.getElementById('featureType').value;
if ((activeType === 'polyline' && layerType !== 'polyline') || (activeType === 'polygon' && layerType !== 'polygon')) {
let { layerType, layer } = event;
if (layerType === 'marker') {
const latlng = layer.getLatLng();
openNewLocationPopup(latlng.lat, latlng.lng);
return;
}
if (layerType === 'circlemarker') {
showMessage('Circle marker dinonaktifkan', 'bg-amber-600 text-white');
return;
}
if (layerType === 'circle') {
layer = convertCircleToPolygon(layer);
layerType = 'polygon';
}
const activeType = (layerType === 'polyline') ? 'polyline' : 'polygon';
document.getElementById('featureType').value = activeType;
const statusSelect = document.getElementById('featureStatus');
statusSelect.innerHTML = buildFeatureStatusOptions(activeType);
if (!statusSelect.value && statusSelect.options.length > 0) {
statusSelect.selectedIndex = 0;
}
const color = getFeatureStatusColor(activeType, document.getElementById('featureStatus').value);
if (layerType === 'polyline') {
if (activeType === 'polyline') {
layer.setStyle({ color, weight: 5 });
} else {
layer.setStyle({ color, fillColor: color, fillOpacity: 0.25, weight: 3 });
@@ -1924,10 +2170,65 @@
}
}
function isSpbuBuka24Jam(status) {
return String(status || '').trim().toLowerCase() === 'ya';
}
function ensureSpbuLayerControl() {
if (spbuLayerControl) return;
spbuLayerControl = L.control.layers(null, {
'SPBU Buka 24 Jam': spbuBuka24JamLayer,
'SPBU Tidak Buka 24 Jam': spbuTidak24JamLayer
}, {
collapsed: false,
position: 'topright'
}).addTo(map);
}
function ensureLeafletUtilityControl() {
if (utilityControl) return;
utilityControl = L.control({ position: 'topleft' });
utilityControl.onAdd = function() {
const container = L.DomUtil.create('div', 'leaflet-bar');
const registryBtn = L.DomUtil.create('a', '', container);
registryBtn.href = '#';
registryBtn.id = 'leafletRegistryBtn';
registryBtn.title = 'Buka Registry';
registryBtn.innerHTML = 'Rg';
const roadDamageBtn = L.DomUtil.create('a', '', container);
roadDamageBtn.href = '#';
roadDamageBtn.id = 'leafletRoadDamageBtn';
roadDamageBtn.title = 'Lapor Jalan Rusak';
roadDamageBtn.innerHTML = 'Jr';
L.DomEvent.disableClickPropagation(container);
L.DomEvent.on(registryBtn, 'click', function(event) {
L.DomEvent.stop(event);
toggleInputRegistry();
});
L.DomEvent.on(roadDamageBtn, 'click', function(event) {
L.DomEvent.stop(event);
setDataMode('roadDamage');
toggleInputRegistry(false);
roadDamageBtn.classList.add('active');
showMessage('Mode laporan jalan rusak aktif. Klik peta untuk menandai lokasi.', 'bg-red-600 text-white');
});
return container;
};
utilityControl.addTo(map);
}
function clearAllMarkers() {
map.eachLayer((layer) => {
if (layer instanceof L.Marker) map.removeLayer(layer);
});
spbuBuka24JamLayer.clearLayers();
spbuTidak24JamLayer.clearLayers();
locationLayerMap = {};
}
@@ -1993,12 +2294,14 @@
if (isNaN(lat) || isNaN(lng)) return;
const color = loc.buka_24jam === 'Ya' ? '#16a34a' : '#dc2626';
const isOpen24 = isSpbuBuka24Jam(loc.buka_24jam);
const color = isOpen24 ? '#16a34a' : '#dc2626';
const targetLayer = isOpen24 ? spbuBuka24JamLayer : spbuTidak24JamLayer;
const marker = L.marker([lat, lng], {
icon: createColoredIcon(color),
draggable: true
}).addTo(map);
}).addTo(targetLayer);
locationLayerMap[loc.id] = marker;
marker.locationData = loc;
@@ -2035,7 +2338,7 @@
// HTML untuk isi Popup (View & Edit Mode)
function getPopupContent(loc) {
const statusClass = loc.buka_24jam === 'Ya' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700';
const statusClass = isSpbuBuka24Jam(loc.buka_24jam) ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700';
return `
<div class="w-full">
@@ -2151,26 +2454,7 @@
if (appMode !== 'point') return;
const lat = e.latlng.lat;
const lng = e.latlng.lng;
const content = `
<div class="p-4 w-full">
<h3 class="font-bold text-indigo-700 border-b pb-2 mb-3 uppercase text-[10px] tracking-widest">Input SPBU Baru</h3>
<div class="space-y-3">
<input type="text" id="new_nama" placeholder="Nama SPBU" class="w-full border rounded p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm">
<input type="text" id="new_nomor" placeholder="Nomor SPBU" class="w-full border rounded p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm">
<select id="new_status" class="w-full border rounded p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm">
<option value="Ya">Buka 24 Jam</option>
<option value="Tidak">Tidak</option>
</select>
<div class="text-[10px] text-slate-400 bg-slate-50 p-2 rounded border border-dashed text-center">
Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}
</div>
<button onclick="saveNew(${lat}, ${lng})" class="w-full bg-indigo-600 text-white py-2 rounded font-bold text-sm shadow-lg hover:bg-indigo-700 transition">Simpan</button>
</div>
</div>
`;
L.popup().setLatLng(e.latlng).setContent(content).openOn(map);
openNewLocationPopup(e.latlng.lat, e.latlng.lng);
});
function saveNew(lat, lng) {
@@ -2245,21 +2529,63 @@
}
window.onload = function () {
ensureSpbuLayerControl();
ensureLeafletUtilityControl();
updateDrawControl();
setDataMode('view');
enforceRegistryDateRange(false);
loadMarkers({ force: true });
loadFeatures(true);
loadRoadDamage(true);
loadExternalRoadNetwork(true);
startAutoRefresh();
};
const toggleEditModeBtn = document.getElementById('toggleEditModeBtn');
if (toggleEditModeBtn) {
toggleEditModeBtn.addEventListener('click', () => {
setEditModeEnabled(!isEditModeEnabled);
});
}
document.getElementById('modeViewBtn').addEventListener('click', () => setDataMode('view'));
document.getElementById('modePointBtn').addEventListener('click', () => setDataMode('point'));
document.getElementById('modePolylineBtn').addEventListener('click', () => setDataMode('polyline'));
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());
const miniModePointBtn = document.getElementById('miniModePointBtn');
if (miniModePointBtn) {
miniModePointBtn.addEventListener('click', () => {
setDataMode('point');
showMessage('Klik peta untuk menambah point SPBU', 'bg-indigo-600 text-white');
});
}
const miniModePolylineBtn = document.getElementById('miniModePolylineBtn');
if (miniModePolylineBtn) {
miniModePolylineBtn.addEventListener('click', () => {
setDataMode('polyline');
startDraw();
});
}
const miniModePolygonBtn = document.getElementById('miniModePolygonBtn');
if (miniModePolygonBtn) {
miniModePolygonBtn.addEventListener('click', () => {
setDataMode('polygon');
startDraw();
});
}
const toggleRegistryDrawerBtn = document.getElementById('toggleRegistryDrawerBtn');
if (toggleRegistryDrawerBtn) {
toggleRegistryDrawerBtn.addEventListener('click', () => toggleInputRegistry());
}
const registryDrawerCloseBtn = document.getElementById('registryDrawerCloseBtn');
if (registryDrawerCloseBtn) {
registryDrawerCloseBtn.addEventListener('click', () => toggleInputRegistry(false));
}
const toggleMainPanelBtn = document.getElementById('toggleMainPanelBtn');
if (toggleMainPanelBtn) {
toggleMainPanelBtn.addEventListener('click', () => toggleMainPanel());
}
document.getElementById('drawStatus').addEventListener('change', () => {
const type = document.getElementById('featureType').value;
document.getElementById('featureStatus').value = document.getElementById('drawStatus').value;
@@ -2296,38 +2622,52 @@
const id = Number(document.getElementById('editFeatureId').value);
deleteFeature(id);
});
document.getElementById('closeInputRegistryBtn').addEventListener('click', () => toggleInputRegistry(false));
document.getElementById('inputTypeFilter').addEventListener('change', renderInputRegistry);
document.getElementById('inputDateFrom').addEventListener('change', () => {
enforceRegistryDateRange(true);
renderInputRegistry();
});
document.getElementById('inputDateTo').addEventListener('change', () => {
enforceRegistryDateRange(true);
renderInputRegistry();
});
document.getElementById('applyInputFilterBtn').addEventListener('click', () => {
enforceRegistryDateRange(true);
renderInputRegistry();
});
document.getElementById('clearInputFilterBtn').addEventListener('click', () => {
document.getElementById('inputSearchQuery').value = '';
document.getElementById('inputTypeFilter').value = 'all';
document.getElementById('inputDateFrom').value = '';
document.getElementById('inputDateTo').value = '';
enforceRegistryDateRange(false);
renderInputRegistry();
});
// Event listener untuk pencarian real-time
document.getElementById('inputSearchQuery').addEventListener('input', () => {
renderInputRegistry();
});
// Event listener untuk filter tipe (instant filtering)
document.getElementById('inputTypeFilter').addEventListener('change', () => {
renderInputRegistry();
});
const registryUi = getRegistryUi();
if (registryUi.type) {
registryUi.type.addEventListener('change', renderInputRegistry);
}
if (registryUi.from) {
registryUi.from.addEventListener('change', () => {
enforceRegistryDateRange(true);
renderInputRegistry();
});
}
if (registryUi.to) {
registryUi.to.addEventListener('change', () => {
enforceRegistryDateRange(true);
renderInputRegistry();
});
}
const registryApplyFilterBtn = document.getElementById('registryApplyFilterBtn');
if (registryApplyFilterBtn) {
registryApplyFilterBtn.addEventListener('click', () => {
enforceRegistryDateRange(true);
renderInputRegistry();
});
}
const registryResetFilterBtn = document.getElementById('registryResetFilterBtn');
if (registryResetFilterBtn) {
registryResetFilterBtn.addEventListener('click', () => {
const ui = getRegistryUi();
if (ui.search) ui.search.value = '';
if (ui.type) ui.type.value = 'all';
if (ui.from) ui.from.value = '';
if (ui.to) ui.to.value = '';
enforceRegistryDateRange(false);
renderInputRegistry();
});
}
if (registryUi.search) {
registryUi.search.addEventListener('input', () => {
renderInputRegistry();
});
registryUi.search.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
renderInputRegistry();
}
});
}
document.getElementById('featureModal').addEventListener('click', (event) => {
if (event.target.id === 'featureModal') closeFeatureEditor();
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB