Files
UAS_SIG/js/map.js
T
2026-06-08 21:16:23 +07:00

883 lines
32 KiB
JavaScript

// ============================================
// map.js — Logic Peta WebGIS Terpadu
// ============================================
// ------- Konfigurasi endpoint backend -------
const API_READ_SPBU = 'backend/read.php';
const API_CREATE_SPBU = 'backend/create.php';
const API_READ_JALAN = 'backend/read_jalan.php';
const API_CREATE_JALAN= 'backend/create_jalan.php';
const API_READ_PARSIL = 'backend/read_parsil.php';
const API_CREATE_PARSIL='backend/create_parsil.php';
const API_READ_IBADAH = 'backend/read_ibadah.php';
const API_CREATE_IBADAH='backend/create_ibadah.php';
const API_READ_MISKIN = 'backend/read_miskin.php';
const API_CREATE_MISKIN='backend/create_miskin.php';
// ------- State Internal & Data Storage -------
window.appData = {
spbu: [],
jalan: [],
parsil: [],
ibadah: [],
miskin: []
};
// ------- Inisialisasi Basemap -------
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, attribution: '© OpenStreetMap'
});
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19, attribution: 'Tiles © Esri'
});
const dark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 19, attribution: '© CartoDB'
});
// ------- Inisialisasi Peta -------
const map = L.map('map', {
center: [-0.0260, 109.3425], // Pusat Kota Pontianak
zoom: 13,
zoomControl: true,
layers: [osm] // Default basemap
});
// Tambah Basemap & Overlay Switcher (Layer Control)
const baseMaps = {
"Peta Standar (OSM)": osm,
"Citra Satelit": satellite,
"Peta Gelap": dark
};
// Inisialisasi Layer Groups
const layerSpbu24 = L.layerGroup().addTo(map);
const layerSpbuTidak = L.layerGroup().addTo(map);
const layerJalan = L.layerGroup().addTo(map);
const layerParsil = L.layerGroup().addTo(map);
const layerIbadah = L.layerGroup().addTo(map);
const layerMiskin = L.layerGroup().addTo(map);
const overlays = {
"SPBU Buka 24 Jam": layerSpbu24,
"SPBU Tidak 24 Jam": layerSpbuTidak,
"Informasi Jalan": layerJalan,
"Parsil Tanah": layerParsil,
"Rumah Ibadah": layerIbadah,
"Penduduk Miskin": layerMiskin
};
L.control.layers(baseMaps, overlays, { position: 'bottomleft' }).addTo(map);
// ------- Tombol Geolocation (Lokasi Saya) -------
const LocateControl = L.Control.extend({
options: { position: 'topleft' },
onAdd: function (map) {
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
container.style.backgroundColor = 'white';
container.style.width = '34px';
container.style.height = '34px';
container.style.cursor = 'pointer';
container.style.display = 'flex';
container.style.justifyContent = 'center';
container.style.alignItems = 'center';
container.innerHTML = '<span style="font-size:18px;" title="Lokasi Saya">📍</span>';
container.onclick = function(e) {
e.stopPropagation();
showToast('Mencari lokasi...', 'success');
map.locate({ setView: true, maxZoom: 17, enableHighAccuracy: true });
}
return container;
}
});
map.addControl(new LocateControl());
let gpsMarker = null;
let gpsPulse = null;
map.on('locationfound', function(e) {
if (gpsMarker) map.removeLayer(gpsMarker);
if (gpsPulse) map.removeLayer(gpsPulse);
const pulseIcon = L.divIcon({ className: 'gps-pulse', iconSize: [40, 40] });
const dotIcon = L.divIcon({ className: 'gps-marker', iconSize: [16, 16] });
gpsPulse = L.marker(e.latlng, { icon: pulseIcon }).addTo(map);
gpsMarker = L.marker(e.latlng, { icon: dotIcon }).addTo(map);
showToast('Lokasi ditemukan!', 'success');
});
map.on('locationerror', function(e) {
showToast('Tidak dapat mengakses GPS.', 'error');
});
// ------- Mode State -------
let currentMode = 'spbu'; // 'spbu', 'jalan', 'parsil', 'ibadah', 'miskin'
let inputPopup = null;
let currentDrawnLayer = null; // layer temp saat menggambar
// Inisialisasi Leaflet-Geoman
map.pm.addControls({
position: 'topleft',
drawMarker: false,
drawCircleMarker: false,
drawPolyline: false,
drawRectangle: false,
drawPolygon: false,
drawCircle: false,
drawText: false,
editMode: false,
dragMode: false,
cutPolygon: false,
removalMode: true
});
// Transisi Mode UI
const btnSpbu = document.getElementById('btn-mode-spbu');
const btnJalan = document.getElementById('btn-mode-jalan');
const btnParsil = document.getElementById('btn-mode-parsil');
const btnIbadah = document.getElementById('btn-mode-ibadah');
const btnMiskin = document.getElementById('btn-mode-miskin');
const hintBar = document.getElementById('hint-bar');
function switchMode(mode) {
currentMode = mode;
btnSpbu.classList.toggle('active', mode === 'spbu');
btnJalan.classList.toggle('active', mode === 'jalan');
btnParsil.classList.toggle('active', mode === 'parsil');
btnIbadah.classList.toggle('active', mode === 'ibadah');
btnMiskin.classList.toggle('active', mode === 'miskin');
// Matikan PM Draw
map.pm.disableDraw();
if (inputPopup) map.closePopup(inputPopup);
if (currentDrawnLayer) {
map.removeLayer(currentDrawnLayer);
currentDrawnLayer = null;
}
if (mode === 'spbu') {
hintBar.innerHTML = '<span>Klik</span> pada peta untuk menambahkan SPBU baru';
} else if (mode === 'ibadah') {
hintBar.innerHTML = '<span>Klik</span> pada peta untuk menambahkan Rumah Ibadah';
} else if (mode === 'miskin') {
hintBar.innerHTML = '<span>Klik</span> pada peta untuk menambahkan Penduduk Miskin';
} else if (mode === 'jalan') {
hintBar.innerHTML = '<span>Klik start</span> lalu gambar garis jalan, klik titik terakhir untuk selesai';
map.pm.enableDraw('Line', { snappable: true, snapDistance: 20, pathOptions: { color: '#3b82f6', weight: 4 } });
} else if (mode === 'parsil') {
hintBar.innerHTML = '<span>Klik start</span> lalu gambar poligon, klik titik awal untuk selesai';
map.pm.enableDraw('Polygon', { snappable: true, snapDistance: 20, pathOptions: { color: '#3b82f6', weight: 2, fillColor: '#3b82f6', fillOpacity: 0.4 } });
}
}
btnSpbu.addEventListener('click', () => switchMode('spbu'));
btnJalan.addEventListener('click', () => switchMode('jalan'));
btnParsil.addEventListener('click', () => switchMode('parsil'));
btnIbadah.addEventListener('click', () => switchMode('ibadah'));
btnMiskin.addEventListener('click', () => switchMode('miskin'));
// ============================================
// SPBU LOGIC
// ============================================
let totalSpbu = 0;
function updateCounter(count) {
totalSpbu = count;
document.getElementById('spbu-count').textContent = totalSpbu;
}
function createSpbuIcon(status) {
const color = status === '24jam' ? '#22c55e' : '#f59e0b';
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 48" width="36" height="48">
<path d="M18 0C8.06 0 0 8.06 0 18c0 13.5 18 30 18 30s18-16.5 18-30C36 8.06 27.94 0 18 0z" fill="${color}" stroke="#fff" stroke-width="2"/>
<text x="18" y="22" text-anchor="middle" fill="#fff" font-family="Arial,sans-serif" font-size="11" font-weight="bold">SPBU</text>
</svg>`;
return L.divIcon({ html: svg, className: '', iconSize: [36, 48], iconAnchor: [18, 48], popupAnchor: [0, -50] });
}
function spbuBadge(status) {
return status === '24jam'
? '<span class="badge badge-active">24 Jam</span>'
: '<span class="badge badge-inactive">Tidak 24 Jam</span>';
}
function buildSpbuPopup(d) {
return `<div class="popup-info">
<div class="popup-title">${escapeHtml(d.nama)}</div>
<table class="popup-table">
<tr><td>No. SPBU</td><td>${escapeHtml(d.no_spbu)}</td></tr>
<tr><td>Status</td><td>${spbuBadge(d.status)}</td></tr>
<tr><td>Lat/Lng</td><td>${d.latitude}, ${d.longitude}</td></tr>
</table>
</div>`;
}
function addSpbuToMap(d) {
const marker = L.marker([d.latitude, d.longitude], { icon: createSpbuIcon(d.status) });
marker.bindPopup(buildSpbuPopup(d), { maxWidth: 260 });
if (d.status === '24jam') {
marker.addTo(layerSpbu24);
} else {
marker.addTo(layerSpbuTidak);
}
window.appData.spbu.push({ data: d, layer: marker });
}
// SPBU, IBADAH, MISKIN Map Click logic
map.on('click', async function (e) {
// Hide search results if clicking map
document.getElementById('search-results').classList.remove('show');
if (!['spbu', 'ibadah', 'miskin'].includes(currentMode)) return; // abaikan jika bukan point mode
if (inputPopup) map.closePopup(inputPopup);
const { lat, lng } = e.latlng;
let formHtml = '';
if (currentMode === 'spbu') {
formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah SPBU Baru</div>
<form id="form-spbu" novalidate>
<div class="form-group">
<label>Nama SPBU</label>
<input id="f-nama" type="text" placeholder="cth. SPBU Sudirman" required />
</div>
<div class="form-group">
<label>Nomor SPBU</label>
<input id="f-no" type="text" placeholder="cth. 64.751.01" required />
</div>
<div class="form-group">
<label>Status</label>
<select id="f-status">
<option value="24jam">24 Jam</option>
<option value="tidak">Tidak 24 Jam</option>
</select>
</div>
<div class="form-actions">
<button type="button" id="btn-cancel" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
} else if (currentMode === 'ibadah') {
showToast('Mendapatkan alamat otomatis...', 'success');
let autoAddress = '';
try {
const resp = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`);
const jData = await resp.json();
if(jData && jData.display_name) autoAddress = jData.display_name;
} catch(err) { console.error(err); }
formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah Rumah Ibadah</div>
<form id="form-ibadah" novalidate>
<div class="form-group">
<label>Nama Rumah Ibadah</label>
<input id="fi-nama" type="text" placeholder="cth. Masjid Raya" required />
</div>
<div class="form-group">
<label>Alamat Otomatis</label>
<input id="fi-alamat" type="text" value="${escapeHtml(autoAddress)}" required />
</div>
<div class="form-actions">
<button type="button" id="btn-cancel" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
} else if (currentMode === 'miskin') {
formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah Penduduk Miskin</div>
<form id="form-miskin" novalidate>
<div class="form-group">
<label>Nama Kepala Keluarga</label>
<input id="fm-nama" type="text" placeholder="cth. Bpk. Joni" required />
</div>
<div class="form-group">
<label>Keterangan Lengkap</label>
<input id="fm-ket" type="text" placeholder="cth. Rumah Papan, dll" />
</div>
<div class="form-actions">
<button type="button" id="btn-cancel" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
}
inputPopup = L.popup({ maxWidth: 320, closeButton: false })
.setLatLng(e.latlng).setContent(formHtml).openOn(map);
setTimeout(() => {
document.getElementById('btn-cancel')?.addEventListener('click', () => map.closePopup(inputPopup));
// SPBU Handler
document.getElementById('form-spbu')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = {
nama: document.getElementById('f-nama').value.trim(),
no_spbu: document.getElementById('f-no').value.trim(),
status: document.getElementById('f-status').value,
latitude: lat, longitude: lng
};
if(!payload.nama || !payload.no_spbu) return;
document.getElementById('btn-save').disabled = true;
try {
const res = await fetch(API_CREATE_SPBU, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup);
addSpbuToMap(json.data);
updateCounter(totalSpbu + 1);
showToast('SPBU berhasil ditambahkan!');
} else showToast(json.message, 'error');
} catch (err) { showToast('Error server', 'error'); }
});
// Ibadah Handler
document.getElementById('form-ibadah')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = { nama: document.getElementById('fi-nama').value.trim(), alamat: document.getElementById('fi-alamat').value.trim(), latitude: lat, longitude: lng };
if(!payload.nama || !payload.alamat) return;
document.getElementById('btn-save').disabled = true;
try {
const res = await fetch(API_CREATE_IBADAH, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup); addIbadahToMap(json.data); showToast('Rumah Ibadah ditambahkan!');
} else showToast(json.message, 'error');
} catch (err) { showToast('Error server', 'error'); }
});
// Miskin Handler
document.getElementById('form-miskin')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = { nama_kk: document.getElementById('fm-nama').value.trim(), keterangan: document.getElementById('fm-ket').value.trim(), latitude: lat, longitude: lng };
if(!payload.nama_kk) return;
document.getElementById('btn-save').disabled = true;
try {
const res = await fetch(API_CREATE_MISKIN, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup); addMiskinToMap(json.data); showToast('Data keluarga ditambahkan!');
} else showToast(json.message, 'error');
} catch (err) { showToast('Error server', 'error'); }
});
}, 0);
});
// ============================================
// IBADAH & MISKIN LOGIC (Radius Analysis)
// ============================================
function createIbadahIcon() {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 48" width="36" height="48">
<path d="M18 0C8.06 0 0 8.06 0 18c0 13.5 18 30 18 30s18-16.5 18-30C36 8.06 27.94 0 18 0z" fill="#8b5cf6" stroke="#fff" stroke-width="2"/>
<text x="18" y="22" text-anchor="middle" fill="#fff" font-family="Arial,sans-serif" font-size="11" font-weight="bold">🕌</text>
</svg>`;
return L.divIcon({ html: svg, className: '', iconSize: [36, 48], iconAnchor: [18, 48], popupAnchor: [0, -48] });
}
let activeRadiusCircle = null;
window.updateRadius = function(id, radius, lat, lng) {
document.getElementById(`radius-val-${id}`).innerText = `${radius} Meter`;
if (activeRadiusCircle) map.removeLayer(activeRadiusCircle);
activeRadiusCircle = L.circle([lat, lng], {
color: '#8b5cf6',
fillColor: '#8b5cf6',
fillOpacity: 0.2,
radius: parseInt(radius)
}).addTo(map);
let caughtCount = 0;
const centerPoint = turf.point([lng, lat]);
window.appData.miskin.forEach(m => {
const targetPoint = turf.point([m.data.longitude, m.data.latitude]);
const distance = turf.distance(centerPoint, targetPoint, { units: 'meters' });
if (distance <= radius) {
caughtCount++;
// Set point menjadi merah
m.layer.setStyle({ fillColor: '#ef4444', color: '#ef4444', radius: 8 });
} else {
// Kembali menjadi hitam
m.layer.setStyle({ fillColor: '#000000', color: '#000000', radius: 6 });
}
});
document.getElementById(`radius-res-${id}`).innerHTML = `<b>${caughtCount}</b> rumah keluarga miskin dalam radius.`;
};
// When popup closes, remove radius circle
map.on('popupclose', () => {
if (activeRadiusCircle) {
map.removeLayer(activeRadiusCircle);
activeRadiusCircle = null;
window.appData.miskin.forEach(m => {
m.layer.setStyle({ fillColor: '#000000', color: '#000000', radius: 6 });
});
}
});
function buildIbadahPopup(d) {
return `<div class="popup-info">
<div class="popup-title">${escapeHtml(d.nama)}</div>
<table class="popup-table">
<tr><td>Alamat</td><td style="white-space:normal; line-height:1.4;">${escapeHtml(d.alamat)}</td></tr>
</table>
<div class="radius-analysis-container">
<label style="font-size:.75rem; color:var(--clr-muted); margin-bottom:4px; display:block">Analisis Radius Kemiskinan</label>
<input type="range" id="radius-slider-${d.id}" min="100" max="5000" step="100" value="0"
oninput="updateRadius(${d.id}, this.value, ${d.latitude}, ${d.longitude})" />
<div style="display:flex; justify-content:space-between; font-size:.75rem;">
<span id="radius-val-${d.id}" class="radius-value">0 Meter</span>
<span id="radius-res-${d.id}" style="color:var(--clr-muted)">Geser slider</span>
</div>
</div>
</div>`;
}
function buildMiskinPopup(d) {
return `<div class="popup-info">
<div class="popup-title">Kepala Keluarga: ${escapeHtml(d.nama_kk)}</div>
<table class="popup-table">
<tr><td>Keterangan</td><td style="white-space:normal;">${escapeHtml(d.keterangan)}</td></tr>
</table>
</div>`;
}
function addIbadahToMap(d) {
const marker = L.marker([d.latitude, d.longitude], { icon: createIbadahIcon() });
marker.bindPopup(buildIbadahPopup(d), { maxWidth: 300, minWidth: 260 });
marker.addTo(layerIbadah);
window.appData.ibadah.push({ data: d, layer: marker });
}
function addMiskinToMap(d) {
// Gunakan L.circleMarker sebagai point sederhana (warna bawaan Hitam)
const marker = L.circleMarker([d.latitude, d.longitude], {
radius: 6,
fillColor: "#000000",
color: "#000000",
weight: 2,
opacity: 1,
fillOpacity: 0.8
});
marker.bindPopup(buildMiskinPopup(d), { maxWidth: 260 });
marker.addTo(layerMiskin);
window.appData.miskin.push({ data: d, layer: marker });
}
// ============================================
// JALAN & PARSIL DRAW LOGIC
// ============================================
// Pemetaan Warna
function getJalanColor(status) {
if(status === 'Nasional') return '#ef4444';
if(status === 'Provinsi') return '#eab308';
return '#10b981'; // Kabupaten
}
function getParsilStyle(status) {
if (status === 'SHM') return { color: '#2563eb', fillColor: '#3b82f6', fillOpacity: 0.5 };
if (status === 'HGB') return { color: '#7c3aed', fillColor: '#8b5cf6', fillOpacity: 0.5 };
if (status === 'HGU') return { color: '#db2777', fillColor: '#ec4899', fillOpacity: 0.5 };
return { color: '#0d9488', fillColor: '#14b8a6', fillOpacity: 0.5 }; // HP
}
// Leaflet Geoman event saat entitas selesai digambar
map.on('pm:create', (e) => {
currentDrawnLayer = e.layer;
const geom = currentDrawnLayer.toGeoJSON().geometry;
if (currentMode === 'jalan' && e.shape === 'Line') {
// Hitung panjang dalam meter via Turf.js
const lengthKm = turf.length(currentDrawnLayer.toGeoJSON());
const panjangMeter = (lengthKm * 1000).toFixed(2);
openJalanForm(e.layer, geom, panjangMeter);
// Refresh mode drawing
map.pm.enableDraw('Line');
}
else if (currentMode === 'parsil' && e.shape === 'Polygon') {
// Hitung luas dalam meter persegi via Turf.js
const areaSqMeters = turf.area(currentDrawnLayer.toGeoJSON());
const luasMeter = areaSqMeters.toFixed(2);
openParsilForm(e.layer, geom, luasMeter);
// Refresh mode drawing
map.pm.enableDraw('Polygon');
}
});
function openJalanForm(layer, geom, panjang) {
const centerLatlng = layer.getBounds().getCenter();
const formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah Data Jalan</div>
<form id="form-jalan" novalidate>
<div class="form-group">
<label>Nama Jalan</label>
<input id="fj-nama" type="text" placeholder="cth. Jl. Ahmad Yani" required />
</div>
<div class="form-group">
<label>Status Jalan</label>
<select id="fj-status">
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten">Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang (otomatis)</label>
<input type="text" value="${panjang} Meter" readonly />
</div>
<div class="form-actions">
<button type="button" id="btn-cancel-draw" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save-draw" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
inputPopup = L.popup({ maxWidth: 320, closeButton: false })
.setLatLng(centerLatlng).setContent(formHtml).openOn(map);
setTimeout(() => {
document.getElementById('btn-cancel-draw')?.addEventListener('click', () => {
map.closePopup(inputPopup);
map.removeLayer(layer);
});
document.getElementById('form-jalan')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = {
nama_jalan: document.getElementById('fj-nama').value.trim(),
status: document.getElementById('fj-status').value,
panjang: panjang,
geom: geom
};
if(!payload.nama_jalan) return;
document.getElementById('btn-save-draw').disabled = true;
try {
const res = await fetch(API_CREATE_JALAN, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup);
map.removeLayer(layer);
currentDrawnLayer = null;
addJalanToMap(json.data);
showToast('Jalan berhasil ditambahkan!');
} else showToast(json.message, 'error');
} catch (e) { showToast('Error server', 'error'); }
document.getElementById('btn-save-draw').disabled = false;
});
}, 0);
}
function openParsilForm(layer, geom, luas) {
const centerLatlng = layer.getBounds().getCenter();
const formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah Parsil Tanah</div>
<form id="form-parsil" novalidate>
<div class="form-group">
<label>Nama Pemilik</label>
<input id="fp-nama" type="text" placeholder="cth. Budi Santoso" required />
</div>
<div class="form-group">
<label>Status Kepemilikan</label>
<select id="fp-status">
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Hak Guna Bangunan (HGB)</option>
<option value="HGU">Hak Guna Usaha (HGU)</option>
<option value="HP">Hak Pakai (HP)</option>
</select>
</div>
<div class="form-group">
<label>Luas (otomatis)</label>
<input type="text" value="${luas} m²" readonly />
</div>
<div class="form-actions">
<button type="button" id="btn-cancel-draw" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save-draw" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
inputPopup = L.popup({ maxWidth: 320, closeButton: false })
.setLatLng(centerLatlng).setContent(formHtml).openOn(map);
setTimeout(() => {
document.getElementById('btn-cancel-draw')?.addEventListener('click', () => {
map.closePopup(inputPopup);
map.removeLayer(layer);
});
document.getElementById('form-parsil')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = {
nama_pemilik: document.getElementById('fp-nama').value.trim(),
status: document.getElementById('fp-status').value,
luas: luas,
geom: geom
};
if(!payload.nama_pemilik) return;
document.getElementById('btn-save-draw').disabled = true;
try {
const res = await fetch(API_CREATE_PARSIL, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup);
map.removeLayer(layer);
currentDrawnLayer = null;
addParsilToMap(json.data);
showToast('Parsil berhasil ditambahkan!');
} else showToast(json.message, 'error');
} catch (e) { showToast('Error server', 'error'); }
document.getElementById('btn-save-draw').disabled = false;
});
}, 0);
}
// Popups Data Terload
function buildJalanPopup(d) {
return `<div class="popup-info">
<div class="popup-title">${escapeHtml(d.nama_jalan)}</div>
<table class="popup-table">
<tr><td>Status</td><td><span class="badge" style="background:#475569;color:#fff">${d.status}</span></td></tr>
<tr><td>Panjang</td><td>${d.panjang} m</td></tr>
</table>
</div>`;
}
function buildParsilPopup(d) {
return `<div class="popup-info">
<div class="popup-title">Pemilik: ${escapeHtml(d.nama_pemilik)}</div>
<table class="popup-table">
<tr><td>Sertifikat</td><td><span class="badge" style="background:#475569;color:#fff">${d.status}</span></td></tr>
<tr><td>Luas</td><td>${d.luas} m²</td></tr>
</table>
</div>`;
}
// Add GeoJSON to Layer
function addJalanToMap(d) {
const layer = L.geoJSON(d.geom, {
style: { color: getJalanColor(d.status), weight: 5, opacity: 0.9 }
}).bindPopup(buildJalanPopup(d)).addTo(layerJalan);
window.appData.jalan.push({ data: d, layer: layer });
}
// ------ Fitur Hover Info Parsil ------
function highlightParsil(e) {
const layer = e.target;
layer.setStyle({
weight: 4,
color: '#fff',
dashArray: '',
fillOpacity: 0.8
});
layer.bringToFront();
}
function resetParsilHighlight(e) {
const layer = e.target;
const status = layer.featureProps.status;
layer.setStyle(getParsilStyle(status));
}
function addParsilToMap(d) {
const layerGroup = L.geoJSON(d.geom, {
style: getParsilStyle(d.status),
onEachFeature: function(feature, layer) {
layer.featureProps = d; // Simpan properti referensi ke dalam layer
const tooltipContent = `
<div class="info-hover">
<h4>Informasi Parsil Tanah</h4>
<b>Pemilik: ${escapeHtml(d.nama_pemilik)}</b><br />
Sertifikat: <span class="badge" style="background:#475569;color:#fff">${d.status}</span><br />
Luas: ${d.luas}
</div>`;
layer.bindTooltip(tooltipContent, {
sticky: true, // Tooltip bergeser mengekori kursor kursor
direction: 'top',
className: 'custom-tooltip'
});
layer.on({
mouseover: highlightParsil,
mouseout: resetParsilHighlight
});
}
});
layerGroup.bindPopup(buildParsilPopup(d)).addTo(layerParsil);
window.appData.parsil.push({ data: d, layer: layerGroup });
}
// ============================================
// LOGIKA PENCARIAN (SEARCH)
// ============================================
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const filterBtns = document.querySelectorAll('.filter-btn');
let currentSearchFilter = 'all';
filterBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
filterBtns.forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
currentSearchFilter = e.target.dataset.filter;
performSearch(searchInput.value);
});
});
searchInput.addEventListener('input', (e) => performSearch(e.target.value));
// Tambahkan event focus jika user kembali nge-klik kolom search yg sudah ada isinya
searchInput.addEventListener('focus', (e) => performSearch(e.target.value));
function performSearch(query) {
query = query.toLowerCase().trim();
searchResults.innerHTML = '';
if (!query) {
searchResults.classList.remove('show');
return;
}
let results = [];
// Filter SPBU
if (currentSearchFilter === 'all' || currentSearchFilter === 'spbu') {
appData.spbu.forEach(item => {
if (item.data.nama.toLowerCase().includes(query) || item.data.no_spbu.toLowerCase().includes(query)) {
results.push({ type: 'SPBU', title: item.data.nama, meta: `No: ${item.data.no_spbu}`, item: item });
}
});
}
// Filter Jalan
if (currentSearchFilter === 'all' || currentSearchFilter === 'jalan') {
appData.jalan.forEach(item => {
if (item.data.nama_jalan.toLowerCase().includes(query)) {
results.push({ type: 'Jalan', title: item.data.nama_jalan, meta: `${item.data.status} · ${item.data.panjang}m`, item: item });
}
});
}
// Filter Parsil
if (currentSearchFilter === 'all' || currentSearchFilter === 'parsil') {
appData.parsil.forEach(item => {
if (item.data.nama_pemilik.toLowerCase().includes(query)) {
results.push({ type: 'Parsil', title: item.data.nama_pemilik, meta: `Sertifikat: ${item.data.status}`, item: item });
}
});
}
if (results.length === 0) {
searchResults.innerHTML = '<div class="search-result-item"><div class="result-title" style="color:var(--clr-muted)">Tidak ada hasil pemetaan</div></div>';
} else {
results.forEach(r => {
const div = document.createElement('div');
div.className = 'search-result-item';
div.innerHTML = `
<div class="result-title">${escapeHtml(r.title)}</div>
<div class="result-meta"><span><b>${r.type}</b></span> <span>${escapeHtml(r.meta)}</span></div>
`;
div.addEventListener('click', () => zoomToResult(r));
searchResults.appendChild(div);
});
}
searchResults.classList.add('show');
}
function zoomToResult(result) {
searchResults.classList.remove('show');
searchInput.value = ''; // Opsional: clear search box saat dipilih
const layer = result.item.layer;
if (result.type === 'SPBU') {
// FlyTo SPBU marker
map.flyTo(layer.getLatLng(), 18, { duration: 1.5 });
// Buka popup saat flyTo selesai (mendekati target)
setTimeout(() => layer.openPopup(), 1000);
} else {
// GeoJSON Layers berbentuk grup (bisa lebih dari satu poligon/garis)
const bounds = layer.getBounds();
map.flyToBounds(bounds, { duration: 1.5, maxZoom: 18 });
setTimeout(() => layer.openPopup(), 1000);
}
}
// ============================================
// DATA LOADER
// ============================================
async function loadAllData() {
try {
// Load SPBU
const r1 = await fetch(API_READ_SPBU);
const j1 = await r1.json();
if(j1.success) { j1.data.forEach(d => addSpbuToMap(d)); updateCounter(j1.count); }
// Load Jalan
const r2 = await fetch(API_READ_JALAN);
const j2 = await r2.json();
if(j2.success) j2.data.forEach(d => addJalanToMap(d));
// Load Parsil
const r3 = await fetch(API_READ_PARSIL);
const j3 = await r3.json();
if(j3.success) j3.data.forEach(d => addParsilToMap(d));
// Load Ibadah
const r4 = await fetch(API_READ_IBADAH);
const j4 = await r4.json();
if(j4.success) j4.data.forEach(d => addIbadahToMap(d));
// Load Miskin
const r5 = await fetch(API_READ_MISKIN);
const j5 = await r5.json();
if(j5.success) j5.data.forEach(d => addMiskinToMap(d));
} catch (err) {
showToast('Gagal memuat beberapa data.', 'error');
}
}
// ------- Helpers -------
function showToast(message, type = 'success') {
const container = document.getElementById('toast-container');
const t = document.createElement('div');
t.className = `toast toast-${type}`;
t.innerHTML = `<span class="toast-icon">${type === 'success' ? '✓' : '✕'}</span><span>${message}</span>`;
container.appendChild(t);
requestAnimationFrame(() => t.classList.add('toast-show'));
setTimeout(() => {
t.classList.remove('toast-show');
t.addEventListener('transitionend', () => t.remove(), { once: true });
}, 3000);
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// Start
loadAllData();