1178 lines
36 KiB
PHP
1178 lines
36 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>WebGIS Manajemen Jalan dan Parsil Tanah</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<!-- LeafletJS CDN -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
|
|
<!-- Leaflet Draw CDN -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
|
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
margin: 0;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
background: #f3f4f6;
|
|
}
|
|
|
|
header {
|
|
height: 50px;
|
|
padding: 6px 14px;
|
|
background: #1f2937;
|
|
color: white;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 2px;
|
|
}
|
|
|
|
header h1 {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
header p {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
color: #d1d5db;
|
|
}
|
|
|
|
#map {
|
|
width: 100%;
|
|
height: calc(100vh - 50px);
|
|
}
|
|
|
|
.popup-title {
|
|
margin: 0 0 8px;
|
|
font-size: 15px;
|
|
font-weight: bold;
|
|
color: #111827;
|
|
}
|
|
|
|
.popup-row {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.popup-row label {
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
color: #374151;
|
|
}
|
|
|
|
.popup-row input,
|
|
.popup-row select {
|
|
width: 100%;
|
|
padding: 7px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 5px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.popup-info {
|
|
padding: 7px;
|
|
background: #f3f4f6;
|
|
border-radius: 5px;
|
|
font-size: 12px;
|
|
color: #374151;
|
|
}
|
|
|
|
.popup-detail {
|
|
min-width: 230px;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.popup-detail b {
|
|
color: #111827;
|
|
}
|
|
|
|
.popup-actions {
|
|
margin-top: 10px;
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn {
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 7px 10px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
color: white;
|
|
}
|
|
|
|
.btn-simpan {
|
|
background: #16a34a;
|
|
}
|
|
|
|
.btn-edit {
|
|
background: #2563eb;
|
|
}
|
|
|
|
.btn-hapus {
|
|
background: #dc2626;
|
|
}
|
|
|
|
.btn-batal {
|
|
background: #6b7280;
|
|
}
|
|
|
|
.btn:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
#legend {
|
|
position: absolute;
|
|
right: 14px;
|
|
bottom: 24px;
|
|
z-index: 1000;
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.25);
|
|
font-size: 12px;
|
|
max-width: 260px;
|
|
}
|
|
|
|
#legend h3 {
|
|
margin: 0 0 7px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
#legend .section-title {
|
|
margin-top: 8px;
|
|
margin-bottom: 4px;
|
|
font-weight: bold;
|
|
color: #111827;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 4px;
|
|
gap: 6px;
|
|
}
|
|
|
|
.legend-line {
|
|
width: 22px;
|
|
height: 4px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.legend-box {
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 3px;
|
|
opacity: 0.75;
|
|
border: 1px solid #11182733;
|
|
}
|
|
|
|
#toast {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 62px;
|
|
transform: translateX(-50%);
|
|
z-index: 2000;
|
|
background: #111827;
|
|
color: white;
|
|
padding: 10px 14px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
display: none;
|
|
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.25);
|
|
}
|
|
|
|
#loading {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 2000;
|
|
background: rgba(17, 24, 39, 0.9);
|
|
color: white;
|
|
padding: 14px 18px;
|
|
border-radius: 10px;
|
|
display: none;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.spinner {
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 3px solid #9ca3af;
|
|
border-top: 3px solid white;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.leaflet-popup-content {
|
|
min-width: 230px;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
header h1 {
|
|
font-size: 14px;
|
|
}
|
|
|
|
header p {
|
|
font-size: 10px;
|
|
}
|
|
|
|
#legend {
|
|
left: 10px;
|
|
right: 10px;
|
|
bottom: 15px;
|
|
max-width: none;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>WebGIS Manajemen Jalan dan Parsil Tanah</h1>
|
|
<p>Gunakan toolbar gambar untuk membuat line/polyline jalan dan polygon parsil tanah.</p>
|
|
</header>
|
|
|
|
<div id="map"></div>
|
|
|
|
<div id="legend">
|
|
<h3>Legenda</h3>
|
|
|
|
<div class="section-title">Status Jalan</div>
|
|
<div class="legend-item">
|
|
<span class="legend-line" style="background:#e11d48;"></span>
|
|
Jalan Nasional
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-line" style="background:#2563eb;"></span>
|
|
Jalan Provinsi
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-line" style="background:#16a34a;"></span>
|
|
Jalan Kabupaten
|
|
</div>
|
|
|
|
<div class="section-title">Status Kepemilikan Tanah</div>
|
|
<div class="legend-item">
|
|
<span class="legend-box" style="background:#16a34a;"></span>
|
|
SHM
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-box" style="background:#2563eb;"></span>
|
|
HGB
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-box" style="background:#f97316;"></span>
|
|
HGU
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-box" style="background:#7c3aed;"></span>
|
|
HP
|
|
</div>
|
|
</div>
|
|
|
|
<div id="toast"></div>
|
|
|
|
<div id="loading">
|
|
<div class="spinner"></div>
|
|
<span>Memproses data...</span>
|
|
</div>
|
|
|
|
<script>
|
|
// Cek Leaflet sebelum peta diinisialisasi.
|
|
if (typeof L === 'undefined') {
|
|
alert('LeafletJS gagal dimuat. Pastikan koneksi internet aktif karena CDN digunakan.');
|
|
} else {
|
|
// Variabel utama peta dan penampung layer.
|
|
const peta = L.map('map').setView([-0.05566422376778593, 109.34854622732368], 17);
|
|
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 22,
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(peta);
|
|
|
|
// Data layer disimpan dalam object dengan key = id.
|
|
let jalanMap = {};
|
|
let parsilMap = {};
|
|
let jalanDataMap = {};
|
|
let parsilDataMap = {};
|
|
|
|
// Layer sementara ketika user menggambar, sebelum data disimpan.
|
|
let tempLayers = {};
|
|
let tempCounter = 0;
|
|
|
|
const drawnItems = new L.FeatureGroup();
|
|
peta.addLayer(drawnItems);
|
|
|
|
// Toolbar Leaflet Draw hanya mengaktifkan polyline dan polygon.
|
|
const drawControl = new L.Control.Draw({
|
|
position: 'topleft',
|
|
draw: {
|
|
polyline: {
|
|
shapeOptions: {
|
|
color: '#111827',
|
|
weight: 4,
|
|
opacity: 0.9
|
|
}
|
|
},
|
|
polygon: {
|
|
allowIntersection: false,
|
|
showArea: true,
|
|
shapeOptions: {
|
|
color: '#111827',
|
|
weight: 2,
|
|
fillOpacity: 0.25
|
|
}
|
|
},
|
|
rectangle: false,
|
|
circle: false,
|
|
marker: false,
|
|
circlemarker: false
|
|
},
|
|
edit: false
|
|
});
|
|
|
|
peta.addControl(drawControl);
|
|
|
|
// Event ketika user selesai menggambar.
|
|
peta.on(L.Draw.Event.CREATED, function (event) {
|
|
const layer = event.layer;
|
|
const layerType = event.layerType;
|
|
|
|
layer.addTo(peta);
|
|
|
|
if (layerType === 'polyline') {
|
|
showTambahJalanPopup(layer);
|
|
}
|
|
|
|
if (layerType === 'polygon') {
|
|
showTambahParsilPopup(layer);
|
|
}
|
|
});
|
|
|
|
// Jika popup tambah ditutup tanpa simpan, layer sementara dihapus.
|
|
peta.on('popupclose', function (event) {
|
|
const content = event.popup.getContent();
|
|
|
|
if (typeof content === 'string') {
|
|
const match = content.match(/data-temp-key="([^"]+)"/);
|
|
|
|
if (match && tempLayers[match[1]]) {
|
|
peta.removeLayer(tempLayers[match[1]]);
|
|
delete tempLayers[match[1]];
|
|
}
|
|
}
|
|
});
|
|
|
|
// Load data ketika halaman pertama dibuka.
|
|
loadData();
|
|
|
|
// =========================
|
|
// Helper Tampilan
|
|
// =========================
|
|
|
|
function showLoading(show) {
|
|
document.getElementById('loading').style.display = show ? 'flex' : 'none';
|
|
}
|
|
|
|
function showToast(message) {
|
|
const toast = document.getElementById('toast');
|
|
toast.textContent = message;
|
|
toast.style.display = 'block';
|
|
|
|
setTimeout(function () {
|
|
toast.style.display = 'none';
|
|
}, 2500);
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
function escapeAttr(value) {
|
|
return escapeHtml(value).replaceAll('`', '`');
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
const number = Number(value);
|
|
|
|
if (Number.isNaN(number)) {
|
|
return '0.00';
|
|
}
|
|
|
|
return number.toLocaleString('id-ID', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
});
|
|
}
|
|
|
|
function getLayerCenter(layer) {
|
|
if (layer.getBounds) {
|
|
return layer.getBounds().getCenter();
|
|
}
|
|
|
|
if (layer.getLatLng) {
|
|
return layer.getLatLng();
|
|
}
|
|
|
|
return peta.getCenter();
|
|
}
|
|
|
|
function getGeojsonObject(geojson) {
|
|
if (typeof geojson === 'string') {
|
|
return JSON.parse(geojson);
|
|
}
|
|
|
|
return geojson;
|
|
}
|
|
|
|
function ringkasanGeojson(geojson) {
|
|
try {
|
|
const obj = getGeojsonObject(geojson);
|
|
const geom = obj.type === 'Feature' ? obj.geometry : obj;
|
|
|
|
if (!geom) {
|
|
return 'Geometry tidak terbaca.';
|
|
}
|
|
|
|
if (geom.type === 'LineString') {
|
|
return `${geom.coordinates.length} titik koordinat LineString.`;
|
|
}
|
|
|
|
if (geom.type === 'MultiLineString') {
|
|
const total = geom.coordinates.reduce((sum, line) => sum + line.length, 0);
|
|
return `${total} titik koordinat MultiLineString.`;
|
|
}
|
|
|
|
if (geom.type === 'Polygon') {
|
|
const total = geom.coordinates[0] ? geom.coordinates[0].length : 0;
|
|
return `${total} titik koordinat Polygon.`;
|
|
}
|
|
|
|
if (geom.type === 'MultiPolygon') {
|
|
let total = 0;
|
|
geom.coordinates.forEach(function (polygon) {
|
|
if (polygon[0]) {
|
|
total += polygon[0].length;
|
|
}
|
|
});
|
|
return `${total} titik koordinat MultiPolygon.`;
|
|
}
|
|
|
|
return `Tipe geometry: ${geom.type}.`;
|
|
} catch (error) {
|
|
return 'GeoJSON tidak valid.';
|
|
}
|
|
}
|
|
|
|
// =========================
|
|
// Warna Layer
|
|
// =========================
|
|
|
|
function getWarnaJalan(status) {
|
|
if (status === 'nasional') {
|
|
return '#e11d48'; // merah
|
|
}
|
|
|
|
if (status === 'provinsi') {
|
|
return '#2563eb'; // biru
|
|
}
|
|
|
|
if (status === 'kabupaten') {
|
|
return '#16a34a'; // hijau
|
|
}
|
|
|
|
return '#111827';
|
|
}
|
|
|
|
function getWarnaParsil(status) {
|
|
if (status === 'SHM') {
|
|
return '#16a34a'; // hijau
|
|
}
|
|
|
|
if (status === 'HGB') {
|
|
return '#2563eb'; // biru
|
|
}
|
|
|
|
if (status === 'HGU') {
|
|
return '#f97316'; // orange
|
|
}
|
|
|
|
if (status === 'HP') {
|
|
return '#7c3aed'; // ungu
|
|
}
|
|
|
|
return '#111827';
|
|
}
|
|
|
|
// =========================
|
|
// Hitung Otomatis Geometry
|
|
// =========================
|
|
|
|
function flattenLatLngs(latlngs) {
|
|
const result = [];
|
|
|
|
latlngs.forEach(function (item) {
|
|
if (Array.isArray(item)) {
|
|
result.push(...flattenLatLngs(item));
|
|
} else {
|
|
result.push(item);
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function hitungPanjangPolyline(layer) {
|
|
const latlngs = flattenLatLngs(layer.getLatLngs());
|
|
let total = 0;
|
|
|
|
for (let i = 1; i < latlngs.length; i++) {
|
|
// Fungsi distanceTo() berasal dari LeafletJS.
|
|
total += latlngs[i - 1].distanceTo(latlngs[i]);
|
|
}
|
|
|
|
return Math.round(total * 100) / 100;
|
|
}
|
|
|
|
function hitungLuasPolygon(layer) {
|
|
const latlngs = layer.getLatLngs();
|
|
const outerRing = Array.isArray(latlngs[0]) ? latlngs[0] : latlngs;
|
|
|
|
// Fungsi geodesicArea tersedia dari Leaflet Draw / GeometryUtil.
|
|
if (L.GeometryUtil && typeof L.GeometryUtil.geodesicArea === 'function') {
|
|
return Math.round(Math.abs(L.GeometryUtil.geodesicArea(outerRing)) * 100) / 100;
|
|
}
|
|
|
|
// Fallback sederhana jika GeometryUtil tidak tersedia.
|
|
// Perhitungan ini hanya cadangan, bukan perhitungan utama.
|
|
let area = 0;
|
|
const earthRadius = 6378137;
|
|
|
|
for (let i = 0; i < outerRing.length; i++) {
|
|
const p1 = outerRing[i];
|
|
const p2 = outerRing[(i + 1) % outerRing.length];
|
|
area += (p2.lng - p1.lng) * Math.PI / 180 *
|
|
(2 + Math.sin(p1.lat * Math.PI / 180) + Math.sin(p2.lat * Math.PI / 180));
|
|
}
|
|
|
|
area = area * earthRadius * earthRadius / 2;
|
|
return Math.round(Math.abs(area) * 100) / 100;
|
|
}
|
|
|
|
// =========================
|
|
// Fetch API
|
|
// =========================
|
|
|
|
async function apiPost(url, formData) {
|
|
showLoading(true);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const text = await response.text();
|
|
let result;
|
|
|
|
try {
|
|
result = JSON.parse(text);
|
|
} catch (error) {
|
|
throw new Error('Response server bukan JSON: ' + text);
|
|
}
|
|
|
|
if (!response.ok || !result.success) {
|
|
throw new Error(result.message || 'Terjadi kesalahan pada server.');
|
|
}
|
|
|
|
return result;
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
async function loadData() {
|
|
showLoading(true);
|
|
|
|
try {
|
|
const response = await fetch('ambil_data.php');
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.message || 'Gagal mengambil data.');
|
|
}
|
|
|
|
// Bersihkan layer lama dari peta.
|
|
Object.values(jalanMap).forEach(function (layer) {
|
|
peta.removeLayer(layer);
|
|
});
|
|
|
|
Object.values(parsilMap).forEach(function (layer) {
|
|
peta.removeLayer(layer);
|
|
});
|
|
|
|
jalanMap = {};
|
|
parsilMap = {};
|
|
jalanDataMap = {};
|
|
parsilDataMap = {};
|
|
|
|
result.jalan.forEach(function (item) {
|
|
renderJalan(item);
|
|
});
|
|
|
|
result.parsil.forEach(function (item) {
|
|
renderParsil(item);
|
|
});
|
|
} catch (error) {
|
|
showToast(error.message);
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
// =========================
|
|
// Render Layer Jalan
|
|
// =========================
|
|
|
|
function renderJalan(data) {
|
|
try {
|
|
const geojson = getGeojsonObject(data.geojson);
|
|
|
|
const layer = L.geoJSON(geojson, {
|
|
style: function () {
|
|
return {
|
|
color: getWarnaJalan(data.status_jalan),
|
|
weight: 5,
|
|
opacity: 0.9
|
|
};
|
|
}
|
|
});
|
|
|
|
layer.eachLayer(function (singleLayer) {
|
|
singleLayer.on('click', function (event) {
|
|
showDetailJalanPopup(data, event.latlng);
|
|
});
|
|
});
|
|
|
|
layer.addTo(peta);
|
|
|
|
jalanMap[data.id] = layer;
|
|
jalanDataMap[data.id] = data;
|
|
} catch (error) {
|
|
console.error('Gagal render jalan:', error, data);
|
|
}
|
|
}
|
|
|
|
function showTambahJalanPopup(layer) {
|
|
const tempKey = 'temp_jalan_' + (++tempCounter);
|
|
tempLayers[tempKey] = layer;
|
|
|
|
const panjang = hitungPanjangPolyline(layer);
|
|
const center = getLayerCenter(layer);
|
|
|
|
const content = `
|
|
<form id="formTambahJalan" data-temp-key="${tempKey}">
|
|
<h3 class="popup-title">Tambah Data Jalan</h3>
|
|
|
|
<div class="popup-row">
|
|
<label>Nama Jalan</label>
|
|
<input type="text" name="nama_jalan" required placeholder="Contoh: Jalan Merdeka">
|
|
</div>
|
|
|
|
<div class="popup-row">
|
|
<label>Status Jalan</label>
|
|
<select name="status_jalan" required>
|
|
<option value="">-- Pilih Status --</option>
|
|
<option value="nasional">Jalan Nasional</option>
|
|
<option value="provinsi">Jalan Provinsi</option>
|
|
<option value="kabupaten">Jalan Kabupaten</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="popup-row">
|
|
<label>Panjang Otomatis</label>
|
|
<div class="popup-info">${formatNumber(panjang)} meter</div>
|
|
</div>
|
|
|
|
<div class="popup-actions">
|
|
<button type="submit" class="btn btn-simpan">Simpan</button>
|
|
<button type="button" class="btn btn-batal" onclick="batalTambah('${tempKey}')">Batal</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
L.popup()
|
|
.setLatLng(center)
|
|
.setContent(content)
|
|
.openOn(peta);
|
|
}
|
|
|
|
function showDetailJalanPopup(data, latlng) {
|
|
const content = `
|
|
<div class="popup-detail">
|
|
<h3 class="popup-title">Detail Data Jalan</h3>
|
|
|
|
<div><b>Nama Jalan:</b> ${escapeHtml(data.nama_jalan)}</div>
|
|
<div><b>Status:</b> ${escapeHtml(labelStatusJalan(data.status_jalan))}</div>
|
|
<div><b>Panjang:</b> ${formatNumber(data.panjang_meter)} meter</div>
|
|
<div><b>GeoJSON:</b> ${escapeHtml(ringkasanGeojson(data.geojson))}</div>
|
|
|
|
<div class="popup-actions">
|
|
<button type="button" class="btn btn-edit" onclick="showEditJalanPopup(${Number(data.id)})">Edit</button>
|
|
<button type="button" class="btn btn-hapus" onclick="hapusData('jalan', ${Number(data.id)})">Hapus</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
L.popup()
|
|
.setLatLng(latlng)
|
|
.setContent(content)
|
|
.openOn(peta);
|
|
}
|
|
|
|
function showEditJalanPopup(id) {
|
|
const data = jalanDataMap[id];
|
|
|
|
if (!data) {
|
|
showToast('Data jalan tidak ditemukan.');
|
|
return;
|
|
}
|
|
|
|
const layer = jalanMap[id];
|
|
const center = getLayerCenter(layer);
|
|
|
|
const content = `
|
|
<form id="formEditJalan" data-id="${Number(id)}">
|
|
<h3 class="popup-title">Edit Data Jalan</h3>
|
|
|
|
<div class="popup-row">
|
|
<label>Nama Jalan</label>
|
|
<input type="text" name="nama_jalan" required value="${escapeAttr(data.nama_jalan)}">
|
|
</div>
|
|
|
|
<div class="popup-row">
|
|
<label>Status Jalan</label>
|
|
<select name="status_jalan" required>
|
|
<option value="nasional" ${data.status_jalan === 'nasional' ? 'selected' : ''}>Jalan Nasional</option>
|
|
<option value="provinsi" ${data.status_jalan === 'provinsi' ? 'selected' : ''}>Jalan Provinsi</option>
|
|
<option value="kabupaten" ${data.status_jalan === 'kabupaten' ? 'selected' : ''}>Jalan Kabupaten</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="popup-row">
|
|
<label>Panjang</label>
|
|
<div class="popup-info">${formatNumber(data.panjang_meter)} meter<br>Tidak dapat diedit manual.</div>
|
|
</div>
|
|
|
|
<div class="popup-actions">
|
|
<button type="submit" class="btn btn-edit">Edit</button>
|
|
<button type="button" class="btn btn-batal" onclick="peta.closePopup()">Batal</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
L.popup()
|
|
.setLatLng(center)
|
|
.setContent(content)
|
|
.openOn(peta);
|
|
}
|
|
|
|
function labelStatusJalan(status) {
|
|
if (status === 'nasional') {
|
|
return 'Jalan Nasional';
|
|
}
|
|
|
|
if (status === 'provinsi') {
|
|
return 'Jalan Provinsi';
|
|
}
|
|
|
|
if (status === 'kabupaten') {
|
|
return 'Jalan Kabupaten';
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
// =========================
|
|
// Render Layer Parsil
|
|
// =========================
|
|
|
|
function renderParsil(data) {
|
|
try {
|
|
const geojson = getGeojsonObject(data.geojson);
|
|
|
|
const layer = L.geoJSON(geojson, {
|
|
style: function () {
|
|
return {
|
|
color: getWarnaParsil(data.status_kepemilikan),
|
|
weight: 2,
|
|
opacity: 0.9,
|
|
fillColor: getWarnaParsil(data.status_kepemilikan),
|
|
fillOpacity: 0.35
|
|
};
|
|
}
|
|
});
|
|
|
|
layer.eachLayer(function (singleLayer) {
|
|
singleLayer.on('click', function (event) {
|
|
showDetailParsilPopup(data, event.latlng);
|
|
});
|
|
});
|
|
|
|
layer.addTo(peta);
|
|
|
|
parsilMap[data.id] = layer;
|
|
parsilDataMap[data.id] = data;
|
|
} catch (error) {
|
|
console.error('Gagal render parsil:', error, data);
|
|
}
|
|
}
|
|
|
|
function showTambahParsilPopup(layer) {
|
|
const tempKey = 'temp_parsil_' + (++tempCounter);
|
|
tempLayers[tempKey] = layer;
|
|
|
|
const luas = hitungLuasPolygon(layer);
|
|
const center = getLayerCenter(layer);
|
|
|
|
const content = `
|
|
<form id="formTambahParsil" data-temp-key="${tempKey}">
|
|
<h3 class="popup-title">Tambah Data Parsil Tanah</h3>
|
|
|
|
<div class="popup-row">
|
|
<label>Nama/Kode Parsil</label>
|
|
<input type="text" name="nama_parsil" required placeholder="Contoh: Kavling A1">
|
|
</div>
|
|
|
|
<div class="popup-row">
|
|
<label>Status Kepemilikan</label>
|
|
<select name="status_kepemilikan" required>
|
|
<option value="">-- Pilih Status --</option>
|
|
<option value="SHM">SHM - Sertifikat Hak Milik</option>
|
|
<option value="HGB">HGB - Sertifikat Hak Guna Bangunan</option>
|
|
<option value="HGU">HGU - Sertifikat Hak Guna Usaha</option>
|
|
<option value="HP">HP - Sertifikat Hak Pakai</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="popup-row">
|
|
<label>Luas Otomatis</label>
|
|
<div class="popup-info">${formatNumber(luas)} meter persegi</div>
|
|
</div>
|
|
|
|
<div class="popup-actions">
|
|
<button type="submit" class="btn btn-simpan">Simpan</button>
|
|
<button type="button" class="btn btn-batal" onclick="batalTambah('${tempKey}')">Batal</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
L.popup()
|
|
.setLatLng(center)
|
|
.setContent(content)
|
|
.openOn(peta);
|
|
}
|
|
|
|
function showDetailParsilPopup(data, latlng) {
|
|
const content = `
|
|
<div class="popup-detail">
|
|
<h3 class="popup-title">Detail Data Parsil Tanah</h3>
|
|
|
|
<div><b>Nama Parsil:</b> ${escapeHtml(data.nama_parsil)}</div>
|
|
<div><b>Status:</b> ${escapeHtml(labelStatusParsil(data.status_kepemilikan))}</div>
|
|
<div><b>Luas:</b> ${formatNumber(data.luas_meter)} m²</div>
|
|
<div><b>GeoJSON:</b> ${escapeHtml(ringkasanGeojson(data.geojson))}</div>
|
|
|
|
<div class="popup-actions">
|
|
<button type="button" class="btn btn-edit" onclick="showEditParsilPopup(${Number(data.id)})">Edit</button>
|
|
<button type="button" class="btn btn-hapus" onclick="hapusData('parsil', ${Number(data.id)})">Hapus</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
L.popup()
|
|
.setLatLng(latlng)
|
|
.setContent(content)
|
|
.openOn(peta);
|
|
}
|
|
|
|
function showEditParsilPopup(id) {
|
|
const data = parsilDataMap[id];
|
|
|
|
if (!data) {
|
|
showToast('Data parsil tanah tidak ditemukan.');
|
|
return;
|
|
}
|
|
|
|
const layer = parsilMap[id];
|
|
const center = getLayerCenter(layer);
|
|
|
|
const content = `
|
|
<form id="formEditParsil" data-id="${Number(id)}">
|
|
<h3 class="popup-title">Edit Data Parsil Tanah</h3>
|
|
|
|
<div class="popup-row">
|
|
<label>Nama/Kode Parsil</label>
|
|
<input type="text" name="nama_parsil" required value="${escapeAttr(data.nama_parsil)}">
|
|
</div>
|
|
|
|
<div class="popup-row">
|
|
<label>Status Kepemilikan</label>
|
|
<select name="status_kepemilikan" required>
|
|
<option value="SHM" ${data.status_kepemilikan === 'SHM' ? 'selected' : ''}>SHM - Sertifikat Hak Milik</option>
|
|
<option value="HGB" ${data.status_kepemilikan === 'HGB' ? 'selected' : ''}>HGB - Sertifikat Hak Guna Bangunan</option>
|
|
<option value="HGU" ${data.status_kepemilikan === 'HGU' ? 'selected' : ''}>HGU - Sertifikat Hak Guna Usaha</option>
|
|
<option value="HP" ${data.status_kepemilikan === 'HP' ? 'selected' : ''}>HP - Sertifikat Hak Pakai</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="popup-row">
|
|
<label>Luas</label>
|
|
<div class="popup-info">${formatNumber(data.luas_meter)} m²<br>Tidak dapat diedit manual.</div>
|
|
</div>
|
|
|
|
<div class="popup-actions">
|
|
<button type="submit" class="btn btn-edit">Edit</button>
|
|
<button type="button" class="btn btn-batal" onclick="peta.closePopup()">Batal</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
L.popup()
|
|
.setLatLng(center)
|
|
.setContent(content)
|
|
.openOn(peta);
|
|
}
|
|
|
|
function labelStatusParsil(status) {
|
|
if (status === 'SHM') {
|
|
return 'SHM - Sertifikat Hak Milik';
|
|
}
|
|
|
|
if (status === 'HGB') {
|
|
return 'HGB - Sertifikat Hak Guna Bangunan';
|
|
}
|
|
|
|
if (status === 'HGU') {
|
|
return 'HGU - Sertifikat Hak Guna Usaha';
|
|
}
|
|
|
|
if (status === 'HP') {
|
|
return 'HP - Sertifikat Hak Pakai';
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
// =========================
|
|
// Event Submit Form Popup
|
|
// =========================
|
|
|
|
document.addEventListener('submit', async function (event) {
|
|
event.preventDefault();
|
|
|
|
const form = event.target;
|
|
|
|
if (form.id === 'formTambahJalan') {
|
|
await prosesTambahJalan(form);
|
|
}
|
|
|
|
if (form.id === 'formEditJalan') {
|
|
await prosesEditJalan(form);
|
|
}
|
|
|
|
if (form.id === 'formTambahParsil') {
|
|
await prosesTambahParsil(form);
|
|
}
|
|
|
|
if (form.id === 'formEditParsil') {
|
|
await prosesEditParsil(form);
|
|
}
|
|
});
|
|
|
|
async function prosesTambahJalan(form) {
|
|
const tempKey = form.dataset.tempKey;
|
|
const layer = tempLayers[tempKey];
|
|
|
|
if (!layer) {
|
|
showToast('Layer gambar tidak ditemukan.');
|
|
return;
|
|
}
|
|
|
|
const panjang = hitungPanjangPolyline(layer);
|
|
const geojson = JSON.stringify(layer.toGeoJSON());
|
|
|
|
const formData = new FormData(form);
|
|
formData.append('tipe', 'jalan');
|
|
formData.append('panjang_meter', panjang);
|
|
formData.append('geojson', geojson);
|
|
|
|
try {
|
|
const result = await apiPost('tambah.php', formData);
|
|
|
|
// Hapus layer sementara, lalu render layer dari data database.
|
|
peta.removeLayer(layer);
|
|
delete tempLayers[tempKey];
|
|
|
|
renderJalan(result.data);
|
|
peta.closePopup();
|
|
showToast(result.message);
|
|
} catch (error) {
|
|
showToast(error.message);
|
|
}
|
|
}
|
|
|
|
async function prosesEditJalan(form) {
|
|
const id = Number(form.dataset.id);
|
|
const formData = new FormData(form);
|
|
formData.append('tipe', 'jalan');
|
|
formData.append('id', id);
|
|
|
|
try {
|
|
const result = await apiPost('edit.php', formData);
|
|
peta.closePopup();
|
|
showToast(result.message);
|
|
|
|
// Load ulang data tanpa reload halaman agar warna layer ikut berubah.
|
|
await loadData();
|
|
} catch (error) {
|
|
showToast(error.message);
|
|
}
|
|
}
|
|
|
|
async function prosesTambahParsil(form) {
|
|
const tempKey = form.dataset.tempKey;
|
|
const layer = tempLayers[tempKey];
|
|
|
|
if (!layer) {
|
|
showToast('Layer gambar tidak ditemukan.');
|
|
return;
|
|
}
|
|
|
|
const luas = hitungLuasPolygon(layer);
|
|
const geojson = JSON.stringify(layer.toGeoJSON());
|
|
|
|
const formData = new FormData(form);
|
|
formData.append('tipe', 'parsil');
|
|
formData.append('luas_meter', luas);
|
|
formData.append('geojson', geojson);
|
|
|
|
try {
|
|
const result = await apiPost('tambah.php', formData);
|
|
|
|
// Hapus layer sementara, lalu render layer dari data database.
|
|
peta.removeLayer(layer);
|
|
delete tempLayers[tempKey];
|
|
|
|
renderParsil(result.data);
|
|
peta.closePopup();
|
|
showToast(result.message);
|
|
} catch (error) {
|
|
showToast(error.message);
|
|
}
|
|
}
|
|
|
|
async function prosesEditParsil(form) {
|
|
const id = Number(form.dataset.id);
|
|
const formData = new FormData(form);
|
|
formData.append('tipe', 'parsil');
|
|
formData.append('id', id);
|
|
|
|
try {
|
|
const result = await apiPost('edit.php', formData);
|
|
peta.closePopup();
|
|
showToast(result.message);
|
|
|
|
// Load ulang data tanpa reload halaman agar warna polygon ikut berubah.
|
|
await loadData();
|
|
} catch (error) {
|
|
showToast(error.message);
|
|
}
|
|
}
|
|
|
|
// =========================
|
|
// Hapus dan Batal
|
|
// =========================
|
|
|
|
async function hapusData(tipe, id) {
|
|
const label = tipe === 'jalan' ? 'jalan' : 'parsil tanah';
|
|
|
|
if (!confirm(`Yakin ingin menghapus data ${label} ini?`)) {
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('tipe', tipe);
|
|
formData.append('id', id);
|
|
|
|
try {
|
|
const result = await apiPost('hapus.php', formData);
|
|
|
|
if (tipe === 'jalan' && jalanMap[id]) {
|
|
peta.removeLayer(jalanMap[id]);
|
|
delete jalanMap[id];
|
|
delete jalanDataMap[id];
|
|
}
|
|
|
|
if (tipe === 'parsil' && parsilMap[id]) {
|
|
peta.removeLayer(parsilMap[id]);
|
|
delete parsilMap[id];
|
|
delete parsilDataMap[id];
|
|
}
|
|
|
|
peta.closePopup();
|
|
showToast(result.message);
|
|
} catch (error) {
|
|
showToast(error.message);
|
|
}
|
|
}
|
|
|
|
function batalTambah(tempKey) {
|
|
if (tempLayers[tempKey]) {
|
|
peta.removeLayer(tempLayers[tempKey]);
|
|
delete tempLayers[tempKey];
|
|
}
|
|
|
|
peta.closePopup();
|
|
}
|
|
|
|
// Fungsi dibuat global agar bisa dipanggil dari tombol popup.
|
|
window.showEditJalanPopup = showEditJalanPopup;
|
|
window.showEditParsilPopup = showEditParsilPopup;
|
|
window.hapusData = hapusData;
|
|
window.batalTambah = batalTambah;
|
|
window.peta = peta;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|