import React, { useState, useEffect } from 'react'; import { MapContainer, TileLayer, Marker, Popup, Circle, Polyline, Polygon, useMapEvents, LayersControl, LayerGroup, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import markerIconPng from "leaflet/dist/images/marker-icon.png"; import markerShadowPng from "leaflet/dist/images/marker-shadow.png"; // Pengaturan Default Marker Objek const DefaultIcon = L.icon({ iconUrl: markerIconPng, shadowUrl: markerShadowPng, iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], }); L.Marker.prototype.options.icon = DefaultIcon; // Pengaturan Marker Kustom Penduduk Miskin const MiskinIcon = L.divIcon({ className: 'custom-miskin-marker', html: `
`, iconSize: [12, 12], iconAnchor: [6, 6] }); // Pengaturan Marker Kustom Rumah Ibadah const IbadahIcon = L.icon({ iconUrl: markerIconPng, iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], }); // Fungsi Parser Geometri GeoJSON dari PostGIS const parsePoint = (geom) => [geom.coordinates[1], geom.coordinates[0]]; const parseLineString = (geom) => geom.coordinates.map(coord => [coord[1], coord[0]]); const parsePolygon = (geom) => geom.coordinates.map(ring => ring.map(coord => [coord[1], coord[0]])); // Fungsi Decoder Polyline OSRM Manual (Pastikan ini ada agar tidak undefined) const decodeOSRMPoints = (str, precision) => { var index = 0, lat = 0, lng = 0, coordinates = [], shift = 0, result = 0, byte = null, lat_change, lng_change, factor = Math.pow(10, precision || 5); while (index < str.length) { byte = null; shift = 0; result = 0; do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); lat_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); lat += lat_change; byte = null; shift = 0; result = 0; do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); lng_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); lng += lng_change; coordinates.push([lat / factor, lng / factor]); } return coordinates; }; // Listener Klik Peta Universal function MapClickHandler({ activeMode, onMapClick }) { useMapEvents({ click(e) { if (activeMode) { onMapClick(e.latlng.lat, e.latlng.lng); } }, }); return null; } // ❇️ MENAMBAHKAN DEKLARASI MAP VIEW UPDATER AGAR TIDAK ERROR REFERENCE function MapViewUpdater({ center }) { const map = useMap(); useEffect(() => { if (center) { map.setView(center, map.getZoom()); } }, [center, map]); return null; } // Sub-Komponen Form Penduduk Miskin (Fokus hanya pada Input Formulir) function PendudukMiskinForm({ selectedLat, selectedLng, onSaveSuccess, onClose }) { const [kepalaKeluarga, setKepalaKeluarga] = useState(''); const [jumlahAnggota, setJumlahAnggota] = useState(1); const [statusBantuan, setStatusBantuan] = useState(0); const [jenisBantuan, setJenisBantuan] = useState([]); const opsiBantuan = [ 'BPJS_Kesehatan', 'PKH', 'PIP/KIP', 'Bansos_Tunai', 'BPNT/Sembako', 'Santunan Anak Yatim-Piatu' ]; const handleCheckboxChange = (bantuan) => { if (jenisBantuan.includes(bantuan)) { setJenisBantuan(jenisBantuan.filter((item) => item !== bantuan)); } else { setJenisBantuan([...jenisBantuan, bantuan]); } }; const handleSubmit = async (e) => { e.preventDefault(); const payload = { kepala_keluarga: kepalaKeluarga, jumlah_anggota: parseInt(jumlahAnggota), status_bantuan: parseInt(statusBantuan), jenis_bantuan: jenisBantuan, lat: selectedLat, lng: selectedLng }; try { const response = await fetch('http://localhost:5000/api/miskin/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); if (result.status === 'success') { alert(`Data Berhasil Disimpan!\nID Ibadah Terdekat: ${result.closest_hub}`); onSaveSuccess(); onClose(); } else { alert(`Gagal menyimpan data: ${result.message}`); } } catch (error) { console.error('Error fetching data:', error); alert('Terjadi kesalahan jaringan saat menyimpan data.'); } }; return (
setKepalaKeluarga(e.target.value)} />
setJumlahAnggota(e.target.value)} />
{parseInt(statusBantuan) === 1 && (
{opsiBantuan.map((bantuan) => (
handleCheckboxChange(bantuan)} style={{ marginRight: '8px' }} />
))}
)}

Koordinat Terpilih: {selectedLat?.toFixed(5)}, {selectedLng?.toFixed(5)}

); } // Komponen Legenda Spasial function MapLegend() { const map = useMap(); useEffect(() => { const legend = L.control({ position: 'bottomright' }); legend.onAdd = () => { const div = L.DomUtil.create('div', 'info legend'); div.style.background = 'white'; div.style.padding = '12px'; div.style.borderRadius = '8px'; div.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)'; div.style.fontSize = '13px'; div.style.lineHeight = '20px'; div.style.color = '#333'; div.innerHTML = `

Legenda Peta

Tempat Ibadah:
Radius Pelayanan (300m+)
Kesejahteraan:
Penduduk Miskin
Jaringan Jalan:
Jln Nasional
Jln Provinsi
Jln Kabupaten
Batas Sektoral:
Wilayah / Area Sektor
`; return div; }; legend.addTo(map); return () => { legend.remove(); }; }, [map]); return null; } const getIbadahEmoji = (jenis) => { if (!jenis) return 'πŸ“'; // Jaga-jaga jika nilainya null/undefined switch(jenis.toLowerCase()) { case 'masjid': return 'πŸ•Œ'; case 'gereja': return 'β›ͺ'; case 'vihara': return 'πŸ›•'; case 'klenteng': return '⛩️'; case 'pura': return 'πŸ•'; default: return 'πŸ“'; } }; // Pengaturan Style Warna Garis Jalan Berdasarkan Klasifikasi Database const getJalanStyle = (status) => { switch(status) { case 'Jln Nasional': return { color: '#0000ff', weight: 5 }; case 'Jln Provinsi': return { color: '#008000', weight: 4 }; default: return { color: '#ffff00', weight: 3 }; } }; function App() { const pontianakCenter = [-0.0263, 109.3425]; const [dataJalan, setDataJalan] = useState([]); const [dataArea, setDataArea] = useState([]); const [dataIbadah, setDataIbadah] = useState([]); const [dataMiskin, setDataMiskin] = useState([]); const [isRoutingLoading, setIsRoutingLoading] = useState(false); const [routingCoords, setRoutingCoords] = useState([]); const [activeInsertionMode, setActiveInsertionMode] = useState(null); const [drawCoords, setDrawCoords] = useState([]); const [showModal, setShowModal] = useState(false); const [formJalan, setFormJalan] = useState({ nama: '', status: 'Jln Kabupaten' }); const [formArea, setFormArea] = useState({ nama: '' }); const [formIbadah, setFormIbadah] = useState({ nama: '', jenis: 'masjid', kontak: '', radius: 300 }); const loadAllData = () => { fetch('http://localhost:5000/api/jalan').then(res => res.json()).then(data => setDataJalan(data)); fetch('http://localhost:5000/api/area').then(res => res.json()).then(data => setDataArea(data)); fetch('http://localhost:5000/api/ibadah').then(res => res.json()).then(data => setDataIbadah(data)); fetch('http://localhost:5000/api/miskin').then(res => res.json()).then(data => setDataMiskin(data)); }; useEffect(() => { loadAllData(); }, []); // Fungsi Routing Network Terpusat (Dikendalikan oleh Komponen App Utama) const handleFetchRoute = async (miskinLat, miskinLng, idIbadahTerdekat) => { setIsRoutingLoading(true); const ibadahTerdekat = dataIbadah.find(i => i.id === idIbadahTerdekat); if (!ibadahTerdekat || !ibadahTerdekat.geom) { alert("Maaf, data rincian koordinat Rumah Ibadah terdekat tidak ditemukan."); setIsRoutingLoading(false); return; } const [ibadahLng, ibadahLat] = ibadahTerdekat.geom.coordinates; try { const url = `https://router.project-osrm.org/route/v1/driving/${ibadahLng},${ibadahLat};${miskinLng},${miskinLat}?overview=full&geometries=polyline`; const response = await fetch(url); const data = await response.json(); if (data.code === 'Ok' && data.routes.length > 0) { const encodedRoute = data.routes[0].geometry; const decodedCoords = decodeOSRMPoints(encodedRoute); setRoutingCoords(decodedCoords); } else { alert("Gagal memetakan rute jalan resmi."); } } catch (error) { console.error(error); alert("Koneksi rute OSRM terputus."); } finally { setIsRoutingLoading(false); } }; const handleToolbarToggle = (mode) => { setActiveInsertionMode(activeInsertionMode === mode ? null : mode); setDrawCoords([]); }; const handleMapClick = (lat, lng) => { if (activeInsertionMode === 'ibadah' || activeInsertionMode === 'miskin') { setDrawCoords([[lat, lng]]); setShowModal(true); } else if (activeInsertionMode === 'jalan' || activeInsertionMode === 'area') { setDrawCoords([...drawCoords, [lat, lng]]); } }; const closeModal = () => { setShowModal(false); setDrawCoords([]); setActiveInsertionMode(null); setFormJalan({ nama: '', status: 'Jln Kabupaten' }); setFormArea({ nama: '' }); setFormIbadah({ nama: '', jenis: 'masjid', kontak: '', radius: 300 }); }; const submitJalanForm = (e) => { e.preventDefault(); fetch('http://localhost:5000/api/jalan/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...formJalan, coords: drawCoords }) }) .then(res => res.json()) .then((result) => { if (result.status === 'success') { alert("Jalan Berhasil Disimpan!"); loadAllData(); closeModal(); } else { alert("Gagal menyimpan jalan: " + result.message); } }).catch(err => alert("Terjadi kesalahan jaringan.")); }; const submitAreaForm = (e) => { e.preventDefault(); fetch('http://localhost:5000/api/area/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...formArea, coords: drawCoords }) }) .then(res => res.json()) .then((result) => { if (result.status === 'success') { alert("Area Berhasil Disimpan!"); loadAllData(); closeModal(); } else { alert("Gagal menyimpan area: " + result.message); } }).catch(err => alert("Terjadi kesalahan jaringan.")); }; const submitIbadahForm = (e) => { e.preventDefault(); fetch('http://localhost:5000/api/ibadah/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...formIbadah, lat: drawCoords[0][0], lng: drawCoords[0][1] }) }) .then(res => res.json()) .then((result) => { if (result.status === 'success') { alert("Rumah Ibadah Berhasil Disimpan!"); loadAllData(); closeModal(); } else { alert("Gagal menyimpan rumah ibadah: " + result.message); } }).catch(err => alert("Terjadi kesalahan jaringan.")); }; return (
{/* Action Toolbar */}
{(activeInsertionMode === 'jalan' || activeInsertionMode === 'area') && drawCoords.length > 1 && ( )}
{/* MAP CONTAINER UTAMA */} {/* 1. LAYER GROUP UNTUK JALAN */} {dataJalan.map(j => { if (!j.geom) return null; const pathOptions = getJalanStyle(j.status_jalan); return ( Nama Jalan: {j.nama_jalan || 'Tanpa Nama'}
Status: {j.status_jalan}
); })}
{/* 2. LAYER GROUP UNTUK AREA BANJIR */} {dataArea.map(a => { if (!a.geom) return null; return ( Area Wilayah Sektor: {a.nama_area || 'Sektor'} ); })} {/* 3. LAYER GROUP UNTUK RUMAH IBADAH (DENGAN RADIUS DINAMIS) */} {dataIbadah.map(i => { if (!i.geom) return null; const [ibadahLat, ibadahLng] = parsePoint(i.geom); return ( {/* πŸ”΅ PERBAIKAN: Menggunakan i.radius sesuai properti dari database */} {/* Marker Rumah Ibadah */} {getIbadahEmoji(i.jenis_ibadah || 'lainnya')} {i.nama_ibadah}
Jenis: {(i.jenis_ibadah?.toUpperCase()) || 'LAINNYA'}
Kontak: {i.kontak || '-'}
{/* Sesuai dengan properti i.radius */} Radius Aksi: {i.radius || 0} meter
); })}
{/* 4. LOOPING DATA PENDUDUK MISKIN YANG BENAR (Di dalam Peta) */} {dataMiskin.map(m => { if (!m.geom) return null; const [miskinLat, miskinLng] = parsePoint(m.geom); return (

πŸ‘€ Kepala Keluarga: {m.kepala_keluarga}

πŸ‘₯ Jumlah Anggota: {m.jumlah_anggota} Jiwa

πŸ“¦ Bantuan: {m.jenis_bantuan && m.jenis_bantuan.length > 0 ? m.jenis_bantuan.join(', ') : 'Belum menerima'}

πŸ•Œ Hub Terdekat ID: {m.id_ibadah_terdekat || 'Kosong'}


); })}
{/* RENDERING JALUR RUTE NYATA */} {routingCoords.length > 0 && ( )} {/* CONTROLLER EVENT MAP */}
{/* Modal Input Overlay */} {showModal && (

Tambah Data {activeInsertionMode?.toUpperCase()}

{activeInsertionMode === 'jalan' && (
setFormJalan({...formJalan, nama: e.target.value})} />
)} {activeInsertionMode === 'area' && (
setFormArea({...formArea, nama: e.target.value})} />
)} {activeInsertionMode === 'ibadah' && (
setFormIbadah({...formIbadah, nama: e.target.value})} />
setFormIbadah({...formIbadah, kontak: e.target.value})} />
setFormIbadah({...formIbadah, radius: parseInt(e.target.value)})} />
)} {activeInsertionMode === 'miskin' && drawCoords.length > 0 && ( { loadAllData(); closeModal(); }} /> )}
)}
); } export default App;