From 990b66e24f2fc8db918f7882085c890021802352 Mon Sep 17 00:00:00 2001 From: Perplexity Computer Date: Sat, 30 May 2026 19:16:05 +0000 Subject: [PATCH] Release GPS2Audio v2.5.46 POI stability --- app/build.gradle.kts | 4 +- .../ui/ExplorerScreenMapLibre.kt | 127 +++++++++++++----- .../waypointaudio/util/ExplorerPoiClient.kt | 12 +- 3 files changed, 104 insertions(+), 39 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2d4aa80..c3b5c4e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -157,8 +157,8 @@ android { // • Keine Regressionen: Phantom-Layer-Fix, Lizenzsystem, Multi-Clip, // PTT, Tour-Zähler, Backup/Cover, GPX-Import, Explorer/Routing, // Tour-/Waypoint-Kopien, Hintergrundwiedergabe. - versionCode = 99 - versionName = "2.5.45" + versionCode = 100 + versionName = "2.5.46" } val storeFilePath = resolveSigningValue("storeFile", "GPS2AUDIO_RELEASE_STORE_FILE") diff --git a/app/src/main/kotlin/de/waypointaudio/ui/ExplorerScreenMapLibre.kt b/app/src/main/kotlin/de/waypointaudio/ui/ExplorerScreenMapLibre.kt index 32167a8..e94da93 100644 --- a/app/src/main/kotlin/de/waypointaudio/ui/ExplorerScreenMapLibre.kt +++ b/app/src/main/kotlin/de/waypointaudio/ui/ExplorerScreenMapLibre.kt @@ -503,34 +503,92 @@ fun ExplorerScreenMapLibre( // Schlüssel inkl. showPoiNames: Marker werden bei Toggle-Änderung neu gezeichnet. // Fix: Kategorie-Bitmap wird per buildExplorerPoiIcon(cat, showPoiNames, name) mit // eindeutiger Kategorie-Kennung erzeugt — verhindert Icon-Cache-Kollisionen in MapLibre. + // v2.5.46 — Atomare POI-Marker-Aktualisierung: erst neue Marker hinzufügen, + // dann alte entfernen. So gibt es keine sichtbare Lücke (kein Flackern) beim + // Kartennachladen. Außerdem: maximale Anzahl angezeigter Marker auf 280 begrenzt, + // benannte und wichtige Subtypen werden priorisiert. LaunchedEffect(explorerPoiResults, styleReady, showPoiNames) { if (!styleReady) return@LaunchedEffect val ml = mapLibreMap ?: return@LaunchedEffect - // Alte Explorer-POI-Marker entfernen + if (explorerPoiResults.isEmpty()) { + // Nur leeren wenn wirklich leer (z.B. manueller Clear) + explorerPoiMarkers.forEach { runCatching { ml.removeMarker(it) } } + explorerPoiMarkers.clear() + explorerPoiMarkerIdToResult.clear() + return@LaunchedEffect + } + + val iconFactory = IconFactory.getInstance(context) + + // v2.5.46 — POI-Dichte-Begrenzung: max. 280 Marker pro Viewport, + // priorisiere benannte POIs und wichtige Subtypen. + val MAX_VISIBLE_POIS = 280 + val importantSubtypes = setOf( + de.waypointaudio.util.PoiSubtype.CASTLE, + de.waypointaudio.util.PoiSubtype.CHURCH, + de.waypointaudio.util.PoiSubtype.MUSEUM, + de.waypointaudio.util.PoiSubtype.VIEWPOINT, + de.waypointaudio.util.PoiSubtype.MONUMENT, + de.waypointaudio.util.PoiSubtype.TRAIN_STATION, + de.waypointaudio.util.PoiSubtype.HOSPITAL, + de.waypointaudio.util.PoiSubtype.PHARMACY, + ) + // Alle POIs flach aufsammeln, sortieren (benannte + wichtige zuerst) + val allPois: List> = + explorerPoiResults.flatMap { (cat, pois) -> pois.map { cat to it } } + val totalCount = allPois.size + val sortedPois = allPois.sortedWith( + compareByDescending> { + val poi = it.second + // Priorität: benannter POI + wichtiger Subtype = höchste Priorität + val isNamed = poi.displayName.isNotBlank() && poi.displayName != poi.subtype.displayLabel + val isImportant = poi.subtype in importantSubtypes + when { + isNamed && isImportant -> 3 + isNamed -> 2 + isImportant -> 1 + else -> 0 + } + } + ) + val visiblePois = if (totalCount > MAX_VISIBLE_POIS) sortedPois.take(MAX_VISIBLE_POIS) else sortedPois + val shownCount = visiblePois.size + + // v2.5.46 — Status-Text (zeigt "N von M" wenn begrenzt) + if (totalCount > MAX_VISIBLE_POIS) { + autoPoiStatus = "POIs: $shownCount von $totalCount angezeigt" + } + + // Neue Marker aufbauen (noch nicht auf der Karte) + val newMarkers = mutableListOf() + val newMarkerIdToResult = mutableMapOf() + + for ((cat, poi) in visiblePois) { + // v2.5.45 — Subtype-Icon: Piktogramm variiert nach Untertyp, Farbe bleibt Kategorie + val iconBitmap = if (showPoiNames) { + buildExplorerPoiIconWithLabel(cat, poi.subtype, poi.displayName) + } else { + buildExplorerPoiIcon(cat, poi.subtype) + } + val icon = iconFactory.fromBitmap(iconBitmap) + val mo = MarkerOptions() + .position(LatLng(poi.latitude, poi.longitude)) + .title(poi.displayName) + .snippet(poi.subtype.displayLabel) // v2.5.45: Subtype-Label statt categoryLabel + .icon(icon) + val marker = runCatching { ml.addMarker(mo) }.getOrNull() ?: continue + newMarkers.add(marker) + newMarkerIdToResult[marker.id] = poi + } + + // v2.5.46 — ERST nach Hinzufügen der neuen Marker die alten entfernen + // → atomare Aktualisierung ohne sichtbare Lücke / Flackern explorerPoiMarkers.forEach { runCatching { ml.removeMarker(it) } } explorerPoiMarkers.clear() explorerPoiMarkerIdToResult.clear() - if (explorerPoiResults.isEmpty()) return@LaunchedEffect - val iconFactory = IconFactory.getInstance(context) - for ((cat, pois) in explorerPoiResults) { - for (poi in pois) { - // v2.5.45 — Subtype-Icon: Piktogramm variiert nach Untertyp, Farbe bleibt Kategorie - val iconBitmap = if (showPoiNames) { - buildExplorerPoiIconWithLabel(cat, poi.subtype, poi.displayName) - } else { - buildExplorerPoiIcon(cat, poi.subtype) - } - val icon = iconFactory.fromBitmap(iconBitmap) - val mo = MarkerOptions() - .position(LatLng(poi.latitude, poi.longitude)) - .title(poi.displayName) - .snippet(poi.subtype.displayLabel) // v2.5.45: Subtype-Label statt categoryLabel - .icon(icon) - val marker = runCatching { ml.addMarker(mo) }.getOrNull() ?: continue - explorerPoiMarkers.add(marker) - explorerPoiMarkerIdToResult[marker.id] = poi - } - } + + explorerPoiMarkers.addAll(newMarkers) + explorerPoiMarkerIdToResult.putAll(newMarkerIdToResult) } var showSearchDialog by remember { mutableStateOf(false) } @@ -2169,10 +2227,10 @@ fun ExplorerScreenMapLibre( lastAutoLoadCats, activeCats ) ) { - // Debounce: 1.5 s + // v2.5.46: Debounce erhöht auf 2.5 s (vorher 1.5 s) poiAutoLoadJob?.cancel() poiAutoLoadJob = scope.launch { - delay(1500L) + delay(2500L) if (explorerPoiLoading) return@launch val bounds = ml.projection.visibleRegion.latLngBounds val bbox = de.waypointaudio.util.LatLngBbox( @@ -2196,7 +2254,7 @@ fun ExplorerScreenMapLibre( lastAutoLoadZoom = zoom lastAutoLoadCats = activeCats val total = results.values.sumOf { it.size } - autoPoiStatus = if (total == 0) null else "$total POIs" + autoPoiStatus = if (total == 0) null else "POIs: $total" } catch (e: Exception) { // Alten Bestand behalten, nur dezente Fehlermeldung autoPoiStatus = null @@ -2437,14 +2495,15 @@ fun ExplorerScreenMapLibre( ) ) } - // Status-Text + // v2.5.46 — Status-Text: nur bei echtem Laden "wird geladen", + // sonst ruhiger Status-Text val statusText = when { - explorerPoiLoading -> "POIs werden aktualisiert…" - autoPoiStatus != null -> "POIs: $autoPoiStatus" + explorerPoiLoading -> "POIs laden…" + autoPoiStatus != null -> autoPoiStatus!! autoPoiLoadEnabled && currentMapZoom < de.waypointaudio.util.ExplorerPoiClient.AUTO_LOAD_MIN_ZOOM -> "Hereinzoomen für Auto-Load" - autoPoiLoadEnabled -> "POIs: Auto-Laden aktiv" - else -> "POIs: manuell laden" + autoPoiLoadEnabled -> "Auto-Laden aktiv" + else -> "POIs: manuell" } Text( text = statusText, @@ -3639,7 +3698,8 @@ private fun buildExplorerPoiIcon( ): Bitmap { // v2.5.43 — Echte Canvas-Piktogramme statt Buchstaben/Zeichen // v2.5.45 — Subtype-Piktogramme für Sights/Gastro/Mobil/Infra - val sizePx = 56 + // v2.5.46 — Größe von 56 auf 68 px erhöht (bessere Lesbarkeit auf Smartphone-Screens) + val sizePx = 68 // Kategorie-Farben — dezent, App-Teal-Stil passend val fillColor = when (category) { @@ -3740,9 +3800,10 @@ private fun buildExplorerPoiIcon( // ── Weißes Piktogramm per Canvas (v2.5.45: Subtype-Varianten) ────────────────── + // v2.5.46: Strichstärke auf 2.8 erhöht (Icon-Größe 68 px statt 56 px) paint.color = 0xFFFFFFFF.toInt() paint.style = android.graphics.Paint.Style.FILL - paint.strokeWidth = 2.2f + paint.strokeWidth = 2.8f when (subtype) { // ─── SIGHTS Subtypes ──────────────────────────────────────────────────────── @@ -4016,7 +4077,7 @@ private fun buildExplorerPoiIconWithLabel( subtype: de.waypointaudio.util.PoiSubtype = de.waypointaudio.util.PoiSubtype.GENERIC_SIGHTS, name: String ): Bitmap { - val iconSize = 56 // gleiche Größe wie buildExplorerPoiIcon + val iconSize = 68 // v2.5.46: gleiche Größe wie buildExplorerPoiIcon (erhöht auf 68 px) val labelMaxChars = 14 val displayName = if (name.length > labelMaxChars) name.take(labelMaxChars - 1) + "…" else name diff --git a/app/src/main/kotlin/de/waypointaudio/util/ExplorerPoiClient.kt b/app/src/main/kotlin/de/waypointaudio/util/ExplorerPoiClient.kt index 037a08b..9866bda 100644 --- a/app/src/main/kotlin/de/waypointaudio/util/ExplorerPoiClient.kt +++ b/app/src/main/kotlin/de/waypointaudio/util/ExplorerPoiClient.kt @@ -160,11 +160,13 @@ object ExplorerPoiClient { private const val OVERPASS_URL = "https://overpass-api.de/api/interpreter" private const val MAX_RADIUS_M = 5000 - // v2.5.45 — Auto-Load-Parameter + // v2.5.46 — Auto-Load-Parameter (erhöhte Schwellen für weniger Flackern) /** Minimaler Zoom für automatisches Laden (unter diesem Zoom: kein Auto-Load). */ const val AUTO_LOAD_MIN_ZOOM = 12.0 - /** Mindest-Kartenbewegung in Metern, bevor ein neuer Auto-Load ausgelöst wird. */ - const val MOVE_THRESHOLD_M = 300.0 + /** Mindest-Kartenbewegung in Metern, bevor ein neuer Auto-Load ausgelöst wird. + * v2.5.46: erhöht von 300 m auf 600 m, um überflüssige Nachlades bei kleinen + * Kamerabewegungen (Kippen, Heading-Änderungen, kleiner Zoom) zu vermeiden. */ + const val MOVE_THRESHOLD_M = 600.0 // In-Memory-Cache: key = "kategorie|bboxFingerprint" private val cache = mutableMapOf>() @@ -195,8 +197,10 @@ object ExplorerPoiClient { if (lastAutoLat == null || lastAutoLon == null || lastAutoZoom == null || lastAutoCategories == null) return true if (activeCategories != lastAutoCategories) return true val moved = haversineM(centerLat, centerLon, lastAutoLat, lastAutoLon) + // v2.5.46: Zoom-Delta-Schwelle erhöht auf 1.2, damit minimales Reinzoomen + // (z. B. Pitch-Änderung oder sehr kleiner Zoom) keinen Reload auslöst. val zoomDelta = kotlin.math.abs(zoom - lastAutoZoom) - return moved > MOVE_THRESHOLD_M || zoomDelta > 0.8 + return moved > MOVE_THRESHOLD_M || zoomDelta > 1.2 } /**