Initial commit untuk UAS SIG
This commit is contained in:
@@ -0,0 +1,882 @@
|
||||
// ============================================
|
||||
// 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();
|
||||
Reference in New Issue
Block a user