commit f104007f772a95b1b75138e746f211af7e9226be Author: muthianura Date: Thu Jun 11 18:10:22 2026 +0700 Initial commit diff --git a/SIG2_jalantanah/api.php b/SIG2_jalantanah/api.php new file mode 100644 index 0000000..4c48c40 --- /dev/null +++ b/SIG2_jalantanah/api.php @@ -0,0 +1,180 @@ + [(float)$p[1], (float)$p[0]], $pts); + return json_encode(['type' => 'LineString', 'coordinates' => $coords]); +} +function toGeoJsonPoly($pts) { + $coords = array_map(fn($p) => [(float)$p[1], (float)$p[0]], $pts); + if (count($coords) && $coords[0] !== $coords[count($coords)-1]) $coords[] = $coords[0]; + return json_encode(['type' => 'Polygon', 'coordinates' => [$coords]]); +} +function toGeoJsonPoint($lat, $lng) { + return json_encode(['type' => 'Point', 'coordinates' => [(float)$lng, (float)$lat]]); +} + +$action = $_GET['action'] ?? $_POST['action'] ?? null; +if (!$action) return; + +header('Content-Type: application/json; charset=utf-8'); +$a = $action; + +if ($a === 'search') { + $q = '%' . $conn->real_escape_string($_GET['q'] ?? '') . '%'; + $tipe = $_GET['tipe'] ?? 'semua'; + $sf = $_GET['status_filter'] ?? ''; + $pmin = (float)($_GET['panjang_min'] ?? 0); + $pmax = (float)($_GET['panjang_max'] ?? 0); + $rusak_periode = $_GET['rusak_periode'] ?? 'semua'; + $out = []; + if ($tipe === 'semua' || $tipe === 'jalan') { + $w = "nama_jalan LIKE '$q'"; + if ($sf) $w .= " AND status_jalan='" . $conn->real_escape_string($sf) . "'"; + if ($pmin > 0) $w .= " AND panjang_meter>=$pmin"; + if ($pmax > 0) $w .= " AND panjang_meter<=$pmax"; + $rj = $conn->query("SELECT id,nama_jalan,status_jalan,panjang_meter,koordinat FROM jalan WHERE $w LIMIT 20"); + while ($row = $rj->fetch_assoc()) { $row['type'] = 'jalan'; $out[] = $row; } + } + if ($tipe === 'semua' || $tipe === 'parsil') { + $w = "(nama_parsil LIKE '$q' OR nama_pemilik LIKE '$q')"; + if ($sf) $w .= " AND status_kepemilikan='" . $conn->real_escape_string($sf) . "'"; + $rp = $conn->query("SELECT id,nama_parsil,nama_pemilik,nomor_sertifikat,status_kepemilikan,luas_m2,koordinat FROM parsil WHERE $w LIMIT 20"); + while ($row = $rp->fetch_assoc()) { $row['type'] = 'parsil'; $out[] = $row; } + } + if ($tipe === 'semua' || $tipe === 'rusak') { + $wR = "nama LIKE '$q'"; + if ($rusak_periode === '1bulan') $wR .= " AND tanggal >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)"; + elseif ($rusak_periode === '3tahun') $wR .= " AND tanggal >= DATE_SUB(CURDATE(), INTERVAL 3 YEAR)"; + $rr = $conn->query("SELECT id,nama,tanggal,latitude,longitude,keterangan,koordinat FROM jalan_rusak WHERE $wR ORDER BY tanggal DESC LIMIT 20"); + while ($row = $rr->fetch_assoc()) { $row['type'] = 'rusak'; $out[] = $row; } + } + echo json_encode($out); exit; +} + +if ($a === 'save_jalan' && isset($_GET['nama'], $_GET['status'], $_GET['ukuran'], $_GET['koordinat'])) { + $nama = $conn->real_escape_string($_GET['nama']); + $status = $conn->real_escape_string($_GET['status']); + $panjang = (float)$_GET['ukuran']; + $raw = json_decode($_GET['koordinat'], true); + $geojson = $conn->real_escape_string(toGeoJsonLine($raw)); + $r = $conn->query("INSERT INTO jalan(nama_jalan,status_jalan,panjang_meter,koordinat) VALUES('$nama','$status','$panjang','$geojson')"); + echo json_encode(array_merge(['ok' => (bool)$r, 'id' => $conn->insert_id, 'msg' => $conn->error], getStats($conn))); exit; +} +if ($a === 'save_parsil' && isset($_GET['nama'], $_GET['status'], $_GET['ukuran'], $_GET['koordinat'])) { + $nama = $conn->real_escape_string($_GET['nama']); + $pemilik = $conn->real_escape_string($_GET['pemilik'] ?? ''); + $nosert = $conn->real_escape_string($_GET['nosert'] ?? ''); + $ket = $conn->real_escape_string($_GET['keterangan'] ?? ''); + $status = $conn->real_escape_string($_GET['status']); + $luas = (float)$_GET['ukuran']; + $raw = json_decode($_GET['koordinat'], true); + $geojson = $conn->real_escape_string(toGeoJsonPoly($raw)); + $r = $conn->query("INSERT INTO parsil(nama_parsil,nama_pemilik,nomor_sertifikat,keterangan,status_kepemilikan,luas_m2,koordinat) VALUES('$nama','$pemilik','$nosert','$ket','$status','$luas','$geojson')"); + echo json_encode(array_merge(['ok' => (bool)$r, 'id' => $conn->insert_id, 'msg' => $conn->error], getStats($conn))); exit; +} +if ($a === 'save_rusak') { + $pelapor = $conn->real_escape_string($_POST['pelapor'] ?? $_GET['pelapor'] ?? '') ?: 'Anonym'; + $nama_jalan = $conn->real_escape_string($_POST['nama_jalan'] ?? $_GET['nama_jalan'] ?? ''); + $tanggal = $conn->real_escape_string($_POST['tanggal'] ?? $_GET['tanggal'] ?? date('Y-m-d')); + $lat = (float)($_POST['lat'] ?? $_GET['lat'] ?? 0); + $lng = (float)($_POST['lng'] ?? $_GET['lng'] ?? 0); + $ket = $conn->real_escape_string($_POST['keterangan'] ?? $_GET['keterangan'] ?? ''); + $foto = $conn->real_escape_string($_POST['foto'] ?? $_GET['foto'] ?? ''); + $namaFull = $conn->real_escape_string($pelapor . ($nama_jalan ? ' | Jalan: ' . $nama_jalan : '')); + $geojson = $conn->real_escape_string(toGeoJsonPoint($lat, $lng)); + $r = $conn->query("INSERT INTO jalan_rusak(nama,tanggal,latitude,longitude,keterangan,foto,koordinat) VALUES('$namaFull','$tanggal','$lat','$lng','$ket','$foto','$geojson')"); + echo json_encode(array_merge(['ok' => (bool)$r, 'id' => $conn->insert_id, 'msg' => $conn->error, 'nama' => $namaFull], getStats($conn))); exit; +} + +if ($a === 'delete_jalan' && isset($_GET['id'])) { + $conn->query("DELETE FROM jalan WHERE id=" . (int)$_GET['id']); + echo json_encode(getStats($conn)); exit; +} +if ($a === 'delete_parsil' && isset($_GET['id'])) { + $conn->query("DELETE FROM parsil WHERE id=" . (int)$_GET['id']); + echo json_encode(getStats($conn)); exit; +} +if ($a === 'delete_rusak' && isset($_GET['id'])) { + $conn->query("DELETE FROM jalan_rusak WHERE id=" . (int)$_GET['id']); + echo json_encode(getStats($conn)); exit; +} + +if ($a === 'edit_jalan' && isset($_GET['id'], $_GET['nama'], $_GET['status'])) { + $id = (int)$_GET['id']; + $n = $conn->real_escape_string($_GET['nama']); + $s = $conn->real_escape_string($_GET['status']); + $coords = $_GET['koordinat'] ?? ''; + $pjg = 0; + if ($coords) { + $g = json_decode($coords, true); + $pts = []; + if (isset($g['type']) && $g['type'] === 'LineString') foreach ($g['coordinates'] as $c) $pts[] = ['lat' => $c[1], 'lng' => $c[0]]; + else foreach (json_decode($coords, true) as $p) $pts[] = ['lat' => $p[0], 'lng' => $p[1]]; + for ($i = 0; $i < count($pts) - 1; $i++) { + $la1 = deg2rad($pts[$i]['lat']); $lo1 = deg2rad($pts[$i]['lng']); + $la2 = deg2rad($pts[$i+1]['lat']); $lo2 = deg2rad($pts[$i+1]['lng']); + $d = 2 * 6371000 * asin(sqrt(sin(($la2-$la1)/2)**2 + cos($la1)*cos($la2)*sin(($lo2-$lo1)/2)**2)); + $pjg += $d; + } + $ce = $conn->real_escape_string($coords); + $conn->query("UPDATE jalan SET nama_jalan='$n',status_jalan='$s',panjang_meter='$pjg',koordinat='$ce' WHERE id=$id"); + } else { + $conn->query("UPDATE jalan SET nama_jalan='$n',status_jalan='$s' WHERE id=$id"); + $pjg = $conn->query("SELECT panjang_meter FROM jalan WHERE id=$id")->fetch_assoc()['panjang_meter']; + } + echo json_encode(array_merge(getStats($conn), ['panjang' => round($pjg, 1)])); exit; +} + +if ($a === 'edit_parsil' && isset($_GET['id'], $_GET['nama'], $_GET['status'])) { + $id = (int)$_GET['id']; + $n = $conn->real_escape_string($_GET['nama']); + $s = $conn->real_escape_string($_GET['status']); + $pemilik = $conn->real_escape_string($_GET['pemilik'] ?? ''); + $nosert = $conn->real_escape_string($_GET['nosert'] ?? ''); + $ket = $conn->real_escape_string($_GET['keterangan'] ?? ''); + $coords = $_GET['koordinat'] ?? ''; + $luas = 0; + if ($coords) { + $g = json_decode($coords, true); + $pts = isset($g['type']) && $g['type'] === 'Polygon' + ? array_map(fn($c) => ['lat' => $c[1], 'lng' => $c[0]], $g['coordinates'][0]) + : array_map(fn($p) => ['lat' => $p[0], 'lng' => $p[1]], json_decode($coords, true)); + $R = 6371000; $n2 = count($pts); $area = 0; + for ($i = 0; $i < $n2; $i++) { + $j = ($i + 1) % $n2; + $xi = $pts[$i]['lng'] * M_PI/180 * $R * cos($pts[$i]['lat'] * M_PI/180); + $yi = $pts[$i]['lat'] * M_PI/180 * $R; + $xj = $pts[$j]['lng'] * M_PI/180 * $R * cos($pts[$j]['lat'] * M_PI/180); + $yj = $pts[$j]['lat'] * M_PI/180 * $R; + $area += ($xi * $yj - $xj * $yi); + } + $luas = abs($area / 2); + $ce = $conn->real_escape_string($coords); + $conn->query("UPDATE parsil SET nama_parsil='$n',status_kepemilikan='$s',nama_pemilik='$pemilik',nomor_sertifikat='$nosert',keterangan='$ket',luas_m2='$luas',koordinat='$ce' WHERE id=$id"); + } else { + $conn->query("UPDATE parsil SET nama_parsil='$n',status_kepemilikan='$s',nama_pemilik='$pemilik',nomor_sertifikat='$nosert',keterangan='$ket' WHERE id=$id"); + $luas = $conn->query("SELECT luas_m2 FROM parsil WHERE id=$id")->fetch_assoc()['luas_m2']; + } + echo json_encode(array_merge(getStats($conn), ['luas' => round($luas, 1)])); exit; +} + +if ($a === 'edit_rusak' && isset($_GET['id'])) { + $id = (int)$_GET['id']; + $n = $conn->real_escape_string($_POST['nama'] ?? $_GET['nama'] ?? ''); + $tgl = $conn->real_escape_string($_POST['tanggal'] ?? $_GET['tanggal'] ?? ''); + $ket = $conn->real_escape_string($_POST['keterangan'] ?? $_GET['keterangan'] ?? ''); + $foto = $conn->real_escape_string($_POST['foto'] ?? $_GET['foto'] ?? ''); + $lat = isset($_POST['latitude']) ? (float)$_POST['latitude'] : (isset($_GET['latitude']) ? (float)$_GET['latitude'] : null); + $lng = isset($_POST['longitude']) ? (float)$_POST['longitude'] : (isset($_GET['longitude']) ? (float)$_GET['longitude'] : null); + if ($lat !== null && $lng !== null) { + $geojson = $conn->real_escape_string(toGeoJsonPoint($lat, $lng)); + $conn->query("UPDATE jalan_rusak SET nama='$n',tanggal='$tgl',keterangan='$ket',foto='$foto',latitude='$lat',longitude='$lng',koordinat='$geojson' WHERE id=$id"); + } else { + $conn->query("UPDATE jalan_rusak SET nama='$n',tanggal='$tgl',keterangan='$ket',foto='$foto' WHERE id=$id"); + } + echo json_encode(array_merge(getStats($conn), ['ok' => true])); exit; +} + +echo json_encode(['error' => 'unknown']); exit; diff --git a/SIG2_jalantanah/assets/jalan-input.js b/SIG2_jalantanah/assets/jalan-input.js new file mode 100644 index 0000000..40a7c8f --- /dev/null +++ b/SIG2_jalantanah/assets/jalan-input.js @@ -0,0 +1,407 @@ +/* Input mode: draw jalan / kavling / rusak on map */ +var drawMode = null, drawing = false, points = [], previewLayer = null, tempMarkers = []; +var pendingKoord = null, pendingUkuran = 0, currentMode = null; +var pendingRusakLatLng = null, fotoBase64 = ''; + +function setMode(m) { + if (drawing) cancelDraw(); + drawMode = m; + currentMode = m; + + var bj = document.getElementById('btn-jalan'); + var bp = document.getElementById('btn-parsil'); + var br = document.getElementById('btn-rusak'); + if (bj) bj.className = m === 'jalan' ? 'active-jalan' : ''; + if (bp) bp.className = m === 'parsil' ? 'active-parsil' : ''; + if (br) br.className = m === 'rusak' ? 'active-rusak' : ''; + + var actions = document.getElementById('draw-actions'); + var hint = document.getElementById('hint'); + + if (m === 'rusak') { + drawing = false; + points = []; + if (hint) hint.textContent = 'Klik titik di peta untuk menandai jalan rusak'; + map.getContainer().style.cursor = 'crosshair'; + if (actions) actions.style.display = 'flex'; + ['undo-btn', 'finish-btn'].forEach(function (id) { + var el = document.getElementById(id); + if (el) el.style.display = 'none'; + }); + var cancel = document.getElementById('cancel-btn'); + if (cancel) cancel.style.display = 'block'; + return; + } + + drawing = true; + points = []; + if (actions) actions.style.display = 'flex'; + ['undo-btn', 'finish-btn', 'cancel-btn'].forEach(function (id) { + var el = document.getElementById(id); + if (el) el.style.display = 'block'; + }); + if (hint) { + hint.textContent = m === 'jalan' + ? 'Klik peta untuk menambah titik jalan — minimal 2 titik' + : 'Klik peta untuk menggambar area kavling — minimal 3 titik'; + } + map.getContainer().style.cursor = 'crosshair'; +} + +map.on('click', function (e) { + if (currentMode === 'rusak') { + pendingRusakLatLng = e.latlng; + openRusakModal(e.latlng); + return; + } + if (!drawing) return; + points.push(e.latlng); + var dot = L.circleMarker(e.latlng, { + radius: 5, + color: drawMode === 'jalan' ? '#2563eb' : '#7c3aed', + fillColor: '#fff', + fillOpacity: 1, + weight: 2.5 + }).addTo(map); + tempMarkers.push(dot); + redrawPreview(); +}); + +function redrawPreview() { + if (previewLayer) map.removeLayer(previewLayer); + if (points.length < 2) return; + var opts = { color: drawMode === 'jalan' ? '#2563eb' : '#7c3aed', weight: 2.5, dashArray: '7,5', opacity: 0.85 }; + previewLayer = drawMode === 'jalan' + ? L.polyline(points, opts).addTo(map) + : L.polygon(points, Object.assign({}, opts, { fillOpacity: 0.1 })).addTo(map); +} + +function undoLast() { + if (!points.length) return; + points.pop(); + var last = tempMarkers.pop(); + if (last) map.removeLayer(last); + redrawPreview(); +} + +function cancelDraw() { + drawing = false; + points = []; + if (previewLayer) { map.removeLayer(previewLayer); previewLayer = null; } + tempMarkers.forEach(function (m) { map.removeLayer(m); }); + tempMarkers = []; + map.getContainer().style.cursor = ''; + var actions = document.getElementById('draw-actions'); + if (actions) actions.style.display = 'none'; + ['btn-jalan', 'btn-parsil', 'btn-rusak'].forEach(function (id) { + var el = document.getElementById(id); + if (el) el.className = ''; + }); + var hint = document.getElementById('hint'); + if (hint) hint.textContent = 'Pilih mode input di atas'; + currentMode = null; + drawMode = null; + pendingRusakLatLng = null; +} + +function finishShape() { + if (drawMode === 'jalan' && points.length < 2) { showToast('Minimal 2 titik untuk jalan!', 'error'); return; } + if (drawMode === 'parsil' && points.length < 3) { showToast('Minimal 3 titik untuk kavling!', 'error'); return; } + drawing = false; + map.getContainer().style.cursor = ''; + + var ukuran = 0; + if (drawMode === 'jalan') { + for (var i = 0; i < points.length - 1; i++) ukuran += points[i].distanceTo(points[i + 1]); + ukuran = Math.round(ukuran * 10) / 10; + document.getElementById('inp-ukuran').value = ukuran.toFixed(1) + ' meter'; + document.getElementById('form-title').textContent = 'Simpan Data Jalan'; + document.getElementById('form-sub').textContent = 'Lengkapi informasi ruas jalan'; + document.getElementById('form-icon').textContent = '🛣'; + document.getElementById('form-icon').className = 'modal-icon jalan'; + document.getElementById('label-nama').textContent = 'Nama Jalan'; + document.getElementById('label-status').textContent = 'Status Jalan'; + document.getElementById('label-ukuran').innerHTML = 'Panjang Jalan Otomatis'; + document.getElementById('inp-nama').placeholder = 'Contoh: Jl. Ahmad Yani'; + document.getElementById('inp-status').innerHTML = + '' + + '' + + ''; + document.getElementById('parsil-extra-fields').style.display = 'none'; + } else { + ukuran = calcArea(points); + ukuran = Math.round(ukuran * 10) / 10; + document.getElementById('inp-ukuran').value = ukuran.toFixed(1) + ' m²'; + document.getElementById('form-title').textContent = 'Simpan Data Kavling'; + document.getElementById('form-sub').textContent = 'Lengkapi informasi kavling tanah'; + document.getElementById('form-icon').textContent = '🏘'; + document.getElementById('form-icon').className = 'modal-icon parsil'; + document.getElementById('label-nama').textContent = 'Nama / Nomor Parsil'; + document.getElementById('label-status').textContent = 'Status Kepemilikan'; + document.getElementById('label-ukuran').innerHTML = 'Luas Area Otomatis'; + document.getElementById('inp-nama').placeholder = 'Contoh: Parsil A-01'; + document.getElementById('inp-status').innerHTML = + '' + + '' + + '' + + ''; + document.getElementById('parsil-extra-fields').style.display = 'block'; + document.getElementById('inp-pemilik').value = ''; + document.getElementById('inp-nosert').value = ''; + document.getElementById('inp-keterangan').value = ''; + } + + pendingKoord = points.map(function (p) { return [p.lat, p.lng]; }); + pendingUkuran = ukuran; + document.getElementById('inp-nama').value = ''; + document.getElementById('form-modal').classList.add('active'); + setTimeout(function () { document.getElementById('inp-nama').focus(); }, 100); +} + +function calcArea(latlngs) { + var R = 6371000, n = latlngs.length, area = 0; + if (n < 3) return 0; + for (var i = 0; i < n; i++) { + var j = (i + 1) % n; + var xi = latlngs[i].lng * Math.PI / 180 * R * Math.cos(latlngs[i].lat * Math.PI / 180); + var yi = latlngs[i].lat * Math.PI / 180 * R; + var xj = latlngs[j].lng * Math.PI / 180 * R * Math.cos(latlngs[j].lat * Math.PI / 180); + var yj = latlngs[j].lat * Math.PI / 180 * R; + area += xi * yj - xj * yi; + } + return Math.abs(area / 2); +} + +function saveData() { + var nama = document.getElementById('inp-nama').value.trim(); + var status = document.getElementById('inp-status').value; + if (!nama) { showToast('Nama tidak boleh kosong!', 'error'); return; } + + var btn = document.querySelector('#form-modal .btn-primary'); + btn.textContent = 'Menyimpan…'; + btn.disabled = true; + + var params = { + action: currentMode === 'jalan' ? 'save_jalan' : 'save_parsil', + nama: nama, + status: status, + ukuran: pendingUkuran, + koordinat: JSON.stringify(pendingKoord) + }; + if (currentMode === 'parsil') { + params.pemilik = document.getElementById('inp-pemilik').value.trim(); + params.nosert = document.getElementById('inp-nosert').value.trim(); + params.keterangan = document.getElementById('inp-keterangan').value.trim(); + } + + fetch('api.php?' + new URLSearchParams(params)) + .then(function (r) { return r.json(); }) + .then(function (res) { + btn.textContent = 'Simpan Data'; + btn.disabled = false; + if (res.ok) { + showToast('Data berhasil disimpan!', 'success'); + closeForm(); + var id = res.id; + var ll = pendingKoord.map(function (p) { return [p[0], p[1]]; }); + if (currentMode === 'jalan') { + var layer = L.polyline(ll, { color: WARNA_JALAN[status] || '#999', weight: 5, opacity: 0.9 }).addTo(map); + layer.bindPopup(makeJalanPopup(id, nama, status, pendingUkuran.toFixed(1)), { maxWidth: 280 }); + layer.on('click', function () { lnavHighlightFromMap('jalan', id); }); + jalanLayers[id] = layer; + jalanData[id] = { id: id, nama: nama, status: status, pjg: pendingUkuran.toFixed(1), ll: ll }; + } else { + var opt = WARNA_PARSIL[status] || { color: '#999', fillColor: '#999', fillOpacity: 0.2, weight: 2 }; + var layer2 = L.polygon(ll, opt).addTo(map); + layer2.bindPopup(makeParsilPopup(id, nama, params.pemilik || '', params.nosert || '', params.keterangan || '', status, pendingUkuran.toFixed(1)), { maxWidth: 280 }); + layer2.on('click', function () { lnavHighlightFromMap('parsil', id); }); + parsilLayers[id] = layer2; + parsilData[id] = { id: id, nama: nama, pemilik: params.pemilik || '', nosert: params.nosert || '', ket: params.keterangan || '', status: status, luas: pendingUkuran.toFixed(1), ll: ll }; + } + if (res.tj !== undefined) updateStats(res); + cancelDraw(); + } else { + showToast('Gagal: ' + (res.msg || 'unknown'), 'error'); + } + }) + .catch(function () { + btn.textContent = 'Simpan Data'; + btn.disabled = false; + showToast('Koneksi gagal!', 'error'); + }); +} + +function closeForm() { + document.getElementById('form-modal').classList.remove('active'); +} + +document.getElementById('form-modal').addEventListener('click', function (e) { + if (e.target === this) closeForm(); +}); + +function openRusakModal(latlng) { + var d = new Date(); + var today = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); + document.getElementById('rusak-tanggal').value = today; + document.getElementById('rusak-koordinat').value = latlng.lat.toFixed(6) + ', ' + latlng.lng.toFixed(6); + document.getElementById('rusak-sub').textContent = latlng.lat.toFixed(5) + ', ' + latlng.lng.toFixed(5); + document.getElementById('rusak-pelapor').value = ''; + document.getElementById('rusak-nama-jalan').value = 'Mencari nama jalan…'; + document.getElementById('rusak-ket').value = ''; + clearFoto(); + + fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + latlng.lat + '&lon=' + latlng.lng + '&zoom=17&accept-language=id') + .then(function (r) { return r.json(); }) + .then(function (geo) { + var road = geo.address && (geo.address.road || geo.address.residential || geo.address.path || geo.address.pedestrian || geo.address.footway || ''); + document.getElementById('rusak-nama-jalan').value = road || '(Jalan tidak teridentifikasi)'; + }) + .catch(function () { + document.getElementById('rusak-nama-jalan').value = '(Gagal deteksi nama jalan)'; + }); + + var badge = document.getElementById('gps-badge'); + badge.style.display = 'inline-flex'; + badge.className = 'gps-badge loading'; + badge.textContent = 'Mengambil GPS…'; + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function (pos) { + badge.className = 'gps-badge'; + badge.textContent = 'GPS: ±' + Math.round(pos.coords.accuracy) + 'm akurasi'; + }, function () { + badge.className = 'gps-badge'; + badge.textContent = 'Koordinat dari klik peta'; + }, { timeout: 5000 }); + } else { + badge.className = 'gps-badge'; + badge.textContent = 'Koordinat dari klik peta'; + } + + document.getElementById('rusak-modal').classList.add('active'); + setTimeout(function () { document.getElementById('rusak-pelapor').focus(); }, 100); +} + +function closeRusakModal() { + document.getElementById('rusak-modal').classList.remove('active'); + fotoBase64 = ''; + pendingRusakLatLng = null; +} + +document.getElementById('rusak-modal').addEventListener('click', function (e) { + if (e.target === this) closeRusakModal(); +}); + +function handleFoto(input) { + var file = input.files[0]; + if (!file) return; + if (file.size > 5 * 1024 * 1024) { showToast('Foto maks 5 MB!', 'error'); return; } + + EXIF.getData(file, function() { + var lat = EXIF.getTag(this, "GPSLatitude"); + var lon = EXIF.getTag(this, "GPSLongitude"); + var latRef = EXIF.getTag(this, "GPSLatitudeRef") || "N"; + var lonRef = EXIF.getTag(this, "GPSLongitudeRef") || "E"; + if (lat && lon) { + function parseDms(dms) { + if (!dms || dms.length < 3) return null; + var d = (typeof dms[0] === 'object' && dms[0].denominator) ? dms[0].numerator / dms[0].denominator : Number(dms[0]); + var m = (typeof dms[1] === 'object' && dms[1].denominator) ? dms[1].numerator / dms[1].denominator : Number(dms[1]); + var s = (typeof dms[2] === 'object' && dms[2].denominator) ? dms[2].numerator / dms[2].denominator : Number(dms[2]); + return d + (m / 60) + (s / 3600); + } + + var latDec = parseDms(lat); + var lonDec = parseDms(lon); + + if (latDec !== null && lonDec !== null && !isNaN(latDec) && !isNaN(lonDec)) { + if (latRef === "S") latDec = -latDec; + if (lonRef === "W") lonDec = -lonDec; + pendingRusakLatLng = {lat: latDec, lng: lonDec}; + document.getElementById('rusak-koordinat').value = latDec.toFixed(6) + ', ' + lonDec.toFixed(6); + showToast('Geotagging terdeteksi!', 'success'); + + var badge = document.getElementById('gps-badge'); + if(badge) { + badge.style.display = 'inline-flex'; + badge.className = 'gps-badge'; + badge.textContent = 'GPS dari Foto'; + } + + fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + latDec + '&lon=' + lonDec + '&zoom=17&accept-language=id') + .then(function (r) { return r.json(); }) + .then(function (geo) { + var road = geo.address && (geo.address.road || geo.address.residential || geo.address.path || geo.address.pedestrian || geo.address.footway || ''); + document.getElementById('rusak-nama-jalan').value = road || geo.display_name || '(Jalan tidak teridentifikasi)'; + }); + } + } + }); + + var reader = new FileReader(); + reader.onload = function (e) { + fotoBase64 = e.target.result; + var preview = document.getElementById('photo-preview'); + preview.src = fotoBase64; + preview.style.display = 'block'; + document.getElementById('foto-clear-btn').style.display = 'flex'; + }; + reader.readAsDataURL(file); +} + +function clearFoto() { + fotoBase64 = ''; + document.getElementById('photo-preview').style.display = 'none'; + document.getElementById('photo-preview').src = ''; + document.getElementById('foto-clear-btn').style.display = 'none'; + document.getElementById('foto-file').value = ''; + document.getElementById('foto-cam').value = ''; +} + +function saveRusak() { + if (!pendingRusakLatLng) { showToast('Pilih titik di peta dulu!', 'error'); return; } + var pelapor = document.getElementById('rusak-pelapor').value.trim() || 'Anonym'; + var namaJalan = document.getElementById('rusak-nama-jalan').value.trim(); + var tanggal = document.getElementById('rusak-tanggal').value; + var ket = document.getElementById('rusak-ket').value.trim(); + var lat = pendingRusakLatLng.lat; + var lng = pendingRusakLatLng.lng; + + var btn = document.getElementById('rusak-save-btn'); + btn.textContent = 'Menyimpan…'; + btn.disabled = true; + + var formData = new FormData(); + formData.append('action', 'save_rusak'); + formData.append('pelapor', pelapor); + formData.append('nama_jalan', namaJalan); + formData.append('tanggal', tanggal); + formData.append('lat', lat); + formData.append('lng', lng); + formData.append('keterangan', ket); + formData.append('foto', fotoBase64); + + fetch('api.php', { method: 'POST', body: formData }) + .then(function (r) { return r.json(); }) + .then(function (res) { + btn.textContent = 'Simpan Titik'; + btn.disabled = false; + if (res.ok) { + showToast('Titik jalan rusak disimpan!', 'success'); + closeRusakModal(); + var id = res.id; + var nama = res.nama || pelapor; + var layer = makeRusakLayer(id, nama, tanggal, lat, lng, ket, fotoBase64, 'recent', false); + rusakLayers[id] = layer; + rusakData[id] = { id: id, nama: nama, tgl: tanggal, lat: lat, lng: lng, ket: ket, foto: fotoBase64, age: 'recent', recurring: false }; + if (res.tr !== undefined) updateStats(res); + cancelDraw(); + } else { + showToast('Gagal: ' + (res.msg || 'unknown'), 'error'); + } + }) + .catch(function () { + btn.textContent = 'Simpan Titik'; + btn.disabled = false; + showToast('Koneksi gagal!', 'error'); + }); +} diff --git a/SIG2_jalantanah/assets/jalan.css b/SIG2_jalantanah/assets/jalan.css new file mode 100644 index 0000000..7898d72 --- /dev/null +++ b/SIG2_jalantanah/assets/jalan.css @@ -0,0 +1,11 @@ +/* SIG Jalan Tanah — shared layout tokens (main styles in index.php) */ +:root { + --bg: #f1f5f9; + --white: #fff; + --border: #e2e8f0; + --text: #0f172a; + --text2: #475569; + --text3: #94a3b8; + --blue: #1d4ed8; + --sidebar-w: 320px; +} diff --git a/SIG2_jalantanah/db2.php b/SIG2_jalantanah/db2.php new file mode 100644 index 0000000..e254929 --- /dev/null +++ b/SIG2_jalantanah/db2.php @@ -0,0 +1,7 @@ +connect_error) { + die("Koneksi gagal: " . $conn->connect_error); +} +$conn->set_charset("utf8"); +?> \ No newline at end of file diff --git a/SIG2_jalantanah/includes/confirm-modal.php b/SIG2_jalantanah/includes/confirm-modal.php new file mode 100644 index 0000000..a3febb2 --- /dev/null +++ b/SIG2_jalantanah/includes/confirm-modal.php @@ -0,0 +1,115 @@ + + + diff --git a/SIG2_jalantanah/includes/stats.php b/SIG2_jalantanah/includes/stats.php new file mode 100644 index 0000000..1bcd45d --- /dev/null +++ b/SIG2_jalantanah/includes/stats.php @@ -0,0 +1,14 @@ +query("SELECT COUNT(*) c FROM jalan")->fetch_assoc()['c']; + $nas = $conn->query("SELECT COUNT(*) c FROM jalan WHERE status_jalan='Jalan Nasional'")->fetch_assoc()['c']; + $pro = $conn->query("SELECT COUNT(*) c FROM jalan WHERE status_jalan='Jalan Provinsi'")->fetch_assoc()['c']; + $kab = $conn->query("SELECT COUNT(*) c FROM jalan WHERE status_jalan='Jalan Kabupaten'")->fetch_assoc()['c']; + $tp = $conn->query("SELECT COUNT(*) c FROM parsil")->fetch_assoc()['c']; + $shm = $conn->query("SELECT COUNT(*) c FROM parsil WHERE status_kepemilikan='SHM'")->fetch_assoc()['c']; + $hgb = $conn->query("SELECT COUNT(*) c FROM parsil WHERE status_kepemilikan='HGB'")->fetch_assoc()['c']; + $hgu = $conn->query("SELECT COUNT(*) c FROM parsil WHERE status_kepemilikan='HGU'")->fetch_assoc()['c']; + $hp = $conn->query("SELECT COUNT(*) c FROM parsil WHERE status_kepemilikan='HP'")->fetch_assoc()['c']; + $tr = $conn->query("SELECT COUNT(*) c FROM jalan_rusak")->fetch_assoc()['c']; + return compact('tj','nas','pro','kab','tp','shm','hgb','hgu','hp','tr'); +} diff --git a/SIG2_jalantanah/index.php b/SIG2_jalantanah/index.php new file mode 100644 index 0000000..268866d --- /dev/null +++ b/SIG2_jalantanah/index.php @@ -0,0 +1,1416 @@ + + + + + + +Sistem Informasi Infrastruktur Jalan — Pontianak + + + + + + + + + +
+ + + + +
+ +
+
+ +
Peta GIS Jalan & Kavling Tanah
+
+
+ + + + Input Baru + + + + + Lihat Peta + + +
+
+ +
+ + +
+ +
+

Legenda

+
Jalan
+
Jalan Nasional
+
Jalan Provinsi
+
Jalan Kabupaten
+ +
Kavling
+
SHM
+
HGB
+
HGU
+
HP
+ +
Jalan Rusak
+
< 1 bulan
+
3-12 bulan
+
> 1 tahun
+
⚡ Berulang rusak
+
+ + +
+

Tampilkan

+ + + +
+ + +
+

Peta Dasar

+
+ + +
+
+
+
+ +
+ + +
+
+
+
🛣
+
Edit Data
+ +
+
+
📋 Data
+
📐 Geometri
+
+ +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ + + +
+ + +
+
+
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SIG_miskin/gis/config.php b/SIG_miskin/gis/config.php new file mode 100644 index 0000000..31f9541 --- /dev/null +++ b/SIG_miskin/gis/config.php @@ -0,0 +1,14 @@ +set_charset('utf8mb4'); + +if ($conn->connect_error) { + http_response_code(500); + die(json_encode(['error' => 'Database connection failed: ' . $conn->connect_error])); +} \ No newline at end of file diff --git a/SIG_miskin/gis/dashboard.php b/SIG_miskin/gis/dashboard.php new file mode 100644 index 0000000..6d9b9a4 --- /dev/null +++ b/SIG_miskin/gis/dashboard.php @@ -0,0 +1,300 @@ +query("SELECT COUNT(*) c FROM penduduk_miskin")->fetch_assoc()['c']; +$total_anggota = $conn->query("SELECT SUM(jumlah_anggota) c FROM penduduk_miskin")->fetch_assoc()['c'] ?? 0; +$total_ibadah = $conn->query("SELECT COUNT(*) c FROM rumah_ibadah")->fetch_assoc()['c']; +$terdaftar = $conn->query("SELECT COUNT(*) c FROM penduduk_miskin WHERE rumah_ibadah_id IS NOT NULL")->fetch_assoc()['c']; +$belum = $total_penduduk - $terdaftar; + +$per_ibadah = $conn->query("SELECT ri.nama, COUNT(pm.id) AS jumlah FROM rumah_ibadah ri LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id=ri.id GROUP BY ri.id ORDER BY jumlah DESC"); +$jenis_ibadah= $conn->query("SELECT jenis, COUNT(*) c FROM rumah_ibadah GROUP BY jenis ORDER BY c DESC"); +$jenis_colors= ['Masjid'=>'blue','Gereja'=>'violet','Pura'=>'amber','Vihara'=>'green','Kelenteng'=>'cyan','Lainnya'=>'gray']; +$rows_ibadah=[]; $max=1; +while($r=$per_ibadah->fetch_assoc()){ $rows_ibadah[]=$r; if($r['jumlah']>$max) $max=$r['jumlah']; } +$rows_jenis=[]; +while($r=$jenis_ibadah->fetch_assoc()) $rows_jenis[]=$r; + +// Pagination logic for latest assistance distributions +$limit = 15; +$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; +$offset = ($page - 1) * $limit; + +// Count total records +$total_bantuan = $conn->query("SELECT COUNT(*) c FROM riwayat_bantuan")->fetch_assoc()['c']; +$total_pages = ceil($total_bantuan / $limit); + +$recent_bantuan = $conn->query(" + SELECT rb.*, pm.nama_kk, ri.nama as nama_ibadah, ri2.nama as ibadah_terdaftar + FROM riwayat_bantuan rb + JOIN penduduk_miskin pm ON rb.penduduk_id = pm.id + LEFT JOIN rumah_ibadah ri ON rb.rumah_ibadah_id = ri.id + LEFT JOIN rumah_ibadah ri2 ON pm.rumah_ibadah_id = ri2.id + ORDER BY rb.tanggal_pemberian DESC, rb.id DESC + LIMIT $limit OFFSET $offset +"); +$current = basename($_SERVER['PHP_SELF']); +?> + + + + +Dashboard — Pemetaan Kemiskinan Terpadu + + + + + + + + +
+
+

Dashboard

+ Diperbarui WIB +
+
+
+
+
KK Penduduk Miskin
+
+
Kepala keluarga terdata
+
+
+
Total Jiwa
+
+
Anggota keluarga
+
+
+
Rumah Ibadah
+
+
Titik ibadah aktif
+
+
+
Terdaftar Radius
+
+
KK dalam cakupan
+
+
+
Belum Terdaftar
+
+
Di luar radius ibadah
+
+
+ +
+
+
+

Binaan per Rumah Ibadah

+

Jumlah KK dalam radius tiap tempat ibadah

+
+
+ 0?round($r['jumlah']/$max*100):0; ?> +
+
+
+
+
+ +
+
+
+
+

Sebaran Jenis Rumah Ibadah

+

Komposisi berdasarkan jenis bangunan

+
+
+ +
+ + tempat +
+ +
+
+
+ + + + + + +
+
+

Log Pembagian Bantuan Terbaru

+

Aktivitas penyaluran bantuan sosial kepada keluarga binaan

+
+
+ + + + + + + + + + + + num_rows > 0): ?> + fetch_assoc()): ?> + + + + + + + + + + + + + + +
Nama Kepala KeluargaLokasi Tempat IbadahJenis BantuanTanggal DiberikanKeterangan
+ + +
+ +
+ + + + + + + ❌ Belum Terdaftar + +
Belum ada riwayat pembagian bantuan.
+
+ 1): ?> + + +
+
+
+ diff --git a/SIG_miskin/gis/data_ibadah.php b/SIG_miskin/gis/data_ibadah.php new file mode 100644 index 0000000..b96ec0f --- /dev/null +++ b/SIG_miskin/gis/data_ibadah.php @@ -0,0 +1,175 @@ +real_escape_string($cari); + $where = "WHERE ri.nama LIKE '%$c%' OR ri.alamat LIKE '%$c%' OR ri.pic LIKE '%$c%' OR ri.jenis LIKE '%$c%'"; +} + +$total_rows = $conn->query("SELECT COUNT(*) c FROM rumah_ibadah ri $where")->fetch_assoc()['c']; +$total_pages = max(1, ceil($total_rows / $per_page)); +$page = min($page, $total_pages); +$offset = ($page - 1) * $per_page; + +$data = $conn->query(" + SELECT ri.*, COUNT(pm.id) AS jumlah_binaan, SUM(pm.jumlah_anggota) AS total_jiwa + FROM rumah_ibadah ri + LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id = ri.id + $where + GROUP BY ri.id ORDER BY ri.nama ASC + LIMIT $per_page OFFSET $offset +"); + +$all = $conn->query("SELECT ri.nama,ri.jenis,ri.alamat,ri.pic,ri.no_wa,ri.radius,COUNT(pm.id) AS jumlah_binaan,SUM(pm.jumlah_anggota) AS total_jiwa FROM rumah_ibadah ri LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id=ri.id GROUP BY ri.id ORDER BY ri.nama"); +$all_rows=[]; +while($r=$all->fetch_assoc()) $all_rows[]=$r; + +$jenis_colors=['Masjid'=>'blue','Gereja'=>'violet','Pura'=>'amber','Vihara'=>'green','Kelenteng'=>'cyan','Lainnya'=>'gray']; +$current = basename($_SERVER['PHP_SELF']); + +function pag_url($p,$cari){ + $q=$cari!==''?'&cari='.urlencode($cari):''; + return '?page='.$p.$q; +} +?> + + + + +Rumah Ibadah — Pemetaan Kemiskinan Terpadu + + + + + + + +
+

