Compare commits

..
11 Commits
29 changed files with 13331 additions and 545 deletions

No files matched your search

File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -22,14 +22,14 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Display name --> <!-- Display name -->
<ApplicationTitle>LMEMappeCatastaliApp</ApplicationTitle> <ApplicationTitle>LME Mappe Catastali</ApplicationTitle>
<!-- App Identifier --> <!-- App Identifier -->
<ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId> <ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.2.9</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>13</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged --> <!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType> <WindowsPackageType>None</WindowsPackageType>
@@ -64,6 +64,7 @@
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.9" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.9" />
<PackageReference Include="Parquet.Net" Version="6.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+5
View File
@@ -17,6 +17,11 @@ public static class MauiProgram
builder.Services.AddMauiBlazorWebView(); builder.Services.AddMauiBlazorWebView();
builder.Services.AddSingleton<PreciseLocationService>(); builder.Services.AddSingleton<PreciseLocationService>();
builder.Services.AddSingleton<CadastralCatalogService>();
builder.Services.AddSingleton<OfficialCadastreService>();
builder.Services.AddSingleton<LocationSearchService>();
builder.Services.AddSingleton<AzureMapsService>();
builder.Services.AddSingleton<ProjectStorageService>();
#if DEBUG #if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools(); builder.Services.AddBlazorWebViewDeveloperTools();
+44 -8
View File
@@ -4,19 +4,38 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
## Funzioni gia implementate ## Funzioni gia implementate
- Mappa interattiva Leaflet con sfondo satellitare Esri e sfondo stradale OpenStreetMap. - Mappa interattiva Leaflet con sfondi Esri, OpenStreetMap, OpenTopoMap, CARTO Voyager, Google Maps e Azure Maps configurabili tramite chiave.
- Layer WMS ufficiale Agenzia delle Entrate: particelle, numeri particella, fogli, fabbricati, strade e acque. - Layer WMS ufficiale Agenzia delle Entrate: particelle, numeri particella, fogli, fabbricati, strade e acque.
- 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.
- POI/progetti salvabili in locale con `localStorage`. - 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. - GPS nativo MAUI ad alta precisione con permessi Android/iOS.
- 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.
- Ricerca Comune/Foglio/Particella tramite indice OnData e successivo recupero geometria dal WFS ufficiale AdE. - 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.
- Catalogo dei Comuni incluso nell'APK e cataloghi regionali OnData scaricati in cache solo quando servono.
- Ricerca catastale eseguita dal codice .NET nativo, senza DuckDB WebAssembly nella WebView Android.
- La registrazione della traccia GPS e' disattivata all'avvio; la toolbar di disegno puo' essere mostrata o nascosta dal tab Progetti.
- Pannello `Layer` accessibile direttamente dalla mappa, con checkbox sincronizzati per confini, numeri particella, fogli, fabbricati, strade e acque.
- Le particelle cercate possono essere aggiunte una dopo l'altra, mantenendo contemporaneamente sulla mappa tutti i confini selezionati.
- Comandi separati per rimuovere le particelle selezionate o pulire importazioni, disegni, POI e traccia GPS.
- Anteprima WMS ufficiale mostrata dentro l'app senza aprire una nuova finestra Android.
- 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.
## Limite ufficiale AdE da considerare ## Limite ufficiale AdE da considerare
@@ -26,7 +45,24 @@ Il WFS dell'Agenzia delle Entrate consente `GetFeature` con bounding box, ma non
2. una chiamata WFS con bounding box piccola intorno a quel punto; 2. una chiamata WFS con bounding box piccola intorno a quel punto;
3. filtro lato app sul risultato per selezionare la particella corretta. 3. filtro lato app sul risultato per selezionare la particella corretta.
Questo passaggio e' implementato nella schermata "Particella catastale". Per la massima affidabilita inserisci il codice catastale del Comune, ad esempio `F205` per Milano o `H501` per Roma. Questo passaggio e' implementato nel tab `Catasto`. Regione e Comune sono disponibili subito; alla prima selezione di un Comune l'app scarica il file della regione, da circa 6 a 72 MB, e lo conserva nella cache del dispositivo. Fogli e particelle vengono poi popolati automaticamente.
I layer cartografici pubblici WMS/WFS sono open data e non richiedono CAPTCHA. Il CAPTCHA o l'autenticazione SPID/CIE appartengono ai servizi web di consultazione e download documentale: l'app li apre nel sito ufficiale dell'Agenzia e non tenta di aggirarli.
## Azure Maps Gen2
Nel tab `Impostazioni` sono disponibili gli sfondi `Strade - Azure Maps` e `Satellite - Azure Maps`. Per usarli:
1. crea una risorsa Azure Maps con piano Gen2 nel portale Azure;
2. copia una delle chiavi di sottoscrizione della risorsa;
3. inseriscila in `Chiave sottoscrizione Azure Maps Gen2` e premi `Salva chiave Azure Maps`.
La chiave viene validata prima del salvataggio e rimane nel `SecureStorage` del dispositivo; non e' inclusa nel codice o nell'APK. Lasciando vuoto il campo e premendo il pulsante, la chiave viene rimossa.
La quota gratuita mensile Gen2 indicata da Microsoft comprende 5.000 transazioni per le mappe di base e 1.000 per le immagini satellitari. Una transazione raggruppa fino a 15 richieste di tessere. Oltre la quota si applicano le condizioni economiche della sottoscrizione Azure, quindi e' opportuno configurare budget e avvisi nel portale.
- Creazione account e autenticazione: https://learn.microsoft.com/azure/azure-maps/how-to-manage-account-keys
- Prezzi e quota gratuita: https://azure.microsoft.com/pricing/details/azure-maps/
## Aprire in Visual Studio ## Aprire in Visual Studio
@@ -50,16 +86,16 @@ Per Android:
dotnet build -f net9.0-android -p:AndroidSdkDirectory=C:\Users\livio\AppData\Local\Android\Sdk dotnet build -f net9.0-android -p:AndroidSdkDirectory=C:\Users\livio\AppData\Local\Android\Sdk
``` ```
## APK generato ## APK Release generato
La build Debug Android genera: La pubblicazione Android genera:
```text ```text
C:\Users\livio\Documents\LMEMappeCatastali\LMEMappeCatastaliApp\bin\Debug\net9.0-android\com.companyname.lmemappecatastaliapp-Signed.apk C:\Users\livio\Documents\LMEMappeCatastali\LMEMappeCatastaliApp\bin\Release\net9.0-android\publish\com.companyname.lmemappecatastaliapp-Signed.apk
``` ```
Per installarlo con telefono collegato e debug USB attivo: Per installarlo con telefono collegato e debug USB attivo:
```powershell ```powershell
C:\Users\livio\AppData\Local\Android\Sdk\platform-tools\adb.exe install -r C:\Users\livio\Documents\LMEMappeCatastali\LMEMappeCatastaliApp\bin\Debug\net9.0-android\com.companyname.lmemappecatastaliapp-Signed.apk C:\Users\livio\AppData\Local\Android\Sdk\platform-tools\adb.exe install -r C:\Users\livio\Documents\LMEMappeCatastali\LMEMappeCatastaliApp\bin\Release\net9.0-android\publish\com.companyname.lmemappecatastaliapp-Signed.apk
``` ```
File diff suppressed because it is too large. Load diff
+53
View File
@@ -0,0 +1,53 @@
using System.Text.Json;
namespace LMEMappeCatastaliApp.Services;
public sealed class AzureMapsService
{
private const string TilesetEndpoint = "https://atlas.microsoft.com/map/tileset";
private readonly HttpClient _httpClient = new()
{
Timeout = TimeSpan.FromSeconds(20)
};
private string? _validatedKey;
public AzureMapsService()
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.9");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
}
public async Task ValidateKeyAsync(string subscriptionKey, CancellationToken cancellationToken = default)
{
var key = subscriptionKey.Trim();
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("Chiave Azure Maps mancante.", nameof(subscriptionKey));
}
if (string.Equals(_validatedKey, key, StringComparison.Ordinal))
{
return;
}
var url = $"{TilesetEndpoint}?api-version=2024-04-01&tilesetId=microsoft.base.road";
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("subscription-key", key);
using var response = await _httpClient.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(response.StatusCode is System.Net.HttpStatusCode.Unauthorized or System.Net.HttpStatusCode.Forbidden
? "Chiave Azure Maps non valida o priva dei permessi Render."
: $"Azure Maps ha risposto {((int)response.StatusCode)}.");
}
await using var content = await response.Content.ReadAsStreamAsync(cancellationToken);
using var payload = await JsonDocument.ParseAsync(content, cancellationToken: cancellationToken);
if (!payload.RootElement.TryGetProperty("tiles", out _))
{
throw new InvalidDataException("Azure Maps ha restituito una configurazione non valida.");
}
_validatedKey = key;
}
}
+342
View File
@@ -0,0 +1,342 @@
using System.Text;
using System.Text.Json;
using Parquet;
using Parquet.Data;
using Parquet.Schema;
namespace LMEMappeCatastaliApp.Services;
public sealed class CadastralCatalogService
{
private const string DataBaseUrl = "https://raw.githubusercontent.com/ondata/dati_catastali/main/S_0000_ITALIA/anagrafica/";
private readonly HttpClient _httpClient = new()
{
Timeout = Timeout.InfiniteTimeSpan
};
private readonly SemaphoreSlim _downloadLock = new(1, 1);
private IReadOnlyList<MunicipalityOption>? _municipalities;
public CadastralCatalogService()
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2");
}
public async Task<IReadOnlyList<RegionOption>> GetRegionsAsync()
{
var municipalities = await GetAllMunicipalitiesAsync();
return municipalities
.GroupBy(item => item.DataFile, StringComparer.OrdinalIgnoreCase)
.Select(group => new RegionOption(RegionName(group.Key), group.Key))
.OrderBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
}
public async Task<IReadOnlyList<MunicipalityOption>> GetMunicipalitiesAsync(string dataFile)
{
var municipalities = await GetAllMunicipalitiesAsync();
return municipalities
.Where(item => string.Equals(item.DataFile, dataFile, StringComparison.OrdinalIgnoreCase))
.OrderBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
}
public async Task<IReadOnlyList<string>> GetSheetsAsync(
MunicipalityOption municipality,
IProgress<CatalogProgress>? progress = null,
CancellationToken cancellationToken = default)
{
var path = await EnsureRegionFileAsync(municipality.DataFile, progress, cancellationToken);
progress?.Report(new CatalogProgress("Leggo i fogli del Comune...", null));
var sheets = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
await using var reader = await ParquetReader.CreateAsync(path, cancellationToken: cancellationToken);
var communeField = RequiredField(reader.Schema, "comune");
var sheetField = RequiredField(reader.Schema, "foglio");
for (var groupIndex = 0; groupIndex < reader.RowGroupCount; groupIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
using var group = reader.OpenRowGroupReader(groupIndex);
if (!MayContain(group.GetStatistics(communeField), municipality.Code))
{
continue;
}
var count = checked((int)group.RowCount);
var communes = new string[count];
var sheetValues = new string[count];
await group.ReadAsync(communeField, communes, cancellationToken: cancellationToken);
await group.ReadAsync(sheetField, sheetValues, cancellationToken: cancellationToken);
for (var index = 0; index < count; index++)
{
if (string.Equals(communes[index], municipality.Code, StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrWhiteSpace(sheetValues[index]))
{
sheets.Add(sheetValues[index]);
}
}
}
progress?.Report(new CatalogProgress($"Trovati {sheets.Count} fogli.", 1));
return sheets.OrderBy(value => value, NaturalStringComparer.Instance).ToArray();
}
public async Task<IReadOnlyList<ParcelOption>> GetParcelsAsync(
MunicipalityOption municipality,
string sheet,
IProgress<CatalogProgress>? progress = null,
CancellationToken cancellationToken = default)
{
var path = await EnsureRegionFileAsync(municipality.DataFile, progress, cancellationToken);
progress?.Report(new CatalogProgress("Leggo le particelle del foglio...", null));
var parcels = new Dictionary<string, ParcelOption>(StringComparer.OrdinalIgnoreCase);
await using var reader = await ParquetReader.CreateAsync(path, cancellationToken: cancellationToken);
var communeField = RequiredField(reader.Schema, "comune");
var sheetField = RequiredField(reader.Schema, "foglio");
var parcelField = RequiredField(reader.Schema, "particella");
var longitudeField = RequiredField(reader.Schema, "x");
var latitudeField = RequiredField(reader.Schema, "y");
var inspireIdField = RequiredField(reader.Schema, "INSPIREID_LOCALID");
for (var groupIndex = 0; groupIndex < reader.RowGroupCount; groupIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
using var group = reader.OpenRowGroupReader(groupIndex);
if (!MayContain(group.GetStatistics(communeField), municipality.Code)
|| !MayContain(group.GetStatistics(sheetField), sheet))
{
continue;
}
var count = checked((int)group.RowCount);
var communes = new string[count];
var sheets = new string[count];
await group.ReadAsync(communeField, communes, cancellationToken: cancellationToken);
await group.ReadAsync(sheetField, sheets, cancellationToken: cancellationToken);
var matchingIndexes = Enumerable.Range(0, count)
.Where(index => string.Equals(communes[index], municipality.Code, StringComparison.OrdinalIgnoreCase)
&& string.Equals(sheets[index], sheet, StringComparison.OrdinalIgnoreCase))
.ToArray();
if (matchingIndexes.Length == 0)
{
continue;
}
var parcelValues = new string[count];
var inspireIds = new string[count];
var longitudes = new long?[count];
var latitudes = new long?[count];
await group.ReadAsync(parcelField, parcelValues, cancellationToken: cancellationToken);
await group.ReadAsync(inspireIdField, inspireIds, cancellationToken: cancellationToken);
await group.ReadAsync<long>(longitudeField, longitudes.AsMemory(), cancellationToken: cancellationToken);
await group.ReadAsync<long>(latitudeField, latitudes.AsMemory(), cancellationToken: cancellationToken);
foreach (var index in matchingIndexes)
{
var number = parcelValues[index];
if (string.IsNullOrWhiteSpace(number)
|| parcels.ContainsKey(number)
|| !latitudes[index].HasValue
|| !longitudes[index].HasValue)
{
continue;
}
parcels[number] = new ParcelOption(
number,
latitudes[index]!.Value / 1_000_000d,
longitudes[index]!.Value / 1_000_000d,
inspireIds[index] ?? string.Empty);
}
}
progress?.Report(new CatalogProgress($"Trovate {parcels.Count} particelle.", 1));
return parcels.Values.OrderBy(item => item.Number, NaturalStringComparer.Instance).ToArray();
}
private async Task<IReadOnlyList<MunicipalityOption>> GetAllMunicipalitiesAsync()
{
if (_municipalities is not null)
{
return _municipalities;
}
await using var stream = await FileSystem.Current.OpenAppPackageFileAsync("Data/municipalities.json");
_municipalities = await JsonSerializer.DeserializeAsync<List<MunicipalityOption>>(
stream,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? [];
return _municipalities;
}
private async Task<string> EnsureRegionFileAsync(
string dataFile,
IProgress<CatalogProgress>? progress,
CancellationToken cancellationToken)
{
var safeFileName = Path.GetFileName(dataFile);
var destination = Path.Combine(FileSystem.Current.CacheDirectory, $"catasto-{safeFileName}");
if (IsValidParquetFile(destination))
{
progress?.Report(new CatalogProgress("Uso il catalogo regionale salvato sul telefono.", 1));
return destination;
}
await _downloadLock.WaitAsync(cancellationToken);
try
{
if (IsValidParquetFile(destination))
{
return destination;
}
var temporary = destination + ".download";
if (File.Exists(temporary))
{
File.Delete(temporary);
}
using var response = await _httpClient.GetAsync(
DataBaseUrl + Uri.EscapeDataString(safeFileName),
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
response.EnsureSuccessStatusCode();
var total = response.Content.Headers.ContentLength;
await using var source = await response.Content.ReadAsStreamAsync(cancellationToken);
await using var target = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, 128 * 1024, true);
var buffer = new byte[128 * 1024];
long downloaded = 0;
int read;
while ((read = await source.ReadAsync(buffer, cancellationToken)) > 0)
{
await target.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
downloaded += read;
double? fraction = total > 0 ? (double)downloaded / total.Value : null;
var megabytes = downloaded / 1_048_576d;
progress?.Report(new CatalogProgress($"Scarico dati regionali: {megabytes:0.0} MB", fraction));
}
await target.FlushAsync(cancellationToken);
target.Close();
File.Move(temporary, destination, true);
return destination;
}
catch
{
var temporary = destination + ".download";
if (File.Exists(temporary))
{
File.Delete(temporary);
}
throw;
}
finally
{
_downloadLock.Release();
}
}
private static bool IsValidParquetFile(string path)
{
if (!File.Exists(path) || new FileInfo(path).Length < 12)
{
return false;
}
using var stream = File.OpenRead(path);
Span<byte> marker = stackalloc byte[4];
if (stream.Read(marker) != marker.Length || Encoding.ASCII.GetString(marker) != "PAR1")
{
return false;
}
stream.Seek(-4, SeekOrigin.End);
return stream.Read(marker) == marker.Length && Encoding.ASCII.GetString(marker) == "PAR1";
}
private static DataField RequiredField(ParquetSchema schema, string name) =>
schema.FindDataField(name) ?? throw new InvalidDataException($"Colonna catastale mancante: {name}");
private static bool MayContain(DataColumnStatistics? statistics, string wanted)
{
if (statistics is null)
{
return true;
}
var minimum = StatisticText(statistics.MinValue);
var maximum = StatisticText(statistics.MaxValue);
return minimum is null || maximum is null
|| (string.Compare(wanted, minimum, StringComparison.OrdinalIgnoreCase) >= 0
&& string.Compare(wanted, maximum, StringComparison.OrdinalIgnoreCase) <= 0);
}
private static string? StatisticText(object? value) => value switch
{
null => null,
string text => text,
byte[] bytes => Encoding.UTF8.GetString(bytes),
ReadOnlyMemory<byte> memory => Encoding.UTF8.GetString(memory.Span),
_ => value.ToString()
};
private static string RegionName(string dataFile)
{
var key = Path.GetFileNameWithoutExtension(dataFile);
return key switch
{
"01_Piemonte" => "Piemonte",
"02_ValledAosta" => "Valle d'Aosta",
"03_Lombardia" => "Lombardia",
"05_Veneto" => "Veneto",
"06_Friuli-VeneziaGiulia" => "Friuli-Venezia Giulia",
"07_Liguria" => "Liguria",
"08_Emilia-Romagna" => "Emilia-Romagna",
"09_Toscana" => "Toscana",
"10_Umbria" => "Umbria",
"11_Marche" => "Marche",
"12_Lazio" => "Lazio",
"13_Abruzzo" => "Abruzzo",
"14_Molise" => "Molise",
"15_Campania" => "Campania",
"16_Puglia" => "Puglia",
"17_Basilicata" => "Basilicata",
"18_Calabria" => "Calabria",
"19_Sicilia" => "Sicilia",
"20_Sardegna" => "Sardegna",
_ => key.Length > 3 ? key[3..] : key
};
}
}
public sealed record RegionOption(string Name, string DataFile);
public sealed record MunicipalityOption(string Code, string Name, string DataFile);
public sealed record ParcelOption(string Number, double Latitude, double Longitude, string InspireId);
public sealed record CatalogProgress(string Message, double? Fraction);
internal sealed class NaturalStringComparer : IComparer<string>
{
public static NaturalStringComparer Instance { get; } = new();
public int Compare(string? left, string? right)
{
if (ReferenceEquals(left, right)) return 0;
if (left is null) return -1;
if (right is null) return 1;
var leftNumber = long.TryParse(left, out var leftValue);
var rightNumber = long.TryParse(right, out var rightValue);
if (leftNumber && rightNumber)
{
var numeric = leftValue.CompareTo(rightValue);
return numeric != 0 ? numeric : string.Compare(left, right, StringComparison.OrdinalIgnoreCase);
}
return string.Compare(left, right, StringComparison.CurrentCultureIgnoreCase);
}
}
+185
View File
@@ -0,0 +1,185 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Maui.Devices.Sensors;
namespace LMEMappeCatastaliApp.Services;
public sealed class LocationSearchService
{
private const string NominatimEndpoint = "https://nominatim.openstreetmap.org/search";
private const string ArcGisEndpoint = "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates";
private readonly HttpClient _httpClient = new()
{
Timeout = TimeSpan.FromSeconds(20)
};
private readonly SemaphoreSlim _requestLock = new(1, 1);
private readonly Dictionary<string, LocationSearchResult?> _cache = new(StringComparer.OrdinalIgnoreCase);
private DateTimeOffset _lastRemoteRequest = DateTimeOffset.MinValue;
public LocationSearchService()
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.2");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
_httpClient.DefaultRequestHeaders.AcceptLanguage.ParseAdd("it-IT");
}
public async Task<LocationSearchResult?> SearchAsync(string text, CancellationToken cancellationToken = default)
{
var query = text.Trim();
if (string.IsNullOrWhiteSpace(query))
{
return null;
}
if (TryParseCoordinates(query, out var coordinates))
{
return coordinates;
}
if (_cache.TryGetValue(query, out var cached))
{
return cached;
}
var nativeResult = await TryNativeGeocodingAsync(query);
if (nativeResult is not null)
{
_cache[query] = nativeResult;
return nativeResult;
}
var arcGisResult = await SearchArcGisAsync(query, cancellationToken);
if (arcGisResult is not null)
{
return arcGisResult;
}
var remoteResult = await SearchNominatimAsync(query, cancellationToken);
_cache[query] = remoteResult;
return remoteResult;
}
private async Task<LocationSearchResult?> SearchArcGisAsync(string query, CancellationToken cancellationToken)
{
try
{
var url = $"{ArcGisEndpoint}?f=json&forStorage=false&sourceCountry=ITA&maxLocations=1&outFields=Match_addr,Addr_type,Score&SingleLine={Uri.EscapeDataString(query)}";
using var response = await _httpClient.GetAsync(url, cancellationToken);
if (!response.IsSuccessStatusCode)
{
return null;
}
await using var content = await response.Content.ReadAsStreamAsync(cancellationToken);
var payload = await JsonSerializer.DeserializeAsync<ArcGisResponse>(content, cancellationToken: cancellationToken);
var candidate = payload?.Candidates?.FirstOrDefault();
if (candidate?.Location is null || candidate.Score < 60)
{
return null;
}
return new LocationSearchResult(candidate.Location.Y, candidate.Location.X, candidate.Address ?? query);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception)
{
return null;
}
}
private static async Task<LocationSearchResult?> TryNativeGeocodingAsync(string query)
{
try
{
var locations = await Geocoding.Default.GetLocationsAsync(query);
var location = locations?.FirstOrDefault();
return location is null
? null
: new LocationSearchResult(location.Latitude, location.Longitude, query);
}
catch (Exception)
{
return null;
}
}
private async Task<LocationSearchResult?> SearchNominatimAsync(string query, CancellationToken cancellationToken)
{
await _requestLock.WaitAsync(cancellationToken);
try
{
var wait = TimeSpan.FromSeconds(1) - (DateTimeOffset.UtcNow - _lastRemoteRequest);
if (wait > TimeSpan.Zero)
{
await Task.Delay(wait, cancellationToken);
}
var url = $"{NominatimEndpoint}?format=jsonv2&limit=1&accept-language=it&q={Uri.EscapeDataString(query)}";
_lastRemoteRequest = DateTimeOffset.UtcNow;
using var response = await _httpClient.GetAsync(url, cancellationToken);
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"Il servizio di ricerca ha risposto {((int)response.StatusCode)}.");
}
await using var content = await response.Content.ReadAsStreamAsync(cancellationToken);
var results = await JsonSerializer.DeserializeAsync<List<NominatimResult>>(content, cancellationToken: cancellationToken);
var result = results?.FirstOrDefault();
if (result is null
|| !double.TryParse(result.Latitude, NumberStyles.Float, CultureInfo.InvariantCulture, out var latitude)
|| !double.TryParse(result.Longitude, NumberStyles.Float, CultureInfo.InvariantCulture, out var longitude))
{
return null;
}
return new LocationSearchResult(latitude, longitude, result.DisplayName ?? query);
}
finally
{
_requestLock.Release();
}
}
private static bool TryParseCoordinates(string query, out LocationSearchResult? result)
{
result = null;
var parts = query.Split([' ', ',', ';'], StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2
|| !double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var latitude)
|| !double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var longitude)
|| Math.Abs(latitude) > 90
|| Math.Abs(longitude) > 180)
{
return false;
}
result = new LocationSearchResult(
latitude,
longitude,
$"Coordinate: {latitude:F6}, {longitude:F6}");
return true;
}
private sealed record NominatimResult(
[property: JsonPropertyName("lat")] string? Latitude,
[property: JsonPropertyName("lon")] string? Longitude,
[property: JsonPropertyName("display_name")] string? DisplayName);
private sealed record ArcGisResponse(
[property: JsonPropertyName("candidates")] List<ArcGisCandidate>? Candidates);
private sealed record ArcGisCandidate(
[property: JsonPropertyName("address")] string? Address,
[property: JsonPropertyName("score")] double Score,
[property: JsonPropertyName("location")] ArcGisLocation? Location);
private sealed record ArcGisLocation(
[property: JsonPropertyName("x")] double X,
[property: JsonPropertyName("y")] double Y);
}
public sealed record LocationSearchResult(double Latitude, double Longitude, string Label);
+85
View File
@@ -0,0 +1,85 @@
using System.Globalization;
using System.Xml.Linq;
namespace LMEMappeCatastaliApp.Services;
public sealed class OfficialCadastreService
{
private const string WfsEndpoint = "https://wfs.cartografia.agenziaentrate.gov.it/inspire/wfs/owfs01.php";
private readonly HttpClient _httpClient = new()
{
Timeout = TimeSpan.FromSeconds(30)
};
public OfficialCadastreService()
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/gml+xml");
}
public async Task<string> GetParcelsNearAsync(
double latitude,
double longitude,
double radiusMeters,
CancellationToken cancellationToken = default)
{
var meters = Math.Clamp(radiusMeters, 2, 30);
var deltaLatitude = meters / 111_320d;
var longitudeScale = Math.Cos(latitude * Math.PI / 180d);
var deltaLongitude = meters / (111_320d * Math.Max(0.1, longitudeScale));
var bbox = string.Join(",",
[
Format(latitude - deltaLatitude),
Format(longitude - deltaLongitude),
Format(latitude + deltaLatitude),
Format(longitude + deltaLongitude)
]);
var query = new Dictionary<string, string>
{
["language"] = "ita",
["SERVICE"] = "WFS",
["VERSION"] = "2.0.0",
["TYPENAMES"] = "CP:CadastralParcel",
["SRSNAME"] = "urn:ogc:def:crs:EPSG::6706",
["BBOX"] = bbox,
["REQUEST"] = "GetFeature",
["COUNT"] = "20"
};
var url = WfsEndpoint + "?" + string.Join("&", query.Select(item =>
$"{Uri.EscapeDataString(item.Key)}={Uri.EscapeDataString(item.Value)}"));
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseContentRead, cancellationToken);
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"Il servizio WFS AdE ha risposto {((int)response.StatusCode)}.");
}
var xml = await response.Content.ReadAsStringAsync(cancellationToken);
ThrowIfExceptionReport(xml);
return xml;
}
private static string Format(double value) => value.ToString("R", CultureInfo.InvariantCulture);
private static void ThrowIfExceptionReport(string xml)
{
try
{
var document = XDocument.Parse(xml);
var exceptionText = document.Descendants()
.FirstOrDefault(element => element.Name.LocalName == "ExceptionText")
?.Value
.Trim();
if (!string.IsNullOrWhiteSpace(exceptionText))
{
throw new InvalidOperationException($"Servizio WFS AdE: {exceptionText}");
}
}
catch (System.Xml.XmlException error)
{
throw new InvalidDataException("Il servizio WFS AdE ha restituito una risposta non valida.", error);
}
}
}
+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);
+722 -139
View File
@@ -1,14 +1,32 @@
:root {
--ink: #17212b;
--muted: #63717c;
--surface: #ffffff;
--panel: #f4f6f7;
--line: #d6dde2;
--accent: #14705e;
--accent-strong: #0c5749;
--warning: #a34b08;
--nav-height: 64px;
}
html, html,
body, body,
#app, #app,
.app-shell { .app-shell,
.map-app,
.map-stage {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
}
html,
body {
font-family: Inter, "Segoe UI", Arial, sans-serif; font-family: Inter, "Segoe UI", Arial, sans-serif;
background: #101820; background: #d8e0e4;
color: #17212b; color: var(--ink);
} }
button, button,
@@ -18,28 +36,32 @@ select {
} }
button { button {
min-height: 38px; min-height: 42px;
border: 0; border: 0;
border-radius: 6px; border-radius: 6px;
padding: 0 12px; padding: 0 14px;
background: #176b87; background: var(--accent);
color: #fff; color: #fff;
font-weight: 700; font-weight: 700;
} }
button:active { button:active:not(:disabled) {
transform: translateY(1px); transform: translateY(1px);
} }
button:disabled {
opacity: 0.48;
}
input, input,
select { select {
width: 100%; width: 100%;
min-height: 38px; min-height: 44px;
border: 1px solid #c8d3dc; border: 1px solid #bfcbd2;
border-radius: 6px; border-radius: 6px;
padding: 0 10px; padding: 0 11px;
background: #fff; background: #fff;
color: #17212b; color: var(--ink);
} }
input[type="range"], input[type="range"],
@@ -47,35 +69,294 @@ input[type="checkbox"] {
min-height: auto; min-height: auto;
} }
.map-app { .map-stage {
display: grid; position: relative;
grid-template-columns: 390px minmax(0, 1fr);
width: 100%;
height: 100%;
} }
.tool-panel { #map {
z-index: 800; position: absolute;
inset: 0;
background: #d8e0e4;
}
.map-toolbar {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
z-index: 900;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 52px;
padding: 7px 8px 7px 13px;
border: 1px solid rgba(255, 255, 255, 0.54);
border-radius: 8px;
background: rgba(22, 31, 38, 0.9);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.22);
color: #fff;
}
.app-identity {
display: flex;
align-items: baseline;
gap: 10px;
min-width: 0;
}
.app-identity strong {
font-size: 18px;
white-space: nowrap;
}
.app-identity span {
overflow: hidden;
color: #bfe6dc;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.toolbar-button {
min-height: 38px;
background: #fff;
color: var(--ink);
}
.toolbar-actions,
.layer-button {
display: flex;
align-items: center;
gap: 7px;
}
.toolbar-actions {
flex: 0 0 auto;
}
.layer-button.is-active {
background: #ccebe3;
color: var(--accent-strong);
}
.layer-symbol {
width: 20px;
height: 20px;
background: url("../lib/leaflet/images/layers.png") center / contain no-repeat;
}
.map-search {
position: absolute;
top: 72px;
left: 12px;
right: 12px;
z-index: 890;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
max-width: 640px;
padding: 8px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.22);
opacity: 0;
pointer-events: none;
transform: translateY(-8px);
transition: opacity 140ms ease, transform 140ms ease;
}
.map-search.is-open {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.map-layer-panel {
position: absolute;
top: 72px;
right: 12px;
z-index: 940;
display: grid;
width: min(320px, calc(100% - 24px));
max-height: calc(100% - 152px);
overflow-y: auto;
border: 1px solid var(--line);
border-radius: 8px;
padding: 9px;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.22);
}
.map-layer-panel > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 0 7px 4px;
border-bottom: 1px solid var(--line);
}
.layer-panel-close {
width: 36px;
min-width: 36px;
min-height: 34px;
padding: 0;
background: #e9eef0;
color: var(--ink);
font-size: 23px;
}
.map-layer-option {
display: flex;
align-items: center;
gap: 9px;
min-height: 45px;
padding: 6px 4px;
border-bottom: 1px solid #e5eaed;
}
.map-layer-option input {
flex: 0 0 auto;
width: 19px;
height: 19px;
}
.map-layer-option span {
display: grid;
gap: 2px;
}
.map-layer-option small {
color: var(--muted);
font-size: 10px;
font-weight: 600;
}
.layer-panel-actions {
display: grid;
gap: 7px;
padding-top: 9px;
}
.gps-overlay {
position: absolute;
top: 72px;
right: auto;
bottom: auto;
left: 12px;
z-index: 850;
display: grid;
grid-template-columns: minmax(160px, auto) auto auto;
align-items: center;
gap: 7px;
max-width: calc(100% - 24px);
padding: 7px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.2);
transition: top 140ms ease;
}
.gps-overlay.search-open {
top: 140px;
}
.gps-state {
display: grid;
min-width: 0;
padding: 0 6px;
font-size: 11px;
}
.gps-state span,
.gps-state strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gps-state strong {
color: #a33227;
}
.gps-main,
.gps-center {
min-height: 38px;
padding: 0 10px;
white-space: nowrap;
}
.gps-center {
background: #e7ecef;
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;
bottom: var(--nav-height);
left: 0;
z-index: 1100;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; width: min(430px, calc(100% - 48px));
overflow-y: auto; border-right: 1px solid var(--line);
padding: 16px; background: var(--panel);
background: #f6f8f9; box-shadow: 7px 0 24px rgba(0, 0, 0, 0.24);
border-right: 1px solid #d8e0e6;
} }
.brand { .sheet-header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 14px;
padding: 18px 16px 13px;
border-bottom: 1px solid var(--line);
background: #fff;
} }
.eyebrow { .eyebrow {
margin: 0 0 3px; margin: 0 0 4px;
color: #667785; color: var(--muted);
font-size: 12px; font-size: 11px;
font-weight: 800; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;
} }
@@ -88,74 +369,98 @@ p {
h1 { h1 {
margin-bottom: 0; margin-bottom: 0;
font-size: 25px; font-size: 22px;
line-height: 1.08; line-height: 1.12;
} }
h2 { h2 {
margin-bottom: 10px; margin: 10px 0 2px;
font-size: 15px; font-size: 15px;
} }
.status { .close-button {
border-radius: 999px; width: 42px;
padding: 5px 9px; min-width: 42px;
background: #dff5e5; padding: 0;
color: #166534; background: #e9eef0;
font-size: 12px; color: var(--ink);
font-weight: 800; font-size: 27px;
line-height: 1;
}
.panel-content {
display: grid;
gap: 12px;
overflow-y: auto;
padding: 15px 16px 24px;
} }
.panel-section { .panel-section {
padding: 12px;
border: 1px solid #dbe4ea;
border-radius: 8px;
background: #fff;
}
.input-row,
.button-row {
display: flex;
gap: 8px;
}
.input-row input {
min-width: 0;
}
.field-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; gap: 12px;
gap: 10px; padding: 0 0 16px;
border-bottom: 1px solid var(--line);
} }
.field-grid.compact { .panel-section:last-child {
grid-template-columns: 1fr; 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;
color: #475866; color: #45545f;
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 800;
} }
.hint { .button-row {
margin: 10px 0 0; display: flex;
color: #667785; gap: 8px;
font-size: 12px; }
line-height: 1.35;
.button-row > * {
flex: 1;
}
.primary-command,
.secondary-command {
width: 100%;
}
.secondary-command {
border: 1px solid #b9c7ce;
background: #fff;
color: var(--ink);
}
.danger-command {
width: 100%;
border: 1px solid #cfaca8;
background: #fff;
color: #9b2f26;
} }
.file-picker { .file-picker {
position: relative; position: relative;
min-height: 42px; min-height: 52px;
place-items: center; place-items: center;
border: 1px dashed #93a7b5; border: 1px dashed #7e969f;
border-radius: 8px; border-radius: 8px;
background: #f2f7fa; background: #fff;
color: #176b87; color: var(--accent-strong);
cursor: pointer; cursor: pointer;
} }
@@ -166,120 +471,398 @@ label {
cursor: pointer; cursor: pointer;
} }
.catalog-progress,
.service-note {
display: grid;
gap: 7px;
padding: 11px;
border: 1px solid #c7dce0;
border-radius: 8px;
background: #edf7f5;
color: #24443e;
font-size: 12px;
line-height: 1.4;
}
.catalog-progress progress {
width: 100%;
accent-color: var(--accent);
}
.service-note p {
margin-bottom: 0;
}
.protected-service {
border-color: #ead4b9;
background: #fff7ea;
color: #5d4024;
}
.layer-toggles { .layer-toggles {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 8px; gap: 9px;
margin-top: 10px; padding: 4px 0;
} }
.layer-toggles label, .layer-toggles label,
.toggle-line { .toggle-line {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
}
.layer-toggles input,
.toggle-line input {
width: 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; gap: 7px;
} }
.gps-readout { .measurement-mode-grid button {
display: grid;
gap: 6px;
margin-bottom: 10px;
padding: 10px;
border-radius: 8px;
background: #eef6ff;
color: #21405d;
font-size: 13px;
}
.gps-readout strong {
color: #b91c1c;
}
.map-stage {
position: relative;
min-width: 0; min-width: 0;
height: 100%; padding: 0 8px;
} }
#map { .lme-measure-label {
width: 100%; width: 0 !important;
height: 100%; height: 0 !important;
background: #d4dde3; 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: 14px; left: 12px;
right: 14px; bottom: 76px;
bottom: 14px; z-index: 820;
z-index: 700; max-width: min(620px, calc(100% - 460px));
max-width: 760px;
border-radius: 8px; border-radius: 8px;
padding: 10px 12px; padding: 9px 11px;
background: rgba(16, 24, 32, 0.86); background: rgba(22, 31, 38, 0.88);
color: #fff; color: #fff;
font-size: 13px; font-size: 12px;
pointer-events: none; pointer-events: none;
} }
footer { .wms-preview[hidden] {
color: #667785; 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 {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 1200;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
height: var(--nav-height);
padding-bottom: env(safe-area-inset-bottom);
border-top: 1px solid #bcc7cd;
background: rgba(255, 255, 255, 0.98);
}
.bottom-tabs button {
min-width: 0;
min-height: 0;
border-radius: 0;
padding: 0 5px;
background: transparent;
color: #52616c;
font-size: 12px; font-size: 12px;
line-height: 1.35; }
.bottom-tabs button.active {
box-shadow: inset 0 3px 0 var(--accent);
color: var(--accent-strong);
}
footer {
margin-top: 4px;
color: var(--muted);
font-size: 11px;
line-height: 1.4;
}
.leaflet-bottom {
bottom: calc(var(--nav-height) + 6px);
} }
#blazor-error-ui { #blazor-error-ui {
background: #fff3cd;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.7rem 1.25rem;
position: fixed; position: fixed;
width: 100%; right: 8px;
z-index: 1000; bottom: calc(var(--nav-height) + 8px);
left: 8px;
z-index: 2000;
display: none;
border: 1px solid #e4be77;
border-radius: 8px;
padding: 10px 42px 10px 12px;
background: #fff3cd;
color: #5f4614;
} }
#blazor-error-ui .dismiss { #blazor-error-ui .dismiss {
cursor: pointer;
position: absolute; position: absolute;
right: 0.75rem; top: 7px;
top: 0.5rem; right: 10px;
cursor: pointer;
} }
.status-bar-safe-area { .status-bar-safe-area {
display: none; display: none;
} }
@media (max-width: 850px) { @media (min-width: 701px) and (max-width: 900px) {
.map-app { .map-measure-tools {
grid-template-columns: 1fr; top: 126px;
grid-template-rows: 45% 55%;
} }
.tool-panel { .map-measure-tools.search-open {
border-right: 0; top: 194px;
border-bottom: 1px solid #d8e0e6;
padding: 12px;
}
.brand {
align-items: center;
}
h1 {
font-size: 22px;
} }
} }
@supports (-webkit-touch-callout: none) { @media (max-width: 700px) {
.status-bar-safe-area { .map-toolbar {
display: flex; top: 8px;
position: sticky; left: 8px;
top: 0; right: 8px;
height: env(safe-area-inset-top); }
background-color: #f6f8f9;
.map-search {
top: 68px;
left: 8px;
right: 8px;
}
.map-layer-panel {
top: 68px;
right: 8px;
width: min(320px, calc(100% - 16px));
max-height: calc(100% - 144px);
}
.tool-sheet {
top: 10%;
bottom: var(--nav-height);
width: 100%; width: 100%;
z-index: 1; border-top: 1px solid var(--line);
border-right: 0;
border-radius: 8px 8px 0 0;
box-shadow: 0 -7px 24px rgba(0, 0, 0, 0.24);
}
.gps-overlay {
top: 68px;
right: 8px;
bottom: auto;
left: 8px;
grid-template-columns: minmax(0, 1fr) auto auto;
max-width: none;
}
.gps-overlay.search-open {
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;
font-size: 11px;
}
.map-info {
right: 8px;
bottom: 72px;
left: 8px;
max-width: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bottom-tabs button {
font-size: 11px;
}
.wms-preview {
padding: 8px;
}
}
@media (max-width: 380px) {
.app-identity span {
display: none;
}
.gps-state {
min-width: 0;
}
.gps-center {
display: none;
}
.bottom-tabs button {
font-size: 10px;
} }
} }
+7 -6
View File
@@ -6,8 +6,8 @@
<title>LME Mappe Catastali</title> <title>LME Mappe Catastali</title>
<base href="/" /> <base href="/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="lib/leaflet/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" /> <link rel="stylesheet" href="lib/leaflet-draw/leaflet.draw.css" />
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="LMEMappeCatastaliApp.styles.css" /> <link rel="stylesheet" href="LMEMappeCatastaliApp.styles.css" />
<link rel="icon" href="data:,"> <link rel="icon" href="data:,">
@@ -26,10 +26,11 @@
</div> </div>
<script src="_framework/blazor.webview.js" autostart="false"></script> <script src="_framework/blazor.webview.js" autostart="false"></script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="lib/leaflet/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script> <script src="lib/leaflet-draw/leaflet.draw.js"></script>
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js"></script> <script src="lib/googlemutant/Leaflet.GoogleMutant.js"></script>
<script src="https://unpkg.com/@tmcw/togeojson@5.8.1/dist/togeojson.umd.js"></script> <script src="lib/jszip/jszip.min.js"></script>
<script src="lib/togeojson/togeojson.umd.js"></script>
<script src="js/lme-map.js"></script> <script src="js/lme-map.js"></script>
</body> </body>
+1434 -276
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,787 @@
this.L = this.L || {};
this.L.GridLayer = this.L.GridLayer || {};
this.L.GridLayer.GoogleMutant = (function (leaflet) {
"use strict";
/**
* GoogleMutant by Iván Sánchez Ortega <ivan@sanchezortega.es> https://ivan.sanchezortega.es
* Source and issue tracking: https://gitlab.com/IvanSanchez/Leaflet.GridLayer.GoogleMutant/
*
* Based on techniques from https://github.com/shramov/leaflet-plugins
* and https://avinmathew.com/leaflet-and-google-maps/ , but relying on MutationObserver.
*
* "THE BEER-WARE LICENSE":
* <ivan@sanchezortega.es> wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return.
*
* Uses MIT-licensed code from https://github.com/rsms/js-lru/
*/
// This implementation of LRUMap is a copy of https://github.com/rsms/js-lru/ ,
// trivially adapted for ES6 exports.
/*
The MIT License
Copyright (c) 2010-2020 Rasmus Andersson <https://rsms.me/>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
/**
* A doubly linked list-based Least Recently Used (LRU) cache. Will keep most
* recently used items while discarding least recently used items when its limit
* is reached.
*
* Licensed under MIT. Copyright (c) 2010 Rasmus Andersson <http://hunch.se/>
* See README.md for details.
*
* Illustration of the design:
*
* entry entry entry entry
* ______ ______ ______ ______
* | head |.newer => | |.newer => | |.newer => | tail |
* | A | | B | | C | | D |
* |______| <= older.|______| <= older.|______| <= older.|______|
*
* removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added
*/
const NEWER = Symbol("newer");
const OLDER = Symbol("older");
class LRUMap {
constructor(limit, entries) {
if (typeof limit !== "number") {
// called as (entries)
entries = limit;
limit = 0;
}
this.size = 0;
this.limit = limit;
this.oldest = this.newest = undefined;
this._keymap = new Map();
if (entries) {
this.assign(entries);
if (limit < 1) {
this.limit = this.size;
}
}
}
_markEntryAsUsed(entry) {
if (entry === this.newest) {
// Already the most recenlty used entry, so no need to update the list
return;
}
// HEAD--------------TAIL
// <.older .newer>
// <--- add direction --
// A B C <D> E
if (entry[NEWER]) {
if (entry === this.oldest) {
this.oldest = entry[NEWER];
}
entry[NEWER][OLDER] = entry[OLDER]; // C <-- E.
}
if (entry[OLDER]) {
entry[OLDER][NEWER] = entry[NEWER]; // C. --> E
}
entry[NEWER] = undefined; // D --x
entry[OLDER] = this.newest; // D. --> E
if (this.newest) {
this.newest[NEWER] = entry; // E. <-- D
}
this.newest = entry;
}
assign(entries) {
let entry,
limit = this.limit || Number.MAX_VALUE;
this._keymap.clear();
let it = entries[Symbol.iterator]();
for (let itv = it.next(); !itv.done; itv = it.next()) {
let e = new Entry(itv.value[0], itv.value[1]);
this._keymap.set(e.key, e);
if (!entry) {
this.oldest = e;
} else {
entry[NEWER] = e;
e[OLDER] = entry;
}
entry = e;
if (limit-- == 0) {
throw new Error("overflow");
}
}
this.newest = entry;
this.size = this._keymap.size;
}
get(key) {
// First, find our cache entry
var entry = this._keymap.get(key);
if (!entry) return; // Not cached. Sorry.
// As <key> was found in the cache, register it as being requested recently
this._markEntryAsUsed(entry);
return entry.value;
}
set(key, value) {
var entry = this._keymap.get(key);
if (entry) {
// update existing
entry.value = value;
this._markEntryAsUsed(entry);
return this;
}
// new entry
this._keymap.set(key, (entry = new Entry(key, value)));
if (this.newest) {
// link previous tail to the new tail (entry)
this.newest[NEWER] = entry;
entry[OLDER] = this.newest;
} else {
// we're first in -- yay
this.oldest = entry;
}
// add new entry to the end of the linked list -- it's now the freshest entry.
this.newest = entry;
++this.size;
if (this.size > this.limit) {
// we hit the limit -- remove the head
this.shift();
}
return this;
}
shift() {
// todo: handle special case when limit == 1
var entry = this.oldest;
if (entry) {
if (this.oldest[NEWER]) {
// advance the list
this.oldest = this.oldest[NEWER];
this.oldest[OLDER] = undefined;
} else {
// the cache is exhausted
this.oldest = undefined;
this.newest = undefined;
}
// Remove last strong reference to <entry> and remove links from the purged
// entry being returned:
entry[NEWER] = entry[OLDER] = undefined;
this._keymap.delete(entry.key);
--this.size;
return [entry.key, entry.value];
}
}
// -------------------------------------------------------------------------------------
// Following code (until end of class definition) is optional and can be removed without
// breaking the core functionality.
find(key) {
let e = this._keymap.get(key);
return e ? e.value : undefined;
}
has(key) {
return this._keymap.has(key);
}
delete(key) {
var entry = this._keymap.get(key);
if (!entry) return;
this._keymap.delete(entry.key);
if (entry[NEWER] && entry[OLDER]) {
// relink the older entry with the newer entry
entry[OLDER][NEWER] = entry[NEWER];
entry[NEWER][OLDER] = entry[OLDER];
} else if (entry[NEWER]) {
// remove the link to us
entry[NEWER][OLDER] = undefined;
// link the newer entry to head
this.oldest = entry[NEWER];
} else if (entry[OLDER]) {
// remove the link to us
entry[OLDER][NEWER] = undefined;
// link the newer entry to head
this.newest = entry[OLDER];
} else {
// if(entry[OLDER] === undefined && entry.newer === undefined) {
this.oldest = this.newest = undefined;
}
this.size--;
return entry.value;
}
clear() {
// Not clearing links should be safe, as we don't expose live links to user
this.oldest = this.newest = undefined;
this.size = 0;
this._keymap.clear();
}
keys() {
return new KeyIterator(this.oldest);
}
values() {
return new ValueIterator(this.oldest);
}
entries() {
return this;
}
[Symbol.iterator]() {
return new EntryIterator(this.oldest);
}
forEach(fun, thisObj) {
if (typeof thisObj !== "object") {
thisObj = this;
}
let entry = this.oldest;
while (entry) {
fun.call(thisObj, entry.value, entry.key, this);
entry = entry[NEWER];
}
}
/** Returns a JSON (array) representation */
toJSON() {
var s = new Array(this.size),
i = 0,
entry = this.oldest;
while (entry) {
s[i++] = { key: entry.key, value: entry.value };
entry = entry[NEWER];
}
return s;
}
/** Returns a String representation */
toString() {
var s = "",
entry = this.oldest;
while (entry) {
s += String(entry.key) + ":" + entry.value;
entry = entry[NEWER];
if (entry) {
s += " < ";
}
}
return s;
}
}
function Entry(key, value) {
this.key = key;
this.value = value;
this[NEWER] = undefined;
this[OLDER] = undefined;
}
function EntryIterator(oldestEntry) {
this.entry = oldestEntry;
}
EntryIterator.prototype[Symbol.iterator] = function () {
return this;
};
EntryIterator.prototype.next = function () {
let ent = this.entry;
if (ent) {
this.entry = ent[NEWER];
return { done: false, value: [ent.key, ent.value] };
} else {
return { done: true, value: undefined };
}
};
function KeyIterator(oldestEntry) {
this.entry = oldestEntry;
}
KeyIterator.prototype[Symbol.iterator] = function () {
return this;
};
KeyIterator.prototype.next = function () {
let ent = this.entry;
if (ent) {
this.entry = ent[NEWER];
return { done: false, value: ent.key };
} else {
return { done: true, value: undefined };
}
};
function ValueIterator(oldestEntry) {
this.entry = oldestEntry;
}
ValueIterator.prototype[Symbol.iterator] = function () {
return this;
};
ValueIterator.prototype.next = function () {
let ent = this.entry;
if (ent) {
this.entry = ent[NEWER];
return { done: false, value: ent.value };
} else {
return { done: true, value: undefined };
}
};
// GoogleMutant by Iván Sánchez Ortega <ivan@sanchezortega.es>
function waitForAPI(callback, context) {
let checkCounter = 0,
intervalId = null;
intervalId = setInterval(function () {
if (checkCounter >= 20) {
clearInterval(intervalId);
throw new Error("window.google not found after 10 seconds");
}
if (!!window.google && !!window.google.maps && !!window.google.maps.Map) {
clearInterval(intervalId);
callback.call(context);
}
++checkCounter;
}, 500);
}
// 🍂class GridLayer.GoogleMutant
// 🍂extends GridLayer
class GoogleMutant extends leaflet.GridLayer {
static {
// Try to set default option values the Leaflet 2 way
if (this.setDefaultOptions) {
this.setDefaultOptions({
maxZoom: 21, // can be 23, but ugly if more than maxNativeZoom
// 🍂option type: String = 'roadmap'
// Google's map type. Valid values are 'roadmap', 'satellite', 'terrain' or 'hybrid'.
type: "roadmap",
maxNativeZoom: 21,
});
} else {
// If Leaflet v2 is not available, fall back.
this.options ??= {};
this.options.maxZoom ??= 21;
this.options.type ??= "roadmap";
this.options.maxNativeZoom ??= 21;
}
}
constructor(options) {
super(options);
// Couple data structures indexed by tile key
this._tileCallbacks = {}; // Callbacks for promises for tiles that are expected
this._lru = new LRUMap(100); // Tile LRU cache
// Before 2024(ish), Google used two tiles for hybrid mode, reusing
// the aerial tiles. This seems to be no longer the case.
// this._imagesPerTile = this.options.type === "hybrid" ? 2 : 1;
this._imagesPerTile = 1;
this._boundOnMutatedImage = this._onMutatedImage.bind(this);
}
onAdd(map) {
super.onAdd(map);
this._initMutantContainer();
// Attribution and logo nodes are not mutated a second time if the
// mutant is removed and re-added to the map, hence they are
// not cleaned up on layer removal, so they can be added here.
if (this._logoContainer) {
map._controlCorners.bottomleft.appendChild(this._logoContainer);
}
if (this._attributionContainer) {
map._controlCorners.bottomright.appendChild(this._attributionContainer);
}
waitForAPI(() => {
if (!this._map) {
return;
}
this._initMutant();
//handle layer being added to a map for which there are no Google tiles at the given zoom
google.maps.event.addListenerOnce(this._mutant, "idle", () => {
if (!this._map) {
return;
}
this._checkZoomLevels();
this._mutantIsReady = true;
});
});
}
onRemove(map) {
super.onRemove(map);
this._observer.disconnect();
map._container.removeChild(this._mutantContainer);
this._logoContainer?.remove();
this._attributionContainer?.remove();
if (this._mutant) {
google.maps.event.clearListeners(this._mutant, "idle");
}
}
// 🍂method addGoogleLayer(name: String, options?: Object): this
// Adds layer with the given name and options to the google Map instance.
// `name`: one of the google maps API layers, with it's constructor available in `google.maps` object.
// currently following values supported: 'TrafficLayer', 'TransitLayer', 'BicyclingLayer'.
// `options`: see https://developers.google.com/maps/documentation/javascript/reference/map
addGoogleLayer(googleLayerName, options) {
if (!this._subLayers) this._subLayers = {};
this.whenReady(() => {
var Constructor = google.maps[googleLayerName];
var googleLayer = new Constructor(options);
googleLayer.setMap(this._mutant);
this._subLayers[googleLayerName] = googleLayer;
});
return this;
}
// 🍂method removeGoogleLayer(name: String): this
// Removes layer with the given name from the google Map instance.
removeGoogleLayer(googleLayerName) {
this.whenReady(() => {
var googleLayer = this._subLayers && this._subLayers[googleLayerName];
if (googleLayer) {
googleLayer.setMap(null);
delete this._subLayers[googleLayerName];
}
});
return this;
}
_initMutantContainer() {
if (!this._mutantContainer) {
this._mutantContainer = leaflet.DomUtil.create(
"div",
"leaflet-google-mutant leaflet-top leaflet-left"
);
this._mutantContainer.id =
"_MutantContainer_" + leaflet.Util.stamp(this._mutantContainer);
this._mutantContainer.style.pointerEvents = "none";
this._mutantContainer.style.visibility = "hidden";
leaflet.DomEvent.off(this._mutantContainer);
}
this._map.getContainer().appendChild(this._mutantContainer);
this.setOpacity(this.options.opacity);
const style = this._mutantContainer.style;
if (this._map.options.zoomSnap < 1) {
// Fractional zoom needs a bigger mutant container in order to load more (smaller) tiles
style.width = "180%";
style.height = "180%";
} else {
style.width = "100%";
style.height = "100%";
}
style.zIndex = -1;
this._attachObserver(this._mutantContainer);
}
_initMutant() {
if (this._mutant) {
return;
}
var options = {
center: { lat: 0, lng: 0 },
zoom: 0,
tilt: 0,
mapTypeId: this.options.type,
disableDefaultUI: true,
keyboardShortcuts: false,
draggable: false,
disableDoubleClickZoom: true,
scrollwheel: false,
styles: this.options.styles || [],
backgroundColor: "transparent",
};
if (this.options.mapId != null) {
options.mapId = this.options.mapId;
}
var map = new google.maps.Map(this._mutantContainer, options);
this._mutant = map;
this._update();
// 🍂event spawned
// Fired when the mutant has been created.
this.fire("spawned", { mapObject: map });
this._waitControls();
this.once("controls_ready", this._setupAttribution);
}
_attachObserver(node) {
if (!this._observer)
this._observer = new MutationObserver(this._onMutations.bind(this));
// pass in the target node, as well as the observer options
this._observer.observe(node, { childList: true, subtree: true });
// if we are reusing an old _mutantContainer, we must manually detect
// all existing tiles in it
Array.prototype.forEach.call(node.querySelectorAll("img"), this._boundOnMutatedImage);
}
_waitControls() {
const id = setInterval(() => {
const layoutManager = this._mutant.__gm.layoutManager;
if (!layoutManager) {
return;
}
clearInterval(id);
let positions;
// iterate through obfuscated key names to find positions set (atm: layoutManager.o)
Object.keys(layoutManager).forEach(function (key) {
const el = layoutManager[key];
if (el.get) {
if (el.get(1) instanceof Node) {
positions = el;
}
}
});
// 🍂event controls_ready
// Fired when controls positions get available (passed in `positions` property).
this.fire("controls_ready", { positions });
}, 50);
}
_setupAttribution(ev) {
if (!this._map) {
return;
}
// https://developers.google.com/maps/documentation/javascript/reference/control#ControlPosition
const pos = google.maps.ControlPosition;
const ctr = (this._attributionContainer = ev.positions.get(pos.BOTTOM_RIGHT));
ctr.classList.add("leaflet-control", "leaflet-control-attribution");
leaflet.DomEvent.disableClickPropagation(ctr);
ctr.style.height = "14px";
this._map._controlCorners.bottomright.appendChild(ctr);
this._logoContainer = ev.positions.get(pos.BOTTOM_LEFT);
this._logoContainer.style.pointerEvents = "auto";
this._map._controlCorners.bottomleft.appendChild(this._logoContainer);
}
_onMutations(mutations) {
for (var i = 0; i < mutations.length; ++i) {
var mutation = mutations[i];
for (var j = 0; j < mutation.addedNodes.length; ++j) {
var node = mutation.addedNodes[j];
if (node instanceof HTMLImageElement) {
this._onMutatedImage(node);
} else if (node instanceof HTMLElement) {
Array.prototype.forEach.call(
node.querySelectorAll("img"),
this._boundOnMutatedImage
);
}
}
}
}
// Only images which 'src' attrib match this will be considered for moving around.
// Looks like some kind of string-based protobuf, maybe??
// Only the roads (and terrain, and vector-based stuff) match this pattern
_roadRegexp = /!1i(\d+)!2i(\d+)!3i(\d+|VinaFnapurmBegrtn)!/;
// On the other hand, raster imagery matches this other pattern
_satRegexp = /x=(\d+)&y=(\d+)&z=(\d+|VinaFnapurmBegrtn)/;
_onMutatedImage(imgNode) {
let coords;
let match = imgNode.src.match(this._roadRegexp);
let sublayer = 0;
if (match) {
coords = {
z: match[1],
x: match[2],
y: match[3],
};
if (this._imagesPerTile > 1) {
imgNode.style.zIndex = 1;
sublayer = 1;
}
} else {
match = imgNode.src.match(this._satRegexp);
if (match) {
coords = {
x: match[1],
y: match[2],
z: match[3],
};
}
// imgNode.style.zIndex = 0;
sublayer = 0;
}
if (coords) {
var tileKey = this._tileCoordsToKey(coords);
imgNode.style.position = "absolute";
var key = tileKey + "/" + sublayer;
// Cache img so it can also be used in subsequent tile requests
this._lru.set(key, imgNode);
if (key in this._tileCallbacks && this._tileCallbacks[key]) {
// Use the tile for *all* pending callbacks. They'll be cloned anyway.
this._tileCallbacks[key].forEach((callback) => callback(imgNode));
delete this._tileCallbacks[key];
}
}
}
createTile(coords, done) {
const key = this._tileCoordsToKey(coords),
tileContainer = leaflet.DomUtil.create("div");
tileContainer.style.textAlign = "left";
tileContainer.dataset.pending = this._imagesPerTile;
done = done.bind(this, null, tileContainer);
for (var i = 0; i < this._imagesPerTile; ++i) {
const key2 = key + "/" + i,
imgNode = this._lru.get(key2);
if (imgNode) {
tileContainer.appendChild(this._clone(imgNode));
--tileContainer.dataset.pending;
} else {
this._tileCallbacks[key2] = this._tileCallbacks[key2] || [];
this._tileCallbacks[key2].push(
function (c /*, k2*/) {
return function (imgNode) {
c.appendChild(this._clone(imgNode));
--c.dataset.pending;
if (!parseInt(c.dataset.pending)) {
done();
}
}.bind(this);
}.bind(this)(tileContainer /*, key2*/)
);
}
}
if (!parseInt(tileContainer.dataset.pending)) {
leaflet.Util.requestAnimFrame(done);
}
return tileContainer;
}
_clone(imgNode) {
const clonedImgNode = imgNode.cloneNode(true);
clonedImgNode.style.visibility = "visible";
return clonedImgNode;
}
_checkZoomLevels() {
//setting the zoom level on the Google map may result in a different zoom level than the one requested
//(it won't go beyond the level for which they have data).
const zoomLevel = this._map.getZoom(),
gMapZoomLevel = this._mutant.getZoom();
if (!zoomLevel || !gMapZoomLevel) return;
if (
gMapZoomLevel !== zoomLevel || //zoom levels are out of sync, Google doesn't have data
gMapZoomLevel > this.options.maxNativeZoom
) {
//at current location, Google does have data (contrary to maxNativeZoom)
//Update maxNativeZoom
this._setMaxNativeZoom(gMapZoomLevel);
}
}
_setMaxNativeZoom(zoomLevel) {
if (zoomLevel !== this.options.maxNativeZoom) {
this.options.maxNativeZoom = zoomLevel;
this._resetView();
}
}
_update(center) {
// zoom level check needs to happen before super's implementation (tile addition/creation)
// otherwise tiles may be missed if maxNativeZoom is not yet correctly determined
if (this._mutant) {
center = center || this._map.getCenter();
const _center = new google.maps.LatLng(center.lat, center.lng),
zoom = Math.round(this._map.getZoom()),
mutantZoom = this._mutant.getZoom();
this._mutant.setCenter(_center);
//ignore fractional zoom levels
if (zoom !== mutantZoom) {
this._mutant.setZoom(zoom);
if (this._mutantIsReady) this._checkZoomLevels();
//else zoom level check will be done later by 'idle' handler
}
}
super._update(center);
}
// @method whenReady(fn: Function, context?: Object): this
// Runs the given function `fn` when the mutant gets initialized, or immediately
// if it's already initialized, optionally passing a function context.
whenReady(callback, context) {
if (this._mutant) {
callback.call(context || this, { target: this });
} else {
this.on("spawned", callback, context);
}
return this;
}
}
// 🍂factory gridLayer.googleMutant(options)
// Returns a new `GridLayer.GoogleMutant` given its options
if (L && L.gridLayer) {
L.gridLayer.googleMutant = function (options) {
return new L.GridLayer.GoogleMutant(options);
};
}
return GoogleMutant;
})(L);
//# sourceMappingURL=Leaflet.GoogleMutant.js.map
+13
View File
@@ -0,0 +1,13 @@
/*!
JSZip v3.10.1 - A JavaScript class for generating and reading zip files
<http://stuartk.com/jszip>
(c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com>
Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown.
JSZip uses the library pako released under the MIT license :
https://github.com/nodeca/pako/blob/main/LICENSE
*/
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){"use strict";var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(Line truncated
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 600 60"
height="60"
width="600"
id="svg4225"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="spritesheet.svg"
inkscape:export-filename="/home/fpuga/development/upstream/icarto.Leaflet.draw/src/images/spritesheet-2x.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<metadata
id="metadata4258">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4256" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1056"
id="namedview4254"
showgrid="false"
inkscape:zoom="1.3101852"
inkscape:cx="237.56928"
inkscape:cy="7.2419621"
inkscape:window-x="1920"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="svg4225" />
<g
id="enabled"
style="fill:#464646;fill-opacity:1">
<g
id="polyline"
style="fill:#464646;fill-opacity:1">
<path
d="m 18,36 0,6 6,0 0,-6 -6,0 z m 4,4 -2,0 0,-2 2,0 0,2 z"
id="path4229"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 36,18 0,6 6,0 0,-6 -6,0 z m 4,4 -2,0 0,-2 2,0 0,2 z"
id="path4231"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 23.142,39.145 -2.285,-2.29 16,-15.998 2.285,2.285 z"
id="path4233"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
</g>
<path
id="polygon"
d="M 100,24.565 97.904,39.395 83.07,42 76,28.773 86.463,18 Z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
id="rectangle"
d="m 140,20 20,0 0,20 -20,0 z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
id="circle"
d="m 221,30 c 0,6.078 -4.926,11 -11,11 -6.074,0 -11,-4.922 -11,-11 0,-6.074 4.926,-11 11,-11 6.074,0 11,4.926 11,11 z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
id="marker"
d="m 270,19 c -4.971,0 -9,4.029 -9,9 0,4.971 5.001,12 9,14 4.001,-2 9,-9.029 9,-14 0,-4.971 -4.029,-9 -9,-9 z m 0,12.5 c -2.484,0 -4.5,-2.014 -4.5,-4.5 0,-2.484 2.016,-4.5 4.5,-4.5 2.485,0 4.5,2.016 4.5,4.5 0,2.486 -2.015,4.5 -4.5,4.5 z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<g
id="edit"
style="fill:#464646;fill-opacity:1">
<path
d="m 337,30.156 0,0.407 0,5.604 c 0,1.658 -1.344,3 -3,3 l -10,0 c -1.655,0 -3,-1.342 -3,-3 l 0,-10 c 0,-1.657 1.345,-3 3,-3 l 6.345,0 3.19,-3.17 -9.535,0 c -3.313,0 -6,2.687 -6,6 l 0,10 c 0,3.313 2.687,6 6,6 l 10,0 c 3.314,0 6,-2.687 6,-6 l 0,-8.809 -3,2.968"
id="path4240"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 338.72,24.637 -8.892,8.892 -2.828,0 0,-2.829 8.89,-8.89 z"
id="path4242"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 338.697,17.826 4,0 0,4 -4,0 z"
transform="matrix(-0.70698336,-0.70723018,0.70723018,-0.70698336,567.55917,274.78273)"
id="path4244"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
</g>
<g
id="remove"
style="fill:#464646;fill-opacity:1">
<path
d="m 381,42 18,0 0,-18 -18,0 0,18 z m 14,-16 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z"
id="path4247"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 395,20 0,-4 -10,0 0,4 -6,0 0,2 22,0 0,-2 -6,0 z m -2,0 -6,0 0,-2 6,0 0,2 z"
id="path4249"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
</g>
</g>
<g
id="disabled"
transform="translate(120,0)"
style="fill:#bbbbbb">
<use
xlink:href="#edit"
id="edit-disabled"
x="0"
y="0"
width="100%"
height="100%" />
<use
xlink:href="#remove"
id="remove-disabled"
x="0"
y="0"
width="100%"
height="100%" />
</g>
<path
style="fill:none;stroke:#464646;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle-3"
d="m 581.65725,30 c 0,6.078 -4.926,11 -11,11 -6.074,0 -11,-4.922 -11,-11 0,-6.074 4.926,-11 11,-11 6.074,0 11,4.926 11,11 z"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

+10
View File
@@ -0,0 +1,10 @@
.leaflet-draw-section{position:relative}.leaflet-draw-toolbar{margin-top:12px}.leaflet-draw-toolbar-top{margin-top:0}.leaflet-draw-toolbar-notop a:first-child{border-top-right-radius:0}.leaflet-draw-toolbar-nobottom a:last-child{border-bottom-right-radius:0}.leaflet-draw-toolbar a{background-image:url('images/spritesheet.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg');background-repeat:no-repeat;background-size:300px 30px;background-clip:padding-box}.leaflet-retina .leaflet-draw-toolbar a{background-image:url('images/spritesheet-2x.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg')}
.leaflet-draw a{display:block;text-align:center;text-decoration:none}.leaflet-draw a .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.leaflet-draw-actions{display:none;list-style:none;margin:0;padding:0;position:absolute;left:26px;top:0;white-space:nowrap}.leaflet-touch .leaflet-draw-actions{left:32px}.leaflet-right .leaflet-draw-actions{right:26px;left:auto}.leaflet-touch .leaflet-right .leaflet-draw-actions{right:32px;left:auto}.leaflet-draw-actions li{display:inline-block}
.leaflet-draw-actions li:first-child a{border-left:0}.leaflet-draw-actions li:last-child a{-webkit-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.leaflet-right .leaflet-draw-actions li:last-child a{-webkit-border-radius:0;border-radius:0}.leaflet-right .leaflet-draw-actions li:first-child a{-webkit-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.leaflet-draw-actions a{background-color:#919187;border-left:1px solid #AAA;color:#FFF;font:11px/19px "Helvetica Neue",Arial,Helvetica,sans-serif;line-height:28px;text-decoration:none;padding-left:10px;padding-right:10px;height:28px}
.leaflet-touch .leaflet-draw-actions a{font-size:12px;line-height:30px;height:30px}.leaflet-draw-actions-bottom{margin-top:0}.leaflet-draw-actions-top{margin-top:1px}.leaflet-draw-actions-top a,.leaflet-draw-actions-bottom a{height:27px;line-height:27px}.leaflet-draw-actions a:hover{background-color:#a0a098}.leaflet-draw-actions-top.leaflet-draw-actions-bottom a{height:26px;line-height:26px}.leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:-2px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:0 -1px}
.leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-31px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-29px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-62px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-60px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-92px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-90px -1px}
.leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-122px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-120px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-273px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-271px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-152px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-150px -1px}
.leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-182px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-180px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-212px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-210px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-242px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-240px -2px}
.leaflet-mouse-marker{background-color:#fff;cursor:crosshair}.leaflet-draw-tooltip{background:#363636;background:rgba(0,0,0,0.5);border:1px solid transparent;-webkit-border-radius:4px;border-radius:4px;color:#fff;font:12px/18px "Helvetica Neue",Arial,Helvetica,sans-serif;margin-left:20px;margin-top:-21px;padding:4px 8px;position:absolute;visibility:hidden;white-space:nowrap;z-index:6}.leaflet-draw-tooltip:before{border-right:6px solid black;border-right-color:rgba(0,0,0,0.5);border-top:6px solid transparent;border-bottom:6px solid transparent;content:"";position:absolute;top:7px;left:-7px}
.leaflet-error-draw-tooltip{background-color:#f2dede;border:1px solid #e6b6bd;color:#b94a48}.leaflet-error-draw-tooltip:before{border-right-color:#e6b6bd}.leaflet-draw-tooltip-single{margin-top:-12px}.leaflet-draw-tooltip-subtext{color:#f8d5e4}.leaflet-draw-guide-dash{font-size:1%;opacity:.6;position:absolute;width:5px;height:5px}.leaflet-edit-marker-selected{background-color:rgba(254,87,161,0.1);border:4px dashed rgba(254,87,161,0.6);-webkit-border-radius:4px;border-radius:4px;box-sizing:content-box}
.leaflet-edit-move{cursor:move}.leaflet-edit-resize{cursor:pointer}.leaflet-oldie .leaflet-draw-toolbar{border:1px solid #999}
+10
View File
@@ -0,0 +1,10 @@
/*
Leaflet.draw 1.0.4, a plugin that adds drawing and editing tools to Leaflet powered maps.
(c) 2012-2017, Jacob Toye, Jon West, Smartrak, Leaflet
https://github.com/Leaflet/Leaflet.draw
http://leafletjs.com
*/
!function(t,e,i){function o(t,e){for(;(t=t.parentElement)&&!t.classList.contains(e););return t}L.drawVersion="1.0.4",L.Draw={},L.drawLocal={draw:{toolbar:{actions:{title:"Cancel drawing",text:"Cancel"},finish:{title:"Finish drawing",text:"Finish"},undo:{title:"Delete last point drawn",text:"Delete last point"},buttons:{polyline:"Draw a polyline",polygon:"Draw a polygon",rectangle:"Draw a rectangle",circle:"Draw a circle",marker:"Draw a marker",circlemarker:"Draw a circlemarker"}},handlers:{circle:{tooltip:{start:"Click and drag to draw circle."},radius:"Radius"},circlemarker:{tooltip:{start:"Click map to place circle marker."}},marker:{tooltip:{start:"Click map to place marker."}},polygon:{tooltip:{start:"Click to start drawing shape.",cont:"Click to continue drawing shape.",end:"Click first point to close this shape."}},polyline:{error:"<strong>Error:</strong> shape edges cannot cross!",tooltip:{start:"Click to start drawing line.",cont:"Click to continue drawing line.",end:"Click last point to finish line."}},rectangle:{tooltip:{start:"Click and drag to draw rectangle."}},simpleshape:{tooltip:{end:"Release mouse to finish drawing."}}}},edit:{toolbar:{actions:{save:{title:"Save changes",text:"Save"},cancel:{title:"Cancel editing, discards all changes",text:"Cancel"},clearAll:{title:"Clear all layers",text:"Clear All"}},buttons:{edit:"Edit layers",editDisabled:"No layers to edit",remove:"Delete layers",removeDisabled:"No layers to delete"}},handlers:{edit:{tooltip:{text:"Drag handles or markers to edit features.",subtext:"Click cancel to undo changes."}},remove:{tooltip:{text:"Click on a feature to remove."}}}}},L.Draw.Event={},L.Draw.Event.CREATED="draw:created",L.Draw.Event.EDITED="draw:edited",L.Draw.Event.DELETED="draw:deleted",L.Draw.Event.DRAWSTART="draw:drawstart",L.Draw.Event.DRAWSTOP="draw:drawstop",L.Draw.Event.DRAWVERTEX="draw:drawvertex",L.Draw.Event.EDITSTART="draw:editstart",L.Draw.Event.EDITMOVE="draw:editmove",L.Draw.Event.EDITRESIZE="draw:editresize",L.Draw.Event.EDITVERTEX="draw:editvertex",L.Draw.Event.EDITSTOP="draw:editstop",L.Draw.Event.DELETESTART="draw:deletestart",L.Draw.Event.DELETESTOP="draw:deletestop",L.Draw.Event.TOOLBAROPENED="draw:toolbaropened",L.Draw.Event.TOOLBARCLOSED="draw:toolbarclosed",L.Draw.Event.MARKERCONTEXT="draw:markercontext",L.Draw=L.Draw||{},L.Draw.Feature=L.Handler.extend({initialize:function(t,e){this._map=t,this._container=t._container,this._overlayPane=t._panes.overlayPane,this._popupPane=t._panes.popupPane,e&&e.shapeOptions&&(e.shapeOptions=L.Util.extend({},this.options.shapeOptions,e.shapeOptions)),L.setOptions(this,e);var i=L.version.split(".");1===parseInt(i[0],10)&&parseInt(i[1],10)>=2?L.Draw.Feature.include(L.Evented.prototype):L.Draw.Feature.include(L.Mixin.Events)},enable:function(){this._enabled||(L.Handler.prototype.enable.call(this),this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.DRAWSTART,{layerType:this.type}))},disable:function(){this._enabled&&(L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.DRAWSTOP,{layerType:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(L.DomUtil.disableTextSelection(),t.getContainer().focus(),this._tooltip=new L.Draw.Tooltip(this._map),L.DomEvent.on(this._container,"keyup",this._cancelDrawing,this))},removeHooks:function(){this._map&&(L.DomUtil.enableTextSelection(),this._tooltip.dispose(),this._tooltip=null,L.DomEvent.off(this._container,"keyup",this._cancelDrawing,this))},setOptions:function(t){L.setOptions(this,t)},_fireCreatedEvent:function(t){this._map.fire(L.Draw.Event.CREATED,{layer:t,layerType:this.type})},_cancelDrawing:function(t){27===t.keyCode&&(this._map.fire("draw:canceled",{layerType:this.type}),this.disable())}}),L.Draw.Polyline=L.Draw.Feature.extend({statics:{TYPE:"polyline"},Poly:L.Polyline,options:{allowIntersection:!0,repeatMode:!1,drawError:{color:"#b00b00",timeout:2500},icon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon"}),touchIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-touch-icon"}),guidelineDistance:20,maxGuideLineLength:4e3,shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!1,clickable:!0},metric:!0,feet:!0,nautic:!1,showLength:!0,zIndexOffset:2e3,factor:1,maxPoints:0},initialize:function(t,e){L.Browser.touch&&(this.options.icon=this.options.touchIcon),this.options.drawError.message=L.drawLocal.draw.handlers.polyline.error,e&&e.drawError&&(e.drawError=L.Util.extend({},this.options.drawError,e.drawError)),this.type=L.Draw.Polyline.TYPE,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._markers=[],this._markerGroup=new L.LayerGroup,this._map.addLayer(this._markerGroup),this._poly=new L.Polyline([],this.options.shapeOptions),this._tooltip.updateContent(this._getTooltipText()),this._mouseMarker||(this._mouseMaLine truncated
className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-resize"}),touchMoveIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-move leaflet-touch-icon"}),touchResizeIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-resize leaflet-touch-icon"})},initialize:function(t,e){L.Browser.touch&&(this.options.moveIcon=this.options.touchMoveIcon,this.options.resizeIcon=this.options.touchResizeIcon),this._shape=t,L.Util.setOptions(this,e)},addHooks:function(){var t=this._shape;this._shape._map&&(this._map=this._shape._map,t.setStyle(t.options.editing),t._map&&(this._map=t._map,this._markerGroup||this._initMarkers(),this._map.addLayer(this._markerGroup)))},removeHooks:function(){var t=this._shape;if(t.setStyle(t.options.original),t._map){this._unbindMarker(this._moveMarker);for(var e=0,i=this._resizeMarkers.length;e<i;e++)this._unbindMarker(this._resizeMarkers[e]);this._resizeMarkers=null,this._map.removeLayer(this._markerGroup),delete this._markerGroup}this._map=null},updateMarkers:function(){this._markerGroup.clearLayers(),this._initMarkers()},_initMarkers:function(){this._markerGroup||(this._markerGroup=new L.LayerGroup),this._createMoveMarker(),this._createResizeMarker()},_createMoveMarker:function(){},_createResizeMarker:function(){},_createMarker:function(t,e){var i=new L.Marker.Touch(t,{draggable:!0,icon:e,zIndexOffset:10});return this._bindMarker(i),this._markerGroup.addLayer(i),i},_bindMarker:function(t){t.on("dragstart",this._onMarkerDragStart,this).on("drag",this._onMarkerDrag,this).on("dragend",this._onMarkerDragEnd,this).on("touchstart",this._onTouchStart,this).on("touchmove",this._onTouchMove,this).on("MSPointerMove",this._onTouchMove,this).on("touchend",this._onTouchEnd,this).on("MSPointerUp",this._onTouchEnd,this)},_unbindMarker:function(t){t.off("dragstart",this._onMarkerDragStart,this).off("drag",this._onMarkerDrag,this).off("dragend",this._onMarkerDragEnd,this).off("touchstart",this._onTouchStart,this).off("touchmove",this._onTouchMove,this).off("MSPointerMove",this._onTouchMove,this).off("touchend",this._onTouchEnd,this).off("MSPointerUp",this._onTouchEnd,this)},_onMarkerDragStart:function(t){t.target.setOpacity(0),this._shape.fire("editstart")},_fireEdit:function(){this._shape.edited=!0,this._shape.fire("edit")},_onMarkerDrag:function(t){var e=t.target,i=e.getLatLng();e===this._moveMarker?this._move(i):this._resize(i),this._shape.redraw(),this._shape.fire("editdrag")},_onMarkerDragEnd:function(t){t.target.setOpacity(1),this._fireEdit()},_onTouchStart:function(t){if(L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this,t),"function"==typeof this._getCorners){var e=this._getCorners(),i=t.target,o=i._cornerIndex;i.setOpacity(0),this._oppositeCorner=e[(o+2)%4],this._toggleCornerMarkers(0,o)}this._shape.fire("editstart")},_onTouchMove:function(t){var e=this._map.mouseEventToLayerPoint(t.originalEvent.touches[0]),i=this._map.layerPointToLatLng(e);return t.target===this._moveMarker?this._move(i):this._resize(i),this._shape.redraw(),!1},_onTouchEnd:function(t){t.target.setOpacity(1),this.updateMarkers(),this._fireEdit()},_move:function(){},_resize:function(){}}),L.Edit=L.Edit||{},L.Edit.Rectangle=L.Edit.SimpleShape.extend({_createMoveMarker:function(){var t=this._shape.getBounds(),e=t.getCenter();this._moveMarker=this._createMarker(e,this.options.moveIcon)},_createResizeMarker:function(){var t=this._getCorners();this._resizeMarkers=[];for(var e=0,i=t.length;e<i;e++)this._resizeMarkers.push(this._createMarker(t[e],this.options.resizeIcon)),this._resizeMarkers[e]._cornerIndex=e},_onMarkerDragStart:function(t){L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this,t);var e=this._getCorners(),i=t.target,o=i._cornerIndex;this._oppositeCorner=e[(o+2)%4],this._toggleCornerMarkers(0,o)},_onMarkerDragEnd:function(t){var e,i,o=t.target;o===this._moveMarker&&(e=this._shape.getBounds(),i=e.getCenter(),o.setLatLng(i)),this._toggleCornerMarkers(1),this._repositionCornerMarkers(),L.Edit.SimpleShape.prototype._onMarkerDragEnd.call(this,t)},_move:function(t){for(var e,i=this._shape._defaultShape?this._shape._defaultShape():this._shape.getLatLngs(),o=this._shape.getBounds(),a=o.getCenter(),n=[],s=0,r=i.length;s<r;s++)e=[i[s].lat-a.lat,i[s].lng-a.lng],n.push([t.lat+e[0],t.lng+e[1]]);this._shape.setLatLngs(n),this._repositionCornerMarkers(),this._map.fire(L.Draw.Event.EDITMOVE,{layer:this._shape})},_resize:function(t){var e;this._shape.setBounds(L.latLngBounds(t,this._oppositeCorner)),e=this._shape.getBounds(),this._moveMarker.setLatLng(e.getCenter()),this._map.fire(L.Draw.Event.EDITRESIZE,{layer:this._shape})},_getCorners:function(){var t=this._shape.getBounds();return[t.getNorthWest(),t.getNorthEast(),t.getSouthEast(),t.getSouthWest()]},_toggleCornerMarkers:function(t){for(var e=0,i=this._resizeMarkers.length;e<i;e++)this._resizeMarkers[e].setOpacity(t)},_repositionCornerMarkers:function(){for(vLine truncated
this._selectedPathOptions&&(e instanceof L.Marker?this._toggleMarkerHighlight(e):(e.setStyle(e.options.previousOptions),delete e.options.previousOptions)),e instanceof L.Marker?(e.dragging.disable(),e.off("dragend",this._onMarkerDragEnd,this).off("touchmove",this._onTouchMove,this).off("MSPointerMove",this._onTouchMove,this).off("touchend",this._onMarkerDragEnd,this).off("MSPointerUp",this._onMarkerDragEnd,this)):e.editing.disable()},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_onMarkerDragEnd:function(t){var e=t.target;e.edited=!0,this._map.fire(L.Draw.Event.EDITMOVE,{layer:e})},_onTouchMove:function(t){var e=t.originalEvent.changedTouches[0],i=this._map.mouseEventToLayerPoint(e),o=this._map.layerPointToLatLng(i);t.target.setLatLng(o)},_hasAvailableLayers:function(){return 0!==this._featureGroup.getLayers().length}}),L.EditToolbar.Delete=L.Handler.extend({statics:{TYPE:"remove"},initialize:function(t,e){if(L.Handler.prototype.initialize.call(this,t),L.Util.setOptions(this,e),this._deletableLayers=this.options.featureGroup,!(this._deletableLayers instanceof L.FeatureGroup))throw new Error("options.featureGroup must be a L.FeatureGroup");this.type=L.EditToolbar.Delete.TYPE;var i=L.version.split(".");1===parseInt(i[0],10)&&parseInt(i[1],10)>=2?L.EditToolbar.Delete.include(L.Evented.prototype):L.EditToolbar.Delete.include(L.Mixin.Events)},enable:function(){!this._enabled&&this._hasAvailableLayers()&&(this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.DELETESTART,{handler:this.type}),L.Handler.prototype.enable.call(this),this._deletableLayers.on("layeradd",this._enableLayerDelete,this).on("layerremove",this._disableLayerDelete,this))},disable:function(){this._enabled&&(this._deletableLayers.off("layeradd",this._enableLayerDelete,this).off("layerremove",this._disableLayerDelete,this),L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.DELETESTOP,{handler:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(t.getContainer().focus(),this._deletableLayers.eachLayer(this._enableLayerDelete,this),this._deletedLayers=new L.LayerGroup,this._tooltip=new L.Draw.Tooltip(this._map),this._tooltip.updateContent({text:L.drawLocal.edit.handlers.remove.tooltip.text}),this._map.on("mousemove",this._onMouseMove,this))},removeHooks:function(){this._map&&(this._deletableLayers.eachLayer(this._disableLayerDelete,this),this._deletedLayers=null,this._tooltip.dispose(),this._tooltip=null,this._map.off("mousemove",this._onMouseMove,this))},revertLayers:function(){this._deletedLayers.eachLayer(function(t){this._deletableLayers.addLayer(t),t.fire("revert-deleted",{layer:t})},this)},save:function(){this._map.fire(L.Draw.Event.DELETED,{layers:this._deletedLayers})},removeAllLayers:function(){this._deletableLayers.eachLayer(function(t){this._removeLayer({layer:t})},this),this.save()},_enableLayerDelete:function(t){(t.layer||t.target||t).on("click",this._removeLayer,this)},_disableLayerDelete:function(t){var e=t.layer||t.target||t;e.off("click",this._removeLayer,this),this._deletedLayers.removeLayer(e)},_removeLayer:function(t){var e=t.layer||t.target||t;this._deletableLayers.removeLayer(e),this._deletedLayers.addLayer(e),e.fire("deleted")},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_hasAvailableLayers:function(){return 0!==this._deletableLayers.getLayers().length}})}(window,document);
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

+661
View File
@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
+6
View File
@@ -0,0 +1,6 @@
/* @preserve
* Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
* (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n<o;n++)for(e in i=arguments[n])t[e]=i[e];return t}var R=Object.create||function(t){return N.prototype=t,new N};function N(){}function a(t,e){var i,n=Array.prototype.slice;return t.bind?t.bind.apply(t,n.call(arguments,1)):(i=n.call(arguments,2),function(){return t.apply(e,i.length?i.concat(n.call(arguments)):arguments)})}var D=0;function h(t){return"_leaflet_id"in t||(t._leaflet_id=++D),t._leaflet_id}function j(t,e,i){var n,o,s=function(){n=!1,o&&(r.apply(i,o),o=!1)},r=function(){n?o=arguments:(t.apply(i,arguments),setTimeout(s,e),n=!0)};return r}function H(t,e,i){var n=e[1],e=e[0],o=n-e;return t===n&&i?t:((t-e)%o+o)%o+e}function u(){return!1}function i(t,e){return!1===e?t:(e=Math.pow(10,void 0===e?6:e),Math.round(t*e)/e)}function W(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function F(t){return W(t).split(/\s+/)}function c(t,e){for(var i in Object.prototype.hasOwnProperty.call(t,"options")||(t.options=t.options?R(t.options):{}),e)t.options[i]=e[i];return t.options}function U(t,e,i){var n,o=[];for(n in t)o.push(encodeURIComponent(i?n.toUpperCase():n)+"="+encodeURIComponent(t[n]));return(e&&-1!==e.indexOf("?")?"&":"?")+o.join("&")}var V=/\{ *([\w_ -]+) *\}/g;function q(t,i){return t.replace(V,function(t,e){e=i[e];if(void 0===e)throw new Error("No value provided for variable "+t);return e="function"==typeof e?e(i):e})}var d=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function G(t,e){for(var i=0;i<t.length;i++)if(t[i]===e)return i;return-1}var K="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";function Y(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}var X=0;function J(t){var e=+new Date,i=Math.max(0,16-(e-X));return X=e+i,window.setTimeout(t,i)}var $=window.requestAnimationFrame||Y("RequestAnimationFrame")||J,Q=window.cancelAnimationFrame||Y("CancelAnimationFrame")||Y("CancelRequestAnimationFrame")||function(t){window.clearTimeout(t)};function x(t,e,i){if(!i||$!==J)return $.call(window,a(t,e));t.call(e)}function r(t){t&&Q.call(window,t)}var tt={__proto__:null,extend:l,create:R,bind:a,get lastId(){return D},stamp:h,throttle:j,wrapNum:H,falseFn:u,formatNum:i,trim:W,splitWords:F,setOptions:c,getParamString:U,template:q,isArray:d,indexOf:G,emptyImageUrl:K,requestFn:$,cancelFn:Q,requestAnimFrame:x,cancelAnimFrame:r};function et(){}et.extend=function(t){function e(){c(this),this.initialize&&this.initialize.apply(this,arguments),this.callInitHooks()}var i,n=e.__super__=this.prototype,o=R(n);for(i in(o.constructor=e).prototype=o,this)Object.prototype.hasOwnProperty.call(this,i)&&"prototype"!==i&&"__super__"!==i&&(e[i]=this[i]);if(t.statics&&l(e,t.statics),t.includes){var s=t.includes;if("undefined"!=typeof L&&L&&L.Mixin){s=d(s)?s:[s];for(var r=0;r<s.length;r++)s[r]===L.Mixin.Events&&console.warn("Deprecated include of L.Mixin.Events: this property will be removed in future releases, please inherit from L.Evented instead.",(new Error).stack)}l.apply(null,[o].concat(t.includes))}return l(o,t),delete o.statics,delete o.includes,o.options&&(o.options=n.options?R(n.options):{},l(o.options,t.options)),o._initHooks=[],o.callInitHooks=function(){if(!this._initHooksCalled){n.callInitHooks&&n.callInitHooks.call(this),this._initHooksCalled=!0;for(var t=0,e=o._initHooks.length;t<e;t++)o._initHooks[t].call(this)}},e},et.include=function(t){var e=this.prototype.options;return l(this.prototype,t),t.options&&(this.prototype.options=e,this.mergeOptions(t.options)),this},et.mergeOptions=function(t){return l(this.prototype.options,t),this},et.addInitHook=function(t){var e=Array.prototype.slice.call(arguments,1),i="function"==typeof t?t:function(){this[t].apply(this,e)};return this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(i),this};var e={on:function(t,e,i){if("object"==typeof t)for(var n in t)this._on(n,t[n],e);else for(var o=0,s=(t=F(t)).length;o<s;o++)this._on(t[o],e,i);return this},off:function(t,e,i){if(arguments.length)if("object"==typeof t)for(var n in t)this._off(n,t[n],e);else{t=F(t);for(var o=1===arguments.length,s=0,r=t.length;s<r;s++)o?this._off(t[s]):this._off(t[s],e,i)}else delete this._events;return this},_on:function(t,e,i,n){"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1===this._listens(t,e,i)&&(e={fn:e,ctx:i=i===this?void 0:i},n&&(e.once=!0),this._events=this._events||{},this._events[t]=this._events[t]||[],this._events[t].push(e))},_off:function(t,e,i){var n,o,s;if(this._events&&(n=this._events[t]))if(1===arguments.length){if(this._firingCount)for(o=0,s=n.length;o<s;o++)n[o].fn=u;delete this._events[t]}else"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1!==(e=this._listens(Line truncated
//# sourceMappingURL=leaflet.js.map
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

+2
View File
@@ -0,0 +1,2 @@
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).toGeoJSON={})}(this,(function(t){"use strict";function e(t,e){return Array.from(t.getElementsByTagName(e))}function n(t){return"#"===t[0]?t:`#${t}`}function o(t){return t?.normalize(),t&&t.textContent||""}function r(t,e,n){const o=t.getElementsByTagName(e),r=o.length?o[0]:null;return r&&n&&n(r),r}function i(t,e,n){const o={};if(!t)return o;const r=t.getElementsByTagName(e),i=r.length?r[0]:null;return i&&n?n(i,o):o}function s(t,e,n){const i=o(r(t,e));return i&&n&&n(i)||{}}function c(t,e,n){const i=parseFloat(o(r(t,e)));if(!isNaN(i))return i&&n&&n(i)||{}}function a(t,e,n){const i=parseFloat(o(r(t,e)));if(!isNaN(i))return n&&n(i),i}function l(t,e){const n={};for(const o of e)s(t,o,(t=>{n[o]=t}));return n}function u(t){return 1===t?.nodeType}function f(t){return i(t,"line",(t=>Object.assign({},s(t,"color",(t=>({stroke:`#${t}`}))),c(t,"opacity",(t=>({"stroke-opacity":t}))),c(t,"width",(t=>({"stroke-width":96*t/25.4}))))))}function p(t){let e=[];if(null===t)return e;for(const n of Array.from(t.childNodes)){if(!u(n))continue;const t=g(n.nodeName);if("gpxtpx:TrackPointExtension"===t)e=e.concat(p(n));else{const r=o(n);e.push([t,d(r)])}}return e}function g(t){return["heart","gpxtpx:hr","hr"].includes(t)?"heart":t}function d(t){const e=parseFloat(t);return isNaN(e)?t:e}function m(t){const e=[parseFloat(t.getAttribute("lon")||""),parseFloat(t.getAttribute("lat")||"")];if(isNaN(e[0])||isNaN(e[1]))return null;a(t,"ele",(t=>{e.push(t)}));const n=r(t,"time");return{coordinates:e,time:n?o(n):null,extendedValues:p(r(t,"extensions"))}}function h(t){const n=l(t,["name","cmt","desc","type","time","keywords"]),r=Array.from(t.getElementsByTagNameNS("http://www.garmin.com/xmlschemas/GpxExtensions/v3","*"));for(const e of r)e.parentNode?.parentNode===t&&(n[e.tagName.replace(":","_")]=o(e));const i=e(t,"link");return i.length&&(n.links=i.map((t=>Object.assign({href:t.getAttribute("href")},l(t,["text","type"]))))),n}function y(t,n){const o=e(t,n),r=[],i=[],s={};for(let t=0;t<o.length;t++){const e=m(o[t]);if(e){r.push(e.coordinates),e.time&&i.push(e.time);for(const[n,r]of e.extendedValues){const e="heart"===n?n:n.replace("gpxtpx:","")+"s";s[e]||(s[e]=Array(o.length).fill(null)),s[e][t]=r}}}if(!(r.length<2))return{line:r,times:i,extendedValues:s}}function b(t){const e=y(t,"rtept");if(e)return{type:"Feature",properties:Object.assign({_gpxType:"rte"},h(t),f(r(t,"extensions"))),geometry:{type:"LineString",coordinates:e.line}}}function N(t){const n=e(t,"trkseg"),o=[],i=[],s=[];for(const t of n){const e=y(t,"trkpt");e&&(s.push(e),e.times&&e.times.length&&i.push(e.times))}if(0===s.length)return null;const c=s.length>1,a=Object.assign({_gpxType:"trk"},h(t),f(r(t,"extensions")),i.length?{coordinateProperties:{times:c?i:i[0]}}:{});for(const t of s){o.push(t.line),a.coordinateProperties||(a.coordinateProperties={});const e=a.coordinateProperties,n=Object.entries(t.extendedValues);for(let t=0;t<n.length;t++){const[o,r]=n[t];c?(e[o]||(e[o]=s.map((t=>new Array(t.line.length).fill(null)))),e[o][t]=r):e[o]=r}}return{type:"Feature",properties:a,geometry:c?{type:"MultiLineString",coordinates:o}:{type:"LineString",coordinates:o[0]}}}function x(t){const e=Object.assign(h(t),l(t,["sym"])),n=m(t);return n?{type:"Feature",properties:e,geometry:{type:"Point",coordinates:n.coordinates}}:null}function*k(t){for(const n of e(t,"trk")){const t=N(n);t&&(yield t)}for(const n of e(t,"rte")){const t=b(n);t&&(yield t)}for(const n of e(t,"wpt")){const t=x(n);t&&(yield t)}}const A=[["heartRate","heartRates"],["Cadence","cadences"],["Speed","speeds"],["Watts","watts"]],S=[["TotalTimeSeconds","totalTimeSeconds"],["DistanceMeters","distanceMeters"],["MaximumSpeed","maxSpeed"],["AverageHeartRateBpm","avgHeartRate"],["MaximumHeartRateBpm","maxHeartRate"],["AvgSpeed","avgSpeed"],["AvgWatts","avgWatts"],["MaxWatts","maxWatts"]];function v(t,e){const n=[];for(const[i,s]of e){let e=r(t,i);if(!e){const n=t.getElementsByTagNameNS("http://www.garmin.com/xmlschemas/ActivityExtension/v2",i);n.length&&(e=n[0])}const c=parseFloat(o(e));isNaN(c)||n.push([s,c])}return n}function T(t){const e=[a(t,"LongitudeDegrees"),a(t,"LatitudeDegrees")];if(void 0===e[0]||isNaN(e[0])||void 0===e[1]||isNaN(e[1]))return null;const n=r(t,"HeartRateBpm"),i=o(r(t,"Time"));return r(t,"AltitudeMeters",(t=>{const n=parseFloat(o(t));isNaN(n)||e.push(n)})),{coordinates:e,time:i||null,heartRate:n?parseFloat(o(n)):null,extensions:v(t,A)}}function F(t){const n=e(t,"Trackpoint"),o=[],r=[],i=[];if(n.length<2)return null;const s={},c={extendedProperties:s};for(let t=0;t<n.length;t++){const e=T(n[t]);if(null===e)continue;o.push(e.coordinates);const{time:c,heartRate:a,extensions:l}=e;c&&r.push(c),a&&i.push(a);for(const[e,o]of l)s[e]||(s[e]=Array(n.length).fill(null)),s[e][t]=o}return o.length<2?null:Object.assign(c,{line:o,timeLine truncated
//# sourceMappingURL=togeojson.umd.js.map