Files
TUGAS_SIG_JOHANESSATRIACHRI…/Projek Jalan/Projek/js/app.js
T

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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return String(text).replace(/[&<>"']/g, m => map[m]);
}