Rumah Ibadah

Total tempat ibadah
+
+
+
+

Daftar Rumah Ibadah

+

Halaman dari · data total

+
+ +
+ +
+
+
+
+ + +
+ + ✕ Reset +
+ dari +
+ + + + + + + fetch_assoc()): + $col=$jenis_colors[$r['jenis']]??'gray'; ?> + + + + + + + + + + + + + +
#NamaJenisAlamatPICNo. WARadiusBinaan (KK)Total Jiwa
m KK
+ + +
+
+
+ + + \ No newline at end of file diff --git a/SIG_miskin/gis/data_penduduk.php b/SIG_miskin/gis/data_penduduk.php new file mode 100644 index 0000000..8ec22b9 --- /dev/null +++ b/SIG_miskin/gis/data_penduduk.php @@ -0,0 +1,207 @@ +real_escape_string($cari); + $where = "WHERE pm.nama_kk LIKE '%$c%' OR pm.alamat LIKE '%$c%' OR ri.nama LIKE '%$c%'"; +} + +$total_rows = $conn->query("SELECT COUNT(*) c FROM penduduk_miskin pm LEFT JOIN rumah_ibadah ri ON ri.id=pm.rumah_ibadah_id $where")->fetch_assoc()['c']; +$total_pages= max(1, ceil($total_rows / $per_page)); +$page = min($page, $total_pages); +$offset = ($page - 1) * $per_page; + +$data = $conn->query(" + SELECT pm.*, ri.nama AS nama_ibadah, ri.jenis AS jenis_ibadah, + (SELECT GROUP_CONCAT(CONCAT(rb.jenis_bantuan, ' (', DATE_FORMAT(rb.tanggal_pemberian, '%d/%m/%Y'), ')') ORDER BY rb.tanggal_pemberian DESC SEPARATOR ', ') + FROM riwayat_bantuan rb WHERE rb.penduduk_id = pm.id) AS riwayat_bantuan_list + FROM penduduk_miskin pm + LEFT JOIN rumah_ibadah ri ON ri.id = pm.rumah_ibadah_id + $where + ORDER BY pm.id ASC + LIMIT $per_page OFFSET $offset +"); + +// Untuk download CSV — ambil semua tanpa limit +$all = $conn->query(" + SELECT pm.nama_kk, pm.jumlah_anggota, pm.alamat, + (SELECT GROUP_CONCAT(CONCAT(rb.jenis_bantuan, ' (', DATE_FORMAT(rb.tanggal_pemberian, '%d/%m/%Y'), ')') ORDER BY rb.tanggal_pemberian DESC SEPARATOR '; ') + FROM riwayat_bantuan rb WHERE rb.penduduk_id = pm.id) AS riwayat_bantuan_list, + ri.nama AS nama_ibadah, ri.jenis AS jenis_ibadah, pm.jarak_ke_ibadah, + IF(pm.rumah_ibadah_id IS NOT NULL,'Terdaftar','Belum Terdaftar') AS status, pm.lat, pm.lng + FROM penduduk_miskin pm + LEFT JOIN rumah_ibadah ri ON ri.id=pm.rumah_ibadah_id + ORDER BY pm.id ASC +"); +$all_rows = []; +while($r=$all->fetch_assoc()) $all_rows[]=$r; + +$jenis_colors=['Masjid'=>'blue','Gereja'=>'violet','Pura'=>'amber','Vihara'=>'green','Kelenteng'=>'cyan','Lainnya'=>'gray']; +$current = basename($_SERVER['PHP_SELF']); + +function pag_url($p, $cari) { + $q = $cari !== '' ? '&cari='.urlencode($cari) : ''; + return '?page='.$p.$q; +} +?> + + + + +Data Penduduk — Pemetaan Kemiskinan Terpadu + + + + + + + +
+

