feat: implement map visualization, layer management, and searching for poverty data records
This commit is contained in:
@@ -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.
|
||||
@@ -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."]);
|
||||
}
|
||||
?>
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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))'));
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
@@ -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]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user