mengubah sistem menjadi lebih terfokus ke poverty
This commit is contained in:
+152
-300
@@ -1,84 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const API_BASE = 'api';
|
||||
const CENTER = [-0.0263, 109.3425];
|
||||
|
||||
const map = L.map('map', { zoomControl: false }).setView(CENTER, 13);
|
||||
L.control.zoom({ position: 'topleft' }).addTo(map);
|
||||
// ─── Map globals (only set on map page) ─────────────────────────────────────
|
||||
let map = null;
|
||||
let layers = null;
|
||||
let layerVisible = null;
|
||||
|
||||
const baseLayers = {
|
||||
'OSM Standard': L.tileLayer(
|
||||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
{ attribution: '© OpenStreetMap', maxZoom: 19 }
|
||||
),
|
||||
'CartoDB Light': L.tileLayer(
|
||||
'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
|
||||
{ attribution: '© CartoDB', maxZoom: 19 }
|
||||
),
|
||||
'Satellite (Esri)': L.tileLayer(
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
{ attribution: '© Esri', maxZoom: 19 }
|
||||
)
|
||||
};
|
||||
baseLayers['OSM Standard'].addTo(map);
|
||||
L.control.layers(baseLayers, {}, { position: 'bottomright', collapsed: true }).addTo(map);
|
||||
|
||||
const layers = {
|
||||
rumahIbadah: L.layerGroup().addTo(map),
|
||||
ibadahRadius: L.layerGroup().addTo(map),
|
||||
pendudukMiskin: L.layerGroup().addTo(map),
|
||||
spbu24: L.layerGroup().addTo(map),
|
||||
spbuNon24: L.layerGroup().addTo(map),
|
||||
jalanNasional: L.layerGroup().addTo(map),
|
||||
jalanProvinsi: L.layerGroup().addTo(map),
|
||||
jalanKabupaten: L.layerGroup().addTo(map),
|
||||
parsilSHM: L.layerGroup().addTo(map),
|
||||
parsilHGB: L.layerGroup().addTo(map),
|
||||
parsilHGU: L.layerGroup().addTo(map),
|
||||
parsilHP: L.layerGroup().addTo(map)
|
||||
};
|
||||
|
||||
const layerVisible = {
|
||||
rumahIbadah: true, ibadahRadius: true, pendudukMiskin: true,
|
||||
spbu24: true, spbuNon24: true,
|
||||
jalanNasional: true, jalanProvinsi: true, jalanKabupaten: true,
|
||||
parsilSHM: true, parsilHGB: true, parsilHGU: true, parsilHP: true
|
||||
};
|
||||
|
||||
const searchEntries = [];
|
||||
let searchActiveIndex = -1;
|
||||
const searchPointZoom = 15;
|
||||
const searchIntentPrefix = {
|
||||
jalan: 'jalan',
|
||||
spbu: 'spbu',
|
||||
rumahibadah: 'ri',
|
||||
pendudukmiskin: 'pm',
|
||||
parsil: 'parsil'
|
||||
};
|
||||
|
||||
const drawnItems = new L.FeatureGroup().addTo(map);
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polyline: { shapeOptions: { color: '#2563eb', weight: 3 } },
|
||||
polygon: { shapeOptions: { color: '#7c3aed', weight: 2, fillOpacity: 0.15 } },
|
||||
rectangle: false, circle: false, circlemarker: false, marker: false
|
||||
},
|
||||
edit: { featureGroup: drawnItems, remove: false }
|
||||
});
|
||||
|
||||
let pendingGeoJSON = null;
|
||||
let pendingLength = null;
|
||||
let pendingArea = null;
|
||||
let currentTab = 'kemiskinan';
|
||||
let geometryEditState = null;
|
||||
let _mapClickCb = null;
|
||||
// ─── Utilities ───────────────────────────────────────────────────────────────
|
||||
|
||||
function toast(msg, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
const t = document.createElement('div');
|
||||
t.className = `toast ${type}`;
|
||||
t.textContent = msg;
|
||||
document.getElementById('toast-container').appendChild(t);
|
||||
setTimeout(() => t.remove(), 3000);
|
||||
container.appendChild(t);
|
||||
setTimeout(() => t.remove(), 3500);
|
||||
}
|
||||
|
||||
function openModal(title, bodyHTML, footerHTML) {
|
||||
@@ -92,54 +30,62 @@ function closeModal() {
|
||||
document.getElementById('modal-overlay').classList.remove('open');
|
||||
}
|
||||
|
||||
document.getElementById('modal-overlay').addEventListener('click', e => {
|
||||
if (e.target.id === 'modal-overlay') closeModal();
|
||||
});
|
||||
const _modalOverlay = document.getElementById('modal-overlay');
|
||||
if (_modalOverlay) {
|
||||
_modalOverlay.addEventListener('click', e => {
|
||||
if (e.target.id === 'modal-overlay') closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
async function callAPI(ep, method = 'GET', body = null) {
|
||||
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
||||
if (body) opts.body = JSON.stringify(body);
|
||||
try {
|
||||
return await (await fetch(`${API_BASE}/${ep}`, opts)).json();
|
||||
const res = await fetch(`${API_BASE}/${ep}`, opts);
|
||||
if (res.status === 401) {
|
||||
window.location.href = '?page=login';
|
||||
return { status: 'error', message: 'Unauthorized' };
|
||||
}
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
return { status: 'error', message: 'Koneksi gagal' };
|
||||
}
|
||||
}
|
||||
|
||||
function showInstruction(msg) {
|
||||
const el = document.getElementById('instruction-banner');
|
||||
el.textContent = '🖱 ' + msg;
|
||||
el.classList.add('show');
|
||||
}
|
||||
|
||||
function hideInstruction() {
|
||||
document.getElementById('instruction-banner').classList.remove('show');
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function showInstruction(msg) {
|
||||
const el = document.getElementById('instruction-banner');
|
||||
if (el) { el.textContent = '🖱 ' + msg; el.classList.add('show'); }
|
||||
}
|
||||
|
||||
function hideInstruction() {
|
||||
const el = document.getElementById('instruction-banner');
|
||||
if (el) el.classList.remove('show');
|
||||
}
|
||||
|
||||
// ─── Search ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const searchEntries = [];
|
||||
let searchActiveIndex = -1;
|
||||
const searchPointZoom = 15;
|
||||
const searchIntentPrefix = { rumahibadah: 'ri', pendudukmiskin: 'pm' };
|
||||
|
||||
function normalizeSearchText(text) {
|
||||
return String(text || '')
|
||||
.toLowerCase()
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9\s]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
return String(text || '').toLowerCase().normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9\s]/g, ' ')
|
||||
.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getSearchIntent(query) {
|
||||
const q = normalizeSearchText(query);
|
||||
if (!q) return null;
|
||||
const aliases = [
|
||||
{ key: 'jalan', terms: ['jalan', 'road', 'street'] },
|
||||
{ key: 'spbu', terms: ['spbu', 'pom', 'bensin', 'bbm'] },
|
||||
{ key: 'rumahibadah', terms: ['masjid', 'gereja', 'pura', 'vihara', 'klenteng', 'ibadah'] },
|
||||
{ key: 'pendudukmiskin', terms: ['miskin', 'penduduk', 'warga', 'keluarga'] },
|
||||
{ key: 'parsil', terms: ['parsil', 'tanah', 'sertifikat', 'bidang', 'lahan'] }
|
||||
{ key: 'pendudukmiskin', terms: ['miskin', 'penduduk', 'warga', 'keluarga'] }
|
||||
];
|
||||
for (const alias of aliases) {
|
||||
if (alias.terms.some(term => q.includes(term))) return alias.key;
|
||||
@@ -167,36 +113,25 @@ function getSearchMatches(query) {
|
||||
const pool = prefix
|
||||
? searchEntries.filter(entry => entry.key.startsWith(prefix + ':'))
|
||||
: searchEntries;
|
||||
|
||||
return pool
|
||||
.map(entry => {
|
||||
let score = 0;
|
||||
const entryLabel = normalizeSearchText(entry.label);
|
||||
const entryType = normalizeSearchText(entry.typeLabel);
|
||||
const entryText = entry.searchText;
|
||||
if (entry.searchText === q) score += 120;
|
||||
if (entry.searchText.startsWith(q)) score += 80;
|
||||
if (entryLabel.startsWith(q)) score += 90;
|
||||
if (entryType.startsWith(q)) score += 60;
|
||||
if (entry.searchText.includes(q)) score += 50;
|
||||
const tokenHits = tokens.filter(token => entryText.includes(token) || entryLabel.includes(token)).length;
|
||||
score += tokenHits * 12;
|
||||
if (tokens.every(token => entryText.includes(token) || entryLabel.includes(token))) score += 40;
|
||||
if (tokens[0] && (entryText.split(' ').some(word => word.startsWith(tokens[0])) || entryLabel.split(' ').some(word => word.startsWith(tokens[0])))) score += 15;
|
||||
if (intent) {
|
||||
if (entry.key.startsWith(intent + ':')) score += 120;
|
||||
if (entryType.includes(intent)) score += 110;
|
||||
if (intent === 'jalan' && entryLabel.startsWith('jalan')) score += 40;
|
||||
if (intent === 'rumahibadah' && /(masjid|gereja|pura|vihara|klenteng)/.test(entryText)) score += 35;
|
||||
if (intent === 'spbu' && /(spbu|pom|bensin|bbm)/.test(entryText)) score += 35;
|
||||
if (intent === 'pendudukmiskin' && /(miskin|penduduk|warga)/.test(entryText)) score += 35;
|
||||
if (intent === 'parsil' && /(parsil|tanah|sertifikat|bidang)/.test(entryText)) score += 35;
|
||||
}
|
||||
return { ...entry, score };
|
||||
})
|
||||
.filter(entry => entry.score > 0)
|
||||
.sort((a, b) => b.score - a.score || a.label.localeCompare(b.label))
|
||||
.slice(0, 10);
|
||||
return pool.map(entry => {
|
||||
let score = 0;
|
||||
const entryLabel = normalizeSearchText(entry.label);
|
||||
const entryType = normalizeSearchText(entry.typeLabel);
|
||||
const entryText = entry.searchText;
|
||||
if (entry.searchText === q) score += 120;
|
||||
if (entry.searchText.startsWith(q)) score += 80;
|
||||
if (entryLabel.startsWith(q)) score += 90;
|
||||
if (entryType.startsWith(q)) score += 60;
|
||||
if (entry.searchText.includes(q)) score += 50;
|
||||
const tokenHits = tokens.filter(token => entryText.includes(token) || entryLabel.includes(token)).length;
|
||||
score += tokenHits * 12;
|
||||
if (tokens.every(token => entryText.includes(token) || entryLabel.includes(token))) score += 40;
|
||||
if (intent) {
|
||||
if (entry.key.startsWith(intent + ':')) score += 120;
|
||||
if (entryType.includes(intent)) score += 110;
|
||||
}
|
||||
return { ...entry, score };
|
||||
}).filter(e => e.score > 0).sort((a, b) => b.score - a.score || a.label.localeCompare(b.label)).slice(0, 10);
|
||||
}
|
||||
|
||||
function renderSearchResults(query) {
|
||||
@@ -209,31 +144,26 @@ function renderSearchResults(query) {
|
||||
panel.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchActiveIndex < 0 || searchActiveIndex >= matches.length) searchActiveIndex = 0;
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="search-results-head">
|
||||
<div class="search-results-title">Rekomendasi</div>
|
||||
</div>
|
||||
<div class="search-results-list">
|
||||
${matches.map((item, index) => `
|
||||
<button type="button" class="search-result-item ${index === searchActiveIndex ? 'active' : ''}" data-index="${index}">
|
||||
<div class="search-result-icon">${item.icon}</div>
|
||||
<div class="search-result-info">
|
||||
<div class="search-result-name">${esc(item.label)}</div>
|
||||
<div class="search-result-sub">${esc(item.subtitle)}</div>
|
||||
</div>
|
||||
<div class="search-result-type">${esc(item.typeLabel)}</div>
|
||||
</button>`).join('')}
|
||||
</div>`;
|
||||
<div class="search-results-head"><div class="search-results-title">Rekomendasi</div></div>
|
||||
<div class="search-results-list">
|
||||
${matches.map((item, index) => `
|
||||
<button type="button" class="search-result-item ${index === searchActiveIndex ? 'active' : ''}" data-index="${index}">
|
||||
<div class="search-result-icon">${item.icon}</div>
|
||||
<div class="search-result-info">
|
||||
<div class="search-result-name">${esc(item.label)}</div>
|
||||
<div class="search-result-sub">${esc(item.subtitle)}</div>
|
||||
</div>
|
||||
<div class="search-result-type">${esc(item.typeLabel)}</div>
|
||||
</button>`).join('')}
|
||||
</div>`;
|
||||
panel.classList.add('show');
|
||||
}
|
||||
|
||||
function closeSearchResults() {
|
||||
const panel = document.getElementById('search-results');
|
||||
if (!panel) return;
|
||||
panel.classList.remove('show');
|
||||
if (panel) panel.classList.remove('show');
|
||||
}
|
||||
|
||||
function selectSearchResult(item) {
|
||||
@@ -248,34 +178,15 @@ function initSearchBar() {
|
||||
const panel = document.getElementById('search-results');
|
||||
const wrapper = document.getElementById('header-search');
|
||||
if (!input || !panel || !wrapper) return;
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
searchActiveIndex = 0;
|
||||
renderSearchResults(input.value);
|
||||
});
|
||||
input.addEventListener('input', () => { searchActiveIndex = 0; renderSearchResults(input.value); });
|
||||
input.addEventListener('focus', () => renderSearchResults(input.value));
|
||||
input.addEventListener('keydown', e => {
|
||||
const matches = getSearchMatches(input.value);
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const active = matches[searchActiveIndex] || matches[0];
|
||||
if (active) selectSearchResult(active);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter') { e.preventDefault(); const active = matches[searchActiveIndex] || matches[0]; if (active) selectSearchResult(active); return; }
|
||||
if (!matches.length) return;
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
searchActiveIndex = Math.min(searchActiveIndex + 1, matches.length - 1);
|
||||
renderSearchResults(input.value);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
searchActiveIndex = Math.max(searchActiveIndex - 1, 0);
|
||||
renderSearchResults(input.value);
|
||||
}
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); searchActiveIndex = Math.min(searchActiveIndex + 1, matches.length - 1); renderSearchResults(input.value); return; }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); searchActiveIndex = Math.max(searchActiveIndex - 1, 0); renderSearchResults(input.value); }
|
||||
});
|
||||
|
||||
panel.addEventListener('mousedown', e => {
|
||||
const item = e.target.closest('.search-result-item');
|
||||
if (!item) return;
|
||||
@@ -284,142 +195,108 @@ function initSearchBar() {
|
||||
const selected = matches[Number(item.dataset.index)];
|
||||
if (selected) selectSearchResult(selected);
|
||||
});
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (!wrapper.contains(e.target)) closeSearchResults();
|
||||
});
|
||||
document.addEventListener('click', e => { if (!wrapper.contains(e.target)) closeSearchResults(); });
|
||||
}
|
||||
|
||||
function onceMapClick(cb) {
|
||||
if (_mapClickCb) map.off('click', _mapClickCb);
|
||||
_mapClickCb = function (e) {
|
||||
_mapClickCb = null;
|
||||
cb(e.latlng.lat, e.latlng.lng);
|
||||
// ─── Map-specific functions (only called when map page is active) ─────────────
|
||||
|
||||
function initMap() {
|
||||
const CENTER = [-0.0263, 109.3425];
|
||||
map = window._leafletMap = L.map('map', { zoomControl: false }).setView(CENTER, 13);
|
||||
L.control.zoom({ position: 'topleft' }).addTo(map);
|
||||
|
||||
const baseLayers = {
|
||||
'OSM Standard': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 }),
|
||||
'CartoDB Light': L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© CartoDB', maxZoom: 19 }),
|
||||
'Satellite (Esri)': L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: '© Esri', maxZoom: 19 })
|
||||
};
|
||||
baseLayers['OSM Standard'].addTo(map);
|
||||
L.control.layers(baseLayers, {}, { position: 'bottomright', collapsed: true }).addTo(map);
|
||||
|
||||
layers = {
|
||||
rumahIbadah: L.layerGroup().addTo(map),
|
||||
ibadahRadius: L.layerGroup().addTo(map),
|
||||
pendudukMiskin: L.layerGroup().addTo(map)
|
||||
};
|
||||
|
||||
layerVisible = { rumahIbadah: true, ibadahRadius: true, pendudukMiskin: true };
|
||||
|
||||
let currentTab = 'rumah_ibadah';
|
||||
window.currentTab = currentTab;
|
||||
window._mapClickCb = null;
|
||||
|
||||
map.on('mousemove', e => {
|
||||
const el = document.getElementById('coord-display');
|
||||
if (el) el.textContent = `Lat: ${e.latlng.lat.toFixed(6)} | Lng: ${e.latlng.lng.toFixed(6)}`;
|
||||
});
|
||||
map.on('zoomend', () => {
|
||||
const el = document.getElementById('status-zoom');
|
||||
if (el) el.textContent = `Zoom: ${map.getZoom()}`;
|
||||
});
|
||||
|
||||
initSearchBar();
|
||||
return map;
|
||||
}
|
||||
|
||||
let currentTab = 'rumah_ibadah';
|
||||
let _mapClickCb = null;
|
||||
|
||||
function onceMapClick(cb) {
|
||||
if (!map) return;
|
||||
if (_mapClickCb) map.off('click', _mapClickCb);
|
||||
_mapClickCb = function (e) { _mapClickCb = null; cb(e.latlng.lat, e.latlng.lng); };
|
||||
map.once('click', _mapClickCb);
|
||||
}
|
||||
|
||||
function cancelMapClick() {
|
||||
if (_mapClickCb) {
|
||||
map.off('click', _mapClickCb);
|
||||
_mapClickCb = null;
|
||||
}
|
||||
if (map && _mapClickCb) { map.off('click', _mapClickCb); _mapClickCb = null; }
|
||||
hideInstruction();
|
||||
document.querySelectorAll('.btn-add.active').forEach(b => b.classList.remove('active'));
|
||||
}
|
||||
|
||||
async function getAddressFromCoords(lat, lng) {
|
||||
try {
|
||||
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`);
|
||||
if (!res.ok) return '';
|
||||
const data = await res.json();
|
||||
return data.display_name || '';
|
||||
} catch (err) { return ''; }
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
currentTab = tab;
|
||||
window.currentTab = tab;
|
||||
cancelMapClick();
|
||||
document.querySelectorAll('.nav-tab').forEach((el, i) => {
|
||||
el.classList.toggle('active', ['kemiskinan', 'spbu', 'jalan', 'parsil'][i] === tab);
|
||||
document.querySelectorAll('.nav-tab').forEach(el => {
|
||||
const onClickAttr = el.getAttribute('onclick') || '';
|
||||
el.classList.toggle('active', onClickAttr.includes(`'${tab}'`));
|
||||
});
|
||||
try { map.removeControl(drawControl); } catch (e) { }
|
||||
const titles = { kemiskinan: 'Ibadah & Kemiskinan', spbu: 'SPBU', jalan: 'Data Jalan', parsil: 'Parsil Tanah' };
|
||||
document.getElementById('panel-title').textContent = titles[tab];
|
||||
const titleEl = document.getElementById('panel-title');
|
||||
if (titleEl) titleEl.textContent = tab === 'rumah_ibadah' ? 'Daftar Rumah Ibadah' : 'Daftar Penduduk Miskin';
|
||||
renderPanel(tab);
|
||||
if (tab === 'jalan' || tab === 'parsil') map.addControl(drawControl);
|
||||
}
|
||||
|
||||
async function renderPanel(tab) {
|
||||
const pb = document.getElementById('panel-body');
|
||||
if (!pb) return;
|
||||
pb.innerHTML = '<div class="loading"><span class="spinner"></span>Memuat data…</div>';
|
||||
if (tab === 'kemiskinan') await renderKemiskinanPanel(pb);
|
||||
if (tab === 'spbu') await renderSpbuPanel(pb);
|
||||
if (tab === 'jalan') await renderJalanPanel(pb);
|
||||
if (tab === 'parsil') await renderParsilPanel(pb);
|
||||
}
|
||||
|
||||
async function preloadFeatureLayers() {
|
||||
const sandbox = document.createElement('div');
|
||||
try {
|
||||
await Promise.all([
|
||||
renderSpbuPanel(sandbox),
|
||||
renderJalanPanel(sandbox),
|
||||
renderParsilPanel(sandbox)
|
||||
]);
|
||||
} catch (err) {
|
||||
console.warn('Gagal memuat sebagian layer awal:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function findGeomLayer(type, id) {
|
||||
const keys = type === 'jalan'
|
||||
? ['jalanNasional', 'jalanProvinsi', 'jalanKabupaten']
|
||||
: ['parsilSHM', 'parsilHGB', 'parsilHGU', 'parsilHP'];
|
||||
for (const key of keys) {
|
||||
let found = null;
|
||||
layers[key].eachLayer(layer => {
|
||||
if (layer._dataId === id) found = layer;
|
||||
});
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getGeometryEditHandler() {
|
||||
return drawControl?._toolbars?.edit?._modes?.edit?.handler || null;
|
||||
}
|
||||
|
||||
function getGeometryGroupKey(type, data) {
|
||||
return type === 'jalan' ? `jalan${data.status_jalan}` : `parsil${data.jenis_hak}`;
|
||||
}
|
||||
|
||||
function cloneEditableLayer(type, sourceLayer) {
|
||||
if (type === 'jalan') {
|
||||
const coords = sourceLayer.getLatLngs().map(ll => [ll.lat, ll.lng]);
|
||||
return L.polyline(coords, { ...sourceLayer.options });
|
||||
}
|
||||
const ring = sourceLayer.getLatLngs()[0].map(ll => [ll.lat, ll.lng]);
|
||||
return L.polygon(ring, { ...sourceLayer.options });
|
||||
}
|
||||
|
||||
function buildGeometryLayer(type, data) {
|
||||
if (type === 'jalan') {
|
||||
const cfg = jalanCfg[data.status_jalan] || jalanCfg.Kabupaten;
|
||||
const coords = (data.geojson?.coordinates || []).map(c => [c[1], c[0]]);
|
||||
return L.polyline(coords, { color: cfg.color, weight: cfg.weight, opacity: 0.9 });
|
||||
}
|
||||
const cfg = parsilCfg[data.jenis_hak] || parsilCfg.SHM;
|
||||
const coords = ((data.geojson?.coordinates || [])[0] || []).map(c => [c[1], c[0]]);
|
||||
return L.polygon(coords, { color: cfg.color, fillColor: cfg.color, fillOpacity: 0.18, weight: 2 });
|
||||
}
|
||||
|
||||
function cancelGeometryEdit() {
|
||||
const handler = getGeometryEditHandler();
|
||||
if (handler) handler.disable();
|
||||
drawnItems.clearLayers();
|
||||
geometryEditState = null;
|
||||
switchTab(currentTab);
|
||||
}
|
||||
|
||||
function focusGeom(id, type) {
|
||||
const keys = type === 'jalan'
|
||||
? ['jalanNasional', 'jalanProvinsi', 'jalanKabupaten']
|
||||
: ['parsilSHM', 'parsilHGB', 'parsilHGU', 'parsilHP'];
|
||||
for (const k of keys) {
|
||||
layers[k].eachLayer(l => {
|
||||
if (l._dataId === id) {
|
||||
map.flyToBounds(l.getBounds(), { padding: [50, 50], duration: 1 });
|
||||
l.openPopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (tab === 'rumah_ibadah') await renderRumahIbadahPanel(pb);
|
||||
else if (tab === 'penduduk_miskin') await renderPendudukMiskinPanel(pb);
|
||||
}
|
||||
|
||||
function focusMarkerById(layerKey, id, zoom = searchPointZoom) {
|
||||
if (!layers || !map) return;
|
||||
const layer = layers[layerKey];
|
||||
if (!layer) return;
|
||||
let target = null;
|
||||
layer.eachLayer(marker => {
|
||||
if (marker._dataId === id) target = marker;
|
||||
});
|
||||
layer.eachLayer(marker => { if (marker._dataId === id) target = marker; });
|
||||
if (!target) return;
|
||||
map.flyTo(target.getLatLng(), zoom, { duration: 1 });
|
||||
map.once('moveend', () => target.openPopup());
|
||||
}
|
||||
|
||||
function toggleLayer(key, el) {
|
||||
if (!layers || !map) return;
|
||||
layerVisible[key] = !layerVisible[key];
|
||||
el.classList.toggle('checked', layerVisible[key]);
|
||||
el.querySelector('.layer-check').textContent = layerVisible[key] ? '✓' : '';
|
||||
@@ -427,36 +304,11 @@ function toggleLayer(key, el) {
|
||||
}
|
||||
|
||||
function updateStatusCount() {
|
||||
if (!layers || !map) return;
|
||||
let total = 0;
|
||||
Object.values(layers).forEach(lg => { if (map.hasLayer(lg)) lg.eachLayer(() => total++); });
|
||||
document.getElementById('status-count').textContent = `Total fitur: ${total}`;
|
||||
document.getElementById('status-zoom').textContent = `Zoom: ${map.getZoom()}`;
|
||||
const countEl = document.getElementById('status-count');
|
||||
if (countEl) countEl.textContent = `Total fitur: ${total}`;
|
||||
const zoomEl = document.getElementById('status-zoom');
|
||||
if (zoomEl) zoomEl.textContent = `Zoom: ${map.getZoom()}`;
|
||||
}
|
||||
|
||||
map.on('mousemove', e => {
|
||||
document.getElementById('coord-display').textContent = `Lat: ${e.latlng.lat.toFixed(6)} | Lng: ${e.latlng.lng.toFixed(6)}`;
|
||||
});
|
||||
map.on('zoomend', () => {
|
||||
document.getElementById('status-zoom').textContent = `Zoom: ${map.getZoom()}`;
|
||||
});
|
||||
map.on(L.Draw.Event.CREATED, function (e) {
|
||||
const { layer, layerType: type } = e;
|
||||
if (type === 'polyline') {
|
||||
const lls = layer.getLatLngs();
|
||||
pendingLength = turf.length(turf.lineString(lls.map(c => [c.lng, c.lat])), { units: 'meters' });
|
||||
pendingGeoJSON = { type: 'LineString', coordinates: lls.map(c => [c.lng, c.lat]) };
|
||||
drawnItems.addLayer(layer);
|
||||
openJalanForm();
|
||||
}
|
||||
if (type === 'polygon') {
|
||||
const lls = layer.getLatLngs()[0];
|
||||
const ring = [...lls.map(c => [c.lng, c.lat]), [lls[0].lng, lls[0].lat]];
|
||||
pendingArea = turf.area(turf.polygon([ring]));
|
||||
pendingGeoJSON = { type: 'Polygon', coordinates: [ring] };
|
||||
drawnItems.addLayer(layer);
|
||||
openParsilForm();
|
||||
}
|
||||
});
|
||||
|
||||
initSearchBar();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user