Compare commits

...
Author SHA1 Message Date
livio 5ea424b46c Improve cadastral GPS reliability and mobile overlays 2026-10-02 08:38:14 +02:00
livio 29c3848a22 Fix managed Google Play product catalog result 2026-10-01 10:58:47 +02:00
8 changed files with 251 additions and 35 deletions

No files matched your search

+41 -5
View File
@@ -109,13 +109,18 @@
</section>
}
<div class="gps-overlay @(ActiveTab == "search" ? "search-open" : null)">
<div class="gps-overlay @(ActiveTab == "search" ? "search-open" : null) @(GpsOverlayCollapsed ? "is-collapsed" : null)">
<div class="gps-state">
<span id="gpsState">GPS non attivo</span>
<strong id="parcelState">Nessuna particella selezionata</strong>
</div>
<button class="gps-main" @onclick="ToggleGps">@(GpsActive ? "Ferma GPS" : "Avvia GPS")</button>
<button class="gps-center" @onclick="CenterOnGps" aria-label="Centra posizione">Centra</button>
<div class="gps-actions">
<button class="gps-main" @onclick="ToggleGps">@(GpsActive ? "Ferma GPS" : "Avvia GPS")</button>
<button class="gps-center" @onclick="CenterOnGps" aria-label="Centra posizione GPS">Centra</button>
</div>
<button class="gps-collapse" @onclick="ToggleGpsOverlay" aria-label="@(GpsOverlayCollapsed ? "Espandi pannello GPS" : "Riduci pannello GPS")" title="@(GpsOverlayCollapsed ? "Espandi pannello GPS" : "Riduci pannello GPS")">
<span aria-hidden="true">@(GpsOverlayCollapsed ? "+" : "−")</span>
</button>
</div>
@if (ActiveTab is not "map" and not "search")
@@ -553,6 +558,11 @@
Trasparenza catasto
<input type="range" min="0" max="1" step="0.05" value="@CadastralOpacity.ToString(System.Globalization.CultureInfo.InvariantCulture)" @onchange="ChangeCadastreOpacity" />
</label>
<label>
Durata messaggi mappa (secondi)
<input type="number" min="0" max="30" step="1" @bind="MapMessageTimeoutSeconds" @bind:after="ApplyMapMessageOptions" />
</label>
<small class="field-help">Imposta 0 per lasciare i messaggi visibili fino alla chiusura manuale.</small>
</section>
<section class="panel-section">
@@ -689,7 +699,10 @@
</aside>
}
<div id="mapInfo" class="map-info">Pronto</div>
<div id="mapInfo" class="map-info" role="status" aria-live="polite">
<span id="mapInfoText" class="map-info-text">Pronto</span>
<button type="button" class="map-info-close" onclick="window.lmeMap.hideInfo()" aria-label="Chiudi messaggio" title="Chiudi messaggio">×</button>
</div>
<div id="wmsPreview" class="wms-preview" hidden>
<section class="wms-preview-dialog" role="dialog" aria-modal="true" aria-labelledby="wmsPreviewTitle">
@@ -743,8 +756,10 @@
private string CatalogMessage { get; set; } = string.Empty;
private double? CatalogFraction { get; set; }
private double CadastralOpacity { get; set; } = 0.75;
private int MapMessageTimeoutSeconds { get; set; } = 3;
private bool CatalogBusy { get; set; }
private bool GpsActive { get; set; }
private bool GpsOverlayCollapsed { get; set; }
private bool ShowGpsPosition { get; set; } = true;
private bool ShowSelectedParcels { get; set; } = true;
private bool ShowImportedData { get; set; } = true;
@@ -1305,7 +1320,7 @@
Interlocked.Exchange(ref LastGpsLookupAt, now);
try
{
var radius = Math.Clamp(location.Accuracy ?? 5, 2, 25);
var radius = Math.Clamp(location.Accuracy ?? 12, 12, 30);
var xml = await OfficialCadastreService.GetParcelsNearAsync(
location.Latitude,
location.Longitude,
@@ -2094,6 +2109,12 @@
private Task CenterOnGps() => JS.InvokeVoidAsync("lmeMap.centerOnGps").AsTask();
private void ToggleGpsOverlay()
{
GpsOverlayCollapsed = !GpsOverlayCollapsed;
SaveFlagPreferences();
}
private string L(string key) => L10n.T(key);
private bool CanUse(string feature) => Entitlements.CanUse(feature);
@@ -2356,6 +2377,11 @@
OfflineExtraZoomLevels = Preferences.Default.Get($"{FlagPreferencePrefix}offline_zoom_levels", 2);
GpsProximityEnabled = GetFlagPreference("gps_proximity", true);
GpsBoundaryAlertEnabled = GetFlagPreference("gps_boundary_alert", false);
GpsOverlayCollapsed = GetFlagPreference("gps_overlay_collapsed", false);
MapMessageTimeoutSeconds = Math.Clamp(
Preferences.Default.Get($"{FlagPreferencePrefix}map_message_timeout", 3),
0,
30);
foreach (var layer in CadastralLayers)
{
@@ -2391,6 +2417,8 @@
Preferences.Default.Set($"{FlagPreferencePrefix}offline_zoom_levels", OfflineExtraZoomLevels);
SetFlagPreference("gps_proximity", GpsProximityEnabled);
SetFlagPreference("gps_boundary_alert", GpsBoundaryAlertEnabled);
SetFlagPreference("gps_overlay_collapsed", GpsOverlayCollapsed);
Preferences.Default.Set($"{FlagPreferencePrefix}map_message_timeout", MapMessageTimeoutSeconds);
foreach (var layer in CadastralLayers)
{
@@ -2400,6 +2428,7 @@
private async Task ApplyFlagPreferencesToMapAsync()
{
await JS.InvokeVoidAsync("lmeMap.setInfoTimeout", MapMessageTimeoutSeconds);
await JS.InvokeVoidAsync("lmeMap.setIncludeKmlSheetLayer", IncludeKmlSheetLayer);
await JS.InvokeVoidAsync("lmeMap.setImportedLayerVisible", ShowImportedData);
await JS.InvokeVoidAsync("lmeMap.setImportedSheetLayerVisible", ShowImportedSheetLayer);
@@ -2416,6 +2445,13 @@
}
}
private async Task ApplyMapMessageOptions()
{
MapMessageTimeoutSeconds = Math.Clamp(MapMessageTimeoutSeconds, 0, 30);
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setInfoTimeout", MapMessageTimeoutSeconds);
}
private async Task ApplyAdvancedOptions()
{
SaveFlagPreferences();
+2 -2
View File
@@ -28,8 +28,8 @@
<ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0.8</ApplicationDisplayVersion>
<ApplicationVersion>108</ApplicationVersion>
<ApplicationDisplayVersion>1.0.10</ApplicationDisplayVersion>
<ApplicationVersion>110</ApplicationVersion>
<Version>$(ApplicationDisplayVersion)</Version>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
+1 -1
View File
@@ -4,7 +4,7 @@
- Nome: LME Mappe Catastali Pro.
- Package name Android: `it.liviomerola.lmemappecatastali.pro`.
- Versione corrente da pubblicare: `1.0.8` / version code `108`.
- Versione corrente da pubblicare: `1.0.10` / version code `110`.
- Formato Release Android: Android App Bundle (`.aab`).
- Target SDK Release: Android 16 / API 36.
@@ -209,17 +209,16 @@ public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingSe
throw new InvalidOperationException(ToMessage(result.Result));
}
var details = result.ProductDetailsList?.FirstOrDefault();
// The .NET async wrapper copies the Java result into the managed
// ProductDetails property; ProductDetailsList has no Java handle here.
var details = result.ProductDetails?.FirstOrDefault();
if (details is not null)
{
_lastCatalogFailure = null;
return details;
}
var unfetched = result.UnfetchedProductList?.FirstOrDefault();
_lastCatalogFailure = unfetched is null
? "Google Play ha restituito un catalogo vuoto per lme_pro. Verifica account Play Store, adesione al test e origine dell'installazione."
: $"Google Play non ha caricato lme_pro (codice catalogo {unfetched.StatusCodeValue}).";
_lastCatalogFailure = "Google Play ha restituito un catalogo vuoto per lme_pro. Verifica account Play Store, adesione al test e origine dell'installazione.";
return null;
}
+10
View File
@@ -4,6 +4,16 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
Informativa privacy: [PRIVACY_POLICY.md](PRIVACY_POLICY.md).
## Versione Pro 1.0.10 test
- Migliora la compatibilita del servizio catastale AdE sui dispositivi Android meno recenti con HTTP/1.1, tentativi automatici e una ricerca GPS piu tollerante.
- I messaggi sulla mappa si chiudono automaticamente dopo un intervallo configurabile, predefinito a 3 secondi, e includono un pulsante di chiusura.
- La barra GPS mantiene sempre disponibili i comandi Avvia/Ferma e Centra, impilati sugli schermi stretti, e puo essere ridotta.
## Versione Pro 1.0.9 test
- Corregge la lettura del catalogo dal wrapper asincrono .NET di Google Play Billing, usando la raccolta managed `ProductDetails` popolata dal binding.
## Versione Pro 1.0.8 test
- Google Play Billing accetta tutte le offerte idonee restituite per i piani `monthly` e `yearly`, preferendo il piano base quando disponibile.
+79 -10
View File
@@ -1,4 +1,6 @@
using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Xml.Linq;
namespace LMEMappeCatastaliApp.Services;
@@ -13,8 +15,9 @@ public sealed class OfficialCadastreService
public OfficialCadastreService()
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/gml+xml");
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/1.0 (Android)");
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/gml+xml"));
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml", 0.9));
}
public async Task<string> GetParcelsNearAsync(
@@ -22,14 +25,30 @@ public sealed class OfficialCadastreService
double longitude,
double radiusMeters,
CancellationToken cancellationToken = default)
=> await GetFeaturesNearAsync(
{
var firstRadius = Math.Clamp(radiusMeters, 12, 30);
var xml = await GetFeaturesNearAsync(
latitude,
longitude,
radiusMeters,
firstRadius,
"CP:CadastralParcel",
100,
cancellationToken);
if (firstRadius >= 30 || ContainsFeatures(xml))
{
return xml;
}
return await GetFeaturesNearAsync(
latitude,
longitude,
30,
"CP:CadastralParcel",
100,
cancellationToken);
}
public async Task<string> GetSheetsNearAsync(
double latitude,
double longitude,
@@ -78,15 +97,65 @@ public sealed class OfficialCadastreService
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)
for (var attempt = 0; attempt < 3; attempt++)
{
throw new HttpRequestException($"Il servizio WFS AdE ha risposto {((int)response.StatusCode)}.");
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Version = HttpVersion.Version11,
VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
};
request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true };
using var response = await _httpClient.SendAsync(
request,
HttpCompletionOption.ResponseContentRead,
cancellationToken);
if (response.IsSuccessStatusCode)
{
var xml = await response.Content.ReadAsStringAsync(cancellationToken);
ThrowIfExceptionReport(xml);
return xml;
}
if (attempt < 2 && IsTransient(response.StatusCode))
{
await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)), cancellationToken);
continue;
}
throw new HttpRequestException($"Il servizio WFS AdE ha risposto {((int)response.StatusCode)}.");
}
catch (HttpRequestException) when (attempt < 2)
{
await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)), cancellationToken);
}
catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested && attempt < 2)
{
await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)), cancellationToken);
}
}
var xml = await response.Content.ReadAsStringAsync(cancellationToken);
ThrowIfExceptionReport(xml);
return xml;
throw new HttpRequestException("Il servizio WFS AdE non ha risposto dopo tre tentativi.");
}
private static bool IsTransient(HttpStatusCode statusCode) =>
statusCode is HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests
|| (int)statusCode >= 500;
private static bool ContainsFeatures(string xml)
{
try
{
var document = XDocument.Parse(xml);
return document.Descendants().Any(element =>
element.Name.LocalName is "member" or "featureMember");
}
catch (System.Xml.XmlException)
{
return false;
}
}
private static string Format(double value) => value.ToString("R", CultureInfo.InvariantCulture);
+66 -8
View File
@@ -1245,6 +1245,16 @@ input[type="checkbox"] {
transition: top 140ms ease;
}
.gps-overlay.is-collapsed {
grid-template-columns: auto;
padding: 5px;
}
.gps-overlay.is-collapsed .gps-state,
.gps-overlay.is-collapsed .gps-actions {
display: none;
}
.gps-overlay.search-open {
top: 140px;
}
@@ -1267,8 +1277,14 @@ input[type="checkbox"] {
color: #a33227;
}
.gps-actions {
display: grid;
gap: 5px;
}
.gps-main,
.gps-center {
.gps-center,
.gps-collapse {
min-height: 38px;
padding: 0 10px;
white-space: nowrap;
@@ -1279,6 +1295,15 @@ input[type="checkbox"] {
color: var(--ink);
}
.gps-collapse {
width: 38px;
padding: 0;
background: #e7ecef;
color: var(--ink);
font-size: 22px;
line-height: 1;
}
.leaflet-top.leaflet-right {
top: 68px;
right: 2px;
@@ -1566,11 +1591,36 @@ label {
z-index: 820;
max-width: min(620px, calc(100% - 460px));
border-radius: 8px;
padding: 9px 11px;
display: flex;
align-items: flex-start;
gap: 8px;
padding: 7px 7px 7px 11px;
background: rgba(22, 31, 38, 0.88);
color: #fff;
font-size: 12px;
pointer-events: none;
pointer-events: auto;
}
.map-info[hidden] {
display: none;
}
.map-info-text {
min-width: 0;
padding: 2px 0;
overflow-wrap: anywhere;
}
.map-info-close {
flex: 0 0 28px;
width: 28px;
min-height: 28px;
padding: 0;
border: 0;
background: transparent;
color: #fff;
font-size: 20px;
line-height: 1;
}
.wms-preview[hidden] {
@@ -1817,12 +1867,12 @@ footer {
}
.leaflet-top.leaflet-right {
top: 122px;
top: 174px;
right: 0;
}
.map-stage.search-active .leaflet-top.leaflet-right {
top: 190px;
top: 242px;
}
.gps-main,
@@ -1835,12 +1885,15 @@ footer {
right: 8px;
bottom: calc(var(--nav-height) + 116px);
left: 8px;
display: -webkit-box;
max-width: none;
overflow: hidden;
line-height: 1.35;
overflow-wrap: anywhere;
white-space: normal;
}
.map-info-text {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
@@ -1864,8 +1917,13 @@ footer {
min-width: 0;
}
.gps-overlay {
grid-template-columns: minmax(0, 1fr) auto auto;
}
.gps-main,
.gps-center {
display: none;
min-width: 86px;
}
.bottom-tabs button {
+48 -4
View File
@@ -50,6 +50,8 @@ const lmeState = {
gpsTrailStartedAt: null,
gpsWatchId: null,
lastPosition: null,
infoTimeoutSeconds: 3,
infoTimer: null,
fileImportBound: false,
resizeBound: false,
googleMapsLoading: null,
@@ -185,7 +187,9 @@ window.lmeMap = {
setLocalization,
setEntitlements,
importExternalGisFile,
showInfo: setInfo
showInfo: setInfo,
hideInfo,
setInfoTimeout
};
function setLocalization(options = {}) {
@@ -5433,13 +5437,24 @@ function refreshCadastreLayer() {
});
let errorShown = false;
cadastreLayer.on("tileerror", () => {
cadastreLayer.on("tileerror", event => {
const tile = event?.tile;
const retryCount = Number(tile?.dataset?.lmeRetry || 0);
if (tile && retryCount < 2) {
tile.dataset.lmeRetry = String(retryCount + 1);
window.setTimeout(() => {
const separator = tile.src.includes("?") ? "&" : "?";
tile.src = `${tile.src}${separator}_lme_retry=${Date.now()}`;
}, 450 * (retryCount + 1));
return;
}
if (errorShown) {
return;
}
errorShown = true;
setInfo("Layer catastali non caricati dal servizio AdE. Controlla la connessione e riprova.");
setInfo("Il servizio cartografico AdE non ha risposto. La connessione internet puo essere attiva: attendi qualche secondo e riprova.");
});
cadastreLayer.on("load", () => {
errorShown = false;
@@ -5905,7 +5920,36 @@ function cadastreUrl() {
}
function setInfo(text) {
setText("mapInfo", text);
const container = document.getElementById("mapInfo");
if (!container) return;
setText("mapInfoText", text);
container.hidden = false;
if (lmeState.infoTimer) {
window.clearTimeout(lmeState.infoTimer);
lmeState.infoTimer = null;
}
if (lmeState.infoTimeoutSeconds > 0) {
lmeState.infoTimer = window.setTimeout(hideInfo, lmeState.infoTimeoutSeconds * 1000);
}
}
function hideInfo() {
const container = document.getElementById("mapInfo");
if (container) container.hidden = true;
if (lmeState.infoTimer) {
window.clearTimeout(lmeState.infoTimer);
lmeState.infoTimer = null;
}
}
function setInfoTimeout(seconds) {
const value = Number(seconds);
lmeState.infoTimeoutSeconds = Number.isFinite(value) ? Math.min(30, Math.max(0, value)) : 3;
const container = document.getElementById("mapInfo");
if (container && !container.hidden) {
setInfo(document.getElementById("mapInfoText")?.textContent || "");
}
}
function setText(id, text) {