Data Penduduk Miskin

Total KK ditemukan
+
+
+
+

Daftar Penduduk Miskin

+

Menampilkan halaman dari · data total

+
+ +
+ +
+
+
+
+ + +
+ + ✕ Reset +
+ dari +
+ + + + + + + + + + + fetch_assoc()): ?> + + + + + + + + + + + + + +
#Nama KKAnggotaAlamatRiwayat BantuanRumah Ibadah TerdekatJarakStatusFoto
+ + $item): + if ($idx < 2): ?> + + + + lainnya + + + Belum ada bantuan + + Terdaftar' : 'Belum' ?>
+ + +
+
+
+ + + \ No newline at end of file diff --git a/SIG_miskin/gis/database.sql b/SIG_miskin/gis/database.sql new file mode 100644 index 0000000..f464a20 --- /dev/null +++ b/SIG_miskin/gis/database.sql @@ -0,0 +1,63 @@ +-- ============================================ +-- DATABASE: gis_penduduk_miskin +-- GIS Web App - Rumah Ibadah & Penduduk Miskin +-- ============================================ + +CREATE DATABASE IF NOT EXISTS gis_penduduk_miskin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE gis_penduduk_miskin; + +-- Tabel Rumah Ibadah +CREATE TABLE IF NOT EXISTS rumah_ibadah ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(200) NOT NULL, + jenis ENUM('Masjid','Gereja','Pura','Vihara','Kelenteng','Lainnya') DEFAULT 'Masjid', + alamat TEXT, + pic VARCHAR(150) COMMENT 'Person In Charge', + no_wa VARCHAR(20), + lat DOUBLE NOT NULL, + lng DOUBLE NOT NULL, + radius INT DEFAULT 500 COMMENT 'Radius dalam meter', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Tabel Penduduk Miskin +CREATE TABLE IF NOT EXISTS penduduk_miskin ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_kk VARCHAR(200) NOT NULL COMMENT 'Nama Kepala Keluarga', + jumlah_anggota INT NOT NULL DEFAULT 1 COMMENT 'Jumlah Anggota KK', + alamat TEXT, + lat DOUBLE NOT NULL, + lng DOUBLE NOT NULL, + rumah_ibadah_id INT NULL COMMENT 'Rumah ibadah terdekat (FK)', + jarak_ke_ibadah DOUBLE NULL COMMENT 'Jarak dalam meter ke rumah ibadah terdekat', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL +); + +-- Index untuk performa query geospasial +CREATE INDEX idx_rumah_ibadah_coords ON rumah_ibadah(lat, lng); +CREATE INDEX idx_penduduk_coords ON penduduk_miskin(lat, lng); +CREATE INDEX idx_penduduk_ibadah ON penduduk_miskin(rumah_ibadah_id); + +-- ===================== +-- DATA CONTOH (Pontianak) +-- ===================== + +INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, lat, lng, radius) VALUES +('Masjid Raya Mujahidin', 'Masjid', 'Jl. Ahmad Yani, Pontianak Selatan', 'H. Abdurrahman', '081234567890', -0.0397, 109.3319, 500), +('Masjid Jami Pontianak', 'Masjid', 'Jl. Tanjungpura, Pontianak Kota', 'H. Mukhlis', '081234567891', -0.0263, 109.3413, 500), +('GBI Pontianak', 'Gereja', 'Jl. Gajahmada, Pontianak Kota', 'Pdt. Stevanus', '081234567892', -0.0280, 109.3350, 500), +('Vihara Bodhisattva Karaniya Metta', 'Vihara', 'Jl. Khatulistiwa, Pontianak Utara', 'Bhikkhu Santoso', '081234567893', -0.0120, 109.3250, 500); + +INSERT INTO penduduk_miskin (nama_kk, jumlah_anggota, alamat, lat, lng) VALUES +('Budi Santoso', 4, 'Jl. Ahmad Yani Gg. 3, Pontianak', -0.0410, 109.3330), +('Siti Rahayu', 3, 'Jl. Ahmad Yani Gg. 5, Pontianak', -0.0380, 109.3310), +('Muhammad Amin', 5, 'Jl. Tanjungpura No. 12, Pontianak', -0.0255, 109.3420), +('Dewi Lestari', 2, 'Jl. Gajahmada No. 7, Pontianak', -0.0290, 109.3360), +('Hendra Kusuma', 6, 'Jl. Khatulistiwa Gg. 2, Pontianak', -0.0130, 109.3260), +('Yanti Wulandari', 3, 'Jl. Reformasi, Pontianak Utara', -0.0100, 109.3240), +('Agus Prasetyo', 4, 'Jl. Sultan Hamid, Pontianak Barat', -0.0450, 109.3200); + +-- Update rumah_ibadah_id dan jarak untuk data contoh (akan dihitung via PHP) diff --git a/SIG_miskin/gis/includes/confirm-modal.php b/SIG_miskin/gis/includes/confirm-modal.php new file mode 100644 index 0000000..c432368 --- /dev/null +++ b/SIG_miskin/gis/includes/confirm-modal.php @@ -0,0 +1,127 @@ + + + diff --git a/SIG_miskin/gis/includes/user-profile.php b/SIG_miskin/gis/includes/user-profile.php new file mode 100644 index 0000000..d71d56b --- /dev/null +++ b/SIG_miskin/gis/includes/user-profile.php @@ -0,0 +1,46 @@ + + +
+
+ +
diff --git a/SIG_miskin/gis/index.php b/SIG_miskin/gis/index.php new file mode 100644 index 0000000..36e9ead --- /dev/null +++ b/SIG_miskin/gis/index.php @@ -0,0 +1,2711 @@ +query("SELECT id,lat,lng,radius FROM rumah_ibadah"); + $ilist=[]; + while($r=$ib->fetch_assoc()) $ilist[]=$r; + $w=$pid?"WHERE id=".intval($pid):""; + $pd=$conn->query("SELECT id,lat,lng FROM penduduk_miskin $w"); + while($p=$pd->fetch_assoc()){ + $best=null;$bd=PHP_FLOAT_MAX; + foreach($ilist as $i){ + $d=haversine($p['lat'],$p['lng'],$i['lat'],$i['lng']); + if($d<=$i['radius']&&$d<$bd){$bd=$d;$best=$i['id'];} + } + if($best){ + $s=$conn->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id=?,jarak_ke_ibadah=? WHERE id=?"); + $s->bind_param('idi',$best,$bd,$p['id']); + } else { + $s=$conn->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id=NULL,jarak_ke_ibadah=NULL WHERE id=?"); + $s->bind_param('i',$p['id']); + } + $s->execute(); + } + } + + // ── GET ALL ──────────────────────────────── + if($action==='get_all'){ + $ri=[]; + $r=$conn->query("SELECT * FROM rumah_ibadah ORDER BY nama"); + while($row=$r->fetch_assoc()) $ri[]=$row; + + $pm=[]; + $r2=$conn->query("SELECT p.*,i.nama as nama_ibadah,i.jenis as jenis_ibadah + FROM penduduk_miskin p + LEFT JOIN rumah_ibadah i ON p.rumah_ibadah_id=i.id + ORDER BY p.nama_kk"); + while($row=$r2->fetch_assoc()) $pm[]=$row; + + $stats=[ + 'total_ibadah'=>(int)$conn->query("SELECT COUNT(*) c FROM rumah_ibadah")->fetch_assoc()['c'], + 'total_penduduk'=>(int)$conn->query("SELECT COUNT(*) c FROM penduduk_miskin")->fetch_assoc()['c'], + 'total_jiwa'=>(int)($conn->query("SELECT COALESCE(SUM(jumlah_anggota),0) c FROM penduduk_miskin")->fetch_assoc()['c']), + 'terdaftar'=>(int)$conn->query("SELECT COUNT(*) c FROM penduduk_miskin WHERE rumah_ibadah_id IS NOT NULL")->fetch_assoc()['c'], + 'belum'=>(int)$conn->query("SELECT COUNT(*) c FROM penduduk_miskin WHERE rumah_ibadah_id IS NULL")->fetch_assoc()['c'], + ]; + echo json_encode(['rumah_ibadah'=>$ri,'penduduk'=>$pm,'stats'=>$stats]); + exit; + } + + // ── ADD IBADAH ───────────────────────────── + if($action==='add_ibadah'&&$_SERVER['REQUEST_METHOD']==='POST'){ + $d=json_decode(file_get_contents('php://input'),true)??[]; + $nama=$conn->real_escape_string($d['nama']??''); + $jenis=$conn->real_escape_string($d['jenis']??'Masjid'); + $alamat=$conn->real_escape_string($d['alamat']??''); + $pic=$conn->real_escape_string($d['pic']??''); + $no_wa=$conn->real_escape_string($d['no_wa']??''); + $lat=floatval($d['lat']??0); + $lng=floatval($d['lng']??0); + $radius=max(500,intval($d['radius']??500)); + if(!$nama||!$lat||!$lng){echo json_encode(['error'=>'Data tidak lengkap']);exit;} + $s=$conn->prepare("INSERT INTO rumah_ibadah(nama,jenis,alamat,pic,no_wa,lat,lng,radius) VALUES(?,?,?,?,?,?,?,?)"); + $s->bind_param('sssssddi',$nama,$jenis,$alamat,$pic,$no_wa,$lat,$lng,$radius); + if($s->execute()){ + $id=$conn->insert_id; + recalc($conn); + $row=$conn->query("SELECT * FROM rumah_ibadah WHERE id=$id")->fetch_assoc(); + echo json_encode(['success'=>true,'id'=>$id,'data'=>$row]); + } else echo json_encode(['error'=>$conn->error]); + exit; + } + + // ── EDIT IBADAH ──────────────────────────── + if($action==='edit_ibadah'&&$_SERVER['REQUEST_METHOD']==='POST'){ + $d=json_decode(file_get_contents('php://input'),true)??[]; + $id=intval($d['id']??0); + $nama=$conn->real_escape_string($d['nama']??''); + $jenis=$conn->real_escape_string($d['jenis']??'Masjid'); + $alamat=$conn->real_escape_string($d['alamat']??''); + $pic=$conn->real_escape_string($d['pic']??''); + $no_wa=$conn->real_escape_string($d['no_wa']??''); + $lat=floatval($d['lat']??0); + $lng=floatval($d['lng']??0); + $radius=max(500,intval($d['radius']??500)); + $s=$conn->prepare("UPDATE rumah_ibadah SET nama=?,jenis=?,alamat=?,pic=?,no_wa=?,lat=?,lng=?,radius=? WHERE id=?"); + $s->bind_param('sssssddii',$nama,$jenis,$alamat,$pic,$no_wa,$lat,$lng,$radius,$id); + if($s->execute()){recalc($conn);echo json_encode(['success'=>true]);} + else echo json_encode(['error'=>$conn->error]); + exit; + } + + // ── DELETE IBADAH ────────────────────────── + if($action==='delete_ibadah'){ + $d=json_decode(file_get_contents('php://input'),true)??[]; + $id=intval($d['id']??$_GET['id']??0); + $s=$conn->prepare("DELETE FROM rumah_ibadah WHERE id=?"); + $s->bind_param('i',$id); + if($s->execute()){recalc($conn);echo json_encode(['success'=>true]);} + else echo json_encode(['error'=>$conn->error]); + exit; + } + + // ── UPLOAD FOTO RUMAH ────────────────────── +if($action==='upload_foto'&&$_SERVER['REQUEST_METHOD']==='POST'){ + $id = intval($_POST['id'] ?? 0); + if(!$id || empty($_FILES['foto']['tmp_name'])){ + echo json_encode(['error'=>'Data tidak valid']); exit; + } + $ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION)); + $allowed = ['jpg','jpeg','png','webp']; + if(!in_array($ext, $allowed)){ echo json_encode(['error'=>'Format tidak didukung']); exit; } + $filename = 'foto_'.$id.'_'.time().'.'.$ext; + $dest = __DIR__.'/uploads/foto_rumah/'.$filename; + if(move_uploaded_file($_FILES['foto']['tmp_name'], $dest)){ + // Hapus foto lama jika ada + $old = $conn->query("SELECT foto_rumah FROM penduduk_miskin WHERE id=$id")->fetch_assoc(); + if($old['foto_rumah'] && file_exists(__DIR__.'/'.$old['foto_rumah'])) + unlink(__DIR__.'/'.$old['foto_rumah']); + $path = 'uploads/foto_rumah/'.$filename; + $conn->query("UPDATE penduduk_miskin SET foto_rumah='$path' WHERE id=$id"); + echo json_encode(['success'=>true,'path'=>$path]); + } else echo json_encode(['error'=>'Gagal upload']); + exit; +} +//ADD PENDUDUK +if($action==='add_penduduk'&&$_SERVER['REQUEST_METHOD']==='POST'){ + // Ambil dari $_POST (bukan json), karena ada file upload + $nama_kk = $conn->real_escape_string($_POST['nama_kk'] ?? ''); + $jml = intval($_POST['jumlah_anggota'] ?? 1); + $alamat = $conn->real_escape_string($_POST['alamat'] ?? ''); + $lat = floatval($_POST['lat'] ?? 0); + $lng = floatval($_POST['lng'] ?? 0); + $jenis_bantuan = $conn->real_escape_string($_POST['jenis_bantuan'] ?? ''); + $tanggal_bantuan = $conn->real_escape_string($_POST['tanggal_bantuan'] ?? ''); + if(!$nama_kk||!$lat||!$lng){echo json_encode(['error'=>'Data tidak lengkap']);exit;} + + $s=$conn->prepare("INSERT INTO penduduk_miskin(nama_kk,jumlah_anggota,alamat,lat,lng,jenis_bantuan,tanggal_bantuan) VALUES(?,?,?,?,?,?,?)"); + $tanggal_bantuan_val = $tanggal_bantuan ? $tanggal_bantuan : null; + $s->bind_param('sisddss',$nama_kk,$jml,$alamat,$lat,$lng,$jenis_bantuan,$tanggal_bantuan_val); + if($s->execute()){ + $id=$conn->insert_id; + // Log to riwayat_bantuan if provided + if($jenis_bantuan && $tanggal_bantuan_val) { + $conn->query("INSERT INTO riwayat_bantuan (penduduk_id, jenis_bantuan, tanggal_pemberian, keterangan) VALUES ($id, '$jenis_bantuan', '$tanggal_bantuan_val', 'Bantuan pertama saat pendaftaran KK')"); + } + // Proses upload foto jika ada + if(!empty($_FILES['foto']['tmp_name'])){ + $ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION)); + $allowed = ['jpg','jpeg','png','webp']; + if(in_array($ext,$allowed)){ + $filename = 'foto_'.$id.'_'.time().'.'.$ext; + $dest = __DIR__.'/uploads/foto_rumah/'.$filename; + if(move_uploaded_file($_FILES['foto']['tmp_name'],$dest)){ + $path = 'uploads/foto_rumah/'.$filename; + $conn->query("UPDATE penduduk_miskin SET foto_rumah='$path' WHERE id=$id"); + } + } + } + recalc($conn,$id); + $row=$conn->query("SELECT p.*,i.nama as nama_ibadah,i.jenis as jenis_ibadah + FROM penduduk_miskin p LEFT JOIN rumah_ibadah i ON p.rumah_ibadah_id=i.id + WHERE p.id=$id")->fetch_assoc(); + echo json_encode(['success'=>true,'id'=>$id,'data'=>$row]); + } else echo json_encode(['error'=>$conn->error]); + exit; +} + + // ── EDIT PENDUDUK ────────────────────────── + if($action==='edit_penduduk'&&$_SERVER['REQUEST_METHOD']==='POST'){ + $id=intval($_POST['id']??0); + $nama_kk=$conn->real_escape_string($_POST['nama_kk']??''); + $jml=intval($_POST['jumlah_anggota']??1); + $alamat=$conn->real_escape_string($_POST['alamat']??''); + $lat=floatval($_POST['lat']??0); + $lng=floatval($_POST['lng']??0); + $jenis_bantuan = $conn->real_escape_string($_POST['jenis_bantuan'] ?? ''); + $tanggal_bantuan = $conn->real_escape_string($_POST['tanggal_bantuan'] ?? ''); + + $s=$conn->prepare("UPDATE penduduk_miskin SET nama_kk=?,jumlah_anggota=?,alamat=?,lat=?,lng=?,jenis_bantuan=?,tanggal_bantuan=? WHERE id=?"); + $tanggal_bantuan_val = $tanggal_bantuan ? $tanggal_bantuan : null; + $s->bind_param('sisddssi',$nama_kk,$jml,$alamat,$lat,$lng,$jenis_bantuan,$tanggal_bantuan_val,$id); + + // Proses upload foto jika ada + if(!empty($_FILES['foto']['tmp_name'])){ + $ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION)); + $allowed = ['jpg','jpeg','png','webp']; + if(in_array($ext,$allowed)){ + $filename = 'foto_'.$id.'_'.time().'.'.$ext; + $dest = __DIR__.'/uploads/foto_rumah/'.$filename; + if(move_uploaded_file($_FILES['foto']['tmp_name'],$dest)){ + // Hapus foto lama jika ada + $old = $conn->query("SELECT foto_rumah FROM penduduk_miskin WHERE id=$id")->fetch_assoc(); + if($old['foto_rumah'] && file_exists(__DIR__.'/'.$old['foto_rumah'])) unlink(__DIR__.'/'.$old['foto_rumah']); + $path = 'uploads/foto_rumah/'.$filename; + $conn->query("UPDATE penduduk_miskin SET foto_rumah='$path' WHERE id=$id"); + } + } + } + if($s->execute()){ + // Log to riwayat_bantuan if new combination + if($jenis_bantuan && $tanggal_bantuan_val) { + $check = $conn->query("SELECT id FROM riwayat_bantuan WHERE penduduk_id=$id AND jenis_bantuan='$jenis_bantuan' AND tanggal_pemberian='$tanggal_bantuan_val'")->num_rows; + if ($check == 0) { + $conn->query("INSERT INTO riwayat_bantuan (penduduk_id, jenis_bantuan, tanggal_pemberian, keterangan) VALUES ($id, '$jenis_bantuan', '$tanggal_bantuan_val', 'Bantuan baru diinput lewat edit KK')"); + } + } + recalc($conn,$id); + echo json_encode(['success'=>true]); + } + else echo json_encode(['error'=>$conn->error]); + exit; + } + + // ── DELETE PENDUDUK ──────────────────────── + if($action==='delete_penduduk'){ + $d=json_decode(file_get_contents('php://input'),true)??[]; + $id=intval($d['id']??$_GET['id']??0); + $s=$conn->prepare("DELETE FROM penduduk_miskin WHERE id=?"); + $s->bind_param('i',$id); + if($s->execute()) echo json_encode(['success'=>true]); + else echo json_encode(['error'=>$conn->error]); + exit; + } + + // ── RECALCULATE ──────────────────────────── + if($action==='recalculate'){ + recalc($conn); + echo json_encode(['success'=>true]); + exit; + } + + // ── ADD BANTUAN ─────────────────────────── + if($action==='add_bantuan'&&$_SERVER['REQUEST_METHOD']==='POST'){ + $d=json_decode(file_get_contents('php://input'),true)??[]; + $pid=intval($d['penduduk_id']??0); + $jenis=$conn->real_escape_string($d['jenis_bantuan']??''); + $tanggal=$conn->real_escape_string($d['tanggal_pemberian']??''); + $ket=$conn->real_escape_string($d['keterangan']??''); + $ri_id=isset($d['rumah_ibadah_id']) && $d['rumah_ibadah_id'] !== '' ? intval($d['rumah_ibadah_id']) : null; + if(!$pid||!$jenis||!$tanggal){echo json_encode(['error'=>'Data tidak lengkap']);exit;} + + $s=$conn->prepare("INSERT INTO riwayat_bantuan(penduduk_id,jenis_bantuan,tanggal_pemberian,keterangan,rumah_ibadah_id) VALUES(?,?,?,?,?)"); + $s->bind_param('isssi',$pid,$jenis,$tanggal,$ket,$ri_id); + if($s->execute()){ + // Update jenis_bantuan & tanggal_bantuan di penduduk_miskin dengan yg terbaru + $conn->query("UPDATE penduduk_miskin SET jenis_bantuan='$jenis', tanggal_bantuan='$tanggal' WHERE id=$pid"); + echo json_encode(['success'=>true,'id'=>$conn->insert_id]); + } else echo json_encode(['error'=>$conn->error]); + exit; + } + + // ── GET BANTUAN (per penduduk) ──────────── + if($action==='get_bantuan'){ + $pid=intval($_GET['penduduk_id']??0); + $rows=[]; + $r=$conn->query("SELECT rb.*, ri.nama as nama_ibadah FROM riwayat_bantuan rb LEFT JOIN rumah_ibadah ri ON rb.rumah_ibadah_id=ri.id WHERE rb.penduduk_id=$pid ORDER BY rb.tanggal_pemberian DESC, rb.id DESC"); + while($row=$r->fetch_assoc()) $rows[]=$row; + echo json_encode($rows); + exit; + } + + // ── DELETE BANTUAN ──────────────────────── + if($action==='delete_bantuan'){ + $d=json_decode(file_get_contents('php://input'),true)??[]; + $id=intval($d['id']??$_GET['id']??0); + // Get penduduk_id before delete + $row=$conn->query("SELECT penduduk_id FROM riwayat_bantuan WHERE id=$id")->fetch_assoc(); + $s=$conn->prepare("DELETE FROM riwayat_bantuan WHERE id=?"); + $s->bind_param('i',$id); + if($s->execute()){ + // Update penduduk with latest remaining bantuan + if($row){ + $pid=$row['penduduk_id']; + $latest=$conn->query("SELECT jenis_bantuan,tanggal_pemberian FROM riwayat_bantuan WHERE penduduk_id=$pid ORDER BY tanggal_pemberian DESC LIMIT 1")->fetch_assoc(); + if($latest) $conn->query("UPDATE penduduk_miskin SET jenis_bantuan='".$conn->real_escape_string($latest['jenis_bantuan'])."', tanggal_bantuan='".$latest['tanggal_pemberian']."' WHERE id=$pid"); + else $conn->query("UPDATE penduduk_miskin SET jenis_bantuan=NULL, tanggal_bantuan=NULL WHERE id=$pid"); + } + echo json_encode(['success'=>true]); + } + else echo json_encode(['error'=>$conn->error]); + exit; + } + + echo json_encode(['error'=>'Action tidak dikenali']); + exit; +} +?> + + + + + +Pemetaan Kemiskinan Terpadu — Pontianak + + + + + + + + +
+ + + + +
+ +
+ +
Peta GIS Rumah Ibadah & Penduduk Miskin
+ + + +
+ + +
+
+ + +
+ + +
+ + +
+ + +
+

