First commit / commit pertama
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
/* =========================================================
|
||||
public-map.js — Peta Publik SKPL
|
||||
Choropleth (KF-06) + Heatmap (KF-08) + Layer switcher (KF-09)
|
||||
+ Legenda + Pencarian wilayah (KF-11).
|
||||
`map` & `APP_BASE` disediakan global oleh peta_publik.php.
|
||||
========================================================= */
|
||||
(function () {
|
||||
const API = APP_BASE + '/api';
|
||||
|
||||
let choroplethLayer = null;
|
||||
let heatLayer = null;
|
||||
let wilayahFeatures = [];
|
||||
|
||||
// ---- Skala warna persentase kemiskinan ----
|
||||
function colorFor(persen) {
|
||||
return persen >= 10 ? '#EF4444' // merah — tinggi
|
||||
: persen >= 5 ? '#FB923C' // oranye — sedang-tinggi
|
||||
: persen >= 2 ? '#FBBF24' // kuning — sedang
|
||||
: persen > 0 ? '#A3E635' // hijau muda — rendah
|
||||
: '#2ECC71'; // hijau — tanpa kasus
|
||||
}
|
||||
|
||||
function styleWilayah(feature) {
|
||||
const p = feature.properties.persentase_miskin || 0;
|
||||
return {
|
||||
fillColor: colorFor(p),
|
||||
fillOpacity: 0.55,
|
||||
color: '#374151',
|
||||
weight: 1.5,
|
||||
};
|
||||
}
|
||||
|
||||
function popupWilayah(props) {
|
||||
return `<div style="font-weight:700;margin-bottom:6px;">${props.nama}</div>
|
||||
<div style="font-size:0.85rem;line-height:1.6;">
|
||||
Penduduk: <strong>${props.jumlah_penduduk}</strong><br>
|
||||
Warga miskin: <strong>${props.jumlah_miskin}</strong><br>
|
||||
Persentase: <strong>${props.persentase_miskin}%</strong>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ---- Choropleth (KF-06) ----
|
||||
async function loadChoropleth() {
|
||||
const r = await fetch(`${API}/wilayah.php`);
|
||||
const j = await r.json();
|
||||
const data = j.status === 'success' ? j.data : { type: 'FeatureCollection', features: [] };
|
||||
wilayahFeatures = data.features || [];
|
||||
|
||||
choroplethLayer = L.geoJSON(data, {
|
||||
style: styleWilayah,
|
||||
onEachFeature: (f, layer) => {
|
||||
layer.bindPopup(popupWilayah(f.properties));
|
||||
layer.bindTooltip(f.properties.nama, { sticky: true });
|
||||
},
|
||||
}).addTo(map);
|
||||
|
||||
if (wilayahFeatures.length) {
|
||||
map.fitBounds(choroplethLayer.getBounds(), { padding: [40, 40] });
|
||||
}
|
||||
return choroplethLayer;
|
||||
}
|
||||
|
||||
// ---- Heatmap (KF-08) ----
|
||||
async function loadHeatmap() {
|
||||
const r = await fetch(`${API}/heatmap.php`);
|
||||
const j = await r.json();
|
||||
const points = j.status === 'success' ? j.data : [];
|
||||
heatLayer = L.heatLayer(points, {
|
||||
radius: 30, blur: 22, maxZoom: 17,
|
||||
gradient: { 0.2: '#3B82F6', 0.5: '#FBBF24', 0.9: '#EF4444' },
|
||||
});
|
||||
return heatLayer;
|
||||
}
|
||||
|
||||
// ---- Legenda (KF-06) ----
|
||||
function addLegend() {
|
||||
const legend = L.control({ position: 'bottomleft' });
|
||||
legend.onAdd = function () {
|
||||
const div = L.DomUtil.create('div', 'legend');
|
||||
const grades = [
|
||||
{ c: '#2ECC71', t: 'Tidak ada kasus' },
|
||||
{ c: '#A3E635', t: '< 2% miskin' },
|
||||
{ c: '#FBBF24', t: '2 – 5%' },
|
||||
{ c: '#FB923C', t: '5 – 10%' },
|
||||
{ c: '#EF4444', t: '> 10% (prioritas)' },
|
||||
];
|
||||
div.innerHTML = '<h4>Tingkat Kemiskinan</h4>' +
|
||||
grades.map(g => `<div><i style="background:${g.c}"></i>${g.t}</div>`).join('');
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
}
|
||||
|
||||
// ---- Pencarian wilayah (KF-11) ----
|
||||
function addSearch() {
|
||||
const ctrl = L.control({ position: 'topright' });
|
||||
ctrl.onAdd = function () {
|
||||
const div = L.DomUtil.create('div', 'map-search');
|
||||
div.innerHTML = `<input type="text" id="searchWilayah" placeholder="🔍 Cari kelurahan..." autocomplete="off">
|
||||
<div id="searchResults" class="search-results"></div>`;
|
||||
L.DomEvent.disableClickPropagation(div);
|
||||
return div;
|
||||
};
|
||||
ctrl.addTo(map);
|
||||
|
||||
const input = document.getElementById('searchWilayah');
|
||||
const results = document.getElementById('searchResults');
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
const q = this.value.toLowerCase().trim();
|
||||
results.innerHTML = '';
|
||||
if (!q) return;
|
||||
wilayahFeatures
|
||||
.filter(f => f.properties.nama.toLowerCase().includes(q))
|
||||
.slice(0, 6)
|
||||
.forEach(f => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'search-item';
|
||||
item.textContent = f.properties.nama;
|
||||
item.onclick = () => {
|
||||
const layer = L.geoJSON(f);
|
||||
map.fitBounds(layer.getBounds(), { padding: [60, 60] });
|
||||
results.innerHTML = '';
|
||||
input.value = f.properties.nama;
|
||||
};
|
||||
results.appendChild(item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Inisialisasi ----
|
||||
Promise.all([loadChoropleth(), loadHeatmap()]).then(([choro, heat]) => {
|
||||
// Layer switcher (KF-09)
|
||||
L.control.layers(null, {
|
||||
'Batas Kelurahan (Choropleth)': choro,
|
||||
'Heatmap Kepadatan': heat,
|
||||
}, { collapsed: false, position: 'topright' }).addTo(map);
|
||||
addLegend();
|
||||
addSearch();
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,127 @@
|
||||
/* =========================================================
|
||||
skpl-layers.js — Layer SKPL reusable untuk peta login.
|
||||
Choropleth kelurahan (KF-06) + Heatmap (KF-08) + legenda +
|
||||
layer switcher overlay (KF-09) + pencarian wilayah (KF-11).
|
||||
|
||||
Pakai: panggil attachSkplLayers(map) setelah map dibuat.
|
||||
Butuh leaflet.heat sudah dimuat di halaman.
|
||||
========================================================= */
|
||||
(function () {
|
||||
function injectStyle() {
|
||||
if (document.getElementById('skpl-layer-style')) return;
|
||||
const css = `
|
||||
.skpl-legend{background:#fff;padding:10px 12px;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.15);font-size:.76rem;line-height:1.6}
|
||||
.skpl-legend h4{font-size:.78rem;margin:0 0 6px}
|
||||
.skpl-legend i{display:inline-block;width:13px;height:13px;border-radius:3px;margin-right:6px;vertical-align:middle}
|
||||
.skpl-search{background:#fff;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.15);padding:6px;width:220px}
|
||||
.skpl-search input{width:100%;border:1px solid #E5E7EB;border-radius:7px;padding:7px 9px;font-size:.83rem;outline:none}
|
||||
.skpl-search input:focus{border-color:#2563EB}
|
||||
.skpl-results .item{padding:7px 9px;font-size:.82rem;cursor:pointer;border-radius:6px}
|
||||
.skpl-results .item:hover{background:#EFF6FF;color:#1D4ED8}`;
|
||||
const s = document.createElement('style');
|
||||
s.id = 'skpl-layer-style';
|
||||
s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function colorFor(p) {
|
||||
return p >= 10 ? '#EF4444' : p >= 5 ? '#FB923C' : p >= 2 ? '#FBBF24' : p > 0 ? '#A3E635' : '#2ECC71';
|
||||
}
|
||||
|
||||
window.attachSkplLayers = async function (map) {
|
||||
if (!map || typeof L === 'undefined') return;
|
||||
injectStyle();
|
||||
const API = (typeof APP_BASE !== 'undefined' ? APP_BASE : '') + '/api';
|
||||
let features = [];
|
||||
|
||||
// Choropleth
|
||||
let choro = null;
|
||||
try {
|
||||
const j = await (await fetch(`${API}/wilayah.php`)).json();
|
||||
const data = j.status === 'success' ? j.data : { type: 'FeatureCollection', features: [] };
|
||||
features = data.features || [];
|
||||
choro = L.geoJSON(data, {
|
||||
style: f => ({
|
||||
fillColor: colorFor(f.properties.persentase_miskin || 0),
|
||||
fillOpacity: 0.5, color: '#374151', weight: 1.5,
|
||||
}),
|
||||
onEachFeature: (f, layer) => {
|
||||
const p = f.properties;
|
||||
layer.bindPopup(`<div style="font-weight:700;margin-bottom:6px;">${p.nama}</div>
|
||||
<div style="font-size:.85rem;line-height:1.6;">
|
||||
Penduduk: <strong>${p.jumlah_penduduk}</strong><br>
|
||||
Warga miskin: <strong>${p.jumlah_miskin}</strong><br>
|
||||
Persentase: <strong>${p.persentase_miskin}%</strong></div>`);
|
||||
layer.bindTooltip(p.nama, { sticky: true });
|
||||
},
|
||||
});
|
||||
} catch (e) { /* abaikan */ }
|
||||
|
||||
// Heatmap
|
||||
let heat = null;
|
||||
try {
|
||||
const j = await (await fetch(`${API}/heatmap.php`)).json();
|
||||
const pts = j.status === 'success' ? j.data : [];
|
||||
if (L.heatLayer) {
|
||||
heat = L.heatLayer(pts, {
|
||||
radius: 30, blur: 22, maxZoom: 17,
|
||||
gradient: { 0.2: '#3B82F6', 0.5: '#FBBF24', 0.9: '#EF4444' },
|
||||
});
|
||||
}
|
||||
} catch (e) { /* abaikan */ }
|
||||
|
||||
// Layer switcher overlay
|
||||
const overlays = {};
|
||||
if (choro) overlays['🗺️ Choropleth Kelurahan'] = choro;
|
||||
if (heat) overlays['🔥 Heatmap Kepadatan'] = heat;
|
||||
if (Object.keys(overlays).length) {
|
||||
L.control.layers(null, overlays, { collapsed: true, position: 'topright' }).addTo(map);
|
||||
}
|
||||
|
||||
// Legenda
|
||||
const legend = L.control({ position: 'bottomleft' });
|
||||
legend.onAdd = function () {
|
||||
const div = L.DomUtil.create('div', 'skpl-legend');
|
||||
const g = [
|
||||
['#2ECC71', 'Tidak ada kasus'], ['#A3E635', '< 2%'],
|
||||
['#FBBF24', '2 – 5%'], ['#FB923C', '5 – 10%'], ['#EF4444', '> 10% (prioritas)'],
|
||||
];
|
||||
div.innerHTML = '<h4>Tingkat Kemiskinan</h4>' +
|
||||
g.map(x => `<div><i style="background:${x[0]}"></i>${x[1]}</div>`).join('');
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
|
||||
// Pencarian wilayah (KF-11)
|
||||
const search = L.control({ position: 'topright' });
|
||||
search.onAdd = function () {
|
||||
const div = L.DomUtil.create('div', 'skpl-search');
|
||||
div.innerHTML = `<input type="text" placeholder="🔍 Cari kelurahan...">
|
||||
<div class="skpl-results"></div>`;
|
||||
L.DomEvent.disableClickPropagation(div);
|
||||
return div;
|
||||
};
|
||||
search.addTo(map);
|
||||
const root = document.querySelector('.skpl-search');
|
||||
if (root) {
|
||||
const input = root.querySelector('input');
|
||||
const res = root.querySelector('.skpl-results');
|
||||
input.addEventListener('input', function () {
|
||||
const q = this.value.toLowerCase().trim();
|
||||
res.innerHTML = '';
|
||||
if (!q) return;
|
||||
features.filter(f => f.properties.nama.toLowerCase().includes(q)).slice(0, 6).forEach(f => {
|
||||
const it = document.createElement('div');
|
||||
it.className = 'item';
|
||||
it.textContent = f.properties.nama;
|
||||
it.onclick = () => {
|
||||
map.fitBounds(L.geoJSON(f).getBounds(), { padding: [60, 60] });
|
||||
res.innerHTML = '';
|
||||
input.value = f.properties.nama;
|
||||
};
|
||||
res.appendChild(it);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,75 @@
|
||||
/* =========================================================
|
||||
skpl-map.js — Peta SKPL (operator & pimpinan)
|
||||
Warga miskin (POI) + fasilitas publik + choropleth + heatmap
|
||||
(choropleth/heatmap/legenda/pencarian via attachSkplLayers).
|
||||
Role ditentukan global SKPL_ROLE ('operator' | 'pimpinan').
|
||||
========================================================= */
|
||||
(function () {
|
||||
const API = APP_BASE + '/api';
|
||||
const ROLE = (typeof SKPL_ROLE !== 'undefined') ? SKPL_ROLE : 'pimpinan';
|
||||
let map, wargaLayer, fasilitasLayer;
|
||||
|
||||
const FAS_ICON = { sekolah: '🏫', puskesmas: '🏥', pasar: '🏪', kantor_kelurahan: '🏛️', rumah_ibadah: '🕌', lainnya: '📍' };
|
||||
|
||||
function divIcon(emoji, bg, size = 30) {
|
||||
return L.divIcon({
|
||||
className: 'div-icon-wrap',
|
||||
html: `<div style="background:${bg};width:${size}px;height:${size}px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);display:flex;align-items:center;justify-content:center;box-shadow:0 2px 5px rgba(0,0,0,.3);border:2px solid #fff;"><span style="transform:rotate(45deg);font-size:${size*0.45}px;">${emoji}</span></div>`,
|
||||
iconSize: [size, size], iconAnchor: [size / 2, size], popupAnchor: [0, -size],
|
||||
});
|
||||
}
|
||||
const wargaIcon = divIcon('👤', '#EF4444');
|
||||
|
||||
function wargaPopup(p) {
|
||||
const vb = { terverifikasi: '#10B981', perlu_tinjauan: '#F59E0B', belum: '#9CA3AF' }[p.status_verifikasi] || '#9CA3AF';
|
||||
let h = `<div style="min-width:200px;"><div style="font-weight:700;margin-bottom:4px;">${p.nama_kk}</div>`;
|
||||
if (p.nik_kk) h += `<div style="font-size:.8rem;">NIK: ${p.nik_kk}</div>`;
|
||||
if (p.alamat) h += `<div style="font-size:.8rem;">${p.alamat}</div>`;
|
||||
h += `<div style="font-size:.8rem;">Penghasilan: Rp ${Number(p.penghasilan||0).toLocaleString('id-ID')}</div>`;
|
||||
h += `<div style="font-size:.8rem;">Jiwa: ${p.jumlah_jiwa||'-'} · Tanggungan: ${p.jumlah_tanggungan||0}</div>`;
|
||||
h += `<div style="font-size:.8rem;">Air: ${p.sumber_air||'-'} · Dinding: ${p.material_dinding||'-'}</div>`;
|
||||
h += `<div style="font-size:.8rem;">Bantuan: <b>${p.bantuan_aktif||'Belum ada'}</b></div>`;
|
||||
h += `<div style="font-size:.8rem;">Status: <b style="color:${vb}">${p.status_verifikasi||'belum'}</b>${p.jumlah_foto?' · 📷 '+p.jumlah_foto:''}</div>`;
|
||||
if (ROLE === 'operator') {
|
||||
h += `<div style="display:flex;gap:6px;margin-top:8px;">
|
||||
<button onclick="window.location.href='${APP_BASE}/admin/data_warga.php'" style="flex:1;padding:6px;border:1px solid #2563EB;background:#2563EB;color:#fff;border-radius:6px;cursor:pointer;font-size:.78rem;">Kelola</button>
|
||||
<button onclick="SKPLMAP_del(${p.id})" style="padding:6px 10px;border:1px solid #EF4444;background:#fff;color:#EF4444;border-radius:6px;cursor:pointer;font-size:.78rem;">Hapus</button>
|
||||
</div>`;
|
||||
}
|
||||
return h + `</div>`;
|
||||
}
|
||||
|
||||
async function loadWarga() {
|
||||
const j = await (await fetch(`${API}/warga_miskin.php`)).json();
|
||||
const data = j.status === 'success' ? j.data : { features: [] };
|
||||
wargaLayer = L.geoJSON(data, {
|
||||
pointToLayer: (f, ll) => L.marker(ll, { icon: wargaIcon }),
|
||||
onEachFeature: (f, l) => l.bindPopup(wargaPopup(f.properties)),
|
||||
}).addTo(map);
|
||||
}
|
||||
async function loadFasilitas() {
|
||||
const j = await (await fetch(`${API}/fasilitas.php`)).json();
|
||||
const data = j.status === 'success' ? j.data : { features: [] };
|
||||
fasilitasLayer = L.geoJSON(data, {
|
||||
pointToLayer: (f, ll) => L.marker(ll, { icon: divIcon(FAS_ICON[f.properties.jenis] || '📍', '#2563EB', 26) }),
|
||||
onEachFeature: (f, l) => l.bindPopup(`<b>${f.properties.nama}</b><br><span style="font-size:.8rem;color:#6B7280;">${f.properties.jenis.replace('_',' ')}</span>`),
|
||||
});
|
||||
}
|
||||
|
||||
window.SKPLMAP_del = async function (id) {
|
||||
if (!confirm('Hapus data warga ini?')) return;
|
||||
const r = await (await fetch(`${API}/warga_miskin.php?id=${id}`, { method: 'DELETE' })).json();
|
||||
if (r.status === 'success') { map.removeLayer(wargaLayer); loadWarga(); }
|
||||
else alert(r.message || 'Gagal');
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
map = L.map('skpl-map').setView([-0.0500, 109.3450], 13);
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { subdomains: 'abcd', maxZoom: 19, attribution: '© OpenStreetMap, © CARTO' }).addTo(map);
|
||||
await loadWarga();
|
||||
await loadFasilitas();
|
||||
if (window.attachSkplLayers) await window.attachSkplLayers(map);
|
||||
// tambahkan toggle warga & fasilitas
|
||||
L.control.layers(null, { '👤 Warga Miskin': wargaLayer, '🏢 Fasilitas Publik': fasilitasLayer }, { collapsed: true, position: 'topright' }).addTo(map);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user