867 lines
26 KiB
JavaScript
867 lines
26 KiB
JavaScript
let map;
|
|
let points = {};
|
|
let roads = {};
|
|
let parcels = {};
|
|
let currentMapClickCoords = null;
|
|
let drawMode = null;
|
|
let currentDrawCoords = [];
|
|
let currentDrawLayer = null;
|
|
const STORAGE_KEYS = {
|
|
points: 'mappoints_local_points',
|
|
roads: 'mappoints_local_roads',
|
|
parcels: 'mappoints_local_parcels'
|
|
};
|
|
|
|
// Initialize map and load stored data
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initMap();
|
|
setupModal();
|
|
loadAllData();
|
|
});
|
|
|
|
function setupModal() {
|
|
const addPointBtn = document.getElementById('add-point-btn');
|
|
const drawRoadBtn = document.getElementById('draw-road-btn');
|
|
const drawParcelBtn = document.getElementById('draw-parcel-btn');
|
|
const finishDrawBtn = document.getElementById('finish-draw-btn');
|
|
const cancelDrawBtn = document.getElementById('cancel-draw-btn');
|
|
const featureCloseBtn = document.querySelector('#feature-modal .close');
|
|
const featureForm = document.getElementById('feature-form');
|
|
const roadCloseBtn = document.querySelector('.road-close');
|
|
const roadForm = document.getElementById('road-form');
|
|
const parcelCloseBtn = document.querySelector('.parcel-close');
|
|
const parcelForm = document.getElementById('parcel-form');
|
|
|
|
addPointBtn.onclick = function() {
|
|
cancelDraw();
|
|
openPointModal(null);
|
|
};
|
|
|
|
drawRoadBtn.onclick = function() {
|
|
startDraw('road');
|
|
};
|
|
|
|
drawParcelBtn.onclick = function() {
|
|
startDraw('parcel');
|
|
};
|
|
|
|
finishDrawBtn.onclick = function() {
|
|
finishDraw();
|
|
};
|
|
|
|
cancelDrawBtn.onclick = function() {
|
|
cancelDraw();
|
|
};
|
|
|
|
featureCloseBtn.onclick = function() {
|
|
closePointModal();
|
|
};
|
|
|
|
featureForm.onsubmit = function(e) {
|
|
e.preventDefault();
|
|
submitPointForm();
|
|
};
|
|
|
|
roadCloseBtn.onclick = closeRoadModal;
|
|
roadForm.onsubmit = function(e) {
|
|
e.preventDefault();
|
|
submitRoadForm();
|
|
};
|
|
|
|
parcelCloseBtn.onclick = closeParcelModal;
|
|
parcelForm.onsubmit = function(e) {
|
|
e.preventDefault();
|
|
submitParcelForm();
|
|
};
|
|
}
|
|
|
|
function initMap() {
|
|
map = L.map('map').setView([-0.0225, 109.3425], 13);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors',
|
|
maxZoom: 19
|
|
}).addTo(map);
|
|
|
|
map.on('click', function(e) {
|
|
if (drawMode) {
|
|
addDrawVertex(e.latlng);
|
|
} else {
|
|
openPointModal(e.latlng);
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadAllData() {
|
|
loadPoints();
|
|
loadRoads();
|
|
loadParcels();
|
|
}
|
|
|
|
function loadPoints() {
|
|
fetch('api/get_points.php')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success && Array.isArray(data.markers)) {
|
|
data.markers.forEach(point => {
|
|
displayPoint(point.id, point.name, point.latitude, point.longitude, point.open_24_hours);
|
|
});
|
|
}
|
|
})
|
|
.catch(error => console.warn('Unable to load points from backend:', error))
|
|
.finally(() => {
|
|
loadLocalPoints().forEach(point => {
|
|
displayPoint(point.id, point.name, point.latitude, point.longitude, point.open_24_hours, true);
|
|
});
|
|
updateCounts();
|
|
});
|
|
}
|
|
|
|
function loadRoads() {
|
|
fetch('api/get_roads.php')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success && Array.isArray(data.roads)) {
|
|
data.roads.forEach(road => {
|
|
displayRoad(road.id, road.name, road.status, parseFloat(road.length), JSON.parse(road.coordinates));
|
|
});
|
|
}
|
|
})
|
|
.catch(error => console.warn('Unable to load roads from backend:', error))
|
|
.finally(() => {
|
|
loadLocalRoads().forEach(road => {
|
|
displayRoad(road.id, road.name, road.status, road.length, road.coordinates, true);
|
|
});
|
|
updateCounts();
|
|
});
|
|
}
|
|
|
|
function loadParcels() {
|
|
fetch('api/get_parcels.php')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success && Array.isArray(data.parcels)) {
|
|
data.parcels.forEach(parcel => {
|
|
displayParcel(parcel.id, parcel.owner_status, parseFloat(parcel.area), JSON.parse(parcel.coordinates));
|
|
});
|
|
}
|
|
})
|
|
.catch(error => console.warn('Unable to load parcels from backend:', error))
|
|
.finally(() => {
|
|
loadLocalParcels().forEach(parcel => {
|
|
displayParcel(parcel.id, parcel.owner_status, parcel.area, parcel.coordinates, true);
|
|
});
|
|
updateCounts();
|
|
});
|
|
}
|
|
|
|
function loadLocalPoints() {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEYS.points);
|
|
const data = raw ? JSON.parse(raw) : [];
|
|
return Array.isArray(data) ? data : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function loadLocalRoads() {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEYS.roads);
|
|
const data = raw ? JSON.parse(raw) : [];
|
|
return Array.isArray(data) ? data : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function loadLocalParcels() {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEYS.parcels);
|
|
const data = raw ? JSON.parse(raw) : [];
|
|
return Array.isArray(data) ? data : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function persistLocalPoints() {
|
|
const localPoints = Object.values(points)
|
|
.filter(item => item._local)
|
|
.map(item => ({
|
|
id: item._localId,
|
|
name: item._name,
|
|
latitude: item.getLatLng().lat,
|
|
longitude: item.getLatLng().lng,
|
|
open_24_hours: item._open24
|
|
}));
|
|
localStorage.setItem(STORAGE_KEYS.points, JSON.stringify(localPoints));
|
|
}
|
|
|
|
function persistLocalRoads() {
|
|
const localRoads = Object.values(roads)
|
|
.filter(item => item._local)
|
|
.map(item => ({
|
|
id: item._localId,
|
|
name: item._name,
|
|
status: item._status,
|
|
length: item._length,
|
|
coordinates: item._coordinates
|
|
}));
|
|
localStorage.setItem(STORAGE_KEYS.roads, JSON.stringify(localRoads));
|
|
}
|
|
|
|
function persistLocalParcels() {
|
|
const localParcels = Object.values(parcels)
|
|
.filter(item => item._local)
|
|
.map(item => ({
|
|
id: item._localId,
|
|
owner_status: item._ownerStatus,
|
|
area: item._area,
|
|
coordinates: item._coordinates
|
|
}));
|
|
localStorage.setItem(STORAGE_KEYS.parcels, JSON.stringify(localParcels));
|
|
}
|
|
|
|
function generateLocalId(prefix) {
|
|
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
|
}
|
|
|
|
function startDraw(type) {
|
|
cancelDraw();
|
|
drawMode = type;
|
|
currentDrawCoords = [];
|
|
showDrawControls(true);
|
|
showNotification(type === 'road' ? 'Draw road vertices on the map.' : 'Draw parcel vertices on the map.');
|
|
}
|
|
|
|
function showDrawControls(show) {
|
|
const drawActions = document.getElementById('draw-actions');
|
|
if (drawActions) {
|
|
drawActions.classList.toggle('hidden', !show);
|
|
}
|
|
}
|
|
|
|
function cancelDraw() {
|
|
drawMode = null;
|
|
currentDrawCoords = [];
|
|
if (currentDrawLayer) {
|
|
map.removeLayer(currentDrawLayer);
|
|
currentDrawLayer = null;
|
|
}
|
|
showDrawControls(false);
|
|
}
|
|
|
|
function finishDraw() {
|
|
if (!drawMode) {
|
|
return;
|
|
}
|
|
if (drawMode === 'road') {
|
|
if (currentDrawCoords.length < 2) {
|
|
showNotification('Road must have at least 2 points.', true);
|
|
return;
|
|
}
|
|
openRoadModal();
|
|
} else if (drawMode === 'parcel') {
|
|
if (currentDrawCoords.length < 3) {
|
|
showNotification('Parcel must have at least 3 points.', true);
|
|
return;
|
|
}
|
|
openParcelModal();
|
|
}
|
|
}
|
|
|
|
function addDrawVertex(latlng) {
|
|
currentDrawCoords.push(latlng);
|
|
if (currentDrawLayer) {
|
|
map.removeLayer(currentDrawLayer);
|
|
currentDrawLayer = null;
|
|
}
|
|
if (drawMode === 'road') {
|
|
currentDrawLayer = L.polyline(currentDrawCoords, {
|
|
color: '#ff9800',
|
|
dashArray: '5,8',
|
|
weight: 5
|
|
}).addTo(map);
|
|
} else if (drawMode === 'parcel') {
|
|
currentDrawLayer = L.polygon(currentDrawCoords, {
|
|
color: '#4CAF50',
|
|
dashArray: '5,8',
|
|
weight: 4,
|
|
fillOpacity: 0.15
|
|
}).addTo(map);
|
|
}
|
|
showNotification(`Vertex added (${currentDrawCoords.length})`);
|
|
}
|
|
|
|
function openPointModal(coords) {
|
|
currentMapClickCoords = coords;
|
|
const modal = document.getElementById('feature-modal');
|
|
const form = document.getElementById('feature-form');
|
|
const latInput = document.getElementById('feature-latitude');
|
|
const lngInput = document.getElementById('feature-longitude');
|
|
const nameInput = document.getElementById('feature-name');
|
|
const statusGroup = document.getElementById('field-status-group');
|
|
const checkboxGroup = document.getElementById('feature-checkbox-group');
|
|
const modalTitle = document.getElementById('modal-title');
|
|
const formInfo = document.getElementById('feature-form-info');
|
|
|
|
form.reset();
|
|
statusGroup.style.display = 'none';
|
|
checkboxGroup.style.display = 'flex';
|
|
formInfo.classList.remove('show');
|
|
|
|
if (coords) {
|
|
latInput.value = coords.lat.toFixed(6);
|
|
lngInput.value = coords.lng.toFixed(6);
|
|
latInput.readOnly = true;
|
|
lngInput.readOnly = true;
|
|
modalTitle.textContent = 'Create Point';
|
|
formInfo.textContent = 'Coordinates auto-filled from map click.';
|
|
formInfo.classList.add('show');
|
|
} else {
|
|
latInput.value = '';
|
|
lngInput.value = '';
|
|
latInput.readOnly = false;
|
|
lngInput.readOnly = false;
|
|
modalTitle.textContent = 'Add New Point';
|
|
formInfo.textContent = 'Enter coordinates manually or click on the map.';
|
|
formInfo.classList.add('show');
|
|
}
|
|
|
|
nameInput.focus();
|
|
modal.classList.add('show');
|
|
}
|
|
|
|
function closePointModal() {
|
|
const modal = document.getElementById('feature-modal');
|
|
modal.classList.remove('show');
|
|
currentMapClickCoords = null;
|
|
}
|
|
|
|
function closeFeatureModal() {
|
|
closePointModal();
|
|
}
|
|
|
|
function submitPointForm() {
|
|
const name = document.getElementById('feature-name').value.trim();
|
|
const latitude = parseFloat(document.getElementById('feature-latitude').value);
|
|
const longitude = parseFloat(document.getElementById('feature-longitude').value);
|
|
const open_24_hours = document.getElementById('feature-24hours').checked ? 1 : 0;
|
|
|
|
if (!name) {
|
|
showNotification('Please enter a point name.', true);
|
|
return;
|
|
}
|
|
if (isNaN(latitude) || isNaN(longitude)) {
|
|
showNotification('Please enter valid coordinates.', true);
|
|
return;
|
|
}
|
|
|
|
addPoint(name, latitude, longitude, open_24_hours);
|
|
closePointModal();
|
|
}
|
|
|
|
function addPoint(name, lat, lng, open_24_hours) {
|
|
fetch('api/save_point.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, latitude: lat, longitude: lng, open_24_hours })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
displayPoint(data.id, data.name, data.latitude, data.longitude, data.open_24_hours);
|
|
showNotification('Point saved successfully.');
|
|
updateCounts();
|
|
} else {
|
|
addLocalPoint(name, lat, lng, open_24_hours);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
addLocalPoint(name, lat, lng, open_24_hours);
|
|
});
|
|
}
|
|
|
|
function addLocalPoint(name, lat, lng, open_24_hours) {
|
|
const id = generateLocalId('point');
|
|
displayPoint(id, name, lat, lng, open_24_hours, true);
|
|
persistLocalPoints();
|
|
showNotification('Point saved locally.');
|
|
updateCounts();
|
|
}
|
|
|
|
function displayPoint(id, name, lat, lng, open_24_hours, isLocal = false) {
|
|
if (points[id]) {
|
|
map.removeLayer(points[id]);
|
|
delete points[id];
|
|
}
|
|
|
|
const marker = L.marker([lat, lng], { draggable: true }).addTo(map);
|
|
marker._local = isLocal;
|
|
marker._localId = id;
|
|
marker._name = name;
|
|
marker._open24 = open_24_hours;
|
|
marker._type = 'point';
|
|
|
|
marker.bindPopup(getPointPopupContent(id, name, lat, lng, open_24_hours));
|
|
marker.on('popupopen', function(e) {
|
|
bindPopupDeleteAction(e.popup.getElement(), 'point', id);
|
|
});
|
|
marker.on('dragend', function(e) {
|
|
const position = e.target.getLatLng();
|
|
updatePointPosition(id, position.lat, position.lng);
|
|
});
|
|
|
|
points[id] = marker;
|
|
}
|
|
|
|
function updatePointPosition(id, lat, lng) {
|
|
const marker = points[id];
|
|
if (!marker) {
|
|
return;
|
|
}
|
|
|
|
marker._name = marker._name || marker._localId;
|
|
marker.bindPopup(getPointPopupContent(id, marker._name, lat, lng, marker._open24));
|
|
marker.setLatLng([lat, lng]);
|
|
|
|
if (marker._local) {
|
|
persistLocalPoints();
|
|
}
|
|
}
|
|
|
|
function getPointPopupContent(id, name, lat, lng, open_24_hours) {
|
|
const hoursText = open_24_hours ? '🕐 Open 24 Hours' : '🕐 Limited Hours';
|
|
return `
|
|
<div class="marker-popup" data-feature-id="${escapeHtml(String(id))}" data-feature-type="point">
|
|
<div class="marker-popup-name">${escapeHtml(name)}</div>
|
|
<div class="marker-popup-hours">${hoursText}</div>
|
|
<div class="marker-popup-info">
|
|
<span>Lat: ${lat.toFixed(4)}</span><br>
|
|
<span>Lng: ${lng.toFixed(4)}</span>
|
|
</div>
|
|
<button type="button" class="delete-feature-btn">Delete</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function openRoadModal() {
|
|
const modal = document.getElementById('road-modal');
|
|
const roadLength = document.getElementById('road-length');
|
|
const roadFormInfo = document.getElementById('road-form-info');
|
|
const roadName = document.getElementById('road-name');
|
|
|
|
roadName.value = '';
|
|
roadLength.value = computeRoadLength(currentDrawCoords).toFixed(2);
|
|
roadFormInfo.textContent = `Calculated automatically from ${currentDrawCoords.length} vertices.`;
|
|
modal.classList.add('show');
|
|
}
|
|
|
|
function closeRoadModal() {
|
|
const modal = document.getElementById('road-modal');
|
|
modal.classList.remove('show');
|
|
}
|
|
|
|
function submitRoadForm() {
|
|
const name = document.getElementById('road-name').value.trim();
|
|
const status = document.getElementById('road-status').value;
|
|
const length = parseFloat(document.getElementById('road-length').value);
|
|
|
|
if (!name) {
|
|
showNotification('Please enter a road name.', true);
|
|
return;
|
|
}
|
|
if (!status) {
|
|
showNotification('Please choose a road status.', true);
|
|
return;
|
|
}
|
|
|
|
addRoad(name, status, currentDrawCoords.slice(), length);
|
|
closeRoadModal();
|
|
cancelDraw();
|
|
}
|
|
|
|
function addRoad(name, status, coordinates, length) {
|
|
fetch('api/save_road.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, status, length, coordinates })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
displayRoad(data.id, data.name, data.status, parseFloat(data.length), JSON.parse(data.coordinates));
|
|
showNotification('Road saved successfully.');
|
|
updateCounts();
|
|
} else {
|
|
addLocalRoad(name, status, length, coordinates);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
addLocalRoad(name, status, length, coordinates);
|
|
});
|
|
}
|
|
|
|
function addLocalRoad(name, status, length, coordinates) {
|
|
const id = generateLocalId('road');
|
|
displayRoad(id, name, status, length, coordinates, true);
|
|
persistLocalRoads();
|
|
showNotification('Road saved locally.');
|
|
updateCounts();
|
|
}
|
|
|
|
function displayRoad(id, name, status, length, coordinates, isLocal = false) {
|
|
if (roads[id]) {
|
|
map.removeLayer(roads[id]);
|
|
delete roads[id];
|
|
}
|
|
|
|
const latlngs = coordinates.map(coord => L.latLng(coord.lat, coord.lng));
|
|
const layer = L.polyline(latlngs, {
|
|
color: getRoadColor(status),
|
|
weight: 6
|
|
}).addTo(map);
|
|
|
|
layer._local = isLocal;
|
|
layer._localId = id;
|
|
layer._name = name;
|
|
layer._status = status;
|
|
layer._length = length;
|
|
layer._coordinates = coordinates;
|
|
layer._type = 'road';
|
|
|
|
layer.bindPopup(getRoadPopupContent(id, name, status, length));
|
|
layer.on('popupopen', function(e) {
|
|
bindPopupDeleteAction(e.popup.getElement(), 'road', id);
|
|
});
|
|
|
|
roads[id] = layer;
|
|
}
|
|
|
|
function getRoadPopupContent(id, name, status, length) {
|
|
return `
|
|
<div class="marker-popup" data-feature-id="${escapeHtml(String(id))}" data-feature-type="road">
|
|
<div class="marker-popup-name">${escapeHtml(name)}</div>
|
|
<div class="marker-popup-hours">${escapeHtml(status)}</div>
|
|
<div class="marker-popup-info">
|
|
<span>Length: ${length.toFixed(2)} m</span>
|
|
</div>
|
|
<button type="button" class="delete-feature-btn">Delete</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function openParcelModal() {
|
|
const modal = document.getElementById('parcel-modal');
|
|
const parcelArea = document.getElementById('parcel-area');
|
|
const parcelFormInfo = document.getElementById('parcel-form-info');
|
|
|
|
const area = computeParcelArea(currentDrawCoords);
|
|
parcelArea.value = area.toFixed(2);
|
|
parcelFormInfo.textContent = `Calculated automatically from ${currentDrawCoords.length} vertices.`;
|
|
modal.classList.add('show');
|
|
}
|
|
|
|
function closeParcelModal() {
|
|
const modal = document.getElementById('parcel-modal');
|
|
modal.classList.remove('show');
|
|
}
|
|
|
|
function submitParcelForm() {
|
|
const ownerStatus = document.getElementById('parcel-owner').value;
|
|
const area = parseFloat(document.getElementById('parcel-area').value);
|
|
|
|
if (!ownerStatus) {
|
|
showNotification('Please choose an ownership status.', true);
|
|
return;
|
|
}
|
|
|
|
addParcel(ownerStatus, area, currentDrawCoords.slice());
|
|
closeParcelModal();
|
|
cancelDraw();
|
|
}
|
|
|
|
function addParcel(ownerStatus, area, coordinates) {
|
|
fetch('api/save_parcel.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ owner_status: ownerStatus, area, coordinates })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
displayParcel(data.id, data.owner_status, parseFloat(data.area), JSON.parse(data.coordinates));
|
|
showNotification('Parcel saved successfully.');
|
|
updateCounts();
|
|
} else {
|
|
addLocalParcel(ownerStatus, area, coordinates);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
addLocalParcel(ownerStatus, area, coordinates);
|
|
});
|
|
}
|
|
|
|
function addLocalParcel(ownerStatus, area, coordinates) {
|
|
const id = generateLocalId('parcel');
|
|
displayParcel(id, ownerStatus, area, coordinates, true);
|
|
persistLocalParcels();
|
|
showNotification('Parcel saved locally.');
|
|
updateCounts();
|
|
}
|
|
|
|
function displayParcel(id, ownerStatus, area, coordinates, isLocal = false) {
|
|
if (parcels[id]) {
|
|
map.removeLayer(parcels[id]);
|
|
delete parcels[id];
|
|
}
|
|
|
|
const latlngs = coordinates.map(coord => L.latLng(coord.lat, coord.lng));
|
|
const layer = L.polygon(latlngs, {
|
|
color: getParcelColor(ownerStatus),
|
|
fillColor: getParcelColor(ownerStatus),
|
|
fillOpacity: 0.3,
|
|
weight: 4
|
|
}).addTo(map);
|
|
|
|
layer._local = isLocal;
|
|
layer._localId = id;
|
|
layer._ownerStatus = ownerStatus;
|
|
layer._area = area;
|
|
layer._coordinates = coordinates;
|
|
layer._type = 'parcel';
|
|
|
|
layer.bindPopup(getParcelPopupContent(id, ownerStatus, area));
|
|
layer.on('popupopen', function(e) {
|
|
bindPopupDeleteAction(e.popup.getElement(), 'parcel', id);
|
|
});
|
|
|
|
parcels[id] = layer;
|
|
}
|
|
|
|
function getParcelPopupContent(id, ownerStatus, area) {
|
|
return `
|
|
<div class="marker-popup" data-feature-id="${escapeHtml(String(id))}" data-feature-type="parcel">
|
|
<div class="marker-popup-name">${escapeHtml(ownerStatus)}</div>
|
|
<div class="marker-popup-hours">Parcel Status</div>
|
|
<div class="marker-popup-info">
|
|
<span>Area: ${area.toFixed(2)} m²</span>
|
|
</div>
|
|
<button type="button" class="delete-feature-btn">Delete</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function bindPopupDeleteAction(popupElement, type, id) {
|
|
if (!popupElement) {
|
|
return;
|
|
}
|
|
const deleteBtn = popupElement.querySelector('.delete-feature-btn');
|
|
if (deleteBtn) {
|
|
deleteBtn.onclick = function() {
|
|
deleteFeature(type, id);
|
|
};
|
|
}
|
|
}
|
|
|
|
function deleteFeature(type, id) {
|
|
if (!confirm('Are you sure you want to delete this item?')) {
|
|
return;
|
|
}
|
|
if (type === 'point') {
|
|
deletePoint(id);
|
|
} else if (type === 'road') {
|
|
deleteRoad(id);
|
|
} else if (type === 'parcel') {
|
|
deleteParcel(id);
|
|
}
|
|
}
|
|
|
|
function deletePoint(id) {
|
|
const point = points[id];
|
|
if (!point) {
|
|
return;
|
|
}
|
|
if (point._local) {
|
|
map.removeLayer(point);
|
|
delete points[id];
|
|
persistLocalPoints();
|
|
updateCounts();
|
|
showNotification('Point deleted locally.');
|
|
return;
|
|
}
|
|
fetch('api/delete_point.php', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
map.removeLayer(point);
|
|
delete points[id];
|
|
updateCounts();
|
|
showNotification('Point deleted successfully.');
|
|
} else {
|
|
showNotification('Unable to delete point on server.', true);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
showNotification('Server unavailable, point deletion aborted.', true);
|
|
});
|
|
}
|
|
|
|
function deleteRoad(id) {
|
|
const road = roads[id];
|
|
if (!road) {
|
|
return;
|
|
}
|
|
if (road._local) {
|
|
map.removeLayer(road);
|
|
delete roads[id];
|
|
persistLocalRoads();
|
|
updateCounts();
|
|
showNotification('Road deleted locally.');
|
|
return;
|
|
}
|
|
fetch('api/delete_road.php', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
map.removeLayer(road);
|
|
delete roads[id];
|
|
updateCounts();
|
|
showNotification('Road deleted successfully.');
|
|
} else {
|
|
showNotification('Unable to delete road on server.', true);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
showNotification('Server unavailable, road deletion aborted.', true);
|
|
});
|
|
}
|
|
|
|
function deleteParcel(id) {
|
|
const parcel = parcels[id];
|
|
if (!parcel) {
|
|
return;
|
|
}
|
|
if (parcel._local) {
|
|
map.removeLayer(parcel);
|
|
delete parcels[id];
|
|
persistLocalParcels();
|
|
updateCounts();
|
|
showNotification('Parcel deleted locally.');
|
|
return;
|
|
}
|
|
fetch('api/delete_parcel.php', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
map.removeLayer(parcel);
|
|
delete parcels[id];
|
|
updateCounts();
|
|
showNotification('Parcel deleted successfully.');
|
|
} else {
|
|
showNotification('Unable to delete parcel on server.', true);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
showNotification('Server unavailable, parcel deletion aborted.', true);
|
|
});
|
|
}
|
|
|
|
function updateCounts() {
|
|
document.getElementById('marker-count').textContent = Object.keys(points).length;
|
|
document.getElementById('road-count').textContent = Object.keys(roads).length;
|
|
document.getElementById('parcel-count').textContent = Object.keys(parcels).length;
|
|
}
|
|
|
|
function getRoadColor(status) {
|
|
switch (status) {
|
|
case 'Jalan Nasional':
|
|
return '#E53935';
|
|
case 'Jalan Provinsi':
|
|
return '#FB8C00';
|
|
case 'Jalan Kabupaten':
|
|
return '#1E88E5';
|
|
default:
|
|
return '#9E9E9E';
|
|
}
|
|
}
|
|
|
|
function getParcelColor(ownerStatus) {
|
|
switch (ownerStatus) {
|
|
case 'SHM':
|
|
return '#388E3C';
|
|
case 'HGB':
|
|
return '#8E24AA';
|
|
case 'HGU':
|
|
return '#00897B';
|
|
case 'HP':
|
|
return '#0288D1';
|
|
default:
|
|
return '#616161';
|
|
}
|
|
}
|
|
|
|
function computeRoadLength(latlngs) {
|
|
if (!latlngs || latlngs.length < 2) {
|
|
return 0;
|
|
}
|
|
let total = 0;
|
|
for (let i = 1; i < latlngs.length; i++) {
|
|
total += map.distance(latlngs[i - 1], latlngs[i]);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
function computeParcelArea(latlngs) {
|
|
if (!latlngs || latlngs.length < 3) {
|
|
return 0;
|
|
}
|
|
if (window.L && L.GeometryUtil && typeof L.GeometryUtil.geodesicArea === 'function') {
|
|
return L.GeometryUtil.geodesicArea(latlngs);
|
|
}
|
|
return approximatePolygonArea(latlngs);
|
|
}
|
|
|
|
function approximatePolygonArea(latlngs) {
|
|
const rad = degrees => degrees * Math.PI / 180;
|
|
let area = 0;
|
|
const radius = 6378137;
|
|
for (let i = 0, len = latlngs.length; i < len; i++) {
|
|
const p1 = latlngs[i];
|
|
const p2 = latlngs[(i + 1) % len];
|
|
area += rad(p2.lng - p1.lng) * (2 + Math.sin(rad(p1.lat)) + Math.sin(rad(p2.lat)));
|
|
}
|
|
return Math.abs(area * radius * radius / 2);
|
|
}
|
|
|
|
function showNotification(message, isError = false) {
|
|
const notification = document.createElement('div');
|
|
notification.className = 'notification' + (isError ? ' error' : '');
|
|
notification.textContent = message;
|
|
document.body.appendChild(notification);
|
|
setTimeout(() => notification.remove(), 3000);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return String(text).replace(/[&<>"']/g, m => map[m]);
|
|
}
|