Files
UAS_SIG/jalan-parsil.html
T
2026-06-11 00:38:46 +07:00

590 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Peta Jalan & Parsil - WebGIS</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--clr-bg: #0f172a; --clr-surface: #1e293b; --clr-surface-light: #334155;
--clr-border: #334155; --clr-text: #f1f5f9; --clr-muted: #94a3b8;
--clr-primary: #14b8a6; --clr-primary-dark: #0f766e;
--clr-warning: #f59e0b; --clr-danger: #ef4444;
--radius: 10px; --shadow: 0 4px 24px rgba(0,0,0,.45);
--transition: .2s ease; --sidebar-width: 260px; --sidebar-collapsed: 70px;
}
html, body { height: 100%; font-family: 'Inter', sans-serif; background: var(--clr-bg); color: var(--clr-text); overflow: hidden; }
button, input, select { font: inherit; }
.app-wrapper { display: flex; height: 100vh; }
.sidebar {
width: var(--sidebar-width); background: linear-gradient(135deg, #123838 0%, #0f172a 100%);
border-right: 1px solid var(--clr-border); display: flex; flex-direction: column;
transition: width var(--transition); z-index: 100; box-shadow: 2px 0 8px rgba(0,0,0,.3); overflow: hidden;
}
.sidebar.collapsed { width: var(--sidebar-collapsed); }
.sidebar-header { padding: 20px; border-bottom: 1px solid var(--clr-border); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
.sidebar-logo { display: flex; align-items: center; gap: 10px; flex: 1; overflow: hidden; }
.sidebar-logo-icon {
width: 40px; height: 40px; flex-shrink: 0; background: linear-gradient(135deg, #14b8a6, #f59e0b);
border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 700;
box-shadow: 0 0 12px rgba(20,184,166,.4);
}
.sidebar-logo-text { overflow: hidden; }
.sidebar-logo-text h2 { font-size: 14px; font-weight: 700; white-space: nowrap; }
.sidebar-logo-text p { font-size: 11px; color: var(--clr-muted); white-space: nowrap; }
.sidebar.collapsed .sidebar-logo-text { display: none; }
.sidebar-toggle { background: transparent; border: none; color: var(--clr-muted); cursor: pointer; font-size: 20px; padding: 4px; flex-shrink: 0; }
.sidebar-toggle:hover { color: var(--clr-text); }
.sidebar-nav { flex: 1; overflow-y: auto; padding: 16px 0; }
.sidebar-section { padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,.05); }
.sidebar-section-title { padding: 10px 16px; font-size: 10px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: .5px; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-section-title { opacity: 0; }
.sidebar-menu { display: flex; flex-direction: column; }
.sidebar-item {
display: flex; align-items: center; gap: 12px; padding: 13px 16px; color: var(--clr-muted);
cursor: pointer; text-decoration: none; transition: all var(--transition); border-left: 3px solid transparent;
}
.sidebar-item:hover { color: var(--clr-text); background: rgba(255,255,255,.05); border-left-color: var(--clr-primary); }
.sidebar-item.active { color: #5eead4; background: rgba(20,184,166,.1); border-left-color: var(--clr-primary); }
.sidebar-item-icon { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.sidebar-item-text { flex: 1; font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-item-text { display: none; }
.main-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
header {
flex-shrink: 0; min-height: 60px; background: linear-gradient(135deg, #123838 0%, #0f172a 100%);
border-bottom: 1px solid var(--clr-border); display: flex; align-items: center; justify-content: space-between;
padding: 0 20px; gap: 14px; box-shadow: 0 2px 12px rgba(0,0,0,.5);
}
.header-title h1 { font-size: 18px; font-weight: 700; color: #99f6e4; }
.header-breadcrumb { font-size: 13px; color: var(--clr-muted); margin-top: 2px; }
.header-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
.count-pill { font-size: 13px; color: #cbd5e1; background: rgba(255,255,255,.06); border: 1px solid #334155; padding: 5px 12px; border-radius: 7px; }
.refresh-btn { background: rgba(20,184,166,.15); border: 1px solid rgba(20,184,166,.3); color: #5eead4; border-radius: 7px; padding: 6px 12px; cursor: pointer; font-size: 13px; }
.refresh-btn:hover { background: rgba(20,184,166,.25); }
.content-area { flex: 1; position: relative; overflow: hidden; }
#map { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; }
.panel {
position: absolute; background: rgba(30, 41, 59, .95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius); box-shadow: var(--shadow); z-index: 1000;
}
.search-panel { top: 16px; left: 60px; width: 320px; padding: 12px; display: flex; flex-direction: column; }
.search-panel h3, .filter-panel h3, .stats-panel h3, .legend-panel h3 {
font-size: 12px; font-weight: 700; color: var(--clr-text); margin-bottom: 10px; text-transform: uppercase; letter-spacing: .5px;
}
.ctrl-input, .ctrl-select {
width: 100%; padding: 9px 12px; background: var(--clr-bg); border: 1px solid var(--clr-border);
border-radius: 6px; color: var(--clr-text); font-size: 13px; outline: none;
}
.ctrl-input:focus, .ctrl-select:focus { border-color: var(--clr-primary); }
.field-label { display: block; font-size: 11px; font-weight: 700; color: var(--clr-muted); margin: 10px 0 5px; }
.search-results { max-height: 230px; overflow-y: auto; margin-top: 8px; display: none; flex-direction: column; gap: 4px; }
.search-results.active { display: flex; }
.search-result-item { padding: 8px 10px; background: rgba(255,255,255,.05); border-radius: 6px; cursor: pointer; border: 1px solid transparent; }
.search-result-item:hover { background: rgba(20,184,166,.1); border-color: rgba(20,184,166,.35); }
.search-result-name { font-size: 13px; font-weight: 600; color: #f8fafc; display: block; margin-bottom: 2px; }
.search-result-desc { font-size: 11px; color: var(--clr-muted); display: block; }
.filter-panel { top: 16px; right: 16px; width: 285px; padding: 14px; }
.mode-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 10px; }
.mode-btn {
border: 1px solid var(--clr-border); background: rgba(15,23,42,.8); color: var(--clr-muted);
border-radius: 7px; padding: 8px; cursor: pointer; font-size: 12px; font-weight: 700;
}
.mode-btn.active { border-color: rgba(20,184,166,.8); color: #99f6e4; background: rgba(20,184,166,.14); }
.analysis-box { margin-top: 12px; border-top: 1px solid rgba(148,163,184,.18); padding-top: 12px; font-size: 12px; color: var(--clr-muted); line-height: 1.5; }
.analysis-box strong { color: #f8fafc; }
.analysis-empty { color: var(--clr-muted); }
.stats-panel { left: 16px; bottom: 16px; width: 300px; padding: 14px; }
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.stat-card { background: rgba(15,23,42,.72); border: 1px solid rgba(148,163,184,.16); border-radius: 8px; padding: 10px; min-height: 68px; }
.stat-value { font-size: 18px; font-weight: 700; color: #f8fafc; margin-bottom: 3px; }
.stat-label { font-size: 11px; color: var(--clr-muted); line-height: 1.35; }
.legend-panel { right: 16px; bottom: 16px; width: 235px; padding: 14px; }
.legend-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; color: var(--clr-text); font-size: 12px; }
.legend-line { width: 24px; height: 0; border-top: 4px solid; border-radius: 999px; flex-shrink: 0; }
.legend-swatch { width: 14px; height: 14px; border-radius: 4px; flex-shrink: 0; border: 1px solid rgba(255,255,255,.3); }
.popup-title { font-weight: 700; color: #0f172a; margin-bottom: 4px; }
.popup-row { font-size: 12px; line-height: 1.5; }
@media (max-width: 900px) {
.sidebar { width: var(--sidebar-collapsed); }
.sidebar-logo-text, .sidebar-item-text, .sidebar-section-title { display: none; }
header { align-items: flex-start; flex-direction: column; padding: 10px 14px; }
.search-panel, .filter-panel, .stats-panel, .legend-panel {
left: 12px; right: 12px; width: auto;
}
.search-panel { top: 12px; }
.filter-panel { top: 190px; }
.stats-panel { display: none; }
.legend-panel { bottom: 12px; }
}
</style>
</head>
<body>
<div class="app-wrapper">
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">
<div class="sidebar-logo-icon">JP</div>
<div class="sidebar-logo-text">
<h2>WebGIS</h2>
<p>Jalan & Parsil</p>
</div>
</div>
<button class="sidebar-toggle" id="sidebar-toggle">=</button>
</div>
<nav class="sidebar-nav">
<div class="sidebar-section">
<div class="sidebar-section-title">Navigasi</div>
<div class="sidebar-menu">
<div class="sidebar-item active">
<div class="sidebar-item-icon">#</div>
<div class="sidebar-item-text">Jalan & Parsil</div>
</div>
<a href="index.php" class="sidebar-item">
<div class="sidebar-item-icon"></div>
<div class="sidebar-item-text">Beranda</div>
</a>
</div>
</div>
</nav>
</aside>
<main class="main-wrapper">
<header>
<div class="header-title">
<h1>Peta Jalan & Parsil Tanah</h1>
<div class="header-breadcrumb">Visualisasi infrastruktur, kepemilikan lahan, dan akses parsil ke jalan</div>
</div>
<div class="header-actions">
<span id="data-count" class="count-pill">Memuat data...</span>
<button id="btn-refresh" class="refresh-btn">Refresh</button>
</div>
</header>
<section class="content-area">
<div id="map"></div>
<div class="panel search-panel">
<h3>Pencarian</h3>
<input id="search-input" class="ctrl-input" type="text" placeholder="Cari nama jalan atau pemilik parsil..." autocomplete="off">
<div id="search-results" class="search-results"></div>
</div>
<div class="panel filter-panel">
<h3>Filter & Analisis</h3>
<div class="mode-grid">
<button class="mode-btn active" data-mode="all">Semua</button>
<button class="mode-btn" data-mode="jalan">Jalan</button>
<button class="mode-btn" data-mode="parsil">Parsil</button>
<button class="mode-btn" data-mode="analysis">Akses</button>
</div>
<label class="field-label" for="filter-jalan">Status Jalan</label>
<select id="filter-jalan" class="ctrl-select">
<option value="all">Semua status</option>
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten">Kabupaten</option>
</select>
<label class="field-label" for="filter-parsil">Status Parsil</label>
<select id="filter-parsil" class="ctrl-select">
<option value="all">Semua status</option>
<option value="SHM">SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
<label class="field-label" for="filter-access">Akses Parsil ke Jalan</label>
<select id="filter-access" class="ctrl-select">
<option value="all">Semua jarak</option>
<option value="near">Dekat (&lt; 50 m)</option>
<option value="medium">Sedang (50-200 m)</option>
<option value="far">Jauh (&gt; 200 m)</option>
</select>
<div id="analysis-box" class="analysis-box">
<span class="analysis-empty">Klik satu parsil untuk melihat jalan terdekat dan estimasi jaraknya.</span>
</div>
</div>
<div class="panel stats-panel">
<h3>Statistik Data</h3>
<div class="stats-grid">
<div class="stat-card"><div id="stat-jalan" class="stat-value">0</div><div class="stat-label">Total jalan tampil</div></div>
<div class="stat-card"><div id="stat-parsil" class="stat-value">0</div><div class="stat-label">Total parsil tampil</div></div>
<div class="stat-card"><div id="stat-panjang" class="stat-value">0 km</div><div class="stat-label">Akumulasi panjang jalan</div></div>
<div class="stat-card"><div id="stat-luas" class="stat-value">0 m2</div><div class="stat-label">Akumulasi luas parsil</div></div>
</div>
</div>
<div class="panel legend-panel">
<h3>Legenda</h3>
<div class="legend-row"><div class="legend-line" style="border-color:#ef4444;"></div> Jalan Nasional</div>
<div class="legend-row"><div class="legend-line" style="border-color:#3b82f6;"></div> Jalan Provinsi</div>
<div class="legend-row"><div class="legend-line" style="border-color:#22c55e;"></div> Jalan Kabupaten</div>
<div class="legend-row"><div class="legend-swatch" style="background:#14b8a6;"></div> Parsil SHM</div>
<div class="legend-row"><div class="legend-swatch" style="background:#f59e0b;"></div> Parsil HGB/HGU/HP</div>
</div>
</section>
</main>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script>
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.getElementById('sidebar').classList.toggle('collapsed');
});
const map = L.map('map').setView([-0.026, 109.342], 13);
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 });
const dark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19 });
osm.addTo(map);
const jalanLayer = L.layerGroup().addTo(map);
const parsilLayer = L.layerGroup().addTo(map);
const highlightLayer = L.layerGroup().addTo(map);
L.control.layers({ 'Standard': osm, 'Dark': dark }, { 'Jalan': jalanLayer, 'Parsil': parsilLayer }, { position: 'bottomleft' }).addTo(map);
let jalanData = [];
let parsilData = [];
let activeMode = 'all';
let selectedParsilId = null;
const jalanStyles = {
Nasional: { color: '#ef4444', weight: 5, opacity: .92 },
Provinsi: { color: '#3b82f6', weight: 4, opacity: .9 },
Kabupaten: { color: '#22c55e', weight: 3, opacity: .88 },
default: { color: '#94a3b8', weight: 3, opacity: .85 }
};
const parsilColors = {
SHM: '#14b8a6',
HGB: '#f59e0b',
HGU: '#a855f7',
HP: '#64748b'
};
function normalizeGeom(geom) {
if (!geom) return null;
const parsed = typeof geom === 'string' ? JSON.parse(geom) : geom;
if (!parsed) return null;
if (parsed.type === 'Feature') return parsed;
if (parsed.type && parsed.coordinates) return { type: 'Feature', properties: {}, geometry: parsed };
return null;
}
function formatLength(meters) {
const value = Number(meters) || 0;
if (value >= 1000) return (value / 1000).toFixed(2) + ' km';
return value.toFixed(0) + ' m';
}
function formatArea(value) {
const area = Number(value) || 0;
if (area >= 10000) return (area / 10000).toFixed(2) + ' ha';
return area.toLocaleString('id-ID', { maximumFractionDigits: 0 }) + ' m2';
}
function jalanStyle(status) {
return jalanStyles[status] || jalanStyles.default;
}
function parsilStyle(status, highlighted = false) {
return {
color: highlighted ? '#f8fafc' : (parsilColors[status] || '#64748b'),
weight: highlighted ? 4 : 2,
opacity: highlighted ? 1 : .95,
fillColor: parsilColors[status] || '#64748b',
fillOpacity: highlighted ? .38 : .24
};
}
function getFilteredJalan() {
const status = document.getElementById('filter-jalan').value;
if (activeMode === 'parsil') return [];
return jalanData.filter(item => status === 'all' || item.status === status);
}
function getFilteredParsil() {
const status = document.getElementById('filter-parsil').value;
const access = document.getElementById('filter-access').value;
if (activeMode === 'jalan') return [];
return parsilData.filter(item => {
if (status !== 'all' && item.status !== status) return false;
if (activeMode !== 'analysis' && access === 'all') return true;
const nearest = findNearestRoad(item);
const distance = nearest ? nearest.distance : Infinity;
if (access === 'near') return distance < 50;
if (access === 'medium') return distance >= 50 && distance <= 200;
if (access === 'far') return distance > 200;
return true;
});
}
function findNearestRoad(parsil) {
const parsilGeom = normalizeGeom(parsil.geom);
if (!parsilGeom || !jalanData.length || !window.turf) return null;
let center;
try {
center = turf.centroid(parsilGeom);
} catch (e) {
return null;
}
let nearest = null;
jalanData.forEach(jalan => {
const roadGeom = normalizeGeom(jalan.geom);
if (!roadGeom) return;
try {
const distance = turf.pointToLineDistance(center, roadGeom, { units: 'meters' });
if (!nearest || distance < nearest.distance) {
nearest = { jalan, distance, center };
}
} catch (e) {}
});
return nearest;
}
function renderJalan() {
jalanLayer.clearLayers();
const filtered = getFilteredJalan();
const bounds = [];
filtered.forEach(item => {
const feature = normalizeGeom(item.geom);
if (!feature) return;
const layer = L.geoJSON(feature, {
style: jalanStyle(item.status),
onEachFeature: (_, lyr) => {
lyr.bindPopup(`
<div class="popup-title">${item.nama_jalan}</div>
<div class="popup-row"><strong>Status:</strong> ${item.status}</div>
<div class="popup-row"><strong>Panjang:</strong> ${formatLength(item.panjang)}</div>
`);
}
}).addTo(jalanLayer);
bounds.push(layer.getBounds());
});
return bounds;
}
function renderParsil() {
parsilLayer.clearLayers();
const filtered = getFilteredParsil();
const bounds = [];
filtered.forEach(item => {
const feature = normalizeGeom(item.geom);
if (!feature) return;
const nearest = findNearestRoad(item);
const layer = L.geoJSON(feature, {
style: parsilStyle(item.status, item.id === selectedParsilId),
onEachFeature: (_, lyr) => {
lyr.on('click', () => selectParsil(item.id));
lyr.bindPopup(`
<div class="popup-title">${item.nama_pemilik}</div>
<div class="popup-row"><strong>Status:</strong> ${item.status}</div>
<div class="popup-row"><strong>Luas:</strong> ${formatArea(item.luas)}</div>
<div class="popup-row"><strong>Jalan terdekat:</strong> ${nearest ? nearest.jalan.nama_jalan : '-'}</div>
<div class="popup-row"><strong>Jarak akses:</strong> ${nearest ? nearest.distance.toFixed(1) + ' m' : '-'}</div>
`);
}
}).addTo(parsilLayer);
bounds.push(layer.getBounds());
});
return bounds;
}
function renderStats() {
const filteredJalan = getFilteredJalan();
const filteredParsil = getFilteredParsil();
const totalPanjang = filteredJalan.reduce((sum, item) => sum + (Number(item.panjang) || 0), 0);
const totalLuas = filteredParsil.reduce((sum, item) => sum + (Number(item.luas) || 0), 0);
document.getElementById('stat-jalan').textContent = filteredJalan.length;
document.getElementById('stat-parsil').textContent = filteredParsil.length;
document.getElementById('stat-panjang').textContent = formatLength(totalPanjang);
document.getElementById('stat-luas').textContent = formatArea(totalLuas);
document.getElementById('data-count').textContent = `${jalanData.length} jalan • ${parsilData.length} parsil`;
}
function renderAnalysis() {
const box = document.getElementById('analysis-box');
highlightLayer.clearLayers();
if (!selectedParsilId) {
box.innerHTML = '<span class="analysis-empty">Klik satu parsil untuk melihat jalan terdekat dan estimasi jaraknya.</span>';
return;
}
const parsil = parsilData.find(item => item.id === selectedParsilId);
const nearest = parsil ? findNearestRoad(parsil) : null;
if (!parsil || !nearest) {
box.innerHTML = '<span class="analysis-empty">Analisis belum tersedia untuk parsil ini.</span>';
return;
}
const center = nearest.center;
const roadGeom = normalizeGeom(nearest.jalan.geom);
if (center && roadGeom) {
try {
const nearestPoint = turf.nearestPointOnLine(roadGeom, center, { units: 'meters' });
const coords = center.geometry.coordinates;
const roadCoords = nearestPoint.geometry.coordinates;
L.polyline([[coords[1], coords[0]], [roadCoords[1], roadCoords[0]]], {
color: '#f59e0b', weight: 3, dashArray: '8,6', opacity: .95
}).addTo(highlightLayer);
} catch (e) {}
}
box.innerHTML = `
<strong>${parsil.nama_pemilik}</strong><br>
Jalan terdekat: <strong>${nearest.jalan.nama_jalan}</strong><br>
Status jalan: <strong>${nearest.jalan.status}</strong><br>
Estimasi jarak akses: <strong>${nearest.distance.toFixed(1)} m</strong>
`;
}
function renderAll(zoomToData = false) {
const roadBounds = renderJalan();
const parcelBounds = renderParsil();
renderStats();
renderAnalysis();
if (zoomToData) {
const allBounds = [...roadBounds, ...parcelBounds].filter(bounds => bounds && bounds.isValid && bounds.isValid());
if (allBounds.length) {
const merged = L.latLngBounds(allBounds[0].getSouthWest(), allBounds[0].getNorthEast());
allBounds.slice(1).forEach(bounds => merged.extend(bounds));
map.fitBounds(merged, { padding: [50, 50] });
}
}
}
function selectParsil(id) {
selectedParsilId = id;
if (activeMode !== 'analysis') setMode('analysis', false);
renderAll(false);
const item = parsilData.find(parsil => parsil.id === id);
const feature = item ? normalizeGeom(item.geom) : null;
if (feature) {
const layer = L.geoJSON(feature);
map.fitBounds(layer.getBounds(), { padding: [70, 70], maxZoom: 18 });
}
}
function setMode(mode, rerender = true) {
activeMode = mode;
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.mode === mode));
if (rerender) renderAll(false);
}
function renderSearchResults(query) {
const results = document.getElementById('search-results');
const q = query.trim().toLowerCase();
if (!q) {
results.classList.remove('active');
results.innerHTML = '';
return;
}
const roads = jalanData
.filter(item => item.nama_jalan.toLowerCase().includes(q))
.slice(0, 8)
.map(item => ({ type: 'jalan', id: item.id, title: item.nama_jalan, desc: `Jalan ${item.status}${formatLength(item.panjang)}` }));
const parcels = parsilData
.filter(item => item.nama_pemilik.toLowerCase().includes(q) || item.status.toLowerCase().includes(q))
.slice(0, 8)
.map(item => ({ type: 'parsil', id: item.id, title: item.nama_pemilik, desc: `Parsil ${item.status}${formatArea(item.luas)}` }));
const matches = [...roads, ...parcels].slice(0, 12);
if (!matches.length) {
results.innerHTML = '<div style="padding:8px 10px; color:#94a3b8; font-size:12px;">Tidak ditemukan.</div>';
} else {
results.innerHTML = matches.map(item => `
<div class="search-result-item" onclick="window.selectSearchResult('${item.type}', ${item.id})">
<span class="search-result-name">${item.title}</span>
<span class="search-result-desc">${item.desc}</span>
</div>
`).join('');
}
results.classList.add('active');
}
window.selectSearchResult = function(type, id) {
document.getElementById('search-results').classList.remove('active');
if (type === 'parsil') {
selectParsil(id);
return;
}
const item = jalanData.find(jalan => jalan.id === id);
const feature = item ? normalizeGeom(item.geom) : null;
if (!feature) return;
setMode('jalan', false);
renderAll(false);
const layer = L.geoJSON(feature, { style: { ...jalanStyle(item.status), weight: 8 } }).addTo(highlightLayer);
map.fitBounds(layer.getBounds(), { padding: [70, 70], maxZoom: 18 });
};
async function loadData(zoomToData = true) {
document.getElementById('data-count').textContent = 'Memuat data...';
try {
const [jalanRes, parsilRes] = await Promise.all([
fetch('backend/read_jalan.php'),
fetch('backend/read_parsil.php')
]);
const jalanJson = await jalanRes.json();
const parsilJson = await parsilRes.json();
jalanData = (jalanJson.success && jalanJson.data) ? jalanJson.data : [];
parsilData = (parsilJson.success && parsilJson.data) ? parsilJson.data : [];
renderAll(zoomToData);
} catch (error) {
console.error('Gagal memuat data jalan/parsil:', error);
document.getElementById('data-count').textContent = 'Gagal memuat data';
}
}
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => setMode(btn.dataset.mode));
});
document.getElementById('filter-jalan').addEventListener('change', () => renderAll(false));
document.getElementById('filter-parsil').addEventListener('change', () => renderAll(false));
document.getElementById('filter-access').addEventListener('change', () => {
if (document.getElementById('filter-access').value !== 'all') setMode('analysis', false);
renderAll(false);
});
document.getElementById('search-input').addEventListener('input', e => renderSearchResults(e.target.value));
document.getElementById('btn-refresh').addEventListener('click', async () => {
const btn = document.getElementById('btn-refresh');
btn.textContent = 'Memuat...';
await loadData(false);
btn.textContent = 'Refresh';
});
loadData();
</script>
</body>
</html>