diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj index b1980e7..9f6e8a3 100644 --- a/LMEMappeCatastaliApp.csproj +++ b/LMEMappeCatastaliApp.csproj @@ -28,8 +28,8 @@ com.companyname.lmemappecatastaliapp - 0.2.23 - 27 + 0.2.24 + 28 None diff --git a/Services/MapExportService.cs b/Services/MapExportService.cs index 3966050..f5d74a0 100644 --- a/Services/MapExportService.cs +++ b/Services/MapExportService.cs @@ -78,6 +78,8 @@ public sealed class MapExportService BuildStyle("drawing", "ff1679f9", "333c92fb"), BuildStyle("poi", "ffff7a00", "33ff7a00"), BuildStyle("gps-trail", "ff00a5ff", "2200a5ff")); + var categoryFolders = new Dictionary(StringComparer.Ordinal); + var groupFolders = new Dictionary<(string Category, string Group), XElement>(); foreach (var feature in features.EnumerateArray()) { @@ -102,7 +104,31 @@ public sealed class MapExportService placemark.Add(extendedData); } placemark.Add(BuildGeometry(geometry)); - document.Add(placemark); + var categoryName = GetString(properties, "_lmeCategoryName")?.Trim(); + var groupName = GetString(properties, "_lmeGroupName")?.Trim(); + var parent = document; + if (!string.IsNullOrWhiteSpace(categoryName)) + { + if (!categoryFolders.TryGetValue(categoryName, out var categoryFolder)) + { + categoryFolder = new XElement(Kml + "Folder", new XElement(Kml + "name", categoryName)); + categoryFolders[categoryName] = categoryFolder; + document.Add(categoryFolder); + } + parent = categoryFolder; + } + if (!string.IsNullOrWhiteSpace(groupName)) + { + var key = (categoryName ?? string.Empty, groupName); + if (!groupFolders.TryGetValue(key, out var groupFolder)) + { + groupFolder = new XElement(Kml + "Folder", new XElement(Kml + "name", groupName)); + groupFolders[key] = groupFolder; + parent.Add(groupFolder); + } + parent = groupFolder; + } + parent.Add(placemark); } return new XDocument(new XDeclaration("1.0", "utf-8", null), new XElement(Kml + "kml", document)).ToString(); diff --git a/wwwroot/css/app.css b/wwwroot/css/app.css index 6887dc0..8dae8a3 100644 --- a/wwwroot/css/app.css +++ b/wwwroot/css/app.css @@ -287,6 +287,10 @@ input[type="checkbox"] { grid-template-columns: 28px 20px minmax(0, 1fr) auto auto auto; } +.layer-manager-renamable > summary { + grid-template-columns: 28px 20px minmax(0, 1fr) auto auto auto; +} + .layer-manager-group > summary::-webkit-details-marker, .layer-manager-block > summary::-webkit-details-marker, .layer-manager-object > summary::-webkit-details-marker { @@ -399,6 +403,12 @@ input[type="checkbox"] { box-shadow: inset 3px 0 #ff00d4; } +.layer-manager-parent-highlighted > summary, +.layer-manager-parent-selected > summary { + background: #fff0fb; + box-shadow: inset 3px 0 #ff00d4; +} + .gps-overlay { position: absolute; top: 72px; diff --git a/wwwroot/js/lme-map.js b/wwwroot/js/lme-map.js index 5b9024f..390b940 100644 --- a/wwwroot/js/lme-map.js +++ b/wwwroot/js/lme-map.js @@ -51,6 +51,10 @@ const lmeState = { managerHighlight: null, managerHighlightedObjectId: null, managerHighlightedCategoryKey: null, + managerHighlightedParentKey: null, + managerHighlightedParentRecords: null, + managerHoveredParentKey: null, + managerHoveredParentRecords: null, managerOutsideSelectionBound: false, managerHiddenObjects: new Map(), managerHiddenObjectIds: new Set(), @@ -58,6 +62,7 @@ const lmeState = { managerHiddenMeasurementSideIds: new Set(), managerHiddenCategories: new Set(), managerObjectOrder: {}, + managerCategoryNames: {}, managerExpandedRows: new Set(), dotNetReference: null }; @@ -223,11 +228,13 @@ function init(dotNetReference) { }); if (!lmeState.managerOutsideSelectionBound) { document.addEventListener("pointerdown", event => { - if (!lmeState.managerHighlightedObjectId) { + if (!lmeState.managerHighlightedObjectId && !lmeState.managerHighlightedParentKey) { return; } - const selectedRow = event.target?.closest?.(".layer-manager-object"); - if (selectedRow?.dataset.objectId === lmeState.managerHighlightedObjectId) { + const selectedObject = event.target?.closest?.(".layer-manager-object"); + const selectedParent = event.target?.closest?.("[data-highlight-key]"); + if (selectedObject?.dataset.objectId === lmeState.managerHighlightedObjectId + || selectedParent?.dataset.highlightKey === lmeState.managerHighlightedParentKey) { return; } clearManagerHighlight(); @@ -1181,6 +1188,69 @@ function renameManagedObject(record, index) { setInfo(`Oggetto rinominato: ${name}.`); } +function managedCategoryName(category, records) { + const storedName = lmeState.managerCategoryNames[category.key]; + const featureName = records + .map(record => record.layer?.feature?.properties?._lmeCategoryName) + .find(Boolean); + const name = String(storedName || featureName || category.label).trim(); + if (name !== category.label) { + lmeState.managerCategoryNames[category.key] = name; + } + return name; +} + +function managedCategoryCanRename(category) { + return ["drawings", "imported", "importedSheets"].includes(category.key); +} + +function renameManagedCategory(category, records) { + const currentName = managedCategoryName(category, records); + const requestedName = window.prompt("Nuovo nome gruppo", currentName); + if (requestedName === null) { + return; + } + const name = requestedName.trim().slice(0, 120); + if (!name) { + setInfo("Nome non modificato: inserisci un testo."); + return; + } + lmeState.managerCategoryNames[category.key] = name; + records.forEach(record => { + if (record.layer?.feature?.properties) { + record.layer.feature.properties._lmeCategoryName = name; + } + }); + refreshLayerManager(); + setInfo(`Gruppo rinominato: ${name}.`); +} + +function managedBlockName(source, records) { + return String(records + .map(record => record.layer?.feature?.properties?._lmeGroupName) + .find(Boolean) || source); +} + +function renameManagedBlock(category, source, records) { + const currentName = managedBlockName(source, records); + const requestedName = window.prompt("Nuovo nome gruppo", currentName); + if (requestedName === null) { + return; + } + const name = requestedName.trim().slice(0, 120); + if (!name) { + setInfo("Nome non modificato: inserisci un testo."); + return; + } + records.forEach(record => { + if (record.layer?.feature?.properties) { + record.layer.feature.properties._lmeGroupName = name; + } + }); + refreshLayerManager(); + setInfo(`Gruppo rinominato: ${name}.`); +} + function managedObjectDetails(layer) { const details = []; const properties = layer?.feature?.properties || {}; @@ -1260,15 +1330,26 @@ function renderLayerManagerCategory(category, records) { group.dataset.dragType = "category"; group.dataset.categoryKey = category.key; bindLayerManagerExpansion(group, `category:${category.key}`); + const categoryName = managedCategoryName(category, records); + const renameButton = managedCategoryCanRename(category) + ? `` + : ""; + group.classList.toggle("layer-manager-renamable", Boolean(renameButton)); const visibleCount = records.filter(record => !lmeState.managerHiddenObjectIds.has(record.id)).length; const categoryVisible = !lmeState.managerHiddenCategories.has(category.key) && (!category.roots.length || category.roots.some(layer => lmeState.map.hasLayer(layer))); const visible = categoryVisible; - group.innerHTML = `${escapeHtml(category.label)}${layerManagerMoveButtons("gruppo")}${records.length}
`; + group.innerHTML = `${escapeHtml(categoryName)}${renameButton}${layerManagerMoveButtons("gruppo")}${records.length}
`; const checkbox = group.querySelector("input"); checkbox.indeterminate = categoryVisible && records.length > 0 && visibleCount < records.length; checkbox.addEventListener("click", event => event.stopPropagation()); checkbox.addEventListener("change", () => setManagedCategoryVisible(category, checkbox.checked)); + group.querySelector(".layer-rename-button")?.addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + renameManagedCategory(category, records); + }); + bindManagedParentHighlight(group, `category:${category.key}`, records); const children = group.querySelector(".layer-manager-children"); if (category.blocks) { const blocks = new Map(); @@ -1293,7 +1374,9 @@ function renderLayerManagerBlock(category, source, records) { bindLayerManagerExpansion(block, `block:${category.key}:${source}`); const visibleCount = records.filter(record => !lmeState.managerHiddenObjectIds.has(record.id)).length; const visible = visibleCount === records.length; - block.innerHTML = `${escapeHtml(source)}${layerManagerMoveButtons("blocco")}${records.length}
`; + const blockName = managedBlockName(source, records); + block.classList.add("layer-manager-renamable"); + block.innerHTML = `${escapeHtml(blockName)}${layerManagerMoveButtons("blocco")}${records.length}
`; const checkbox = block.querySelector("input"); checkbox.indeterminate = visibleCount > 0 && !visible; checkbox.addEventListener("click", event => event.stopPropagation()); @@ -1301,6 +1384,12 @@ function renderLayerManagerBlock(category, source, records) { records.forEach(record => setManagedObjectVisible(record, checkbox.checked, false)); refreshLayerManager(); }); + block.querySelector(".layer-rename-button").addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + renameManagedBlock(category, source, records); + }); + bindManagedParentHighlight(block, `block:${category.key}:${source}`, records); const children = block.querySelector(".layer-manager-children"); records.forEach((record, index) => children.appendChild(renderLayerManagerObject(record, index))); return block; @@ -1559,33 +1648,107 @@ function highlightManagedObject(record) { document.querySelectorAll(".layer-manager-object").forEach(row => { row.classList.toggle("layer-manager-object-selected", row.dataset.objectId === record.id); }); - const geojson = record.layer.toGeoJSON?.(); - if (geojson) { - L.geoJSON(geojson, { - pane: "managerHighlightPane", - interactive: false, - style: { color: "#ff00d4", weight: 8, opacity: 1, fillColor: "#00fff0", fillOpacity: 0.16 }, - pointToLayer: (feature, latlng) => L.circleMarker(latlng, { - pane: "managerHighlightPane", - radius: 12, - color: "#ff00d4", - weight: 5, - fillColor: "#00fff0", - fillOpacity: 0.9, - interactive: false - }) - }).addTo(lmeState.managerHighlight); - } + renderManagerHighlight([record]); fitLayer(record.layer); } +function bindManagedParentHighlight(row, key, records) { + const summary = row.querySelector(":scope > summary"); + row.dataset.highlightKey = key; + row.classList.toggle("layer-manager-parent-selected", lmeState.managerHighlightedParentKey === key); + if (lmeState.managerHighlightedParentKey === key) { + lmeState.managerHighlightedParentRecords = records; + } + row.querySelector(":scope > summary > .layer-manager-title").addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + row.open = true; + highlightManagedParent(row, key, records); + }); + summary.addEventListener("pointerenter", () => { + lmeState.managerHoveredParentKey = key; + lmeState.managerHoveredParentRecords = records; + row.classList.add("layer-manager-parent-highlighted"); + renderManagerHighlight(records); + }); + summary.addEventListener("pointerleave", () => { + if (lmeState.managerHoveredParentKey !== key) { + return; + } + lmeState.managerHoveredParentKey = null; + lmeState.managerHoveredParentRecords = null; + row.classList.remove("layer-manager-parent-highlighted"); + renderSelectedManagerHighlight(); + }); +} + +function highlightManagedParent(row, key, records) { + if (lmeState.managerHighlightedParentKey === key) { + clearManagerHighlight(); + return; + } + clearManagerHighlight(); + lmeState.managerHighlightedParentKey = key; + lmeState.managerHighlightedParentRecords = records; + lmeState.managerHighlightedCategoryKey = records[0]?.categoryKey || null; + row.classList.add("layer-manager-parent-selected"); + renderManagerHighlight(records); +} + +function renderSelectedManagerHighlight() { + if (lmeState.managerHighlightedParentRecords?.length) { + renderManagerHighlight(lmeState.managerHighlightedParentRecords); + return; + } + const selected = lmeState.managerRecords.get(lmeState.managerHighlightedObjectId); + renderManagerHighlight(selected ? [selected] : []); +} + +function renderManagerHighlight(records) { + lmeState.managerHighlight?.clearLayers(); + const features = records.flatMap(record => { + const geojson = record.layer?.toGeoJSON?.(); + if (!geojson) { + return []; + } + return geojson.type === "FeatureCollection" ? geojson.features : [geojson]; + }); + if (!features.length) { + return; + } + L.geoJSON({ type: "FeatureCollection", features }, { + pane: "managerHighlightPane", + interactive: false, + style: { color: "#ff00d4", weight: 8, opacity: 1, fillColor: "#00fff0", fillOpacity: 0.16 }, + pointToLayer: (feature, latlng) => L.circleMarker(latlng, { + pane: "managerHighlightPane", + radius: 12, + color: "#ff00d4", + weight: 5, + fillColor: "#00fff0", + fillOpacity: 0.9, + interactive: false + }) + }).addTo(lmeState.managerHighlight); +} + function clearManagerHighlight() { lmeState.managerHighlight?.clearLayers(); lmeState.managerHighlightedObjectId = null; lmeState.managerHighlightedCategoryKey = null; + lmeState.managerHighlightedParentKey = null; + lmeState.managerHighlightedParentRecords = null; + lmeState.managerHoveredParentKey = null; + lmeState.managerHoveredParentRecords = null; document.querySelectorAll(".layer-manager-object-selected").forEach(row => { row.classList.remove("layer-manager-object-selected"); }); + document.querySelectorAll(".layer-manager-parent-highlighted").forEach(row => { + row.classList.remove("layer-manager-parent-highlighted"); + }); + document.querySelectorAll(".layer-manager-parent-selected").forEach(row => { + row.classList.remove("layer-manager-parent-selected"); + }); } function layerManagerMoveButtons(label) { @@ -2531,6 +2694,7 @@ function captureDisplayOptions() { selectedParcelsVisible: lmeState.selectedParcelsVisible, layerOrder: [...lmeState.layerOrder], layerObjectOrder: { ...lmeState.managerObjectOrder }, + layerCategoryNames: { ...lmeState.managerCategoryNames }, hiddenLayerObjects: [...lmeState.managerHiddenObjectIds], hiddenLayerMeasurementSummaries: [...lmeState.managerHiddenMeasurementSummaryIds], hiddenLayerMeasurementSides: [...lmeState.managerHiddenMeasurementSideIds], @@ -2567,6 +2731,9 @@ function applyProjectDisplayOptions(options) { if (options.layerObjectOrder && typeof options.layerObjectOrder === "object") { lmeState.managerObjectOrder = options.layerObjectOrder; } + if (options.layerCategoryNames && typeof options.layerCategoryNames === "object") { + lmeState.managerCategoryNames = { ...options.layerCategoryNames }; + } if (Array.isArray(options.hiddenLayerObjects)) { lmeState.managerHiddenObjectIds = new Set(options.hiddenLayerObjects); } @@ -2653,6 +2820,12 @@ function managedGroupGeoJson(group, categoryKey) { } }); }); + const categoryName = lmeState.managerCategoryNames[categoryKey]; + if (categoryName) { + features.forEach(feature => { + feature.properties = { ...(feature.properties || {}), _lmeCategoryName: categoryName }; + }); + } return { type: "FeatureCollection", features }; } @@ -2663,6 +2836,7 @@ function resetLayerManagerProjectState() { lmeState.managerHiddenMeasurementSideIds.clear(); lmeState.managerHiddenCategories.clear(); lmeState.managerObjectOrder = {}; + lmeState.managerCategoryNames = {}; clearManagerHighlight(); }