Fix cadastral layer controls and WMS preview

This commit is contained in:
livio committed 2026-09-11 20:31:04 +02:00
1 parent 9733137fdd
commit 94ca2d9f88
6 files changed
+437 -38

No files matched your search

+100 -18
View File
@@ -18,7 +18,16 @@
<strong>LME Catasto</strong>
<span>@Status</span>
</div>
<div class="toolbar-actions">
<button class="toolbar-button layer-button @(LayerPanelOpen ? "is-active" : null)"
title="Layer catastali"
aria-expanded="@LayerPanelOpen"
@onclick="ToggleLayerPanel">
<span class="layer-symbol" aria-hidden="true"></span>
<span>Layer</span>
</button>
<button class="toolbar-button" @onclick='() => SelectTab(ActiveTab == "search" ? "map" : "search")'>Cerca</button>
</div>
</header>
<div class="map-search @(ActiveTab == "search" ? "is-open" : null)">
@@ -26,6 +35,33 @@
<button @onclick="SearchLocation">Vai</button>
</div>
@if (LayerPanelOpen)
{
<section class="map-layer-panel" aria-label="Layer catastali">
<header>
<strong>Layer catastali</strong>
<button class="layer-panel-close" aria-label="Chiudi layer" @onclick="ToggleLayerPanel">×</button>
</header>
@foreach (var layer in CadastralLayers)
{
<label class="map-layer-option">
<input type="checkbox" checked="@layer.Enabled" @onchange="(args) => ToggleCadastreLayer(layer, args)" />
<span>
<strong>@layer.Name</strong>
@if (!string.IsNullOrWhiteSpace(layer.Detail))
{
<small>@layer.Detail</small>
}
</span>
</label>
}
<div class="layer-panel-actions">
<button class="secondary-command" @onclick="ClearSelectedParcels">Rimuovi particelle selezionate</button>
<button class="danger-command" @onclick="ClearMapElements">Pulisci elementi mappa</button>
</div>
</section>
}
<div class="gps-overlay @(ActiveTab == "search" ? "search-open" : null)">
<div class="gps-state">
<span id="gpsState">GPS non attivo</span>
@@ -178,16 +214,6 @@
Trasparenza catasto
<input type="range" min="0" max="1" step="0.05" value="0.75" @onchange="ChangeCadastreOpacity" />
</label>
<div class="layer-toggles">
@foreach (var layer in CadastralLayers)
{
<label>
<input type="checkbox" checked="@layer.Enabled" @onchange="(args) => ToggleCadastreLayer(layer.Key, args)" />
@layer.Name
</label>
}
</div>
<h2>GPS e percorso</h2>
<label class="toggle-line">
<input type="checkbox" checked="@TrailEnabled" @onchange="ToggleTrail" />
@@ -202,7 +228,7 @@
<button class="secondary-command" @onclick="OpenOfficialReservedArea">Apri Area riservata per download</button>
</div>
<button class="secondary-command" @onclick="ExportCadastralImage">Apri vista WMS corrente</button>
<button class="secondary-command" @onclick="ExportCadastralImage">Mostra vista WMS corrente</button>
<footer>Dati catastali: Agenzia delle Entrate, licenza CC BY 4.0. App indipendente.</footer>
</section>
}
@@ -211,6 +237,22 @@
<div id="mapInfo" class="map-info">Pronto</div>
<div id="wmsPreview" class="wms-preview" hidden>
<section class="wms-preview-dialog" role="dialog" aria-modal="true" aria-labelledby="wmsPreviewTitle">
<header>
<div>
<strong id="wmsPreviewTitle">Vista WMS catastale</strong>
<small>Immagine ufficiale AdE dell'area visibile, senza mappa di sfondo</small>
</div>
<button id="closeWmsPreview" class="close-button" aria-label="Chiudi vista WMS">×</button>
</header>
<div class="wms-preview-body">
<span id="wmsPreviewLoading">Caricamento...</span>
<img id="wmsPreviewImage" alt="Vista WMS catastale dell'area corrente" />
</div>
</section>
</div>
<nav class="bottom-tabs" aria-label="Sezioni principali">
<button class="@(ActiveTab == "map" || ActiveTab == "search" ? "active" : null)" @onclick='() => SelectTab("map")'>Mappa</button>
<button class="@(ActiveTab == "cadastre" ? "active" : null)" @onclick='() => SelectTab("cadastre")'>Catasto</button>
@@ -238,6 +280,7 @@
private bool GpsActive { get; set; }
private bool DrawToolsVisible { get; set; }
private bool TrailEnabled { get; set; }
private bool LayerPanelOpen { get; set; }
private CancellationTokenSource? CatalogCancellation { get; set; }
private CancellationTokenSource? GpsLookupCancellation { get; set; }
private long LastGpsLookupAt;
@@ -272,11 +315,11 @@
private readonly List<CadastralLayer> CadastralLayers =
[
new("CP.CadastralParcel", "Particelle", true),
new("codice_plla", "Numeri particelle", true),
new("CP.CadastralZoning", "Fogli", true),
new("CP.CadastralParcel", "Confini particelle", true),
new("codice_plla", "Numeri particelle", true, "Visibili circa da zoom 17"),
new("CP.CadastralZoning", "Fogli catastali", true),
new("fabbricati", "Fabbricati", true),
new("strade", "Strade", false),
new("strade", "Strade catastali", false),
new("acque", "Acque", false)
];
@@ -321,6 +364,22 @@
private async Task SelectTab(string tab)
{
ActiveTab = tab;
if (tab != "map")
{
LayerPanelOpen = false;
}
await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize");
}
private async Task ToggleLayerPanel()
{
LayerPanelOpen = !LayerPanelOpen;
if (LayerPanelOpen)
{
ActiveTab = "map";
}
await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize");
}
@@ -711,8 +770,25 @@
}
}
private Task ChangeCadastreOpacity(ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.setCadastreOpacity", args.Value?.ToString()).AsTask();
private Task ToggleCadastreLayer(string layer, ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.toggleCadastreLayer", layer, (bool?)args.Value == true).AsTask();
private Task ExportCadastralImage() => JS.InvokeVoidAsync("lmeMap.exportCadastreView").AsTask();
private async Task ToggleCadastreLayer(CadastralLayer layer, ChangeEventArgs args)
{
layer.Enabled = args.Value is true;
await JS.InvokeVoidAsync("lmeMap.toggleCadastreLayer", layer.Key, layer.Enabled);
}
private async Task ExportCadastralImage()
{
ActiveTab = "map";
LayerPanelOpen = false;
await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize");
await JS.InvokeVoidAsync("lmeMap.exportCadastreView");
}
private Task ClearSelectedParcels() => JS.InvokeVoidAsync("lmeMap.clearSelectedParcels").AsTask();
private Task ClearMapElements() => JS.InvokeVoidAsync("lmeMap.clearMapElements").AsTask();
private Task OpenOfficialGeoportal() => Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync(
new Uri("https://geoportale.cartografia.agenziaentrate.gov.it/"));
@@ -720,7 +796,13 @@
private Task OpenOfficialReservedArea() => Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync(
new Uri("https://www.agenziaentrate.gov.it/portale/area-riservata"));
private sealed record CadastralLayer(string Key, string Name, bool Enabled);
private sealed class CadastralLayer(string key, string name, bool enabled, string detail = "")
{
public string Key { get; } = key;
public string Name { get; } = name;
public bool Enabled { get; set; } = enabled;
public string Detail { get; } = detail;
}
public ValueTask DisposeAsync()
{
+2 -2
View File
@@ -28,8 +28,8 @@
<ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>0.2.3</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion>
<ApplicationDisplayVersion>0.2.4</ApplicationDisplayVersion>
<ApplicationVersion>8</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>
+4
View File
@@ -24,6 +24,10 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
- Catalogo dei Comuni incluso nell'APK e cataloghi regionali OnData scaricati in cache solo quando servono.
- Ricerca catastale eseguita dal codice .NET nativo, senza DuckDB WebAssembly nella WebView Android.
- La registrazione della traccia GPS e' disattivata all'avvio; la toolbar di disegno puo' essere mostrata o nascosta dal tab Progetti.
- Pannello `Layer` accessibile direttamente dalla mappa, con checkbox sincronizzati per confini, numeri particella, fogli, fabbricati, strade e acque.
- Le particelle cercate possono essere aggiunte una dopo l'altra, mantenendo contemporaneamente sulla mappa tutti i confini selezionati.
- Comandi separati per rimuovere le particelle selezionate o pulire importazioni, disegni, POI e traccia GPS.
- Anteprima WMS ufficiale mostrata dentro l'app senza aprire una nuova finestra Android.
- Collegamenti separati al Geoportale pubblico e all'Area riservata AdE per CAPTCHA, SPID/CIE e download ufficiali.
## Limite ufficiale AdE da considerare
+1 -1
View File
@@ -13,7 +13,7 @@ public sealed class AzureMapsService
public AzureMapsService()
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.3");
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.4");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
}
+162
View File
@@ -124,6 +124,28 @@ input[type="checkbox"] {
color: var(--ink);
}
.toolbar-actions,
.layer-button {
display: flex;
align-items: center;
gap: 7px;
}
.toolbar-actions {
flex: 0 0 auto;
}
.layer-button.is-active {
background: #ccebe3;
color: var(--accent-strong);
}
.layer-symbol {
width: 20px;
height: 20px;
background: url("../lib/leaflet/images/layers.png") center / contain no-repeat;
}
.map-search {
position: absolute;
top: 72px;
@@ -150,6 +172,73 @@ input[type="checkbox"] {
transform: translateY(0);
}
.map-layer-panel {
position: absolute;
top: 72px;
right: 12px;
z-index: 940;
display: grid;
width: min(320px, calc(100% - 24px));
max-height: calc(100% - 152px);
overflow-y: auto;
border: 1px solid var(--line);
border-radius: 8px;
padding: 9px;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.22);
}
.map-layer-panel > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 0 7px 4px;
border-bottom: 1px solid var(--line);
}
.layer-panel-close {
width: 36px;
min-width: 36px;
min-height: 34px;
padding: 0;
background: #e9eef0;
color: var(--ink);
font-size: 23px;
}
.map-layer-option {
display: flex;
align-items: center;
gap: 9px;
min-height: 45px;
padding: 6px 4px;
border-bottom: 1px solid #e5eaed;
}
.map-layer-option input {
flex: 0 0 auto;
width: 19px;
height: 19px;
}
.map-layer-option span {
display: grid;
gap: 2px;
}
.map-layer-option small {
color: var(--muted);
font-size: 10px;
font-weight: 600;
}
.layer-panel-actions {
display: grid;
gap: 7px;
padding-top: 9px;
}
.gps-overlay {
position: absolute;
top: 72px;
@@ -384,6 +473,68 @@ label {
pointer-events: none;
}
.wms-preview[hidden] {
display: none;
}
.wms-preview {
position: absolute;
inset: 0;
z-index: 1400;
display: grid;
place-items: center;
padding: 16px;
background: rgba(15, 22, 27, 0.72);
}
.wms-preview-dialog {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
width: min(980px, 100%);
height: min(820px, 100%);
overflow: hidden;
border-radius: 8px;
background: #fff;
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.34);
}
.wms-preview-dialog > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 11px 12px;
border-bottom: 1px solid var(--line);
}
.wms-preview-dialog > header div {
display: grid;
gap: 2px;
}
.wms-preview-dialog > header small {
color: var(--muted);
font-size: 10px;
}
.wms-preview-body {
display: grid;
min-height: 0;
place-items: center;
overflow: auto;
padding: 12px;
background: #eef2f3;
color: var(--muted);
}
.wms-preview-body img {
display: block;
max-width: 100%;
max-height: 100%;
object-fit: contain;
background: #fff;
}
.bottom-tabs {
position: absolute;
right: 0;
@@ -462,6 +613,13 @@ footer {
right: 8px;
}
.map-layer-panel {
top: 68px;
right: 8px;
width: min(320px, calc(100% - 16px));
max-height: calc(100% - 144px);
}
.tool-sheet {
top: 10%;
bottom: var(--nav-height);
@@ -504,6 +662,10 @@ footer {
.bottom-tabs button {
font-size: 11px;
}
.wms-preview {
padding: 8px;
}
}
@media (max-width: 380px) {
+168 -17
View File
@@ -13,7 +13,8 @@ const lmeState = {
gpsMarker: null,
gpsAccuracy: null,
gpsTrail: null,
currentParcelLayer: null,
selectedParcelsLayer: null,
gpsParcelLayer: null,
gpsTrailEnabled: false,
gpsTrailPoints: [],
gpsWatchId: null,
@@ -30,6 +31,9 @@ window.lmeMap = {
showSearchResult,
displayOfficialParcel,
exportCadastreView,
closeCadastrePreview,
clearSelectedParcels,
clearMapElements,
fitImported,
clearImported,
addProjectMarker,
@@ -104,7 +108,8 @@ function init() {
lmeState.currentBase = lmeState.baseLayers.satellite.addTo(lmeState.map);
lmeState.imported = L.featureGroup().addTo(lmeState.map);
lmeState.drawings = L.featureGroup().addTo(lmeState.map);
lmeState.currentParcelLayer = L.featureGroup().addTo(lmeState.map);
lmeState.selectedParcelsLayer = L.featureGroup().addTo(lmeState.map);
lmeState.gpsParcelLayer = L.featureGroup().addTo(lmeState.map);
lmeState.gpsTrail = L.polyline([], { color: "#f97316", weight: 4, opacity: 0.85 }).addTo(lmeState.map);
refreshCadastreLayer();
@@ -124,6 +129,18 @@ function init() {
});
lmeState.map.on("moveend", () => void updateAzureAttribution());
document.getElementById("closeWmsPreview")?.addEventListener("click", closeCadastrePreview);
document.getElementById("wmsPreview")?.addEventListener("click", event => {
if (event.target?.id === "wmsPreview") {
closeCadastrePreview();
}
});
document.addEventListener("keydown", event => {
if (event.key === "Escape") {
closeCadastrePreview();
}
});
setInfo("Mappa pronta: satellite + catasto AdE");
}
@@ -169,18 +186,39 @@ function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shou
return;
}
lmeState.currentParcelLayer.clearLayers();
const ref = feature.properties?.NATIONALCADASTRALREFERENCE || feature.properties?.INSPIREID_LOCALID || label;
const targetGroup = normalizedParcel ? lmeState.selectedParcelsLayer : lmeState.gpsParcelLayer;
if (!normalizedParcel) {
targetGroup.clearLayers();
}
const existingLayer = normalizedParcel
? targetGroup.getLayers().find(item => item.lmeParcelReference === ref)
: null;
if (existingLayer) {
if (shouldFit) {
fitLayer(existingLayer);
}
setText("parcelState", `Particella selezionata: ${ref}`);
setInfo(`La particella ${ref} e' gia evidenziata sulla mappa.`);
return;
}
const layer = L.geoJSON(feature, {
style: { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 }
}).addTo(lmeState.currentParcelLayer);
style: normalizedParcel
? { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 }
: { color: "#1d4ed8", weight: 4, opacity: 1, fillColor: "#38bdf8", fillOpacity: 0.2 }
}).addTo(targetGroup);
layer.lmeParcelReference = ref;
layer.bindPopup(`<strong>${escapeHtml(ref)}</strong>`);
if (shouldFit) {
fitLayer(layer);
}
const ref = feature.properties?.NATIONALCADASTRALREFERENCE || feature.properties?.INSPIREID_LOCALID || label;
if (normalizedParcel) {
setText("parcelState", `Particella selezionata: ${ref}`);
setInfo(`Trovata ${ref}. Ora puoi attivare il GPS e camminare dentro il confine evidenziato.`);
const count = lmeState.selectedParcelsLayer.getLayers().length;
setInfo(`Aggiunta ${ref}. Particelle evidenziate: ${count}.`);
} else {
setText("parcelState", `Dentro particella AdE: ${ref}`);
}
@@ -201,12 +239,66 @@ function resize() {
}
function exportCadastreView() {
const center = lmeState.map.getCenter();
const zoom = lmeState.map.getZoom();
const layers = lmeState.cadastreLayers.join(",");
const url = `${cadastreUrl()}service=WMS&version=1.3.0&request=GetMap&layers=${encodeURIComponent(layers)}&styles=&format=image/png&transparent=true&width=1200&height=1200&crs=EPSG:4258&bbox=${center.lat - 0.01},${center.lng - 0.01},${center.lat + 0.01},${center.lng + 0.01}`;
window.open(url, "_blank");
setInfo(`Vista catastale aperta per download. Zoom corrente: ${zoom}.`);
const layers = activeCadastreLayers();
if (!layers.length) {
setInfo("Attiva almeno un layer catastale prima di aprire la vista WMS.");
return;
}
const bounds = lmeState.map.getBounds();
const size = lmeState.map.getSize();
const scale = Math.min(1, 1200 / size.x, 1200 / size.y);
const width = Math.max(256, Math.round(size.x * scale));
const height = Math.max(256, Math.round(size.y * scale));
const bbox = [bounds.getSouth(), bounds.getWest(), bounds.getNorth(), bounds.getEast()].join(",");
const params = new URLSearchParams({
service: "WMS",
version: "1.3.0",
request: "GetMap",
layers: layers.join(","),
styles: "",
format: "image/png",
transparent: "true",
width: String(width),
height: String(height),
crs: "EPSG:4258",
bbox
});
const preview = document.getElementById("wmsPreview");
const image = document.getElementById("wmsPreviewImage");
const loading = document.getElementById("wmsPreviewLoading");
if (!preview || !image || !loading) {
setInfo("Vista WMS interna non disponibile.");
return;
}
loading.textContent = "Caricamento...";
loading.hidden = false;
image.hidden = true;
image.onload = () => {
loading.hidden = true;
image.hidden = false;
setInfo(`Vista WMS caricata con ${layers.length} layer.`);
};
image.onerror = () => {
loading.textContent = "Il servizio WMS non ha restituito l'immagine. Riprova tra poco.";
setInfo("Vista WMS non disponibile.");
};
preview.hidden = false;
image.src = `${cadastreUrl()}${params}`;
}
function closeCadastrePreview() {
const preview = document.getElementById("wmsPreview");
const image = document.getElementById("wmsPreviewImage");
if (preview) {
preview.hidden = true;
}
if (image) {
image.onload = null;
image.onerror = null;
image.removeAttribute("src");
}
}
function bindFileImport() {
@@ -590,6 +682,19 @@ function toggleCadastreLayer(layer, enabled) {
lmeState.cadastreLayers = lmeState.cadastreLayers.filter(item => item !== layer);
}
refreshCadastreLayer();
const layerNames = {
"CP.CadastralParcel": "Confini particelle",
codice_plla: "Numeri particelle",
"CP.CadastralZoning": "Fogli catastali",
fabbricati: "Fabbricati",
strade: "Strade catastali",
acque: "Acque"
};
if (enabled && layer === "codice_plla" && lmeState.map.getZoom() < 17) {
setInfo("Numeri particelle attivati. Ingrandisci la mappa almeno fino a zoom 17 per renderli visibili.");
return;
}
setInfo(`${layerNames[layer] || layer}: ${enabled ? "attivo" : "nascosto"}.`);
}
function refreshCadastreLayer() {
@@ -597,12 +702,14 @@ function refreshCadastreLayer() {
lmeState.map.removeLayer(lmeState.cadastre);
}
if (!lmeState.cadastreLayers.length) {
const layers = activeCadastreLayers();
lmeState.cadastre = null;
if (!layers.length) {
return;
}
lmeState.cadastre = L.tileLayer.wms(cadastreUrl(), {
layers: lmeState.cadastreLayers.join(","),
layers: layers.join(","),
format: "image/png",
transparent: true,
version: "1.3.0",
@@ -613,6 +720,49 @@ function refreshCadastreLayer() {
}).addTo(lmeState.map);
}
function activeCadastreLayers() {
const drawOrder = [
"CP.CadastralZoning",
"strade",
"acque",
"fabbricati",
"CP.CadastralParcel",
"codice_plla"
];
return drawOrder.filter(layer => lmeState.cadastreLayers.includes(layer));
}
function clearSelectedParcels() {
const count = lmeState.selectedParcelsLayer.getLayers().length;
lmeState.selectedParcelsLayer.clearLayers();
updateGpsInsideState();
setInfo(count ? `${count} particelle selezionate rimosse.` : "Non ci sono particelle selezionate da rimuovere.");
}
function clearMapElements() {
const count = lmeState.selectedParcelsLayer.getLayers().length
+ lmeState.gpsParcelLayer.getLayers().length
+ lmeState.imported.getLayers().length
+ lmeState.drawings.getLayers().length
+ lmeState.gpsTrailPoints.length;
if (!count) {
setInfo("La mappa non contiene elementi da pulire.");
return;
}
if (!window.confirm("Rimuovere particelle evidenziate, file importati, disegni, POI e traccia GPS dalla mappa?")) {
return;
}
lmeState.selectedParcelsLayer.clearLayers();
lmeState.gpsParcelLayer.clearLayers();
lmeState.imported.clearLayers();
lmeState.drawings.clearLayers();
lmeState.gpsTrailPoints = [];
lmeState.gpsTrail.setLatLngs([]);
updateGpsInsideState();
setInfo("Elementi della mappa rimossi. Sfondo e layer catastali restano attivi.");
}
function startGps() {
if (!navigator.geolocation) {
setInfo("Geolocalizzazione non disponibile sul dispositivo.");
@@ -711,7 +861,8 @@ function updateGpsInsideState() {
}
const point = [lmeState.lastPosition[1], lmeState.lastPosition[0]];
const officialMatch = findContainingPolygon(point, lmeState.currentParcelLayer.toGeoJSON());
const officialMatch = findContainingPolygon(point, lmeState.selectedParcelsLayer.toGeoJSON())
|| findContainingPolygon(point, lmeState.gpsParcelLayer.toGeoJSON());
if (officialMatch) {
const reference = officialMatch.properties?.NATIONALCADASTRALREFERENCE
|| officialMatch.properties?.INSPIREID_LOCALID
@@ -731,7 +882,7 @@ function updateGpsInsideState() {
setText(
"parcelState",
lmeState.currentParcelLayer.getLayers().length
lmeState.selectedParcelsLayer.getLayers().length || lmeState.gpsParcelLayer.getLayers().length
? "Fuori dalla particella catastale evidenziata"
: "Fuori dai poligoni caricati");
}