Prepare Pro 1.0.1 imports and license tooling
This commit is contained in:
1 parent
62f28c5621
commit
f4b3ca6ea8
13 files changed
+437
-48
No files matched your search
+128
-3
@@ -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
|
||||||
@inject AppLocalizationService L10n
|
@inject AppLocalizationService L10n
|
||||||
@inject EntitlementService Entitlements
|
@inject EntitlementService Entitlements
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
@@ -741,6 +742,7 @@
|
|||||||
private bool NavigationMenuOpen { get; set; }
|
private bool NavigationMenuOpen { get; set; }
|
||||||
private bool HasUnsavedChanges { get; set; }
|
private bool HasUnsavedChanges { get; set; }
|
||||||
private DotNetObjectReference<Home>? MapCallbackReference { get; set; }
|
private DotNetObjectReference<Home>? MapCallbackReference { get; set; }
|
||||||
|
private bool MapReady { get; set; }
|
||||||
private bool IncludeCadastralParcels { get; set; } = true;
|
private bool IncludeCadastralParcels { get; set; } = true;
|
||||||
private bool IncludeKmlSheetLayer { get; set; }
|
private bool IncludeKmlSheetLayer { get; set; }
|
||||||
private bool ShowImportedSheetLayer { get; set; }
|
private bool ShowImportedSheetLayer { get; set; }
|
||||||
@@ -830,6 +832,7 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
IncomingFiles.FileAvailable += OnIncomingFileAvailable;
|
||||||
L10n.Load();
|
L10n.Load();
|
||||||
L10n.ApplyCulture();
|
L10n.ApplyCulture();
|
||||||
await Entitlements.LoadAsync();
|
await Entitlements.LoadAsync();
|
||||||
@@ -878,6 +881,7 @@
|
|||||||
{
|
{
|
||||||
MapCallbackReference = DotNetObjectReference.Create(this);
|
MapCallbackReference = DotNetObjectReference.Create(this);
|
||||||
await JS.InvokeVoidAsync("lmeMap.init", MapCallbackReference);
|
await JS.InvokeVoidAsync("lmeMap.init", MapCallbackReference);
|
||||||
|
MapReady = true;
|
||||||
await ApplyLocalizationToMapAsync();
|
await ApplyLocalizationToMapAsync();
|
||||||
await ApplyEntitlementsToMapAsync();
|
await ApplyEntitlementsToMapAsync();
|
||||||
await ApplyFlagPreferencesToMapAsync();
|
await ApplyFlagPreferencesToMapAsync();
|
||||||
@@ -886,6 +890,7 @@
|
|||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1582,7 +1587,11 @@
|
|||||||
private async Task RestoreBackup()
|
private async Task RestoreBackup()
|
||||||
{
|
{
|
||||||
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
|
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
|
||||||
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);
|
||||||
@@ -1593,7 +1602,11 @@
|
|||||||
private async Task ImportProject()
|
private async Task ImportProject()
|
||||||
{
|
{
|
||||||
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
|
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
|
||||||
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);
|
||||||
@@ -1612,6 +1625,117 @@
|
|||||||
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))
|
||||||
|
{
|
||||||
|
if (!CanUse(AppFeatures.ProjectArchive))
|
||||||
|
{
|
||||||
|
await ShowProRequiredAsync();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
if (!CanUse(AppFeatures.ProjectArchive))
|
||||||
|
{
|
||||||
|
await ShowProRequiredAsync();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1928,7 +2052,7 @@
|
|||||||
UpdateStatus = $"Installata {AppUpdateService.CurrentVersion}. Disponibile {update.Version}.";
|
UpdateStatus = $"Installata {AppUpdateService.CurrentVersion}. Disponibile {update.Version}.";
|
||||||
UpdateDownloadUrl = update.DownloadUrl;
|
UpdateDownloadUrl = update.DownloadUrl;
|
||||||
}
|
}
|
||||||
else UpdateStatus = $"Versione {AppUpdateService.CurrentVersion} aggiornata. Ultima release Gitea: {update.Version}.";
|
else UpdateStatus = $"Versione {AppUpdateService.CurrentVersion} aggiornata. Ultima release Pro su Gitea: {update.Version}.";
|
||||||
}
|
}
|
||||||
catch (Exception error) { UpdateStatus = $"Controllo non riuscito: {error.GetBaseException().Message}"; }
|
catch (Exception error) { UpdateStatus = $"Controllo non riuscito: {error.GetBaseException().Message}"; }
|
||||||
finally { UpdateBusy = false; }
|
finally { UpdateBusy = false; }
|
||||||
@@ -2349,6 +2473,7 @@
|
|||||||
|
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
|
IncomingFiles.FileAvailable -= OnIncomingFileAvailable;
|
||||||
CancelCatalogWork();
|
CancelCatalogWork();
|
||||||
LocationService.StopTracking();
|
LocationService.StopTracking();
|
||||||
GpsLookupCancellation?.Cancel();
|
GpsLookupCancellation?.Cancel();
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
<ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId>
|
<ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId>
|
||||||
|
|
||||||
<!-- Versions -->
|
<!-- Versions -->
|
||||||
<ApplicationDisplayVersion>1.0.0</ApplicationDisplayVersion>
|
<ApplicationDisplayVersion>1.0.1</ApplicationDisplayVersion>
|
||||||
<ApplicationVersion>100</ApplicationVersion>
|
<ApplicationVersion>101</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 -->
|
||||||
|
|||||||
@@ -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);
|
||||||
builder.Services.AddSingleton<AppLocalizationService>();
|
builder.Services.AddSingleton<AppLocalizationService>();
|
||||||
builder.Services.AddSingleton<ManualLicenseService>();
|
builder.Services.AddSingleton<ManualLicenseService>();
|
||||||
#if ANDROID
|
#if ANDROID
|
||||||
|
|||||||
@@ -1,10 +1,83 @@
|
|||||||
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 == 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
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 Pro 1.0.1 test
|
||||||
|
|
||||||
|
- Il controllo aggiornamenti Pro ignora le release e gli APK della versione Base e segue esclusivamente il canale Pro.
|
||||||
|
- 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.
|
||||||
|
- Il gestore licenze mantiene una chiave locale incompatibile come backup e consente il ripristino della chiave corretta senza arrestarsi.
|
||||||
|
- I comandi di backup e ripristino chiave restano visibili anche su finestre strette.
|
||||||
|
|
||||||
## Versione 0.4.7
|
## Versione 0.4.7
|
||||||
|
|
||||||
## Preparazione versione Pro Play Store
|
## Preparazione versione Pro Play Store
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace LMEMappeCatastaliApp.Services;
|
namespace LMEMappeCatastaliApp.Services;
|
||||||
|
|
||||||
public sealed class AppUpdateService
|
public sealed class AppUpdateService
|
||||||
{
|
{
|
||||||
private const string LatestReleaseUrl = "https://gitea.lmcloud.it/api/v1/repos/livio/LMEMappeCatastaliApp/releases/latest";
|
private const string ReleasesUrl = "https://gitea.lmcloud.it/api/v1/repos/livio/LMEMappeCatastaliProApp/releases?limit=50";
|
||||||
|
private static readonly Regex ProTagPattern = new(
|
||||||
|
@"^v?(?<version>\d+\.\d+\.\d+)-pro(?:[-.].*)?$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(20) };
|
private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(20) };
|
||||||
|
|
||||||
public static string CurrentVersion =>
|
public static string CurrentVersion =>
|
||||||
@@ -17,21 +21,53 @@ public sealed class AppUpdateService
|
|||||||
|
|
||||||
public async Task<AppUpdateResult> CheckAsync(CancellationToken cancellationToken = default)
|
public async Task<AppUpdateResult> CheckAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var release = await _httpClient.GetFromJsonAsync<GiteaRelease>(LatestReleaseUrl, cancellationToken)
|
var releases = await _httpClient.GetFromJsonAsync<GiteaRelease[]>(ReleasesUrl, cancellationToken)
|
||||||
?? throw new InvalidDataException("Risposta aggiornamenti non valida.");
|
?? throw new InvalidDataException("Risposta aggiornamenti non valida.");
|
||||||
var latestText = (release.TagName ?? string.Empty).TrimStart('v', 'V');
|
GiteaRelease? selectedRelease = null;
|
||||||
|
GiteaAsset? selectedApk = null;
|
||||||
|
Version? selectedVersion = null;
|
||||||
|
foreach (var release in releases.Where(item => !item.Draft))
|
||||||
|
{
|
||||||
|
var match = ProTagPattern.Match(release.TagName ?? string.Empty);
|
||||||
|
var apk = release.Assets?.FirstOrDefault(item => IsProApk(item.Name));
|
||||||
|
if (!match.Success || apk is null || !Version.TryParse(match.Groups["version"].Value, out var version))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedVersion is null || version > selectedVersion)
|
||||||
|
{
|
||||||
|
selectedRelease = release;
|
||||||
|
selectedApk = apk;
|
||||||
|
selectedVersion = version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedRelease is null || selectedApk is null || selectedVersion is null)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Nessuna release compatibile con LME Mappe Catastali Pro trovata.");
|
||||||
|
}
|
||||||
|
|
||||||
_ = Version.TryParse(CurrentVersion, out var current);
|
_ = Version.TryParse(CurrentVersion, out var current);
|
||||||
_ = Version.TryParse(latestText, out var latest);
|
return new(
|
||||||
var apk = release.Assets?.FirstOrDefault(item => item.Name?.EndsWith(".apk", StringComparison.OrdinalIgnoreCase) == true);
|
current is not null && selectedVersion > current,
|
||||||
return new(latest is not null && current is not null && latest > current, latestText, release.HtmlUrl ?? string.Empty, apk?.BrowserDownloadUrl ?? release.HtmlUrl ?? string.Empty, release.Body ?? string.Empty);
|
selectedVersion.ToString(3),
|
||||||
|
selectedRelease.HtmlUrl ?? string.Empty,
|
||||||
|
selectedApk.BrowserDownloadUrl ?? selectedRelease.HtmlUrl ?? string.Empty,
|
||||||
|
selectedRelease.Body ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsProApk(string? name) =>
|
||||||
|
name?.StartsWith("LME-Mappe-Catastali-Pro-", StringComparison.OrdinalIgnoreCase) == true
|
||||||
|
&& name.EndsWith(".apk", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private sealed class GiteaRelease
|
private sealed class GiteaRelease
|
||||||
{
|
{
|
||||||
[JsonPropertyName("tag_name")] public string? TagName { get; set; }
|
[JsonPropertyName("tag_name")] public string? TagName { get; set; }
|
||||||
[JsonPropertyName("html_url")] public string? HtmlUrl { get; set; }
|
[JsonPropertyName("html_url")] public string? HtmlUrl { get; set; }
|
||||||
[JsonPropertyName("assets")] public GiteaAsset[]? Assets { get; set; }
|
[JsonPropertyName("assets")] public GiteaAsset[]? Assets { get; set; }
|
||||||
[JsonPropertyName("body")] public string? Body { get; set; }
|
[JsonPropertyName("body")] public string? Body { get; set; }
|
||||||
|
[JsonPropertyName("draft")] public bool Draft { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class GiteaAsset
|
private sealed class GiteaAsset
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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);
|
||||||
@@ -11,6 +11,12 @@ $signingDirectory = Join-Path $env:LOCALAPPDATA "LME\Signing"
|
|||||||
$keyStore = Join-Path $signingDirectory "lme-pro-test.keystore"
|
$keyStore = Join-Path $signingDirectory "lme-pro-test.keystore"
|
||||||
$passwordFile = Join-Path $signingDirectory "lme-pro-test-password.dpapi"
|
$passwordFile = Join-Path $signingDirectory "lme-pro-test-password.dpapi"
|
||||||
$outputDirectory = Join-Path $projectRoot "artifacts\android-test"
|
$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
|
||||||
|
if ([string]::IsNullOrWhiteSpace($appVersion)) {
|
||||||
|
throw "ApplicationDisplayVersion non trovata nel progetto."
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($requiredPath in @($dotnet, $keytool, $androidSdk)) {
|
foreach ($requiredPath in @($dotnet, $keytool, $androidSdk)) {
|
||||||
if (-not (Test-Path -LiteralPath $requiredPath)) {
|
if (-not (Test-Path -LiteralPath $requiredPath)) {
|
||||||
@@ -50,7 +56,7 @@ $env:LME_ANDROID_TEST_KEY_PASS = $plainPassword
|
|||||||
$copiedPackages = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
|
$copiedPackages = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
|
||||||
try {
|
try {
|
||||||
foreach ($format in @("apk", "aab")) {
|
foreach ($format in @("apk", "aab")) {
|
||||||
& $dotnet publish (Join-Path $projectRoot "LMEMappeCatastaliApp.csproj") `
|
& $dotnet publish $projectPath `
|
||||||
-f net10.0-android `
|
-f net10.0-android `
|
||||||
-c $Configuration `
|
-c $Configuration `
|
||||||
-p:AndroidSdkDirectory=$androidSdk `
|
-p:AndroidSdkDirectory=$androidSdk `
|
||||||
@@ -72,7 +78,7 @@ try {
|
|||||||
throw "Nessun pacchetto $format firmato prodotto in $publishDirectory"
|
throw "Nessun pacchetto $format firmato prodotto in $publishDirectory"
|
||||||
}
|
}
|
||||||
|
|
||||||
$destination = Join-Path $outputDirectory "LME-Mappe-Catastali-Pro-1.0.0-test.$format"
|
$destination = Join-Path $outputDirectory "LME-Mappe-Catastali-Pro-$appVersion-test.$format"
|
||||||
Copy-Item -LiteralPath $package.FullName -Destination $destination -Force
|
Copy-Item -LiteralPath $package.FullName -Destination $destination -Force
|
||||||
$copiedPackages.Add((Get-Item -LiteralPath $destination))
|
$copiedPackages.Add((Get-Item -LiteralPath $destination))
|
||||||
}
|
}
|
||||||
@@ -89,6 +95,12 @@ $filesToHash = @($copiedPackages)
|
|||||||
if (Test-Path -LiteralPath $licenseManagerArchive) {
|
if (Test-Path -LiteralPath $licenseManagerArchive) {
|
||||||
$filesToHash += Get-Item -LiteralPath $licenseManagerArchive
|
$filesToHash += Get-Item -LiteralPath $licenseManagerArchive
|
||||||
}
|
}
|
||||||
|
$publicKeySource = Join-Path $projectRoot "tools\LmeLicenseManager\manual-license-public.pem"
|
||||||
|
if (Test-Path -LiteralPath $publicKeySource) {
|
||||||
|
$publicKeyDestination = Join-Path $outputDirectory "manual-license-public.pem"
|
||||||
|
Copy-Item -LiteralPath $publicKeySource -Destination $publicKeyDestination -Force
|
||||||
|
$filesToHash += Get-Item -LiteralPath $publicKeyDestination
|
||||||
|
}
|
||||||
|
|
||||||
$hashLines = $filesToHash | ForEach-Object {
|
$hashLines = $filesToHash | ForEach-Object {
|
||||||
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
|
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
|
||||||
|
|||||||
@@ -240,14 +240,25 @@ internal sealed class LicenseRepository
|
|||||||
{
|
{
|
||||||
if (File.Exists(PrivateKeyPath))
|
if (File.Exists(PrivateKeyPath))
|
||||||
{
|
{
|
||||||
using var existing = LoadPrivateKey();
|
try
|
||||||
if (!_allowKeyGeneration && !IsExpectedSigningKey(existing))
|
|
||||||
{
|
{
|
||||||
throw new CryptographicException("La chiave locale non corrisponde alla chiave pubblica incorporata in LME Pro.");
|
using var existing = LoadPrivateKey();
|
||||||
}
|
if (!_allowKeyGeneration && !IsExpectedSigningKey(existing))
|
||||||
|
{
|
||||||
|
PreserveIncompatiblePrivateKey();
|
||||||
|
File.WriteAllText(PublicKeyPath, ExpectedPublicKeyPem, Encoding.ASCII);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
File.WriteAllText(PublicKeyPath, existing.ExportSubjectPublicKeyInfoPem(), Encoding.ASCII);
|
File.WriteAllText(PublicKeyPath, existing.ExportSubjectPublicKeyInfoPem(), Encoding.ASCII);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
catch (CryptographicException) when (!_allowKeyGeneration)
|
||||||
|
{
|
||||||
|
PreserveIncompatiblePrivateKey();
|
||||||
|
File.WriteAllText(PublicKeyPath, ExpectedPublicKeyPem, Encoding.ASCII);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_allowKeyGeneration)
|
if (!_allowKeyGeneration)
|
||||||
@@ -265,6 +276,15 @@ internal sealed class LicenseRepository
|
|||||||
File.SetAttributes(PrivateKeyPath, FileAttributes.Hidden);
|
File.SetAttributes(PrivateKeyPath, FileAttributes.Hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PreserveIncompatiblePrivateKey()
|
||||||
|
{
|
||||||
|
File.SetAttributes(PrivateKeyPath, FileAttributes.Normal);
|
||||||
|
var preservedPath = Path.Combine(
|
||||||
|
_directory,
|
||||||
|
$"manual-license-private-incompatible-{DateTime.UtcNow:yyyyMMddHHmmss}.dpapi");
|
||||||
|
File.Move(PrivateKeyPath, preservedPath);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsExpectedSigningKey(RSA rsa)
|
private static bool IsExpectedSigningKey(RSA rsa)
|
||||||
{
|
{
|
||||||
using var expected = RSA.Create();
|
using var expected = RSA.Create();
|
||||||
|
|||||||
@@ -65,16 +65,23 @@ internal sealed class MainForm : Form
|
|||||||
};
|
};
|
||||||
panel.Controls.Add(text, 0, 0);
|
panel.Controls.Add(text, 0, 0);
|
||||||
|
|
||||||
var commands = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight, Margin = new Padding(8, 0, 0, 12) };
|
var commands = new TableLayoutPanel
|
||||||
|
{
|
||||||
|
AutoSize = true,
|
||||||
|
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
||||||
|
ColumnCount = 1,
|
||||||
|
RowCount = 3,
|
||||||
|
Margin = new Padding(8, 0, 0, 12)
|
||||||
|
};
|
||||||
var openFolder = new Button { Text = "Apri archivio", AutoSize = true };
|
var openFolder = new Button { Text = "Apri archivio", AutoSize = true };
|
||||||
openFolder.Click += (_, _) => Process.Start(new ProcessStartInfo("explorer.exe", _repository.StoragePath) { UseShellExecute = true });
|
openFolder.Click += (_, _) => Process.Start(new ProcessStartInfo("explorer.exe", _repository.StoragePath) { UseShellExecute = true });
|
||||||
var exportKey = new Button { Text = "Backup chiave", AutoSize = true };
|
var exportKey = new Button { Text = "Backup chiave", AutoSize = true };
|
||||||
exportKey.Click += (_, _) => ExportSigningKey();
|
exportKey.Click += (_, _) => ExportSigningKey();
|
||||||
var importKey = new Button { Text = "Ripristina chiave", AutoSize = true };
|
var importKey = new Button { Text = "Ripristina chiave", AutoSize = true };
|
||||||
importKey.Click += (_, _) => ImportSigningKey();
|
importKey.Click += (_, _) => ImportSigningKey();
|
||||||
commands.Controls.Add(openFolder);
|
commands.Controls.Add(openFolder, 0, 0);
|
||||||
commands.Controls.Add(exportKey);
|
commands.Controls.Add(exportKey, 0, 1);
|
||||||
commands.Controls.Add(importKey);
|
commands.Controls.Add(importKey, 0, 2);
|
||||||
panel.Controls.Add(commands, 1, 0);
|
panel.Controls.Add(commands, 1, 0);
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,21 @@ internal static class Program
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
private static void Main(string[] args)
|
private static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
if (args.Length == 2 && args[0].Equals("--export-key", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var password = Environment.GetEnvironmentVariable("LME_LICENSE_BACKUP_PASSWORD");
|
||||||
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Imposta LME_LICENSE_BACKUP_PASSWORD per esportare la chiave.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var exportRepository = new LicenseRepository();
|
||||||
|
exportRepository.Initialize();
|
||||||
|
exportRepository.ExportSigningKeyBackup(Path.GetFullPath(args[1]), password);
|
||||||
|
Console.WriteLine($"Backup chiave creato: {Path.GetFullPath(args[1])}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Contains("--self-test", StringComparer.OrdinalIgnoreCase))
|
if (args.Contains("--self-test", StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
RunSelfTest();
|
RunSelfTest();
|
||||||
@@ -26,6 +41,7 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
var testDirectory = Path.Combine(Path.GetTempPath(), $"LME-License-SelfTest-{Guid.NewGuid():N}");
|
var testDirectory = Path.Combine(Path.GetTempPath(), $"LME-License-SelfTest-{Guid.NewGuid():N}");
|
||||||
var importedDirectory = Path.Combine(Path.GetTempPath(), $"LME-License-SelfTest-{Guid.NewGuid():N}");
|
var importedDirectory = Path.Combine(Path.GetTempPath(), $"LME-License-SelfTest-{Guid.NewGuid():N}");
|
||||||
|
var incompatibleDirectory = Path.Combine(Path.GetTempPath(), $"LME-License-SelfTest-{Guid.NewGuid():N}");
|
||||||
const string backupPassword = "LME-Test-Backup-Password-2026";
|
const string backupPassword = "LME-Test-Backup-Password-2026";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -60,11 +76,21 @@ internal static class Program
|
|||||||
throw new InvalidOperationException("La chiave importata non produce licenze compatibili.");
|
throw new InvalidOperationException("La chiave importata non produce licenze compatibili.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var incompatibleRepository = new LicenseRepository(incompatibleDirectory, allowKeyGeneration: true);
|
||||||
|
incompatibleRepository.Initialize();
|
||||||
|
var strictRepository = new LicenseRepository(incompatibleDirectory);
|
||||||
|
strictRepository.Initialize();
|
||||||
|
if (File.Exists(Path.Combine(incompatibleDirectory, "manual-license-private.dpapi"))
|
||||||
|
|| Directory.GetFiles(incompatibleDirectory, "manual-license-private-incompatible-*.dpapi").Length != 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("La chiave incompatibile non e stata conservata correttamente.");
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine("Self-test licenze e portabilita chiave completato.");
|
Console.WriteLine("Self-test licenze e portabilita chiave completato.");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
foreach (var directory in new[] { testDirectory, importedDirectory })
|
foreach (var directory in new[] { testDirectory, importedDirectory, incompatibleDirectory })
|
||||||
{
|
{
|
||||||
var fullPath = Path.GetFullPath(directory);
|
var fullPath = Path.GetFullPath(directory);
|
||||||
var tempRoot = Path.GetFullPath(Path.GetTempPath());
|
var tempRoot = Path.GetFullPath(Path.GetTempPath());
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAmjh4E1fpaaonATNDipsR
|
||||||
|
1iy5cNSStD7fgB7i5xJTSZV36Td1B7hQXfZB8Z7vx9iFJpbCQMePuFiDKNbTUQFf
|
||||||
|
EKEp8lBq7SHo0LvbKNOSK9RMkqpb3ova9Er14gSCgshivf1c774VXTlt9J7uAWoE
|
||||||
|
QeQMNKJUSogoMkxEKc4Xmye65lGjJVeiuqdDCJh5o42j8DfGwqOyGX4808vSeixe
|
||||||
|
3lArRmoq9SGNO05VVunF8NKJyhiRLfropUvC+eFsiJMp3VMEYVn5FUcd8ljO67jv
|
||||||
|
pnngWh6zCZxs+h0mT0YRU9F3tWHqqsG1PE6NZnspmBdNfUfb9LxKt1RshEhUWiBx
|
||||||
|
0vvdUtg20nESOvFnieIX64pcx0LqFlDMmEWDMQjZ7KEtwXVphN1mhsryUiS6Cz3b
|
||||||
|
oKTryH4FQY9iIWNNzdzXujrV5ycE49orFEtrXRPnXBlB97Hl3Dlxi19lvPwxWXQ5
|
||||||
|
fKXrTJ2rL/WvlMpZ5YOggAmv7P7tA4B+Oi5uMiHzXEDlAgMBAAE=
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
+70
-23
@@ -182,6 +182,7 @@ window.lmeMap = {
|
|||||||
resize,
|
resize,
|
||||||
setLocalization,
|
setLocalization,
|
||||||
setEntitlements,
|
setEntitlements,
|
||||||
|
importExternalGisFile,
|
||||||
showInfo: setInfo
|
showInfo: setInfo
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -779,29 +780,7 @@ 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));
|
||||||
const result = await readGisFile(file);
|
await importGisFile(file);
|
||||||
rememberMapChange();
|
|
||||||
const geojson = result.geojson;
|
|
||||||
const parcelLayer = addImportedFeatures(geojson);
|
|
||||||
const sheetGeojson = result.sheetGeojson || emptyFeatureCollection();
|
|
||||||
const sheetLayer = addImportedSheetFeatures(sheetGeojson);
|
|
||||||
applyImportedSheetVisibility();
|
|
||||||
fitLayer(geojson.features.length ? parcelLayer : sheetLayer);
|
|
||||||
refreshImportedMeasurements();
|
|
||||||
const featureCount = geojson.features.length;
|
|
||||||
const sheetCount = sheetGeojson.features.length;
|
|
||||||
const ignored = result.ignoredCount
|
|
||||||
? `; esclusi ${result.ignoredCount} elementi del foglio o non particella`
|
|
||||||
: "";
|
|
||||||
const includedSheets = sheetCount
|
|
||||||
? `; ${sheetCount} elementi foglio ${lmeState.importedSheetsVisible ? "visibili" : "nascosti"}`
|
|
||||||
: "";
|
|
||||||
const reprojected = result.reprojected ? "; coordinate Web Mercator convertite" : "";
|
|
||||||
const warnings = result.validationWarnings ? `; ${result.validationWarnings} geometrie da verificare` : "";
|
|
||||||
setInfo(`Caricato ${file.name}: ${featureCount} particelle/elementi${includedSheets}${ignored}${reprojected}${warnings}.`);
|
|
||||||
updateGpsInsideState();
|
|
||||||
refreshLayerManager();
|
|
||||||
finishMapChange();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setInfo(`File non leggibile: ${error.message}`);
|
setInfo(`File non leggibile: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -811,6 +790,49 @@ function bindFileImport() {
|
|||||||
lmeState.fileImportBound = true;
|
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);
|
||||||
|
rememberMapChange();
|
||||||
|
const geojson = result.geojson;
|
||||||
|
const parcelLayer = addImportedFeatures(geojson);
|
||||||
|
const sheetGeojson = result.sheetGeojson || emptyFeatureCollection();
|
||||||
|
const sheetLayer = addImportedSheetFeatures(sheetGeojson);
|
||||||
|
applyImportedSheetVisibility();
|
||||||
|
fitLayer(geojson.features.length ? parcelLayer : sheetLayer);
|
||||||
|
refreshImportedMeasurements();
|
||||||
|
const featureCount = geojson.features.length;
|
||||||
|
const sheetCount = sheetGeojson.features.length;
|
||||||
|
const ignored = result.ignoredCount
|
||||||
|
? `; esclusi ${result.ignoredCount} elementi del foglio o non particella`
|
||||||
|
: "";
|
||||||
|
const includedSheets = sheetCount
|
||||||
|
? `; ${sheetCount} elementi foglio ${lmeState.importedSheetsVisible ? "visibili" : "nascosti"}`
|
||||||
|
: "";
|
||||||
|
const reprojected = result.reprojected ? "; coordinate Web Mercator convertite" : "";
|
||||||
|
const warnings = result.validationWarnings ? `; ${result.validationWarnings} geometrie da verificare` : "";
|
||||||
|
const message = `Caricato ${file.name}: ${featureCount} particelle/elementi${includedSheets}${ignored}${reprojected}${warnings}.`;
|
||||||
|
setInfo(message);
|
||||||
|
updateGpsInsideState();
|
||||||
|
refreshLayerManager();
|
||||||
|
finishMapChange();
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
async function readGisFile(file) {
|
async function readGisFile(file) {
|
||||||
const lowerName = file.name.toLowerCase();
|
const lowerName = file.name.toLowerCase();
|
||||||
|
|
||||||
@@ -1027,6 +1049,15 @@ 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);
|
||||||
@@ -1047,6 +1078,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) {
|
||||||
|
|||||||
Reference in new issue
Block a user