560 lines
21 KiB
JavaScript
560 lines
21 KiB
JavaScript
// Asset Geometry Editing via MapLibre with MapboxDraw
|
|
// This module provides functionality to edit asset geometry (Point, LineString, Polygon) directly on the map
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
let mapInstance = null;
|
|
let drawInstance = null;
|
|
let isEditingGeometry = false;
|
|
let originalGeometry = null;
|
|
let editedGeometry = null;
|
|
let currentGeometryType = 'Point'; // 'Point', 'LineString', or 'Polygon'
|
|
let editMarker = null; // For simple point editing fallback
|
|
let drawControlAdded = false;
|
|
|
|
// MapboxDraw styles for asset editing
|
|
const drawStyles = [
|
|
// Polygon fill
|
|
{
|
|
id: 'gl-draw-polygon-fill',
|
|
type: 'fill',
|
|
filter: ['all', ['==', '$type', 'Polygon']],
|
|
paint: {
|
|
'fill-color': ['case', ['==', ['get', 'active'], 'true'], '#007bff', '#3388ff'],
|
|
'fill-opacity': ['case', ['==', ['get', 'active'], 'true'], 0.3, 0.2]
|
|
}
|
|
},
|
|
// Lines (for LineString and Polygon outlines)
|
|
{
|
|
id: 'gl-draw-lines',
|
|
type: 'line',
|
|
filter: ['any', ['==', '$type', 'LineString'], ['==', '$type', 'Polygon']],
|
|
layout: {
|
|
'line-cap': 'round',
|
|
'line-join': 'round'
|
|
},
|
|
paint: {
|
|
'line-color': ['case', ['==', ['get', 'active'], 'true'], '#007bff', '#3388ff'],
|
|
'line-width': ['case', ['==', ['get', 'active'], 'true'], 3, 2]
|
|
}
|
|
},
|
|
// Point (outer circle - white border)
|
|
{
|
|
id: 'gl-draw-point-outer',
|
|
type: 'circle',
|
|
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'feature']],
|
|
paint: {
|
|
'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 8, 6],
|
|
'circle-color': 'white'
|
|
}
|
|
},
|
|
// Point (inner circle - colored)
|
|
{
|
|
id: 'gl-draw-point-inner',
|
|
type: 'circle',
|
|
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'feature']],
|
|
paint: {
|
|
'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 6, 4],
|
|
'circle-color': ['case', ['==', ['get', 'active'], 'true'], '#007bff', '#3388ff']
|
|
}
|
|
},
|
|
// Vertex points (outer)
|
|
{
|
|
id: 'gl-draw-vertex-outer',
|
|
type: 'circle',
|
|
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['!=', 'mode', 'simple_select']],
|
|
paint: {
|
|
'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 7, 5],
|
|
'circle-color': 'white'
|
|
}
|
|
},
|
|
// Vertex points (inner)
|
|
{
|
|
id: 'gl-draw-vertex-inner',
|
|
type: 'circle',
|
|
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['!=', 'mode', 'simple_select']],
|
|
paint: {
|
|
'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 5, 3],
|
|
'circle-color': '#007bff'
|
|
}
|
|
},
|
|
// Midpoints
|
|
{
|
|
id: 'gl-draw-midpoint',
|
|
type: 'circle',
|
|
filter: ['all', ['==', 'meta', 'midpoint']],
|
|
paint: {
|
|
'circle-radius': 4,
|
|
'circle-color': '#007bff'
|
|
}
|
|
}
|
|
];
|
|
|
|
// Initialize when map is ready
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Wait for map to be available
|
|
const checkMap = setInterval(function() {
|
|
if (window.map) {
|
|
mapInstance = window.map;
|
|
clearInterval(checkMap);
|
|
initializeGeometryEditing();
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
function initializeGeometryEditing() {
|
|
// Listen for edit mode events
|
|
document.addEventListener('asset-cms-edit-mode-enter', enableMapEditing);
|
|
document.addEventListener('asset-cms-edit-mode-exit', disableMapEditing);
|
|
}
|
|
|
|
function initializeMapboxDraw() {
|
|
if (!window.MapboxDraw) {
|
|
console.warn('MapboxDraw is not available. Falling back to simple point editing.');
|
|
return false;
|
|
}
|
|
|
|
// Set MapboxDraw constants for MapLibre compatibility
|
|
MapboxDraw.constants.classes.CANVAS = 'maplibregl-canvas';
|
|
MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl';
|
|
MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-';
|
|
MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group';
|
|
MapboxDraw.constants.classes.ATTRIBUTION = 'maplibregl-ctrl-attrib';
|
|
|
|
// Determine which controls to show based on geometry type
|
|
const controls = {
|
|
point: currentGeometryType === 'Point',
|
|
line_string: currentGeometryType === 'LineString',
|
|
polygon: currentGeometryType === 'Polygon',
|
|
trash: true,
|
|
combine_features: false,
|
|
uncombine_features: false
|
|
};
|
|
|
|
drawInstance = new MapboxDraw({
|
|
displayControlsDefault: false,
|
|
controls: controls,
|
|
styles: drawStyles
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
// Export function to enable editing
|
|
window.enableMapEditing = function() {
|
|
if (!mapInstance) return;
|
|
|
|
// Ensure map is loaded before enabling editing
|
|
if (!mapInstance.loaded()) {
|
|
mapInstance.on('load', function() {
|
|
window.enableMapEditing();
|
|
});
|
|
return;
|
|
}
|
|
|
|
isEditingGeometry = true;
|
|
|
|
// Hide original read-only asset layers to avoid duplicate drawing
|
|
const layersToHide = [
|
|
'asset-point-outer', 'asset-point-middle', 'asset-point-inner',
|
|
'asset-line-outline', 'asset-line-fill',
|
|
'asset-polygon-fill', 'asset-polygon-outline'
|
|
];
|
|
layersToHide.forEach(layerId => {
|
|
try {
|
|
if (mapInstance.getLayer(layerId)) {
|
|
mapInstance.setLayoutProperty(layerId, 'visibility', 'none');
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Failed to hide layer ${layerId}:`, err);
|
|
}
|
|
});
|
|
|
|
// Get current asset geometry type from page data
|
|
const container = document.getElementById('asset-detail-container');
|
|
if (!container) return;
|
|
|
|
// Store original geometry
|
|
originalGeometry = getAssetGeometryFromPage();
|
|
currentGeometryType = originalGeometry?.type || 'Point';
|
|
|
|
// Show edit instructions
|
|
showEditInstructions();
|
|
|
|
// Try to use MapboxDraw for all geometry types
|
|
if (initializeMapboxDraw() && drawInstance) {
|
|
try {
|
|
// Add the draw control
|
|
if (!drawControlAdded) {
|
|
mapInstance.addControl(drawInstance, 'top-right');
|
|
drawControlAdded = true;
|
|
}
|
|
|
|
// Load existing geometry into draw
|
|
if (originalGeometry) {
|
|
const feature = {
|
|
type: 'Feature',
|
|
geometry: originalGeometry,
|
|
properties: {}
|
|
};
|
|
drawInstance.add(feature);
|
|
|
|
// Select the feature for editing
|
|
// Use simple_select for points, direct_select for lines/polygons
|
|
const features = drawInstance.getAll();
|
|
if (features.features.length > 0) {
|
|
const featureId = features.features[0].id;
|
|
if (currentGeometryType === 'Point') {
|
|
// For points, use simple_select mode
|
|
drawInstance.changeMode('simple_select', {
|
|
featureIds: [featureId]
|
|
});
|
|
} else {
|
|
// For lines and polygons, use direct_select mode
|
|
drawInstance.changeMode('direct_select', {
|
|
featureId: featureId
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
// No existing geometry: enter draw mode so the user can click to place
|
|
if (currentGeometryType === 'Point') {
|
|
drawInstance.changeMode('draw_point');
|
|
} else if (currentGeometryType === 'LineString') {
|
|
drawInstance.changeMode('draw_line_string');
|
|
} else if (currentGeometryType === 'Polygon') {
|
|
drawInstance.changeMode('draw_polygon');
|
|
}
|
|
}
|
|
|
|
// Listen for draw events
|
|
mapInstance.on('draw.create', handleDrawCreate);
|
|
mapInstance.on('draw.update', handleDrawUpdate);
|
|
mapInstance.on('draw.delete', handleDrawDelete);
|
|
|
|
// Cursor management for draw mode
|
|
mapInstance.on('mousemove', handleDrawMouseMove);
|
|
} catch (e) {
|
|
console.warn('MapboxDraw error, falling back to simple point editing:', e);
|
|
// Cleanup any partial initialization
|
|
if (drawControlAdded && drawInstance) {
|
|
try {
|
|
mapInstance.removeControl(drawInstance);
|
|
} catch (removeError) {
|
|
// Ignore removal errors
|
|
}
|
|
drawControlAdded = false;
|
|
drawInstance = null;
|
|
}
|
|
// Fallback to simple point editing
|
|
enableSimplePointEditing();
|
|
}
|
|
} else {
|
|
// Fallback to simple point editing
|
|
enableSimplePointEditing();
|
|
}
|
|
};
|
|
|
|
// Export function to disable editing
|
|
window.disableMapEditing = function() {
|
|
if (!mapInstance) return;
|
|
|
|
isEditingGeometry = false;
|
|
|
|
// Restore visibility of original read-only asset layers
|
|
const layersToShow = [
|
|
'asset-point-outer', 'asset-point-middle', 'asset-point-inner',
|
|
'asset-line-outline', 'asset-line-fill',
|
|
'asset-polygon-fill', 'asset-polygon-outline'
|
|
];
|
|
layersToShow.forEach(layerId => {
|
|
try {
|
|
if (mapInstance.getLayer(layerId)) {
|
|
mapInstance.setLayoutProperty(layerId, 'visibility', 'visible');
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Failed to show layer ${layerId}:`, err);
|
|
}
|
|
});
|
|
|
|
// Remove draw control and listeners
|
|
if (drawInstance && drawControlAdded) {
|
|
mapInstance.off('draw.create', handleDrawCreate);
|
|
mapInstance.off('draw.update', handleDrawUpdate);
|
|
mapInstance.off('draw.delete', handleDrawDelete);
|
|
mapInstance.off('mousemove', handleDrawMouseMove);
|
|
|
|
// Remove all drawn features
|
|
drawInstance.deleteAll();
|
|
|
|
// Remove the control
|
|
mapInstance.removeControl(drawInstance);
|
|
drawControlAdded = false;
|
|
drawInstance = null;
|
|
}
|
|
|
|
// Cleanup simple point editing
|
|
disableSimplePointEditing();
|
|
|
|
// Reset geometry if not saved
|
|
if (!editedGeometry) {
|
|
editedGeometry = null;
|
|
}
|
|
|
|
// Hide edit instructions
|
|
hideEditInstructions();
|
|
|
|
// Reset cursor
|
|
mapInstance.getCanvas().style.cursor = '';
|
|
};
|
|
|
|
// Export function to get geometry changes
|
|
window.getGeometryChanges = function() {
|
|
// Get the latest geometry from draw if available
|
|
if (drawInstance) {
|
|
const features = drawInstance.getAll();
|
|
if (features.features.length > 0) {
|
|
return features.features[0].geometry;
|
|
}
|
|
}
|
|
return editedGeometry;
|
|
};
|
|
|
|
function handleDrawCreate(e) {
|
|
const features = drawInstance.getAll().features;
|
|
|
|
// Keep only the latest feature (delete old ones)
|
|
if (features.length > 1) {
|
|
const keepId = e.features[0].id;
|
|
const idsToDelete = features
|
|
.filter(f => f.id !== keepId)
|
|
.map(f => f.id);
|
|
drawInstance.delete(idsToDelete);
|
|
}
|
|
|
|
editedGeometry = e.features[0].geometry;
|
|
updateCoordinateDisplay(editedGeometry);
|
|
showGeometryUpdateConfirmation();
|
|
}
|
|
|
|
function handleDrawUpdate(e) {
|
|
editedGeometry = e.features[0].geometry;
|
|
updateCoordinateDisplay(editedGeometry);
|
|
showGeometryUpdateConfirmation();
|
|
}
|
|
|
|
function handleDrawDelete(e) {
|
|
editedGeometry = null;
|
|
updateCoordinateDisplay(null);
|
|
}
|
|
|
|
function handleDrawMouseMove(e) {
|
|
if (!drawInstance) return;
|
|
|
|
const canvas = mapInstance.getCanvas();
|
|
|
|
// Get draw layers
|
|
const allLayers = mapInstance.getStyle().layers;
|
|
const drawLayerPrefixes = ['gl-draw-polygon-fill', 'gl-draw-vertex-outer', 'gl-draw-midpoint', 'gl-draw-point'];
|
|
const existingLayers = allLayers
|
|
.filter(layer => drawLayerPrefixes.some(prefix => layer.id.startsWith(prefix)))
|
|
.map(layer => layer.id);
|
|
|
|
if (existingLayers.length === 0) return;
|
|
|
|
const features = mapInstance.queryRenderedFeatures(e.point, { layers: existingLayers });
|
|
|
|
if (features.length > 0) {
|
|
const feature = features[0];
|
|
if (feature.properties.meta === 'vertex' || feature.properties.meta === 'midpoint') {
|
|
canvas.style.cursor = 'grab';
|
|
} else if (feature.properties.active === 'true') {
|
|
canvas.style.cursor = 'move';
|
|
} else {
|
|
canvas.style.cursor = 'pointer';
|
|
}
|
|
} else {
|
|
const currentMode = drawInstance.getMode();
|
|
if (currentMode === 'draw_polygon' || currentMode === 'draw_line_string' || currentMode === 'draw_point') {
|
|
canvas.style.cursor = 'crosshair';
|
|
} else {
|
|
canvas.style.cursor = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Simple Point Editing (fallback) ---
|
|
function enableSimplePointEditing() {
|
|
mapInstance.getCanvas().style.cursor = 'crosshair';
|
|
document.getElementById('asset-map')?.classList.add('edit-mode');
|
|
mapInstance.on('click', handleSimpleMapClick);
|
|
|
|
// Create draggable marker for existing point
|
|
if (originalGeometry && originalGeometry.type === 'Point') {
|
|
createDraggableMarker(originalGeometry.coordinates[0], originalGeometry.coordinates[1]);
|
|
}
|
|
}
|
|
|
|
function disableSimplePointEditing() {
|
|
mapInstance.off('click', handleSimpleMapClick);
|
|
document.getElementById('asset-map')?.classList.remove('edit-mode');
|
|
|
|
if (editMarker) {
|
|
editMarker.remove();
|
|
editMarker = null;
|
|
}
|
|
}
|
|
|
|
function handleSimpleMapClick(e) {
|
|
if (!isEditingGeometry) return;
|
|
|
|
const lngLat = e.lngLat;
|
|
editedGeometry = {
|
|
type: 'Point',
|
|
coordinates: [lngLat.lng, lngLat.lat]
|
|
};
|
|
|
|
createDraggableMarker(lngLat.lng, lngLat.lat);
|
|
updateCoordinateDisplay(editedGeometry);
|
|
showGeometryUpdateConfirmation();
|
|
}
|
|
|
|
function createDraggableMarker(lng, lat) {
|
|
if (editMarker) {
|
|
editMarker.setLngLat([lng, lat]);
|
|
} else {
|
|
const el = document.createElement('div');
|
|
el.className = 'edit-marker';
|
|
el.style.cssText = 'width: 20px; height: 20px; background-color: #007bff; border-radius: 50%; border: 3px solid #ffffff; box-shadow: 0 2px 6px rgba(0,0,0,0.3); cursor: move;';
|
|
|
|
editMarker = new maplibregl.Marker({
|
|
element: el,
|
|
draggable: true
|
|
})
|
|
.setLngLat([lng, lat])
|
|
.addTo(mapInstance);
|
|
|
|
editMarker.on('dragend', function() {
|
|
const lngLat = editMarker.getLngLat();
|
|
editedGeometry = {
|
|
type: 'Point',
|
|
coordinates: [lngLat.lng, lngLat.lat]
|
|
};
|
|
updateCoordinateDisplay(editedGeometry);
|
|
});
|
|
}
|
|
|
|
mapInstance.flyTo({
|
|
center: [lng, lat],
|
|
zoom: mapInstance.getZoom()
|
|
});
|
|
}
|
|
|
|
// --- Helper Functions ---
|
|
function getAssetGeometryFromPage() {
|
|
// Try to get geometry from geojson data attribute
|
|
const container = document.getElementById('asset-detail-container');
|
|
if (container && container.dataset.assetGeometry) {
|
|
try {
|
|
return JSON.parse(container.dataset.assetGeometry);
|
|
} catch (e) {
|
|
console.warn('Failed to parse asset geometry:', e);
|
|
}
|
|
}
|
|
|
|
// Fallback: try to get from lon/lat inputs
|
|
const lonInput = document.querySelector('.asset-editable-field[data-field="lon"] input');
|
|
const latInput = document.querySelector('.asset-editable-field[data-field="lat"] input');
|
|
|
|
if (lonInput && latInput && lonInput.value && latInput.value) {
|
|
return {
|
|
type: 'Point',
|
|
coordinates: [parseFloat(lonInput.value), parseFloat(latInput.value)]
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function updateCoordinateDisplay(geometry) {
|
|
if (!geometry) return;
|
|
|
|
// Update lon/lat inputs for Point geometries
|
|
if (geometry.type === 'Point') {
|
|
const lonInput = document.querySelector('.asset-editable-field[data-field="lon"] .field-edit input');
|
|
const latInput = document.querySelector('.asset-editable-field[data-field="lat"] .field-edit input');
|
|
|
|
if (lonInput) lonInput.value = geometry.coordinates[0].toFixed(6);
|
|
if (latInput) latInput.value = geometry.coordinates[1].toFixed(6);
|
|
}
|
|
|
|
// Update geometry display for LineString and Polygon
|
|
const geomDisplay = document.getElementById('geometry-display');
|
|
if (geomDisplay) {
|
|
if (geometry.type === 'LineString') {
|
|
geomDisplay.textContent = `Ligne: ${geometry.coordinates.length} points`;
|
|
} else if (geometry.type === 'Polygon') {
|
|
const pointCount = geometry.coordinates[0] ? geometry.coordinates[0].length : 0;
|
|
geomDisplay.textContent = `Polygone: ${pointCount} points`;
|
|
}
|
|
}
|
|
}
|
|
|
|
function showEditInstructions() {
|
|
let instructions = document.getElementById('geometry-edit-instructions');
|
|
if (instructions) instructions.remove();
|
|
|
|
instructions = document.createElement('div');
|
|
instructions.id = 'geometry-edit-instructions';
|
|
instructions.className = 'alert alert-info alert-dismissible position-absolute';
|
|
instructions.style.cssText = 'top: 10px; left: 10px; right: 50px; z-index: 1000; font-size: 0.875rem;';
|
|
|
|
let message = '';
|
|
switch (currentGeometryType) {
|
|
case 'Point':
|
|
message = originalGeometry
|
|
? 'Cliquez sur la carte pour déplacer le point, ou faites glisser le marqueur.'
|
|
: 'Cliquez sur la carte pour placer le point.';
|
|
break;
|
|
case 'LineString':
|
|
message = 'Cliquez sur les sommets pour les déplacer. Utilisez le bouton ligne pour dessiner une nouvelle géométrie.';
|
|
break;
|
|
case 'Polygon':
|
|
message = 'Cliquez sur les sommets pour les déplacer. Utilisez le bouton polygone pour dessiner une nouvelle géométrie.';
|
|
break;
|
|
default:
|
|
message = 'Utilisez les outils de dessin pour modifier la géométrie.';
|
|
}
|
|
|
|
instructions.innerHTML = `
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
const mapContainer = document.getElementById('asset-map') || document.getElementById('map');
|
|
if (mapContainer) {
|
|
mapContainer.style.position = 'relative';
|
|
mapContainer.appendChild(instructions);
|
|
}
|
|
}
|
|
|
|
function hideEditInstructions() {
|
|
const instructions = document.getElementById('geometry-edit-instructions');
|
|
if (instructions) {
|
|
instructions.remove();
|
|
}
|
|
}
|
|
|
|
function showGeometryUpdateConfirmation() {
|
|
const msg = document.createElement('div');
|
|
msg.className = 'alert alert-success position-fixed';
|
|
msg.style.cssText = 'bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 1060;';
|
|
msg.innerHTML = '<i class="bi bi-check-circle me-2"></i>Géométrie mise à jour';
|
|
document.body.appendChild(msg);
|
|
|
|
setTimeout(() => msg.remove(), 2000);
|
|
}
|
|
|
|
})();
|