1220 lines
50 KiB
HTML
1220 lines
50 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WebGIS App</title>
|
|
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
|
|
<style>
|
|
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
|
|
#map { height: 100vh; width: 100%; }
|
|
|
|
#sidebar {
|
|
position: fixed;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: #f8f9fa;
|
|
padding: 8px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
z-index: 1000;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
max-width: 500px;
|
|
}
|
|
|
|
.tool-btn {
|
|
padding: 6px 10px;
|
|
background: #007bff;
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tool-btn:hover { background: #0056b3; }
|
|
.tool-btn.active { background: #004085; border: 2px solid #000; box-shadow: 0 0 8px rgba(0,0,0,0.5); }
|
|
.cancel-btn { background: #dc3545; }
|
|
.cancel-btn:hover { background: #c82333; }
|
|
|
|
#sidebar h2 { display: none; }
|
|
#sidebar p { display: none; }
|
|
#sidebar hr { display: none; }
|
|
|
|
.popup-form { display: flex; flex-direction: column; gap: 10px; min-width: 220px; }
|
|
.popup-form input, .popup-form select { padding: 8px; font-size: 14px; }
|
|
.popup-form button { padding: 8px 12px; background: #28a745; color: white; border: none; cursor: pointer; border-radius: 4px; }
|
|
.popup-form button:hover { background: #218838; }
|
|
|
|
.edit-mode-btn { background: #ff9800; color: white; }
|
|
.edit-mode-btn:hover { background: #e68900; }
|
|
|
|
.legend { background: white; padding: 10px; border-radius: 5px; box-shadow: 0 1px 5px rgba(0,0,0,0.4); font-size: 12px; }
|
|
.legend span { display: inline-block; width: 12px; height: 12px; margin-right: 5px; }
|
|
|
|
/* Edit Mode Styles */
|
|
#edit-mode-indicator {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: #ff9800;
|
|
color: white;
|
|
padding: 12px 16px;
|
|
border-radius: 4px;
|
|
z-index: 1000;
|
|
font-weight: bold;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
display: none;
|
|
font-size: 13px;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.vertex-marker { z-index: 999; }
|
|
.vertex-marker.dragging { filter: drop-shadow(0 0 4px #ff0000); }
|
|
.add-point-hint { z-index: 998; cursor: crosshair; }
|
|
|
|
.edit-popup { background: white; border-left: 4px solid #ff9800; padding: 12px; border-radius: 4px; }
|
|
.edit-popup h4 { margin: 0 0 10px 0; color: #333; }
|
|
.edit-popup .instructions { font-size: 12px; color: #666; margin-bottom: 10px; padding: 8px; background: #f5f5f5; border-radius: 3px; }
|
|
.edit-popup .button-group { display: flex; gap: 5px; }
|
|
.edit-popup button { padding: 8px 12px; font-size: 12px; border: none; border-radius: 3px; cursor: pointer; flex: 1; }
|
|
.finish-btn { background: #28a745; color: white; }
|
|
.finish-btn:hover { background: #218838; }
|
|
.cancel-edit-btn { background: #6c757d; color: white; }
|
|
.cancel-edit-btn:hover { background: #5a6268; }
|
|
|
|
#edit-panel {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: white;
|
|
border: 3px solid #ff9800;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
z-index: 1001;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
display: none;
|
|
max-width: 350px;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
#edit-panel h4 {
|
|
margin: 0 0 12px 0;
|
|
color: #333;
|
|
font-size: 16px;
|
|
text-align: center;
|
|
}
|
|
|
|
#edit-panel .instructions {
|
|
font-size: 13px;
|
|
color: #555;
|
|
margin-bottom: 12px;
|
|
padding: 10px;
|
|
background: #f9f9f9;
|
|
border-radius: 4px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
#edit-panel .button-group {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
#edit-panel .finish-btn,
|
|
#edit-panel .cancel-edit-btn {
|
|
flex: 1;
|
|
padding: 10px 12px;
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
#edit-panel .finish-btn {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
#edit-panel .finish-btn:hover {
|
|
background: #218838;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
#edit-panel .cancel-edit-btn {
|
|
background: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
#edit-panel .cancel-edit-btn:hover {
|
|
background: #c82333;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
#edit-panel .keyboard-hint {
|
|
font-size: 11px;
|
|
text-align: center;
|
|
color: #999;
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="map"></div>
|
|
<div id="edit-mode-indicator">✏️ EDIT MODE - Drag points to move • Click point to delete • Click between points to add • Press ESC to exit</div>
|
|
|
|
<div id="edit-panel">
|
|
<h4>✏️ SHAPE EDITOR</h4>
|
|
<div class="instructions">
|
|
🔴 <strong>Drag red points</strong> to move them<br>
|
|
❌ <strong>Click red point</strong> to delete<br>
|
|
🟢 <strong>Click green dots</strong> to add points
|
|
</div>
|
|
<div class="button-group">
|
|
<button class="finish-btn" id="finish-btn">✓ Save Changes</button>
|
|
<button class="cancel-edit-btn" id="cancel-btn">✕ Cancel</button>
|
|
</div>
|
|
<div class="keyboard-hint">
|
|
💡 Tip: Press <strong>ESC</strong> to cancel anytime
|
|
</div>
|
|
</div>
|
|
|
|
<div id="sidebar">
|
|
<h2>GIS Digitizer</h2>
|
|
<p>1. Select a tool.<br>2. Click on the map to draw.<br>3. Double-click to finish and save.</p>
|
|
<hr>
|
|
<button id="btn-road" class="tool-btn" onclick="setMode('road')">📍 Mark Road (Polyline)</button>
|
|
<button id="btn-land" class="tool-btn" onclick="setMode('land')">🟩 Mark Land (Polygon)</button>
|
|
<button class="tool-btn cancel-btn" onclick="setMode('none')">❌ Cancel Drawing</button>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
|
|
|
|
<script>
|
|
// Initialize Map (Centered on Pontianak, Indonesia)
|
|
const map = L.map('map').setView([-0.0227, 109.3340], 13);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© OpenStreetMap'
|
|
}).addTo(map);
|
|
|
|
map.doubleClickZoom.disable();
|
|
|
|
let currentMode = 'none';
|
|
let currentPoints = [];
|
|
let tempLayer = null;
|
|
let previewLayer = null;
|
|
let currentMousePos = null;
|
|
let clickTimeout = null;
|
|
let clickCount = 0;
|
|
|
|
// --- EDIT MODE STATE ---
|
|
let editModeActive = false;
|
|
let editingShape = null;
|
|
let editingData = null;
|
|
let originalCoordinates = null;
|
|
let vertexMarkers = [];
|
|
let midpointMarkers = [];
|
|
let addPointHintLayer = null;
|
|
let isDraggingVertex = false;
|
|
let editModeKeyHandler = null;
|
|
|
|
// --- STYLING HELPERS ---
|
|
function getRoadColor(type) {
|
|
return type === 'Jalan nasional' ? '#dc3545' :
|
|
type === 'Jalan provinsi' ? '#fd7e14' : '#28a745'; // Red, Orange, Green
|
|
}
|
|
|
|
function getLandColor(status) {
|
|
return status === 'Sertifikat Hak Milik (SHM)' ? '#007bff' :
|
|
status === 'Sertifikat Hak Guna Bangunan (HGB)' ? '#6f42c1' :
|
|
status === 'Sertifikat Hak Guna Usaha (HGU)' ? '#17a2b8' : '#ffc107'; // Blue, Purple, Teal, Yellow
|
|
}
|
|
|
|
// --- INITIAL DATA LOAD ---
|
|
window.onload = async () => {
|
|
await loadRoads();
|
|
await loadLands();
|
|
};
|
|
|
|
async function loadRoads() {
|
|
const response = await fetch('/api/roads');
|
|
const roads = await response.json();
|
|
|
|
roads.forEach(road => {
|
|
const coords = typeof road.coordinates === 'string' ? JSON.parse(road.coordinates) : road.coordinates;
|
|
const polyline = L.polyline(coords, { color: getRoadColor(road.road_type), weight: 4 }).addTo(map);
|
|
|
|
// Store road data on the polyline for later access
|
|
polyline.roadData = road;
|
|
|
|
polyline.on('click', function(e) {
|
|
L.DomEvent.preventDefault(e);
|
|
L.DomEvent.stopPropagation(e);
|
|
showRoadEditPopup(road, polyline);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function loadLands() {
|
|
const response = await fetch('/api/lands');
|
|
const lands = await response.json();
|
|
|
|
lands.forEach(land => {
|
|
const coords = typeof land.coordinates === 'string' ? JSON.parse(land.coordinates) : land.coordinates;
|
|
const polygon = L.polygon(coords, { color: getLandColor(land.ownership_status), fillOpacity: 0.4 }).addTo(map);
|
|
|
|
// Store land data on the polygon for later access
|
|
polygon.landData = land;
|
|
|
|
polygon.on('click', function(e) {
|
|
L.DomEvent.preventDefault(e);
|
|
L.DomEvent.stopPropagation(e);
|
|
showLandEditPopup(land, polygon);
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- EDIT & DELETE POPUPS ---
|
|
function showRoadEditPopup(road, polyline) {
|
|
const formHTML = `
|
|
<div class="popup-form" onclick="event.stopPropagation();">
|
|
<h3 style="margin:0 0 5px 0;">Edit Road</h3>
|
|
<label><strong>Name:</strong></label>
|
|
<input type="text" id="editRoadName" value="${road.name}" required>
|
|
<label><strong>Type:</strong></label>
|
|
<select id="editRoadType">
|
|
<option value="Jalan nasional" ${road.road_type === 'Jalan nasional' ? 'selected' : ''}>Jalan Nasional</option>
|
|
<option value="Jalan provinsi" ${road.road_type === 'Jalan provinsi' ? 'selected' : ''}>Jalan Provinsi</option>
|
|
<option value="Jalan kabupaten" ${road.road_type === 'Jalan kabupaten' ? 'selected' : ''}>Jalan Kabupaten</option>
|
|
</select>
|
|
<small>Distance: ${road.distance_meters} m</small>
|
|
<div style="display: flex; gap: 5px;">
|
|
<button class="edit-mode-btn" onclick="enterEditMode(${road.id}, 'road', this); return false;" style="flex: 0.8;">✏️ Edit Shape</button>
|
|
<button style="background: #28a745; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer; flex: 1;" onclick="updateRoad(${road.id}, '${road.distance_meters}'); return false;">Save</button>
|
|
<button style="background: #dc3545; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer; flex: 1;" onclick="deleteRoad(${road.id}); return false;">Delete</button>
|
|
<button style="background: #6c757d; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer; flex: 1;" onclick="map.closePopup(); return false;">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
polyline.bindPopup(formHTML).openPopup();
|
|
window.currentEditPolyline = polyline;
|
|
}
|
|
|
|
function showLandEditPopup(land, polygon) {
|
|
const formHTML = `
|
|
<div class="popup-form" onclick="event.stopPropagation();">
|
|
<h3 style="margin:0 0 5px 0;">Edit Land</h3>
|
|
<label><strong>Name:</strong></label>
|
|
<input type="text" id="editLandName" value="${land.name}" required>
|
|
<label><strong>Status:</strong></label>
|
|
<select id="editLandStatus">
|
|
<option value="Sertifikat Hak Milik (SHM)" ${land.ownership_status === 'Sertifikat Hak Milik (SHM)' ? 'selected' : ''}>Sertifikat Hak Milik (SHM)</option>
|
|
<option value="Sertifikat Hak Guna Bangunan (HGB)" ${land.ownership_status === 'Sertifikat Hak Guna Bangunan (HGB)' ? 'selected' : ''}>Sertifikat Hak Guna Bangunan (HGB)</option>
|
|
<option value="Sertifikat Hak Guna Usaha (HGU)" ${land.ownership_status === 'Sertifikat Hak Guna Usaha (HGU)' ? 'selected' : ''}>Sertifikat Hak Guna Usaha (HGU)</option>
|
|
<option value="Sertifikat Hak Pakai (HP)" ${land.ownership_status === 'Sertifikat Hak Pakai (HP)' ? 'selected' : ''}>Sertifikat Hak Pakai (HP)</option>
|
|
</select>
|
|
<small>Area: ${land.area_sq_meters} m²</small>
|
|
<div style="display: flex; gap: 5px;">
|
|
<button class="edit-mode-btn" onclick="enterEditMode(${land.id}, 'land', this); return false;" style="flex: 0.8;">✏️ Edit Shape</button>
|
|
<button style="background: #28a745; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer; flex: 1;" onclick="updateLand(${land.id}, '${land.area_sq_meters}'); return false;">Save</button>
|
|
<button style="background: #dc3545; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer; flex: 1;" onclick="deleteLand(${land.id}); return false;">Delete</button>
|
|
<button style="background: #6c757d; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer; flex: 1;" onclick="map.closePopup(); return false;">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
polygon.bindPopup(formHTML).openPopup();
|
|
window.currentEditPolygon = polygon;
|
|
}
|
|
|
|
// --- EDIT MODE FUNCTIONS ---
|
|
function enterEditMode(id, type, button) {
|
|
if (editModeActive) return;
|
|
|
|
editModeActive = true;
|
|
const shape = type === 'road' ? window.currentEditPolyline : window.currentEditPolygon;
|
|
let points = shape._latlngs;
|
|
|
|
// Handle polygon nested arrays - Leaflet stores polygon rings as nested arrays
|
|
if (type === 'land') {
|
|
// Check if points is an array of arrays (multi-ring polygon)
|
|
if (Array.isArray(points) && points.length > 0 && Array.isArray(points[0])) {
|
|
points = points[0]; // Get the outer ring
|
|
}
|
|
}
|
|
|
|
// Ensure points is an array of LatLng objects
|
|
if (!Array.isArray(points) || points.length === 0) {
|
|
console.error('Invalid points:', points);
|
|
alert('Error: Could not read shape coordinates');
|
|
return;
|
|
}
|
|
|
|
// Store original coordinates for undo
|
|
originalCoordinates = points.map(p => {
|
|
if (p.lat && p.lng) {
|
|
return [p.lat, p.lng];
|
|
} else if (Array.isArray(p) && p.length === 2) {
|
|
return p;
|
|
}
|
|
return [0, 0];
|
|
});
|
|
|
|
editingShape = shape;
|
|
editingData = { id, type };
|
|
|
|
// Close current popup
|
|
map.closePopup();
|
|
|
|
// Show edit mode indicator
|
|
document.getElementById('edit-mode-indicator').style.display = 'block';
|
|
|
|
// Change map style
|
|
map.getContainer().style.cursor = 'grab';
|
|
|
|
// Keep the shape visible but update its style
|
|
if (type === 'road') {
|
|
editingShape.setStyle({ color: '#666666', weight: 2, dashArray: '5, 5', opacity: 0.7 });
|
|
} else {
|
|
editingShape.setStyle({ color: '#666666', weight: 2, dashArray: '5, 5', opacity: 0.3, fillOpacity: 0.1 });
|
|
}
|
|
|
|
// Create draggable vertex markers
|
|
createVertexMarkers(points);
|
|
|
|
// Create midpoint visual hints
|
|
createMidpointHints(points);
|
|
|
|
// Show edit panel (fixed on top of map)
|
|
document.getElementById('edit-panel').style.display = 'block';
|
|
|
|
// Set up edit panel button handlers
|
|
document.getElementById('finish-btn').onclick = finishEditingShape;
|
|
document.getElementById('cancel-btn').onclick = cancelEditMode;
|
|
|
|
// Add Escape key handler to exit edit mode
|
|
editModeKeyHandler = function(e) {
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
cancelEditMode();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', editModeKeyHandler);
|
|
|
|
// Add event listeners for edit mode
|
|
map.on('click', editModeClickHandler);
|
|
}
|
|
|
|
function getShapeCenter(points) {
|
|
let lat = 0, lng = 0;
|
|
points.forEach(p => {
|
|
lat += p.lat;
|
|
lng += p.lng;
|
|
});
|
|
return L.latLng(lat / points.length, lng / points.length);
|
|
}
|
|
|
|
function createVertexMarkers(points) {
|
|
vertexMarkers = [];
|
|
|
|
points.forEach((point, index) => {
|
|
// Create circular marker at each vertex
|
|
const marker = L.circleMarker(point, {
|
|
radius: 8,
|
|
fillColor: '#ff6b6b',
|
|
color: '#fff',
|
|
weight: 2,
|
|
opacity: 1,
|
|
fillOpacity: 0.8,
|
|
className: 'vertex-marker'
|
|
}).addTo(map);
|
|
|
|
marker.pointIndex = index;
|
|
marker.isVertex = true;
|
|
marker.justDragged = false;
|
|
|
|
// Make marker draggable
|
|
marker.on('mousedown', function(e) {
|
|
L.DomEvent.preventDefault(e);
|
|
L.DomEvent.stopPropagation(e);
|
|
this.justDragged = false;
|
|
startDragVertex(this, e);
|
|
});
|
|
|
|
// Click handler to delete vertex
|
|
marker.on('click', function(e) {
|
|
L.DomEvent.preventDefault(e);
|
|
L.DomEvent.stopPropagation(e);
|
|
|
|
// If we just finished dragging, don't delete
|
|
if (this.justDragged) {
|
|
this.justDragged = false;
|
|
return;
|
|
}
|
|
|
|
deleteVertex(this.pointIndex);
|
|
});
|
|
|
|
// Add tooltip
|
|
marker.bindTooltip(`Point ${index + 1}<br>Click to delete<br>Drag to move`, {
|
|
permanent: false,
|
|
direction: 'top',
|
|
offset: [0, -15]
|
|
});
|
|
|
|
vertexMarkers.push(marker);
|
|
});
|
|
}
|
|
|
|
function createMidpointHints(points) {
|
|
if (midpointMarkers.length > 0) {
|
|
midpointMarkers.forEach(m => map.removeLayer(m));
|
|
midpointMarkers = [];
|
|
}
|
|
|
|
const maxMidpoints = (editingData && editingData.type === 'road') ? points.length - 1 : points.length;
|
|
|
|
// Create visual hints at midpoints to add new points
|
|
for (let i = 0; i < maxMidpoints; i++) {
|
|
const current = points[i];
|
|
const next = points[(i + 1) % points.length];
|
|
|
|
const midLat = (current.lat + next.lat) / 2;
|
|
const midLng = (current.lng + next.lng) / 2;
|
|
|
|
const midMarker = L.circleMarker([midLat, midLng], {
|
|
radius: 5,
|
|
fillColor: '#4ecdc4',
|
|
color: '#fff',
|
|
weight: 1,
|
|
opacity: 0.6,
|
|
fillOpacity: 0.4,
|
|
className: 'add-point-hint'
|
|
}).addTo(map);
|
|
|
|
midMarker.pointIndex = i;
|
|
midMarker.isMidpoint = true;
|
|
|
|
// Click handler to add point at midpoint
|
|
midMarker.on('click', function(e) {
|
|
L.DomEvent.preventDefault(e);
|
|
L.DomEvent.stopPropagation(e);
|
|
// Get the approximate position between the two points
|
|
const pos = L.latLng((current.lat + next.lat) / 2, (current.lng + next.lng) / 2);
|
|
addPointAtIndex(this.pointIndex, pos);
|
|
});
|
|
|
|
midMarker.bindTooltip('Click to add point', {
|
|
permanent: false,
|
|
direction: 'top',
|
|
offset: [0, -15]
|
|
});
|
|
|
|
midpointMarkers.push(midMarker);
|
|
}
|
|
}
|
|
|
|
let draggedMarker = null;
|
|
let dragStartPos = null;
|
|
let hasMoved = false;
|
|
|
|
function startDragVertex(marker, e) {
|
|
L.DomEvent.preventDefault(e);
|
|
L.DomEvent.stopPropagation(e);
|
|
|
|
isDraggingVertex = true;
|
|
draggedMarker = marker;
|
|
dragStartPos = e.latlng;
|
|
hasMoved = false;
|
|
|
|
marker.setStyle({ fillColor: '#ff9800', radius: 10 });
|
|
|
|
// Disable map dragging during vertex drag
|
|
map.dragging.disable();
|
|
map.doubleClickZoom.disable();
|
|
|
|
const moveHandler = (moveEvent) => {
|
|
if (!draggedMarker) return;
|
|
|
|
L.DomEvent.preventDefault(moveEvent);
|
|
L.DomEvent.stopPropagation(moveEvent);
|
|
|
|
hasMoved = true;
|
|
|
|
const delta = {
|
|
lat: moveEvent.latlng.lat - dragStartPos.lat,
|
|
lng: moveEvent.latlng.lng - dragStartPos.lng
|
|
};
|
|
|
|
dragStartPos = moveEvent.latlng;
|
|
const newPos = L.latLng(
|
|
draggedMarker.getLatLng().lat + delta.lat,
|
|
draggedMarker.getLatLng().lng + delta.lng
|
|
);
|
|
|
|
draggedMarker.setLatLng(newPos);
|
|
updateShapeFromVertices();
|
|
};
|
|
|
|
const endHandler = () => {
|
|
if (!draggedMarker) return;
|
|
|
|
// Mark that we just finished dragging only if it actually moved
|
|
if (hasMoved) {
|
|
draggedMarker.justDragged = true;
|
|
}
|
|
|
|
draggedMarker.setStyle({ fillColor: '#ff6b6b', radius: 8 });
|
|
map.off('mousemove', moveHandler);
|
|
map.off('mouseup', endHandler);
|
|
|
|
// Re-enable map dragging
|
|
map.dragging.enable();
|
|
map.doubleClickZoom.enable();
|
|
|
|
isDraggingVertex = false;
|
|
|
|
const prevMarker = draggedMarker;
|
|
draggedMarker = null;
|
|
|
|
// Reset the flag after a brief moment
|
|
setTimeout(() => {
|
|
if (prevMarker) prevMarker.justDragged = false;
|
|
}, 100);
|
|
};
|
|
|
|
map.on('mousemove', moveHandler);
|
|
map.on('mouseup', endHandler);
|
|
}
|
|
|
|
function updateShapeFromVertices() {
|
|
const newPoints = vertexMarkers.map(m => {
|
|
const latlng = m.getLatLng();
|
|
return L.latLng(latlng.lat, latlng.lng);
|
|
});
|
|
|
|
if (editingShape) {
|
|
// Handle both polylines and polygons using setLatLngs to safely update paths
|
|
if (editingData.type === 'land') {
|
|
// Provide an array of points (Leaflet translates this into a ring)
|
|
editingShape.setLatLngs([newPoints]);
|
|
} else {
|
|
// Polyline takes just the array of points
|
|
editingShape.setLatLngs(newPoints);
|
|
}
|
|
}
|
|
|
|
// Update midpoint hints
|
|
const currentPoints = vertexMarkers.map(m => m.getLatLng());
|
|
createMidpointHints(currentPoints);
|
|
}
|
|
|
|
function editModeClickHandler(e) {
|
|
if (!editModeActive) return;
|
|
|
|
// Prevent map interactions while in edit mode for direct map clicks
|
|
L.DomEvent.preventDefault(e);
|
|
L.DomEvent.stopPropagation(e);
|
|
}
|
|
|
|
function deleteVertex(index) {
|
|
const minPoints = editingData.type === 'road' ? 2 : 3;
|
|
if (vertexMarkers.length <= minPoints) {
|
|
alert(`${editingData.type === 'road' ? 'Road' : 'Land'} needs at least ${minPoints} points!`);
|
|
return;
|
|
}
|
|
|
|
// Animate deletion
|
|
const marker = vertexMarkers[index];
|
|
marker.setStyle({ fillColor: '#ff0000', radius: 6 });
|
|
|
|
// Remove marker from display after a brief delay
|
|
setTimeout(() => {
|
|
map.removeLayer(marker);
|
|
|
|
// Remove from array
|
|
vertexMarkers.splice(index, 1);
|
|
|
|
// Update indices
|
|
vertexMarkers.forEach((m, i) => m.pointIndex = i);
|
|
updateShapeFromVertices();
|
|
}, 150);
|
|
}
|
|
|
|
function addPointAtIndex(index, latlng) {
|
|
// Create new point at the clicked location
|
|
const newMarker = L.circleMarker(latlng, {
|
|
radius: 8,
|
|
fillColor: '#ff6b6b',
|
|
color: '#fff',
|
|
weight: 2,
|
|
opacity: 1,
|
|
fillOpacity: 0.8,
|
|
className: 'vertex-marker'
|
|
}).addTo(map);
|
|
|
|
const newIndex = index + 1;
|
|
newMarker.pointIndex = newIndex;
|
|
newMarker.isVertex = true;
|
|
newMarker.justDragged = false;
|
|
|
|
// Make marker draggable
|
|
newMarker.on('mousedown', function(e) {
|
|
L.DomEvent.preventDefault(e);
|
|
L.DomEvent.stopPropagation(e);
|
|
this.justDragged = false;
|
|
startDragVertex(this, e);
|
|
});
|
|
|
|
// Click handler to delete vertex
|
|
newMarker.on('click', function(e) {
|
|
L.DomEvent.preventDefault(e);
|
|
L.DomEvent.stopPropagation(e);
|
|
|
|
// If we just finished dragging, don't delete
|
|
if (this.justDragged) {
|
|
this.justDragged = false;
|
|
return;
|
|
}
|
|
|
|
deleteVertex(this.pointIndex);
|
|
});
|
|
|
|
newMarker.bindTooltip(`Point ${newIndex + 1}<br>Click to delete<br>Drag to move`, {
|
|
permanent: false,
|
|
direction: 'top',
|
|
offset: [0, -15]
|
|
});
|
|
|
|
// Insert into array at correct position
|
|
vertexMarkers.splice(newIndex, 0, newMarker);
|
|
|
|
// Update all indices
|
|
vertexMarkers.forEach((m, i) => m.pointIndex = i);
|
|
|
|
updateShapeFromVertices();
|
|
}
|
|
|
|
function exitEditMode() {
|
|
if (!editModeActive) return;
|
|
|
|
editModeActive = false;
|
|
|
|
// Remove keyboard event listener
|
|
if (editModeKeyHandler) {
|
|
document.removeEventListener('keydown', editModeKeyHandler);
|
|
editModeKeyHandler = null;
|
|
}
|
|
|
|
// Hide edit panel
|
|
document.getElementById('edit-panel').style.display = 'none';
|
|
|
|
// Remove all vertex and midpoint markers
|
|
vertexMarkers.forEach(m => map.removeLayer(m));
|
|
midpointMarkers.forEach(m => map.removeLayer(m));
|
|
vertexMarkers = [];
|
|
midpointMarkers = [];
|
|
|
|
// Remove edit mode click handler
|
|
map.off('click', editModeClickHandler);
|
|
|
|
// Hide indicator
|
|
document.getElementById('edit-mode-indicator').style.display = 'none';
|
|
|
|
// Reset cursor
|
|
map.getContainer().style.cursor = 'grab';
|
|
|
|
editingShape = null;
|
|
editingData = null;
|
|
}
|
|
|
|
function cancelEditMode() {
|
|
if (!editModeActive) return;
|
|
|
|
// Remove keyboard handler first
|
|
if (editModeKeyHandler) {
|
|
document.removeEventListener('keydown', editModeKeyHandler);
|
|
editModeKeyHandler = null;
|
|
}
|
|
|
|
// Restore the original shape coordinates
|
|
if (originalCoordinates && editingShape) {
|
|
let coordsToSet = originalCoordinates.map(coord => L.latLng(coord[0], coord[1]));
|
|
|
|
// Handle polygon nested arrays - set coordinates back to original structure
|
|
if (editingData.type === 'land') {
|
|
if (Array.isArray(editingShape._latlngs[0])) {
|
|
// Multi-ring polygon
|
|
editingShape._latlngs[0] = coordsToSet;
|
|
} else {
|
|
// Single-ring polygon
|
|
editingShape._latlngs = coordsToSet;
|
|
}
|
|
} else {
|
|
// Polyline
|
|
editingShape._latlngs = coordsToSet;
|
|
}
|
|
|
|
editingShape.redraw();
|
|
|
|
// Restore original style
|
|
if (editingData.type === 'road') {
|
|
const roadData = window.currentEditPolyline.roadData;
|
|
if (roadData) {
|
|
editingShape.setStyle({
|
|
color: getRoadColor(roadData.road_type),
|
|
weight: 4,
|
|
dashArray: null,
|
|
opacity: 1
|
|
});
|
|
}
|
|
} else {
|
|
const landData = window.currentEditPolygon.landData;
|
|
if (landData) {
|
|
editingShape.setStyle({
|
|
color: getLandColor(landData.ownership_status),
|
|
weight: 2,
|
|
dashArray: null,
|
|
opacity: 1,
|
|
fillOpacity: 0.4
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
exitEditMode();
|
|
map.closePopup();
|
|
alert('Changes discarded - shape restored to original state!');
|
|
}
|
|
|
|
async function finishEditingShape() {
|
|
if (!editModeActive || !editingData) return;
|
|
|
|
// Remove keyboard handler first
|
|
if (editModeKeyHandler) {
|
|
document.removeEventListener('keydown', editModeKeyHandler);
|
|
editModeKeyHandler = null;
|
|
}
|
|
|
|
const newPoints = vertexMarkers.map(m => {
|
|
const latlng = m.getLatLng();
|
|
return [latlng.lat, latlng.lng];
|
|
});
|
|
|
|
// Restore shape visual style before exiting edit mode
|
|
if (editingData.type === 'road') {
|
|
const roadData = window.currentEditPolyline.roadData;
|
|
if (roadData) {
|
|
editingShape.setStyle({
|
|
color: getRoadColor(roadData.road_type),
|
|
weight: 4,
|
|
dashArray: null,
|
|
opacity: 1
|
|
});
|
|
await updateRoadCoordinates(editingData.id, newPoints, roadData.name, roadData.road_type);
|
|
}
|
|
} else {
|
|
const landData = window.currentEditPolygon.landData;
|
|
if (landData) {
|
|
editingShape.setStyle({
|
|
color: getLandColor(landData.ownership_status),
|
|
weight: 2,
|
|
dashArray: null,
|
|
opacity: 1,
|
|
fillOpacity: 0.4
|
|
});
|
|
await updateLandCoordinates(editingData.id, newPoints, landData.name, landData.ownership_status);
|
|
}
|
|
}
|
|
|
|
exitEditMode();
|
|
|
|
// Show original popup again after a brief delay
|
|
setTimeout(() => {
|
|
if (window.currentEditPolyline) {
|
|
window.currentEditPolyline.openPopup();
|
|
} else if (window.currentEditPolygon) {
|
|
window.currentEditPolygon.openPopup();
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
async function updateRoadCoordinates(roadId, coordinates, name, type) {
|
|
try {
|
|
const lineString = turf.lineString(coordinates.map(p => [p[1], p[0]]));
|
|
const distanceMeters = turf.length(lineString, {units: 'meters'}).toFixed(2);
|
|
|
|
const response = await fetch(`/api/roads/${roadId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name,
|
|
road_type: type,
|
|
distance_meters: parseFloat(distanceMeters),
|
|
coordinates
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
window.currentEditPolyline.setStyle({ color: getRoadColor(type) });
|
|
window.currentEditPolyline.bindPopup(`<b>${name}</b><br>Type: ${type}<br>Distance: ${distanceMeters} m`);
|
|
alert('Road shape updated successfully!');
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating road coordinates:", error);
|
|
alert("Failed to update road shape.");
|
|
}
|
|
}
|
|
|
|
async function updateLandCoordinates(landId, coordinates, name, status) {
|
|
try {
|
|
// Flatten coordinates if they're nested (from polygon structure)
|
|
let flatCoordinates = coordinates;
|
|
if (Array.isArray(coordinates) && coordinates.length > 0 && Array.isArray(coordinates[0]) && Array.isArray(coordinates[0][0])) {
|
|
flatCoordinates = coordinates[0]; // In case it's double-nested
|
|
}
|
|
|
|
let turfPoints = flatCoordinates.map(p => [p[1], p[0]]);
|
|
turfPoints.push(turfPoints[0]);
|
|
const polygon = turf.polygon([turfPoints]);
|
|
const areaSqMeters = turf.area(polygon).toFixed(2);
|
|
|
|
const response = await fetch(`/api/lands/${landId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name,
|
|
ownership_status: status,
|
|
area_sq_meters: parseFloat(areaSqMeters),
|
|
coordinates: flatCoordinates
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
window.currentEditPolygon.setStyle({ color: getLandColor(status) });
|
|
window.currentEditPolygon.bindPopup(`<b>${name}</b><br>Status: ${status}<br>Area: ${areaSqMeters} m²`);
|
|
alert('Land shape updated successfully!');
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating land coordinates:", error);
|
|
alert("Failed to update land shape.");
|
|
}
|
|
}
|
|
|
|
async function updateRoad(roadId, distance) {
|
|
const name = document.getElementById('editRoadName').value;
|
|
const type = document.getElementById('editRoadType').value;
|
|
|
|
if (!name) return alert("Name is required");
|
|
|
|
const coords = typeof window.currentEditPolyline._latlngs === 'object' ?
|
|
window.currentEditPolyline._latlngs.map(l => [l.lat, l.lng]) :
|
|
window.currentEditPolyline._latlngs;
|
|
|
|
try {
|
|
const response = await fetch(`/api/roads/${roadId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, road_type: type, distance_meters: parseFloat(distance), coordinates: coords })
|
|
});
|
|
|
|
if (response.ok) {
|
|
window.currentEditPolyline.setStyle({ color: getRoadColor(type) });
|
|
window.currentEditPolyline.bindPopup(`<b>${name}</b><br>Type: ${type}<br>Distance: ${distance} m`);
|
|
map.closePopup();
|
|
alert('Road updated successfully!');
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating road:", error);
|
|
alert("Failed to update road.");
|
|
}
|
|
}
|
|
|
|
async function deleteRoad(roadId) {
|
|
if (!confirm('Are you sure you want to delete this road?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/roads/${roadId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
map.removeLayer(window.currentEditPolyline);
|
|
map.closePopup();
|
|
alert('Road deleted successfully!');
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting road:", error);
|
|
alert("Failed to delete road.");
|
|
}
|
|
}
|
|
|
|
async function updateLand(landId, area) {
|
|
const name = document.getElementById('editLandName').value;
|
|
const status = document.getElementById('editLandStatus').value;
|
|
|
|
if (!name) return alert("Name is required");
|
|
|
|
const coords = typeof window.currentEditPolygon._latlngs === 'object' ?
|
|
window.currentEditPolygon._latlngs.map(l => [l.lat, l.lng]) :
|
|
window.currentEditPolygon._latlngs;
|
|
|
|
try {
|
|
const response = await fetch(`/api/lands/${landId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, ownership_status: status, area_sq_meters: parseFloat(area), coordinates: coords })
|
|
});
|
|
|
|
if (response.ok) {
|
|
window.currentEditPolygon.setStyle({ color: getLandColor(status) });
|
|
window.currentEditPolygon.bindPopup(`<b>${name}</b><br>Status: ${status}<br>Area: ${area} m²`);
|
|
map.closePopup();
|
|
alert('Land updated successfully!');
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating land:", error);
|
|
alert("Failed to update land.");
|
|
}
|
|
}
|
|
|
|
async function deleteLand(landId) {
|
|
if (!confirm('Are you sure you want to delete this land?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/lands/${landId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
map.removeLayer(window.currentEditPolygon);
|
|
map.closePopup();
|
|
alert('Land deleted successfully!');
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting land:", error);
|
|
alert("Failed to delete land.");
|
|
}
|
|
}
|
|
|
|
// --- DRAWING LOGIC ---
|
|
function setMode(mode) {
|
|
currentMode = mode;
|
|
currentPoints = [];
|
|
if (tempLayer) map.removeLayer(tempLayer);
|
|
if (previewLayer) map.removeLayer(previewLayer);
|
|
tempLayer = null;
|
|
previewLayer = null;
|
|
currentMousePos = null;
|
|
|
|
if (clickTimeout) {
|
|
clearTimeout(clickTimeout);
|
|
clickTimeout = null;
|
|
}
|
|
clickCount = 0;
|
|
|
|
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
|
|
if(mode === 'road') document.getElementById('btn-road').classList.add('active');
|
|
if(mode === 'land') document.getElementById('btn-land').classList.add('active');
|
|
}
|
|
|
|
map.on('click', (e) => {
|
|
if (currentMode === 'none') return;
|
|
|
|
clickCount++;
|
|
|
|
if (clickTimeout) {
|
|
clearTimeout(clickTimeout);
|
|
}
|
|
|
|
clickTimeout = setTimeout(() => {
|
|
if (clickCount === 1) {
|
|
currentPoints.push([e.latlng.lat, e.latlng.lng]);
|
|
|
|
if (tempLayer) map.removeLayer(tempLayer);
|
|
|
|
if (currentMode === 'road') {
|
|
tempLayer = L.polyline(currentPoints, {color: '#6c757d', dashArray: '5, 5', weight: 3}).addTo(map);
|
|
} else if (currentMode === 'land') {
|
|
tempLayer = L.polygon(currentPoints, {color: '#6c757d', dashArray: '5, 5'}).addTo(map);
|
|
}
|
|
}
|
|
clickCount = 0;
|
|
clickTimeout = null;
|
|
}, 250);
|
|
});
|
|
|
|
// --- PREVIEW LINE FOLLOWING CURSOR ---
|
|
map.on('mousemove', (e) => {
|
|
if (currentMode === 'none' || currentPoints.length === 0) return;
|
|
|
|
currentMousePos = e.latlng;
|
|
|
|
if (previewLayer) map.removeLayer(previewLayer);
|
|
|
|
const previewPoints = [...currentPoints, [e.latlng.lat, e.latlng.lng]];
|
|
|
|
if (currentMode === 'road') {
|
|
previewLayer = L.polyline(previewPoints, {color: '#ff9800', weight: 2, opacity: 0.6}).addTo(map);
|
|
} else if (currentMode === 'land') {
|
|
previewLayer = L.polygon(previewPoints, {color: '#ff9800', weight: 2, opacity: 0.6, fillOpacity: 0.1}).addTo(map);
|
|
}
|
|
});
|
|
|
|
map.on('dblclick', (e) => {
|
|
e.originalEvent.preventDefault();
|
|
|
|
if (clickTimeout) {
|
|
clearTimeout(clickTimeout);
|
|
clickTimeout = null;
|
|
}
|
|
clickCount = 0;
|
|
|
|
if (currentMode === 'none') return;
|
|
|
|
const minPoints = currentMode === 'road' ? 2 : 3;
|
|
if (currentPoints.length < minPoints) {
|
|
alert(`${currentMode === 'road' ? 'Road' : 'Land'} needs at least ${minPoints} points!`);
|
|
return;
|
|
}
|
|
|
|
const finalPoints = [...currentPoints];
|
|
const activeMode = currentMode;
|
|
setMode('none');
|
|
|
|
let finalLayer, formHTML;
|
|
window.lastDrawnCoordinates = finalPoints;
|
|
|
|
if (activeMode === 'road') {
|
|
const lineString = turf.lineString(finalPoints.map(p => [p[1], p[0]]));
|
|
const distanceMeters = turf.length(lineString, {units: 'meters'}).toFixed(2);
|
|
|
|
finalLayer = L.polyline(finalPoints, {color: 'black', weight: 4}).addTo(map);
|
|
|
|
formHTML = `
|
|
<div class="popup-form">
|
|
<h3 style="margin:0 0 5px 0;">Save Road</h3>
|
|
<small>Distance: ${distanceMeters} m</small>
|
|
<input type="text" id="roadName" placeholder="Road Name" required>
|
|
<select id="roadType">
|
|
<option value="Jalan nasional">Jalan Nasional</option>
|
|
<option value="Jalan provinsi">Jalan Provinsi</option>
|
|
<option value="Jalan kabupaten">Jalan Kabupaten</option>
|
|
</select>
|
|
<button onclick="saveRoad('${distanceMeters}', window.lastDrawnCoordinates)">Save Road</button>
|
|
</div>
|
|
`;
|
|
} else if (activeMode === 'land') {
|
|
if (finalPoints.length < 3) return alert('Polygons need at least 3 points!');
|
|
|
|
let turfPoints = finalPoints.map(p => [p[1], p[0]]);
|
|
turfPoints.push(turfPoints[0]);
|
|
|
|
const polygon = turf.polygon([turfPoints]);
|
|
const areaSqMeters = turf.area(polygon).toFixed(2);
|
|
|
|
finalLayer = L.polygon(finalPoints, {color: 'black'}).addTo(map);
|
|
|
|
formHTML = `
|
|
<div class="popup-form">
|
|
<h3 style="margin:0 0 5px 0;">Save Land</h3>
|
|
<small>Area: ${areaSqMeters} m²</small>
|
|
<input type="text" id="landName" placeholder="Land Name" required>
|
|
<select id="landStatus">
|
|
<option value="Sertifikat Hak Milik (SHM)">Sertifikat Hak Milik (SHM)</option>
|
|
<option value="Sertifikat Hak Guna Bangunan (HGB)">Sertifikat Hak Guna Bangunan (HGB)</option>
|
|
<option value="Sertifikat Hak Guna Usaha (HGU)">Sertifikat Hak Guna Usaha (HGU)</option>
|
|
<option value="Sertifikat Hak Pakai (HP)">Sertifikat Hak Pakai (HP)</option>
|
|
</select>
|
|
<button onclick="saveLand('${areaSqMeters}', window.lastDrawnCoordinates)">Save Land</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
finalLayer.bindPopup(formHTML).openPopup();
|
|
window.lastDrawnLayer = finalLayer;
|
|
});
|
|
|
|
// --- SAVING LOGIC ---
|
|
async function saveRoad(distance, coordinates) {
|
|
const name = document.getElementById('roadName').value;
|
|
const type = document.getElementById('roadType').value;
|
|
if(!name) return alert("Name is required");
|
|
|
|
try {
|
|
const response = await fetch('/api/roads', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, road_type: type, distance_meters: parseFloat(distance), coordinates })
|
|
});
|
|
|
|
if(response.ok) {
|
|
const result = await response.json();
|
|
const roadData = { id: result.id, name, road_type: type, distance_meters: parseFloat(distance), coordinates };
|
|
|
|
window.lastDrawnLayer.roadData = roadData;
|
|
window.lastDrawnLayer.setStyle({ color: getRoadColor(type) });
|
|
window.lastDrawnLayer.on('click', function() {
|
|
showRoadEditPopup(roadData, window.lastDrawnLayer);
|
|
});
|
|
window.lastDrawnLayer.bindPopup(`<b>${name}</b><br>Type: ${type}<br>Distance: ${distance} m`);
|
|
window.lastDrawnLayer.closePopup();
|
|
} else {
|
|
alert('Error saving road to server.');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Connection error saving road.');
|
|
}
|
|
}
|
|
|
|
async function saveLand(area, coordinates) {
|
|
const name = document.getElementById('landName').value;
|
|
const status = document.getElementById('landStatus').value;
|
|
if(!name) return alert("Name is required");
|
|
|
|
try {
|
|
const response = await fetch('/api/lands', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, ownership_status: status, area_sq_meters: parseFloat(area), coordinates })
|
|
});
|
|
|
|
if(response.ok) {
|
|
const result = await response.json();
|
|
const landData = { id: result.id, name, ownership_status: status, area_sq_meters: parseFloat(area), coordinates };
|
|
|
|
window.lastDrawnLayer.landData = landData;
|
|
window.lastDrawnLayer.setStyle({ color: getLandColor(status) });
|
|
window.lastDrawnLayer.on('click', function() {
|
|
showLandEditPopup(landData, window.lastDrawnLayer);
|
|
});
|
|
window.lastDrawnLayer.bindPopup(`<b>${name}</b><br>Status: ${status}<br>Area: ${area} m²`);
|
|
window.lastDrawnLayer.closePopup();
|
|
} else {
|
|
alert('Error saving land to server.');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Connection error saving land.');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |