First commit / commit pertama

This commit is contained in:
Mr.Haruna
2026-06-13 11:24:58 +07:00
commit 522c4f7200
166 changed files with 13326 additions and 0 deletions
+127
View File
@@ -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);
});
});
}
};
})();