Initial WebGIS portal project
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,155 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const DEFAULT_SOURCE = 'assets/data/Kecamatan.json';
|
||||
const DEFAULT_ANCHOR = {
|
||||
lat: -0.06054796552220232,
|
||||
lng: 109.34490231930592
|
||||
};
|
||||
const DEFAULT_GEO_BOUNDS = {
|
||||
south: -0.0981948,
|
||||
west: 109.2741676,
|
||||
north: 0.0381168,
|
||||
east: 109.3853475
|
||||
};
|
||||
const DEFAULT_CALIBRATION = {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
offsetLat: 0,
|
||||
offsetLng: 0
|
||||
};
|
||||
|
||||
function walkCoordinates(coordinates, callback) {
|
||||
if (!Array.isArray(coordinates)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (coordinates.length >= 2 && typeof coordinates[0] === 'number' && typeof coordinates[1] === 'number') {
|
||||
callback(coordinates[0], coordinates[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
coordinates.forEach((child) => walkCoordinates(child, callback));
|
||||
}
|
||||
|
||||
function getCoordinateBounds(data) {
|
||||
const bounds = {
|
||||
minX: Infinity,
|
||||
minY: Infinity,
|
||||
maxX: -Infinity,
|
||||
maxY: -Infinity
|
||||
};
|
||||
|
||||
function addGeometry(geometry) {
|
||||
if (!geometry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (geometry.type === 'GeometryCollection') {
|
||||
(geometry.geometries || []).forEach(addGeometry);
|
||||
return;
|
||||
}
|
||||
|
||||
walkCoordinates(geometry.coordinates, (x, y) => {
|
||||
bounds.minX = Math.min(bounds.minX, x);
|
||||
bounds.minY = Math.min(bounds.minY, y);
|
||||
bounds.maxX = Math.max(bounds.maxX, x);
|
||||
bounds.maxY = Math.max(bounds.maxY, y);
|
||||
});
|
||||
}
|
||||
|
||||
if (data.type === 'FeatureCollection') {
|
||||
(data.features || []).forEach((feature) => addGeometry(feature.geometry));
|
||||
} else if (data.type === 'Feature') {
|
||||
addGeometry(data.geometry);
|
||||
} else {
|
||||
addGeometry(data);
|
||||
}
|
||||
|
||||
if (!Number.isFinite(bounds.minX) || !Number.isFinite(bounds.minY)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
function createBoundingBoxTransform(sourceBounds, targetBounds, calibration) {
|
||||
const sourceWidth = sourceBounds.maxX - sourceBounds.minX;
|
||||
const sourceHeight = sourceBounds.maxY - sourceBounds.minY;
|
||||
const targetCenterLng = (targetBounds.west + targetBounds.east) / 2 + calibration.offsetLng;
|
||||
const targetCenterLat = (targetBounds.south + targetBounds.north) / 2 + calibration.offsetLat;
|
||||
const targetWidth = (targetBounds.east - targetBounds.west) * calibration.scaleX;
|
||||
const targetHeight = (targetBounds.north - targetBounds.south) * calibration.scaleY;
|
||||
|
||||
return function coordsToLatLng(coordinates) {
|
||||
const xRatio = ((coordinates[0] - sourceBounds.minX) / sourceWidth) - 0.5;
|
||||
const yRatio = ((coordinates[1] - sourceBounds.minY) / sourceHeight) - 0.5;
|
||||
const lng = targetCenterLng + (xRatio * targetWidth);
|
||||
const lat = targetCenterLat + (yRatio * targetHeight);
|
||||
return L.latLng(lat, lng);
|
||||
};
|
||||
}
|
||||
|
||||
window.addKecamatanLayer = async function addKecamatanLayer(map, options) {
|
||||
const settings = Object.assign({
|
||||
source: DEFAULT_SOURCE,
|
||||
anchor: DEFAULT_ANCHOR,
|
||||
targetBounds: DEFAULT_GEO_BOUNDS,
|
||||
calibration: DEFAULT_CALIBRATION,
|
||||
fitBounds: false
|
||||
}, options || {});
|
||||
|
||||
if (!map || typeof L === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!map.getPane('kecamatanPane')) {
|
||||
map.createPane('kecamatanPane');
|
||||
map.getPane('kecamatanPane').style.zIndex = 350;
|
||||
}
|
||||
|
||||
const response = await fetch(settings.source, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Gagal memuat data batas kecamatan.');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const coordinateBounds = getCoordinateBounds(data);
|
||||
|
||||
if (!coordinateBounds) {
|
||||
throw new Error('Data batas kecamatan tidak memiliki koordinat valid.');
|
||||
}
|
||||
|
||||
const layer = L.geoJSON(data, {
|
||||
coordsToLatLng: createBoundingBoxTransform(coordinateBounds, settings.targetBounds, settings.calibration),
|
||||
pane: 'kecamatanPane',
|
||||
style: {
|
||||
color: '#0f766e',
|
||||
weight: 2,
|
||||
opacity: 0.9,
|
||||
fillColor: '#14b8a6',
|
||||
fillOpacity: 0.08,
|
||||
dashArray: '6 4'
|
||||
},
|
||||
onEachFeature: function (feature, featureLayer) {
|
||||
const name = feature && feature.properties && (
|
||||
feature.properties.nama ||
|
||||
feature.properties.NAMA ||
|
||||
feature.properties.KECAMATAN ||
|
||||
feature.properties.kecamatan
|
||||
);
|
||||
|
||||
featureLayer.bindPopup(name ? `Kecamatan: ${name}` : 'Batas Kecamatan');
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
if (settings.fitBounds && layer.getBounds().isValid()) {
|
||||
map.fitBounds(layer.getBounds().pad(0.12), { maxZoom: 15 });
|
||||
}
|
||||
|
||||
return layer;
|
||||
};
|
||||
}());
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,622 @@
|
||||
function setStatus(element, message, type) {
|
||||
element.textContent = message;
|
||||
element.classList.remove('error', 'success');
|
||||
if (type) {
|
||||
element.classList.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
function roadColorByStatus(status) {
|
||||
if (status === 'nasional') {
|
||||
return '#dc2626';
|
||||
}
|
||||
if (status === 'provinsi') {
|
||||
return '#2563eb';
|
||||
}
|
||||
return '#16a34a';
|
||||
}
|
||||
|
||||
function parcelColorByStatus(status) {
|
||||
if (status === 'SHM') {
|
||||
return '#ea580c';
|
||||
}
|
||||
if (status === 'HGB') {
|
||||
return '#0ea5e9';
|
||||
}
|
||||
if (status === 'HGU') {
|
||||
return '#22c55e';
|
||||
}
|
||||
return '#a855f7';
|
||||
}
|
||||
|
||||
function calculatePolylineLengthMeters(latlngs) {
|
||||
let total = 0;
|
||||
for (let index = 1; index < latlngs.length; index += 1) {
|
||||
total += latlngs[index - 1].distanceTo(latlngs[index]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function calculatePolygonAreaMeters(latlngs) {
|
||||
if (window.L && L.GeometryUtil && typeof L.GeometryUtil.geodesicArea === 'function') {
|
||||
return L.GeometryUtil.geodesicArea(latlngs);
|
||||
}
|
||||
|
||||
const earthRadius = 6378137;
|
||||
const toRad = (degree) => degree * Math.PI / 180;
|
||||
|
||||
let area = 0;
|
||||
for (let i = 0; i < latlngs.length; i += 1) {
|
||||
const p1 = latlngs[i];
|
||||
const p2 = latlngs[(i + 1) % latlngs.length];
|
||||
area += toRad(p2.lng - p1.lng) * (2 + Math.sin(toRad(p1.lat)) + Math.sin(toRad(p2.lat)));
|
||||
}
|
||||
|
||||
return Math.abs(area * earthRadius * earthRadius / 2);
|
||||
}
|
||||
|
||||
function polylineToWkt(latlngs) {
|
||||
const coordinates = latlngs.map((point) => `${point.lng} ${point.lat}`).join(', ');
|
||||
return `LINESTRING(${coordinates})`;
|
||||
}
|
||||
|
||||
function polygonToWkt(latlngs) {
|
||||
const ring = latlngs.map((point) => `${point.lng} ${point.lat}`);
|
||||
if (ring[0] !== ring[ring.length - 1]) {
|
||||
ring.push(ring[0]);
|
||||
}
|
||||
return `POLYGON((${ring.join(', ')}))`;
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
throw new Error('Response server bukan JSON valid.');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const map = L.map('map').setView([-0.0605, 109.3449], 13);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
if (typeof window.addKecamatanLayer === 'function') {
|
||||
window.addKecamatanLayer(map, { fitBounds: true }).catch((error) => {
|
||||
console.warn('Data kecamatan belum termuat:', error);
|
||||
});
|
||||
}
|
||||
|
||||
const editableLayer = new L.FeatureGroup();
|
||||
map.addLayer(editableLayer);
|
||||
|
||||
const modalBackdrop = document.getElementById('modal-backdrop');
|
||||
const modalTitle = document.getElementById('modal-title');
|
||||
const featureForm = document.getElementById('feature-form');
|
||||
const roadFields = document.getElementById('road-fields');
|
||||
const parcelFields = document.getElementById('parcel-fields');
|
||||
const formStatus = document.getElementById('form-status');
|
||||
const cancelButton = document.getElementById('btn-cancel');
|
||||
|
||||
let pendingLayer = null;
|
||||
let pendingType = null;
|
||||
let pendingMode = 'create';
|
||||
|
||||
function closeModal(shouldRemovePendingLayer) {
|
||||
modalBackdrop.classList.remove('open');
|
||||
modalBackdrop.setAttribute('aria-hidden', 'true');
|
||||
setStatus(formStatus, '', '');
|
||||
|
||||
if (shouldRemovePendingLayer && pendingMode === 'create' && pendingLayer) {
|
||||
editableLayer.removeLayer(pendingLayer);
|
||||
}
|
||||
|
||||
pendingLayer = null;
|
||||
pendingType = null;
|
||||
pendingMode = 'create';
|
||||
featureForm.reset();
|
||||
}
|
||||
|
||||
function openRoadModal(layer, mode, initialData) {
|
||||
pendingLayer = layer;
|
||||
pendingType = 'road';
|
||||
pendingMode = mode;
|
||||
|
||||
modalTitle.textContent = mode === 'create' ? 'Tambah Data Jalan' : 'Update Data Jalan';
|
||||
roadFields.hidden = false;
|
||||
parcelFields.hidden = true;
|
||||
|
||||
const latlngs = layer.getLatLngs();
|
||||
const lengthMeters = calculatePolylineLengthMeters(latlngs);
|
||||
|
||||
document.getElementById('nama_jalan').value = initialData && initialData.nama_jalan ? initialData.nama_jalan : '';
|
||||
document.getElementById('status_jalan').value = initialData && initialData.status ? initialData.status : 'nasional';
|
||||
document.getElementById('panjang_meter').value = lengthMeters.toFixed(2);
|
||||
|
||||
modalBackdrop.classList.add('open');
|
||||
modalBackdrop.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
function openParcelModal(layer, mode, initialData) {
|
||||
pendingLayer = layer;
|
||||
pendingType = 'parcel';
|
||||
pendingMode = mode;
|
||||
|
||||
modalTitle.textContent = mode === 'create' ? 'Tambah Data Parsil Tanah' : 'Update Data Parsil Tanah';
|
||||
roadFields.hidden = true;
|
||||
parcelFields.hidden = false;
|
||||
|
||||
const latlngs = layer.getLatLngs()[0];
|
||||
const areaM2 = calculatePolygonAreaMeters(latlngs);
|
||||
|
||||
document.getElementById('nama_pemilik').value = initialData && initialData.nama_pemilik ? initialData.nama_pemilik : '';
|
||||
document.getElementById('status_kepemilikan').value = initialData && initialData.status_kepemilikan ? initialData.status_kepemilikan : 'SHM';
|
||||
document.getElementById('luas_m2').value = areaM2.toFixed(2);
|
||||
|
||||
modalBackdrop.classList.add('open');
|
||||
modalBackdrop.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
function attachRoadPopup(layer) {
|
||||
const props = layer.featureProps;
|
||||
layer.setStyle({
|
||||
color: roadColorByStatus(props.status),
|
||||
weight: 5
|
||||
});
|
||||
|
||||
layer.bindPopup(`
|
||||
<b>Jalan:</b> ${props.nama_jalan}<br>
|
||||
<b>Status:</b> ${props.status}<br>
|
||||
<b>Panjang:</b> ${Number(props.panjang_meter).toFixed(2)} m
|
||||
<div class="popup-actions">
|
||||
<button type="button" data-action="edit">Edit</button>
|
||||
<button type="button" class="btn-danger" data-action="delete">Hapus</button>
|
||||
</div>
|
||||
<div class="status" data-role="popup-status"></div>
|
||||
`);
|
||||
|
||||
layer.on('popupopen', (event) => {
|
||||
const popupElement = event.popup.getElement();
|
||||
const editButton = popupElement.querySelector('[data-action="edit"]');
|
||||
const deleteButton = popupElement.querySelector('[data-action="delete"]');
|
||||
const popupStatus = popupElement.querySelector('[data-role="popup-status"]');
|
||||
|
||||
editButton.addEventListener('click', () => {
|
||||
map.closePopup();
|
||||
openRoadModal(layer, 'update', layer.featureProps);
|
||||
});
|
||||
|
||||
deleteButton.addEventListener('click', async () => {
|
||||
if (!window.confirm('Hapus data jalan ini?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(popupStatus, 'Menghapus data...', '');
|
||||
deleteButton.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('api/road_delete.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: layer.featureId })
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || 'Gagal menghapus data jalan.');
|
||||
}
|
||||
|
||||
editableLayer.removeLayer(layer);
|
||||
map.closePopup();
|
||||
} catch (error) {
|
||||
setStatus(popupStatus, error.message || 'Gagal menghapus data.', 'error');
|
||||
deleteButton.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function attachParcelPopup(layer) {
|
||||
const props = layer.featureProps;
|
||||
layer.setStyle({
|
||||
color: parcelColorByStatus(props.status_kepemilikan),
|
||||
fillColor: parcelColorByStatus(props.status_kepemilikan),
|
||||
fillOpacity: 0.35,
|
||||
weight: 3
|
||||
});
|
||||
|
||||
layer.bindPopup(`
|
||||
<b>Pemilik:</b> ${props.nama_pemilik}<br>
|
||||
<b>Status:</b> ${props.status_kepemilikan}<br>
|
||||
<b>Luas:</b> ${Number(props.luas_m2).toFixed(2)} m²
|
||||
<div class="popup-actions">
|
||||
<button type="button" data-action="edit">Edit</button>
|
||||
<button type="button" class="btn-danger" data-action="delete">Hapus</button>
|
||||
</div>
|
||||
<div class="status" data-role="popup-status"></div>
|
||||
`);
|
||||
|
||||
layer.on('popupopen', (event) => {
|
||||
const popupElement = event.popup.getElement();
|
||||
const editButton = popupElement.querySelector('[data-action="edit"]');
|
||||
const deleteButton = popupElement.querySelector('[data-action="delete"]');
|
||||
const popupStatus = popupElement.querySelector('[data-role="popup-status"]');
|
||||
|
||||
editButton.addEventListener('click', () => {
|
||||
map.closePopup();
|
||||
openParcelModal(layer, 'update', layer.featureProps);
|
||||
});
|
||||
|
||||
deleteButton.addEventListener('click', async () => {
|
||||
if (!window.confirm('Hapus data parsil ini?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(popupStatus, 'Menghapus data...', '');
|
||||
deleteButton.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('api/parcel_delete.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: layer.featureId })
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || 'Gagal menghapus data parsil.');
|
||||
}
|
||||
|
||||
editableLayer.removeLayer(layer);
|
||||
map.closePopup();
|
||||
} catch (error) {
|
||||
setStatus(popupStatus, error.message || 'Gagal menghapus data.', 'error');
|
||||
deleteButton.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRoads() {
|
||||
const response = await fetch('api/road_read.php', {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || 'Gagal memuat data jalan.');
|
||||
}
|
||||
|
||||
result.data.forEach((item) => {
|
||||
if (!item.geometry_geojson) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layer = L.geoJSON(item.geometry_geojson).getLayers()[0];
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
layer.featureType = 'road';
|
||||
layer.featureId = item.id;
|
||||
layer.featureProps = {
|
||||
nama_jalan: item.nama_jalan,
|
||||
status: item.status,
|
||||
panjang_meter: item.panjang_meter
|
||||
};
|
||||
|
||||
attachRoadPopup(layer);
|
||||
editableLayer.addLayer(layer);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadParcels() {
|
||||
const response = await fetch('api/parcel_read.php', {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || 'Gagal memuat data parsil.');
|
||||
}
|
||||
|
||||
result.data.forEach((item) => {
|
||||
if (!item.geometry_geojson) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layer = L.geoJSON(item.geometry_geojson).getLayers()[0];
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
layer.featureType = 'parcel';
|
||||
layer.featureId = item.id;
|
||||
layer.featureProps = {
|
||||
nama_pemilik: item.nama_pemilik,
|
||||
status_kepemilikan: item.status_kepemilikan,
|
||||
luas_m2: item.luas_m2
|
||||
};
|
||||
|
||||
attachParcelPopup(layer);
|
||||
editableLayer.addLayer(layer);
|
||||
});
|
||||
}
|
||||
|
||||
const drawControl = new L.Control.Draw({
|
||||
position: 'topleft',
|
||||
draw: {
|
||||
polyline: {
|
||||
shapeOptions: {
|
||||
color: '#dc2626',
|
||||
weight: 5
|
||||
}
|
||||
},
|
||||
polygon: {
|
||||
allowIntersection: false,
|
||||
showArea: true,
|
||||
shapeOptions: {
|
||||
color: '#ea580c',
|
||||
fillOpacity: 0.35,
|
||||
weight: 3
|
||||
}
|
||||
},
|
||||
rectangle: false,
|
||||
circle: false,
|
||||
marker: false,
|
||||
circlemarker: false
|
||||
},
|
||||
edit: {
|
||||
featureGroup: editableLayer,
|
||||
remove: true
|
||||
}
|
||||
});
|
||||
|
||||
map.addControl(drawControl);
|
||||
|
||||
map.on(L.Draw.Event.CREATED, (event) => {
|
||||
const layer = event.layer;
|
||||
const geometryType = event.layerType;
|
||||
|
||||
editableLayer.addLayer(layer);
|
||||
|
||||
if (geometryType === 'polyline') {
|
||||
openRoadModal(layer, 'create', null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (geometryType === 'polygon') {
|
||||
openParcelModal(layer, 'create', null);
|
||||
return;
|
||||
}
|
||||
|
||||
editableLayer.removeLayer(layer);
|
||||
});
|
||||
|
||||
map.on(L.Draw.Event.EDITED, async (event) => {
|
||||
const layers = event.layers.getLayers();
|
||||
|
||||
for (const layer of layers) {
|
||||
try {
|
||||
if (layer.featureType === 'road') {
|
||||
const lengthMeters = calculatePolylineLengthMeters(layer.getLatLngs());
|
||||
const payload = {
|
||||
id: layer.featureId,
|
||||
nama_jalan: layer.featureProps.nama_jalan,
|
||||
status: layer.featureProps.status,
|
||||
panjang_meter: Number(lengthMeters.toFixed(2)),
|
||||
wkt: polylineToWkt(layer.getLatLngs())
|
||||
};
|
||||
|
||||
const response = await fetch('api/road_update.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || 'Gagal update geometri jalan.');
|
||||
}
|
||||
|
||||
layer.featureProps.panjang_meter = payload.panjang_meter;
|
||||
attachRoadPopup(layer);
|
||||
}
|
||||
|
||||
if (layer.featureType === 'parcel') {
|
||||
const areaM2 = calculatePolygonAreaMeters(layer.getLatLngs()[0]);
|
||||
const payload = {
|
||||
id: layer.featureId,
|
||||
nama_pemilik: layer.featureProps.nama_pemilik,
|
||||
status_kepemilikan: layer.featureProps.status_kepemilikan,
|
||||
luas_m2: Number(areaM2.toFixed(2)),
|
||||
wkt: polygonToWkt(layer.getLatLngs()[0])
|
||||
};
|
||||
|
||||
const response = await fetch('api/parcel_update.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || 'Gagal update geometri parsil.');
|
||||
}
|
||||
|
||||
layer.featureProps.luas_m2 = payload.luas_m2;
|
||||
attachParcelPopup(layer);
|
||||
}
|
||||
} catch (error) {
|
||||
window.alert(error.message || 'Gagal menyimpan update geometri.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
map.on(L.Draw.Event.DELETED, async (event) => {
|
||||
const layers = event.layers.getLayers();
|
||||
|
||||
for (const layer of layers) {
|
||||
if (!layer.featureType || !layer.featureId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoint = layer.featureType === 'road' ? 'api/road_delete.php' : 'api/parcel_delete.php';
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: layer.featureId })
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || 'Gagal menghapus data.');
|
||||
}
|
||||
} catch (error) {
|
||||
window.alert(error.message || 'Gagal sinkron delete ke database.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
featureForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
if (!pendingLayer || !pendingType) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(formStatus, 'Menyimpan data...', '');
|
||||
|
||||
try {
|
||||
if (pendingType === 'road') {
|
||||
const namaJalan = document.getElementById('nama_jalan').value.trim();
|
||||
const statusJalan = document.getElementById('status_jalan').value;
|
||||
const panjangMeter = Number(document.getElementById('panjang_meter').value);
|
||||
const wkt = polylineToWkt(pendingLayer.getLatLngs());
|
||||
|
||||
const endpoint = pendingMode === 'create' ? 'api/road_create.php' : 'api/road_update.php';
|
||||
const payload = {
|
||||
nama_jalan: namaJalan,
|
||||
status: statusJalan,
|
||||
panjang_meter: panjangMeter,
|
||||
wkt: wkt
|
||||
};
|
||||
|
||||
if (pendingMode === 'update') {
|
||||
payload.id = pendingLayer.featureId;
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || 'Gagal menyimpan data jalan.');
|
||||
}
|
||||
|
||||
pendingLayer.featureType = 'road';
|
||||
pendingLayer.featureId = pendingMode === 'create' ? result.id : pendingLayer.featureId;
|
||||
pendingLayer.featureProps = {
|
||||
nama_jalan: namaJalan,
|
||||
status: statusJalan,
|
||||
panjang_meter: panjangMeter
|
||||
};
|
||||
attachRoadPopup(pendingLayer);
|
||||
}
|
||||
|
||||
if (pendingType === 'parcel') {
|
||||
const namaPemilik = document.getElementById('nama_pemilik').value.trim();
|
||||
const statusKepemilikan = document.getElementById('status_kepemilikan').value;
|
||||
const luasM2 = Number(document.getElementById('luas_m2').value);
|
||||
const wkt = polygonToWkt(pendingLayer.getLatLngs()[0]);
|
||||
|
||||
const endpoint = pendingMode === 'create' ? 'api/parcel_create.php' : 'api/parcel_update.php';
|
||||
const payload = {
|
||||
nama_pemilik: namaPemilik,
|
||||
status_kepemilikan: statusKepemilikan,
|
||||
luas_m2: luasM2,
|
||||
wkt: wkt
|
||||
};
|
||||
|
||||
if (pendingMode === 'update') {
|
||||
payload.id = pendingLayer.featureId;
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.message || 'Gagal menyimpan data parsil.');
|
||||
}
|
||||
|
||||
pendingLayer.featureType = 'parcel';
|
||||
pendingLayer.featureId = pendingMode === 'create' ? result.id : pendingLayer.featureId;
|
||||
pendingLayer.featureProps = {
|
||||
nama_pemilik: namaPemilik,
|
||||
status_kepemilikan: statusKepemilikan,
|
||||
luas_m2: luasM2
|
||||
};
|
||||
attachParcelPopup(pendingLayer);
|
||||
}
|
||||
|
||||
setStatus(formStatus, 'Data berhasil disimpan.', 'success');
|
||||
setTimeout(() => closeModal(false), 350);
|
||||
} catch (error) {
|
||||
setStatus(formStatus, error.message || 'Gagal menyimpan data.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
cancelButton.addEventListener('click', () => closeModal(true));
|
||||
modalBackdrop.addEventListener('click', (event) => {
|
||||
if (event.target === modalBackdrop) {
|
||||
closeModal(true);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await loadRoads();
|
||||
await loadParcels();
|
||||
|
||||
if (editableLayer.getLayers().length > 0) {
|
||||
map.fitBounds(editableLayer.getBounds().pad(0.2));
|
||||
}
|
||||
} catch (error) {
|
||||
window.alert(error.message || 'Gagal memuat data spasial.');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user