loko/streetup/assets/static/assets/asset_geometry_edit.js
2026-07-22 14:48:40 +02:00

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);
}
})();