Files
FH 6901d07126 Refactor database interactions and schema updates
- Updated column names in insert_data.php and update_data.php to match the database schema.
- Added new tables for 'jalan', 'parsil', and 'jalan_rusak' in spbu_db.sql to enhance data structure.
- Changed admin password hash in spbu_db.sql for security improvements.
2026-06-12 11:02:31 +07:00

1101 lines
47 KiB
PHP

<?php
include 'koneksi.php';
$result = mysqli_query($conn, "SELECT * FROM spbu");
$data = [];
while ($row = mysqli_fetch_assoc($result)) {
$data[] = $row;
}
?>
<!DOCTYPE html>
<html>
<head>
<title>WebGIS SPBU - Modern</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Inter', system-ui, sans-serif;
background: #111827;
overflow: hidden;
height: 100vh;
}
#map {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
transition: all 0.3s ease;
}
.sidebar {
position: absolute;
top: 0;
left: 0;
width: 340px;
height: 100%;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(2px);
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.08);
z-index: 2;
transform: translateX(0);
transition: transform 0.3s cubic-bezier(0.2, 0.9, 0.4, 1.1);
display: flex;
flex-direction: column;
overflow: hidden;
border-right: 1px solid rgba(0, 0, 0, 0.05);
}
.sidebar.closed {
transform: translateX(-100%);
}
.toggle-sidebar-btn {
position: absolute;
top: 18px;
left: 18px;
z-index: 3;
background: white;
border: none;
border-radius: 40px;
width: 44px;
height: 44px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
cursor: pointer;
font-size: 1.4rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
color: #1f2937;
}
body.sidebar-open .toggle-sidebar-btn {
display: none;
}
.leaflet-control-zoom {
transition: margin-left 0.3s ease;
margin-left: 60px;
}
body.sidebar-open .leaflet-control-zoom {
margin-left: 340px;
}
@media (max-width: 640px) {
body.sidebar-open .leaflet-control-zoom {
margin-left: 280px;
}
.sidebar {
width: 280px;
}
}
.sidebar-header {
padding: 20px 20px 12px 20px;
border-bottom: 1px solid #eef2f6;
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header h2 {
font-size: 1.35rem;
font-weight: 600;
background: linear-gradient(135deg, #1e293b, #2d3a4a);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.close-sidebar {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #64748b;
width: 32px;
height: 32px;
border-radius: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.close-sidebar:hover {
background: #f1f5f9;
color: #0f172a;
}
.search-area {
padding: 16px 20px;
background: #ffffff;
border-bottom: 1px solid #edf2f7;
}
.search-box input {
width: 100%;
padding: 10px 16px;
border: 1px solid #e2e8f0;
border-radius: 44px;
font-size: 0.9rem;
outline: none;
transition: 0.2s;
}
.search-box input:focus {
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
.filter-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.filter-chip {
background: #f1f5f9;
border: none;
padding: 6px 14px;
border-radius: 40px;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: 0.2s;
color: #334155;
}
.filter-chip.active {
background: #6366f1;
color: white;
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
}
.result-card {
background: #ffffff;
border-radius: 16px;
padding: 12px 14px;
margin-bottom: 10px;
cursor: pointer;
transition: 0.2s;
border: 1px solid #ecf3fa;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
.result-card:hover {
background: #fafcff;
transform: translateX(4px);
border-color: #cbdff2;
}
.result-title {
font-weight: 600;
color: #0f172a;
display: flex;
align-items: center;
gap: 6px;
}
.result-sub {
font-size: 0.7rem;
color: #5b6e8c;
margin-top: 4px;
}
.floating-toolbar {
position: absolute;
bottom: 24px;
right: 24px;
z-index: 3;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(12px);
border-radius: 56px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
padding: 8px 16px;
display: flex;
gap: 12px;
border: 1px solid rgba(255, 255, 255, 0.5);
}
.mode-icon {
background: #f1f5f9;
border: none;
padding: 8px 18px;
border-radius: 40px;
font-weight: 600;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: 0.2s;
color: #1e293b;
}
.mode-icon.active {
background: #6366f1;
color: white;
}
.mode-icon.edit-mode {
background: #1e293b;
color: white;
}
.mode-icon.edit-mode.active {
background: #10b981;
}
.draw-actions {
display: flex;
gap: 12px;
transition: all 0.3s ease;
}
body.view-mode .draw-actions {
display: none;
}
.custom-draw-toolbar {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: #1e293b;
border-radius: 60px;
padding: 8px 20px;
display: flex;
gap: 16px;
z-index: 10;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
.custom-draw-toolbar button {
background: #334155;
border: none;
color: white;
padding: 8px 20px;
border-radius: 40px;
cursor: pointer;
}
.custom-draw-toolbar button:hover {
background: #6366f1;
}
.edit-popup {
min-width: 220px;
}
.edit-popup input,
.edit-popup select,
.edit-popup textarea {
width: 100%;
padding: 8px 12px;
margin: 8px 0;
border: 1px solid #cbd5e1;
border-radius: 30px;
font-size: 0.85rem;
}
.edit-popup button {
padding: 6px 14px;
border-radius: 30px;
border: none;
margin-right: 6px;
cursor: pointer;
font-weight: 500;
}
.btn-save {
background: #10b981;
color: white;
}
.btn-cancel {
background: #94a3b8;
color: white;
}
.btn-edit {
background: #f59e0b;
color: white;
}
.btn-delete {
background: #ef4444;
color: white;
}
.toast {
position: fixed;
bottom: 20px;
left: 20px;
background: #1e293b;
color: white;
padding: 8px 18px;
border-radius: 40px;
z-index: 1000;
opacity: 0;
transition: 0.2s;
pointer-events: none;
}
.toast.show {
opacity: 1;
}
.drawing-cursor {
cursor: crosshair !important;
}
body.view-mode #map {
cursor: grab;
}
body.view-mode #map:active {
cursor: grabbing;
}
.leaflet-marker-dragging {
cursor: grabbing !important;
}
.popup-img {
max-width: 200px;
max-height: 150px;
border-radius: 12px;
margin-top: 6px;
border: 1px solid #ddd;
}
</style>
</head>
<body>
<button class="toggle-sidebar-btn" id="openSidebarBtn">☰</button>
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<h2>🗺️ GIS Dashboard</h2>
<button class="close-sidebar" id="closeSidebarBtn">✕</button>
</div>
<div class="search-area">
<div class="search-box">
<input type="text" id="searchInput" placeholder="Cari nama, jalan, pemilik, jalan rusak..."
autocomplete="off">
</div>
<div class="filter-buttons">
<button class="filter-chip active" data-filter="all">Semua</button>
<button class="filter-chip" data-filter="marker">📍 SPBU</button>
<button class="filter-chip" data-filter="polyline">📈 Jalan</button>
<button class="filter-chip" data-filter="polygon">🔷 Tanah</button>
<button class="filter-chip" data-filter="road_damage">⚠️ Jalan Rusak</button>
</div>
</div>
<div class="sidebar-content" id="sidebarContent">
<div style="text-align:center; padding:20px;">Memuat data...</div>
</div>
</div>
<div class="floating-toolbar" id="floatingToolbar">
<div class="draw-actions">
<button class="mode-icon" id="btn-marker" data-mode="marker_add">📍 SPBU</button>
<button class="mode-icon" id="btn-polyline" data-mode="polyline">📈 Jalan</button>
<button class="mode-icon" id="btn-polygon" data-mode="polygon">🔷 Tanah</button>
<button class="mode-icon" id="btn-road-damage" data-mode="road_damage">⚠️ Jalan Rusak</button>
</div>
<div style="width:1px; background:#ccc; margin:0 4px;"></div>
<button class="mode-icon edit-mode" id="btn-view-mode">👁️ View</button>
<button class="mode-icon edit-mode active" id="btn-edit-mode">✏️ Edit</button>
</div>
<div id="customDrawToolbar" class="custom-draw-toolbar" style="display: none;">
<button id="undoPointBtn">↩️ Undo</button>
<button id="finishDrawBtn">✅ Selesai</button>
<button id="cancelDrawBtn">✖️ Batal</button>
</div>
<div id="toast" class="toast"></div>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://unpkg.com/leaflet-geometryutil"></script>
<script>
// ================= INIT MAP =================
const map = L.map('map').setView([-0.02, 109.34], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
// ================= GLOBAL STATE =================
let globalEditMode = true;
let activeMode = null;
let customDrawingActive = false;
let customDrawingType = null;
let customPoints = [];
let customTempLayer = null;
let customPreviewLine = null;
let markers = {};
let polylines = {};
let polygons = {};
let roadDamageMarkers = {};
let dataSpbu = <?php echo json_encode($data); ?>;
let jalanData = [];
let parsilData = [];
let roadDamageData = [];
let dataLoaded = { jalan: false, parsil: false, roadDamage: false };
const greenIcon = L.icon({ iconUrl: 'https://maps.google.com/mapfiles/ms/icons/green-dot.png', iconSize: [32, 32] });
const yellowIcon = L.icon({ iconUrl: 'https://maps.google.com/mapfiles/ms/icons/yellow-dot.png', iconSize: [32, 32] });
const blueIcon = L.icon({ iconUrl: 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png', iconSize: [32, 32] });
const damageIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
function postForm(url, data) {
const params = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => params.append(key, value));
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: params.toString()
}).then(res => res.text()).then(res => res.trim());
}
function uploadForm(url, formData) {
return fetch(url, {
method: 'POST',
body: formData
}).then(res => res.text()).then(res => res.trim());
}
function toast(msg) {
const t = document.getElementById('toast');
t.innerText = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2000);
}
// ================= SIDEBAR TOGGLE =================
const sidebar = document.getElementById('sidebar');
const openBtn = document.getElementById('openSidebarBtn');
const closeBtn = document.getElementById('closeSidebarBtn');
function updateSidebarState() {
const isOpen = !sidebar.classList.contains('closed');
if (isOpen) {
document.body.classList.add('sidebar-open');
openBtn.style.display = 'none';
} else {
document.body.classList.remove('sidebar-open');
openBtn.style.display = 'flex';
}
setTimeout(() => map.invalidateSize(), 250);
}
openBtn.addEventListener('click', () => { sidebar.classList.remove('closed'); updateSidebarState(); });
closeBtn.addEventListener('click', () => { sidebar.classList.add('closed'); updateSidebarState(); });
updateSidebarState();
// ================= EDIT / VIEW MODE =================
function setGlobalMode(modeType) {
globalEditMode = (modeType === 'edit');
if (globalEditMode) {
document.body.classList.remove('view-mode');
document.getElementById('btn-edit-mode').classList.add('active');
document.getElementById('btn-view-mode').classList.remove('active');
for (let id in markers) if (markers[id].dragging) markers[id].dragging.enable();
for (let id in roadDamageMarkers) if (roadDamageMarkers[id].dragging) roadDamageMarkers[id].dragging.enable();
toast("Mode Edit aktif");
} else {
document.body.classList.add('view-mode');
document.getElementById('btn-view-mode').classList.add('active');
document.getElementById('btn-edit-mode').classList.remove('active');
if (customDrawingActive) cancelCustomDrawing();
if (activeMode === 'marker_add' || activeMode === 'road_damage') setActiveMode(null);
for (let id in markers) if (markers[id].dragging) markers[id].dragging.disable();
for (let id in roadDamageMarkers) if (roadDamageMarkers[id].dragging) roadDamageMarkers[id].dragging.disable();
toast("Mode View aktif");
}
refreshAllPopups();
filterSidebar();
}
document.getElementById('btn-edit-mode').addEventListener('click', () => setGlobalMode('edit'));
document.getElementById('btn-view-mode').addEventListener('click', () => setGlobalMode('view'));
function setActiveMode(mode) {
if (activeMode === mode && mode !== null) { mode = null; }
activeMode = mode;
document.querySelectorAll('.mode-icon[data-mode]').forEach(btn => btn.classList.remove('active'));
if (activeMode === 'marker_add') {
document.getElementById('btn-marker').classList.add('active');
map.getContainer().style.cursor = 'crosshair';
if (customDrawingActive) cancelCustomDrawing();
} else if (activeMode === 'polyline') {
document.getElementById('btn-polyline').classList.add('active');
startCustomDrawing('polyline');
} else if (activeMode === 'polygon') {
document.getElementById('btn-polygon').classList.add('active');
startCustomDrawing('polygon');
} else if (activeMode === 'road_damage') {
document.getElementById('btn-road-damage').classList.add('active');
map.getContainer().style.cursor = 'crosshair';
if (customDrawingActive) cancelCustomDrawing();
} else {
map.getContainer().style.cursor = '';
if (customDrawingActive) cancelCustomDrawing();
}
}
document.getElementById('btn-marker').addEventListener('click', () => {
if (!globalEditMode) return;
if (activeMode === 'marker_add') { setActiveMode(null); } else { setActiveMode('marker_add'); }
});
document.getElementById('btn-polyline').addEventListener('click', () => {
if (!globalEditMode) return;
if (customDrawingActive && customDrawingType === 'polyline') { cancelCustomDrawing(); setActiveMode(null); } else { setActiveMode('polyline'); }
});
document.getElementById('btn-polygon').addEventListener('click', () => {
if (!globalEditMode) return;
if (customDrawingActive && customDrawingType === 'polygon') { cancelCustomDrawing(); setActiveMode(null); } else { setActiveMode('polygon'); }
});
document.getElementById('btn-road-damage').addEventListener('click', () => {
if (!globalEditMode) return;
if (activeMode === 'road_damage') { setActiveMode(null); } else { setActiveMode('road_damage'); }
});
// ================= CUSTOM DRAWING =================
function startCustomDrawing(type) {
if (!globalEditMode) return;
if (customDrawingActive) cancelCustomDrawing();
customDrawingActive = true;
customDrawingType = type;
customPoints = [];
map.dragging.disable();
map.getContainer().classList.add('drawing-cursor');
document.getElementById('customDrawToolbar').style.display = 'flex';
toast(`Mode ${type === 'polyline' ? 'Jalan' : 'Tanah'} aktif. Klik pada peta untuk titik.`);
map.on('mousemove', onMouseMovePreview);
}
function onMouseMovePreview(e) {
if (!customDrawingActive || customPoints.length === 0) return;
if (customPreviewLine) map.removeLayer(customPreviewLine);
const last = customPoints[customPoints.length - 1];
customPreviewLine = L.polyline([last, e.latlng], { color: '#6366f1', weight: 3, dashArray: '5,10', opacity: 0.7 });
customPreviewLine.addTo(map);
}
function addCustomPoint(latlng) {
if (!customDrawingActive) return;
customPoints.push(latlng);
updateCustomLayer();
if (customPreviewLine) { map.removeLayer(customPreviewLine); customPreviewLine = null; }
}
function updateCustomLayer() {
if (customTempLayer) map.removeLayer(customTempLayer);
if (customPoints.length < 2) return;
if (customDrawingType === 'polyline') {
customTempLayer = L.polyline(customPoints, { color: '#6366f1', weight: 5 });
} else {
if (customPoints.length >= 3)
customTempLayer = L.polygon(customPoints, { color: '#6366f1', weight: 3, fillOpacity: 0.2 });
else
customTempLayer = L.polyline(customPoints, { color: '#6366f1', weight: 4, dashArray: '5,10' });
}
customTempLayer.addTo(map);
}
function undoLastPoint() {
if (!customDrawingActive || customPoints.length === 0) return;
customPoints.pop();
updateCustomLayer();
toast(`Titik dihapus, tersisa ${customPoints.length}`);
}
function finishCustomDrawing() {
if (!customDrawingActive) return;
const min = (customDrawingType === 'polyline') ? 2 : 3;
if (customPoints.length < min) { toast(`Minimal ${min} titik`); return; }
const panjang = hitungPanjang(customPoints);
const luas = (customDrawingType === 'polygon') ? hitungLuas([customPoints]) : null;
const center = customPoints[Math.floor(customPoints.length / 2)];
let formHtml = '';
if (customDrawingType === 'polyline') {
formHtml = `<div class="edit-popup">
<b>📏 Panjang: ${panjang.toFixed(2)} m</b>
<input id="nama_jalan" placeholder="Nama Jalan" autocomplete="off">
<select id="status_jalan">
<option value="" disabled selected>-- Pilih Status Jalan --</option>
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten">Kabupaten</option>
<option value="Kota">Kota</option>
</select>
<button class="btn-save" onclick="simpanJalanCustom()">Simpan</button>
<button class="btn-cancel" onclick="batalCustomDrawing()">Batal</button>
</div>`;
} else {
formHtml = `<div class="edit-popup">
<b>📐 Luas: ${luas.toFixed(2)} m²</b>
<input id="pemilik" placeholder="Nama Pemilik" autocomplete="off">
<select id="status_tanah">
<option value="" disabled selected>-- Pilih Status Tanah --</option>
<option value="SHM">SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
<button class="btn-save" onclick="simpanParsilCustom()">Simpan</button>
<button class="btn-cancel" onclick="batalCustomDrawing()">Batal</button>
</div>`;
}
if (customTempLayer) customTempLayer.bindPopup(formHtml).openPopup();
else {
let tmp = L.marker(center).addTo(map);
tmp.bindPopup(formHtml).openPopup();
window.tempPopupMarker = tmp;
}
}
window.simpanJalanCustom = function () {
const namaInput = document.getElementById('nama_jalan');
const statusSelect = document.getElementById('status_jalan');
if (!namaInput || !statusSelect) { toast("Error: Form tidak ditemukan"); return; }
const nama = namaInput.value.trim();
const status = statusSelect.value;
if (nama === "") { alert("❌ Nama jalan harus diisi!"); return; }
if (status === "") { alert("❌ Silakan pilih status jalan!"); return; }
const geo = JSON.stringify(customPoints.map(p => [p.lat, p.lng]));
const panjang = hitungPanjang(customPoints);
postForm('insert_jalan.php', { nama, status, panjang, geojson: geo })
.then(res => { if (res === "success") { toast("Jalan tersimpan"); location.reload(); } else { alert("Gagal menyimpan: " + res); } })
.catch(err => alert("Error: " + err));
};
window.simpanParsilCustom = function () {
const pemilikInput = document.getElementById('pemilik');
const statusSelect = document.getElementById('status_tanah');
if (!pemilikInput || !statusSelect) { toast("Error: Form tidak ditemukan"); return; }
const pemilik = pemilikInput.value.trim();
const status = statusSelect.value;
if (pemilik === "") { alert("❌ Nama pemilik harus diisi!"); return; }
if (status === "") { alert("❌ Silakan pilih status tanah!"); return; }
const geo = JSON.stringify([customPoints.map(p => [p.lat, p.lng])]);
const luas = hitungLuas([customPoints]);
postForm('insert_parsil.php', { pemilik, status, luas, geojson: geo })
.then(res => { if (res === "success") { toast("Persil tersimpan"); location.reload(); } else { alert("Gagal menyimpan: " + res); } })
.catch(err => alert("Error: " + err));
};
function cancelCustomDrawing() {
if (customTempLayer) map.removeLayer(customTempLayer);
if (customPreviewLine) map.removeLayer(customPreviewLine);
if (window.tempPopupMarker) map.removeLayer(window.tempPopupMarker);
map.off('mousemove', onMouseMovePreview);
map.dragging.enable();
map.getContainer().classList.remove('drawing-cursor');
customDrawingActive = false;
customPoints = [];
document.getElementById('customDrawToolbar').style.display = 'none';
if (activeMode === 'polyline' || activeMode === 'polygon') { setActiveMode(null); }
toast("Drawing dibatalkan");
}
window.batalCustomDrawing = cancelCustomDrawing;
function hitungPanjang(latlngs) {
let total = 0;
for (let i = 0; i < latlngs.length - 1; i++) total += map.distance(latlngs[i], latlngs[i + 1]);
return total;
}
function hitungLuas(latlngs) {
return L.GeometryUtil.geodesicArea(latlngs[0]);
}
// ================= LOAD DATA =================
function loadJalan() {
fetch('get_jalan.php').then(res => res.json()).then(data => {
jalanData = data;
data.forEach(j => {
let coords = JSON.parse(j.geojson);
let colorMap = { "Nasional": "#ef4444", "Provinsi": "#f97316", "Kabupaten": "#3b82f6", "Kota": "#10b981" };
let color = colorMap[j.status] || "#6b7280";
let polyline = L.polyline(coords, { color: color, weight: 4, opacity: 0.8 }).addTo(map);
polyline.on('click', () => polyline.bindPopup(getPopupJalan(j)).openPopup());
polylines[j.id] = polyline;
j.layer = polyline;
});
dataLoaded.jalan = true;
if (dataLoaded.jalan && dataLoaded.parsil && dataLoaded.roadDamage) filterSidebar();
}).catch(err => console.error(err));
}
function loadParsil() {
fetch('get_parsil.php').then(res => res.json()).then(data => {
parsilData = data;
data.forEach(p => {
let coords = JSON.parse(p.geojson);
let colorMap = { SHM: "#10b981", HGB: "#3b82f6", HGU: "#f59e0b", HP: "#8b5cf6" };
let polygon = L.polygon(coords, { color: colorMap[p.status], weight: 2, fillOpacity: 0.3 }).addTo(map);
polygon.on('click', () => polygon.bindPopup(getPopupParsil(p)).openPopup());
polygons[p.id] = polygon;
p.layer = polygon;
});
dataLoaded.parsil = true;
if (dataLoaded.jalan && dataLoaded.parsil && dataLoaded.roadDamage) filterSidebar();
}).catch(err => console.error(err));
}
function loadRoadDamage() {
fetch('get_jalan_rusak.php').then(res => res.json()).then(data => {
roadDamageData = data;
data.forEach(rd => {
let m = L.marker([parseFloat(rd.latitude), parseFloat(rd.longitude)], { icon: damageIcon, draggable: globalEditMode }).addTo(map);
m.bindPopup(getPopupRoadDamage(rd));
m.on('dragend', function () {
if (!globalEditMode) return;
let pos = m.getLatLng();
postForm('update_jalan_rusak.php', { id: rd.id, lat: pos.lat, lng: pos.lng, deskripsi: rd.deskripsi, foto_lama: rd.foto })
.then(res => { if (res !== "success") alert("Gagal update posisi: " + res); else toast("Posisi jalan rusak diperbarui"); })
.catch(err => alert("Error: " + err));
rd.latitude = pos.lat;
rd.longitude = pos.lng;
});
roadDamageMarkers[rd.id] = m;
rd.marker = m;
});
dataLoaded.roadDamage = true;
if (dataLoaded.jalan && dataLoaded.parsil && dataLoaded.roadDamage) filterSidebar();
}).catch(err => console.error(err));
}
// Load marker SPBU
dataSpbu.forEach(d => {
let icon = d.status === '24jam' ? greenIcon : yellowIcon;
let m = L.marker([d.latitude, d.longitude], { icon: icon, draggable: globalEditMode }).addTo(map);
markers[d.id] = m;
m.bindPopup(getPopupMarker(d));
m.on('dragstart', () => { map.getContainer().style.cursor = 'grabbing'; });
m.on('dragend', () => {
map.getContainer().style.cursor = '';
if (!globalEditMode) return;
let pos = m.getLatLng();
postForm('update.php', { id: d.id, lat: pos.lat, lng: pos.lng })
.then(res => { if (res !== "OK") alert("Gagal memperbarui posisi SPBU: " + res); })
.catch(err => alert("Error: " + err));
d.latitude = pos.lat; d.longitude = pos.lng;
toast("Posisi SPBU diperbarui");
});
});
loadJalan();
loadParsil();
loadRoadDamage();
// ================= POPUP GENERATOR =================
function getPopupMarker(d) {
if (globalEditMode) {
return `<div class="edit-popup">
<b>📍 ${d.nama}</b><br>${d.no_spbu}<br>${d.status === '24jam' ? '24 Jam' : 'Terbatas'}<br>
<button class="btn-edit" onclick="editMarker(${d.id})">Edit</button>
<button class="btn-delete" onclick="deleteMarker(${d.id})">Hapus</button>
</div>`;
} else {
return `<div><b>${d.nama}</b><br>${d.no_spbu}<br>${d.status === '24jam' ? '24 Jam' : 'Terbatas'}</div>`;
}
}
function getPopupJalan(j) {
if (globalEditMode) {
return `<div class="edit-popup">
<b>🛣️ ${j.nama_jalan}</b><br>Status: ${j.status}<br>Panjang: ${j.panjang} m<br>
<button class="btn-edit" onclick="editJalan(${j.id})">Edit</button>
<button class="btn-delete" onclick="deleteJalan(${j.id})">Hapus</button>
</div>`;
} else {
return `<div><b>🛣️ ${j.nama_jalan}</b><br>${j.status}</div>`;
}
}
function getPopupParsil(p) {
if (globalEditMode) {
return `<div class="edit-popup">
<b>🏞️ ${p.pemilik}</b><br>Status: ${p.status}<br>Luas: ${p.luas} m²<br>
<button class="btn-edit" onclick="editParsil(${p.id})">Edit</button>
<button class="btn-delete" onclick="deleteParsil(${p.id})">Hapus</button>
</div>`;
} else {
return `<div><b>🏞️ ${p.pemilik}</b><br>${p.status}</div>`;
}
}
function getPopupRoadDamage(rd) {
const fotoUrl = 'uploads/' + rd.foto;
if (globalEditMode) {
return `<div class="edit-popup">
<b>⚠️ Laporan Jalan Rusak</b><br>
<img src="${fotoUrl}" class="popup-img" onerror="this.src='https://via.placeholder.com/150?text=No+Image'"><br>
<textarea id="deskripsi_edit" rows="2" placeholder="Deskripsi">${rd.deskripsi}</textarea>
<input type="file" id="foto_edit" accept="image/*"><br>
<button class="btn-save" onclick="saveEditRoadDamage(${rd.id})">Simpan</button>
<button class="btn-delete" onclick="deleteRoadDamage(${rd.id})">Hapus</button>
<button class="btn-cancel" onclick="refreshAllPopups()">Batal</button>
</div>`;
} else {
return `<div><b>⚠️ Jalan Rusak</b><br>${rd.deskripsi}<br><img src="${fotoUrl}" class="popup-img" onerror="this.src='https://via.placeholder.com/150?text=No+Image'"></div>`;
}
}
function refreshAllPopups() {
dataSpbu.forEach(d => { if (markers[d.id]) markers[d.id].setPopupContent(getPopupMarker(d)); });
jalanData.forEach(j => { if (polylines[j.id]) polylines[j.id].setPopupContent(getPopupJalan(j)); });
parsilData.forEach(p => { if (polygons[p.id]) polygons[p.id].setPopupContent(getPopupParsil(p)); });
roadDamageData.forEach(rd => { if (roadDamageMarkers[rd.id]) roadDamageMarkers[rd.id].setPopupContent(getPopupRoadDamage(rd)); });
}
// ================= SIDEBAR SEARCH & FILTER =================
function filterSidebar() {
let searchText = document.getElementById('searchInput').value.toLowerCase();
let activeFilter = document.querySelector('.filter-chip.active')?.dataset.filter || 'all';
let results = [];
if (activeFilter === 'all' || activeFilter === 'marker') {
dataSpbu.forEach(d => {
if (d.nama.toLowerCase().includes(searchText))
results.push({ type: 'marker', id: d.id, name: d.nama, desc: d.status === '24jam' ? '24 Jam' : 'Terbatas', latlng: [d.latitude, d.longitude] });
});
}
if (activeFilter === 'all' || activeFilter === 'polyline') {
jalanData.forEach(j => {
if (j.nama_jalan.toLowerCase().includes(searchText))
results.push({ type: 'polyline', id: j.id, name: j.nama_jalan, desc: `Jalan ${j.status}`, bounds: j.layer.getBounds() });
});
}
if (activeFilter === 'all' || activeFilter === 'polygon') {
parsilData.forEach(p => {
if (p.pemilik.toLowerCase().includes(searchText))
results.push({ type: 'polygon', id: p.id, name: p.pemilik, desc: `Tanah ${p.status}`, bounds: p.layer.getBounds() });
});
}
if (activeFilter === 'all' || activeFilter === 'road_damage') {
roadDamageData.forEach(rd => {
if (rd.deskripsi.toLowerCase().includes(searchText))
results.push({ type: 'road_damage', id: rd.id, name: 'Jalan Rusak', desc: rd.deskripsi.substring(0, 50), latlng: [rd.latitude, rd.longitude] });
});
}
let html = `<div style="margin-bottom:10px; font-size:0.75rem;">${results.length} ditemukan</div>`;
results.forEach(r => {
let icon = r.type === 'marker' ? '📍' : (r.type === 'polyline' ? '📈' : (r.type === 'polygon' ? '🔷' : '⚠️'));
html += `<div class="result-card" onclick="focusFeature('${r.type}', ${r.id})">
<div class="result-title">${icon} ${r.name}</div>
<div class="result-sub">${r.desc}</div>
</div>`;
});
if (results.length === 0) html += '<div style="text-align:center; padding:20px;">❌ Tidak ada data</div>';
document.getElementById('sidebarContent').innerHTML = html;
}
window.focusFeature = function (type, id) {
if (type === 'marker' && markers[id]) { map.setView(markers[id].getLatLng(), 17); markers[id].openPopup(); }
else if (type === 'polyline') { let j = jalanData.find(j => j.id == id); if (j && j.layer) { map.fitBounds(j.layer.getBounds()); j.layer.openPopup(); } }
else if (type === 'polygon') { let p = parsilData.find(p => p.id == id); if (p && p.layer) { map.fitBounds(p.layer.getBounds()); p.layer.openPopup(); } }
else if (type === 'road_damage' && roadDamageMarkers[id]) { map.setView(roadDamageMarkers[id].getLatLng(), 17); roadDamageMarkers[id].openPopup(); }
};
document.querySelectorAll('.filter-chip').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
filterSidebar();
});
});
document.getElementById('searchInput').addEventListener('input', filterSidebar);
// ================= CRUD FUNCTIONS =================
window.editMarker = function (id) {
let d = dataSpbu.find(x => x.id == id);
let form = `<div class="edit-popup">
<input id="nama" value="${d.nama}"><input id="no" value="${d.no_spbu}">
<select id="status"><option ${d.status == "24jam" ? "selected" : ""} value="24jam">24 Jam</option><option ${d.status == "terbatas" ? "selected" : ""} value="terbatas">Terbatas</option></select>
<button class="btn-save" onclick="saveEditMarker(${id})">Simpan</button>
<button class="btn-cancel" onclick="refreshAllPopups()">Batal</button>
</div>`;
markers[id].setPopupContent(form).openPopup();
};
window.saveEditMarker = function (id) {
let nama = document.getElementById('nama').value;
let no = document.getElementById('no').value;
let status = document.getElementById('status').value;
postForm('update_data.php', { id, nama, no, status })
.then(res => { if (res === "success") location.reload(); else alert("Error:" + res); })
.catch(err => alert("Error: " + err));
};
window.deleteMarker = function (id) {
if (!confirm("Hapus SPBU?")) return;
postForm('delete.php', { id })
.then(res => { if (res === "success") location.reload(); else alert("Error:" + res); })
.catch(err => alert("Error: " + err));
};
window.editJalan = function (id) {
let j = jalanData.find(j => j.id == id);
let form = `<div class="edit-popup"><input id="nama_jalan" value="${j.nama_jalan}"><select id="status_jalan"><option ${j.status == "Nasional" ? "selected" : ""}>Nasional</option><option ${j.status == "Provinsi" ? "selected" : ""}>Provinsi</option><option ${j.status == "Kabupaten" ? "selected" : ""}>Kabupaten</option></select><button class="btn-save" onclick="saveEditJalan(${id})">Simpan</button><button class="btn-cancel" onclick="refreshAllPopups()">Batal</button></div>`;
polylines[id].setPopupContent(form).openPopup();
};
window.saveEditJalan = function (id) {
let nama = document.getElementById('nama_jalan').value;
let status = document.getElementById('status_jalan').value;
postForm('update_jalan.php', { id, nama, status })
.then(res => { if (res === "success") location.reload(); else alert("Error:" + res); })
.catch(err => alert("Error: " + err));
};
window.deleteJalan = function (id) {
if (!confirm("Hapus jalan?")) return;
postForm('delete_jalan.php', { id })
.then(res => { if (res === "success") location.reload(); else alert("Error:" + res); })
.catch(err => alert("Error: " + err));
};
window.editParsil = function (id) {
let p = parsilData.find(p => p.id == id);
let form = `<div class="edit-popup"><input id="pemilik" value="${p.pemilik}"><select id="status_tanah"><option ${p.status == "SHM" ? "selected" : ""}>SHM</option><option ${p.status == "HGB" ? "selected" : ""}>HGB</option><option ${p.status == "HGU" ? "selected" : ""}>HGU</option><option ${p.status == "HP" ? "selected" : ""}>HP</option></select><button class="btn-save" onclick="saveEditParcil(${id})">Simpan</button><button class="btn-cancel" onclick="refreshAllPopups()">Batal</button></div>`;
polygons[id].setPopupContent(form).openPopup();
};
window.saveEditParcil = function (id) {
let pemilik = document.getElementById('pemilik').value;
let status = document.getElementById('status_tanah').value;
postForm('update_parsil.php', { id, pemilik, status })
.then(res => { if (res === "success") location.reload(); else alert("Error:" + res); })
.catch(err => alert("Error: " + err));
};
window.deleteParsil = function (id) {
if (!confirm("Hapus persil?")) return;
postForm('delete_parsil.php', { id })
.then(res => { if (res === "success") location.reload(); else alert("Error:" + res); })
.catch(err => alert("Error: " + err));
};
// ================= JALAN RUSAK =================
let tempRoadMarker = null;
map.off('click');
let tempMarker = null;
map.on('click', function (e) {
if (!globalEditMode) return;
if (customDrawingActive) { addCustomPoint(e.latlng); return; }
if (activeMode === 'marker_add') {
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker(e.latlng, { icon: blueIcon }).addTo(map);
let form = `<div class="edit-popup">
<input id="nama_baru" placeholder="Nama SPBU" autocomplete="off"><input id="no_baru" placeholder="No SPBU" autocomplete="off">
<select id="status_baru"><option value="24jam">24 Jam</option><option value="terbatas">Terbatas</option></select>
<button class="btn-save" onclick="simpanMarkerBaru(${e.latlng.lat}, ${e.latlng.lng})">Simpan</button>
<button class="btn-cancel" onclick="batalMarkerBaru()">Batal</button>
</div>`;
tempMarker.bindPopup(form).openPopup();
} else if (activeMode === 'road_damage') {
if (tempRoadMarker) map.removeLayer(tempRoadMarker);
tempRoadMarker = L.marker(e.latlng, { icon: damageIcon }).addTo(map);
let form = `<div class="edit-popup">
<textarea id="deskripsi_lapor" rows="3" placeholder="Deskripsi kerusakan jalan..." autocomplete="off"></textarea>
<input type="file" id="foto_lapor" accept="image/*" required>
<button class="btn-save" onclick="simpanRoadDamageBaru(${e.latlng.lat}, ${e.latlng.lng})">Laporkan</button>
<button class="btn-cancel" onclick="batalRoadDamageBaru()">Batal</button>
</div>`;
tempRoadMarker.bindPopup(form).openPopup();
}
});
window.batalMarkerBaru = function () { if (tempMarker) map.removeLayer(tempMarker); tempMarker = null; };
window.simpanMarkerBaru = function (lat, lng) {
let nama = document.getElementById('nama_baru').value.trim();
let no = document.getElementById('no_baru').value.trim();
let status = document.getElementById('status_baru').value;
if (!nama) { alert("❌ Nama SPBU harus diisi!"); return; }
if (!no) { alert("❌ Nomor SPBU harus diisi!"); return; }
if (!status) { alert("❌ Status harus dipilih!"); return; }
postForm('insert_data.php', { nama, no, status, lat, lng })
.then(res => { if (res === "success") location.reload(); else alert("Error:" + res); })
.catch(err => alert("Error: " + err));
};
window.batalRoadDamageBaru = function () { if (tempRoadMarker) map.removeLayer(tempRoadMarker); tempRoadMarker = null; };
window.simpanRoadDamageBaru = function (lat, lng) {
let deskripsi = document.getElementById('deskripsi_lapor').value.trim();
let fileInput = document.getElementById('foto_lapor');
if (!deskripsi) { alert("❌ Deskripsi harus diisi!"); return; }
if (!fileInput.files.length) { alert("❌ Foto harus diupload!"); return; }
let formData = new FormData();
formData.append('deskripsi', deskripsi);
formData.append('foto', fileInput.files[0]);
formData.append('lat', lat);
formData.append('lng', lng);
uploadForm('insert_jalan_rusak.php', formData)
.then(res => { if (res === "success") location.reload(); else alert("Gagal simpan: " + res); })
.catch(err => alert("Error: " + err));
};
window.saveEditRoadDamage = function (id) {
let rd = roadDamageData.find(r => r.id == id);
let deskripsiBaru = document.getElementById('deskripsi_edit').value.trim();
let fotoFile = document.getElementById('foto_edit').files[0];
if (!deskripsiBaru) { alert("Deskripsi tidak boleh kosong"); return; }
let formData = new FormData();
formData.append('id', id);
formData.append('deskripsi', deskripsiBaru);
if (fotoFile) formData.append('foto', fotoFile);
formData.append('foto_lama', rd.foto);
uploadForm('update_jalan_rusak.php', formData)
.then(res => { if (res === "success") location.reload(); else alert("Gagal update: " + res); })
.catch(err => alert("Error: " + err));
};
window.deleteRoadDamage = function (id) {
if (!confirm("Hapus laporan jalan rusak?")) return;
let rd = roadDamageData.find(r => r.id == id);
postForm('delete_jalan_rusak.php', { id, foto: rd.foto })
.then(res => { if (res === "success") location.reload(); else alert("Gagal hapus: " + res); })
.catch(err => alert("Error: " + err));
};
// ================= INITIAL =================
document.getElementById('undoPointBtn').addEventListener('click', undoLastPoint);
document.getElementById('finishDrawBtn').addEventListener('click', finishCustomDrawing);
document.getElementById('cancelDrawBtn').addEventListener('click', cancelCustomDrawing);
setActiveMode(null);
setGlobalMode('edit');
</script>
</body>
</html>