diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj index 437bb95..34fc145 100644 --- a/LMEMappeCatastaliApp.csproj +++ b/LMEMappeCatastaliApp.csproj @@ -28,8 +28,8 @@ com.companyname.lmemappecatastaliapp - 0.2.27 - 31 + 0.2.28 + 32 None diff --git a/wwwroot/js/lme-map.js b/wwwroot/js/lme-map.js index a3c600d..b35b221 100644 --- a/wwwroot/js/lme-map.js +++ b/wwwroot/js/lme-map.js @@ -22,8 +22,12 @@ const lmeState = { importedSheetsVisible: false, importedMeasurements: null, drawings: null, + drawingsRenderer: null, + drawingSelection: null, drawingMeasurements: null, drawControl: null, + drawingDeleteButton: null, + ignoreMapClickUntil: 0, activeDrawHandler: null, drawEventsBound: false, includeKmlSheetLayer: false, @@ -178,10 +182,12 @@ function init(dotNetReference) { }); const managerHighlightPane = lmeState.map.createPane("managerHighlightPane"); managerHighlightPane.style.zIndex = "650"; + lmeState.drawingsRenderer = L.canvas({ pane: "drawingsPane", tolerance: 14 }); setLayerOrder(lmeState.layerOrder); lmeState.imported = L.featureGroup().addTo(lmeState.map); lmeState.importedSheets = L.featureGroup(); lmeState.drawings = L.featureGroup().addTo(lmeState.map); + lmeState.drawingSelection = L.featureGroup(); lmeState.importedMeasurements = L.layerGroup().addTo(lmeState.map); lmeState.drawingMeasurements = L.layerGroup().addTo(lmeState.map); lmeState.selectedParcelsLayer = L.featureGroup().addTo(lmeState.map); @@ -205,6 +211,9 @@ function init(dotNetReference) { } lmeState.map.on("click", async event => { + if (event.originalEvent?._lmeDrawingSelected || Date.now() < lmeState.ignoreMapClickUntil) { + return; + } clearManagerHighlight(); const info = await queryFeatureInfo(event.latlng); if (info) { @@ -232,6 +241,9 @@ function init(dotNetReference) { if (!lmeState.managerHighlightedObjectId && !lmeState.managerHighlightedParentKey) { return; } + if (event.target?.closest?.(".leaflet-draw")) { + return; + } const selectedObject = event.target?.closest?.(".layer-manager-object"); const selectedParent = event.target?.closest?.("[data-highlight-key]"); if (selectedObject?.dataset.objectId === lmeState.managerHighlightedObjectId @@ -1288,6 +1300,17 @@ function applyLayerManagerPalette(row, key) { row.style.setProperty("--layer-group-accent", palette.accent); } +function clearManagedObjectState(id) { + lmeState.managerHiddenObjects.delete(id); + lmeState.managerHiddenObjectIds.delete(id); + lmeState.managerHiddenMeasurementSummaryIds.delete(id); + lmeState.managerHiddenMeasurementSideIds.delete(id); + Object.keys(lmeState.managerObjectOrder).forEach(categoryKey => { + lmeState.managerObjectOrder[categoryKey] = lmeState.managerObjectOrder[categoryKey] + .filter(objectId => objectId !== id); + }); +} + function removeManagedRecords(records, subject) { if (!records.length) { return; @@ -1304,10 +1327,7 @@ function removeManagedRecords(records, subject) { if (record.parent?.hasLayer?.(record.layer)) { record.parent.removeLayer(record.layer); } - lmeState.managerHiddenObjects.delete(record.id); - lmeState.managerHiddenObjectIds.delete(record.id); - lmeState.managerHiddenMeasurementSummaryIds.delete(record.id); - lmeState.managerHiddenMeasurementSideIds.delete(record.id); + clearManagedObjectState(record.id); }); categoryKeys.forEach(categoryKey => { const order = lmeState.managerObjectOrder[categoryKey] || []; @@ -1761,10 +1781,60 @@ function highlightManagedObject(record) { document.querySelectorAll(".layer-manager-object").forEach(row => { row.classList.toggle("layer-manager-object-selected", row.dataset.objectId === record.id); }); + setSelectedDrawing(record.categoryKey === "drawings" ? record.layer : null); renderManagerHighlight([record]); fitLayer(record.layer); } +function drawingEditToolbar() { + return lmeState.drawControl?._toolbars?.[L.EditToolbar.TYPE] || null; +} + +function drawingEditActive() { + return Boolean(drawingEditToolbar()?.enabled?.()); +} + +function updateDrawingSelectionActions() { + const hasSelection = Boolean(lmeState.drawingSelection?.getLayers?.().length); + lmeState.drawingDeleteButton?.classList.toggle("leaflet-disabled", !hasSelection); + if (lmeState.drawingDeleteButton) { + lmeState.drawingDeleteButton.setAttribute("aria-disabled", String(!hasSelection)); + } +} + +function setSelectedDrawing(layer) { + if (!lmeState.drawingSelection || drawingEditActive()) { + return; + } + lmeState.drawingSelection.clearLayers(); + if (layer) { + lmeState.drawingSelection.addLayer(layer); + } + updateDrawingSelectionActions(); +} + +function selectDrawingLayer(layer) { + if (drawingEditActive()) { + return; + } + const record = { + id: managedLayerId(layer, "drawings"), + categoryKey: "drawings", + layer, + parent: lmeState.drawings, + source: String(layer?.feature?.properties?._lmeSource || "Progetto corrente") + }; + clearManagerHighlight(); + lmeState.managerHighlightedObjectId = record.id; + lmeState.managerHighlightedCategoryKey = "drawings"; + document.querySelectorAll(".layer-manager-object").forEach(row => { + row.classList.toggle("layer-manager-object-selected", row.dataset.objectId === record.id); + }); + setSelectedDrawing(layer); + renderManagerHighlight([record]); + setInfo(`Selezione: ${managedObjectLabel(record, 0)}. Usa la matita per modificare o il cestino per eliminare.`); +} + function bindManagedParentHighlight(row, key, records) { const summary = row.querySelector(":scope > summary"); row.dataset.highlightKey = key; @@ -1804,6 +1874,7 @@ function highlightManagedParent(row, key, records) { lmeState.managerHighlightedParentKey = key; lmeState.managerHighlightedParentRecords = records; lmeState.managerHighlightedCategoryKey = records[0]?.categoryKey || null; + setSelectedDrawing(null); row.classList.add("layer-manager-parent-selected"); renderManagerHighlight(records); } @@ -1853,6 +1924,7 @@ function clearManagerHighlight() { lmeState.managerHighlightedParentRecords = null; lmeState.managerHoveredParentKey = null; lmeState.managerHoveredParentRecords = null; + setSelectedDrawing(null); document.querySelectorAll(".layer-manager-object-selected").forEach(row => { row.classList.remove("layer-manager-object-selected"); }); @@ -2126,13 +2198,14 @@ function clearImported() { function addProjectMarker(name) { const center = lmeState.map.getCenter(); const label = (name || "Nuovo POI").trim(); - const marker = L.marker(center, { pane: "drawingsPane", draggable: true }).addTo(lmeState.drawings); + const marker = L.marker(center, { pane: "drawingsPane", draggable: false }).addTo(lmeState.drawings); marker.lmeManagerLabel = `POI: ${label}`; marker.feature = { type: "Feature", properties: { name: label } }; marker.bindPopup(`${escapeHtml(label)}
${center.lat.toFixed(6)}, ${center.lng.toFixed(6)}`).openPopup(); + bindDrawingLayerSelection(marker); refreshDrawingMeasurements(); setInfo(`POI aggiunto: ${label}`); refreshLayerManager(); @@ -2144,8 +2217,8 @@ function configureDrawLabels() { L.drawLocal.draw.toolbar.buttons.circle = "Misura cerchio"; L.drawLocal.draw.toolbar.buttons.rectangle = "Disegna rettangolo"; L.drawLocal.draw.toolbar.buttons.marker = "Aggiungi POI"; - L.drawLocal.edit.toolbar.buttons.edit = "Modifica disegni"; - L.drawLocal.edit.toolbar.buttons.remove = "Elimina disegni"; + L.drawLocal.edit.toolbar.buttons.edit = "Modifica oggetto selezionato"; + L.drawLocal.edit.toolbar.buttons.remove = "Elimina oggetto selezionato"; L.drawLocal.draw.handlers.polygon.tooltip.start = "Tocca il primo vertice dell'area."; L.drawLocal.draw.handlers.polygon.tooltip.cont = "Tocca per aggiungere un vertice."; L.drawLocal.draw.handlers.polygon.tooltip.end = "Tocca il primo punto per chiudere l'area."; @@ -2173,6 +2246,7 @@ function bindDrawingEvents() { }; setInfo(completedLabels[event.layerType] || "Disegno completato."); refreshLayerManager(); + selectDrawingLayer(event.layer); }); lmeState.map.on(L.Draw.Event.EDITED, event => { event.layers.eachLayer(layer => annotateDrawingLayer(layer)); @@ -2180,7 +2254,9 @@ function bindDrawingEvents() { updateGpsInsideState(); refreshLayerManager(); }); - lmeState.map.on(L.Draw.Event.DELETED, () => { + lmeState.map.on(L.Draw.Event.DELETED, event => { + event.layers?.eachLayer(layer => lmeState.drawings.removeLayer(layer)); + clearManagerHighlight(); refreshDrawingMeasurements(); updateGpsInsideState(); refreshLayerManager(); @@ -2198,10 +2274,28 @@ function enableDrawTools() { } configureDrawLabels(); - const orangeArea = { pane: "drawingsPane", color: "#f97316", weight: 4, fillColor: "#fb923c", fillOpacity: 0.2 }; + const orangeArea = { + pane: "drawingsPane", + renderer: lmeState.drawingsRenderer, + color: "#f97316", + weight: 4, + fillColor: "#fb923c", + fillOpacity: 0.2 + }; lmeState.drawControl = new L.Control.Draw({ position: "topright", - edit: { featureGroup: lmeState.drawings }, + edit: { + featureGroup: lmeState.drawingSelection, + remove: false, + edit: { + selectedPathOptions: { + color: "#ff00d4", + fillColor: "#00fff0", + fillOpacity: 0.16, + maintainColor: false + } + } + }, draw: { circle: { metric: true, feet: false, showRadius: true, shapeOptions: orangeArea }, circlemarker: false, @@ -2217,14 +2311,72 @@ function enableDrawTools() { metric: true, feet: false, showLength: true, - shapeOptions: { pane: "drawingsPane", color: "#f97316", weight: 4, opacity: 0.95 } + shapeOptions: { + pane: "drawingsPane", + renderer: lmeState.drawingsRenderer, + color: "#f97316", + weight: 4, + opacity: 0.95 + } }, rectangle: { metric: true, shapeOptions: orangeArea } } }); lmeState.map.addControl(lmeState.drawControl); + installSelectedDrawingDeleteButton(); bindDrawingEvents(); - setInfo("Barra strumenti attiva: misure, disegni, POI, modifica e cancellazione."); + setInfo("Barra strumenti attiva: seleziona un disegno sulla mappa, poi usa matita o cestino."); +} + +function installSelectedDrawingDeleteButton() { + const toolbar = drawingEditToolbar(); + const container = toolbar?._toolbarContainer; + if (!container) { + return; + } + const button = L.DomUtil.create("a", "leaflet-draw-edit-remove", container); + button.href = "#"; + button.title = L.drawLocal.edit.toolbar.buttons.remove; + button.setAttribute("role", "button"); + button.setAttribute("aria-label", L.drawLocal.edit.toolbar.buttons.remove); + const accessibleLabel = L.DomUtil.create("span", "sr-only", button); + accessibleLabel.textContent = L.drawLocal.edit.toolbar.buttons.remove; + L.DomEvent.on(button, "click", L.DomEvent.stop) + .on(button, "mousedown", L.DomEvent.stopPropagation) + .on(button, "touchstart", L.DomEvent.stopPropagation) + .on(button, "click", removeSelectedDrawing); + lmeState.drawingDeleteButton = button; + updateDrawingSelectionActions(); +} + +function removeSelectedDrawing() { + const layer = lmeState.drawingSelection?.getLayers?.()[0]; + if (!layer) { + setInfo("Seleziona prima il disegno da eliminare toccandolo sulla mappa."); + return; + } + if (drawingEditActive()) { + setInfo("Salva o annulla la modifica prima di eliminare il disegno."); + return; + } + const record = { + id: managedLayerId(layer, "drawings"), + categoryKey: "drawings", + layer, + parent: lmeState.drawings + }; + const name = managedObjectLabel(record, 0); + if (!window.confirm(`Eliminare solo '${name}' dal progetto?`)) { + return; + } + lmeState.drawingSelection.clearLayers(); + lmeState.drawings.removeLayer(layer); + clearManagedObjectState(record.id); + clearManagerHighlight(); + refreshDrawingMeasurements(); + updateGpsInsideState(); + refreshLayerManager(); + setInfo(`'${name}' eliminato dal progetto.`); } function startMeasurementDrawing(mode) { @@ -2242,19 +2394,39 @@ function startMeasurementDrawing(mode) { showArea: true, metric: true, feet: false, - shapeOptions: { pane: "drawingsPane", color: "#f97316", weight: 4, fillColor: "#fb923c", fillOpacity: 0.2 } + shapeOptions: { + pane: "drawingsPane", + renderer: lmeState.drawingsRenderer, + color: "#f97316", + weight: 4, + fillColor: "#fb923c", + fillOpacity: 0.2 + } }), distance: () => new L.Draw.Polyline(lmeState.map, { metric: true, feet: false, showLength: true, - shapeOptions: { pane: "drawingsPane", color: "#f97316", weight: 4, opacity: 0.95 } + shapeOptions: { + pane: "drawingsPane", + renderer: lmeState.drawingsRenderer, + color: "#f97316", + weight: 4, + opacity: 0.95 + } }), circle: () => new L.Draw.Circle(lmeState.map, { metric: true, feet: false, showRadius: true, - shapeOptions: { pane: "drawingsPane", color: "#f97316", weight: 4, fillColor: "#fb923c", fillOpacity: 0.2 } + shapeOptions: { + pane: "drawingsPane", + renderer: lmeState.drawingsRenderer, + color: "#f97316", + weight: 4, + fillColor: "#fb923c", + fillOpacity: 0.2 + } }) }; const createHandler = handlers[mode]; @@ -2275,6 +2447,9 @@ function startMeasurementDrawing(mode) { function annotateDrawingLayer(layer, layerType) { layer.options.pane = "drawingsPane"; + if (layer instanceof L.Path) { + layer.options.renderer = lmeState.drawingsRenderer; + } layer.feature = layer.feature || { type: "Feature", properties: {} }; layer.feature.properties = layer.feature.properties || {}; if (layer instanceof L.Circle) { @@ -2284,6 +2459,25 @@ function annotateDrawingLayer(layer, layerType) { layer.feature.properties._lmeShape = layerType; } layer.lmeManagerLabel = drawingLayerLabel(layer, layerType); + bindDrawingLayerSelection(layer); +} + +function bindDrawingLayerSelection(layer) { + if (layer.lmeDrawingSelectionBound) { + return; + } + layer.lmeDrawingSelectionBound = true; + layer.on("click", event => { + if (drawingEditActive()) { + return; + } + lmeState.ignoreMapClickUntil = Date.now() + 1000; + if (event.originalEvent) { + event.originalEvent._lmeDrawingSelected = true; + L.DomEvent.stopPropagation(event.originalEvent); + } + selectDrawingLayer(layer); + }); } function setDrawToolsVisible(visible) { @@ -2303,6 +2497,7 @@ function setDrawToolsVisible(visible) { }); lmeState.map.removeControl(lmeState.drawControl); lmeState.drawControl = null; + lmeState.drawingDeleteButton = null; setInfo("Strumenti di disegno nascosti."); } @@ -2317,6 +2512,9 @@ function clearDrawings() { return; } + drawingEditToolbar()?.disable?.(); + lmeState.drawingSelection?.clearLayers(); + clearManagerHighlight(); lmeState.drawings.clearLayers(); clearManagedCategoryState("drawings"); lmeState.drawingMeasurements.clearLayers(); @@ -2699,12 +2897,18 @@ function loadProjectSnapshot(raw, projectName) { const restoredDrawings = L.geoJSON(data.drawings || emptyFeatureCollection(), { pane: "drawingsPane", - style: { color: "#f97316", weight: 3, fillOpacity: 0.16 }, + style: { + renderer: lmeState.drawingsRenderer, + color: "#f97316", + weight: 3, + fillOpacity: 0.16 + }, pointToLayer: (feature, latlng) => { const properties = feature?.properties || {}; if (properties._lmeShape === "circle" && Number(properties._lmeRadius) > 0) { return L.circle(latlng, { pane: "drawingsPane", + renderer: lmeState.drawingsRenderer, radius: Number(properties._lmeRadius), color: "#f97316", weight: 3, @@ -2712,7 +2916,7 @@ function loadProjectSnapshot(raw, projectName) { fillOpacity: 0.2 }); } - return L.marker(latlng, { pane: "drawingsPane", draggable: true }); + return L.marker(latlng, { pane: "drawingsPane", draggable: false }); }, onEachFeature: (feature, layer) => { bindStoredFeaturePopup(feature, layer); @@ -2947,6 +3151,8 @@ function managedGroupGeoJson(group, categoryKey) { } function resetLayerManagerProjectState() { + drawingEditToolbar()?.disable?.(); + lmeState.drawingSelection?.clearLayers(); lmeState.managerHiddenObjects.clear(); lmeState.managerHiddenObjectIds.clear(); lmeState.managerHiddenMeasurementSummaryIds.clear();