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