Compare commits

..
7 Commits
9 changed files with 2227 additions and 120 deletions

No files matched your search

+446 -44
View File
@@ -5,6 +5,7 @@
@inject OfficialCadastreService OfficialCadastreService @inject OfficialCadastreService OfficialCadastreService
@inject LocationSearchService LocationSearchService @inject LocationSearchService LocationSearchService
@inject AzureMapsService AzureMapsService @inject AzureMapsService AzureMapsService
@inject ProjectStorageService ProjectStorageService
@implements IAsyncDisposable @implements IAsyncDisposable
<PageTitle>LME Mappe Catastali</PageTitle> <PageTitle>LME Mappe Catastali</PageTitle>
@@ -18,7 +19,16 @@
<strong>LME Catasto</strong> <strong>LME Catasto</strong>
<span>@Status</span> <span>@Status</span>
</div> </div>
<div class="toolbar-actions">
<button class="toolbar-button layer-button @(LayerPanelOpen ? "is-active" : null)"
title="Layer catastali"
aria-expanded="@LayerPanelOpen"
@onclick="ToggleLayerPanel">
<span class="layer-symbol" aria-hidden="true"></span>
<span>Layer</span>
</button>
<button class="toolbar-button" @onclick='() => SelectTab(ActiveTab == "search" ? "map" : "search")'>Cerca</button> <button class="toolbar-button" @onclick='() => SelectTab(ActiveTab == "search" ? "map" : "search")'>Cerca</button>
</div>
</header> </header>
<div class="map-search @(ActiveTab == "search" ? "is-open" : null)"> <div class="map-search @(ActiveTab == "search" ? "is-open" : null)">
@@ -26,6 +36,33 @@
<button @onclick="SearchLocation">Vai</button> <button @onclick="SearchLocation">Vai</button>
</div> </div>
@if (LayerPanelOpen)
{
<section class="map-layer-panel" aria-label="Layer catastali">
<header>
<strong>Layer catastali</strong>
<button class="layer-panel-close" aria-label="Chiudi layer" @onclick="ToggleLayerPanel">×</button>
</header>
@foreach (var layer in CadastralLayers)
{
<label class="map-layer-option">
<input type="checkbox" checked="@layer.Enabled" @onchange="(args) => ToggleCadastreLayer(layer, args)" />
<span>
<strong>@layer.Name</strong>
@if (!string.IsNullOrWhiteSpace(layer.Detail))
{
<small>@layer.Detail</small>
}
</span>
</label>
}
<div class="layer-panel-actions">
<button class="secondary-command" @onclick="ClearSelectedParcels">Rimuovi particelle selezionate</button>
<button class="danger-command" @onclick="ClearMapElements">Pulisci elementi mappa</button>
</div>
</section>
}
<div class="gps-overlay @(ActiveTab == "search" ? "search-open" : null)"> <div class="gps-overlay @(ActiveTab == "search" ? "search-open" : null)">
<div class="gps-state"> <div class="gps-state">
<span id="gpsState">GPS non attivo</span> <span id="gpsState">GPS non attivo</span>
@@ -35,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">
@@ -112,39 +159,97 @@
</div> </div>
</section> </section>
} }
else if (ActiveTab == "projects") else if (ActiveTab == "data")
{ {
<section class="panel-content"> <section class="panel-content">
<section class="panel-section">
<h2>File GIS</h2> <h2>File GIS</h2>
<label class="file-picker"> <label class="file-picker">
<span>Carica KML, KMZ o GeoJSON</span> <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" /> <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>
<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"> <div class="button-row">
<button @onclick="FitImportedLayers">Inquadra</button> <button @onclick="FitImportedLayers">Inquadra dati</button>
<button class="secondary-command" @onclick="ClearImportedLayers">Pulisci</button> <button class="secondary-command" @onclick="ClearImportedLayers">Rimuovi dati</button>
</div> </div>
</section>
<h2>Progetti e POI</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> <label>
Nome Nome POI
<input @bind="ProjectName" @bind:event="oninput" placeholder="Nome progetto o POI" /> <input @bind="PoiName" @bind:event="oninput" placeholder="Nome del punto" />
</label> </label>
<button class="primary-command" @onclick="AddProjectMarker">Aggiungi POI al centro mappa</button> <button class="primary-command" @onclick="AddProjectMarker">Aggiungi POI al centro mappa</button>
<button class="secondary-command" aria-pressed="@DrawToolsVisible" @onclick="ToggleDrawTools"> </section>
@(DrawToolsVisible ? "Nascondi strumenti di disegno" : "Mostra strumenti di disegno") </section>
</button> }
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" />
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="IncludeCadastralParcels" />
<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)">
<option value="">Seleziona un progetto</option>
@foreach (var project in Projects)
{
<option value="@project.Id">@project.Name - @project.UpdatedAt.ToLocalTime().ToString("dd/MM/yyyy HH:mm")</option>
}
</select>
</label>
<div class="button-row"> <div class="button-row">
<button @onclick="SaveWorkspace">Salva</button> <button @onclick="LoadProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Apri</button>
<button @onclick="LoadWorkspace">Carica</button> <button class="danger-command" @onclick="DeleteProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Elimina</button>
</div> </div>
<button class="danger-command" @onclick="ClearDrawings">Elimina disegni e POI</button> <div class="project-archive-state">@ProjectArchiveMessage</div>
</section>
</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">
<h2>Mappa e sfondo</h2>
<label> <label>
Sfondo Sfondo
<select value="@SelectedBaseLayer" @onchange="ChangeBaseLayer"> <select value="@SelectedBaseLayer" @onchange="ChangeBaseLayer">
@@ -160,6 +265,14 @@
<option value="google-roadmap">Strade - Google Maps</option> <option value="google-roadmap">Strade - Google Maps</option>
</select> </select>
</label> </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> <label>
Chiave Google Maps Platform Chiave Google Maps Platform
<input type="password" @bind="GoogleMapsApiKey" placeholder="API key" autocomplete="off" /> <input type="password" @bind="GoogleMapsApiKey" placeholder="API key" autocomplete="off" />
@@ -174,26 +287,21 @@
<strong>Quota gratuita Azure Maps Gen2</strong> <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> <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> </div>
<label> </section>
Trasparenza catasto
<input type="range" min="0" max="1" step="0.05" value="0.75" @onchange="ChangeCadastreOpacity" />
</label>
<div class="layer-toggles">
@foreach (var layer in CadastralLayers)
{
<label>
<input type="checkbox" checked="@layer.Enabled" @onchange="(args) => ToggleCadastreLayer(layer.Key, args)" />
@layer.Name
</label>
}
</div>
<section class="panel-section">
<h2>GPS e percorso</h2> <h2>GPS e percorso</h2>
<label class="toggle-line"> <label class="toggle-line project-save-option">
<input type="checkbox" checked="@TrailEnabled" @onchange="ToggleTrail" /> <input type="checkbox" checked="@ShowGpsPosition" @onchange="ToggleGpsPosition" />
Registra la traccia mentre cammini <span>Mostra punto e cerchio GPS</span>
</label> </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> <h2>Servizi Agenzia delle Entrate</h2>
<div class="service-note protected-service"> <div class="service-note protected-service">
<strong>Download documenti e fogli ufficiali</strong> <strong>Download documenti e fogli ufficiali</strong>
@@ -201,19 +309,39 @@
<button class="primary-command" @onclick="OpenOfficialGeoportal">Apri Geoportale catastale</button> <button class="primary-command" @onclick="OpenOfficialGeoportal">Apri Geoportale catastale</button>
<button class="secondary-command" @onclick="OpenOfficialReservedArea">Apri Area riservata per download</button> <button class="secondary-command" @onclick="OpenOfficialReservedArea">Apri Area riservata per download</button>
</div> </div>
<button class="secondary-command" @onclick="ExportCadastralImage">Mostra vista WMS corrente</button>
<button class="secondary-command" @onclick="ExportCadastralImage">Apri vista WMS corrente</button>
<footer>Dati catastali: Agenzia delle Entrate, licenza CC BY 4.0. App indipendente.</footer> <footer>Dati catastali: Agenzia delle Entrate, licenza CC BY 4.0. App indipendente.</footer>
</section> </section>
</section>
} }
</aside> </aside>
} }
<div id="mapInfo" class="map-info">Pronto</div> <div id="mapInfo" class="map-info">Pronto</div>
<div id="wmsPreview" class="wms-preview" hidden>
<section class="wms-preview-dialog" role="dialog" aria-modal="true" aria-labelledby="wmsPreviewTitle">
<header>
<div>
<strong id="wmsPreviewTitle">Vista WMS catastale</strong>
<small>Immagine ufficiale AdE con le particelle evidenziate dall'app</small>
</div>
<button id="closeWmsPreview" class="close-button" aria-label="Chiudi vista WMS">×</button>
</header>
<div class="wms-preview-body">
<span id="wmsPreviewLoading">Caricamento...</span>
<div id="wmsPreviewMap" class="wms-preview-map" hidden>
<img id="wmsPreviewImage" alt="Vista WMS catastale dell'area corrente" />
<svg id="wmsPreviewOverlay" aria-hidden="true" preserveAspectRatio="none"></svg>
</div>
</div>
</section>
</div>
<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>
@@ -223,7 +351,10 @@
@code { @code {
private string ActiveTab { get; set; } = "map"; private string ActiveTab { get; set; } = "map";
private string SearchText { get; set; } = string.Empty; private string SearchText { get; set; } = string.Empty;
private string ProjectName { get; set; } = string.Empty; private string NewProjectName { get; set; } = string.Empty;
private string PoiName { get; set; } = string.Empty;
private string SelectedProjectId { get; set; } = string.Empty;
private string ProjectArchiveMessage { get; set; } = "Nessun progetto salvato.";
private string Status { get; set; } = "online"; private string Status { get; set; } = "online";
private string SelectedBaseLayer { get; set; } = "satellite"; private string SelectedBaseLayer { get; set; } = "satellite";
private string GoogleMapsApiKey { get; set; } = string.Empty; private string GoogleMapsApiKey { get; set; } = string.Empty;
@@ -236,8 +367,15 @@
private double? CatalogFraction { get; set; } private double? CatalogFraction { get; set; }
private bool CatalogBusy { get; set; } private bool CatalogBusy { get; set; }
private bool GpsActive { get; set; } private bool GpsActive { get; set; }
private bool ShowGpsPosition { get; set; } = true;
private bool DrawToolsVisible { get; set; } private bool DrawToolsVisible { get; set; }
private bool TrailEnabled { get; set; } private bool TrailEnabled { get; set; }
private bool LayerPanelOpen { get; set; }
private bool IncludeCadastralParcels { get; set; } = true;
private bool IncludeKmlSheetLayer { get; set; }
private bool ShowImportedMeasurements { get; set; }
private bool ShowDrawingMeasurements { get; set; } = true;
private bool ProjectBusy { get; set; }
private CancellationTokenSource? CatalogCancellation { get; set; } private CancellationTokenSource? CatalogCancellation { get; set; }
private CancellationTokenSource? GpsLookupCancellation { get; set; } private CancellationTokenSource? GpsLookupCancellation { get; set; }
private long LastGpsLookupAt; private long LastGpsLookupAt;
@@ -247,6 +385,7 @@
private IReadOnlyList<MunicipalityOption> Municipalities { get; set; } = []; private IReadOnlyList<MunicipalityOption> Municipalities { get; set; } = [];
private IReadOnlyList<string> Sheets { get; set; } = []; private IReadOnlyList<string> Sheets { get; set; } = [];
private IReadOnlyList<ParcelOption> Parcels { get; set; } = []; private IReadOnlyList<ParcelOption> Parcels { get; set; } = [];
private IReadOnlyList<MapProjectSummary> Projects { get; set; } = [];
private MunicipalityOption? SelectedMunicipality => private MunicipalityOption? SelectedMunicipality =>
Municipalities.FirstOrDefault(item => item.Code == SelectedMunicipalityCode); Municipalities.FirstOrDefault(item => item.Code == SelectedMunicipalityCode);
@@ -257,7 +396,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
}; };
@@ -265,18 +405,19 @@
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
}; };
private readonly List<CadastralLayer> CadastralLayers = private readonly List<CadastralLayer> CadastralLayers =
[ [
new("CP.CadastralParcel", "Particelle", true), new("CP.CadastralParcel", "Confini particelle", true),
new("codice_plla", "Numeri particelle", true), new("codice_plla", "Numeri particelle", true, "Visibili circa da zoom 17"),
new("CP.CadastralZoning", "Fogli", true), new("CP.CadastralZoning", "Fogli catastali", true),
new("fabbricati", "Fabbricati", true), new("fabbricati", "Fabbricati", true),
new("strade", "Strade", false), new("strade", "Strade catastali", false),
new("acque", "Acque", false) new("acque", "Acque", false)
]; ];
@@ -308,6 +449,8 @@
{ {
CatalogMessage = $"Catalogo Comuni non disponibile: {error.Message}"; CatalogMessage = $"Catalogo Comuni non disponibile: {error.Message}";
} }
await RefreshProjectsAsync();
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -315,12 +458,84 @@
if (firstRender) if (firstRender)
{ {
await JS.InvokeVoidAsync("lmeMap.init"); 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
{
var legacySnapshot = await JS.InvokeAsync<string?>("lmeMap.getLegacyProjectSnapshot");
if (string.IsNullOrWhiteSpace(legacySnapshot))
{
return;
}
var migrated = await ProjectStorageService.SaveAsync(
"Progetto precedente",
legacySnapshot,
false);
await JS.InvokeVoidAsync("lmeMap.clearLegacyProjectSnapshot");
await RefreshProjectsAsync();
SelectedProjectId = migrated.Id;
await InvokeAsync(StateHasChanged);
}
catch
{
// The legacy value remains available and migration can be retried at the next start.
} }
} }
private async Task SelectTab(string tab) private async Task SelectTab(string tab)
{ {
ActiveTab = tab; ActiveTab = tab;
if (tab != "map")
{
LayerPanelOpen = false;
}
if (tab == "projects")
{
await RefreshProjectsAsync();
}
await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize");
}
private async Task ToggleLayerPanel()
{
LayerPanelOpen = !LayerPanelOpen;
if (LayerPanelOpen)
{
ActiveTab = "map";
}
await Task.Delay(30); await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize"); await JS.InvokeVoidAsync("lmeMap.resize");
} }
@@ -496,6 +711,12 @@
} }
} }
private async Task ToggleGpsPosition(ChangeEventArgs args)
{
ShowGpsPosition = (bool?)args.Value == true;
await JS.InvokeVoidAsync("lmeMap.setGpsPositionVisible", ShowGpsPosition);
}
private async Task StartGps() private async Task StartGps()
{ {
GpsActive = true; GpsActive = true;
@@ -603,7 +824,37 @@
private Task FitImportedLayers() => JS.InvokeVoidAsync("lmeMap.fitImported").AsTask(); private Task FitImportedLayers() => JS.InvokeVoidAsync("lmeMap.fitImported").AsTask();
private Task ClearImportedLayers() => JS.InvokeVoidAsync("lmeMap.clearImported").AsTask(); private Task ClearImportedLayers() => JS.InvokeVoidAsync("lmeMap.clearImported").AsTask();
private Task AddProjectMarker() => JS.InvokeVoidAsync("lmeMap.addProjectMarker", ProjectName).AsTask(); private Task AddProjectMarker() => JS.InvokeVoidAsync("lmeMap.addProjectMarker", PoiName).AsTask();
private async Task ToggleKmlSheetLayer(ChangeEventArgs args)
{
IncludeKmlSheetLayer = (bool?)args.Value == true;
await JS.InvokeVoidAsync("lmeMap.setIncludeKmlSheetLayer", IncludeKmlSheetLayer);
}
private async Task ToggleImportedMeasurements(ChangeEventArgs args)
{
ShowImportedMeasurements = (bool?)args.Value == true;
await JS.InvokeVoidAsync("lmeMap.setImportedMeasurementsVisible", ShowImportedMeasurements);
}
private async Task ToggleDrawingMeasurements(ChangeEventArgs args)
{
ShowDrawingMeasurements = (bool?)args.Value == true;
await JS.InvokeVoidAsync("lmeMap.setDrawingMeasurementsVisible", ShowDrawingMeasurements);
}
private async Task StartMeasurementDrawing(string mode)
{
DrawToolsVisible = false;
await JS.InvokeVoidAsync("lmeMap.setDrawToolsVisible", false);
ActiveTab = "map";
LayerPanelOpen = false;
await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize");
await JS.InvokeVoidAsync("lmeMap.startMeasurementDrawing", mode);
}
private async Task ToggleDrawTools() private async Task ToggleDrawTools()
{ {
DrawToolsVisible = !DrawToolsVisible; DrawToolsVisible = !DrawToolsVisible;
@@ -616,8 +867,136 @@
} }
private Task ClearDrawings() => JS.InvokeVoidAsync("lmeMap.clearDrawings").AsTask(); private Task ClearDrawings() => JS.InvokeVoidAsync("lmeMap.clearDrawings").AsTask();
private Task SaveWorkspace() => JS.InvokeVoidAsync("lmeMap.saveWorkspace").AsTask();
private Task LoadWorkspace() => JS.InvokeVoidAsync("lmeMap.loadWorkspace").AsTask(); private async Task RefreshProjectsAsync()
{
try
{
Projects = await ProjectStorageService.GetProjectsAsync();
if (Projects.All(project => project.Id != SelectedProjectId))
{
SelectedProjectId = string.Empty;
}
ProjectArchiveMessage = Projects.Count == 0
? "Nessun progetto salvato."
: $"{Projects.Count} {(Projects.Count == 1 ? "progetto salvato" : "progetti salvati")} nell'archivio interno.";
}
catch (Exception error)
{
ProjectArchiveMessage = $"Archivio non disponibile: {error.GetBaseException().Message}";
}
}
private Task SelectProject(ChangeEventArgs args)
{
SelectedProjectId = args.Value?.ToString() ?? string.Empty;
return Task.CompletedTask;
}
private async Task SaveProject()
{
var name = NewProjectName.Trim();
if (string.IsNullOrWhiteSpace(name))
{
await JS.InvokeVoidAsync("lmeMap.showInfo", "Inserisci il nome del progetto.");
return;
}
var existing = Projects.FirstOrDefault(project =>
string.Equals(project.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (existing is not null
&& !await JS.InvokeAsync<bool>("confirm", $"Il progetto '{existing.Name}' esiste gia. Sovrascriverlo?"))
{
return;
}
ProjectBusy = true;
try
{
var snapshot = await JS.InvokeAsync<string>("lmeMap.createProjectSnapshot", IncludeCadastralParcels);
var saved = await ProjectStorageService.SaveAsync(
name,
snapshot,
IncludeCadastralParcels,
existing?.Id);
await RefreshProjectsAsync();
SelectedProjectId = saved.Id;
NewProjectName = saved.Name;
await JS.InvokeVoidAsync(
"lmeMap.showInfo",
IncludeCadastralParcels
? $"Progetto salvato: {saved.Name}, incluse le particelle rosse."
: $"Progetto salvato: {saved.Name}, senza particelle rosse.");
}
catch (Exception error)
{
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto non salvato: {error.GetBaseException().Message}");
}
finally
{
ProjectBusy = false;
}
}
private async Task LoadProject()
{
var project = Projects.FirstOrDefault(item => item.Id == SelectedProjectId);
if (project is null)
{
await JS.InvokeVoidAsync("lmeMap.showInfo", "Seleziona il progetto da caricare.");
return;
}
ProjectBusy = true;
try
{
var snapshot = await ProjectStorageService.LoadAsync(project.Id);
await JS.InvokeVoidAsync("lmeMap.loadProjectSnapshot", snapshot, project.Name);
NewProjectName = project.Name;
IncludeCadastralParcels = project.IncludesCadastralParcels;
ActiveTab = "map";
await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize");
}
catch (Exception error)
{
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto non caricato: {error.GetBaseException().Message}");
}
finally
{
ProjectBusy = false;
}
}
private async Task DeleteProject()
{
var project = Projects.FirstOrDefault(item => item.Id == SelectedProjectId);
if (project is null)
{
return;
}
if (!await JS.InvokeAsync<bool>("confirm", $"Eliminare definitivamente il progetto '{project.Name}'?"))
{
return;
}
ProjectBusy = true;
try
{
await ProjectStorageService.DeleteAsync(project.Id);
SelectedProjectId = string.Empty;
await RefreshProjectsAsync();
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto eliminato: {project.Name}.");
}
catch (Exception error)
{
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto non eliminato: {error.GetBaseException().Message}");
}
finally
{
ProjectBusy = false;
}
}
private Task CenterOnGps() => JS.InvokeVoidAsync("lmeMap.centerOnGps").AsTask(); private Task CenterOnGps() => JS.InvokeVoidAsync("lmeMap.centerOnGps").AsTask();
private async Task ToggleTrail(ChangeEventArgs args) private async Task ToggleTrail(ChangeEventArgs args)
{ {
@@ -711,8 +1090,25 @@
} }
} }
private Task ChangeCadastreOpacity(ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.setCadastreOpacity", args.Value?.ToString()).AsTask(); private Task ChangeCadastreOpacity(ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.setCadastreOpacity", args.Value?.ToString()).AsTask();
private Task ToggleCadastreLayer(string layer, ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.toggleCadastreLayer", layer, (bool?)args.Value == true).AsTask();
private Task ExportCadastralImage() => JS.InvokeVoidAsync("lmeMap.exportCadastreView").AsTask(); private async Task ToggleCadastreLayer(CadastralLayer layer, ChangeEventArgs args)
{
layer.Enabled = args.Value is true;
await JS.InvokeVoidAsync("lmeMap.toggleCadastreLayer", layer.Key, layer.Enabled);
}
private async Task ExportCadastralImage()
{
ActiveTab = "map";
LayerPanelOpen = false;
await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize");
await JS.InvokeVoidAsync("lmeMap.exportCadastreView");
}
private Task ClearSelectedParcels() => JS.InvokeVoidAsync("lmeMap.clearSelectedParcels").AsTask();
private Task ClearMapElements() => JS.InvokeVoidAsync("lmeMap.clearMapElements").AsTask();
private Task OpenOfficialGeoportal() => Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync( private Task OpenOfficialGeoportal() => Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync(
new Uri("https://geoportale.cartografia.agenziaentrate.gov.it/")); new Uri("https://geoportale.cartografia.agenziaentrate.gov.it/"));
@@ -720,7 +1116,13 @@
private Task OpenOfficialReservedArea() => Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync( private Task OpenOfficialReservedArea() => Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync(
new Uri("https://www.agenziaentrate.gov.it/portale/area-riservata")); new Uri("https://www.agenziaentrate.gov.it/portale/area-riservata"));
private sealed record CadastralLayer(string Key, string Name, bool Enabled); private sealed class CadastralLayer(string key, string name, bool enabled, string detail = "")
{
public string Key { get; } = key;
public string Name { get; } = name;
public bool Enabled { get; set; } = enabled;
public string Detail { get; } = detail;
}
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
+2 -2
View File
@@ -28,8 +28,8 @@
<ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId> <ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>0.2.3</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.2.10</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>14</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>
+1
View File
@@ -21,6 +21,7 @@ public static class MauiProgram
builder.Services.AddSingleton<OfficialCadastreService>(); builder.Services.AddSingleton<OfficialCadastreService>();
builder.Services.AddSingleton<LocationSearchService>(); builder.Services.AddSingleton<LocationSearchService>();
builder.Services.AddSingleton<AzureMapsService>(); builder.Services.AddSingleton<AzureMapsService>();
builder.Services.AddSingleton<ProjectStorageService>();
#if DEBUG #if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools(); builder.Services.AddBlazorWebViewDeveloperTools();
+15 -2
View File
@@ -9,21 +9,34 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
- Trasparenza configurabile del layer catastale. - Trasparenza configurabile del layer catastale.
- 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.
- 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 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. - Toolbar di disegno completa di icone, modifica, eliminazione singola e pulizia di tutti i disegni/POI.
- POI/progetti salvabili in locale con `localStorage`. - 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. - 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. - 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.
- Catalogo dei Comuni incluso nell'APK e cataloghi regionali OnData scaricati in cache solo quando servono. - Catalogo dei Comuni incluso nell'APK e cataloghi regionali OnData scaricati in cache solo quando servono.
- Ricerca catastale eseguita dal codice .NET nativo, senza DuckDB WebAssembly nella WebView Android. - Ricerca catastale eseguita dal codice .NET nativo, senza DuckDB WebAssembly nella WebView Android.
- La registrazione della traccia GPS e' disattivata all'avvio; la toolbar di disegno puo' essere mostrata o nascosta dal tab Progetti. - La registrazione della traccia GPS e' disattivata all'avvio; la toolbar di disegno puo' essere mostrata o nascosta dal tab Progetti.
- Pannello `Layer` accessibile direttamente dalla mappa, con checkbox sincronizzati per confini, numeri particella, fogli, fabbricati, strade e acque.
- Le particelle cercate possono essere aggiunte una dopo l'altra, mantenendo contemporaneamente sulla mappa tutti i confini selezionati.
- Comandi separati per rimuovere le particelle selezionate o pulire importazioni, disegni, POI e traccia GPS.
- Anteprima WMS ufficiale mostrata dentro l'app senza aprire una nuova finestra Android.
- Confini delle particelle selezionate e della particella GPS sovrapposti anche all'anteprima WMS corrente.
- Richieste WMS catastali in EPSG:4258 compatibili con il servizio AdE; i numeri particella sono visibili alle scale di dettaglio consentite dal servizio (circa zoom 17 o superiore).
- Collegamenti separati al Geoportale pubblico e all'Area riservata AdE per CAPTCHA, SPID/CIE e download ufficiali. - Collegamenti separati al Geoportale pubblico e all'Area riservata AdE per CAPTCHA, SPID/CIE e download ufficiali.
## Limite ufficiale AdE da considerare ## Limite ufficiale AdE da considerare
+1 -1
View File
@@ -13,7 +13,7 @@ public sealed class AzureMapsService
public AzureMapsService() public AzureMapsService()
{ {
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.3"); _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.10");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json"); _httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
} }
+41 -8
View File
@@ -20,6 +20,37 @@ public sealed class PreciseLocationService : IAsyncDisposable
return status; 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) public async Task StartTrackingAsync(Func<LocationSnapshot, Task> onLocation, Func<string, Task> onError)
{ {
if (trackingCancellation is not null) if (trackingCancellation is not null)
@@ -47,14 +78,7 @@ public sealed class PreciseLocationService : IAsyncDisposable
var location = await Geolocation.Default.GetLocationAsync(request, token); var location = await Geolocation.Default.GetLocationAsync(request, token);
if (location is not null) if (location is not null)
{ {
await onLocation(new LocationSnapshot( await onLocation(ToSnapshot(location));
location.Latitude,
location.Longitude,
location.Accuracy,
location.Altitude,
location.Course,
location.Speed,
location.Timestamp));
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -84,6 +108,15 @@ public sealed class PreciseLocationService : IAsyncDisposable
StopTracking(); StopTracking();
return ValueTask.CompletedTask; 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( public sealed record LocationSnapshot(
+202
View File
@@ -0,0 +1,202 @@
using System.Text;
using System.Text.Json;
namespace LMEMappeCatastaliApp.Services;
public sealed class ProjectStorageService
{
private const string ProjectExtension = ".lmeproject.json";
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
private string ProjectsDirectory => Path.Combine(FileSystem.AppDataDirectory, "Projects");
public async Task<IReadOnlyList<MapProjectSummary>> GetProjectsAsync()
{
await _gate.WaitAsync();
try
{
var files = await ReadProjectFilesAsync();
return files
.Select(item => item.Document.ToSummary())
.OrderByDescending(item => item.UpdatedAt)
.ThenBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
}
finally
{
_gate.Release();
}
}
public async Task<MapProjectSummary> SaveAsync(
string name,
string snapshotJson,
bool includesCadastralParcels,
string? projectId = null)
{
var normalizedName = NormalizeName(name);
using var snapshotDocument = JsonDocument.Parse(snapshotJson);
if (snapshotDocument.RootElement.ValueKind != JsonValueKind.Object)
{
throw new InvalidDataException("I dati della mappa non sono validi.");
}
await _gate.WaitAsync();
try
{
Directory.CreateDirectory(ProjectsDirectory);
var files = await ReadProjectFilesAsync();
var existing = files.FirstOrDefault(item =>
(!string.IsNullOrWhiteSpace(projectId) && item.Document.Id == projectId)
|| string.Equals(item.Document.Name, normalizedName, StringComparison.CurrentCultureIgnoreCase));
var now = DateTimeOffset.UtcNow;
var id = existing?.Document.Id ?? Guid.NewGuid().ToString("N");
var document = new MapProjectDocument
{
SchemaVersion = 1,
Id = id,
Name = normalizedName,
CreatedAt = existing?.Document.CreatedAt ?? now,
UpdatedAt = now,
IncludesCadastralParcels = includesCadastralParcels,
Map = snapshotDocument.RootElement.Clone()
};
var path = existing?.Path ?? Path.Combine(ProjectsDirectory, BuildFileName(normalizedName, id));
var temporaryPath = path + ".tmp";
try
{
await File.WriteAllTextAsync(
temporaryPath,
JsonSerializer.Serialize(document, _jsonOptions),
new UTF8Encoding(false));
File.Move(temporaryPath, path, true);
}
finally
{
if (File.Exists(temporaryPath))
{
File.Delete(temporaryPath);
}
}
return document.ToSummary();
}
finally
{
_gate.Release();
}
}
public async Task<string> LoadAsync(string projectId)
{
await _gate.WaitAsync();
try
{
var project = (await ReadProjectFilesAsync())
.FirstOrDefault(item => item.Document.Id == projectId)
?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
return project.Document.Map.GetRawText();
}
finally
{
_gate.Release();
}
}
public async Task DeleteAsync(string projectId)
{
await _gate.WaitAsync();
try
{
var project = (await ReadProjectFilesAsync())
.FirstOrDefault(item => item.Document.Id == projectId)
?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
File.Delete(project.Path);
}
finally
{
_gate.Release();
}
}
private async Task<IReadOnlyList<ProjectFileEntry>> ReadProjectFilesAsync()
{
Directory.CreateDirectory(ProjectsDirectory);
var projects = new List<ProjectFileEntry>();
foreach (var path in Directory.EnumerateFiles(ProjectsDirectory, $"*{ProjectExtension}"))
{
try
{
await using var stream = File.OpenRead(path);
var document = await JsonSerializer.DeserializeAsync<MapProjectDocument>(stream, _jsonOptions);
if (document is not null
&& !string.IsNullOrWhiteSpace(document.Id)
&& !string.IsNullOrWhiteSpace(document.Name)
&& document.Map.ValueKind == JsonValueKind.Object)
{
projects.Add(new ProjectFileEntry(path, document));
}
}
catch (JsonException)
{
// A damaged project is ignored so the remaining archive stays usable.
}
catch (IOException)
{
}
}
return projects;
}
private static string NormalizeName(string name)
{
var normalized = string.Join(' ', (name ?? string.Empty).Split(
[' ', '\t', '\r', '\n'],
StringSplitOptions.RemoveEmptyEntries));
if (string.IsNullOrWhiteSpace(normalized))
{
throw new ArgumentException("Inserisci il nome del progetto.");
}
return normalized.Length <= 80 ? normalized : normalized[..80];
}
private static string BuildFileName(string name, string id)
{
var invalid = Path.GetInvalidFileNameChars().ToHashSet();
var safeName = new string(name
.Select(character => invalid.Contains(character) ? '-' : character)
.ToArray())
.Trim(' ', '.');
safeName = string.IsNullOrWhiteSpace(safeName) ? "Progetto" : safeName;
return $"{safeName}--{id[..8]}{ProjectExtension}";
}
private sealed record ProjectFileEntry(string Path, MapProjectDocument Document);
private sealed class MapProjectDocument
{
public int SchemaVersion { get; init; }
public string Id { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset UpdatedAt { get; init; }
public bool IncludesCadastralParcels { get; init; }
public JsonElement Map { get; init; }
public MapProjectSummary ToSummary() => new(Id, Name, UpdatedAt, IncludesCadastralParcels);
}
}
public sealed record MapProjectSummary(
string Id,
string Name,
DateTimeOffset UpdatedAt,
bool IncludesCadastralParcels);
+344 -1
View File
@@ -124,6 +124,28 @@ input[type="checkbox"] {
color: var(--ink); color: var(--ink);
} }
.toolbar-actions,
.layer-button {
display: flex;
align-items: center;
gap: 7px;
}
.toolbar-actions {
flex: 0 0 auto;
}
.layer-button.is-active {
background: #ccebe3;
color: var(--accent-strong);
}
.layer-symbol {
width: 20px;
height: 20px;
background: url("../lib/leaflet/images/layers.png") center / contain no-repeat;
}
.map-search { .map-search {
position: absolute; position: absolute;
top: 72px; top: 72px;
@@ -150,6 +172,73 @@ input[type="checkbox"] {
transform: translateY(0); transform: translateY(0);
} }
.map-layer-panel {
position: absolute;
top: 72px;
right: 12px;
z-index: 940;
display: grid;
width: min(320px, calc(100% - 24px));
max-height: calc(100% - 152px);
overflow-y: auto;
border: 1px solid var(--line);
border-radius: 8px;
padding: 9px;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.22);
}
.map-layer-panel > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 0 7px 4px;
border-bottom: 1px solid var(--line);
}
.layer-panel-close {
width: 36px;
min-width: 36px;
min-height: 34px;
padding: 0;
background: #e9eef0;
color: var(--ink);
font-size: 23px;
}
.map-layer-option {
display: flex;
align-items: center;
gap: 9px;
min-height: 45px;
padding: 6px 4px;
border-bottom: 1px solid #e5eaed;
}
.map-layer-option input {
flex: 0 0 auto;
width: 19px;
height: 19px;
}
.map-layer-option span {
display: grid;
gap: 2px;
}
.map-layer-option small {
color: var(--muted);
font-size: 10px;
font-weight: 600;
}
.layer-panel-actions {
display: grid;
gap: 7px;
padding-top: 9px;
}
.gps-overlay { .gps-overlay {
position: absolute; position: absolute;
top: 72px; top: 72px;
@@ -203,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;
@@ -269,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;
@@ -370,6 +519,69 @@ label {
height: 18px; height: 18px;
} }
.project-save-option {
padding: 10px 11px;
border: 1px solid var(--line);
border-radius: 6px;
background: #fff;
}
.project-archive-state {
color: var(--muted);
font-size: 11px;
line-height: 1.35;
}
.measurement-mode-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 7px;
}
.measurement-mode-grid button {
min-width: 0;
padding: 0 8px;
}
.lme-measure-label {
width: 0 !important;
height: 0 !important;
border: 0 !important;
background: transparent !important;
pointer-events: none !important;
}
.lme-measure-label span {
position: absolute;
transform: translate(-50%, -50%);
border: 1px solid rgba(23, 33, 43, 0.35);
border-radius: 4px;
padding: 2px 5px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24);
color: #17212b;
font-size: 10px;
font-weight: 800;
line-height: 1.25;
white-space: nowrap;
}
.lme-measure-label.imported span {
border-color: #15806a;
color: #0c5749;
}
.lme-measure-label.drawing span {
border-color: #d35d0b;
color: #7c2d12;
}
.lme-measure-label.side span {
padding: 1px 4px;
font-size: 9px;
font-weight: 700;
}
.map-info { .map-info {
position: absolute; position: absolute;
left: 12px; left: 12px;
@@ -384,6 +596,100 @@ label {
pointer-events: none; pointer-events: none;
} }
.wms-preview[hidden] {
display: none;
}
.wms-preview {
position: absolute;
inset: 0;
z-index: 1400;
display: grid;
place-items: center;
padding: 16px;
background: rgba(15, 22, 27, 0.72);
}
.wms-preview-dialog {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
width: min(980px, 100%);
height: min(820px, 100%);
overflow: hidden;
border-radius: 8px;
background: #fff;
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.34);
}
.wms-preview-dialog > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 11px 12px;
border-bottom: 1px solid var(--line);
}
.wms-preview-dialog > header div {
display: grid;
gap: 2px;
}
.wms-preview-dialog > header small {
color: var(--muted);
font-size: 10px;
}
.wms-preview-body {
display: grid;
min-height: 0;
place-items: center;
overflow: auto;
padding: 12px;
background: #eef2f3;
color: var(--muted);
}
.wms-preview-map {
position: relative;
flex: 0 0 auto;
overflow: hidden;
background: #fff;
}
.wms-preview-map[hidden] {
display: none;
}
.wms-preview-map img,
.wms-preview-map svg {
display: block;
width: 100%;
height: 100%;
}
.wms-preview-map svg {
position: absolute;
inset: 0;
pointer-events: none;
}
.wms-preview-parcel {
fill: rgba(251, 191, 36, 0.28);
fill-rule: evenodd;
stroke: #ef4444;
stroke-width: 4px;
vector-effect: non-scaling-stroke;
}
.wms-preview-gps-parcel {
fill: rgba(56, 189, 248, 0.24);
fill-rule: evenodd;
stroke: #1d4ed8;
stroke-width: 4px;
vector-effect: non-scaling-stroke;
}
.bottom-tabs { .bottom-tabs {
position: absolute; position: absolute;
right: 0; right: 0;
@@ -391,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;
@@ -449,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;
@@ -462,6 +778,13 @@ footer {
right: 8px; right: 8px;
} }
.map-layer-panel {
top: 68px;
right: 8px;
width: min(320px, calc(100% - 16px));
max-height: calc(100% - 144px);
}
.tool-sheet { .tool-sheet {
top: 10%; top: 10%;
bottom: var(--nav-height); bottom: var(--nav-height);
@@ -485,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;
@@ -504,6 +843,10 @@ footer {
.bottom-tabs button { .bottom-tabs button {
font-size: 11px; font-size: 11px;
} }
.wms-preview {
padding: 8px;
}
} }
@media (max-width: 380px) { @media (max-width: 380px) {
+1175 -62
View File
File diff suppressed because it is too large. Load diff