Radius Default

+
+ Jarak cakupan: + 500 m +
+ + +
+ + + +
+

Legenda

+
Masjid
+
Gereja
+
Pura
+
Vihara / Kelenteng
+
Penduduk — Terdaftar
+
Penduduk — Belum
+
Radius Ibadah
+
+ + +
+

Tampilkan

+ + + +
+
+ + + +
+

Edit Radius

+
+
+ Radius: + 500 m +
+ +
Min. 500 m · Geser untuk ubah ukuran lingkaran
+
+ + +
+
+ + + +
+
+

Data

+ +
+
+
Ibadah
+
Penduduk
+
+ + +
+
+
+
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/SIG_miskin/gis/login.php b/SIG_miskin/gis/login.php new file mode 100644 index 0000000..aac0f7a --- /dev/null +++ b/SIG_miskin/gis/login.php @@ -0,0 +1,200 @@ +real_escape_string($_POST['username'] ?? ''); + $role = $_POST['role'] ?? ''; + if ($role === 'admin') { + $row = $conn->query("SELECT * FROM users WHERE username='$u' AND role IN ('admin','user')")->fetch_assoc(); + } else { + $roleEsc = $conn->real_escape_string($role); + $row = $conn->query("SELECT * FROM users WHERE username='$u' AND role='$roleEsc'")->fetch_assoc(); + } + if ($row && password_verify($_POST['password'] ?? '', $row['password'])) { + session_regenerate_id(true); + $_SESSION['user'] = $row['id']; + $_SESSION['nama'] = $row['nama']; + $_SESSION['role'] = $row['role']; + header('Location: ' . ($row['role'] === 'superadmin' ? 'dashboard.php' : 'index.php')); + exit; + } + $err = 'Kredensial tidak valid untuk peran yang dipilih.'; +} +?> + + + + + +Masuk — Poverty Map + + + + + +
+ + + Portal WebGIS + + Poverty Map · Pontianak +
+ +
+
+
Sistem Informasi Geografis
+

