diff --git a/Components/MenuHelp.razor b/Components/MenuHelp.razor
index fecdbde..f78c591 100644
--- a/Components/MenuHelp.razor
+++ b/Components/MenuHelp.razor
@@ -57,7 +57,7 @@
"data" =>
[
"Importazione: premi Carica KML, KMZ o GeoJSON e scegli il file dal dispositivo. I nomi e gli attributi riconosciuti vengono mantenuti.",
- "Per KML e KMZ, Includi anche il layer del foglio importa pure cornici e riquadri generali; disattivalo quando vuoi conservare soltanto le particelle riconosciute.",
+ "Per KML e KMZ, Includi anche il layer del foglio conserva le cornici presenti nel file. Mostra cornice/layer del foglio recupera inoltre dal servizio ufficiale una cornice distinta per ciascun foglio delle particelle, senza creare un unico riquadro tra fogli lontani.",
"Usa Mostra dati importati e Mostra cornice per controllare separatamente la visibilita. Inquadra dati porta la mappa sull'estensione completa del file.",
"Disegno: premi Mostra strumenti sulla mappa, torna alla mappa e scegli linea, poligono, rettangolo, cerchio o POI nella barra Leaflet.",
"Modifica ed eliminazione possono agire sull'oggetto selezionato o su tutti i disegni. Prima seleziona sulla mappa la linea o il poligono interessato, poi scegli l'ambito dal piccolo comando accanto alla matita o al cestino.",
diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor
index 90dd7ba..8246470 100644
--- a/Components/Pages/Home.razor
+++ b/Components/Pages/Home.razor
@@ -219,7 +219,7 @@
Mostra cornice/layer del foglio importato
- La cornice grande resta separata dalle particelle e puo essere nascosta anche nei progetti salvati.
+ Carica dal servizio ufficiale una cornice distinta per ogni foglio delle particelle; le cornici restano separate, trasferibili e salvabili nel progetto.
Mostra misure dei file caricati
@@ -1299,6 +1299,10 @@
return InvokeAsync(StateHasChanged);
}
+ [JSInvokable]
+ public Task GetOfficialCadastralSheetsNear(double latitude, double longitude) =>
+ OfficialCadastreService.GetSheetsNearAsync(latitude, longitude);
+
[JSInvokable]
public Task OnMapChanged(string action)
{
diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj
index 4ca4f85..77c12a7 100644
--- a/LMEMappeCatastaliApp.csproj
+++ b/LMEMappeCatastaliApp.csproj
@@ -28,8 +28,8 @@
com.companyname.lmemappecatastaliapp
- 0.4.5
- 40
+ 0.4.6
+ 41
$(ApplicationDisplayVersion)
diff --git a/README.md b/README.md
index 085429c..bf8f273 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,14 @@
App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata per consultare mappe catastali e lavorare sul campo con GPS.
+## Versione 0.4.6
+
+- `Mostra cornice/layer del foglio` carica dal WFS ufficiale AdE il vero confine `CP:CadastralZoning` di ciascun foglio delle particelle.
+- Le particelle appartenenti a fogli diversi producono cornici distinte, senza piu creare un unico rettangolo cumulativo tra aree lontane.
+- Le cornici gia presenti nel progetto o trasferite in un altro gruppo vengono riconosciute e non vengono scaricate o duplicate.
+- Ogni foglio e salvato come oggetto autonomo e puo essere rinominato, bloccato, rimosso o spostato in un altro gruppo dal pannello Layer.
+- I vecchi riquadri cumulativi generati automaticamente vengono rimossi durante il caricamento e sostituiti dai fogli ufficiali.
+
## Versione 0.4.5
- Corretto il layout del pannello Layer sui telefoni e sui foldable: checkbox, lucchetto, ordinamento e freccia di espansione non si sovrappongono e restano allineati.
diff --git a/Services/MapExportService.cs b/Services/MapExportService.cs
index 76e1b08..492073a 100644
--- a/Services/MapExportService.cs
+++ b/Services/MapExportService.cs
@@ -85,7 +85,7 @@ public sealed class MapExportService
{
var root = new XElement("gpx",
new XAttribute("version", "1.1"),
- new XAttribute("creator", "LME Mappe Catastali 0.4.5"),
+ new XAttribute("creator", "LME Mappe Catastali 0.4.6"),
new XAttribute(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance"),
new XElement("metadata", new XElement("name", name)));
foreach (var feature in features.EnumerateArray())
diff --git a/Services/OfficialCadastreService.cs b/Services/OfficialCadastreService.cs
index c399d3d..594dc46 100644
--- a/Services/OfficialCadastreService.cs
+++ b/Services/OfficialCadastreService.cs
@@ -22,6 +22,34 @@ public sealed class OfficialCadastreService
double longitude,
double radiusMeters,
CancellationToken cancellationToken = default)
+ => await GetFeaturesNearAsync(
+ latitude,
+ longitude,
+ radiusMeters,
+ "CP:CadastralParcel",
+ 100,
+ cancellationToken);
+
+ public async Task GetSheetsNearAsync(
+ double latitude,
+ double longitude,
+ double radiusMeters = 10,
+ CancellationToken cancellationToken = default)
+ => await GetFeaturesNearAsync(
+ latitude,
+ longitude,
+ radiusMeters,
+ "CP:CadastralZoning",
+ 20,
+ cancellationToken);
+
+ private async Task GetFeaturesNearAsync(
+ double latitude,
+ double longitude,
+ double radiusMeters,
+ string typeName,
+ int count,
+ CancellationToken cancellationToken)
{
var meters = Math.Clamp(radiusMeters, 2, 30);
var deltaLatitude = meters / 111_320d;
@@ -40,11 +68,11 @@ public sealed class OfficialCadastreService
["language"] = "ita",
["SERVICE"] = "WFS",
["VERSION"] = "2.0.0",
- ["TYPENAMES"] = "CP:CadastralParcel",
+ ["TYPENAMES"] = typeName,
["SRSNAME"] = "urn:ogc:def:crs:EPSG::6706",
["BBOX"] = bbox,
["REQUEST"] = "GetFeature",
- ["COUNT"] = "100"
+ ["COUNT"] = count.ToString(CultureInfo.InvariantCulture)
};
var url = WfsEndpoint + "?" + string.Join("&", query.Select(item =>
diff --git a/wwwroot/js/lme-map.js b/wwwroot/js/lme-map.js
index 5cd4f66..7bc9b4c 100644
--- a/wwwroot/js/lme-map.js
+++ b/wwwroot/js/lme-map.js
@@ -3461,77 +3461,155 @@ function applyManagedVisualOrder(container) {
});
}
-function setImportedSheetLayerVisible(enabled) {
+async function setImportedSheetLayerVisible(enabled) {
migrateMisclassifiedImportedSheets();
lmeState.importedSheetsVisible = Boolean(enabled);
- const generated = lmeState.importedSheetsVisible && ensureImportedSheetOutline();
+ let sheetResult = null;
+ if (lmeState.importedSheetsVisible) {
+ setInfo("Carico i fogli catastali delle particelle...");
+ sheetResult = await ensureImportedSheetOutlines();
+ }
applyImportedSheetVisibility();
const count = lmeState.importedSheets?.toGeoJSON()?.features?.length || 0;
setInfo(lmeState.importedSheetsVisible
? (count
- ? `${generated ? "Cornice generata dai dati importati" : "Layer del foglio importato visibile"} (${count} elementi).`
- : "Nessun dato importato da cui ricavare la cornice del foglio.")
+ ? `${sheetResult?.added ? "Fogli catastali ufficiali caricati" : "Layer dei fogli catastali visibile"} (${count} cornici).`
+ : (sheetResult?.failed
+ ? "Il servizio ufficiale dei fogli catastali non e' al momento disponibile. Riprova mostrando nuovamente il layer."
+ : "Nessuna particella con cui individuare il foglio catastale."))
: "Layer del foglio importato nascosto; le particelle restano visibili.");
}
-function ensureImportedSheetOutline() {
- if (!lmeState.imported || !lmeState.importedSheets) {
- return false;
- }
-
- const currentSheets = lmeState.importedSheets.toGeoJSON();
- const hasAreaOutline = (currentSheets.features || []).some(feature =>
- ["Polygon", "MultiPolygon"].includes(feature?.geometry?.type));
- if (hasAreaOutline) {
- return false;
- }
-
- const imported = lmeState.imported.toGeoJSON();
- const points = collectGeoJsonCoordinates(imported);
- if (!points.length) {
- return false;
- }
-
- const bounds = points.reduce((result, point) => ({
- west: Math.min(result.west, point[0]),
- south: Math.min(result.south, point[1]),
- east: Math.max(result.east, point[0]),
- north: Math.max(result.north, point[1])
- }), { west: Infinity, south: Infinity, east: -Infinity, north: -Infinity });
- if (!Number.isFinite(bounds.west)
- || !Number.isFinite(bounds.south)
- || bounds.east <= bounds.west
- || bounds.north <= bounds.south) {
- return false;
- }
-
- const longitudePadding = Math.max((bounds.east - bounds.west) * 0.01, 0.000001);
- const latitudePadding = Math.max((bounds.north - bounds.south) * 0.01, 0.000001);
- const west = bounds.west - longitudePadding;
- const south = bounds.south - latitudePadding;
- const east = bounds.east + longitudePadding;
- const north = bounds.north + latitudePadding;
- addImportedSheetFeatures({
- type: "FeatureCollection",
- features: [{
- type: "Feature",
- properties: {
- name: "Cornice del foglio importato",
- _lmeGeneratedSheetOutline: true
- },
- geometry: {
- type: "Polygon",
- coordinates: [[
- [west, south],
- [east, south],
- [east, north],
- [west, north],
- [west, south]
- ]]
- }
- }]
+function removeLegacyGeneratedSheetOutlines() {
+ const category = layerManagerCategories().find(item => item.key === "importedSheets");
+ if (!category) return 0;
+ const generated = managedRecordsForCategory(category).filter(record =>
+ Boolean(record.layer?.feature?.properties?._lmeGeneratedSheetOutline));
+ generated.forEach(record => {
+ record.parent?.removeLayer?.(record.layer);
+ clearManagedObjectState(record.id);
});
- return true;
+ return generated.length;
+}
+
+function cadastralSheetIdentity(feature, index) {
+ const properties = feature?.properties || {};
+ const explicit = properties.foglio ?? properties.FOGLIO ?? properties.sheet ?? properties.SHEET;
+ const administrativeUnit = String(properties.ADMINISTRATIVEUNIT || "").trim().toUpperCase();
+ if (String(explicit ?? "").trim()) {
+ return `${administrativeUnit}|${String(explicit).trim().replace(/^0+/, "") || "0"}`;
+ }
+ const context = [
+ properties.NATIONALCADASTRALREFERENCE,
+ properties.INSPIREID_LOCALID,
+ properties._lmeParcelReference,
+ properties._lmeSource,
+ properties._lmeFolder,
+ properties.name,
+ properties.Name
+ ].filter(Boolean).join(" ").toUpperCase();
+ const namedSheet = context.match(/\bFOGLIO\s*[_ .-]*0*(\d+[A-Z]?)\b/i);
+ const codedSheet = context.match(/\b([A-Z]\d{3})[._-]([0-9]{4}[A-Z0-9]{2})(?=[._/-]|\b)/i);
+ if (namedSheet) return `${administrativeUnit}|${namedSheet[1]}`;
+ if (codedSheet) return `${codedSheet[1]}|${codedSheet[2]}`;
+ return `geometria-${index}`;
+}
+
+function sheetRepresentativePoint(feature) {
+ try {
+ return turf.pointOnFeature(feature).geometry.coordinates;
+ } catch {
+ const bounds = geoJsonBounds(feature?.geometry);
+ return bounds ? boundsCenter(bounds) : null;
+ }
+}
+
+function cadastralSheetFeatureKey(feature) {
+ const properties = feature?.properties || {};
+ return String(properties.NATIONALCADASTRALZONINGREFERENCE
+ || properties.INSPIREID_LOCALID
+ || properties.LABEL
+ || JSON.stringify(feature?.geometry || {})).trim().toUpperCase();
+}
+
+async function ensureImportedSheetOutlines() {
+ if (!lmeState.imported || !lmeState.importedSheets) {
+ return { added: 0, failed: false };
+ }
+
+ removeLegacyGeneratedSheetOutlines();
+ const existingAreaSheets = layerManagerCategories()
+ .flatMap(category => managedRecordsForCategory(category))
+ .map(record => ({ categoryKey: record.categoryKey, feature: record.layer?.toGeoJSON?.() }))
+ .filter(item => ["Polygon", "MultiPolygon"].includes(item.feature?.geometry?.type)
+ && !item.feature?.properties?._lmeGeneratedSheetOutline
+ && (item.categoryKey === "importedSheets"
+ || item.feature?.properties?._lmeOfficialSheet
+ || item.feature?.properties?.NATIONALCADASTRALZONINGREFERENCE
+ || isSheetOrOverlayFeature(item.feature)))
+ .map(item => item.feature);
+
+ const parcelFeatures = [
+ ...(lmeState.imported.toGeoJSON()?.features || []),
+ ...(lmeState.selectedParcelsLayer?.toGeoJSON?.()?.features || [])
+ ].filter(feature => ["Polygon", "MultiPolygon"].includes(feature?.geometry?.type));
+ const representatives = new Map();
+ parcelFeatures.forEach((feature, index) => {
+ const identity = cadastralSheetIdentity(feature, index);
+ if (!representatives.has(identity)) representatives.set(identity, feature);
+ });
+ if (!representatives.size || !lmeState.dotNetReference) {
+ return { added: 0, failed: false };
+ }
+
+ const existingKeys = new Set(existingAreaSheets.map(cadastralSheetFeatureKey));
+ const officialSheets = new Map();
+ let failures = 0;
+ for (const [identity, parcelFeature] of representatives) {
+ const point = sheetRepresentativePoint(parcelFeature);
+ if (!point) continue;
+ const existingSheet = findContainingPolygon(point, {
+ type: "FeatureCollection",
+ features: [...existingAreaSheets, ...officialSheets.values()]
+ });
+ if (existingSheet) continue;
+ try {
+ const xml = await lmeState.dotNetReference.invokeMethodAsync(
+ "GetOfficialCadastralSheetsNear",
+ point[1],
+ point[0]);
+ const candidates = gmlParcelsToGeoJson(xml).features || [];
+ const containing = findContainingPolygon(point, { type: "FeatureCollection", features: candidates });
+ const sheet = containing || candidates[0];
+ if (!sheet) continue;
+ const key = cadastralSheetFeatureKey(sheet);
+ if (existingKeys.has(key) || officialSheets.has(key)) continue;
+ const reference = String(sheet.properties?.NATIONALCADASTRALZONINGREFERENCE
+ || sheet.properties?.LABEL
+ || identity.split("|").pop()
+ || "").trim();
+ sheet.properties = {
+ ...(sheet.properties || {}),
+ name: reference ? `Foglio catastale ${reference}` : "Foglio catastale",
+ foglio: reference,
+ _lmeSheetIdentity: identity,
+ _lmeOfficialSheet: true,
+ _lmeSource: reference ? `Foglio catastale ${reference}` : "Fogli catastali AdE",
+ _lmeGroupName: reference ? `Foglio ${reference}` : "Fogli catastali AdE"
+ };
+ officialSheets.set(key, sheet);
+ } catch {
+ failures++;
+ }
+ }
+
+ if (officialSheets.size) {
+ addImportedSheetFeatures({ type: "FeatureCollection", features: [...officialSheets.values()] });
+ applyImportedSheetVisibility();
+ refreshLayerManager();
+ finishMapChange(`${officialSheets.size} fogli catastali ufficiali caricati.`);
+ }
+ return { added: officialSheets.size, failed: failures > 0 && !officialSheets.size };
}
function migrateMisclassifiedImportedSheets() {
@@ -4551,9 +4629,6 @@ function loadProjectSnapshot(raw, projectName) {
lmeState.importedSheetsVisible = projectDisplayOptions
? Boolean(projectDisplayOptions.importedSheetsVisible)
: data.schemaVersion >= 2 && Boolean(data.importedSheetsVisible);
- if (lmeState.importedSheetsVisible) {
- ensureImportedSheetOutline();
- }
applyImportedSheetVisibility();
L.geoJSON(data.selectedParcels || emptyFeatureCollection(), {
pane: "selectedParcelsPane",
@@ -4576,6 +4651,12 @@ function loadProjectSnapshot(raw, projectName) {
applyStyleFromProperties(layer);
}
}).addTo(lmeState.selectedParcelsLayer);
+ if (lmeState.importedSheetsVisible) {
+ void ensureImportedSheetOutlines().then(() => {
+ applyImportedSheetVisibility();
+ refreshLayerManager();
+ });
+ }
lmeState.gpsTrailPoints = Array.isArray(data.trail) ? data.trail : [];
lmeState.gpsTrailStartedAt = data.trailStartedAt || null;
@@ -5559,7 +5640,16 @@ function parcelMemberToFeature(member) {
}
const properties = {};
- ["INSPIREID_LOCALID", "INSPIREID_NAMESPACE", "LABEL", "NATIONALCADASTRALREFERENCE", "ADMINISTRATIVEUNIT"].forEach(name => {
+ [
+ "INSPIREID_LOCALID",
+ "INSPIREID_NAMESPACE",
+ "LABEL",
+ "NATIONALCADASTRALREFERENCE",
+ "NATIONALCADASTRALZONINGREFERENCE",
+ "LEVEL",
+ "LEVELNAME",
+ "ADMINISTRATIVEUNIT"
+ ].forEach(name => {
const value = member.getElementsByTagNameNS("*", name)[0]?.textContent;
if (value) {
properties[name] = value;