Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0344227a1b | ||
|
|
7933b2e86a |
No files matched your search
+121
-46
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.");
|
||||
|
||||
Reference in new issue
Block a user