Poverty Map

+

Pemetaan rumah ibadah dan penduduk miskin untuk perencanaan program binaan berbasis lokasi.

+
    +
  • Peta interaktif rumah ibadah dan kepala keluarga
  • +
  • Analisis radius cakupan binaan
  • +
  • Manajemen data untuk super admin
  • +
+
+ +
+ +
+
+ + + diff --git a/SIG_miskin/gis/logout.php b/SIG_miskin/gis/logout.php new file mode 100644 index 0000000..ee222ca --- /dev/null +++ b/SIG_miskin/gis/logout.php @@ -0,0 +1,5 @@ +query("DELETE FROM tempat WHERE id = $id"); + header('Location: index.php'); + exit; +} + +if (isset($_GET['action']) && $_GET['action'] === 'edit' && + isset($_GET['id'], $_GET['nama'], $_GET['nomor'], $_GET['status'])) { + $id = (int) $_GET['id']; + $nama = $conn->real_escape_string($_GET['nama']); + $nomor = $conn->real_escape_string($_GET['nomor']); + $status = $conn->real_escape_string($_GET['status']); + $conn->query("UPDATE tempat SET nama_tempat='$nama', nomor_tempat='$nomor', status='$status' WHERE id=$id"); + header('Location: index.php'); + exit; +} + +if (isset($_GET['action']) && $_GET['action'] === 'update_pos' && + isset($_GET['id'], $_GET['lat'], $_GET['lng'])) { + $id = (int) $_GET['id']; + $lat = (float) $_GET['lat']; + $lng = (float) $_GET['lng']; + $result = $conn->query("UPDATE tempat SET latitude='$lat', longitude='$lng' WHERE id=$id"); + header('Content-Type: text/plain'); + echo ($result) ? 'ok' : 'error: ' . $conn->error; + exit; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save'])) { + $nama = $conn->real_escape_string($_POST['nama']); + $nomor = $conn->real_escape_string($_POST['nomor']); + $status = $conn->real_escape_string($_POST['status']); + $lat = (float) $_POST['lat']; + $lng = (float) $_POST['lng']; + $sql = "INSERT INTO tempat (nama_tempat, nomor_tempat, status, latitude, longitude) + VALUES ('$nama','$nomor','$status','$lat','$lng')"; + if ($conn->query($sql)) { + header('Location: index.php?saved=1'); + exit; + } + header('Location: tambah.php?err=' . urlencode($conn->error)); + exit; +} diff --git a/SIG_spbu/assets/map-base.js b/SIG_spbu/assets/map-base.js new file mode 100644 index 0000000..53eeb55 --- /dev/null +++ b/SIG_spbu/assets/map-base.js @@ -0,0 +1,40 @@ +function initBaseLayers(map) { + var baseMap = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap', maxZoom: 19 + }); + var baseSat = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '© Esri', maxZoom: 19 + }); + baseMap.addTo(map); + + document.getElementById('base-map').addEventListener('click', function() { + if (map.hasLayer(baseSat)) map.removeLayer(baseSat); + if (!map.hasLayer(baseMap)) baseMap.addTo(map); + this.classList.add('active'); + document.getElementById('base-sat').classList.remove('active'); + }); + document.getElementById('base-sat').addEventListener('click', function() { + if (map.hasLayer(baseMap)) map.removeLayer(baseMap); + if (!map.hasLayer(baseSat)) baseSat.addTo(map); + this.classList.add('active'); + document.getElementById('base-map').classList.remove('active'); + }); +} + +function makeSpbuIcon(color, size) { + size = size || 18; + var h = size / 2; + return L.divIcon({ + className: '', + html: '
', + iconSize: [size, size], iconAnchor: [h, h], popupAnchor: [0, -12] + }); +} + +function showToast(msg) { + var el = document.getElementById('toast'); + if (!el) return; + el.textContent = msg; + el.classList.add('show'); + setTimeout(function() { el.classList.remove('show'); }, 3000); +} diff --git a/SIG_spbu/assets/spbu.css b/SIG_spbu/assets/spbu.css new file mode 100644 index 0000000..9052219 --- /dev/null +++ b/SIG_spbu/assets/spbu.css @@ -0,0 +1,234 @@ +:root { + --bg: #f1f5f9; --white: #fff; --border: #e2e8f0; + --text: #0f172a; --text2: #475569; --text3: #94a3b8; + --blue: #1d4ed8; --blue-lt: #eff6ff; + --green: #15803d; --green-lt: #f0fdf4; + --red: #b91c1c; --red-lt: #fef2f2; + --sidebar-w: 320px; +} +* { box-sizing: border-box; margin: 0; padding: 0; } +html, body { height: 100%; font-family: 'DM Sans', sans-serif; color: var(--text); background: var(--bg); overflow: hidden; } +body { display: flex; } + +#sidebar { + width: var(--sidebar-w); min-width: var(--sidebar-w); + background: var(--white); border-right: 1px solid var(--border); + display: flex; flex-direction: column; z-index: 200; +} +.sb-head { + padding: 18px 16px 14px; border-bottom: 1px solid var(--border); + background: var(--white); color: var(--text); +} +.sb-head-top { display: flex; align-items: center; gap: 11px; margin-bottom: 12px; } +.sb-logo { + width: 38px; height: 38px; border-radius: 10px; + background: linear-gradient(135deg, var(--blue), #60a5fa); + display: flex; align-items: center; justify-content: center; flex-shrink: 0; + box-shadow: 0 2px 8px rgba(29,78,216,.25); +} +.sb-logo svg { width: 18px; height: 18px; fill: #fff; } +.sb-title h1 { font-size: 14px; font-weight: 700; color: var(--text); } +.sb-title p { font-size: 10.5px; color: var(--text3); margin-top: 2px; } +.sb-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; } +.sb-stat { + background: var(--bg); border: 1px solid var(--border); + border-radius: 8px; padding: 8px 6px; text-align: center; +} +.sb-stat b { display: block; font-size: 16px; font-weight: 800; font-family: 'DM Mono', monospace; line-height: 1; } +.sb-stat span { font-size: 8.5px; color: var(--text3); margin-top: 4px; display: block; text-transform: uppercase; letter-spacing: .3px; font-weight: 600; } +.sb-stat.total b { color: var(--blue); } +.sb-stat.open b { color: var(--green); } +.sb-stat.closed b{ color: var(--red); } + +.sb-section-lbl { + font-size: 9.5px; font-weight: 700; color: var(--text3); + text-transform: uppercase; letter-spacing: .8px; padding: 10px 8px 4px; +} +.sb-item { + display: flex; align-items: center; gap: 9px; padding: 8px 10px; + border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500; + color: var(--text2); transition: all .15s; border: 1px solid transparent; text-decoration: none; +} +.sb-item:hover { background: var(--bg); color: var(--text); } +.sb-item.active { background: var(--blue-lt); color: var(--blue); border-color: #bfdbfe; font-weight: 600; } +.sb-item .ico { font-size: 16px; width: 20px; text-align: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; } + +.sb-body { flex: 1; overflow: hidden; display: flex; flex-direction: column; } +.sb-search { padding: 12px; border-bottom: 1px solid var(--border); } +.sb-search input { + width: 100%; padding: 9px 12px 9px 34px; + border: 1px solid var(--border); border-radius: 8px; font-size: 13px; + outline: none; font-family: inherit; background: var(--bg); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='%2394a3b8' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.099zM6.5 12a5.5 5.5 0 1 1 0-11 5.5 5.5 0 0 1 0 11z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; background-position: 11px center; +} +.sb-search input:focus { border-color: var(--blue); background-color: #fff; } + +.sb-filter { display: flex; padding: 0 12px 10px; gap: 5px; } +.sb-filter button { + flex: 1; padding: 6px 4px; font-size: 11px; font-weight: 600; + border: 1px solid var(--border); border-radius: 6px; background: var(--white); + color: var(--text2); cursor: pointer; font-family: inherit; transition: all .15s; +} +.sb-filter button.active { background: var(--blue); color: #fff; border-color: var(--blue); } + +.sb-list-head { + padding: 0 14px 8px; display: flex; justify-content: space-between; align-items: center; + font-size: 11px; color: var(--text3); font-weight: 600; text-transform: uppercase; letter-spacing: .4px; +} +.sb-list { flex: 1; overflow-y: auto; } +.sb-list::-webkit-scrollbar { width: 4px; } +.sb-list::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; } + +.spbu-item { + padding: 11px 14px; border-bottom: 1px solid #f1f5f9; cursor: pointer; + display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: start; + transition: background .12s; +} +.spbu-item:hover { background: #f8fafc; } +.spbu-item.active { background: var(--blue-lt); border-left: 3px solid var(--blue); padding-left: 11px; } +.spbu-dot { + width: 10px; height: 10px; border-radius: 50%; margin-top: 4px; + border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.1); +} +.spbu-nama { font-size: 13px; font-weight: 600; line-height: 1.3; } +.spbu-meta { font-size: 11px; color: var(--text3); margin-top: 3px; font-family: 'DM Mono', monospace; } +.spbu-badge { font-size: 10px; font-weight: 700; padding: 3px 7px; border-radius: 5px; white-space: nowrap; } +.badge-green { background: var(--green-lt); color: var(--green); } +.badge-red { background: var(--red-lt); color: var(--red); } +.no-result { padding: 32px 16px; text-align: center; color: var(--text3); font-size: 13px; } + +.sb-foot { + padding: 10px 14px; border-top: 1px solid var(--border); + font-size: 11px; color: var(--text3); display: flex; justify-content: space-between; +} +.sb-foot b { color: var(--text2); } + +/* Add form in sidebar */ +.form-panel { padding: 16px; flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 0; } +.form-panel h3 { font-size: 14px; font-weight: 700; margin-bottom: 4px; } +.form-panel .form-desc { font-size: 12px; color: var(--text2); line-height: 1.5; margin-bottom: 16px; } +.form-group { margin-bottom: 14px; } +.form-group label { + display: block; font-size: 11px; font-weight: 700; color: var(--text3); + text-transform: uppercase; letter-spacing: .4px; margin-bottom: 5px; +} +.form-group input, .form-group select { + width: 100%; padding: 10px 12px; border: 1px solid var(--border); + border-radius: 8px; font-size: 14px; font-family: inherit; outline: none; background: #fff; + transition: border-color .15s, box-shadow .15s; +} +.form-group input:focus, .form-group select:focus { + border-color: var(--blue); box-shadow: 0 0 0 3px rgba(29,78,216,.1); +} +.form-group input[readonly] { background: var(--bg); color: var(--text2); font-family: 'DM Mono', monospace; font-size: 12px; } +.coord-box { + display: grid; grid-template-columns: 1fr 1fr; gap: 8px; +} +.coord-box input { text-align: center; } +.form-alert { + padding: 10px 12px; border-radius: 8px; font-size: 12px; margin-bottom: 14px; + background: var(--red-lt); color: var(--red); border: 1px solid #fecaca; +} +.form-hint { + font-size: 11px; color: var(--text3); padding: 10px 12px; + background: var(--bg); border-radius: 8px; border: 1px solid var(--border); + line-height: 1.5; margin-top: auto; +} +.form-hint.active { border-color: #93c5fd; background: var(--blue-lt); color: var(--blue); } +.form-actions { display: flex; gap: 8px; margin-top: 16px; } +.btn-primary { + flex: 1; padding: 11px; background: var(--blue); color: #fff; border: none; + border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; +} +.btn-primary:hover { background: #1e40af; } +.btn-primary:disabled { opacity: .5; cursor: not-allowed; } +.btn-secondary { + padding: 11px 14px; background: #f1f5f9; color: var(--text2); border: 1px solid var(--border); + border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; +} + +#map-area { flex: 1; position: relative; min-width: 0; display: flex; flex-direction: column; height: 100%; } +#map { flex: 1; min-height: 0; width: 100%; position: relative; } + +#map-controls { + position: absolute; top: 14px; right: 14px; z-index: 800; + background: var(--white); border: 1px solid var(--border); + border-radius: 10px; box-shadow: 0 2px 12px rgba(0,0,0,.1); + width: 190px; overflow: hidden; font-size: 12px; +} +.mc-section { padding: 10px 12px; border-bottom: 1px solid var(--border); } +.mc-section:last-child { border-bottom: none; } +.mc-label { + font-size: 10px; font-weight: 700; color: var(--text3); + text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px; +} +.mc-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; cursor: pointer; color: var(--text2); } +.mc-row:last-child { margin-bottom: 0; } +.mc-row input { accent-color: var(--blue); } +.mc-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; } +.mc-base { display: flex; gap: 5px; } +.mc-base-btn { + flex: 1; padding: 7px 4px; border: 1px solid var(--border); border-radius: 6px; + background: var(--white); font-size: 11px; font-weight: 600; color: var(--text2); + cursor: pointer; font-family: inherit; transition: all .15s; +} +.mc-base-btn.active { background: var(--blue); color: #fff; border-color: var(--blue); } + +#map-hint { + position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%); + z-index: 800; background: rgba(15,23,42,.88); color: #fff; + padding: 8px 16px; border-radius: 8px; font-size: 12px; pointer-events: none; +} +#toast { + position: fixed; bottom: 20px; right: 20px; z-index: 9000; + background: #0f172a; color: #fff; padding: 11px 16px; border-radius: 8px; + font-size: 13px; display: none; box-shadow: 0 4px 16px rgba(0,0,0,.2); +} +#toast.show { display: block; } + +#edit-modal { + display: none; position: fixed; inset: 0; z-index: 9999; + background: rgba(15,23,42,.4); align-items: center; justify-content: center; padding: 20px; +} +#edit-modal.active { display: flex; } +#edit-box { + background: #fff; border-radius: 12px; padding: 24px; + width: 100%; max-width: 380px; box-shadow: 0 16px 48px rgba(0,0,0,.18); +} +#edit-box h3 { font-size: 16px; font-weight: 700; margin-bottom: 16px; } +#edit-box label { + display: block; font-size: 11px; font-weight: 700; color: var(--text3); + text-transform: uppercase; letter-spacing: .4px; margin: 12px 0 4px; +} +#edit-box input, #edit-box select { + width: 100%; padding: 9px 12px; border: 1px solid var(--border); + border-radius: 8px; font-size: 14px; font-family: inherit; outline: none; +} +#edit-box input:focus, #edit-box select:focus { border-color: var(--blue); } +.edit-btns { display: flex; gap: 8px; margin-top: 20px; } +.edit-btns button { + flex: 1; padding: 10px; border-radius: 8px; font-size: 13px; + font-weight: 600; cursor: pointer; font-family: inherit; border: none; +} +.btn-save { background: var(--blue); color: #fff; } +.btn-cancel { background: #f1f5f9; color: var(--text2); } + +.popup-wrap { font-family: 'DM Sans', sans-serif; min-width: 190px; } +.popup-title { font-size: 14px; font-weight: 700; margin-bottom: 6px; } +.popup-row { font-size: 12px; color: var(--text2); margin-bottom: 3px; } +.popup-coord { font-size: 11px; color: var(--text3); font-family: 'DM Mono', monospace; margin: 4px 0 8px; } +.popup-badge { display: inline-block; padding: 2px 8px; border-radius: 5px; font-size: 10px; font-weight: 700; } +.popup-actions { display: flex; gap: 6px; margin-top: 10px; } +.btn-edit, .btn-delete { + flex: 1; border: none; padding: 7px; border-radius: 6px; + font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; +} +.btn-edit { background: var(--blue); color: #fff; } +.btn-delete { background: var(--red); color: #fff; } +.btn-yes, .btn-no { + border: none; padding: 7px 16px; border-radius: 6px; + font-size: 12px; font-weight: 600; cursor: pointer; margin: 3px; font-family: inherit; +} +.btn-yes { background: var(--green); color: #fff; } +.btn-no { background: #e2e8f0; color: var(--text2); } diff --git a/SIG_spbu/db.php b/SIG_spbu/db.php new file mode 100644 index 0000000..2749a38 --- /dev/null +++ b/SIG_spbu/db.php @@ -0,0 +1,7 @@ +connect_error) { + die("Koneksi gagal: " . $conn->connect_error); +} +?> \ No newline at end of file diff --git a/SIG_spbu/includes/confirm-modal.php b/SIG_spbu/includes/confirm-modal.php new file mode 100644 index 0000000..a3febb2 --- /dev/null +++ b/SIG_spbu/includes/confirm-modal.php @@ -0,0 +1,115 @@ + + + diff --git a/SIG_spbu/includes/map-controls.php b/SIG_spbu/includes/map-controls.php new file mode 100644 index 0000000..8462b64 --- /dev/null +++ b/SIG_spbu/includes/map-controls.php @@ -0,0 +1,14 @@ +
+
+
Layer SPBU
+ + +
+
+
Tampilan Peta
+
+ + +
+
+
diff --git a/SIG_spbu/includes/sidebar.php b/SIG_spbu/includes/sidebar.php new file mode 100644 index 0000000..90401dd --- /dev/null +++ b/SIG_spbu/includes/sidebar.php @@ -0,0 +1,44 @@ + + + +
+ +
+
+ +
Peta GIS Kanalisasi SPBU
+
+
+ + + Tambah Baru + +
+
+ +
+ +
+ +
+
+

Edit Data SPBU

+ + + + + + + +
+ + +
+
+
+ +
+ + + + + diff --git a/SIG_spbu/tambah.php b/SIG_spbu/tambah.php new file mode 100644 index 0000000..5741d77 --- /dev/null +++ b/SIG_spbu/tambah.php @@ -0,0 +1,149 @@ + + + + + + +Tambah SPBU — Sistem Informasi Geografis SPBU + + + + + + + + +
+
+

Input Data SPBU

+

Klik titik pada peta untuk menentukan koordinat, lalu lengkapi formulir di bawah.

+
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+ +
+ + +
+ +
Belum ada lokasi dipilih — klik peta di sebelah kanan
+
+
+
+
+ Operasional 24 jam: % + +
+ + + Kembali ke Portal + +
+ + +
+ +
+
+
Tambah Baru Lokasi Depot SPBU
+
+
+ + + Lihat Peta + +
+
+ +
+ +
Klik peta untuk menandai lokasi SPBU
+
+ +
+ + + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..cf60fa3 --- /dev/null +++ b/index.php @@ -0,0 +1,202 @@ + + + + + +WebGIS Pontianak — Portal + + + + + + + +
+
+ + Sistem Informasi Geografis +
+

Portal Terpadu WebGIS

+

Pusat akses untuk aplikasi pemetaan wilayah Pontianak, Kalimantan Barat. Pilih modul di bawah untuk membuka project yang sesuai dengan kebutuhan Anda.

+
+ +
+ +
+ + + + +