Files
webgis-poverty-mapping/radius.php
T
2026-06-04 21:28:53 +07:00

539 lines
23 KiB
PHP

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analisis Radius Bantuan WebGIS</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Plus Jakarta Sans', sans-serif; background: #0b132b; color: #fff; overflow: hidden; }
#map { height: 100vh; width: 100%; z-index: 1; }
/* SIDEBAR */
.sidebar {
position: absolute; top: 20px; left: 20px; z-index: 1000;
width: 270px; background: rgba(11,19,43,0.88);
backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
border: 1px solid rgba(255,255,255,0.1); border-radius: 20px;
padding: 22px; box-shadow: 0 8px 40px rgba(0,0,0,0.6);
}
.sidebar-brand {
display: flex; align-items: center; gap: 10px;
font-size: 0.7rem; font-weight: 800; color: #5bc0be;
text-transform: uppercase; letter-spacing: 2px;
cursor: pointer; user-select: none;
justify-content: space-between;
}
.sidebar-brand span { display: flex; align-items: center; gap: 10px; }
.sidebar-brand .icon-wrap {
width: 34px; height: 34px; border-radius: 10px;
background: linear-gradient(135deg, #5bc0be, #3a506b);
display: flex; align-items: center; justify-content: center;
font-size: 1rem;
}
#chevron { transition: transform .3s; }
.sidebar.open #chevron { transform: rotate(180deg); }
.sidebar-body { overflow: hidden; max-height: 0; transition: max-height .4s ease; }
.sidebar.open .sidebar-body { max-height: 600px; }
.divider { border: none; border-top: 1px solid rgba(255,255,255,0.08); margin: 16px 0; }
.section-label {
font-size: 0.62rem; font-weight: 800; color: #5bc0be;
text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 10px;
}
.btn-mode {
width: 100%; display: flex; align-items: center; gap: 12px;
padding: 12px 14px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);
background: rgba(255,255,255,0.03); color: #e0e1dd; font-size: 0.85rem;
font-weight: 600; cursor: pointer; transition: all .25s; margin-bottom: 8px;
font-family: inherit;
}
.btn-mode i { font-size: 1.1rem; }
.btn-mode:hover { background: rgba(58,80,107,0.4); transform: translateX(4px); border-color: rgba(91,192,190,0.3); }
.btn-mode.active {
background: linear-gradient(135deg,#1c2541,#3a506b);
border-color: #5bc0be; color: #fff;
box-shadow: 0 4px 15px rgba(91,192,190,0.35);
}
.btn-mode.active i { color: #6fffe9; }
/* LEGEND */
.legend { margin-top: 4px; }
.legend-item { display: flex; align-items: center; gap: 9px; font-size: 0.78rem; color: #b0bec5; margin-bottom: 7px; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.legend-dot.ibadah { background: #6fffe9; box-shadow: 0 0 6px #6fffe9; }
.legend-dot.hijau { background: #2ecc71; box-shadow: 0 0 6px #2ecc71; }
.legend-dot.merah { background: #e74c3c; box-shadow: 0 0 6px #e74c3c; }
.legend-circle { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #5bc0be; flex-shrink: 0; }
/* STATS BAR */
.stats-bar {
position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%);
z-index: 1000; display: flex; gap: 12px;
}
.stat-card {
background: rgba(11,19,43,0.88); backdrop-filter: blur(14px);
border: 1px solid rgba(255,255,255,0.1); border-radius: 14px;
padding: 10px 20px; text-align: center; min-width: 120px;
}
.stat-card .val { font-size: 1.6rem; font-weight: 800; line-height: 1; }
.stat-card .lbl { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 1px; color: #78909c; margin-top: 3px; }
.stat-ibadah .val { color: #6fffe9; }
.stat-hijau .val { color: #2ecc71; }
.stat-merah .val { color: #e74c3c; }
/* TOAST */
#toast {
position: absolute; top: 24px; right: 24px; z-index: 9999;
background: rgba(11,19,43,0.95); border: 1px solid rgba(255,255,255,0.12);
border-radius: 14px; padding: 14px 20px; font-size: 0.82rem;
max-width: 280px; display: none; backdrop-filter: blur(12px);
box-shadow: 0 8px 30px rgba(0,0,0,0.5); animation: slideIn .3s ease;
}
@keyframes slideIn { from { opacity:0; transform:translateX(30px); } to { opacity:1; transform:translateX(0); } }
/* POPUP */
.leaflet-popup-content-wrapper {
background: rgba(14,23,44,0.96) !important; backdrop-filter: blur(16px);
border: 1px solid rgba(255,255,255,0.1); border-radius: 18px !important;
padding: 0 !important; overflow: hidden;
box-shadow: 0 15px 40px rgba(0,0,0,0.6) !important; color: #fff;
}
.leaflet-popup-tip { background: rgba(14,23,44,0.96) !important; }
.leaflet-popup-content { margin: 0 !important; width: 300px !important; }
.leaflet-popup-close-button { color: #fff !important; top: 12px !important; right: 12px !important; font-size: 18px !important; z-index: 10; }
.leaflet-popup-close-button:hover { color: #5bc0be !important; }
.p-header {
background: linear-gradient(135deg,#0b132b,#1c2541);
border-bottom: 1px solid rgba(255,255,255,0.08);
padding: 16px 40px 16px 18px;
font-size: 0.75rem; font-weight: 800;
text-transform: uppercase; letter-spacing: 1.5px; color: #6fffe9;
}
.p-body { padding: 18px; }
.f-label {
font-size: 0.65rem; font-weight: 700; color: #5bc0be;
text-transform: uppercase; letter-spacing: 1px;
display: block; margin-bottom: 6px;
}
.f-input {
width: 100%; background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.1);
border-radius: 10px; padding: 11px 13px; font-size: 0.84rem; color: #fff;
font-family: inherit; transition: border-color .25s; margin-bottom: 14px;
}
.f-input:focus { outline: none; border-color: #5bc0be; background: rgba(0,0,0,0.4); }
.f-input::placeholder { color: rgba(255,255,255,0.35); }
.btn-submit {
width: 100%; padding: 12px; border-radius: 11px; border: none; cursor: pointer;
font-weight: 700; font-size: 0.88rem; font-family: inherit;
background: linear-gradient(135deg,#5bc0be,#3a506b); color: #fff;
transition: transform .25s, box-shadow .25s;
}
.btn-submit:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(91,192,190,0.4); }
.info-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 0.82rem; }
.info-row .key { color: #78909c; }
.info-row .val { font-weight: 700; }
.badge-warna {
display: inline-block; padding: 4px 12px; border-radius: 20px;
font-size: 0.7rem; font-weight: 700; text-transform: uppercase;
}
.badge-hijau { background: rgba(46,204,113,0.2); color: #2ecc71; border: 1px solid #2ecc71; }
.badge-merah { background: rgba(231,76,60,0.2); color: #e74c3c; border: 1px solid #e74c3c; }
</style>
</head>
<body>
<!-- SIDEBAR -->
<div class="sidebar" id="sidebar">
<div class="sidebar-brand" onclick="toggleSidebar()">
<span><div class="icon-wrap"><i class="bi bi-broadcast-pin"></i></div> Radius Bantuan</span>
<i class="bi bi-chevron-down" id="chevron"></i>
</div>
<div class="sidebar-body">
<hr class="divider">
<p class="section-label">Mode Input</p>
<button class="btn-mode active" id="btn-view" onclick="setMode('view')">
<i class="bi bi-eye-fill"></i> Mode Lihat
</button>
<button class="btn-mode" id="btn-ibadah" onclick="setMode('ibadah')">
<i class="bi bi-building-fill" style="color:#6fffe9"></i> Tambah Rumah Ibadah
</button>
<button class="btn-mode" id="btn-penduduk" onclick="setMode('penduduk')">
<i class="bi bi-person-fill" style="color:#f39c12"></i> Tambah Penduduk
</button>
<hr class="divider">
<p class="section-label">Legenda</p>
<div class="legend">
<div class="legend-item"><div class="legend-dot ibadah"></div> Rumah Ibadah</div>
<div class="legend-item"><div class="legend-circle"></div> Radius Jangkauan</div>
<div class="legend-item"><div class="legend-dot hijau"></div> Penduduk Tercakup</div>
<div class="legend-item"><div class="legend-dot merah"></div> Penduduk Belum Tercakup</div>
</div>
<hr class="divider">
<button class="btn-mode" onclick="loadData()" style="color:#6fffe9;">
<i class="bi bi-arrow-clockwise"></i> Refresh Data
</button>
</div>
</div>
<!-- STATS BAR -->
<div class="stats-bar">
<div class="stat-card stat-ibadah">
<div class="val" id="stat-ibadah">0</div>
<div class="lbl">Rumah Ibadah</div>
</div>
<div class="stat-card stat-hijau">
<div class="val" id="stat-hijau">0</div>
<div class="lbl">Tercakup</div>
</div>
<div class="stat-card stat-merah">
<div class="val" id="stat-merah">0</div>
<div class="lbl">Belum Tercakup</div>
</div>
</div>
<!-- TOAST -->
<div id="toast"></div>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script>
// ===== INIT MAP =====
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.control.zoom({ position: 'bottomright' }).addTo(map);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// ===== STATE =====
let currentMode = 'view';
let layerIbadah = L.layerGroup().addTo(map);
let layerPenduduk = L.layerGroup().addTo(map);
let allIbadah = [];
let allPenduduk = [];
// ===== ICON FACTORY =====
function makeIcon(color) {
const colors = {
cyan: '#6fffe9',
green: '#2ecc71',
red: '#e74c3c',
orange: '#f39c12'
};
const hex = colors[color] || colors.cyan;
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 42" width="32" height="42">
<filter id="sh"><feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="${hex}" flood-opacity="0.6"/></filter>
<path filter="url(#sh)" d="M16 0C9.4 0 4 5.4 4 12c0 9 12 26 12 26S28 21 28 12C28 5.4 22.6 0 16 0z" fill="${hex}"/>
<circle cx="16" cy="12" r="5" fill="#0b132b"/>
</svg>`;
return L.divIcon({
html: svg, className: '', iconSize: [32,42], iconAnchor: [16,42], popupAnchor: [0,-40]
});
}
// ===== SIDEBAR =====
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
}
// Buka sidebar saat load
document.getElementById('sidebar').classList.add('open');
// ===== MODE =====
function setMode(mode) {
currentMode = mode;
document.querySelectorAll('.btn-mode').forEach(b => b.classList.remove('active'));
document.getElementById('btn-' + mode).classList.add('active');
showToast(
mode === 'view' ? '<i class="bi bi-eye-fill"></i> Mode Lihat aktif' :
mode === 'ibadah' ? '<i class="bi bi-building-fill"></i> Klik peta untuk tambah Rumah Ibadah' :
'<i class="bi bi-person-fill"></i> Klik peta untuk tambah Penduduk',
mode === 'view' ? '#5bc0be' : '#f39c12'
);
}
// ===== TOAST =====
let toastTimer;
function showToast(msg, color = '#5bc0be') {
const el = document.getElementById('toast');
el.innerHTML = msg;
el.style.borderLeftColor = color;
el.style.borderLeft = `3px solid ${color}`;
el.style.display = 'block';
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { el.style.display = 'none'; }, 3000);
}
// ===== STATS =====
function updateStats() {
document.getElementById('stat-ibadah').textContent = allIbadah.length;
const hijau = allPenduduk.filter(p => p.status_warna === 'Hijau').length;
const merah = allPenduduk.filter(p => p.status_warna === 'Merah').length;
document.getElementById('stat-hijau').textContent = hijau;
document.getElementById('stat-merah').textContent = merah;
}
// ===== LOAD DATA =====
async function loadData() {
try {
const res = await fetch('ambil.php');
const data = await res.json();
allIbadah = data.ibadah || [];
allPenduduk = data.penduduk || [];
renderIbadah();
renderPenduduk();
await cekRadius();
updateStats();
} catch(e) {
showToast('<i class="bi bi-exclamation-triangle"></i> Gagal memuat data.', '#e74c3c');
console.error(e);
}
}
// ===== RENDER IBADAH =====
function renderIbadah() {
layerIbadah.clearLayers();
allIbadah.forEach(d => {
const marker = L.marker([d.latitude, d.longitude], { icon: makeIcon('cyan') });
const circle = L.circle([d.latitude, d.longitude], {
radius: d.radius,
color: '#5bc0be', fillColor: '#5bc0be',
fillOpacity: 0.08, weight: 1.5, dashArray: '6,4'
});
marker.bindPopup(`
<div class="p-header"><i class="bi bi-building-fill"></i> Rumah Ibadah</div>
<div class="p-body">
<div class="info-row"><span class="key">Nama</span><span class="val">${d.nama_ibadah}</span></div>
<div class="info-row"><span class="key">Radius</span><span class="val">${d.radius.toLocaleString()} m</span></div>
<div class="info-row"><span class="key">Koordinat</span><span class="val">${d.latitude.toFixed(5)}, ${d.longitude.toFixed(5)}</span></div>
<button class="btn-submit" style="background: rgba(231,76,60,0.1); color: #e74c3c; border: 1px solid #e74c3c; margin-top: 12px; box-shadow: none;" onclick="hapusData('ibadah', ${d.id})"><i class="bi bi-trash"></i> Hapus Rumah Ibadah</button>
</div>
`);
layerIbadah.addLayer(circle);
layerIbadah.addLayer(marker);
});
}
// ===== RENDER PENDUDUK =====
function renderPenduduk() {
layerPenduduk.clearLayers();
allPenduduk.forEach(d => {
const color = d.status_warna === 'Hijau' ? 'green' : 'red';
const marker = L.marker([d.latitude, d.longitude], { icon: makeIcon(color) });
const badge = d.status_warna === 'Hijau'
? '<span class="badge-warna badge-hijau">Tercakup</span>'
: '<span class="badge-warna badge-merah">Belum Tercakup</span>';
marker.bindPopup(`
<div class="p-header"><i class="bi bi-person-fill"></i> Data Penduduk</div>
<div class="p-body">
<div class="info-row"><span class="key">Nama</span><span class="val">${d.nama_penduduk}</span></div>
<div class="info-row"><span class="key">Status</span>${badge}</div>
<div class="info-row"><span class="key">Koordinat</span><span class="val">${d.latitude.toFixed(5)}, ${d.longitude.toFixed(5)}</span></div>
<button class="btn-submit" style="background: rgba(231,76,60,0.1); color: #e74c3c; border: 1px solid #e74c3c; margin-top: 12px; box-shadow: none;" onclick="hapusData('penduduk', ${d.id})"><i class="bi bi-trash"></i> Hapus Data Penduduk</button>
</div>
`);
layerPenduduk.addLayer(marker);
});
}
// ===== CEK RADIUS (Proximity Analysis dengan Turf.js) =====
async function cekRadius() {
if (allIbadah.length === 0 || allPenduduk.length === 0) return;
for (const penduduk of allPenduduk) {
const ptPenduduk = turf.point([penduduk.longitude, penduduk.latitude]);
let dalamRadius = false;
for (const ibadah of allIbadah) {
const ptIbadah = turf.point([ibadah.longitude, ibadah.latitude]);
// turf.distance mengembalikan km, ubah ke meter
const jarakM = turf.distance(ptPenduduk, ptIbadah, { units: 'kilometers' }) * 1000;
if (jarakM <= ibadah.radius) {
dalamRadius = true;
break;
}
}
const warnaTarget = dalamRadius ? 'Hijau' : 'Merah';
// Hanya kirim update jika status berubah (efisiensi request)
if (penduduk.status_warna !== warnaTarget) {
penduduk.status_warna = warnaTarget; // update lokal
await updateWarnaPenduduk(penduduk.id, warnaTarget);
}
}
renderPenduduk();
updateStats();
}
// ===== UPDATE STATUS WARNA KE SERVER =====
async function updateWarnaPenduduk(id, warna) {
try {
const fd = new FormData();
fd.append('action', 'update_warna');
fd.append('id', id);
fd.append('status_warna', warna);
const res = await fetch('ambil.php', { method: 'POST', body: fd });
const data = await res.json();
console.log('Update warna:', data.message);
} catch(e) {
console.error('Gagal update warna:', e);
}
}
// ===== HAPUS DATA =====
async function hapusData(type, id) {
if(!confirm("Yakin ingin menghapus data ini?")) return;
try {
const fd = new FormData();
fd.append('action', 'hapus');
fd.append('type_hapus', type);
fd.append('id', id);
const res = await fetch('ambil.php', { method: 'POST', body: fd });
const text = await res.text();
try {
const data = JSON.parse(text);
if (data.status === 'success') {
map.closePopup();
showToast(`<i class="bi bi-trash"></i> ${data.message}`, '#e74c3c');
await loadData();
} else {
showToast(`<i class="bi bi-x-circle"></i> ${data.message}`, '#e74c3c');
}
} catch(parseErr) {
showToast(`<i class="bi bi-bug"></i> Error Server: ${text.substring(0, 50)}...`, '#e74c3c');
console.error('Response bukan JSON:', text);
}
} catch(e) {
showToast('<i class="bi bi-wifi-off"></i> Gagal menghapus data.', '#e74c3c');
console.error(e);
}
}
// ===== KLIK PETA =====
map.on('click', function(e) {
if (currentMode === 'view') return;
const { lat, lng } = e.latlng;
if (currentMode === 'ibadah') {
const content = `
<div class="p-header"><i class="bi bi-building-fill"></i> Tambah Rumah Ibadah</div>
<div class="p-body">
<label class="f-label">Nama Rumah Ibadah</label>
<input id="inp-nama-ibadah" class="f-input" type="text" placeholder="cth: Masjid Al-Hikmah" required>
<label class="f-label">Radius Jangkauan (meter)</label>
<input id="inp-radius" class="f-input" type="number" placeholder="cth: 500" value="500" min="50" max="50000" required>
<button class="btn-submit" onclick="simpanIbadah(${lat}, ${lng})">
<i class="bi bi-check2-circle"></i> Simpan Rumah Ibadah
</button>
</div>`;
L.popup({ minWidth: 300 }).setLatLng(e.latlng).setContent(content).openOn(map);
}
if (currentMode === 'penduduk') {
const content = `
<div class="p-header"><i class="bi bi-person-fill"></i> Tambah Data Penduduk</div>
<div class="p-body">
<label class="f-label">Nama Penduduk</label>
<input id="inp-nama-penduduk" class="f-input" type="text" placeholder="cth: Budi Santoso" required>
<button class="btn-submit" onclick="simpanPenduduk(${lat}, ${lng})">
<i class="bi bi-check2-circle"></i> Simpan Penduduk
</button>
</div>`;
L.popup({ minWidth: 300 }).setLatLng(e.latlng).setContent(content).openOn(map);
}
});
// ===== SIMPAN IBADAH =====
async function simpanIbadah(lat, lng) {
const nama = document.getElementById('inp-nama-ibadah')?.value?.trim();
const radius = parseInt(document.getElementById('inp-radius')?.value);
if (!nama) { showToast('<i class="bi bi-exclamation"></i> Nama tidak boleh kosong!', '#e74c3c'); return; }
if (!radius || radius < 1) { showToast('<i class="bi bi-exclamation"></i> Radius tidak valid!', '#e74c3c'); return; }
try {
const fd = new FormData();
fd.append('type', 'ibadah');
fd.append('nama_ibadah', nama);
fd.append('latitude', lat);
fd.append('longitude', lng);
fd.append('radius', radius);
const res = await fetch('ambil.php', { method: 'POST', body: fd });
const text = await res.text(); // Ambil teks mentah dulu untuk debug
try {
const data = JSON.parse(text);
if (data.status === 'success') {
map.closePopup();
showToast(`<i class="bi bi-check-circle-fill"></i> ${data.message}`, '#2ecc71');
await loadData();
} else {
showToast(`<i class="bi bi-x-circle"></i> ${data.message}`, '#e74c3c');
}
} catch(parseErr) {
// Jika bukan JSON, tampilkan 50 karakter pertama dari teks error-nya
showToast(`<i class="bi bi-bug"></i> Error Server: ${text.substring(0, 50)}...`, '#e74c3c');
console.error('Response bukan JSON:', text);
}
} catch(e) {
showToast('<i class="bi bi-wifi-off"></i> Gagal terhubung ke ambil.php', '#e74c3c');
console.error(e);
}
}
// ===== SIMPAN PENDUDUK =====
async function simpanPenduduk(lat, lng) {
const nama = document.getElementById('inp-nama-penduduk')?.value?.trim();
if (!nama) { showToast('<i class="bi bi-exclamation"></i> Nama tidak boleh kosong!', '#e74c3c'); return; }
try {
const fd = new FormData();
fd.append('type', 'penduduk');
fd.append('nama_penduduk', nama);
fd.append('latitude', lat);
fd.append('longitude', lng);
const res = await fetch('ambil.php', { method: 'POST', body: fd });
const text = await res.text(); // Ambil teks mentah
try {
const data = JSON.parse(text);
if (data.status === 'success') {
map.closePopup();
showToast(`<i class="bi bi-check-circle-fill"></i> ${data.message}`, '#2ecc71');
await loadData();
} else {
showToast(`<i class="bi bi-x-circle"></i> ${data.message}`, '#e74c3c');
}
} catch(parseErr) {
showToast(`<i class="bi bi-bug"></i> Error Server: ${text.substring(0, 50)}...`, '#e74c3c');
console.error('Response bukan JSON:', text);
}
} catch(e) {
showToast('<i class="bi bi-wifi-off"></i> Gagal terhubung ke ambil.php', '#e74c3c');
console.error(e);
}
}
// ===== BOOT =====
loadData();
</script>
</body>
</html>