feat: implement map visualization, layer management, and searching for poverty data records

This commit is contained in:
Syariffullah
2026-06-06 14:25:49 +07:00
parent 977dec4e36
commit 5585c34c5c
11 changed files with 704 additions and 47 deletions
+60
View File
@@ -0,0 +1,60 @@
# WebGIS Pemetaan Kemiskinan Kota Pontianak
Aplikasi WebGIS sederhana berbasis Leaflet.js untuk memetakan data kemiskinan, sebaran penduduk miskin, log bantuan, rumah ibadah, SPBU, serta data jalan dan parsil tanah di Kota Pontianak.
## Fitur Utama
- **Pemetaan Interaktif**: Visualisasi peta dengan Leaflet.js yang berpusat di Kota Pontianak.
- **Manajemen Data Spasial**:
- **SPBU**: Lokasi titik SPBU beserta informasi layanan 24 jam.
- **Jalan**: Fitur line/polyline dengan kategori status jalan (Nasional, Provinsi, Kabupaten).
- **Parsil**: Fitur area/polygon tanah dengan kategori sertifikasi (SHM, HGB, HGU, HP).
- **Rumah Ibadah**: Lokasi titik rumah ibadah (Masjid, Gereja, Vihara, dll) beserta jangkauan radius jemaah/bantuan.
- **Penduduk Miskin**: Pemetaan warga kurang mampu dan integrasi log/riwayat bantuan sosial yang disalurkan melalui rumah ibadah terdekat.
- **Pencarian Cepat**: Cari data jalan, parsil, SPBU, atau rumah ibadah langsung dari bilah pencarian.
- **Impor GeoJSON Eksternal**: Pengguna dapat menambahkan file GeoJSON dari perangkat lokal ke dalam peta.
## Persyaratan Sistem
- **Server Web**: Apache (misal menggunakan XAMPP atau Laragon).
- **Bahasa Pemrograman**: PHP 7.4 ke atas.
- **Database**: MySQL versi 5.7+ atau MariaDB versi 10.2+ (Wajib mendukung fitur fungsi spasial seperti `ST_GeomFromGeoJSON` dan `ST_AsGeoJSON`).
## Cara Instalasi & Penggunaan
1. **Unduh/Clone Repositori**:
Tempatkan folder `webgis-poverty-pontianak` di dalam direktori root server web Anda (misalnya `C:/xampp/htdocs/webgis/webgis-poverty-pontianak`).
2. **Setup Database**:
- Buka phpMyAdmin atau MySQL client pilihan Anda.
- Buat database baru bernama `webgis`:
```sql
CREATE DATABASE webgis;
```
- Import file `database.sql` ke dalam database `webgis` tersebut.
3. **Konfigurasi Koneksi**:
- Buka file [api/db_connect.php](api/db_connect.php).
- Sesuaikan konfigurasi host, user, password, dan database sesuai dengan setup database MySQL lokal Anda:
```php
$host = "localhost";
$user = "root";
$pass = "";
$db = "webgis";
```
4. **Jalankan Aplikasi**:
- Aktifkan modul **Apache** dan **MySQL** di Control Panel XAMPP Anda.
- Buka browser dan navigasikan ke alamat:
[http://localhost/webgis/webgis-poverty-pontianak/index.php](http://localhost/webgis/webgis-poverty-pontianak/index.php)
## Struktur Folder
- `api/` — Kumpulan REST API endpoint untuk operasi CRUD masing-masing fitur.
- `jalan/`, `parsil/`, `spbu/`, `rumah_ibadah/`, `penduduk_miskin/`, `log_bantuan/`
- `assets/` — File aset frontend.
- `css/` — Gaya UI (custom style.css).
- `js/` — Logika frontend peta dan pengelolaan interaksi panel data.
- `features/` — Modul fitur Leaflet (menggambar peta, manajemen layer, geolokasi).
- `index.php` — Halaman utama interface WebGIS.
- `database.sql` — Skema database beserta data dummy Pontianak untuk pengetesan awal.
+43
View File
@@ -0,0 +1,43 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if (is_array($data)) {
$conn->begin_transaction();
$inserted = 0;
$errors = [];
foreach ($data as $item) {
if (isset($item->lat) && isset($item->lng) && $item->lat !== '' && $item->lng !== '') {
$nama = $conn->real_escape_string($item->nama ?? '');
$kategori = $conn->real_escape_string($item->kategori_bantuan ?? 'Makan');
$jumlah_jiwa = (int)($item->jumlah_jiwa ?? 1);
$lat = (float)$item->lat;
$lng = (float)$item->lng;
$query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng)
VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng)";
if (!$conn->query($query)) {
$errors[] = $conn->error;
} else {
$inserted++;
}
}
}
if (empty($errors) && $inserted > 0) {
$conn->commit();
echo json_encode(["status" => "success", "message" => "Berhasil mengimpor $inserted data penduduk miskin."]);
} else if ($inserted === 0) {
$conn->rollback();
echo json_encode(["status" => "error", "message" => "Tidak ada data valid yang diimpor."]);
} else {
$conn->rollback();
echo json_encode(["status" => "error", "message" => "Gagal mengimpor beberapa data. Transaksi dibatalkan.", "errors" => $errors]);
}
} else {
echo json_encode(["status" => "error", "message" => "Format data salah."]);
}
?>
+52
View File
@@ -841,3 +841,55 @@ body.right-panel-open .custom-layer-panel {
.emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); }
.emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); }
.emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); }
/* ===== Toast Notification ===== */
#toastContainer {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
color: white;
box-shadow: 0 6px 24px rgba(0,0,0,0.18);
backdrop-filter: blur(6px);
pointer-events: auto;
animation: toastSlideIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;
max-width: 340px;
text-align: center;
}
.toast.hiding {
animation: toastSlideOut 0.3s ease forwards;
}
.toast-success { background: linear-gradient(135deg, #22c55e, #16a34a); }
.toast-error { background: linear-gradient(135deg, #ef4444, #dc2626); }
.toast-info { background: linear-gradient(135deg, #6366f1, #4f46e5); }
.toast-warning { background: linear-gradient(135deg, #f59e0b, #d97706); }
.toast-icon { font-size: 16px; flex-shrink: 0; }
@keyframes toastSlideIn {
0% { opacity: 0; transform: translateY(20px) scale(0.9); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toastSlideOut {
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(10px) scale(0.95); }
}
+140 -43
View File
@@ -16,6 +16,18 @@ window.toggleGeoJsonMenu = function() {
}
};
// Cek apakah fitur GeoJSON ini terlihat seperti data penduduk miskin
function isMiskinFeature(feature) {
if (!feature || !feature.geometry || feature.geometry.type !== 'Point') return false;
const props = feature.properties || {};
const keys = Object.keys(props).map(k => k.toLowerCase());
// Dianggap data miskin jika punya properti nama (dan bukan ibadah atau spbu)
const hasNama = keys.some(k => ['nama', 'name', 'penduduk'].includes(k));
const isIbadah = keys.some(k => ['jenis', 'radius', 'alamat'].includes(k));
const isSpbu = keys.some(k => ['alamat_spbu', 'is_24_jam'].includes(k));
return hasNama && !isIbadah && !isSpbu;
}
fileGeoJson.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
@@ -25,58 +37,143 @@ fileGeoJson.addEventListener('change', function(e) {
try {
const geoJsonData = JSON.parse(event.target.result);
const fileName = file.name;
const layerId = 'gj_' + (++geoJsonCounter);
// Buat FeatureGroup khusus untuk file ini
const individualLayer = L.featureGroup();
// Pisahkan fitur menjadi dua kelompok: miskin dan non-miskin
const features = geoJsonData.type === 'FeatureCollection'
? geoJsonData.features
: [geoJsonData];
// Tambahkan ke map
L.geoJSON(geoJsonData, {
onEachFeature: function (feature, layer) {
if (feature.properties) {
let popupContent = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0;">';
for (let key in feature.properties) {
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
}
popupContent += '</ul>';
layer.bindPopup(popupContent);
}
},
style: function(feature) {
const miskinFeatures = features.filter(f => isMiskinFeature(f));
const otherFeatures = features.filter(f => !isMiskinFeature(f));
// ---- Proses fitur miskin: Simpan ke DB, lalu tampilkan seperti penduduk miskin ----
if (miskinFeatures.length > 0) {
const bulkData = miskinFeatures.map(f => {
const props = f.properties || {};
const coords = f.geometry.coordinates;
return {
color: feature.properties.color || '#3388ff',
weight: 2,
fillOpacity: 0.4
nama: props.nama || props.name || props.penduduk || 'Data Impor',
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
jumlah_jiwa: parseInt(props.jumlah_jiwa || props.jumlah || props.jiwa || 1, 10) || 1,
lat: parseFloat(coords[1]),
lng: parseFloat(coords[0])
};
});
fetch('api/penduduk_miskin/bulk_create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bulkData)
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
// Reload layer penduduk miskin agar marker tampil dengan edit/hapus/log bantuan
if (typeof loadPendudukMiskin === 'function') {
loadPendudukMiskin();
}
if (typeof window.refreshActivePanel === 'function') {
window.refreshActivePanel();
}
showToast(data.message || 'Data berhasil diimpor ke layer Penduduk Miskin.', 'success');
} else {
showToast(data.message || 'Gagal menyimpan data miskin.', 'error');
}
})
.catch(err => {
console.error(err);
showToast('Gagal terhubung ke server saat menyimpan data miskin.', 'error');
});
}
// ---- Proses fitur non-miskin: Render sebagai layer GeoJSON biasa ----
if (otherFeatures.length > 0) {
const layerId = 'gj_' + (++geoJsonCounter);
const individualLayer = L.featureGroup();
const otherGeoJson = {
type: 'FeatureCollection',
features: otherFeatures
};
L.geoJSON(otherGeoJson, {
pointToLayer: function(feature, latlng) {
const props = feature.properties || {};
let emoji = props.emoji;
if (!emoji) {
const text = JSON.stringify(props).toLowerCase();
if (text.includes('spbu')) emoji = '⛽';
else if (text.includes('masjid')) emoji = '🕌';
else if (text.includes('gereja')) emoji = '⛪';
else if (text.includes('vihara')) emoji = '🪷';
else if (text.includes('pura')) emoji = '🛕';
else if (text.includes('kelenteng')) emoji = '🏮';
else emoji = '📍';
}
let cls = 'miskin-out';
if (emoji === '⛽') cls = 'spbu-24';
else if (['🕌','⛪','🛕','🪷','🏮'].includes(emoji)) cls = 'ibadah';
const icon = L.divIcon({
className: '',
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>${emoji}</span></div></div>`,
iconSize: [38, 38],
iconAnchor: [19, 38],
popupAnchor: [0, -40]
});
return L.marker(latlng, { icon: icon });
},
onEachFeature: function(feature, layer) {
if (feature.properties) {
let popupContent = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0; font-size:12px; margin: 0;">';
for (let key in feature.properties) {
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
}
popupContent += '</ul>';
layer.bindPopup(popupContent);
}
},
style: function(feature) {
return {
color: (feature.properties && feature.properties.color) || '#3388ff',
weight: 2,
fillOpacity: 0.4
};
}
}).addTo(individualLayer);
individualLayer.addTo(map);
geoJsonLayers[layerId] = individualLayer;
// Tambahkan checkbox ke UI
const container = document.getElementById('geoJsonLayersContainer');
const label = document.createElement('label');
label.className = 'layer-option';
label.innerHTML = `<input type="checkbox" id="chk_${layerId}" checked> ${fileName}`;
label.querySelector('input').addEventListener('change', function(e) {
if (e.target.checked) {
map.addLayer(geoJsonLayers[layerId]);
} else {
map.removeLayer(geoJsonLayers[layerId]);
}
});
container.appendChild(label);
if (miskinFeatures.length === 0) {
showToast('File GeoJSON berhasil dimuat!', 'success');
}
}).addTo(individualLayer);
}
individualLayer.addTo(map);
geoJsonLayers[layerId] = individualLayer;
// Tambahkan checkbox ke UI
const container = document.getElementById('geoJsonLayersContainer');
const label = document.createElement('label');
label.className = 'layer-option';
label.innerHTML = `<input type="checkbox" id="chk_${layerId}" checked> ${fileName}`;
label.querySelector('input').addEventListener('change', function(e) {
if (e.target.checked) {
map.addLayer(geoJsonLayers[layerId]);
} else {
map.removeLayer(geoJsonLayers[layerId]);
}
});
container.appendChild(label);
// Reset input file agar bisa import file yang sama jika dihapus
// Reset input file agar bisa import file yang sama
fileGeoJson.value = '';
alert('File GeoJSON berhasil dimuat!');
} catch (error) {
alert('Gagal memproses file GeoJSON. Pastikan format valid.');
showToast('Gagal memproses file GeoJSON. Pastikan format valid.', 'error');
console.error(error);
}
};
+231
View File
@@ -552,6 +552,237 @@ function updateSemuaWarnaMiskin() {
});
}
// ==========================
// ===== Import Feature =====
// ==========================
let parsedImportData = [];
window.openImportMiskinModal = function() {
parsedImportData = []; // Reset data
const bodyHTML = `
<div class="form-group">
<label>Pilih File (CSV, JSON, atau GeoJSON)</label>
<input type="file" id="importMiskinFile" accept=".csv,.json,.geojson" style="display:block; margin-bottom:10px; width:100%;">
<div style="font-size:12px; color:#666; margin-bottom:15px; line-height:1.4;">
<strong>Format CSV yang didukung:</strong><br>
Header minimal berisi: <code>nama</code>, <code>kategori_bantuan</code> (atau <code>kategori</code>), <code>jumlah_jiwa</code>, <code>lat</code>, <code>lng</code>.<br>
Pemisah: Koma (<code>,</code>), Titik Koma (<code>;</code>), atau Tab.
</div>
</div>
<div id="importPreviewContainer" style="display:none; margin-top: 15px;">
<h4 style="margin:0 0 8px 0; font-size: 13px; color: #333; display: flex; justify-content: space-between;">
<span>Pratinjau Data (<span id="importPreviewCount">0</span> item)</span>
<span id="importPreviewStatus" style="font-weight: normal; font-size: 11px;"></span>
</h4>
<div style="max-height:180px; overflow-y:auto; border:1px solid #e2e8f0; border-radius:6px; background:#f8fafc;">
<table style="width:100%; border-collapse:collapse; font-size:11px; text-align:left;">
<thead>
<tr style="background:#f1f5f9; border-bottom:1px solid #cbd5e1; color:#475569; position: sticky; top: 0;">
<th style="padding:6px 8px;">Nama</th>
<th style="padding:6px 8px;">Kategori</th>
<th style="padding:6px 8px;">Jiwa</th>
<th style="padding:6px 8px;">Koordinat</th>
</tr>
</thead>
<tbody id="importPreviewTableBody" style="color:#334155;"></tbody>
</table>
</div>
</div>
<div id="importErrorMsg" style="display:none; margin-top:10px; color:#ef4444; font-size:12px; font-weight: 500;"></div>
`;
map.closePopup();
openModal("Impor Penduduk Miskin", bodyHTML, function() {
if (parsedImportData.length === 0) {
showToast('Silakan pilih file terlebih dahulu atau pastikan data valid.', 'warning');
return;
}
// Kirim data ke API bulk_create.php
fetch('api/penduduk_miskin/bulk_create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsedImportData)
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
closeModal();
loadPendudukMiskin();
showToast(data.message, 'success');
} else {
showToast(data.message || 'Gagal mengimpor data.', 'error');
}
})
.catch(err => {
console.error(err);
showToast('Terjadi kesalahan saat menghubungi server.', 'error');
});
});
// Register file input listener
document.getElementById('importMiskinFile').addEventListener('change', handleImportFileChange);
};
function parseCSV(text) {
const lines = text.split(/\r?\n/);
if (lines.length < 2) return [];
const firstLine = lines[0];
let delimiter = ',';
if (firstLine.includes(';')) delimiter = ';';
else if (firstLine.includes('\t')) delimiter = '\t';
const headers = firstLine.split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, '').toLowerCase());
const results = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
let fields = [];
let currentField = '';
let insideQuote = false;
for (let j = 0; j < line.length; j++) {
const char = line[j];
if (char === '"' || char === "'") {
insideQuote = !insideQuote;
} else if (char === delimiter && !insideQuote) {
fields.push(currentField.trim());
currentField = '';
} else {
currentField += char;
}
}
fields.push(currentField.trim());
fields = fields.map(f => f.replace(/^["']|["']$/g, ''));
const row = {};
headers.forEach((header, index) => {
row[header] = fields[index] || '';
});
results.push(row);
}
return results;
}
function handleImportFileChange(e) {
const file = e.target.files[0];
const previewContainer = document.getElementById('importPreviewContainer');
const previewCount = document.getElementById('importPreviewCount');
const previewTableBody = document.getElementById('importPreviewTableBody');
const errorMsg = document.getElementById('importErrorMsg');
if (!file) {
previewContainer.style.display = 'none';
parsedImportData = [];
return;
}
errorMsg.style.display = 'none';
previewContainer.style.display = 'none';
previewTableBody.innerHTML = '';
parsedImportData = [];
const reader = new FileReader();
reader.onload = function(event) {
try {
const text = event.target.result;
let rawData = [];
if (file.name.endsWith('.csv')) {
rawData = parseCSV(text);
} else if (file.name.endsWith('.json') || file.name.endsWith('.geojson')) {
const json = JSON.parse(text);
if (json.type === 'FeatureCollection' && Array.isArray(json.features)) {
rawData = json.features.map(f => {
const props = f.properties || {};
const coords = f.geometry && f.geometry.coordinates;
return {
nama: props.nama || props.name || props.penduduk || '',
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
jumlah_jiwa: props.jumlah_jiwa || props.jumlah || props.jiwa || 1,
lat: coords ? coords[1] : (props.lat || props.latitude),
lng: coords ? coords[0] : (props.lng || props.longitude)
};
});
} else if (Array.isArray(json)) {
rawData = json;
} else {
throw new Error("Format JSON tidak didukung.");
}
}
if (rawData.length === 0) {
errorMsg.textContent = "File kosong atau format tidak didukung.";
errorMsg.style.display = 'block';
return;
}
const validRows = [];
let invalidCount = 0;
rawData.forEach(row => {
const nama = row.nama || row.name || row.penduduk || row['nama lengkap'] || row.fullname || '';
const kategori_bantuan = row.kategori_bantuan || row.kategori || row.bantuan || row['kategori bantuan'] || 'Makan';
const jumlah_jiwa = parseInt(row.jumlah_jiwa || row.jumlah || row.jiwa || row['jumlah jiwa'] || 1, 10) || 1;
const lat = parseFloat(row.lat || row.latitude || row.y);
const lng = parseFloat(row.lng || row.longitude || row.long || row.x);
const isValid = !isNaN(lat) && !isNaN(lng) && lat !== 0 && lng !== 0 && nama.trim() !== '';
if (isValid) {
validRows.push({ nama, kategori_bantuan, jumlah_jiwa, lat, lng });
} else {
invalidCount++;
}
});
parsedImportData = validRows;
if (validRows.length === 0) {
errorMsg.textContent = "Tidak ditemukan data valid (pastikan kolom nama, lat, dan lng terisi).";
errorMsg.style.display = 'block';
return;
}
validRows.forEach(row => {
const tr = document.createElement('tr');
tr.style.borderBottom = '1px solid #e2e8f0';
tr.innerHTML = `
<td style="padding:6px 8px; font-weight: 500;">${row.nama}</td>
<td style="padding:6px 8px;">${row.kategori_bantuan}</td>
<td style="padding:6px 8px;">${row.jumlah_jiwa}</td>
<td style="padding:6px 8px; color: #64748b; font-family: monospace;">${row.lat.toFixed(5)}, ${row.lng.toFixed(5)}</td>
`;
previewTableBody.appendChild(tr);
});
previewCount.textContent = validRows.length;
previewContainer.style.display = 'block';
const statusEl = document.getElementById('importPreviewStatus');
if (statusEl) {
if (invalidCount > 0) {
statusEl.innerHTML = `<span style="color:#e11d48;"><i class="fas fa-exclamation-triangle"></i> ${invalidCount} baris tidak valid diabaikan</span>`;
} else {
statusEl.innerHTML = `<span style="color:#16a34a;"><i class="fas fa-check-circle"></i> Semua data valid</span>`;
}
}
} catch (err) {
console.error(err);
errorMsg.textContent = "Gagal memproses file. Pastikan format file sesuai.";
errorMsg.style.display = 'block';
}
};
reader.readAsText(file);
}
// Initial Load
loadRumahIbadah();
loadPendudukMiskin();
+17
View File
@@ -2,6 +2,23 @@
// Koordinat awal: [-0.0263, 109.3425] (Pontianak)
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
// ===== Toast Notification =====
window.showToast = function(message, type = 'success', duration = 3500) {
const icons = { success: '✅', error: '❌', info: '️', warning: '⚠️' };
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `<span class="toast-icon">${icons[type] || '️'}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('hiding');
toast.addEventListener('animationend', () => toast.remove());
}, duration);
};
// Custom Zoom Control Logic
document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); });
document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); });
+5 -1
View File
@@ -347,7 +347,11 @@
});
document.getElementById('btnPanelImportMiskin').addEventListener('click', () => {
alert('Fitur impor CSV akan segera tersedia.');
if (typeof window.openImportMiskinModal === 'function') {
window.openImportMiskinModal();
} else {
alert('Fitur impor belum siap.');
}
});
const all = [];
+117
View File
@@ -0,0 +1,117 @@
-- Database schema for WebGIS Pemetaan Kemiskinan Pontianak
CREATE DATABASE IF NOT EXISTS webgis;
USE webgis;
-- 1. Tabel rumah_ibadah
DROP TABLE IF EXISTS `log_bantuan`;
DROP TABLE IF EXISTS `rumah_ibadah`;
CREATE TABLE `rumah_ibadah` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nama` varchar(255) NOT NULL,
`jenis` varchar(100) NOT NULL,
`alamat` text,
`radius` double NOT NULL DEFAULT 0,
`lat` double NOT NULL,
`lng` double NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. Tabel penduduk_miskin
DROP TABLE IF EXISTS `penduduk_miskin`;
CREATE TABLE `penduduk_miskin` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nama` varchar(255) NOT NULL,
`kategori_bantuan` varchar(255) NOT NULL,
`jumlah_jiwa` int(11) NOT NULL DEFAULT 1,
`lat` double NOT NULL,
`lng` double NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. Tabel log_bantuan
CREATE TABLE `log_bantuan` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`miskin_id` int(11) NOT NULL,
`ibadah_id` int(11) NOT NULL,
`tipe_bantuan` varchar(255) NOT NULL,
`tanggal` date NOT NULL,
`keterangan` text,
PRIMARY KEY (`id`),
KEY `fk_log_miskin` (`miskin_id`),
KEY `fk_log_ibadah` (`ibadah_id`),
CONSTRAINT `fk_log_ibadah` FOREIGN KEY (`ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_log_miskin` FOREIGN KEY (`miskin_id`) REFERENCES `penduduk_miskin` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. Tabel jalan
DROP TABLE IF EXISTS `jalan`;
CREATE TABLE `jalan` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nama` varchar(255) NOT NULL,
`status` varchar(100) NOT NULL DEFAULT 'Kabupaten',
`panjang` double NOT NULL DEFAULT 0,
`geom` geometry NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 5. Tabel parsil
DROP TABLE IF EXISTS `parsil`;
CREATE TABLE `parsil` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nama` varchar(255) NOT NULL,
`status` varchar(100) NOT NULL DEFAULT 'SHM',
`luas` double NOT NULL DEFAULT 0,
`geom` geometry NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 6. Tabel spbu
DROP TABLE IF EXISTS `spbu`;
CREATE TABLE `spbu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nama` varchar(255) NOT NULL,
`no_wa` varchar(20) DEFAULT NULL,
`is_24_jam` tinyint(1) NOT NULL DEFAULT 0,
`lat` double NOT NULL,
`lng` double NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data Contoh (Sample Data) untuk Pontianak
-- Data Contoh Rumah Ibadah
INSERT INTO `rumah_ibadah` (`nama`, `jenis`, `alamat`, `radius`, `lat`, `lng`) VALUES
('Masjid Raya Mujahidin', 'Masjid', 'Jl. Jenderal Ahmad Yani, Akcaya, Kec. Pontianak Sel.', 500, -0.0435, 109.3440),
('Gereja Katedral Santo Yosef', 'Gereja', 'Jl. Patria Tama, Darat Sekip, Kec. Pontianak Kota', 500, -0.0278, 109.3370),
('Vihara Bodhisatva Karaniya Metta', 'Vihara', 'Jl. Kapten Marsan, Darat Sekip, Kec. Pontianak Kota', 500, -0.0232, 109.3432);
-- Data Contoh Penduduk Miskin
INSERT INTO `penduduk_miskin` (`nama`, `kategori_bantuan`, `jumlah_jiwa`, `lat`, `lng`) VALUES
('Budi Santoso', 'Makan', 4, -0.0420, 109.3415),
('Siti Aminah', 'Pemberdayaan', 3, -0.0285, 109.3395),
('Ahmad Subarjo', 'Makan', 5, -0.0245, 109.3448);
-- Data Contoh Log Bantuan
INSERT INTO `log_bantuan` (`miskin_id`, `ibadah_id`, `tipe_bantuan`, `tanggal`, `keterangan`) VALUES
(1, 1, 'Sembako', '2026-05-15', 'Pembagian paket sembako bulanan'),
(2, 2, 'Pendidikan', '2026-05-20', 'Bantuan biaya sekolah anak'),
(3, 1, 'Uang Tunai', '2026-06-01', 'Santunan tunai darurat');
-- Data Contoh SPBU
INSERT INTO `spbu` (`nama`, `no_wa`, `is_24_jam`, `lat`, `lng`) VALUES
('SPBU Ahmad Yani', '081234567890', 1, -0.0485, 109.3490),
('SPBU Oesman Sapta Odang', '081234567891', 0, -0.0345, 109.3512),
('SPBU Kota Baru', '081234567892', 1, -0.0512, 109.3198);
-- Data Contoh Jalan (Polyline)
-- Menggunakan ST_GeomFromText untuk membuat LineString
INSERT INTO `jalan` (`nama`, `status`, `panjang`, `geom`) VALUES
('Jl. Jenderal Ahmad Yani', 'Nasional', 2500, ST_GeomFromText('LINESTRING(109.3400 -0.0380, 109.3450 -0.0450, 109.3500 -0.0520)')),
('Jl. Gajah Mada', 'Provinsi', 1800, ST_GeomFromText('LINESTRING(109.3350 -0.0290, 109.3410 -0.0280, 109.3480 -0.0270)'));
-- Data Contoh Parsil (Polygon)
-- Menggunakan ST_GeomFromText untuk membuat Polygon (pastikan koordinat berputar kembali ke titik awal untuk menutup polygon)
INSERT INTO `parsil` (`nama`, `status`, `luas`, `geom`) VALUES
('Parsil Tanah A', 'SHM', 150, ST_GeomFromText('POLYGON((109.3360 -0.0250, 109.3375 -0.0250, 109.3375 -0.0260, 109.3360 -0.0260, 109.3360 -0.0250))')),
('Parsil Tanah B', 'HGB', 320, ST_GeomFromText('POLYGON((109.3405 -0.0310, 109.3420 -0.0310, 109.3420 -0.0330, 109.3405 -0.0330, 109.3405 -0.0310))'));
+3
View File
@@ -290,6 +290,9 @@
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer"></div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
+4
View File
@@ -0,0 +1,4 @@
nama,kategori_bantuan,jumlah_jiwa,lat,lng
Budi Santoso,Makan,4,-0.025,109.340
Siti Aminah,Pemberdayaan,3,-0.028,109.345
Andi Wijaya,Makan,5,-0.024,109.341
1 nama kategori_bantuan jumlah_jiwa lat lng
2 Budi Santoso Makan 4 -0.025 109.340
3 Siti Aminah Pemberdayaan 3 -0.028 109.345
4 Andi Wijaya Makan 5 -0.024 109.341
+29
View File
@@ -0,0 +1,29 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"nama": "Joko Widodo",
"kategori_bantuan": "Pemberdayaan",
"jumlah_jiwa": 2
},
"geometry": {
"type": "Point",
"coordinates": [109.343, -0.027]
}
},
{
"type": "Feature",
"properties": {
"nama": "Mega Wati",
"kategori_bantuan": "Makan",
"jumlah_jiwa": 6
},
"geometry": {
"type": "Point",
"coordinates": [109.344, -0.029]
}
}
]
}