Compare commits

...
4 Commits
Author SHA1 Message Date
livio 41405bcc4f Speed up Android test packages 2026-09-28 20:29:51 +02:00
livio 9b4ff9e188 Always retain KML lines and points 2026-09-28 20:29:21 +02:00
livio c0adc27cf5 Use existing Base signing certificate 2026-09-28 19:55:22 +02:00
livio 3dfeef54ea Release 0.4.8 with Android file imports 2026-09-28 19:42:26 +02:00
9 changed files with 328 additions and 26 deletions

No files matched your search

+101 -2
View File
@@ -10,6 +10,7 @@
@inject MapExportService MapExportService @inject MapExportService MapExportService
@inject CloudSyncService CloudSyncService @inject CloudSyncService CloudSyncService
@inject AppUpdateService AppUpdateService @inject AppUpdateService AppUpdateService
@inject IncomingFileService IncomingFiles
@implements IAsyncDisposable @implements IAsyncDisposable
<PageTitle>LME Mappe Catastali</PageTitle> <PageTitle>LME Mappe Catastali</PageTitle>
@@ -697,6 +698,7 @@
private bool SyncBusy { get; set; } private bool SyncBusy { get; set; }
private string SyncStatus { get; set; } = "Sincronizzazione non configurata."; private string SyncStatus { get; set; } = "Sincronizzazione non configurata.";
private bool UpdateBusy { get; set; } private bool UpdateBusy { get; set; }
private bool MapReady { get; set; }
private string UpdateStatus { get; set; } = $"Versione installata: {AppUpdateService.CurrentVersion}"; private string UpdateStatus { get; set; } = $"Versione installata: {AppUpdateService.CurrentVersion}";
private string UpdateDownloadUrl { get; set; } = string.Empty; private string UpdateDownloadUrl { get; set; } = string.Empty;
private string UpdateReleaseNotes { get; set; } = string.Empty; private string UpdateReleaseNotes { get; set; } = string.Empty;
@@ -754,6 +756,7 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
IncomingFiles.FileAvailable += OnIncomingFileAvailable;
LoadFlagPreferences(); LoadFlagPreferences();
try try
@@ -797,12 +800,14 @@
{ {
MapCallbackReference = DotNetObjectReference.Create(this); MapCallbackReference = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("lmeMap.init", MapCallbackReference); await JS.InvokeVoidAsync("lmeMap.init", MapCallbackReference);
MapReady = true;
await ApplyFlagPreferencesToMapAsync(); await ApplyFlagPreferencesToMapAsync();
await ApplyAdvancedOptions(); await ApplyAdvancedOptions();
await CenterOnInitialLocationAsync(); await CenterOnInitialLocationAsync();
await MigrateLegacyProjectAsync(); await MigrateLegacyProjectAsync();
var offlineCount = await JS.InvokeAsync<int>("lmeMap.getOfflineTileStats"); var offlineCount = await JS.InvokeAsync<int>("lmeMap.getOfflineTileStats");
OfflineStatus = offlineCount == 0 ? "Nessuna tessera salvata." : $"{offlineCount} tessere disponibili offline."; OfflineStatus = offlineCount == 0 ? "Nessuna tessera salvata." : $"{offlineCount} tessere disponibili offline.";
await ProcessIncomingFilesAsync();
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
} }
@@ -1492,7 +1497,11 @@
private async Task RestoreBackup() private async Task RestoreBackup()
{ {
var file = await FilePicker.Default.PickAsync(new PickOptions { PickerTitle = "Scegli backup LME" }); var file = await FilePicker.Default.PickAsync(new PickOptions
{
PickerTitle = "Scegli backup LME (.lmebackup.zip)",
FileTypes = BackupPickerFileTypes
});
if (file is null) return; if (file is null) return;
var path = await CopyPickedFileToCacheAsync(file); var path = await CopyPickedFileToCacheAsync(file);
var count = await ProjectStorageService.RestoreBackupAsync(path); var count = await ProjectStorageService.RestoreBackupAsync(path);
@@ -1502,7 +1511,11 @@
private async Task ImportProject() private async Task ImportProject()
{ {
var file = await FilePicker.Default.PickAsync(new PickOptions { PickerTitle = "Scegli progetto LME" }); var file = await FilePicker.Default.PickAsync(new PickOptions
{
PickerTitle = "Scegli progetto LME (.lmeproject.json)",
FileTypes = ProjectPickerFileTypes
});
if (file is null) return; if (file is null) return;
var path = await CopyPickedFileToCacheAsync(file); var path = await CopyPickedFileToCacheAsync(file);
var imported = await ProjectStorageService.ImportProjectAsync(path); var imported = await ProjectStorageService.ImportProjectAsync(path);
@@ -1521,6 +1534,91 @@
return path; return path;
} }
private void OnIncomingFileAvailable()
{
if (MapReady) _ = InvokeAsync(ProcessIncomingFilesAsync);
}
private async Task ProcessIncomingFilesAsync()
{
while (IncomingFiles.TryDequeue(out var incoming) && incoming is not null)
{
try
{
var lowerName = incoming.FileName.ToLowerInvariant();
if (lowerName.EndsWith(".kml", StringComparison.Ordinal)
|| lowerName.EndsWith(".kmz", StringComparison.Ordinal)
|| lowerName.EndsWith(".geojson", StringComparison.Ordinal)
|| await IsGeoJsonAsync(incoming.Path))
{
var content = Convert.ToBase64String(await File.ReadAllBytesAsync(incoming.Path));
await JS.InvokeAsync<string>("lmeMap.importExternalGisFile", incoming.FileName, content, incoming.ContentType);
continue;
}
if (lowerName.EndsWith(".zip", StringComparison.Ordinal))
{
var count = await ProjectStorageService.RestoreBackupAsync(incoming.Path);
await RefreshProjectsAsync();
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Backup aperto: {count} progetti ripristinati.");
continue;
}
if (lowerName.EndsWith(".json", StringComparison.Ordinal))
{
var imported = await ProjectStorageService.ImportProjectAsync(incoming.Path);
await RefreshProjectsAsync();
SelectedProjectId = imported.Id;
await RefreshProjectVersionsAsync(true);
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto aperto: {imported.Name}.");
continue;
}
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Formato non riconosciuto: {incoming.FileName}.");
}
catch (Exception error)
{
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Impossibile aprire {incoming.FileName}: {error.GetBaseException().Message}");
}
finally
{
try { File.Delete(incoming.Path); } catch { }
}
}
}
private static async Task<bool> IsGeoJsonAsync(string path)
{
if (!Path.GetExtension(path).Equals(".json", StringComparison.OrdinalIgnoreCase)) return false;
try
{
await using var stream = File.OpenRead(path);
using var document = await System.Text.Json.JsonDocument.ParseAsync(stream);
if (!document.RootElement.TryGetProperty("type", out var typeProperty)) return false;
var type = typeProperty.GetString();
return type is "Feature" or "FeatureCollection" or "Point" or "MultiPoint"
or "LineString" or "MultiLineString" or "Polygon" or "MultiPolygon"
or "GeometryCollection";
}
catch { return false; }
}
private static readonly FilePickerFileType ProjectPickerFileTypes = new(new Dictionary<DevicePlatform, IEnumerable<string>>
{
[DevicePlatform.Android] = ["application/json", "text/json", "text/plain", "application/octet-stream"],
[DevicePlatform.WinUI] = [".json"],
[DevicePlatform.iOS] = ["public.json", "public.data"],
[DevicePlatform.macOS] = ["public.json", "public.data"]
});
private static readonly FilePickerFileType BackupPickerFileTypes = new(new Dictionary<DevicePlatform, IEnumerable<string>>
{
[DevicePlatform.Android] = ["application/zip", "application/x-zip-compressed", "application/octet-stream"],
[DevicePlatform.WinUI] = [".zip"],
[DevicePlatform.iOS] = ["public.zip-archive", "public.data"],
[DevicePlatform.macOS] = ["public.zip-archive", "public.data"]
});
private async Task LoadProjectVersion() private async Task LoadProjectVersion()
{ {
if (SelectedProject is null || string.IsNullOrWhiteSpace(SelectedVersionId)) return; if (SelectedProject is null || string.IsNullOrWhiteSpace(SelectedVersionId)) return;
@@ -2121,6 +2219,7 @@
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
IncomingFiles.FileAvailable -= OnIncomingFileAvailable;
CancelCatalogWork(); CancelCatalogWork();
LocationService.StopTracking(); LocationService.StopTracking();
GpsLookupCancellation?.Cancel(); GpsLookupCancellation?.Cancel();
+13 -5
View File
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Razor"> <Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks> <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks> <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET --> <!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> --> <!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
@@ -28,8 +28,8 @@
<ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId> <ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>0.4.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.4.9</ApplicationDisplayVersion>
<ApplicationVersion>42</ApplicationVersion> <ApplicationVersion>44</ApplicationVersion>
<Version>$(ApplicationDisplayVersion)</Version> <Version>$(ApplicationDisplayVersion)</Version>
<!-- 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 -->
@@ -43,6 +43,14 @@
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release' and $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<AndroidTargetSdkVersion>36</AndroidTargetSdkVersion>
<AndroidPackageFormat>aab</AndroidPackageFormat>
<AndroidUseAapt2>true</AndroidUseAapt2>
<AndroidCreatePackagePerAbi>false</AndroidCreatePackagePerAbi>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- App Icon --> <!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#075E54" /> <MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#075E54" />
@@ -64,7 +72,7 @@
<ItemGroup> <ItemGroup>
<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="10.0.12" />
<PackageReference Include="Parquet.Net" Version="6.1.0" /> <PackageReference Include="Parquet.Net" Version="6.1.0" />
</ItemGroup> </ItemGroup>
+1
View File
@@ -26,6 +26,7 @@ public static class MauiProgram
builder.Services.AddSingleton<MapExportService>(); builder.Services.AddSingleton<MapExportService>();
builder.Services.AddSingleton<CloudSyncService>(); builder.Services.AddSingleton<CloudSyncService>();
builder.Services.AddSingleton<AppUpdateService>(); builder.Services.AddSingleton<AppUpdateService>();
builder.Services.AddSingleton(IncomingFileService.Shared);
#if DEBUG #if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools(); builder.Services.AddBlazorWebViewDeveloperTools();
+67 -2
View File
@@ -1,10 +1,75 @@
using Android.App; using Android.App;
using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.Database;
using Android.OS; using Android.OS;
using Android.Provider;
using LMEMappeCatastaliApp.Services;
namespace LMEMappeCatastaliApp; namespace LMEMappeCatastaliApp;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "application/json")]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "application/geo+json")]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "application/geojson")]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "application/vnd.google-earth.kml+xml")]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "application/vnd.google-earth.kmz")]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "application/xml")]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "text/xml")]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "application/zip")]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "application/x-zip-compressed")]
[IntentFilter([Intent.ActionView], Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable], DataMimeType = "application/octet-stream")]
public class MainActivity : MauiAppCompatActivity public class MainActivity : MauiAppCompatActivity
{ {
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
QueueViewedFile(Intent);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
Intent = intent;
QueueViewedFile(intent);
}
private void QueueViewedFile(Intent? intent)
{
if (intent?.Action == Android.Content.Intent.ActionView && intent.Data is not null)
{
_ = CopyViewedFileAsync(intent);
}
}
private async Task CopyViewedFileAsync(Intent intent)
{
try
{
var uri = intent.Data!;
var fileName = ReadDisplayName(uri) ?? $"file-{Guid.NewGuid():N}";
fileName = string.Concat(fileName.Select(character => Path.GetInvalidFileNameChars().Contains(character) ? '_' : character));
var directory = Path.Combine(FileSystem.CacheDirectory, "Incoming");
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, $"{Guid.NewGuid():N}-{fileName}");
await using var input = ContentResolver!.OpenInputStream(uri)
?? throw new IOException("Impossibile leggere il file selezionato.");
await using var output = File.Create(path);
await input.CopyToAsync(output);
IncomingFileService.Shared.Enqueue(new IncomingFile(path, fileName, intent.Type));
}
catch (Exception error)
{
System.Diagnostics.Debug.WriteLine($"Apertura file LME non riuscita: {error}");
}
}
private string? ReadDisplayName(Android.Net.Uri uri)
{
if (uri.Scheme == ContentResolver.SchemeFile) return Path.GetFileName(uri.Path);
using ICursor? cursor = ContentResolver?.Query(uri, [IOpenableColumns.DisplayName], null, null, null);
if (cursor is null || !cursor.MoveToFirst()) return null;
var index = cursor.GetColumnIndex(IOpenableColumns.DisplayName);
return index >= 0 ? cursor.GetString(index) : null;
}
} }
+11
View File
@@ -2,6 +2,17 @@
App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata per consultare mappe catastali e lavorare sul campo con GPS. App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata per consultare mappe catastali e lavorare sul campo con GPS.
## Versione 0.4.9
- Linee e punti KML/KMZ vengono sempre importati, anche quando il documento viene riconosciuto come catastale; il filtro particelle separa soltanto i poligoni di foglio o cornice.
## Versione 0.4.8
- Aggiornamento a .NET 10 e target Android API 36.
- KML e KMZ generici conservano punti, linee e poligoni; il filtro particelle viene applicato soltanto ai file riconosciuti come catastali.
- Android propone LME tra le app disponibili per aprire KML, KMZ, GeoJSON, progetti JSON e backup ZIP.
- I selettori file Android mostrano correttamente progetti condivisi e backup con estensione composta.
## Versione 0.4.7 ## Versione 0.4.7
- La ricerca nel pannello Layer conserva correttamente posizione e selezione del cursore durante il filtraggio; digitando `pio` il testo non viene piu invertito in `oip`. - La ricerca nel pannello Layer conserva correttamente posizione e selezione del cursore durante il filtraggio; digitando `pio` il testo non viene piu invertito in `oip`.
+20
View File
@@ -0,0 +1,20 @@
using System.Collections.Concurrent;
namespace LMEMappeCatastaliApp.Services;
public sealed class IncomingFileService
{
private readonly ConcurrentQueue<IncomingFile> _files = new();
public static IncomingFileService Shared { get; } = new();
public event Action? FileAvailable;
public void Enqueue(IncomingFile file)
{
_files.Enqueue(file);
MainThread.BeginInvokeOnMainThread(() => FileAvailable?.Invoke());
}
public bool TryDequeue(out IncomingFile? file) => _files.TryDequeue(out file);
}
public sealed record IncomingFile(string Path, string FileName, string? ContentType);
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.401",
"rollForward": "latestPatch"
}
}
+52
View File
@@ -0,0 +1,52 @@
param([string]$Configuration = "Release")
$ErrorActionPreference = "Stop"
$projectRoot = Split-Path -Parent $PSScriptRoot
$dotnet = Join-Path $env:USERPROFILE ".dotnet10\dotnet.exe"
$androidSdk = Join-Path $env:LOCALAPPDATA "Android\Sdk"
$keyStore = Join-Path $env:LOCALAPPDATA "Xamarin\Mono for Android\debug.keystore"
$outputDirectory = Join-Path $projectRoot "artifacts\android-test"
$projectPath = Join-Path $projectRoot "LMEMappeCatastaliApp.csproj"
[xml]$projectXml = Get-Content -Raw -LiteralPath $projectPath
$appVersion = ($projectXml.Project.PropertyGroup | Where-Object ApplicationDisplayVersion | Select-Object -First 1).ApplicationDisplayVersion
foreach ($requiredPath in @($dotnet, $androidSdk, $keyStore)) {
if (-not (Test-Path -LiteralPath $requiredPath)) { throw "Componente richiesto non trovato: $requiredPath" }
}
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
$packages = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
foreach ($format in @("apk", "aab")) {
$publishArguments = @(
"publish", $projectPath,
"-f", "net10.0-android",
"-c", $Configuration,
"-p:AndroidSdkDirectory=$androidSdk",
"-p:AndroidPackageFormat=$format",
"-p:RunAOTCompilation=false",
"-p:AndroidKeyStore=true",
"-p:AndroidSigningKeyStore=$keyStore",
"-p:AndroidSigningKeyAlias=androiddebugkey",
"-p:AndroidSigningStorePass=android",
"-p:AndroidSigningKeyPass=android"
)
& $dotnet @publishArguments
if ($LASTEXITCODE -ne 0) { throw "Pubblicazione Android $format non riuscita." }
$publishDirectory = Join-Path $projectRoot "bin\$Configuration\net10.0-android\publish"
$package = Get-ChildItem -LiteralPath $publishDirectory -Filter "*-Signed.$format" -File |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($null -eq $package) { throw "Nessun pacchetto $format firmato prodotto." }
$destination = Join-Path $outputDirectory "LME-Mappe-Catastali-$appVersion.$format"
Copy-Item -LiteralPath $package.FullName -Destination $destination -Force
$packages.Add((Get-Item -LiteralPath $destination))
}
$packages | ForEach-Object {
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
"$($hash.Hash.ToLowerInvariant()) $($_.Name)"
} | Set-Content -LiteralPath (Join-Path $outputDirectory "SHA256SUMS.txt") -Encoding ascii
$packages | Select-Object Name, Length, LastWriteTime
+57 -17
View File
@@ -167,6 +167,7 @@ window.lmeMap = {
clearOfflineTiles, clearOfflineTiles,
getOfflineTileStats, getOfflineTileStats,
resize, resize,
importExternalGisFile,
showInfo: setInfo showInfo: setInfo
}; };
@@ -737,6 +738,28 @@ function bindFileImport() {
if (file.size > 100 * 1024 * 1024) throw new Error("file superiore a 100 MB; suddividilo prima dell'importazione"); if (file.size > 100 * 1024 * 1024) throw new Error("file superiore a 100 MB; suddividilo prima dell'importazione");
setInfo(`Analisi di ${file.name} (${(file.size / 1024 / 1024).toFixed(1)} MB)...`); setInfo(`Analisi di ${file.name} (${(file.size / 1024 / 1024).toFixed(1)} MB)...`);
await new Promise(resolve => window.setTimeout(resolve, 20)); await new Promise(resolve => window.setTimeout(resolve, 20));
await importGisFile(file);
} catch (error) {
setInfo(`File non leggibile: ${error.message}`);
} finally {
event.target.value = "";
}
});
lmeState.fileImportBound = true;
}
async function importExternalGisFile(fileName, base64Content, mimeType) {
const binary = atob(base64Content);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
const file = new File([bytes], fileName, { type: mimeType || "application/octet-stream" });
if (file.size > 100 * 1024 * 1024) throw new Error("file superiore a 100 MB; suddividilo prima dell'importazione");
setInfo(`Analisi di ${file.name} (${(file.size / 1024 / 1024).toFixed(1)} MB)...`);
await new Promise(resolve => window.setTimeout(resolve, 20));
return importGisFile(file);
}
async function importGisFile(file) {
const result = await readGisFile(file); const result = await readGisFile(file);
rememberMapChange(); rememberMapChange();
const geojson = result.geojson; const geojson = result.geojson;
@@ -748,25 +771,16 @@ function bindFileImport() {
refreshImportedMeasurements(); refreshImportedMeasurements();
const featureCount = geojson.features.length; const featureCount = geojson.features.length;
const sheetCount = sheetGeojson.features.length; const sheetCount = sheetGeojson.features.length;
const ignored = result.ignoredCount const ignored = result.ignoredCount ? `; esclusi ${result.ignoredCount} elementi del foglio o non particella` : "";
? `; esclusi ${result.ignoredCount} elementi del foglio o non particella` const includedSheets = sheetCount ? `; ${sheetCount} elementi foglio ${lmeState.importedSheetsVisible ? "visibili" : "nascosti"}` : "";
: "";
const includedSheets = sheetCount
? `; ${sheetCount} elementi foglio ${lmeState.importedSheetsVisible ? "visibili" : "nascosti"}`
: "";
const reprojected = result.reprojected ? "; coordinate Web Mercator convertite" : ""; const reprojected = result.reprojected ? "; coordinate Web Mercator convertite" : "";
const warnings = result.validationWarnings ? `; ${result.validationWarnings} geometrie da verificare` : ""; const warnings = result.validationWarnings ? `; ${result.validationWarnings} geometrie da verificare` : "";
setInfo(`Caricato ${file.name}: ${featureCount} particelle/elementi${includedSheets}${ignored}${reprojected}${warnings}.`); const message = `Caricato ${file.name}: ${featureCount} particelle/elementi${includedSheets}${ignored}${reprojected}${warnings}.`;
setInfo(message);
updateGpsInsideState(); updateGpsInsideState();
refreshLayerManager(); refreshLayerManager();
finishMapChange(); finishMapChange();
} catch (error) { return message;
setInfo(`File non leggibile: ${error.message}`);
} finally {
event.target.value = "";
}
});
lmeState.fileImportBound = true;
} }
async function readGisFile(file) { async function readGisFile(file) {
@@ -985,17 +999,27 @@ function normalizeKmlFiles(files) {
throw new Error("KML senza geometrie utilizzabili"); throw new Error("KML senza geometrie utilizzabili");
} }
if (!looksLikeCadastralKml(features)) {
return {
geojson: { type: "FeatureCollection", features },
sheetGeojson: emptyFeatureCollection(),
ignoredCount: 0,
reprojected: false
};
}
const polygonParts = splitKmlPolygonFeatures(features); const polygonParts = splitKmlPolygonFeatures(features);
const parcels = filterKmlParcelFeatures(polygonParts); const parcels = filterKmlParcelFeatures(polygonParts);
const parcelSet = new Set(parcels); const parcelSet = new Set(parcels);
const sheetParts = polygonParts.filter(feature => !parcelSet.has(feature)); const sheetParts = polygonParts.filter(feature => !parcelSet.has(feature));
const nonPolygonFeatures = features.filter(feature => geometryPolygons(feature.geometry).length === 0); const nonPolygonFeatures = features.filter(feature => geometryPolygons(feature.geometry).length === 0);
const optionalFeatures = [...sheetParts, ...nonPolygonFeatures]; const importedFeatures = [...parcels, ...nonPolygonFeatures];
if (!parcels.length && !(lmeState.includeKmlSheetLayer && optionalFeatures.length)) { const optionalFeatures = sheetParts;
if (!importedFeatures.length && !(lmeState.includeKmlSheetLayer && optionalFeatures.length)) {
throw new Error("nessuna particella trovata; attiva 'includi layer del foglio' per importare gli altri elementi"); throw new Error("nessuna particella trovata; attiva 'includi layer del foglio' per importare gli altri elementi");
} }
return { return {
geojson: { type: "FeatureCollection", features: parcels }, geojson: { type: "FeatureCollection", features: importedFeatures },
sheetGeojson: { sheetGeojson: {
type: "FeatureCollection", type: "FeatureCollection",
features: lmeState.includeKmlSheetLayer ? optionalFeatures : [] features: lmeState.includeKmlSheetLayer ? optionalFeatures : []
@@ -1005,6 +1029,22 @@ function normalizeKmlFiles(files) {
}; };
} }
function looksLikeCadastralKml(features) {
return features.some(feature => {
const properties = feature?.properties || {};
const name = String(properties.name || properties.Name || properties.LABEL || "").trim();
const context = [
name,
properties.layer,
properties.Layer,
properties.type,
properties.Tipo,
properties._lmeFolder
].filter(Boolean).join(" ").toLowerCase();
return /\b(particell|mappal|catast|foglio|parcel|cadastral)\w*/i.test(context);
});
}
function kmlTextToGeoJson(text, sourceName) { function kmlTextToGeoJson(text, sourceName) {
const xml = new DOMParser().parseFromString(text, "text/xml"); const xml = new DOMParser().parseFromString(text, "text/xml");
if (xml.getElementsByTagName("parsererror").length) { if (xml.getElementsByTagName("parsererror").length) {