Upload seluruh tugas SIG Point Line dan Polygon
This commit is contained in:
@@ -0,0 +1,866 @@
|
||||
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]);
|
||||
}
|
||||
Reference in New Issue
Block a user