Files
WEB-GIS/index.html
T
2026-04-07 17:17:55 +07:00

1296 lines
64 KiB
HTML

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sistem GIS Manajemen SPBU</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
html, body { height: 100%; }
body { overflow: hidden; }
.app-shell {
position: relative;
width: 100%;
height: 100vh;
padding: calc(1rem + env(safe-area-inset-top)) 1rem 1rem 1rem;
box-sizing: border-box;
}
#map {
height: calc(100vh - 2rem);
width: 100%;
border-radius: 1rem;
z-index: 1;
}
.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 { margin: 0; width: 280px !important; }
.leaflet-draw-toolbar a { background-color: white; }
/* 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: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; }
</style>
</head>
<body class="bg-slate-50 text-slate-800 font-sans">
<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 class="flex items-start justify-between gap-3">
<div>
<h1 class="text-lg font-black tracking-tight text-indigo-700">GIS SPBU</h1>
<p id="modeHint" class="mt-1 text-xs text-slate-500">Mode default hanya menampilkan peta dan atribut data yang sudah dibuat.</p>
</div>
<button onclick="loadMarkers({ force: true }); loadFeatures(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="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>
<div class="mt-3 flex flex-wrap gap-2">
<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>
</div>
<div id="spatialControls" class="mt-3 hidden rounded-2xl border border-slate-200 bg-white p-3 shadow-sm">
<div id="statusControlWrap">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Status</label>
<select id="drawStatus" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500"></select>
</div>
<div id="measureControlWrap" class="mt-2">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Panjang / Luas</label>
<input id="modeMeasure" type="text" readonly class="w-full rounded-lg border border-slate-200 bg-slate-50 p-2 text-sm text-slate-600">
</div>
<div class="mt-3 flex gap-2">
<button id="runActionBtn" class="flex-1 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-bold text-white transition hover:bg-indigo-700">Mulai</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 class="mt-3 flex gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500">
<span class="rounded-full bg-green-50 px-3 py-1 text-green-700">Buka 24 Jam</span>
<span class="rounded-full bg-red-50 px-3 py-1 text-red-700">Jam Terbatas</span>
</div>
</div>
<!-- Notifikasi -->
<div id="statusMsg" class="custom-msg p-4 rounded-lg shadow-xl font-medium transition-all transform"></div>
<!-- Modal Edit Fitur -->
<div id="featureModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/50 px-4">
<div class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-2xl">
<div class="flex items-start justify-between gap-4 border-b pb-3">
<div>
<h3 class="text-lg font-bold text-slate-800">Edit Fitur Spasial</h3>
<p class="text-sm text-slate-500">Ubah atribut atau simpan geometri yang sudah diedit di peta.</p>
</div>
<button id="closeFeatureModalBtn" class="rounded-lg bg-slate-100 px-3 py-2 text-sm font-bold text-slate-700 hover:bg-slate-200">Tutup</button>
</div>
<div class="mt-4 grid gap-3 sm:grid-cols-2">
<input id="editFeatureId" type="hidden">
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Nama Objek</label>
<input id="editFeatureName" type="text" 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>
<input id="editFeatureKategori" type="text" readonly class="w-full rounded-lg border border-slate-200 bg-slate-50 p-2 text-sm text-slate-600">
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Tipe</label>
<input id="editFeatureType" type="text" readonly class="w-full rounded-lg border border-slate-200 bg-slate-50 p-2 text-sm text-slate-600">
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Status</label>
<select id="editFeatureStatus" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500"></select>
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Panjang / Luas</label>
<input id="editFeatureMeasure" type="text" readonly class="w-full rounded-lg border border-slate-200 bg-slate-50 p-2 text-sm text-slate-600">
</div>
</div>
<div class="mt-5 flex flex-wrap gap-2 border-t pt-4">
<button id="saveFeatureAttrBtn" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-bold text-white hover:bg-indigo-700">Simpan Atribut</button>
<button id="toggleFeatureGeometryBtn" class="rounded-lg bg-emerald-600 px-4 py-2 text-sm font-bold text-white hover:bg-emerald-700">Edit Geometri</button>
<button id="saveFeatureGeometryBtn" class="rounded-lg bg-amber-500 px-4 py-2 text-sm font-bold text-white hover:bg-amber-600">Simpan Geometri</button>
<button id="deleteFeatureBtn" class="rounded-lg bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700">Hapus</button>
</div>
</div>
</div>
<!-- Modal Fitur Baru -->
<div id="newFeatureModal" class="fixed inset-0 z-[10001] hidden items-center justify-center bg-slate-900/50 px-4">
<div class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-2xl">
<div class="flex items-start justify-between gap-4 border-b pb-3">
<div>
<h3 class="text-lg font-bold text-slate-800">Input Fitur Baru</h3>
<p class="text-sm text-slate-500">Isi nama setelah garis atau polygon selesai digambar.</p>
</div>
<button id="closeNewFeatureModalBtn" class="rounded-lg bg-slate-100 px-3 py-2 text-sm font-bold text-slate-700 hover:bg-slate-200">Tutup</button>
</div>
<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">
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Kategori</label>
<input id="newFeatureKategori" type="text" readonly class="w-full rounded-lg border border-slate-200 bg-slate-50 p-2 text-sm text-slate-600">
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Tipe</label>
<input id="newFeatureTypeLabel" type="text" readonly class="w-full rounded-lg border border-slate-200 bg-slate-50 p-2 text-sm text-slate-600">
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Status</label>
<select id="newFeatureStatus" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500"></select>
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Panjang / Luas</label>
<input id="newFeatureMeasure" type="text" readonly class="w-full rounded-lg border border-slate-200 bg-slate-50 p-2 text-sm text-slate-600">
</div>
</div>
<div class="mt-5 flex flex-wrap gap-2 border-t pt-4">
<button id="saveNewFeatureBtn" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-bold text-white hover:bg-indigo-700">Simpan</button>
<button id="cancelNewFeatureBtn" class="rounded-lg bg-slate-100 px-4 py-2 text-sm font-bold text-slate-700 hover:bg-slate-200">Batal</button>
</div>
</div>
</div>
<!-- Area Peta -->
<div id="map" class="shadow-lg border border-slate-200 bg-slate-200"></div>
<select id="featureType" class="hidden">
<option value="polyline">polyline</option>
<option value="polygon">polygon</option>
</select>
<select id="featureStatus" class="hidden"></select>
</div>
<!-- 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>
// Inisialisasi Peta (Default Pontianak)
const map = L.map('map').setView([-0.0263, 109.3425], 13);
const REFRESH_INTERVAL_MS = 5000;
let autoRefreshTimer = null;
let isSyncing = false;
let suppressAutoReload = false;
let isFeatureDrawing = false;
let isFeatureSaving = false;
let latestSnapshotHash = '';
let featureLayer = L.featureGroup().addTo(map);
let drawControl = null;
let activeDrawHandler = null;
let currentDrawMode = 'polyline';
let appMode = 'view';
let pendingShape = null;
let pendingNewFeatureLayer = null;
let featureLayerMap = {};
let editingFeatureId = null;
const FEATURE_STATUS_OPTIONS = {
polyline: [
{ value: 'Jalan Nasional', label: 'Jalan Nasional', color: '#16a34a' },
{ value: 'Jalan Provinsi', label: 'Jalan Provinsi', color: '#f59e0b' },
{ value: 'Jalan Kabupaten', label: 'Jalan Kabupaten', color: '#dc2626' }
],
polygon: [
{ value: 'SHM', label: 'SHM', color: '#10b981' },
{ value: 'HGB', label: 'HGB', color: '#3b82f6' },
{ value: 'HGU', label: 'HGU', color: '#8b5cf6' },
{ value: 'HP', label: 'HP', color: '#64748b' }
]
};
function getFeatureStatusColor(type, status) {
const options = FEATURE_STATUS_OPTIONS[type] || [];
const found = options.find(item => item.value === status);
return found ? found.color : '#0f172a';
}
function formatMeters(value) {
return `${value.toFixed(2)} m`;
}
function formatSquareMeters(value) {
return `${value.toFixed(2)} m²`;
}
function getFeatureOptions(type) {
return FEATURE_STATUS_OPTIONS[type] || [];
}
function setSpatialType(type) {
document.getElementById('featureType').value = type;
const statusSelect = document.getElementById('featureStatus');
const drawStatus = document.getElementById('drawStatus');
drawStatus.innerHTML = '';
getFeatureOptions(type).forEach(option => {
const el = document.createElement('option');
el.value = option.value;
el.textContent = option.label;
drawStatus.appendChild(el);
});
if (!drawStatus.value) drawStatus.selectedIndex = 0;
statusSelect.innerHTML = '';
Array.from(drawStatus.options).forEach(option => {
statusSelect.appendChild(option.cloneNode(true));
});
statusSelect.value = drawStatus.value;
currentDrawMode = type;
updateDrawControl(type);
updateFeatureMeasurePreview();
}
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');
const clearDraftBtn = document.getElementById('clearDraftBtn');
const spatialControls = document.getElementById('spatialControls');
const modeButtons = {
view: document.getElementById('modeViewBtn'),
point: document.getElementById('modePointBtn'),
polyline: document.getElementById('modePolylineBtn'),
polygon: document.getElementById('modePolygonBtn')
};
Object.values(modeButtons).forEach(button => button.classList.remove('active'));
if (modeButtons[mode]) {
modeButtons[mode].classList.add('active');
}
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;
clearDraftBtn.disabled = true;
updateDrawControl(null);
document.getElementById('modeMeasure').value = '';
return;
}
runActionBtn.disabled = false;
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;
updateDrawControl(null);
document.getElementById('modeMeasure').value = '';
return;
}
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 updateFeatureMeasurePreview() {
const type = document.getElementById('featureType').value;
const layer = pendingShape;
const measureField = document.getElementById('modeMeasure');
if (!layer) {
measureField.value = type === 'polyline' ? 'Panjang otomatis setelah gambar' : 'Luas otomatis setelah gambar';
return;
}
if (type === 'polyline') {
measureField.value = formatMeters(calculatePolylineLength(layer));
} else {
measureField.value = formatSquareMeters(calculatePolygonArea(layer));
}
}
function updateDrawControl(type) {
if (drawControl) {
map.removeControl(drawControl);
drawControl = null;
}
if (type !== 'polyline' && type !== 'polygon') {
return;
}
drawControl = new L.Control.Draw({
edit: false,
draw: {
polygon: type === 'polygon' ? {
allowIntersection: false,
showArea: true,
shapeOptions: {
color: getFeatureStatusColor('polygon', document.getElementById('featureStatus').value),
weight: 3
}
} : false,
polyline: type === 'polyline' ? {
shapeOptions: {
color: getFeatureStatusColor('polyline', document.getElementById('featureStatus').value),
weight: 5
}
} : false,
rectangle: false,
circle: false,
circlemarker: false,
marker: false
}
});
map.addControl(drawControl);
}
function calculatePolylineLength(layer) {
const latlngs = layer.getLatLngs();
const points = Array.isArray(latlngs[0]) ? latlngs.flat() : latlngs;
let total = 0;
for (let i = 1; i < points.length; i += 1) {
total += map.distance(points[i - 1], points[i]);
}
return total;
}
function calculatePolygonArea(layer) {
const latlngs = layer.getLatLngs();
const rings = Array.isArray(latlngs[0]) ? latlngs[0] : latlngs;
if (L.GeometryUtil && typeof L.GeometryUtil.geodesicArea === 'function') {
return L.GeometryUtil.geodesicArea(rings);
}
return 0;
}
function geometryToCoordinates(layer) {
const latlngs = layer.getLatLngs();
const type = document.getElementById('featureType').value;
if (type === 'polyline') {
return latlngs.map(point => [point.lat, point.lng]);
}
const ring = Array.isArray(latlngs[0]) ? latlngs[0] : latlngs;
return [ring.map(point => [point.lat, point.lng])];
}
function getFeaturePopup(feature) {
const color = getFeatureStatusColor(feature.type, feature.status_objek);
const measureText = feature.type === 'polyline'
? formatMeters(Number(feature.nilai_ukur))
: formatSquareMeters(Number(feature.nilai_ukur));
return `
<div class="w-full p-4">
<h3 class="text-lg font-bold text-slate-800">${feature.nama_objek}</h3>
<p class="mt-1 text-sm text-slate-600">Kategori: ${feature.kategori}</p>
<p class="mt-1 text-sm text-slate-600">Status: <span class="font-semibold" style="color: ${color}">${feature.status_objek}</span></p>
<p class="mt-1 text-sm text-slate-600">${feature.type === 'polyline' ? 'Panjang' : 'Luas'}: <span class="font-semibold">${measureText}</span></p>
<div class="mt-4 grid grid-cols-2 gap-2 border-t pt-3">
<button onclick="openFeatureEditor(${feature.id})" class="rounded-lg bg-indigo-50 px-3 py-2 text-xs font-bold text-indigo-700 hover:bg-indigo-100">Edit Atribut</button>
<button onclick="toggleFeatureGeometryEditing(${feature.id})" class="rounded-lg bg-emerald-50 px-3 py-2 text-xs font-bold text-emerald-700 hover:bg-emerald-100">Edit Geometri</button>
<button onclick="saveFeatureGeometry(${feature.id})" class="rounded-lg bg-amber-50 px-3 py-2 text-xs font-bold text-amber-700 hover:bg-amber-100">Simpan Geometri</button>
<button onclick="deleteFeature(${feature.id})" class="rounded-lg bg-red-50 px-3 py-2 text-xs font-bold text-red-700 hover:bg-red-100">Hapus</button>
</div>
</div>
`;
}
function buildFeatureStatusOptions(type) {
return getFeatureOptions(type).map(option => `<option value="${option.value}">${option.label}</option>`).join('');
}
function geometryToLayer(type, geometry) {
if (type === 'polyline') {
return L.polyline(geometry);
}
return L.polygon(geometry);
}
function getFeatureGeometry(layer) {
const type = layer instanceof L.Polygon ? 'polygon' : 'polyline';
const latlngs = layer.getLatLngs();
if (type === 'polyline') {
return latlngs.map(point => [point.lat, point.lng]);
}
const ring = Array.isArray(latlngs[0]) ? latlngs[0] : latlngs;
return [ring.map(point => [point.lat, point.lng])];
}
function updateFeatureLayerStyle(layer, type, status) {
const color = getFeatureStatusColor(type, status);
layer.setStyle(type === 'polyline'
? { color, weight: 5 }
: { color, fillColor: color, fillOpacity: 0.25, weight: 3 }
);
}
function getFeatureLayerById(id) {
return featureLayerMap[id] || null;
}
function clearFeatureInput() {
document.getElementById('modeMeasure').value = '';
document.getElementById('newFeatureName').value = '';
document.getElementById('newFeatureMeasure').value = '';
document.getElementById('newFeatureKategori').value = '';
document.getElementById('newFeatureTypeLabel').value = '';
document.getElementById('newFeatureStatus').innerHTML = '';
pendingShape = null;
pendingNewFeatureLayer = null;
if (appMode === 'polyline' || appMode === 'polygon') {
setSpatialType(appMode);
}
}
function openNewFeatureModal(layer) {
const type = document.getElementById('featureType').value;
const status = document.getElementById('featureStatus').value;
const modal = document.getElementById('newFeatureModal');
pendingNewFeatureLayer = layer;
document.getElementById('newFeatureKategori').value = type === 'polyline' ? 'Jalan' : 'Tanah';
document.getElementById('newFeatureTypeLabel').value = type === 'polyline' ? 'Jalan (Polyline)' : 'Parsil Tanah (Polygon)';
document.getElementById('newFeatureStatus').innerHTML = buildFeatureStatusOptions(type);
document.getElementById('newFeatureStatus').value = status;
document.getElementById('newFeatureMeasure').value = type === 'polyline'
? formatMeters(calculatePolylineLength(layer))
: formatSquareMeters(calculatePolygonArea(layer));
document.getElementById('newFeatureName').value = '';
modal.classList.remove('hidden');
modal.classList.add('flex');
suppressAutoReload = true;
}
function closeNewFeatureModal(removeLayer = true) {
const modal = document.getElementById('newFeatureModal');
if (removeLayer && pendingNewFeatureLayer) {
featureLayer.removeLayer(pendingNewFeatureLayer);
}
modal.classList.add('hidden');
modal.classList.remove('flex');
clearFeatureInput();
}
function loadFeatures(force = false) {
if (!force && (document.hidden || suppressAutoReload || isFeatureDrawing || isFeatureSaving)) return;
fetch('get_features.php')
.then(res => {
if (!res.ok) throw new Error('HTTP Error: ' + res.status);
return res.text();
})
.then(text => {
const data = JSON.parse(text);
if (!Array.isArray(data)) {
if (data && data.status === 'error') {
throw new Error(data.message || 'Terjadi error pada server');
}
throw new Error('Format data fitur tidak valid');
}
featureLayer.clearLayers();
featureLayerMap = {};
data.forEach(feature => {
const color = getFeatureStatusColor(feature.type, feature.status_objek);
const geometry = feature.geometry_data;
if (feature.type === 'polyline') {
const layer = L.polyline(geometry, { color, weight: 5 });
layer.featureData = feature;
layer.featureType = 'polyline';
featureLayerMap[feature.id] = layer;
featureLayer.addLayer(layer);
layer.bindPopup(getFeaturePopup(feature));
} else if (feature.type === 'polygon') {
const layer = L.polygon(geometry, { color, fillColor: color, fillOpacity: 0.25, weight: 3 });
layer.featureData = feature;
layer.featureType = 'polygon';
featureLayerMap[feature.id] = layer;
featureLayer.addLayer(layer);
layer.bindPopup(getFeaturePopup(feature));
}
});
})
.catch(err => {
console.error('Gagal memuat fitur spasial:', err);
if (force) {
showMessage('Gagal memuat data jalan/parsil', 'bg-red-600 text-white');
}
});
}
function saveFeature(layer) {
const type = document.getElementById('featureType').value;
const nama = document.getElementById('newFeatureName').value.trim();
const status = document.getElementById('featureStatus').value;
const kategori = type === 'polyline' ? 'Jalan' : 'Tanah';
const nilai = type === 'polyline' ? calculatePolylineLength(layer) : calculatePolygonArea(layer);
const geometry = geometryToCoordinates(layer);
if (!nama) {
showMessage('Nama objek wajib diisi', 'bg-red-600 text-white');
return;
}
const formData = new FormData();
formData.append('nama', nama);
formData.append('kategori', kategori);
formData.append('status', status);
formData.append('type', type);
formData.append('geometry', JSON.stringify(geometry));
formData.append('nilai', nilai);
isFeatureSaving = true;
fetch('save_feature.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'save_feature.php'))
.then(data => {
if (data.status === 'success') {
showMessage('✅ Fitur berhasil disimpan', 'bg-green-600 text-white');
loadFeatures(true);
clearFeatureInput();
closeNewFeatureModal(false);
} else {
featureLayer.removeLayer(layer);
throw new Error(data.message || 'Gagal menyimpan fitur');
}
})
.catch(err => {
featureLayer.removeLayer(layer);
showMessage(err.message || 'Gagal menyimpan fitur', 'bg-red-600 text-white');
})
.finally(() => {
isFeatureSaving = false;
});
}
function saveNewFeatureFromModal() {
const layer = pendingNewFeatureLayer;
if (!layer) {
showMessage('Tidak ada fitur baru untuk disimpan', 'bg-red-600 text-white');
return;
}
if (!document.getElementById('newFeatureName').value.trim()) {
showMessage('Nama objek wajib diisi', 'bg-red-600 text-white');
return;
}
document.getElementById('featureStatus').value = document.getElementById('newFeatureStatus').value;
saveFeature(layer);
}
function resetSpatialDraft(type) {
const measureField = document.getElementById('modeMeasure');
measureField.value = '';
if (pendingNewFeatureLayer && document.getElementById('featureType').value === type) {
closeNewFeatureModal(true);
return;
}
pendingShape = null;
pendingNewFeatureLayer = null;
updateFeatureMeasurePreview();
showMessage(`Draft ${type === 'polyline' ? 'jalan' : 'polygon'} dibersihkan`, 'bg-slate-700 text-white');
}
function openFeatureEditor(id) {
const layer = getFeatureLayerById(id);
if (!layer || !layer.featureData) {
showMessage('Fitur tidak ditemukan', 'bg-red-600 text-white');
return;
}
if (editingFeatureId && featureLayerMap[editingFeatureId] && featureLayerMap[editingFeatureId].editing) {
featureLayerMap[editingFeatureId].editing.disable();
editingFeatureId = null;
}
const feature = layer.featureData;
const modal = document.getElementById('featureModal');
document.getElementById('editFeatureId').value = feature.id;
document.getElementById('editFeatureName').value = feature.nama_objek;
document.getElementById('editFeatureKategori').value = feature.kategori;
document.getElementById('editFeatureType').value = feature.type;
document.getElementById('editFeatureStatus').innerHTML = buildFeatureStatusOptions(feature.type);
document.getElementById('editFeatureStatus').value = feature.status_objek;
document.getElementById('editFeatureMeasure').value = feature.type === 'polyline'
? formatMeters(Number(feature.nilai_ukur))
: formatSquareMeters(Number(feature.nilai_ukur));
modal.classList.remove('hidden');
modal.classList.add('flex');
suppressAutoReload = true;
}
function closeFeatureEditor() {
if (editingFeatureId && featureLayerMap[editingFeatureId] && featureLayerMap[editingFeatureId].editing) {
featureLayerMap[editingFeatureId].editing.disable();
editingFeatureId = null;
}
const modal = document.getElementById('featureModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)') || !!editingFeatureId;
}
function collectFeatureFormData(id, layer) {
const feature = layer.featureData;
const type = feature.type;
const nama = document.getElementById('editFeatureName').value.trim();
const kategori = document.getElementById('editFeatureKategori').value.trim();
const status = document.getElementById('editFeatureStatus').value;
const geometry = JSON.stringify(getFeatureGeometry(layer));
const nilai = type === 'polyline' ? calculatePolylineLength(layer) : calculatePolygonArea(layer);
return { id, nama, kategori, status, type, geometry, nilai };
}
function saveFeatureAttributes() {
const id = Number(document.getElementById('editFeatureId').value);
const layer = getFeatureLayerById(id);
if (!layer) {
showMessage('Fitur tidak ditemukan', 'bg-red-600 text-white');
return;
}
const payload = collectFeatureFormData(id, layer);
if (!payload.nama) {
showMessage('Nama objek wajib diisi', 'bg-red-600 text-white');
return;
}
const formData = new FormData();
Object.entries(payload).forEach(([key, value]) => formData.append(key, value));
fetch('update_feature.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'update_feature.php'))
.then(data => {
if (data.status === 'success') {
layer.featureData.nama_objek = payload.nama;
layer.featureData.kategori = payload.kategori;
layer.featureData.status_objek = payload.status;
layer.featureData.nilai_ukur = payload.nilai;
updateFeatureLayerStyle(layer, payload.type, payload.status);
layer.bindPopup(getFeaturePopup(layer.featureData));
document.getElementById('editFeatureMeasure').value = payload.type === 'polyline'
? formatMeters(payload.nilai)
: formatSquareMeters(payload.nilai);
showMessage('✅ Atribut fitur diperbarui', 'bg-green-600 text-white');
loadFeatures(true);
} else {
throw new Error(data.message || 'Gagal menyimpan perubahan');
}
})
.catch(err => showMessage(err.message || 'Gagal menyimpan perubahan', 'bg-red-600 text-white'));
}
function toggleFeatureGeometryEditing(id) {
const layer = getFeatureLayerById(id);
if (!layer) {
showMessage('Fitur tidak ditemukan', 'bg-red-600 text-white');
return;
}
if (!layer.editing || typeof layer.editing.enable !== 'function') {
showMessage('Edit geometri tidak didukung untuk layer ini', 'bg-red-600 text-white');
return;
}
if (editingFeatureId === id) {
layer.editing.disable();
editingFeatureId = null;
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)');
showMessage('Mode edit geometri dimatikan', 'bg-slate-700 text-white');
return;
}
if (editingFeatureId && featureLayerMap[editingFeatureId] && featureLayerMap[editingFeatureId].editing) {
featureLayerMap[editingFeatureId].editing.disable();
}
layer.editing.enable();
editingFeatureId = id;
suppressAutoReload = true;
showMessage('Geser titik vertex lalu klik Simpan Geometri', 'bg-emerald-600 text-white');
}
function saveFeatureGeometry(id) {
const layer = getFeatureLayerById(id);
if (!layer || !layer.featureData) {
showMessage('Fitur tidak ditemukan', 'bg-red-600 text-white');
return;
}
if (layer.editing && typeof layer.editing.disable === 'function') {
layer.editing.disable();
}
editingFeatureId = null;
const payload = collectFeatureFormData(id, layer);
const formData = new FormData();
Object.entries(payload).forEach(([key, value]) => formData.append(key, value));
fetch('update_feature.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'update_feature.php'))
.then(data => {
if (data.status === 'success') {
layer.featureData.geometry_data = JSON.parse(payload.geometry);
layer.featureData.nilai_ukur = payload.nilai;
updateFeatureLayerStyle(layer, payload.type, payload.status);
layer.setLatLngs(layer.featureData.geometry_data);
layer.bindPopup(getFeaturePopup(layer.featureData));
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)');
document.getElementById('editFeatureMeasure').value = payload.type === 'polyline'
? formatMeters(payload.nilai)
: formatSquareMeters(payload.nilai);
showMessage('✅ Geometri fitur diperbarui', 'bg-green-600 text-white');
loadFeatures(true);
} else {
throw new Error(data.message || 'Gagal menyimpan geometri');
}
})
.catch(err => showMessage(err.message || 'Gagal menyimpan geometri', 'bg-red-600 text-white'));
}
function deleteFeature(id) {
if (!confirm('Hapus fitur ini dari database?')) return;
const layer = getFeatureLayerById(id);
const formData = new FormData();
formData.append('id', id);
fetch('delete_feature.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'delete_feature.php'))
.then(data => {
if (data.status === 'success') {
if (layer) featureLayer.removeLayer(layer);
delete featureLayerMap[id];
if (editingFeatureId === id) editingFeatureId = null;
closeFeatureEditor();
showMessage('🗑️ Fitur berhasil dihapus', 'bg-orange-600 text-white');
} else {
throw new Error(data.message || 'Gagal menghapus fitur');
}
})
.catch(err => showMessage(err.message || 'Gagal menghapus fitur', 'bg-red-600 text-white'));
}
function startDraw() {
const type = document.getElementById('featureType').value;
const status = document.getElementById('featureStatus').value;
if (appMode !== 'polyline' && appMode !== 'polygon') {
showMessage('Pilih mode Polyline atau Polygon dulu', 'bg-slate-700 text-white');
return;
}
if (!status) {
showMessage('Pilih status terlebih dahulu', 'bg-red-600 text-white');
return;
}
const shapeOptions = type === 'polyline'
? {
color: getFeatureStatusColor('polyline', status),
weight: 5
}
: {
color: getFeatureStatusColor('polygon', status),
fillColor: getFeatureStatusColor('polygon', status),
fillOpacity: 0.25,
weight: 3
};
const toolbar = document.querySelector(type === 'polyline' ? '.leaflet-draw-draw-polyline' : '.leaflet-draw-draw-polygon');
if (toolbar) {
isFeatureDrawing = true;
suppressAutoReload = true;
map.closePopup();
toolbar.click();
return;
}
// Fallback: aktifkan handler draw langsung jika tombol toolbar tidak ditemukan.
if (activeDrawHandler && typeof activeDrawHandler.disable === 'function') {
activeDrawHandler.disable();
}
activeDrawHandler = type === 'polyline'
? new L.Draw.Polyline(map, { shapeOptions })
: new L.Draw.Polygon(map, { allowIntersection: false, showArea: true, shapeOptions });
isFeatureDrawing = true;
suppressAutoReload = true;
map.closePopup();
activeDrawHandler.enable();
showMessage('Mode gambar aktif, klik pada peta untuk mulai menggambar', 'bg-indigo-600 text-white');
}
map.on(L.Draw.Event.DRAWSTOP, function() {
if (activeDrawHandler && typeof activeDrawHandler.disable === 'function') {
activeDrawHandler.disable();
activeDrawHandler = null;
}
isFeatureDrawing = false;
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)');
});
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')) {
return;
}
const color = getFeatureStatusColor(activeType, document.getElementById('featureStatus').value);
if (layerType === 'polyline') {
layer.setStyle({ color, weight: 5 });
} else {
layer.setStyle({ color, fillColor: color, fillOpacity: 0.25, weight: 3 });
}
pendingShape = layer;
featureLayer.addLayer(layer);
updateFeatureMeasurePreview();
isFeatureDrawing = false;
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)');
openNewFeatureModal(layer);
});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
// Fungsi membuat ikon SVG berwarna
function createColoredIcon(color) {
return L.divIcon({
className: "custom-marker",
html: `<svg width="30" height="42" viewBox="0 0 24 24" fill="${color}" stroke="white" stroke-width="1.5" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0C7.58 0 4 3.58 4 8c0 5.25 7 13 8 13s8-7.75 8-13c0-4.42-3.58-8-8-8zm0 11c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/>
</svg>`,
iconSize: [30, 42],
iconAnchor: [15, 42],
popupAnchor: [0, -40]
});
}
function setConnectionStatus(message, colorClass) {
const statusEl = document.getElementById('connectionStatus');
statusEl.innerText = message;
statusEl.className = `mt-2 text-[10px] font-bold uppercase tracking-wider ${colorClass}`;
}
function clearAllMarkers() {
map.eachLayer((layer) => {
if (layer instanceof L.Marker) map.removeLayer(layer);
});
}
function createSnapshotHash(locations) {
const normalized = locations
.map(loc => ({
id: Number(loc.id),
nama: loc.nama,
nomor: loc.nomor,
buka_24jam: loc.buka_24jam,
latitude: Number(loc.latitude),
longitude: Number(loc.longitude)
}))
.sort((a, b) => a.id - b.id);
return JSON.stringify(normalized);
}
// Memuat semua data dari database
function loadMarkers(options = {}) {
const { silent = false, force = false } = options;
if (isSyncing) return;
if (!force && (document.hidden || suppressAutoReload)) return;
isSyncing = true;
if (!silent) {
setConnectionStatus("🔄 Memuat data...", "text-blue-500");
}
fetch('get_location.php')
.then(res => {
if (!res.ok) throw new Error('HTTP Error: ' + res.status);
return res.text(); // Ambil teks dulu untuk debug jika ada error PHP
})
.then(text => {
try {
const data = JSON.parse(text);
if (!Array.isArray(data)) {
if (data && data.status === 'error') {
throw new Error(data.message || 'Terjadi error pada server');
}
throw new Error('Format data bukan array');
}
const currentHash = createSnapshotHash(data);
if (!force && currentHash === latestSnapshotHash) {
if (!silent) {
setConnectionStatus("✅ Terhubung ke database", "text-green-600");
}
return;
}
latestSnapshotHash = currentHash;
clearAllMarkers();
document.getElementById('totalCount').innerText = data.length;
data.forEach(loc => {
// Konversi lat/lng ke float (MySQL Decimal sering dikirim sebagai string)
const lat = parseFloat(loc.latitude);
const lng = parseFloat(loc.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const color = loc.buka_24jam === 'Ya' ? '#16a34a' : '#dc2626';
const marker = L.marker([lat, lng], {
icon: createColoredIcon(color),
draggable: true
}).addTo(map);
// Event Drag & Drop untuk Update Lokasi
marker.on('dragend', function(event) {
const newPos = event.target.getLatLng();
updateMarkerLocation(loc.id, newPos.lat, newPos.lng);
});
marker.bindPopup(getPopupContent(loc));
});
setConnectionStatus("✅ Terhubung ke database", "text-green-600");
} catch (e) {
console.error("JSON Parse Error:", e, "Raw text:", text);
throw new Error('Gagal membaca data dari server (JSON Error)');
}
})
.catch(err => {
console.error("Fetch Error:", err);
setConnectionStatus(`❌ ${err.message || 'Gagal terhubung ke database'}`, "text-red-600");
// Jika di Canvas Preview, berikan info spesifik
if (window.location.hostname === "") {
console.warn("Info: Jalankan proyek ini di Laragon (http://localhost/...) agar PHP dapat berjalan.");
}
})
.finally(() => {
isSyncing = false;
});
}
// 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';
return `
<div class="w-full">
<!-- Mode Tampilan -->
<div id="view-mode-${loc.id}" class="p-4">
<h3 class="font-bold text-lg text-slate-800 leading-tight">${loc.nama}</h3>
<p class="text-sm text-slate-600 mt-1 italic">Telp: ${loc.nomor}</p>
<div class="mt-2">
<span class="inline-block px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wide ${statusClass}">
24 Jam: ${loc.buka_24jam}
</span>
</div>
<div class="mt-4 flex gap-2 border-t pt-3">
<button onclick="toggleEdit(${loc.id})" class="flex-1 bg-indigo-50 text-indigo-600 py-2 rounded text-xs font-bold hover:bg-indigo-100 transition">Edit</button>
<button onclick="deleteData(${loc.id})" class="flex-1 bg-red-50 text-red-600 py-2 rounded text-xs font-bold hover:bg-red-100 transition">Hapus</button>
</div>
</div>
<!-- Mode Edit -->
<div id="edit-mode-${loc.id}" class="hidden p-4 bg-slate-50">
<h4 class="font-bold text-[10px] text-slate-400 mb-3 uppercase tracking-widest border-b pb-1">Update Informasi</h4>
<div class="space-y-3">
<input type="text" id="edit_nama_${loc.id}" value="${loc.nama}" class="w-full border p-2 text-sm rounded outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm">
<input type="text" id="edit_nomor_${loc.id}" value="${loc.nomor}" class="w-full border p-2 text-sm rounded outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm">
<select id="edit_status_${loc.id}" class="w-full border p-2 text-sm rounded outline-none focus:ring-2 focus:ring-indigo-500 shadow-sm">
<option value="Ya" ${loc.buka_24jam === 'Ya' ? 'selected' : ''}>Buka 24 Jam</option>
<option value="Tidak" ${loc.buka_24jam === 'Tidak' ? 'selected' : ''}>Tidak</option>
</select>
<div class="flex gap-2 pt-1">
<button onclick="saveEdit(${loc.id})" class="flex-1 bg-green-600 text-white py-2 rounded text-xs font-bold shadow-md hover:bg-green-700 transition">Simpan</button>
<button onclick="toggleEdit(${loc.id})" class="flex-1 bg-slate-300 text-slate-700 py-2 rounded text-xs font-bold hover:bg-slate-400 transition">Batal</button>
</div>
</div>
</div>
</div>
`;
}
// Fungsi Update Koordinat (Drag)
function updateMarkerLocation(id, lat, lng) {
const formData = new FormData();
formData.append('id', id);
formData.append('lat', lat);
formData.append('lng', lng);
fetch('update_coords.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'update_coords.php'))
.then(data => {
if(data.status === 'success') showMessage("📍 Lokasi diperbarui!", "bg-green-600 text-white");
})
.catch(err => showMessage("Gagal simpan lokasi", "bg-red-600 text-white"));
}
// Fungsi Hapus Data
function deleteData(id) {
if(!confirm("Hapus data ini dari database?")) return;
const formData = new FormData();
formData.append('id', id);
fetch('delete_location.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'delete_location.php'))
.then(data => {
if(data.status === 'success') {
showMessage("🗑️ Data berhasil dihapus", "bg-orange-600 text-white");
loadMarkers({ force: true });
}
})
.catch(err => showMessage("Gagal hapus data", "bg-red-600 text-white"));
}
function toggleEdit(id) {
document.getElementById(`view-mode-${id}`).classList.toggle('hidden');
document.getElementById(`edit-mode-${id}`).classList.toggle('hidden');
// Tahan auto refresh saat ada popup edit terbuka agar input tidak tertimpa.
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)');
}
// Fungsi Update Atribut
function saveEdit(id) {
const nama = document.getElementById(`edit_nama_${id}`).value;
const nomor = document.getElementById(`edit_nomor_${id}`).value;
const status = document.getElementById(`edit_status_${id}`).value;
if(!nama || !nomor) return alert("Field tidak boleh kosong!");
const formData = new FormData();
formData.append('id', id);
formData.append('nama', nama);
formData.append('nomor', nomor);
formData.append('status', status);
fetch('update_details.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'update_details.php'))
.then(data => {
if(data.status === 'success') {
suppressAutoReload = false;
showMessage("✅ Data berhasil diperbarui", "bg-indigo-600 text-white");
loadMarkers({ force: true });
}
})
.catch(err => showMessage("Gagal update data", "bg-red-600 text-white"));
}
// Klik Peta untuk Input Baru
map.on('click', function(e) {
if (isFeatureDrawing) return;
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);
});
function saveNew(lat, lng) {
const nama = document.getElementById('new_nama').value;
const nomor = document.getElementById('new_nomor').value;
const status = document.getElementById('new_status').value;
if(!nama || !nomor) return alert("Mohon lengkapi semua field.");
const formData = new FormData();
formData.append('nama', nama);
formData.append('nomor', nomor);
formData.append('status', status);
formData.append('lat', lat);
formData.append('lng', lng);
fetch('save_location.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'save_location.php'))
.then(data => {
if(data.status === 'success') {
showMessage("✨ Berhasil disimpan ke database!", "bg-green-600 text-white");
map.closePopup();
loadMarkers({ force: true });
}
})
.catch(err => showMessage("Gagal menyimpan ke server", "bg-red-600 text-white"));
}
function startAutoRefresh() {
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
autoRefreshTimer = setInterval(() => {
loadMarkers({ silent: true });
loadFeatures();
}, REFRESH_INTERVAL_MS);
}
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
loadMarkers({ silent: true, force: true });
loadFeatures(true);
}
});
// Fungsi Notifikasi Toast
function showMessage(text, classes) {
const el = document.getElementById('statusMsg');
el.innerText = text;
el.className = `custom-msg p-4 rounded-lg shadow-2xl font-medium ${classes}`;
el.style.display = 'block';
setTimeout(() => { el.style.display = 'none'; }, 3000);
}
async function parseJsonResponse(res, endpointName) {
const text = await res.text();
if (!text || !text.trim()) {
throw new Error(`Respons kosong dari ${endpointName}`);
}
let data;
try {
data = JSON.parse(text);
} catch (error) {
throw new Error(`Respons ${endpointName} bukan JSON valid: ${text.slice(0, 160)}`);
}
if (!res.ok) {
throw new Error(data.message || `HTTP ${res.status} dari ${endpointName}`);
}
return data;
}
window.onload = function () {
setDataMode('view');
loadMarkers({ force: true });
loadFeatures(true);
startAutoRefresh();
};
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('drawStatus').addEventListener('change', () => {
const type = document.getElementById('featureType').value;
document.getElementById('featureStatus').value = document.getElementById('drawStatus').value;
if (type === 'polyline' || type === 'polygon') {
updateDrawControl(type);
}
});
document.getElementById('runActionBtn').addEventListener('click', () => {
if (appMode === 'polyline' || appMode === 'polygon') {
startDraw();
return;
}
if (appMode === 'point') {
showMessage('Klik peta untuk menambah point SPBU', 'bg-indigo-600 text-white');
}
});
document.getElementById('clearDraftBtn').addEventListener('click', () => {
if (appMode === 'polyline' || appMode === 'polygon') {
resetSpatialDraft(appMode);
}
});
document.getElementById('closeFeatureModalBtn').addEventListener('click', closeFeatureEditor);
document.getElementById('saveFeatureAttrBtn').addEventListener('click', saveFeatureAttributes);
document.getElementById('toggleFeatureGeometryBtn').addEventListener('click', () => {
const id = Number(document.getElementById('editFeatureId').value);
toggleFeatureGeometryEditing(id);
});
document.getElementById('saveFeatureGeometryBtn').addEventListener('click', () => {
const id = Number(document.getElementById('editFeatureId').value);
saveFeatureGeometry(id);
});
document.getElementById('deleteFeatureBtn').addEventListener('click', () => {
const id = Number(document.getElementById('editFeatureId').value);
deleteFeature(id);
});
document.getElementById('featureModal').addEventListener('click', (event) => {
if (event.target.id === 'featureModal') closeFeatureEditor();
});
document.getElementById('closeNewFeatureModalBtn').addEventListener('click', () => closeNewFeatureModal(true));
document.getElementById('cancelNewFeatureBtn').addEventListener('click', () => closeNewFeatureModal(true));
document.getElementById('saveNewFeatureBtn').addEventListener('click', saveNewFeatureFromModal);
document.getElementById('newFeatureModal').addEventListener('click', (event) => {
if (event.target.id === 'newFeatureModal') closeNewFeatureModal(true);
});
</script>
</body>
</html>