883 lines
32 KiB
JavaScript
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} m²
|
|
</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, '&').replace(/</g, '<')
|
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
// Start
|
|
loadAllData();
|