Compare commits

...
1 Commits
Author SHA1 Message Date
livio 7933b2e86a Reorganize map tools and improve KML sheet filtering 2026-09-21 11:01:12 +02:00
6 changed files with 331 additions and 173 deletions

No files matched your search

+157 -119
View File
@@ -72,6 +72,16 @@
<button class="gps-center" @onclick="CenterOnGps" aria-label="Centra posizione">Centra</button> <button class="gps-center" @onclick="CenterOnGps" aria-label="Centra posizione">Centra</button>
</div> </div>
@if ((ActiveTab is "map" or "search") && !LayerPanelOpen)
{
<div class="map-measure-tools @(ActiveTab == "search" ? "search-open" : null)" aria-label="Strumenti rapidi di misura">
<span>Misura</span>
<button title="Disegna e misura un'area" @onclick='() => StartMeasurementDrawing("area")'>Area</button>
<button title="Disegna e misura una distanza" @onclick='() => StartMeasurementDrawing("distance")'>Distanza</button>
<button title="Disegna e misura un cerchio" @onclick='() => StartMeasurementDrawing("circle")'>Cerchio</button>
</div>
}
@if (ActiveTab is not "map" and not "search") @if (ActiveTab is not "map" and not "search")
{ {
<aside class="tool-sheet"> <aside class="tool-sheet">
@@ -149,130 +159,155 @@
</div> </div>
</section> </section>
} }
else if (ActiveTab == "data")
{
<section class="panel-content">
<section class="panel-section">
<h2>File GIS</h2>
<label class="file-picker">
<span>Carica KML, KMZ o GeoJSON</span>
<input id="gisFileInput" type="file" accept=".kml,.kmz,.geojson,.json,application/vnd.google-earth.kml+xml,application/vnd.google-earth.kmz,application/geo+json" />
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" checked="@IncludeKmlSheetLayer" @onchange="ToggleKmlSheetLayer" />
<span>KML/KMZ: includi anche il layer del foglio</span>
</label>
<small class="field-help">
@(IncludeKmlSheetLayer
? "Attivato: importa foglio, particelle e altri elementi KML/KMZ."
: "Disattivato: importa solo i poligoni delle particelle riconosciute.")
</small>
<label class="toggle-line project-save-option">
<input type="checkbox" checked="@ShowImportedMeasurements" @onchange="ToggleImportedMeasurements" />
<span>Mostra misure dei file caricati</span>
</label>
<div class="button-row">
<button @onclick="FitImportedLayers">Inquadra dati</button>
<button class="secondary-command" @onclick="ClearImportedLayers">Rimuovi dati</button>
</div>
</section>
<section class="panel-section">
<h2>Disegni e misure</h2>
<label class="toggle-line project-save-option">
<input type="checkbox" checked="@ShowDrawingMeasurements" @onchange="ToggleDrawingMeasurements" />
<span>Mostra superficie, perimetro e lati</span>
</label>
<button class="secondary-command" aria-pressed="@DrawToolsVisible" @onclick="ToggleDrawTools">
@(DrawToolsVisible ? "Nascondi barra completa" : "Mostra barra completa")
</button>
<button class="danger-command" @onclick="ClearDrawings">Elimina disegni e POI</button>
</section>
<section class="panel-section">
<h2>Punti di interesse</h2>
<label>
Nome POI
<input @bind="PoiName" @bind:event="oninput" placeholder="Nome del punto" />
</label>
<button class="primary-command" @onclick="AddProjectMarker">Aggiungi POI al centro mappa</button>
</section>
</section>
}
else if (ActiveTab == "projects") else if (ActiveTab == "projects")
{ {
<section class="panel-content"> <section class="panel-content">
<h2>File GIS</h2> <section class="panel-section">
<label class="file-picker"> <h2>Salva progetto</h2>
<span>Carica KML, KMZ o GeoJSON</span> <label>
<input id="gisFileInput" type="file" accept=".kml,.kmz,.geojson,.json,application/vnd.google-earth.kml+xml,application/vnd.google-earth.kmz,application/geo+json" /> Nome progetto
</label> <input @bind="NewProjectName" @bind:event="oninput" placeholder="Es. Rilievo San Nicola" maxlength="80" />
<label class="toggle-line project-save-option"> </label>
<input type="checkbox" checked="@ImportParcelsOnly" @onchange="ToggleImportParcelsOnly" /> <label class="toggle-line project-save-option">
<span>KML/KMZ: importa solo le particelle</span> <input type="checkbox" @bind="IncludeCadastralParcels" />
</label> <span>Includi le particelle catastali rosse</span>
<label class="toggle-line project-save-option"> </label>
<input type="checkbox" checked="@ShowImportedMeasurements" @onchange="ToggleImportedMeasurements" /> <button class="primary-command" @onclick="SaveProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(NewProjectName))">Salva progetto</button>
<span>Mostra misure dei file caricati</span> </section>
</label>
<div class="button-row">
<button @onclick="FitImportedLayers">Inquadra</button>
<button class="secondary-command" @onclick="ClearImportedLayers">Pulisci</button>
</div>
<h2>Archivio progetti</h2> <section class="panel-section">
<label> <h2>Apri progetto</h2>
Nome progetto <label>
<input @bind="NewProjectName" @bind:event="oninput" placeholder="Es. Rilievo San Nicola" maxlength="80" /> Progetto salvato
</label> <select value="@SelectedProjectId" @onchange="SelectProject" disabled="@(Projects.Count == 0 || ProjectBusy)">
<label class="toggle-line project-save-option"> <option value="">Seleziona un progetto</option>
<input type="checkbox" @bind="IncludeCadastralParcels" /> @foreach (var project in Projects)
<span>Includi le particelle catastali rosse</span> {
</label> <option value="@project.Id">@project.Name - @project.UpdatedAt.ToLocalTime().ToString("dd/MM/yyyy HH:mm")</option>
<button class="primary-command" @onclick="SaveProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(NewProjectName))">Salva progetto</button> }
</select>
<label> </label>
Progetto salvato <div class="button-row">
<select value="@SelectedProjectId" @onchange="SelectProject" disabled="@(Projects.Count == 0 || ProjectBusy)"> <button @onclick="LoadProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Apri</button>
<option value="">Seleziona un progetto</option> <button class="danger-command" @onclick="DeleteProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Elimina</button>
@foreach (var project in Projects) </div>
{ <div class="project-archive-state">@ProjectArchiveMessage</div>
<option value="@project.Id">@project.Name - @project.UpdatedAt.ToLocalTime().ToString("dd/MM/yyyy HH:mm")</option> </section>
}
</select>
</label>
<div class="button-row">
<button @onclick="LoadProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Carica</button>
<button class="danger-command" @onclick="DeleteProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Elimina</button>
</div>
<div class="project-archive-state">@ProjectArchiveMessage</div>
<h2>Misura e disegna</h2>
<div class="measurement-mode-grid" aria-label="Strumenti di misura">
<button @onclick='() => StartMeasurementDrawing("area")'>Area</button>
<button @onclick='() => StartMeasurementDrawing("distance")'>Distanza</button>
<button @onclick='() => StartMeasurementDrawing("circle")'>Cerchio</button>
</div>
<label class="toggle-line project-save-option">
<input type="checkbox" checked="@ShowDrawingMeasurements" @onchange="ToggleDrawingMeasurements" />
<span>Mostra superficie, perimetro e lati</span>
</label>
<button class="secondary-command" aria-pressed="@DrawToolsVisible" @onclick="ToggleDrawTools">
@(DrawToolsVisible ? "Nascondi strumenti completi" : "Mostra strumenti completi")
</button>
<h2>POI</h2>
<label>
Nome POI
<input @bind="PoiName" @bind:event="oninput" placeholder="Nome del punto" />
</label>
<button class="primary-command" @onclick="AddProjectMarker">Aggiungi POI al centro mappa</button>
<button class="danger-command" @onclick="ClearDrawings">Elimina disegni e POI</button>
</section> </section>
} }
else if (ActiveTab == "settings") else if (ActiveTab == "settings")
{ {
<section class="panel-content"> <section class="panel-content">
<h2>Visualizzazione</h2> <section class="panel-section">
<label> <h2>Mappa e sfondo</h2>
Sfondo <label>
<select value="@SelectedBaseLayer" @onchange="ChangeBaseLayer"> Sfondo
<option value="satellite">Satellite - Esri World Imagery</option> <select value="@SelectedBaseLayer" @onchange="ChangeBaseLayer">
<option value="osm">Strade - OpenStreetMap</option> <option value="satellite">Satellite - Esri World Imagery</option>
<option value="streets-esri">Strade dettagliate - Esri World Street Map</option> <option value="osm">Strade - OpenStreetMap</option>
<option value="topographic">Topografica - Esri World Topographic</option> <option value="streets-esri">Strade dettagliate - Esri World Street Map</option>
<option value="terrain">Terreno - OpenTopoMap</option> <option value="topographic">Topografica - Esri World Topographic</option>
<option value="carto">Strade chiare - CARTO Voyager</option> <option value="terrain">Terreno - OpenTopoMap</option>
<option value="azure-road">Strade - Azure Maps</option> <option value="carto">Strade chiare - CARTO Voyager</option>
<option value="azure-satellite">Satellite - Azure Maps</option> <option value="azure-road">Strade - Azure Maps</option>
<option value="google-satellite">Satellite - Google Maps</option> <option value="azure-satellite">Satellite - Azure Maps</option>
<option value="google-roadmap">Strade - Google Maps</option> <option value="google-satellite">Satellite - Google Maps</option>
</select> <option value="google-roadmap">Strade - Google Maps</option>
</label> </select>
<label> </label>
Chiave Google Maps Platform <label>
<input type="password" @bind="GoogleMapsApiKey" placeholder="API key" autocomplete="off" /> Trasparenza catasto
</label> <input type="range" min="0" max="1" step="0.05" value="0.75" @onchange="ChangeCadastreOpacity" />
<button class="secondary-command" @onclick="SaveGoogleMapsApiKey">Salva chiave Google</button> </label>
<label> </section>
Chiave sottoscrizione Azure Maps Gen2
<input type="password" @bind="AzureMapsSubscriptionKey" placeholder="Subscription key" autocomplete="off" />
</label>
<button class="secondary-command" @onclick="SaveAzureMapsKey">Salva chiave Azure Maps</button>
<div class="service-note">
<strong>Quota gratuita Azure Maps Gen2</strong>
<p>Include ogni mese 5.000 transazioni per le mappe stradali e 1.000 per le immagini satellitari. Ogni transazione comprende fino a 15 tessere.</p>
</div>
<label>
Trasparenza catasto
<input type="range" min="0" max="1" step="0.05" value="0.75" @onchange="ChangeCadastreOpacity" />
</label>
<h2>GPS e percorso</h2>
<label class="toggle-line">
<input type="checkbox" checked="@TrailEnabled" @onchange="ToggleTrail" />
Registra la traccia mentre cammini
</label>
<h2>Servizi Agenzia delle Entrate</h2> <section class="panel-section">
<div class="service-note protected-service"> <h2>Servizi mappa con chiave</h2>
<strong>Download documenti e fogli ufficiali</strong> <label>
<p>Il CAPTCHA o l'accesso SPID/CIE vengono gestiti direttamente dal sito dell'Agenzia. Per sicurezza l'app non tenta di aggirarli.</p> Chiave Google Maps Platform
<button class="primary-command" @onclick="OpenOfficialGeoportal">Apri Geoportale catastale</button> <input type="password" @bind="GoogleMapsApiKey" placeholder="API key" autocomplete="off" />
<button class="secondary-command" @onclick="OpenOfficialReservedArea">Apri Area riservata per download</button> </label>
</div> <button class="secondary-command" @onclick="SaveGoogleMapsApiKey">Salva chiave Google</button>
<label>
Chiave sottoscrizione Azure Maps Gen2
<input type="password" @bind="AzureMapsSubscriptionKey" placeholder="Subscription key" autocomplete="off" />
</label>
<button class="secondary-command" @onclick="SaveAzureMapsKey">Salva chiave Azure Maps</button>
<div class="service-note">
<strong>Quota gratuita Azure Maps Gen2</strong>
<p>Include ogni mese 5.000 transazioni per le mappe stradali e 1.000 per le immagini satellitari. Ogni transazione comprende fino a 15 tessere.</p>
</div>
</section>
<button class="secondary-command" @onclick="ExportCadastralImage">Mostra vista WMS corrente</button> <section class="panel-section">
<footer>Dati catastali: Agenzia delle Entrate, licenza CC BY 4.0. App indipendente.</footer> <h2>GPS e percorso</h2>
<label class="toggle-line project-save-option">
<input type="checkbox" checked="@TrailEnabled" @onchange="ToggleTrail" />
<span>Registra la traccia mentre cammini</span>
</label>
</section>
<section class="panel-section">
<h2>Servizi Agenzia delle Entrate</h2>
<div class="service-note protected-service">
<strong>Download documenti e fogli ufficiali</strong>
<p>Il CAPTCHA o l'accesso SPID/CIE vengono gestiti direttamente dal sito dell'Agenzia. Per sicurezza l'app non tenta di aggirarli.</p>
<button class="primary-command" @onclick="OpenOfficialGeoportal">Apri Geoportale catastale</button>
<button class="secondary-command" @onclick="OpenOfficialReservedArea">Apri Area riservata per download</button>
</div>
<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>
</section> </section>
} }
</aside> </aside>
@@ -302,6 +337,7 @@
<nav class="bottom-tabs" aria-label="Sezioni principali"> <nav class="bottom-tabs" aria-label="Sezioni principali">
<button class="@(ActiveTab == "map" || ActiveTab == "search" ? "active" : null)" @onclick='() => SelectTab("map")'>Mappa</button> <button class="@(ActiveTab == "map" || ActiveTab == "search" ? "active" : null)" @onclick='() => SelectTab("map")'>Mappa</button>
<button class="@(ActiveTab == "cadastre" ? "active" : null)" @onclick='() => SelectTab("cadastre")'>Catasto</button> <button class="@(ActiveTab == "cadastre" ? "active" : null)" @onclick='() => SelectTab("cadastre")'>Catasto</button>
<button class="@(ActiveTab == "data" ? "active" : null)" @onclick='() => SelectTab("data")'>Dati</button>
<button class="@(ActiveTab == "projects" ? "active" : null)" @onclick='() => SelectTab("projects")'>Progetti</button> <button class="@(ActiveTab == "projects" ? "active" : null)" @onclick='() => SelectTab("projects")'>Progetti</button>
<button class="@(ActiveTab == "settings" ? "active" : null)" @onclick='() => SelectTab("settings")'>Impostazioni</button> <button class="@(ActiveTab == "settings" ? "active" : null)" @onclick='() => SelectTab("settings")'>Impostazioni</button>
</nav> </nav>
@@ -331,7 +367,7 @@
private bool TrailEnabled { get; set; } private bool TrailEnabled { get; set; }
private bool LayerPanelOpen { get; set; } private bool LayerPanelOpen { get; set; }
private bool IncludeCadastralParcels { get; set; } = true; private bool IncludeCadastralParcels { get; set; } = true;
private bool ImportParcelsOnly { get; set; } = true; private bool IncludeKmlSheetLayer { get; set; }
private bool ShowImportedMeasurements { get; set; } private bool ShowImportedMeasurements { get; set; }
private bool ShowDrawingMeasurements { get; set; } = true; private bool ShowDrawingMeasurements { get; set; } = true;
private bool ProjectBusy { get; set; } private bool ProjectBusy { get; set; }
@@ -355,7 +391,8 @@
private string PanelTitle => ActiveTab switch private string PanelTitle => ActiveTab switch
{ {
"cadastre" => "Ricerca catastale", "cadastre" => "Ricerca catastale",
"projects" => "Progetti e dati", "data" => "Dati e disegni",
"projects" => "Archivio progetti",
"settings" => "Impostazioni", "settings" => "Impostazioni",
_ => string.Empty _ => string.Empty
}; };
@@ -363,7 +400,8 @@
private string PanelEyebrow => ActiveTab switch private string PanelEyebrow => ActiveTab switch
{ {
"cadastre" => "Regione, Comune, Foglio, Particella", "cadastre" => "Regione, Comune, Foglio, Particella",
"projects" => "KML, KMZ, GeoJSON e POI", "data" => "File GIS, misure e POI",
"projects" => "Salva e apri i tuoi lavori",
"settings" => "Mappa, GPS e servizi ufficiali", "settings" => "Mappa, GPS e servizi ufficiali",
_ => string.Empty _ => string.Empty
}; };
@@ -751,10 +789,10 @@
private Task ClearImportedLayers() => JS.InvokeVoidAsync("lmeMap.clearImported").AsTask(); private Task ClearImportedLayers() => JS.InvokeVoidAsync("lmeMap.clearImported").AsTask();
private Task AddProjectMarker() => JS.InvokeVoidAsync("lmeMap.addProjectMarker", PoiName).AsTask(); private Task AddProjectMarker() => JS.InvokeVoidAsync("lmeMap.addProjectMarker", PoiName).AsTask();
private async Task ToggleImportParcelsOnly(ChangeEventArgs args) private async Task ToggleKmlSheetLayer(ChangeEventArgs args)
{ {
ImportParcelsOnly = (bool?)args.Value == true; IncludeKmlSheetLayer = (bool?)args.Value == true;
await JS.InvokeVoidAsync("lmeMap.setImportParcelOnly", ImportParcelsOnly); await JS.InvokeVoidAsync("lmeMap.setIncludeKmlSheetLayer", IncludeKmlSheetLayer);
} }
private async Task ToggleImportedMeasurements(ChangeEventArgs args) private async Task ToggleImportedMeasurements(ChangeEventArgs args)
+2 -2
View File
@@ -28,8 +28,8 @@
<ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId> <ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>0.2.8</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.2.9</ApplicationDisplayVersion>
<ApplicationVersion>12</ApplicationVersion> <ApplicationVersion>13</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged --> <!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType> <WindowsPackageType>None</WindowsPackageType>
+3 -3
View File
@@ -10,10 +10,10 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
- Interrogazione WMS con click/tap sulla mappa. - Interrogazione WMS con click/tap sulla mappa.
- Import di file KML, KMZ, GeoJSON e JSON. - Import di file KML, KMZ, GeoJSON e JSON.
- Import GeoJSON compatibile con BOM, geometrie singole, array, FeatureCollection e coordinate Web Mercator EPSG:3857. - Import GeoJSON compatibile con BOM, geometrie singole, array, FeatureCollection e coordinate Web Mercator EPSG:3857.
- Filtro KML/KMZ `solo particelle` attivo per impostazione predefinita, con esclusione di GroundOverlay e perimetri del foglio. - Filtro KML/KMZ `solo particelle` attivo per impostazione predefinita, con separazione dei MultiPolygon ed esclusione di GroundOverlay e perimetri del foglio; il layer foglio resta importabile tramite opzione.
- Inquadramento e pulizia dei layer importati. - Inquadramento e pulizia dei layer importati.
- Disegno di marker, linee, poligoni e rettangoli. - Disegno di marker, linee, poligoni e rettangoli.
- Modalita rapide Area, Distanza e Cerchio con superficie, perimetro, raggio, circonferenza e lunghezza dei singoli lati. - Modalita rapide Area, Distanza e Cerchio disponibili direttamente sulla mappa, con superficie, perimetro, raggio, circonferenza e lunghezza dei singoli lati.
- Misure permanenti attivabili anche sui layer importati da KML, KMZ e GeoJSON. - Misure permanenti attivabili anche sui layer importati da KML, KMZ e GeoJSON.
- Toolbar di disegno completa di icone, modifica, eliminazione singola e pulizia di tutti i disegni/POI. - Toolbar di disegno completa di icone, modifica, eliminazione singola e pulizia di tutti i disegni/POI.
- Archivio multiprogetto nella cartella privata dell'app, con nome, selezione da elenco, sovrascrittura ed eliminazione. - Archivio multiprogetto nella cartella privata dell'app, con nome, selezione da elenco, sovrascrittura ed eliminazione.
@@ -22,7 +22,7 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
- Marker posizione, cerchio accuratezza in metri, tracciamento percorso. - Marker posizione, cerchio accuratezza in metri, tracciamento percorso.
- Controllo se la posizione GPS cade dentro un poligono importato o disegnato. - Controllo se la posizione GPS cade dentro un poligono importato o disegnato.
- Interrogazione WFS ufficiale Agenzia delle Entrate attorno alla posizione GPS per evidenziare la particella catastale corrente quando il servizio restituisce la geometria. - Interrogazione WFS ufficiale Agenzia delle Entrate attorno alla posizione GPS per evidenziare la particella catastale corrente quando il servizio restituisce la geometria.
- Mappa a tutto schermo con tab Catasto, Progetti e Impostazioni, adattabile agli schermi pieghevoli. - Mappa a tutto schermo con tab separati Catasto, Dati, Progetti e Impostazioni, adattabile agli schermi pieghevoli.
- Ingrandimento oltre lo zoom satellitare nativo senza tessere "Map data not yet available". - Ingrandimento oltre lo zoom satellitare nativo senza tessere "Map data not yet available".
- Tendine concatenate Regione, Comune, Foglio e Particella. - Tendine concatenate Regione, Comune, Foglio e Particella.
- Ricerca di localita e indirizzi eseguita nativamente con fallback Esri e OpenStreetMap, senza richieste CORS dalla WebView. - Ricerca di localita e indirizzi eseguita nativamente con fallback Esri e OpenStreetMap, senza richieste CORS dalla WebView.
+1 -1
View File
@@ -13,7 +13,7 @@ public sealed class AzureMapsService
public AzureMapsService() public AzureMapsService()
{ {
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.8"); _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.9");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json"); _httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
} }
+87 -1
View File
@@ -292,6 +292,43 @@ input[type="checkbox"] {
color: var(--ink); color: var(--ink);
} }
.map-measure-tools {
position: absolute;
top: 72px;
right: 12px;
z-index: 860;
display: grid;
grid-template-columns: auto repeat(3, minmax(76px, auto));
align-items: center;
gap: 6px;
padding: 7px;
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.2);
transition: top 140ms ease;
}
.map-measure-tools > span {
padding: 0 4px;
color: var(--muted);
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
}
.map-measure-tools button {
min-height: 38px;
padding: 0 10px;
background: #fff3e8;
color: #8a3a08;
border: 1px solid #e4b58f;
}
.map-measure-tools.search-open {
top: 140px;
}
.tool-sheet { .tool-sheet {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -358,6 +395,29 @@ h2 {
padding: 15px 16px 24px; padding: 15px 16px 24px;
} }
.panel-section {
display: grid;
gap: 12px;
padding: 0 0 16px;
border-bottom: 1px solid var(--line);
}
.panel-section:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.panel-section h2 {
margin-top: 0;
}
.field-help {
margin-top: -7px;
color: var(--muted);
font-size: 10px;
line-height: 1.35;
}
label { label {
display: grid; display: grid;
gap: 5px; gap: 5px;
@@ -637,7 +697,7 @@ label {
left: 0; left: 0;
z-index: 1200; z-index: 1200;
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
height: var(--nav-height); height: var(--nav-height);
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
border-top: 1px solid #bcc7cd; border-top: 1px solid #bcc7cd;
@@ -695,6 +755,16 @@ footer {
display: none; display: none;
} }
@media (min-width: 701px) and (max-width: 900px) {
.map-measure-tools {
top: 126px;
}
.map-measure-tools.search-open {
top: 194px;
}
}
@media (max-width: 700px) { @media (max-width: 700px) {
.map-toolbar { .map-toolbar {
top: 8px; top: 8px;
@@ -738,6 +808,22 @@ footer {
top: 136px; top: 136px;
} }
.map-measure-tools,
.map-measure-tools.search-open {
top: 126px;
right: 8px;
left: 8px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.map-measure-tools.search-open {
top: 194px;
}
.map-measure-tools > span {
display: none;
}
.gps-main, .gps-main,
.gps-center { .gps-center {
padding: 0 8px; padding: 0 8px;
+81 -47
View File
@@ -13,7 +13,7 @@ const lmeState = {
drawControl: null, drawControl: null,
activeDrawHandler: null, activeDrawHandler: null,
drawEventsBound: false, drawEventsBound: false,
importParcelOnly: true, includeKmlSheetLayer: false,
showImportedMeasurements: false, showImportedMeasurements: false,
showDrawingMeasurements: true, showDrawingMeasurements: true,
measurementRefreshTimer: null, measurementRefreshTimer: null,
@@ -47,7 +47,7 @@ window.lmeMap = {
clearMapElements, clearMapElements,
fitImported, fitImported,
clearImported, clearImported,
setImportParcelOnly, setIncludeKmlSheetLayer,
setImportedMeasurementsVisible, setImportedMeasurementsVisible,
setDrawingMeasurementsVisible, setDrawingMeasurementsVisible,
addProjectMarker, addProjectMarker,
@@ -447,7 +447,7 @@ function bindFileImport() {
refreshImportedMeasurements(); refreshImportedMeasurements();
const featureCount = geojson.features.length; const featureCount = geojson.features.length;
const ignored = result.ignoredCount const ignored = result.ignoredCount
? `; esclusi ${result.ignoredCount} elementi non particella` ? `; esclusi ${result.ignoredCount} elementi del foglio o non particella`
: ""; : "";
const reprojected = result.reprojected ? "; coordinate Web Mercator convertite" : ""; const reprojected = result.reprojected ? "; coordinate Web Mercator convertite" : "";
setInfo(`Caricato ${file.name}: ${featureCount} elementi${ignored}${reprojected}.`); setInfo(`Caricato ${file.name}: ${featureCount} elementi${ignored}${reprojected}.`);
@@ -652,13 +652,14 @@ function normalizeKmlFiles(files) {
throw new Error("KML senza geometrie utilizzabili"); throw new Error("KML senza geometrie utilizzabili");
} }
const selected = lmeState.importParcelOnly ? filterKmlParcelFeatures(features) : features; const polygonParts = splitKmlPolygonFeatures(features);
const selected = lmeState.includeKmlSheetLayer ? features : filterKmlParcelFeatures(polygonParts);
if (!selected.length) { if (!selected.length) {
throw new Error("nessuna particella trovata; disattiva 'solo particelle' per importare tutti gli elementi"); throw new Error("nessuna particella trovata; attiva 'includi layer del foglio' per importare tutti gli elementi");
} }
return { return {
geojson: { type: "FeatureCollection", features: selected }, geojson: { type: "FeatureCollection", features: selected },
ignoredCount: features.length - selected.length, ignoredCount: lmeState.includeKmlSheetLayer ? 0 : Math.max(0, polygonParts.length - selected.length),
reprojected: false reprojected: false
}; };
} }
@@ -690,10 +691,7 @@ function kmlTextToGeoJson(text, sourceName) {
} }
function filterKmlParcelFeatures(features) { function filterKmlParcelFeatures(features) {
const polygons = features const filtered = features.filter(feature => {
.map(feature => ({ ...feature, geometry: polygonOnlyGeometry(feature.geometry) }))
.filter(feature => feature.geometry);
const filtered = polygons.filter(feature => {
const properties = feature.properties || {}; const properties = feature.properties || {};
if (properties["@geometry-type"] === "groundoverlay") { if (properties["@geometry-type"] === "groundoverlay") {
return false; return false;
@@ -706,45 +704,81 @@ function filterKmlParcelFeatures(features) {
const folder = String(properties._lmeFolder || "").toLowerCase(); const folder = String(properties._lmeFolder || "").toLowerCase();
const isParcel = /particell|mappal|parcel/.test(`${identity} ${folder}`) const isParcel = /particell|mappal|parcel/.test(`${identity} ${folder}`)
|| /^[a-z]?\s*\d+[a-z]?(?:[\/. -]\d+[a-z]?)?$/i.test(name); || /^[a-z]?\s*\d+[a-z]?(?:[\/. -]\d+[a-z]?)?$/i.test(name);
const isSheet = /\bfoglio\b|\bsheet\b|quadro[ _-]?d[' ]?unione|perimetro[ _-]?foglio/.test(identity); const isSheet = /\bfoglio\b|\bsheet\b|quadro[ _-]?d[' ]?unione|(?:perimetro|bordo|limite|contorno)[ _-]?(?:del[ _-]?)?foglio|sheet[ _-]?boundary/.test(identity);
return isParcel || !isSheet; return isParcel || !isSheet;
}); });
return removeLikelySheetOutline(filtered); return removeLikelySheetOutlines(filtered);
} }
function polygonOnlyGeometry(geometry) { function splitKmlPolygonFeatures(features) {
if (!geometry) { const result = [];
return null; const addGeometry = (feature, geometry, partIndex) => {
} if (!geometry) {
if (geometry.type === "Polygon" || geometry.type === "MultiPolygon") { return;
return geometry; }
} if (geometry.type === "Polygon") {
if (geometry.type === "GeometryCollection") { result.push({
const geometries = (geometry.geometries || []).filter(item => ...feature,
item.type === "Polygon" || item.type === "MultiPolygon"); properties: { ...(feature.properties || {}), _lmePart: partIndex },
return geometries.length ? { type: "GeometryCollection", geometries } : null; geometry
} });
return null; return;
}
if (geometry.type === "MultiPolygon") {
(geometry.coordinates || []).forEach((coordinates, index) => addGeometry(
feature,
{ type: "Polygon", coordinates },
`${partIndex}.${index}`));
return;
}
if (geometry.type === "GeometryCollection") {
(geometry.geometries || []).forEach((item, index) => addGeometry(feature, item, `${partIndex}.${index}`));
}
};
features.forEach((feature, index) => addGeometry(feature, feature.geometry, index));
return result;
} }
function removeLikelySheetOutline(features) { function removeLikelySheetOutlines(features) {
if (features.length < 3) { let remaining = [...features];
return features; while (remaining.length >= 2) {
const candidates = remaining
.map(feature => ({ feature, bounds: geoJsonBounds(feature.geometry) }))
.filter(item => item.bounds);
if (candidates.length < 2) {
break;
}
const largest = candidates.reduce((current, item) =>
boundsArea(item.bounds) > boundsArea(current.bounds) ? item : current);
if (isStrongParcelFeature(largest.feature)) {
break;
}
const others = candidates.filter(item => item !== largest);
const comparisonAreas = others.map(item => boundsArea(item.bounds)).sort((a, b) => a - b);
const referenceArea = comparisonAreas[Math.floor(comparisonAreas.length / 2)];
const contained = others.filter(item => boundsContains(largest.bounds, boundsCenter(item.bounds))).length;
const minimumContained = candidates.length === 2 ? 1 : Math.ceil(others.length * 0.6);
const areaFactor = 2.5;
const looksLikeSheet = referenceArea > 0
&& boundsArea(largest.bounds) > referenceArea * areaFactor
&& contained >= minimumContained;
if (!looksLikeSheet) {
break;
}
remaining = remaining.filter(feature => feature !== largest.feature);
} }
const candidates = features.map(feature => ({ feature, bounds: geoJsonBounds(feature.geometry) })); return remaining;
const valid = candidates.filter(item => item.bounds); }
if (valid.length < 3) {
return features; function isStrongParcelFeature(feature) {
} const properties = feature?.properties || {};
const areas = valid.map(item => boundsArea(item.bounds)).sort((a, b) => a - b); const name = String(properties.name || properties.Name || properties.LABEL || "").trim();
const median = areas[Math.floor(areas.length / 2)]; const context = [name, properties.layer, properties.Layer, properties.type, properties.Tipo, properties._lmeFolder]
const largest = valid.reduce((current, item) => .filter(Boolean)
boundsArea(item.bounds) > boundsArea(current.bounds) ? item : current); .join(" ");
const others = valid.filter(item => item !== largest); return /particell|mappal|parcel/i.test(context)
const contained = others.filter(item => boundsContains(largest.bounds, boundsCenter(item.bounds))).length; || /^[a-z]?\s*\d+[a-z]?(?:[\/. -]\d+[a-z]?)?$/i.test(name);
const looksLikeSheet = boundsArea(largest.bounds) > median * 8
&& contained >= Math.ceil(others.length * 0.75);
return looksLikeSheet ? features.filter(feature => feature !== largest.feature) : features;
} }
function geoJsonBounds(geometry) { function geoJsonBounds(geometry) {
@@ -774,11 +808,11 @@ function boundsContains(bounds, point) {
&& point[1] >= bounds.south && point[1] <= bounds.north; && point[1] >= bounds.south && point[1] <= bounds.north;
} }
function setImportParcelOnly(enabled) { function setIncludeKmlSheetLayer(enabled) {
lmeState.importParcelOnly = Boolean(enabled); lmeState.includeKmlSheetLayer = Boolean(enabled);
setInfo(lmeState.importParcelOnly setInfo(lmeState.includeKmlSheetLayer
? "Importazione KML/KMZ: verranno caricate solo le particelle." ? "Importazione KML/KMZ: verranno inclusi anche foglio e altri elementi."
: "Importazione KML/KMZ: verranno caricati tutti gli elementi."); : "Importazione KML/KMZ: verranno caricate solo le particelle riconosciute.");
} }
function setImportedMeasurementsVisible(enabled) { function setImportedMeasurementsVisible(enabled) {