Fix geometry comparison and add map multi-select

This commit is contained in:
livio committed 2026-10-05 11:35:05 +02:00
1 parent 47b58a9b0e
commit e70a30f7d7
3 files changed
+225 -25

No files matched your search

+48
View File
@@ -1271,6 +1271,54 @@ input[type="checkbox"] {
opacity: 0.72;
}
.lme-geometry-toolbar a.lme-multi-selection-button.is-active {
background: #0f766e;
color: #fff;
box-shadow: inset 0 0 0 2px #99f6e4;
}
.lme-geometry-toolbar {
position: relative;
}
.lme-comparison-actions {
position: absolute;
right: calc(100% + 6px);
z-index: 1000;
display: grid;
min-width: 154px;
overflow: hidden;
border: 1px solid #b8c1c8;
border-radius: 4px;
background: #fff;
box-shadow: 0 2px 8px rgb(0 0 0 / 22%);
}
.lme-geometry-toolbar .lme-comparison-actions a {
display: flex !important;
width: auto !important;
height: 36px !important;
padding: 0 10px;
justify-content: flex-start;
border: 0;
border-bottom: 1px solid #d7dde1;
color: #1f2933;
font-size: 13px !important;
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
}
.lme-geometry-toolbar .lme-comparison-actions a:last-child {
border-bottom: 0;
}
.lme-geometry-toolbar .lme-comparison-actions a:hover,
.lme-geometry-toolbar .lme-comparison-actions a:focus-visible {
background: #e8f4f1;
color: #075e54;
}
.leaflet-help {
display: grid;
gap: 14px;
+175 -23
View File
@@ -37,6 +37,9 @@ const lmeState = {
drawingDeleteButton: null,
drawingScopeMenu: null,
geometryToolsControl: null,
comparisonMenu: null,
multiSelectionEnabled: false,
multiSelectionButton: null,
fiducialDownloadButton: null,
ignoreMapClickUntil: 0,
activeDrawHandler: null,
@@ -262,6 +265,17 @@ function canAddDrawingObjects(amount = 1) {
return false;
}
function ensureLeafletRotateRendererCompatibility() {
const rendererPrototype = window.L?.Renderer?.prototype;
const currentOnAdd = rendererPrototype?.onAdd;
if (!currentOnAdd || currentOnAdd._lmeMapFallback) return;
const compatibleOnAdd = function (map) {
return currentOnAdd.call(this, map || this._map || lmeState.map);
};
compatibleOnAdd._lmeMapFallback = true;
rendererPrototype.onAdd = compatibleOnAdd;
}
function init(dotNetReference) {
if (dotNetReference) {
lmeState.dotNetReference = dotNetReference;
@@ -270,6 +284,7 @@ function init(dotNetReference) {
return;
}
ensureLeafletRotateRendererCompatibility();
lmeState.map = L.map("map", {
zoomControl: false,
attributionControl: false,
@@ -334,6 +349,7 @@ function init(dotNetReference) {
});
lmeState.map.createPane("drawingsMarkerPane", lmeState.map._norotatePane || lmeState.map._mapPane);
lmeState.map.createPane("gpsAccuracyPane", lmeState.map._rotatePane || lmeState.map._mapPane);
lmeState.map.createPane("comparisonPane", lmeState.map._rotatePane || lmeState.map._mapPane);
const managerHighlightPane = lmeState.map.createPane("managerHighlightPane", lmeState.map._rotatePane || lmeState.map._mapPane);
managerHighlightPane.style.zIndex = "650";
lmeState.drawingsRenderer = L.canvas({ pane: "drawingsPane", tolerance: 14 });
@@ -421,6 +437,10 @@ function init(dotNetReference) {
&& !event.target?.closest?.(".leaflet-draw-edit-edit, .leaflet-draw-edit-remove")) {
hideDrawingScopeMenu();
}
if (lmeState.comparisonMenu
&& !event.target?.closest?.(".lme-comparison-actions, .lme-comparison-button")) {
hideComparisonMenu();
}
if (!lmeState.managerHighlightedObjectId && !lmeState.managerHighlightedParentKey) {
return;
}
@@ -1587,7 +1607,7 @@ function setLayerOrder(order) {
normalized.forEach((key, index) => {
const zIndex = String(590 - index * 20);
const paneNames = [panes[key]];
if (key === "drawings") paneNames.push("drawingsMarkerPane");
if (key === "drawings") paneNames.push("drawingsMarkerPane", "comparisonPane");
if (key === "gpsPosition") paneNames.push("gpsAccuracyPane");
paneNames.forEach(paneName => {
const pane = lmeState.map.getPane(paneName);
@@ -3053,7 +3073,7 @@ function addDrawingFeature(feature, select = false) {
function storedFeatureStyle(feature) {
const properties = feature?.properties || {};
return {
renderer: lmeState.drawingsRenderer,
...(properties._lmeComparisonResult ? { pane: "comparisonPane" } : { renderer: lmeState.drawingsRenderer }),
color: properties._lmeColor || "#f97316",
fillColor: properties._lmeFillColor || "#fb923c",
weight: Number(properties._lmeWeight) || 3,
@@ -3238,22 +3258,116 @@ function compareSelectedGeometries() {
const onlySecond = turf.difference(turf.featureCollection([second, first]));
const overlap = turf.intersect(turf.featureCollection([first, second]));
rememberMapChange();
const addResult = (feature, name, color) => {
const resultLayers = [];
const addResult = (feature, name, color, role) => {
if (!feature) return;
feature.properties = { name, _lmeColor: color, _lmeFillColor: color, _lmeFillOpacity: 0.38, _lmeManagerId: undefined };
addDrawingFeature(feature);
feature.properties = {
name,
_lmeColor: color,
_lmeFillColor: color,
_lmeWeight: 3,
_lmeOpacity: 1,
_lmeFillOpacity: 0.48,
_lmeComparisonResult: true,
_lmeComparisonRole: role,
_lmeManagerId: undefined
};
const layer = addDrawingFeature(feature);
if (layer) resultLayers.push(layer);
};
addResult(onlyFirst, `Solo ${managedObjectLabel(polygons[0], 0)}`, "#ef4444");
addResult(onlySecond, `Solo ${managedObjectLabel(polygons[1], 1)}`, "#3b82f6");
addResult(overlap, "Sovrapposizione", "#16a34a");
addResult(onlyFirst, `Solo ${managedObjectLabel(polygons[0], 0)}`, "#ef4444", "only-first");
addResult(onlySecond, `Solo ${managedObjectLabel(polygons[1], 1)}`, "#2563eb", "only-second");
addResult(overlap, "Sovrapposizione", "#16a34a", "overlap");
const firstArea = onlyFirst ? turf.area(onlyFirst) : 0; const secondArea = onlySecond ? turf.area(onlySecond) : 0; const overlapArea = overlap ? turf.area(overlap) : 0;
const message = `Confronto creato. Solo primo confine: ${formatArea(firstArea)}. Solo secondo confine: ${formatArea(secondArea)}. Sovrapposizione: ${formatArea(overlapArea)}.`;
lmeState.managerSelectedObjectIds.clear();
clearManagerHighlight();
finishManagedGeometryChange(message);
resultLayers.forEach(layer => layer.bringToFront?.());
showGeometryResult("Confronto completato", `${message} Sulla mappa: rosso solo primo, blu solo secondo, verde sovrapposizione.`, "success");
return true;
} catch (error) { showGeometryResult("Confronto non riuscito", error.message, "error"); return false; }
}
function isComparisonResultLayer(layer) {
const properties = layer?.feature?.properties || {};
if (properties._lmeComparisonResult || properties._lmeComparisonRole) return true;
const name = String(properties.name || "");
const color = String(properties._lmeFillColor || properties._lmeColor || "").toLowerCase();
return (name.startsWith("Solo ") && ["#ef4444", "#3b82f6", "#2563eb"].includes(color))
|| (name === "Sovrapposizione" && color === "#16a34a");
}
function clearComparisonResults() {
hideComparisonMenu();
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return false; }
const layers = lmeState.drawings?.getLayers?.().filter(isComparisonResultLayer) || [];
if (!layers.length) {
setInfo("Non ci sono risultati di confronto da rimuovere.");
return false;
}
rememberMapChange();
layers.forEach(layer => {
const id = layer.feature?.properties?._lmeManagerId;
if (id) {
lmeState.managerSelectedObjectIds.delete(id);
clearManagedObjectState(id);
}
lmeState.drawings.removeLayer(layer);
});
clearManagerHighlight();
const message = `Rimossi ${layers.length} layer del confronto.`;
finishManagedGeometryChange(message);
return true;
}
function showComparisonMenu(button) {
if (lmeState.comparisonMenu) {
hideComparisonMenu();
return;
}
const menu = L.DomUtil.create("div", "lme-comparison-actions", button.parentElement);
menu.style.top = `${button.offsetTop}px`;
menu.innerHTML = `<a href="#" data-comparison-action="compare">Confronta</a><a href="#" data-comparison-action="clear">Pulisci confronto</a>`;
lmeState.comparisonMenu = menu;
menu.querySelectorAll("a").forEach(link => {
L.DomEvent.on(link, "click", L.DomEvent.stop)
.on(link, "mousedown", L.DomEvent.stopPropagation)
.on(link, "touchstart", L.DomEvent.stopPropagation)
.on(link, "click", () => {
const action = link.dataset.comparisonAction;
hideComparisonMenu();
if (action === "compare") compareSelectedGeometries();
else clearComparisonResults();
});
});
}
function hideComparisonMenu() {
lmeState.comparisonMenu?.remove();
lmeState.comparisonMenu = null;
}
function toggleMultiSelection() {
lmeState.multiSelectionEnabled = !lmeState.multiSelectionEnabled;
lmeState.multiSelectionButton?.classList.toggle("is-active", lmeState.multiSelectionEnabled);
lmeState.multiSelectionButton?.setAttribute("aria-pressed", String(lmeState.multiSelectionEnabled));
setInfo(lmeState.multiSelectionEnabled
? "Selezione multipla attiva: tocca gli oggetti per aggiungerli o rimuoverli dalla selezione."
: "Selezione multipla disattivata.");
return lmeState.multiSelectionEnabled;
}
function refreshMultipleMapSelection(preferredRecord) {
clearManagerHighlight();
const selectedRecords = selectedManagedRecords();
if (!selectedRecords.length) return;
const current = selectedRecords.find(record => record.id === preferredRecord?.id) || selectedRecords.at(-1);
lmeState.managerHighlightedObjectId = current?.id || null;
lmeState.managerHighlightedCategoryKey = current?.categoryKey || null;
renderManagerHighlight(selectedRecords);
}
function showGeometryResult(title, message, tone = "success") {
setInfo(`${title}: ${message}`);
const icons = { success: "✓", warning: "!", error: "×" };
@@ -3275,9 +3389,10 @@ function showLeafletToolbarHelp() {
<div><dt>▱</dt><dd><strong>Elimina disegni</strong>Premi il cestino e scegli Solo selezionato oppure Tutti. L'oggetto rimosso può essere recuperato con Annulla o dal cestino negli strumenti avanzati.</dd></div>
<div><dt>✓</dt><dd><strong>Controlla e ripara</strong>Verifica gli oggetti selezionati e prova a correggere coordinate duplicate o poligoni non validi. Esempio: usalo quando un contorno importato non si esporta o si riempie male; al termine compare un riepilogo.</dd></div>
<div><dt>◉</dt><dd><strong>Crea foro</strong>Seleziona esattamente due poligoni: prima l'area esterna, poi quella interna. Esempio: sottrai il cortile dal perimetro di un fabbricato o un'isola esclusa da un'area di lavoro.</dd></div>
<div><dt>⇄</dt><dd><strong>Confronta due confini</strong>Seleziona esattamente due poligoni. L'app crea il solo primo in rosso, il solo secondo in blu e la sovrapposizione in verde, mostrando le rispettive aree.</dd></div>
<div><dt>☑</dt><dd><strong>Selezione multipla</strong>Attiva il comando e tocca più oggetti sulla mappa per aggiungerli o rimuoverli dalla selezione. Disattivalo per tornare alla selezione singola. È utile prima di Confronta, Unisci, Buffer e altre operazioni su più oggetti.</dd></div>
<div><dt>⇄</dt><dd><strong>Confronta due confini</strong>Apri il comando e scegli Confronta dopo aver selezionato esattamente due poligoni. L'app crea il solo primo in rosso, il solo secondo in blu e la sovrapposizione in verde. Usa Pulisci confronto per rimuovere soltanto questi risultati.</dd></div>
<div><dt>PF</dt><dd><strong>Scarica Punti Fiduciali</strong>Seleziona una particella o un foglio catastale; in alternativa usa la particella rilevata dal GPS. I PF vengono aggiunti come sottogruppo del relativo foglio e richiedono LME Pro.</dd></div>
<div><dt>?</dt><dd><strong>Guida</strong>Riapre questa descrizione. I risultati di riparazione e confronto vengono mostrati anche in una finestra; per uscire dal confronto usa Annulla oppure avvia un nuovo confronto.</dd></div>
<div><dt>?</dt><dd><strong>Guida</strong>Riapre questa descrizione. I risultati di riparazione e confronto vengono mostrati anche in una finestra; per uscire dalla visualizzazione rosso, blu e verde usa Pulisci confronto.</dd></div>
</dl>
</div>`);
}
@@ -3291,7 +3406,8 @@ function addGeometryToolsControl() {
const actions = [
{ icon: "✓", title: "Controlla e ripara geometrie", run: validateAndRepairSelected },
{ icon: "◉", title: "Crea foro da due poligoni", run: createHoleFromSelected },
{ icon: "⇄", title: "Confronta due confini", run: compareSelectedGeometries },
{ icon: "☑", title: "Attiva o disattiva selezione multipla", run: toggleMultiSelection, multiSelection: true },
{ icon: "⇄", title: "Confronta o pulisci confronto", comparison: true },
{ icon: "PF", title: "Scarica PF del foglio o della particella", run: downloadFiducialsForMapContext, fiducials: true },
{ icon: "?", title: "Guida strumenti mappa", run: showLeafletToolbarHelp }
];
@@ -3306,7 +3422,17 @@ function addGeometryToolsControl() {
button.classList.add("lme-fiducial-download");
lmeState.fiducialDownloadButton = button;
}
L.DomEvent.on(button, "click", event => { L.DomEvent.stop(event); action.run(); });
if (action.comparison) button.classList.add("lme-comparison-button");
if (action.multiSelection) {
button.classList.add("lme-multi-selection-button");
button.setAttribute("aria-pressed", String(lmeState.multiSelectionEnabled));
lmeState.multiSelectionButton = button;
}
L.DomEvent.on(button, "click", event => {
L.DomEvent.stop(event);
if (action.comparison) showComparisonMenu(button);
else action.run();
});
});
L.DomEvent.disableClickPropagation(container);
return container;
@@ -4667,12 +4793,17 @@ 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 || {};
const isComparisonResult = Boolean(layer.feature.properties._lmeComparisonResult);
layer.options.pane = isComparisonResult ? "comparisonPane" : "drawingsPane";
if (layer instanceof L.Path) {
if (isComparisonResult) {
delete layer.options.renderer;
} else {
layer.options.renderer = lmeState.drawingsRenderer;
}
}
if (layer instanceof L.Circle) {
layer.feature.properties._lmeShape = "circle";
layer.feature.properties._lmeRadius = layer.getRadius();
@@ -4698,16 +4829,27 @@ function bindDrawingLayerSelection(layer) {
L.DomEvent.stopPropagation(event.originalEvent);
}
const id = managedLayerId(layer, "drawings");
if (event.originalEvent?.ctrlKey || event.originalEvent?.metaKey || event.originalEvent?.shiftKey) {
const multiple = lmeState.multiSelectionEnabled
|| event.originalEvent?.ctrlKey
|| event.originalEvent?.metaKey
|| event.originalEvent?.shiftKey;
if (multiple) {
if (lmeState.managerSelectedObjectIds.has(id)) lmeState.managerSelectedObjectIds.delete(id);
else lmeState.managerSelectedObjectIds.add(id);
} else {
lmeState.managerSelectedObjectIds.clear();
lmeState.managerSelectedObjectIds.add(id);
}
selectDrawingLayer(layer);
refreshLayerManager();
window.requestAnimationFrame(() => revealManagedRecordInLayerManager(id));
if (multiple) {
refreshMultipleMapSelection({ id, categoryKey: "drawings", layer, parent: lmeState.drawings });
setInfo(`${lmeState.managerSelectedObjectIds.size} oggetti selezionati.`);
} else {
selectDrawingLayer(layer);
}
if (lmeState.managerSelectedObjectIds.has(id)) {
window.requestAnimationFrame(() => revealManagedRecordInLayerManager(id));
}
});
}
@@ -4722,14 +4864,24 @@ function bindManagedMapSelection(layer, categoryKey, parent) {
L.DomEvent.stopPropagation(event.originalEvent);
}
const record = { id: managedLayerId(layer, categoryKey), categoryKey, layer, parent, source: String(layer?.feature?.properties?._lmeSource || "Progetto corrente") };
const multiple = event.originalEvent?.ctrlKey || event.originalEvent?.metaKey || event.originalEvent?.shiftKey;
const multiple = lmeState.multiSelectionEnabled
|| event.originalEvent?.ctrlKey
|| event.originalEvent?.metaKey
|| event.originalEvent?.shiftKey;
if (!multiple) lmeState.managerSelectedObjectIds.clear();
if (multiple && lmeState.managerSelectedObjectIds.has(record.id)) lmeState.managerSelectedObjectIds.delete(record.id);
else lmeState.managerSelectedObjectIds.add(record.id);
highlightManagedObject(record);
refreshLayerManager();
window.requestAnimationFrame(() => revealManagedRecordInLayerManager(record.id));
setInfo(`Selezionato dalla mappa: ${managedObjectEditableName(record) || "oggetto"}.`);
if (multiple) {
refreshMultipleMapSelection(record);
setInfo(`${lmeState.managerSelectedObjectIds.size} oggetti selezionati.`);
} else {
highlightManagedObject(record);
setInfo(`Selezionato dalla mappa: ${managedObjectEditableName(record) || "oggetto"}.`);
}
if (lmeState.managerSelectedObjectIds.has(record.id)) {
window.requestAnimationFrame(() => revealManagedRecordInLayerManager(record.id));
}
});
}