Compare commits

..
2 Commits
Author SHA1 Message Date
livio 0344227a1b Center map on startup location 2026-09-21 11:24:22 +02:00
livio 7933b2e86a Reorganize map tools and improve KML sheet filtering 2026-09-21 11:01:12 +02:00
7 changed files with 403 additions and 109 deletions

No files matched your search

+121 -46
View File
@@ -72,6 +72,16 @@
<button class="gps-center" @onclick="CenterOnGps" aria-label="Centra posizione">Centra</button>
</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")
{
<aside class="tool-sheet">
@@ -149,28 +159,61 @@
</div>
</section>
}
else if (ActiveTab == "projects")
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="@ImportParcelsOnly" @onchange="ToggleImportParcelsOnly" />
<span>KML/KMZ: importa solo le particelle</span>
<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</button>
<button class="secondary-command" @onclick="ClearImportedLayers">Pulisci</button>
<button @onclick="FitImportedLayers">Inquadra dati</button>
<button class="secondary-command" @onclick="ClearImportedLayers">Rimuovi dati</button>
</div>
</section>
<h2>Archivio progetti</h2>
<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")
{
<section class="panel-content">
<section class="panel-section">
<h2>Salva progetto</h2>
<label>
Nome progetto
<input @bind="NewProjectName" @bind:event="oninput" placeholder="Es. Rilievo San Nicola" maxlength="80" />
@@ -180,7 +223,10 @@
<span>Includi le particelle catastali rosse</span>
</label>
<button class="primary-command" @onclick="SaveProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(NewProjectName))">Salva progetto</button>
</section>
<section class="panel-section">
<h2>Apri progetto</h2>
<label>
Progetto salvato
<select value="@SelectedProjectId" @onchange="SelectProject" disabled="@(Projects.Count == 0 || ProjectBusy)">
@@ -192,38 +238,18 @@
</select>
</label>
<div class="button-row">
<button @onclick="LoadProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Carica</button>
<button @onclick="LoadProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Apri</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")
{
<section class="panel-content">
<h2>Visualizzazione</h2>
<section class="panel-section">
<h2>Mappa e sfondo</h2>
<label>
Sfondo
<select value="@SelectedBaseLayer" @onchange="ChangeBaseLayer">
@@ -239,6 +265,14 @@
<option value="google-roadmap">Strade - Google Maps</option>
</select>
</label>
<label>
Trasparenza catasto
<input type="range" min="0" max="1" step="0.05" value="0.75" @onchange="ChangeCadastreOpacity" />
</label>
</section>
<section class="panel-section">
<h2>Servizi mappa con chiave</h2>
<label>
Chiave Google Maps Platform
<input type="password" @bind="GoogleMapsApiKey" placeholder="API key" autocomplete="off" />
@@ -253,16 +287,21 @@
<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>
</section>
<section class="panel-section">
<h2>GPS e percorso</h2>
<label class="toggle-line project-save-option">
<input type="checkbox" checked="@ShowGpsPosition" @onchange="ToggleGpsPosition" />
<span>Mostra punto e cerchio GPS</span>
</label>
<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>
@@ -270,10 +309,10 @@
<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>
}
</aside>
}
@@ -302,6 +341,7 @@
<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>
<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 == "settings" ? "active" : null)" @onclick='() => SelectTab("settings")'>Impostazioni</button>
</nav>
@@ -327,11 +367,12 @@
private double? CatalogFraction { get; set; }
private bool CatalogBusy { get; set; }
private bool GpsActive { get; set; }
private bool ShowGpsPosition { get; set; } = true;
private bool DrawToolsVisible { get; set; }
private bool TrailEnabled { get; set; }
private bool LayerPanelOpen { get; set; }
private bool IncludeCadastralParcels { get; set; } = true;
private bool ImportParcelsOnly { get; set; } = true;
private bool IncludeKmlSheetLayer { get; set; }
private bool ShowImportedMeasurements { get; set; }
private bool ShowDrawingMeasurements { get; set; } = true;
private bool ProjectBusy { get; set; }
@@ -355,7 +396,8 @@
private string PanelTitle => ActiveTab switch
{
"cadastre" => "Ricerca catastale",
"projects" => "Progetti e dati",
"data" => "Dati e disegni",
"projects" => "Archivio progetti",
"settings" => "Impostazioni",
_ => string.Empty
};
@@ -363,7 +405,8 @@
private string PanelEyebrow => ActiveTab switch
{
"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",
_ => string.Empty
};
@@ -415,10 +458,36 @@
if (firstRender)
{
await JS.InvokeVoidAsync("lmeMap.init");
await CenterOnInitialLocationAsync();
await MigrateLegacyProjectAsync();
}
}
private async Task CenterOnInitialLocationAsync()
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(12));
try
{
var location = await LocationService.GetCurrentLocationAsync(timeout.Token);
if (location is not null)
{
await JS.InvokeVoidAsync("lmeMap.showInitialPosition", location);
}
else
{
await JS.InvokeVoidAsync("lmeMap.showInfo", "Posizione iniziale non disponibile. La mappa resta utilizzabile normalmente.");
}
}
catch (OperationCanceledException)
{
await JS.InvokeVoidAsync("lmeMap.showInfo", "Ricerca della posizione iniziale scaduta. Usa Avvia GPS per riprovare.");
}
catch (Exception error)
{
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Posizione iniziale non disponibile: {error.GetBaseException().Message}");
}
}
private async Task MigrateLegacyProjectAsync()
{
try
@@ -642,6 +711,12 @@
}
}
private async Task ToggleGpsPosition(ChangeEventArgs args)
{
ShowGpsPosition = (bool?)args.Value == true;
await JS.InvokeVoidAsync("lmeMap.setGpsPositionVisible", ShowGpsPosition);
}
private async Task StartGps()
{
GpsActive = true;
@@ -751,10 +826,10 @@
private Task ClearImportedLayers() => JS.InvokeVoidAsync("lmeMap.clearImported").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;
await JS.InvokeVoidAsync("lmeMap.setImportParcelOnly", ImportParcelsOnly);
IncludeKmlSheetLayer = (bool?)args.Value == true;
await JS.InvokeVoidAsync("lmeMap.setIncludeKmlSheetLayer", IncludeKmlSheetLayer);
}
private async Task ToggleImportedMeasurements(ChangeEventArgs args)
+2 -2
View File
@@ -28,8 +28,8 @@
<ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>0.2.8</ApplicationDisplayVersion>
<ApplicationVersion>12</ApplicationVersion>
<ApplicationDisplayVersion>0.2.10</ApplicationDisplayVersion>
<ApplicationVersion>14</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>
+5 -3
View File
@@ -10,19 +10,21 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
- Interrogazione WMS con click/tap sulla mappa.
- Import di file KML, KMZ, GeoJSON e JSON.
- 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.
- 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.
- 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.
- Ogni progetto salva file GIS, POI, disegni, traccia e inquadratura; l'inclusione delle particelle catastali rosse e' opzionale.
- GPS nativo MAUI ad alta precisione con permessi Android/iOS.
- Centratura automatica una tantum sulla posizione attuale all'apertura, senza mostrare il punto o il cerchio e senza avviare il tracking GPS continuo.
- Punto e cerchio di precisione del GPS live possono essere mostrati o nascosti dalle impostazioni senza fermare il rilevamento.
- Marker posizione, cerchio accuratezza in metri, tracciamento percorso.
- 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.
- 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".
- Tendine concatenate Regione, Comune, Foglio e Particella.
- 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()
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.8");
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.10");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
}
+41 -8
View File
@@ -20,6 +20,37 @@ public sealed class PreciseLocationService : IAsyncDisposable
return status;
}
public async Task<LocationSnapshot?> GetCurrentLocationAsync(CancellationToken cancellationToken = default)
{
var permission = await EnsurePermissionAsync();
if (permission != PermissionStatus.Granted)
{
return null;
}
Location? location = null;
try
{
var request = new GeolocationRequest(GeolocationAccuracy.Best, TimeSpan.FromSeconds(8));
location = await Geolocation.Default.GetLocationAsync(request, cancellationToken);
}
catch (FeatureNotSupportedException)
{
return null;
}
if (location is null)
{
var lastKnown = await Geolocation.Default.GetLastKnownLocationAsync();
if (lastKnown is not null && lastKnown.Timestamp >= DateTimeOffset.UtcNow.AddMinutes(-15))
{
location = lastKnown;
}
}
return location is null ? null : ToSnapshot(location);
}
public async Task StartTrackingAsync(Func<LocationSnapshot, Task> onLocation, Func<string, Task> onError)
{
if (trackingCancellation is not null)
@@ -47,14 +78,7 @@ public sealed class PreciseLocationService : IAsyncDisposable
var location = await Geolocation.Default.GetLocationAsync(request, token);
if (location is not null)
{
await onLocation(new LocationSnapshot(
location.Latitude,
location.Longitude,
location.Accuracy,
location.Altitude,
location.Course,
location.Speed,
location.Timestamp));
await onLocation(ToSnapshot(location));
}
}
catch (OperationCanceledException)
@@ -84,6 +108,15 @@ public sealed class PreciseLocationService : IAsyncDisposable
StopTracking();
return ValueTask.CompletedTask;
}
private static LocationSnapshot ToSnapshot(Location location) => new(
location.Latitude,
location.Longitude,
location.Accuracy,
location.Altitude,
location.Course,
location.Speed,
location.Timestamp);
}
public sealed record LocationSnapshot(
+87 -1
View File
@@ -292,6 +292,43 @@ input[type="checkbox"] {
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 {
position: absolute;
top: 0;
@@ -358,6 +395,29 @@ h2 {
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 {
display: grid;
gap: 5px;
@@ -637,7 +697,7 @@ label {
left: 0;
z-index: 1200;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
height: var(--nav-height);
padding-bottom: env(safe-area-inset-bottom);
border-top: 1px solid #bcc7cd;
@@ -695,6 +755,16 @@ footer {
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) {
.map-toolbar {
top: 8px;
@@ -738,6 +808,22 @@ footer {
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-center {
padding: 0 8px;
+146 -48
View File
@@ -13,12 +13,15 @@ const lmeState = {
drawControl: null,
activeDrawHandler: null,
drawEventsBound: false,
importParcelOnly: true,
includeKmlSheetLayer: false,
showImportedMeasurements: false,
showDrawingMeasurements: true,
measurementRefreshTimer: null,
gpsMarker: null,
gpsAccuracy: null,
gpsPositionVisible: true,
hasLiveGpsFix: false,
lastAccuracy: 0,
gpsTrail: null,
selectedParcelsLayer: null,
gpsParcelLayer: null,
@@ -40,6 +43,7 @@ const cadastreCrs = L.extend({}, L.CRS.EPSG4326, { code: "EPSG:4258" });
window.lmeMap = {
init,
showSearchResult,
showInitialPosition,
displayOfficialParcel,
exportCadastreView,
closeCadastrePreview,
@@ -47,7 +51,7 @@ window.lmeMap = {
clearMapElements,
fitImported,
clearImported,
setImportParcelOnly,
setIncludeKmlSheetLayer,
setImportedMeasurementsVisible,
setDrawingMeasurementsVisible,
addProjectMarker,
@@ -69,6 +73,7 @@ window.lmeMap = {
centerOnGps,
toggleTrail,
setNativeGpsPosition,
setGpsPositionVisible,
setGpsError,
resize,
showInfo: setInfo
@@ -178,6 +183,23 @@ function showSearchResult(latitude, longitude, label) {
setInfo(label || `${lat.toFixed(6)}, ${lon.toFixed(6)}`);
}
function showInitialPosition(location) {
const latitude = Number(location?.latitude);
const longitude = Number(location?.longitude);
const accuracy = Math.max(0, Number(location?.accuracy) || 0);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
return;
}
const latlng = [latitude, longitude];
lmeState.lastPosition = latlng;
lmeState.lastAccuracy = accuracy;
lmeState.map.setView(latlng, accuracy > 80 ? 17 : 19);
setText("gpsState", "Posizione iniziale acquisita - GPS non attivo");
setInfo("Mappa centrata sulla posizione attuale. GPS continuo non attivo.");
updateGpsInsideState();
}
function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shouldFit) {
const lat = Number(latitude);
const lon = Number(longitude);
@@ -447,7 +469,7 @@ function bindFileImport() {
refreshImportedMeasurements();
const featureCount = geojson.features.length;
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" : "";
setInfo(`Caricato ${file.name}: ${featureCount} elementi${ignored}${reprojected}.`);
@@ -652,13 +674,14 @@ function normalizeKmlFiles(files) {
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) {
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 {
geojson: { type: "FeatureCollection", features: selected },
ignoredCount: features.length - selected.length,
ignoredCount: lmeState.includeKmlSheetLayer ? 0 : Math.max(0, polygonParts.length - selected.length),
reprojected: false
};
}
@@ -690,10 +713,7 @@ function kmlTextToGeoJson(text, sourceName) {
}
function filterKmlParcelFeatures(features) {
const polygons = features
.map(feature => ({ ...feature, geometry: polygonOnlyGeometry(feature.geometry) }))
.filter(feature => feature.geometry);
const filtered = polygons.filter(feature => {
const filtered = features.filter(feature => {
const properties = feature.properties || {};
if (properties["@geometry-type"] === "groundoverlay") {
return false;
@@ -706,45 +726,81 @@ function filterKmlParcelFeatures(features) {
const folder = String(properties._lmeFolder || "").toLowerCase();
const isParcel = /particell|mappal|parcel/.test(`${identity} ${folder}`)
|| /^[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 removeLikelySheetOutline(filtered);
return removeLikelySheetOutlines(filtered);
}
function polygonOnlyGeometry(geometry) {
function splitKmlPolygonFeatures(features) {
const result = [];
const addGeometry = (feature, geometry, partIndex) => {
if (!geometry) {
return null;
return;
}
if (geometry.type === "Polygon" || geometry.type === "MultiPolygon") {
return geometry;
if (geometry.type === "Polygon") {
result.push({
...feature,
properties: { ...(feature.properties || {}), _lmePart: partIndex },
geometry
});
return;
}
if (geometry.type === "MultiPolygon") {
(geometry.coordinates || []).forEach((coordinates, index) => addGeometry(
feature,
{ type: "Polygon", coordinates },
`${partIndex}.${index}`));
return;
}
if (geometry.type === "GeometryCollection") {
const geometries = (geometry.geometries || []).filter(item =>
item.type === "Polygon" || item.type === "MultiPolygon");
return geometries.length ? { type: "GeometryCollection", geometries } : null;
(geometry.geometries || []).forEach((item, index) => addGeometry(feature, item, `${partIndex}.${index}`));
}
return null;
};
features.forEach((feature, index) => addGeometry(feature, feature.geometry, index));
return result;
}
function removeLikelySheetOutline(features) {
if (features.length < 3) {
return features;
function removeLikelySheetOutlines(features) {
let remaining = [...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 candidates = features.map(feature => ({ feature, bounds: geoJsonBounds(feature.geometry) }));
const valid = candidates.filter(item => item.bounds);
if (valid.length < 3) {
return features;
}
const areas = valid.map(item => boundsArea(item.bounds)).sort((a, b) => a - b);
const median = areas[Math.floor(areas.length / 2)];
const largest = valid.reduce((current, item) =>
const largest = candidates.reduce((current, item) =>
boundsArea(item.bounds) > boundsArea(current.bounds) ? item : current);
const others = valid.filter(item => item !== largest);
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 looksLikeSheet = boundsArea(largest.bounds) > median * 8
&& contained >= Math.ceil(others.length * 0.75);
return looksLikeSheet ? features.filter(feature => feature !== largest.feature) : features;
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);
}
return remaining;
}
function isStrongParcelFeature(feature) {
const properties = feature?.properties || {};
const name = String(properties.name || properties.Name || properties.LABEL || "").trim();
const context = [name, properties.layer, properties.Layer, properties.type, properties.Tipo, properties._lmeFolder]
.filter(Boolean)
.join(" ");
return /particell|mappal|parcel/i.test(context)
|| /^[a-z]?\s*\d+[a-z]?(?:[\/. -]\d+[a-z]?)?$/i.test(name);
}
function geoJsonBounds(geometry) {
@@ -774,11 +830,11 @@ function boundsContains(bounds, point) {
&& point[1] >= bounds.south && point[1] <= bounds.north;
}
function setImportParcelOnly(enabled) {
lmeState.importParcelOnly = Boolean(enabled);
setInfo(lmeState.importParcelOnly
? "Importazione KML/KMZ: verranno caricate solo le particelle."
: "Importazione KML/KMZ: verranno caricati tutti gli elementi.");
function setIncludeKmlSheetLayer(enabled) {
lmeState.includeKmlSheetLayer = Boolean(enabled);
setInfo(lmeState.includeKmlSheetLayer
? "Importazione KML/KMZ: verranno inclusi anche foglio e altri elementi."
: "Importazione KML/KMZ: verranno caricate solo le particelle riconosciute.");
}
function setImportedMeasurementsVisible(enabled) {
@@ -1671,6 +1727,25 @@ function setNativeGpsPosition(location) {
});
}
function setGpsPositionVisible(enabled) {
lmeState.gpsPositionVisible = Boolean(enabled);
if (!lmeState.gpsPositionVisible) {
if (lmeState.gpsMarker && lmeState.map.hasLayer(lmeState.gpsMarker)) {
lmeState.map.removeLayer(lmeState.gpsMarker);
}
if (lmeState.gpsAccuracy && lmeState.map.hasLayer(lmeState.gpsAccuracy)) {
lmeState.map.removeLayer(lmeState.gpsAccuracy);
}
setInfo("Punto e cerchio GPS nascosti. Il rilevamento continua.");
return;
}
if (lmeState.hasLiveGpsFix && lmeState.lastPosition) {
updateGpsPositionLayers(lmeState.lastPosition, lmeState.lastAccuracy);
}
setInfo("Punto e cerchio GPS visibili.");
}
function setGpsError(error) {
setText("gpsState", "GPS non disponibile");
setInfo(error || "Errore GPS.");
@@ -1695,14 +1770,12 @@ function onGpsPosition(position) {
const { latitude, longitude, accuracy } = position.coords;
const latlng = [latitude, longitude];
lmeState.lastPosition = latlng;
if (!lmeState.gpsMarker) {
lmeState.gpsMarker = L.marker(latlng, { title: "La tua posizione" }).addTo(lmeState.map);
lmeState.gpsAccuracy = L.circle(latlng, { radius: accuracy, color: "#2563eb", weight: 2, fillColor: "#60a5fa", fillOpacity: 0.16 }).addTo(lmeState.map);
lmeState.lastAccuracy = Math.max(0, Number(accuracy) || 0);
const firstLiveFix = !lmeState.hasLiveGpsFix;
lmeState.hasLiveGpsFix = true;
updateGpsPositionLayers(latlng, lmeState.lastAccuracy);
if (firstLiveFix) {
lmeState.map.setView(latlng, 19);
} else {
lmeState.gpsMarker.setLatLng(latlng);
lmeState.gpsAccuracy.setLatLng(latlng).setRadius(accuracy);
}
if (lmeState.gpsTrailEnabled) {
@@ -1710,10 +1783,35 @@ function onGpsPosition(position) {
lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints);
}
setText("gpsState", `${latitude.toFixed(6)}, ${longitude.toFixed(6)} - precisione ${Math.round(accuracy)} m`);
setText("gpsState", `${latitude.toFixed(6)}, ${longitude.toFixed(6)} - precisione ${Math.round(lmeState.lastAccuracy)} m`);
updateGpsInsideState();
}
function updateGpsPositionLayers(latlng, accuracy) {
if (!lmeState.gpsPositionVisible) {
return;
}
if (!lmeState.gpsMarker) {
lmeState.gpsMarker = L.marker(latlng, { title: "La tua posizione" });
lmeState.gpsAccuracy = L.circle(latlng, {
radius: accuracy,
color: "#2563eb",
weight: 2,
fillColor: "#60a5fa",
fillOpacity: 0.16
});
} else {
lmeState.gpsMarker.setLatLng(latlng);
lmeState.gpsAccuracy.setLatLng(latlng).setRadius(accuracy);
}
if (!lmeState.map.hasLayer(lmeState.gpsMarker)) {
lmeState.gpsMarker.addTo(lmeState.map);
}
if (!lmeState.map.hasLayer(lmeState.gpsAccuracy)) {
lmeState.gpsAccuracy.addTo(lmeState.map);
}
}
function onGpsError(error) {
setText("gpsState", "GPS non autorizzato o non disponibile");
setInfo(error.message || "Errore GPS.");