Release GPS2Audio v2.5.45 Explorer POI autoload

This commit is contained in:
Perplexity Computer
2026-05-30 18:04:55 +00:00
parent 014e6c1bf1
commit 5cfc5aee1d
7 changed files with 836 additions and 197 deletions
+2 -2
View File
@@ -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 = 98
versionName = "2.5.44"
versionCode = 99
versionName = "2.5.45"
}
val storeFilePath = resolveSigningValue("storeFile", "GPS2AUDIO_RELEASE_STORE_FILE")
@@ -121,8 +121,11 @@ import de.waypointaudio.util.LatLngBbox
import de.waypointaudio.util.WaypointAudioDuration
import de.waypointaudio.viewmodel.WaypointViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import de.waypointaudio.util.PoiSubtype
import org.maplibre.android.annotations.IconFactory
import org.maplibre.android.annotations.Marker
import org.maplibre.android.annotations.MarkerOptions
@@ -324,6 +327,15 @@ fun ExplorerScreenMapLibre(
var currentMapZoom by remember { mutableStateOf(13.0) }
val snackbarHostState = remember { SnackbarHostState() }
// v2.5.45 — Auto-POI-Load State
var autoPoiLoadEnabled by remember { mutableStateOf(true) } // standardmäßig aktiviert
var poiAutoLoadJob by remember { mutableStateOf<Job?>(null) }
var lastAutoLoadLat by remember { mutableStateOf<Double?>(null) }
var lastAutoLoadLon by remember { mutableStateOf<Double?>(null) }
var lastAutoLoadZoom by remember { mutableStateOf<Double?>(null) }
var lastAutoLoadCats by remember { mutableStateOf<Set<de.waypointaudio.data.PoiCategory>?>(null) }
var autoPoiStatus by remember { mutableStateOf<String?>(null) } // kurzer Status-Text
var lastLat by remember { mutableStateOf<Double?>(null) }
var lastLng by remember { mutableStateOf<Double?>(null) }
var lastAccuracyM by remember { mutableStateOf<Float?>(null) }
@@ -455,10 +467,14 @@ fun ExplorerScreenMapLibre(
}
}
// v2.5.38 — POI-Layer-Auswahl persistent speichern.
// v2.5.45 — Bei Kategorie-Änderung: Cache leeren und Auto-Load-Cursor zurücksetzen.
LaunchedEffect(layerState.poiLayers) {
if (styleHydrated && layerState.poiLayers != mapStyleSettings.poiLayers) {
runCatching { mapStyleStore.setPoiLayers(layerState.poiLayers) }
}
// Kategorie-Wechsel → nächster Camera-Idle lädt frisch
lastAutoLoadCats = null
de.waypointaudio.util.ExplorerPoiClient.clearCache()
}
// v2.5.38 — POI-Sichtbarkeit auf den geladenen MapLibre-Style anwenden.
// Greift auf heuristische Layer-ID-Präfixe des OpenMapTiles-Schemas zurück.
@@ -498,16 +514,17 @@ fun ExplorerScreenMapLibre(
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.displayName)
buildExplorerPoiIconWithLabel(cat, poi.subtype, poi.displayName)
} else {
buildExplorerPoiIcon(cat)
buildExplorerPoiIcon(cat, poi.subtype)
}
val icon = iconFactory.fromBitmap(iconBitmap)
val mo = MarkerOptions()
.position(LatLng(poi.latitude, poi.longitude))
.title(poi.displayName)
.snippet(poi.categoryLabel)
.snippet(poi.subtype.displayLabel) // v2.5.45: Subtype-Label statt categoryLabel
.icon(icon)
val marker = runCatching { ml.addMarker(mo) }.getOrNull() ?: continue
explorerPoiMarkers.add(marker)
@@ -2131,8 +2148,65 @@ fun ExplorerScreenMapLibre(
override fun onMoveEnd(detector: org.maplibre.android.gestures.MoveGestureDetector) {}
})
// v2.5.41 — Zoom-Tracking für zoom-abhängige POI-Label-Anzeige
// v2.5.45 — Auto-POI-Load: debounced nach Kamera-Idle
ml.addOnCameraIdleListener {
currentMapZoom = ml.cameraPosition.zoom
// Auto-Load nur wenn aktiviert, POI-Layer an, Zoom genügend
if (autoPoiLoadEnabled && !explorerPoiLoading) {
val cp = ml.cameraPosition
val zoom = cp.zoom
val cLat = cp.target?.latitude ?: return@addOnCameraIdleListener
val cLon = cp.target?.longitude ?: return@addOnCameraIdleListener
val activeCats = buildSet<de.waypointaudio.data.PoiCategory> {
if (layerState.poiLayers.showSights) add(de.waypointaudio.data.PoiCategory.SIGHTS)
if (layerState.poiLayers.showGastronomy) add(de.waypointaudio.data.PoiCategory.GASTRONOMY)
if (layerState.poiLayers.showMobility) add(de.waypointaudio.data.PoiCategory.MOBILITY)
if (layerState.poiLayers.showInfrastructure) add(de.waypointaudio.data.PoiCategory.INFRASTRUCTURE)
}
if (de.waypointaudio.util.ExplorerPoiClient.shouldAutoLoad(
zoom, cLat, cLon,
lastAutoLoadLat, lastAutoLoadLon, lastAutoLoadZoom,
lastAutoLoadCats, activeCats
)
) {
// Debounce: 1.5 s
poiAutoLoadJob?.cancel()
poiAutoLoadJob = scope.launch {
delay(1500L)
if (explorerPoiLoading) return@launch
val bounds = ml.projection.visibleRegion.latLngBounds
val bbox = de.waypointaudio.util.LatLngBbox(
south = bounds.southWest.latitude,
west = bounds.southWest.longitude,
north = bounds.northEast.latitude,
east = bounds.northEast.longitude
)
explorerPoiLoading = true
autoPoiStatus = null
try {
val results = de.waypointaudio.util.ExplorerPoiClient.loadPois(
context = context,
bbox = bbox,
categories = activeCats,
onRadiusClamped = {}
)
explorerPoiResults = results
lastAutoLoadLat = cLat
lastAutoLoadLon = cLon
lastAutoLoadZoom = zoom
lastAutoLoadCats = activeCats
val total = results.values.sumOf { it.size }
autoPoiStatus = if (total == 0) null else "$total POIs"
} catch (e: Exception) {
// Alten Bestand behalten, nur dezente Fehlermeldung
autoPoiStatus = null
explorerPoiSnackbar = "POI-Aktualisierung fehlgeschlagen."
} finally {
explorerPoiLoading = false
}
}
}
}
}
}
mv.onStart()
@@ -2274,6 +2348,12 @@ fun ExplorerScreenMapLibre(
},
onShowRoutingInfo = { showRoutingInfo = true },
onShowRoutePlanner = { showRoutePlanner = true },
// v2.5.45 — Auto-Load-Toggle
poiAutoLoadEnabled = autoPoiLoadEnabled,
onPoiAutoLoadChange = { enabled ->
autoPoiLoadEnabled = enabled
if (!enabled) poiAutoLoadJob?.cancel()
},
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 8.dp, end = 8.dp)
@@ -2320,42 +2400,72 @@ fun ExplorerScreenMapLibre(
)
}
// v2.5.40 — Explorer-POI-Lade-Button (mittig unten, über Attribution)
if (layerState.poiLayers.let {
it.showSights || it.showGastronomy || it.showMobility || it.showInfrastructure
}) {
// v2.5.45 — Explorer-POI-Bar (Auto-Load-Status + manueller Fallback)
// Kompaktes Pill-Design: links Status/Ladeindikator, rechts manueller Reload-Button.
// Nur sichtbar wenn mindestens ein POI-Layer aktiv ist.
val anyPoiLayerActiveForBar = layerState.poiLayers.let {
it.showSights || it.showGastronomy || it.showMobility || it.showInfrastructure
}
if (anyPoiLayerActiveForBar) {
Surface(
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.95f),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.92f),
shadowElevation = 3.dp,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 32.dp)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
// Status-Icon/Spinner
if (explorerPoiLoading) {
CircularProgressIndicator(
modifier = Modifier.size(14.dp),
strokeWidth = 2.dp,
modifier = Modifier.size(13.dp),
strokeWidth = 1.8.dp,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
} else {
Icon(
Icons.Filled.Explore,
if (autoPoiLoadEnabled) Icons.Filled.Explore else Icons.Filled.Explore,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer
modifier = Modifier.size(13.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer.copy(
alpha = if (autoPoiLoadEnabled) 1f else 0.55f
)
)
}
// Status-Text
val statusText = when {
explorerPoiLoading -> "POIs werden aktualisiert…"
autoPoiStatus != null -> "POIs: $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"
}
Text(
text = if (explorerPoiLoading) "POIs werden geladen …"
else "POIs im Kartenbereich laden",
text = statusText,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
// Trennstrich
Text(
"·",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.5f)
)
// Manueller Neu-Laden Button
Text(
text = "",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
color = if (explorerPoiLoading)
MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.4f)
else
MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.clickable(enabled = !explorerPoiLoading) {
val ml = mapLibreMap ?: return@clickable
val bounds = ml.projection.visibleRegion.latLngBounds
@@ -2371,7 +2481,11 @@ fun ExplorerScreenMapLibre(
if (layerState.poiLayers.showMobility) add(PoiCategory.MOBILITY)
if (layerState.poiLayers.showInfrastructure) add(PoiCategory.INFRASTRUCTURE)
}
// Manueller Reload: Cache leeren, Cursor zurücksetzen
de.waypointaudio.util.ExplorerPoiClient.clearCache()
lastAutoLoadCats = null
explorerPoiLoading = true
autoPoiStatus = null
scope.launch {
try {
val results = ExplorerPoiClient.loadPois(
@@ -2379,17 +2493,21 @@ fun ExplorerScreenMapLibre(
bbox = bbox,
categories = cats,
onRadiusClamped = {
explorerPoiSnackbar = "Kartenausschnitt zu groß Abfrage auf max. 5 km Radius begrenzt."
explorerPoiSnackbar = "Kartenausschnitt zu groß Abfrage auf max. 5 km begrenzt."
}
)
val total = results.values.sumOf { it.size }
explorerPoiResults = results
// v2.5.44 — POI-Zusammenfassung im Snackbar
explorerPoiSnackbar = if (total == 0) {
"Keine POIs in diesem Bereich gefunden."
} else {
val cp = ml.cameraPosition
lastAutoLoadLat = cp.target?.latitude
lastAutoLoadLon = cp.target?.longitude
lastAutoLoadZoom = cp.zoom
lastAutoLoadCats = cats
autoPoiStatus = if (total > 0) "$total" else null
explorerPoiSnackbar = if (total == 0)
"Keine POIs in diesem Bereich."
else
ExplorerPoiClient.summarize(results)
}
} catch (e: Exception) {
explorerPoiSnackbar = "POI-Laden fehlgeschlagen: ${e.message?.take(60) ?: "Netzwerkfehler"}"
} finally {
@@ -2446,12 +2564,13 @@ fun ExplorerScreenMapLibre(
else "%.1f km entfernt".format(d / 1000.0)
} else null
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
// v2.5.45 — Subtype-Label (konkreter) statt nur Kategorie
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.secondaryContainer
) {
Text(
poi.categoryLabel,
poi.subtype.displayLabel, // z.B. "Burg/Schloss" statt "Sehenswürdigkeit"
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp)
@@ -3506,8 +3625,20 @@ fun MapCompactAttributionBadge(modifier: Modifier = Modifier) {
* MOBILITY — Indigo (#3F51B5) + Abgerundetes Quadrat + Bus-Symbol (B)
* INFRASTRUCTURE — Koralle (#E53935) + Kreis + Plus-Symbol (+)
*/
private fun buildExplorerPoiIcon(category: de.waypointaudio.data.PoiCategory): Bitmap {
/**
* v2.5.45 — Subtype-Overload: rückwärtskompatibel (Default: GENERIC je Kategorie)
*/
private fun buildExplorerPoiIcon(
category: de.waypointaudio.data.PoiCategory,
subtype: de.waypointaudio.util.PoiSubtype = when (category) {
de.waypointaudio.data.PoiCategory.SIGHTS -> de.waypointaudio.util.PoiSubtype.GENERIC_SIGHTS
de.waypointaudio.data.PoiCategory.GASTRONOMY -> de.waypointaudio.util.PoiSubtype.GENERIC_GASTRONOMY
de.waypointaudio.data.PoiCategory.MOBILITY -> de.waypointaudio.util.PoiSubtype.GENERIC_MOBILITY
de.waypointaudio.data.PoiCategory.INFRASTRUCTURE-> de.waypointaudio.util.PoiSubtype.GENERIC_INFRASTRUCTURE
}
): Bitmap {
// v2.5.43 — Echte Canvas-Piktogramme statt Buchstaben/Zeichen
// v2.5.45 — Subtype-Piktogramme für Sights/Gastro/Mobil/Infra
val sizePx = 56
// Kategorie-Farben — dezent, App-Teal-Stil passend
@@ -3518,13 +3649,15 @@ private fun buildExplorerPoiIcon(category: de.waypointaudio.data.PoiCategory): B
de.waypointaudio.data.PoiCategory.INFRASTRUCTURE-> 0xFF558B2F.toInt() // Dunkelgrün (Infrastruktur/Info)
}
// Sentinel-Byte-Wert je Kategorie (eindeutige Pixel-Markierung für MapLibre-Cache)
val sentinelAlpha = when (category) {
de.waypointaudio.data.PoiCategory.SIGHTS -> 0xFC
de.waypointaudio.data.PoiCategory.GASTRONOMY -> 0xFD
de.waypointaudio.data.PoiCategory.MOBILITY -> 0xFE
de.waypointaudio.data.PoiCategory.INFRASTRUCTURE-> 0xFF
// v2.5.45 — Sentinel-Wert berücksichtigt Kategorie + Subtype-Ordinal,
// um MapLibre-Icon-Cache-Kollisionen zwischen verschiedenen Subtypen derselben Kategorie zu verhindern.
val categoryBase = when (category) {
de.waypointaudio.data.PoiCategory.SIGHTS -> 0
de.waypointaudio.data.PoiCategory.GASTRONOMY -> 32
de.waypointaudio.data.PoiCategory.MOBILITY -> 64
de.waypointaudio.data.PoiCategory.INFRASTRUCTURE-> 96
}
val sentinelAlpha = ((categoryBase + subtype.ordinal) % 200 + 55).coerceIn(1, 254)
val bmp = android.graphics.Bitmap.createBitmap(sizePx, sizePx, android.graphics.Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(bmp)
@@ -3605,101 +3738,269 @@ private fun buildExplorerPoiIcon(category: de.waypointaudio.data.PoiCategory): B
}
}
// ── Weißes Piktogramm per Canvas ─────────────────────────────────────────
// ── Weißes Piktogramm per Canvas (v2.5.45: Subtype-Varianten) ──────────────────
paint.color = 0xFFFFFFFF.toInt()
paint.style = android.graphics.Paint.Style.FILL
paint.strokeWidth = 2.2f
when (category) {
de.waypointaudio.data.PoiCategory.SIGHTS -> {
// Stern-Marker: Stern-Hintergrund ist schon der Rahmen.
// Piktogramm: kleiner innerer Punkt + Strahllinien wie eine Landmarke/Monument-Nadel
// Vertikaler Stab (Monument-Pin)
val stW = 3.5f
val stH = 14f
val stTop = cy - 10f
val stBot = cy + 4f
val stLeft = cx - stW / 2f
canvas.drawRect(stLeft, stTop, stLeft + stW, stBot, paint)
// Kleines Dreieck-Dach oben (Turm/Monument)
when (subtype) {
// ─── SIGHTS Subtypes ────────────────────────────────────────────────────────
de.waypointaudio.util.PoiSubtype.CASTLE -> {
// Turm/Burg: Zinnenkranz + Tor
paint.style = android.graphics.Paint.Style.FILL
val tw = 8f; val th = 14f
val tl = cx - tw/2f; val tt = cy - th/2f - 1f
canvas.drawRect(tl, tt + 4f, tl + tw, tt + th, paint)
for (zi in 0..2) {
val zx = tl + zi * (tw/2f) - 0.5f
canvas.drawRect(zx, tt, zx + 2.5f, tt + 4f, paint)
}
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 1.8f
val tgW = 4f
canvas.drawArc(android.graphics.RectF(cx - tgW, tt + th - tgW*1.5f - 2f, cx + tgW, tt + th - 2f), 180f, -180f, false, paint)
}
de.waypointaudio.util.PoiSubtype.CHURCH -> {
// Kreuz
paint.style = android.graphics.Paint.Style.FILL
canvas.drawRect(cx - 2f, cy - 12f, cx + 2f, cy + 10f, paint)
canvas.drawRect(cx - 7f, cy - 6f, cx + 7f, cy - 2f, paint)
}
de.waypointaudio.util.PoiSubtype.MONASTERY -> {
// Kleines Kreuz + Bogen
paint.style = android.graphics.Paint.Style.FILL
canvas.drawRect(cx - 1.8f, cy - 10f, cx + 1.8f, cy + 8f, paint)
canvas.drawRect(cx - 6f, cy - 5f, cx + 6f, cy - 2f, paint)
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 1.8f
canvas.drawArc(android.graphics.RectF(cx - 7f, cy - 1f, cx + 7f, cy + 10f), 0f, 180f, false, paint)
}
de.waypointaudio.util.PoiSubtype.MUSEUM -> {
// Giebelhaus
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.0f
val roofP = android.graphics.Path().apply {
moveTo(cx, cy - 12f); lineTo(cx - 10f, cy - 4f); lineTo(cx + 10f, cy - 4f); close()
}
canvas.drawPath(roofP, paint)
canvas.drawLine(cx - 10f, cy + 8f, cx + 10f, cy + 8f, paint)
paint.strokeWidth = 1.8f
for (si in 0..2) { val sx = cx - 7f + si * 7f; canvas.drawLine(sx, cy - 4f, sx, cy + 8f, paint) }
}
de.waypointaudio.util.PoiSubtype.VIEWPOINT -> {
// Fernglas
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.2f
canvas.drawCircle(cx - 5f, cy + 2f, 5f, paint)
canvas.drawCircle(cx + 5f, cy + 2f, 5f, paint)
paint.style = android.graphics.Paint.Style.FILL
canvas.drawRect(cx - 2f, cy - 8f, cx + 2f, cy - 3f, paint)
}
de.waypointaudio.util.PoiSubtype.MONUMENT, de.waypointaudio.util.PoiSubtype.HISTORIC_OTHER -> {
// Säule
paint.style = android.graphics.Paint.Style.FILL
canvas.drawRect(cx - 2.5f, cy - 9f, cx + 2.5f, cy + 7f, paint)
canvas.drawRect(cx - 5f, cy - 11f, cx + 5f, cy - 8f, paint)
canvas.drawRect(cx - 6f, cy + 6f, cx + 6f, cy + 9f, paint)
}
de.waypointaudio.util.PoiSubtype.RUIN -> {
// Gebrochene Mauer
paint.style = android.graphics.Paint.Style.FILL
val rb = cy + 8f
canvas.drawRect(cx - 10f, cy - 2f, cx - 4f, rb, paint)
canvas.drawRect(cx - 10f, cy - 8f, cx - 7f, cy - 2f, paint)
canvas.drawRect(cx + 2f, cy, cx + 10f, rb, paint)
canvas.drawRect(cx + 5f, cy - 5f, cx + 9f, cy, paint)
}
de.waypointaudio.util.PoiSubtype.GALLERY -> {
// Bilderrahmen
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.2f
canvas.drawRoundRect(android.graphics.RectF(cx - 9f, cy - 9f, cx + 9f, cy + 9f), 2f, 2f, paint)
paint.strokeWidth = 1.5f
canvas.drawRect(cx - 5f, cy - 5f, cx + 5f, cy + 5f, paint)
}
de.waypointaudio.util.PoiSubtype.THEATRE -> {
// Zwei Drama-Bogen
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.0f
canvas.drawArc(android.graphics.RectF(cx - 10f, cy - 8f, cx - 1f, cy + 4f), 0f, 270f, false, paint)
canvas.drawArc(android.graphics.RectF(cx + 1f, cy - 8f, cx + 10f, cy + 4f), 270f, 270f, false, paint)
}
de.waypointaudio.util.PoiSubtype.ZOO -> {
// Pfoten-Symbol
paint.style = android.graphics.Paint.Style.FILL
canvas.drawCircle(cx, cy + 4f, 6f, paint)
canvas.drawCircle(cx - 6f, cy - 3f, 3f, paint)
canvas.drawCircle(cx, cy - 7f, 3f, paint)
canvas.drawCircle(cx + 6f, cy - 3f, 3f, paint)
}
de.waypointaudio.util.PoiSubtype.GENERIC_SIGHTS, de.waypointaudio.util.PoiSubtype.ATTRACTION -> {
// Monument-Nadel (wie v2.5.44)
paint.style = android.graphics.Paint.Style.FILL
val stTop = cy - 10f; val stBot = cy + 4f
canvas.drawRect(cx - 1.75f, stTop, cx + 1.75f, stBot, paint)
val roofPath = android.graphics.Path().apply {
moveTo(cx, stTop - 5f)
lineTo(cx - 5f, stTop)
lineTo(cx + 5f, stTop)
close()
moveTo(cx, stTop - 5f); lineTo(cx - 5f, stTop); lineTo(cx + 5f, stTop); close()
}
canvas.drawPath(roofPath, paint)
// Sockel/Basis unten
canvas.drawRect(cx - 6f, stBot, cx + 6f, stBot + 2.5f, paint)
}
de.waypointaudio.data.PoiCategory.GASTRONOMY -> {
// Tasse mit Henkel und Dampf-Linie
val cupLeft = cx - 9f
val cupRight = cx + 9f
val cupTop = cy - 3f
val cupBot = cy + 9f
// Tassenform: Trapez (breiter unten)
val cupPath = android.graphics.Path().apply {
moveTo(cupLeft + 2f, cupTop)
lineTo(cupLeft, cupBot)
lineTo(cupRight, cupBot)
lineTo(cupRight - 2f, cupTop)
close()
// ─── GASTRONOMY Subtypes ────────────────────────────────────────────────────
de.waypointaudio.util.PoiSubtype.RESTAURANT -> {
// Messer + Gabel
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.0f
canvas.drawLine(cx - 5f, cy + 9f, cx - 5f, cy - 10f, paint)
paint.strokeWidth = 1.5f
canvas.drawLine(cx - 7f, cy - 10f, cx - 7f, cy - 4f, paint)
canvas.drawLine(cx - 3f, cy - 10f, cx - 3f, cy - 4f, paint)
paint.strokeWidth = 2.0f
canvas.drawLine(cx + 5f, cy + 9f, cx + 5f, cy - 4f, paint)
val knifePath = android.graphics.Path().apply {
moveTo(cx + 5f, cy - 4f); lineTo(cx + 8f, cy - 10f); lineTo(cx + 5f, cy - 10f); close()
}
paint.style = android.graphics.Paint.Style.STROKE
paint.strokeWidth = 2.3f
paint.style = android.graphics.Paint.Style.FILL; canvas.drawPath(knifePath, paint)
}
de.waypointaudio.util.PoiSubtype.CAFE -> {
// Tasse mit Dampf
val cupLeft = cx - 9f; val cupRight = cx + 9f; val cupTop = cy - 3f; val cupBot = cy + 9f
val cupPath = android.graphics.Path().apply {
moveTo(cupLeft + 2f, cupTop); lineTo(cupLeft, cupBot); lineTo(cupRight, cupBot); lineTo(cupRight - 2f, cupTop); close()
}
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.3f
canvas.drawPath(cupPath, paint)
// Untertasse
paint.strokeWidth = 2f
canvas.drawLine(cupLeft - 2f, cupBot + 2f, cupRight + 2f, cupBot + 2f, paint)
// Henkel rechts
val hRect = android.graphics.RectF(cupRight - 1f, cupTop + 4f, cupRight + 6f, cupBot - 2f)
canvas.drawArc(hRect, -90f, 180f, false, paint)
// Dampf-Wellenlinie oben (eine kleine Kurve)
paint.strokeWidth = 2f; canvas.drawLine(cupLeft - 2f, cupBot + 2f, cupRight + 2f, cupBot + 2f, paint)
canvas.drawArc(android.graphics.RectF(cupRight - 1f, cupTop + 4f, cupRight + 6f, cupBot - 2f), -90f, 180f, false, paint)
paint.strokeWidth = 1.8f
val steamPath = android.graphics.Path().apply {
moveTo(cx - 3f, cupTop - 3f)
quadTo(cx - 1f, cupTop - 7f, cx + 1f, cupTop - 3f)
quadTo(cx + 3f, cupTop + 1f, cx + 5f, cupTop - 3f)
}
canvas.drawPath(steamPath, paint)
moveTo(cx - 3f, cupTop - 3f); quadTo(cx - 1f, cupTop - 7f, cx + 1f, cupTop - 3f)
}; canvas.drawPath(steamPath, paint)
}
de.waypointaudio.data.PoiCategory.MOBILITY -> {
// Bus-Silhouette: rechteckiger Körper, Räder, Fenster
val busLeft = cx - 10f
val busRight = cx + 10f
val busTop = cy - 8f
val busBot = cy + 6f
// Buskarosserie
paint.style = android.graphics.Paint.Style.STROKE
paint.strokeWidth = 2.2f
val busRect = android.graphics.RectF(busLeft, busTop, busRight, busBot)
canvas.drawRoundRect(busRect, 3f, 3f, paint)
// Frontscheibe (linke Seite)
de.waypointaudio.util.PoiSubtype.BAR -> {
// Bierglas
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.0f
val glassPath = android.graphics.Path().apply {
moveTo(cx - 7f, cy - 9f); lineTo(cx - 9f, cy + 9f); lineTo(cx + 9f, cy + 9f); lineTo(cx + 7f, cy - 9f); close()
}
canvas.drawPath(glassPath, paint)
canvas.drawArc(android.graphics.RectF(cx + 7f, cy - 5f, cx + 13f, cy + 5f), -90f, 180f, false, paint)
}
de.waypointaudio.util.PoiSubtype.FAST_FOOD -> {
// Burger-Silhouette
paint.style = android.graphics.Paint.Style.FILL
canvas.drawArc(android.graphics.RectF(cx - 9f, cy - 10f, cx + 9f, cy + 2f), 180f, 180f, true, paint)
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.8f
canvas.drawLine(cx - 9f, cy + 2f, cx + 9f, cy + 2f, paint)
canvas.drawLine(cx - 9f, cy + 6f, cx + 9f, cy + 6f, paint)
paint.style = android.graphics.Paint.Style.FILL
canvas.drawArc(android.graphics.RectF(cx - 9f, cy + 4f, cx + 9f, cy + 12f), 0f, 180f, true, paint)
}
de.waypointaudio.util.PoiSubtype.GENERIC_GASTRONOMY -> {
// Tasse (Fallback, wie v2.5.44)
val cupLeft = cx - 9f; val cupRight = cx + 9f; val cupTop = cy - 3f; val cupBot = cy + 9f
val cupPath = android.graphics.Path().apply {
moveTo(cupLeft + 2f, cupTop); lineTo(cupLeft, cupBot); lineTo(cupRight, cupBot); lineTo(cupRight - 2f, cupTop); close()
}
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.3f
canvas.drawPath(cupPath, paint)
paint.strokeWidth = 2f; canvas.drawLine(cupLeft - 2f, cupBot + 2f, cupRight + 2f, cupBot + 2f, paint)
canvas.drawArc(android.graphics.RectF(cupRight - 1f, cupTop + 4f, cupRight + 6f, cupBot - 2f), -90f, 180f, false, paint)
}
// ─── MOBILITY Subtypes ──────────────────────────────────────────────────────
de.waypointaudio.util.PoiSubtype.PARKING -> {
// "P"-Piktogramm
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 3f
canvas.drawLine(cx - 5f, cy - 10f, cx - 5f, cy + 10f, paint)
canvas.drawArc(android.graphics.RectF(cx - 5f, cy - 10f, cx + 8f, cy + 0f), -90f, 180f, false, paint)
}
de.waypointaudio.util.PoiSubtype.BUS_STOP -> {
// Bus-Silhouette (wie v2.5.44)
val busLeft = cx - 10f; val busRight = cx + 10f; val busTop = cy - 8f; val busBot = cy + 6f
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.2f
canvas.drawRoundRect(android.graphics.RectF(busLeft, busTop, busRight, busBot), 3f, 3f, paint)
canvas.drawLine(busLeft + 2f, busTop + 2f, busLeft + 2f, busTop + 7f, paint)
// Fenster 1
val w1 = android.graphics.RectF(busLeft + 4f, busTop + 2f, busLeft + 9f, busTop + 6f)
paint.strokeWidth = 1.5f
canvas.drawRoundRect(w1, 1f, 1f, paint)
// Fenster 2
val w2 = android.graphics.RectF(busLeft + 11f, busTop + 2f, busLeft + 16f, busTop + 6f)
canvas.drawRoundRect(w2, 1f, 1f, paint)
// Rad links
canvas.drawRoundRect(android.graphics.RectF(busLeft + 4f, busTop + 2f, busLeft + 9f, busTop + 6f), 1f, 1f, paint)
canvas.drawRoundRect(android.graphics.RectF(busLeft + 11f, busTop + 2f, busLeft + 16f, busTop + 6f), 1f, 1f, paint)
paint.strokeWidth = 2f
canvas.drawCircle(busLeft + 5f, busBot + 1f, 3f, paint)
// Rad rechts
canvas.drawCircle(busRight - 5f, busBot + 1f, 3f, paint)
}
de.waypointaudio.data.PoiCategory.INFRASTRUCTURE -> {
// Info-"i"-Piktogramm: großer Punkt oben, vertikaler Stab unten
de.waypointaudio.util.PoiSubtype.TRAIN_STATION -> {
// Zug (vereinfacht)
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.0f
canvas.drawRoundRect(android.graphics.RectF(cx - 12f, cy - 7f, cx + 12f, cy + 5f), 3f, 3f, paint)
paint.style = android.graphics.Paint.Style.FILL
canvas.drawRect(cx - 6f, cy - 11f, cx - 3f, cy - 7f, paint)
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 1.8f
canvas.drawCircle(cx - 7f, cy + 7f, 3f, paint)
canvas.drawCircle(cx + 2f, cy + 7f, 3f, paint)
canvas.drawCircle(cx + 9f, cy + 7f, 3f, paint)
}
de.waypointaudio.util.PoiSubtype.TRAM_STOP -> {
// Straßenbahn
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.0f
canvas.drawRoundRect(android.graphics.RectF(cx - 10f, cy - 7f, cx + 10f, cy + 5f), 3f, 3f, paint)
canvas.drawLine(cx - 10f, cy - 9f, cx + 10f, cy - 9f, paint)
paint.strokeWidth = 1.5f; canvas.drawLine(cx, cy - 9f, cx, cy - 7f, paint)
canvas.drawCircle(cx - 5f, cy + 7f, 2.5f, paint)
canvas.drawCircle(cx + 5f, cy + 7f, 2.5f, paint)
}
de.waypointaudio.util.PoiSubtype.FUEL -> {
// Zapfsäule
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.0f
canvas.drawRect(cx - 6f, cy - 5f, cx + 6f, cy + 9f, paint)
canvas.drawLine(cx - 6f, cy - 5f, cx - 10f, cy - 9f, paint)
canvas.drawLine(cx - 10f, cy - 9f, cx - 10f, cy - 3f, paint)
paint.strokeWidth = 1.5f; canvas.drawRect(cx - 4f, cy - 3f, cx + 4f, cy + 3f, paint)
}
de.waypointaudio.util.PoiSubtype.GENERIC_MOBILITY -> {
// Bus (Fallback, wie v2.5.44)
val busLeft = cx - 10f; val busRight = cx + 10f; val busTop = cy - 8f; val busBot = cy + 6f
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.2f
canvas.drawRoundRect(android.graphics.RectF(busLeft, busTop, busRight, busBot), 3f, 3f, paint)
canvas.drawLine(busLeft + 2f, busTop + 2f, busLeft + 2f, busTop + 7f, paint)
paint.strokeWidth = 1.5f
canvas.drawRoundRect(android.graphics.RectF(busLeft + 4f, busTop + 2f, busLeft + 9f, busTop + 6f), 1f, 1f, paint)
canvas.drawRoundRect(android.graphics.RectF(busLeft + 11f, busTop + 2f, busLeft + 16f, busTop + 6f), 1f, 1f, paint)
paint.strokeWidth = 2f
canvas.drawCircle(busLeft + 5f, busBot + 1f, 3f, paint)
canvas.drawCircle(busRight - 5f, busBot + 1f, 3f, paint)
}
// ─── INFRASTRUCTURE Subtypes ────────────────────────────────────────────────
de.waypointaudio.util.PoiSubtype.PHARMACY -> {
// Apotheken-Kreuz
paint.style = android.graphics.Paint.Style.FILL
canvas.drawRect(cx - 3f, cy - 11f, cx + 3f, cy + 11f, paint)
canvas.drawRect(cx - 11f, cy - 3f, cx + 11f, cy + 3f, paint)
}
de.waypointaudio.util.PoiSubtype.HOSPITAL -> {
// H-Symbol
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 3f
canvas.drawLine(cx - 7f, cy - 10f, cx - 7f, cy + 10f, paint)
canvas.drawLine(cx + 7f, cy - 10f, cx + 7f, cy + 10f, paint)
canvas.drawLine(cx - 7f, cy, cx + 7f, cy, paint)
}
de.waypointaudio.util.PoiSubtype.TOILETS -> {
// WC-Piktogramm (Person-Silhouette)
paint.style = android.graphics.Paint.Style.FILL
canvas.drawCircle(cx - 6f, cy - 9f, 3f, paint)
val dressPath = android.graphics.Path().apply {
moveTo(cx - 10f, cy + 8f); lineTo(cx - 6f, cy - 6f); lineTo(cx - 2f, cy + 8f); close()
}; canvas.drawPath(dressPath, paint)
canvas.drawCircle(cx + 6f, cy - 9f, 3f, paint)
canvas.drawRect(cx + 4f, cy - 6f, cx + 8f, cy + 8f, paint)
}
de.waypointaudio.util.PoiSubtype.DRINKING_WATER -> {
// Wassertropfen
val dropPath = android.graphics.Path().apply {
moveTo(cx, cy + 10f)
cubicTo(cx - 10f, cy + 2f, cx - 10f, cy - 6f, cx, cy - 12f)
cubicTo(cx + 10f, cy - 6f, cx + 10f, cy + 2f, cx, cy + 10f)
}
paint.style = android.graphics.Paint.Style.FILL; canvas.drawPath(dropPath, paint)
}
de.waypointaudio.util.PoiSubtype.GENERIC_INFRASTRUCTURE -> {
// Info-i (wie v2.5.44)
paint.style = android.graphics.Paint.Style.FILL
// Punkt oben
canvas.drawCircle(cx, cy - 8f, 3.5f, paint)
// Stab unten — leicht breiter, abgerundet
val stW2 = 4.5f
val rect2 = android.graphics.RectF(cx - stW2 / 2f, cy - 2f, cx + stW2 / 2f, cy + 9f)
canvas.drawRoundRect(rect2, 2f, 2f, paint)
// Basis-Strich
canvas.drawRoundRect(android.graphics.RectF(cx - stW2/2f, cy - 2f, cx + stW2/2f, cy + 9f), 2f, 2f, paint)
canvas.drawRoundRect(android.graphics.RectF(cx - 7f, cy + 8f, cx + 7f, cy + 10f), 2f, 2f, paint)
}
}
@@ -3712,6 +4013,7 @@ private fun buildExplorerPoiIcon(category: de.waypointaudio.data.PoiCategory): B
private fun buildExplorerPoiIconWithLabel(
category: de.waypointaudio.data.PoiCategory,
subtype: de.waypointaudio.util.PoiSubtype = de.waypointaudio.util.PoiSubtype.GENERIC_SIGHTS,
name: String
): Bitmap {
val iconSize = 56 // gleiche Größe wie buildExplorerPoiIcon
@@ -3736,7 +4038,7 @@ private fun buildExplorerPoiIconWithLabel(
val paint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
// Zeichne Kategorie-Icon zentriert oben
val iconBmp = buildExplorerPoiIcon(category)
val iconBmp = buildExplorerPoiIcon(category, subtype)
val iconLeft = (totalW - iconSize) / 2f
canvas.drawBitmap(iconBmp, iconLeft, 0f, null)
iconBmp.recycle()
@@ -3844,11 +4146,17 @@ private fun ExplorerPoiLegend(
)
}
}
Text(
"Symbole zeigen Untertypen",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f),
modifier = Modifier.padding(top = 1.dp)
)
Text(
"antippen zum Einklappen",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f),
modifier = Modifier.padding(top = 1.dp)
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f),
modifier = Modifier.padding(top = 0.dp)
)
}
}
@@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AltRoute
import androidx.compose.material.icons.filled.Explore
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.GpsFixed
import androidx.compose.material.icons.filled.Info
@@ -105,6 +106,9 @@ fun MapLayerFab(
onPerspectiveTiltChange: (Int) -> Unit = {},
onShowRoutingInfo: () -> Unit = {},
onShowRoutePlanner: () -> Unit = {},
// v2.5.45 — Auto-POI-Load-Toggle
poiAutoLoadEnabled: Boolean = true,
onPoiAutoLoadChange: (Boolean) -> Unit = {},
modifier: Modifier = Modifier
) {
var showSheet by remember { mutableStateOf(false) }
@@ -490,6 +494,19 @@ fun MapLayerFab(
)
}
)
// v2.5.45 — Auto-POI-Laden Toggle
LayerSwitchRow(
icon = Icons.Filled.Explore,
label = "POIs automatisch nachladen",
checked = poiAutoLoadEnabled,
onCheckedChange = onPoiAutoLoadChange
)
Text(
"Auto-Laden: POIs werden beim Kartenverschieben aktualisiert (ab Zoom 12)",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f),
modifier = Modifier.padding(start = 28.dp)
)
}
}
@@ -11,6 +11,87 @@ import java.net.URL
import java.net.URLEncoder
import kotlin.math.*
/**
* POI-Untertyp — v2.5.45.
*
* Abgeleitet aus OSM-Tags (amenity, tourism, historic, railway, highway …).
* Wird für Marker-Piktogramm-Varianten und Detail-Sheet genutzt.
* KEIN Persistenzmodell-Einfluss: nur In-Memory/Runtime.
*
* Für SIGHTS detailliert (Burg, Kirche, Museum, Aussicht, Denkmal …);
* für GASTRONOMY (Restaurant, Café, Bar, Schnellimbiss);
* für MOBILITY und INFRASTRUCTURE: wichtigste Typen.
*/
enum class PoiSubtype {
// SIGHTS
CASTLE, // historic=castle|palace|fort
RUIN, // historic=ruins|archaeological_site
CHURCH, // amenity=place_of_worship + religion=christian | historic=church|chapel|monastery
MONASTERY, // historic=monastery
MUSEUM, // tourism=museum
VIEWPOINT, // tourism=viewpoint
MONUMENT, // historic=monument|memorial
GALLERY, // tourism=gallery|artwork
THEATRE, // amenity=theatre|cinema
ZOO, // tourism=zoo|theme_park
ATTRACTION, // tourism=attraction (generic)
HISTORIC_OTHER, // historic=* (other)
// GASTRONOMY
RESTAURANT, // amenity=restaurant
CAFE, // amenity=cafe
BAR, // amenity=bar|pub|biergarten
FAST_FOOD, // amenity=fast_food|food_court|ice_cream
// MOBILITY
PARKING, // amenity=parking
BUS_STOP, // highway=bus_stop
TRAIN_STATION, // railway=station|halt
TRAM_STOP, // railway=tram_stop
FUEL, // amenity=fuel|charging_station
// INFRASTRUCTURE
PHARMACY, // amenity=pharmacy
HOSPITAL, // amenity=hospital|clinic
TOILETS, // amenity=toilets
DRINKING_WATER, // amenity=drinking_water
// Generic fallback per category
GENERIC_SIGHTS,
GENERIC_GASTRONOMY,
GENERIC_MOBILITY,
GENERIC_INFRASTRUCTURE;
/** Menschenlesbarer Label für das Detail-Sheet. */
val displayLabel: String get() = when (this) {
CASTLE -> "Burg/Schloss"
RUIN -> "Ruine/Archäologie"
CHURCH -> "Kirche/Gotteshaus"
MONASTERY -> "Kloster"
MUSEUM -> "Museum"
VIEWPOINT -> "Aussichtspunkt"
MONUMENT -> "Denkmal/Gedenkstätte"
GALLERY -> "Galerie/Kunstwerk"
THEATRE -> "Theater/Kino"
ZOO -> "Zoo/Freizeitpark"
ATTRACTION -> "Sehenswürdigkeit"
HISTORIC_OTHER -> "Historisch"
RESTAURANT -> "Restaurant"
CAFE -> "Café"
BAR -> "Bar/Pub"
FAST_FOOD -> "Schnellimbiss"
PARKING -> "Parkplatz"
BUS_STOP -> "Bushaltestelle"
TRAIN_STATION -> "Bahnhof"
TRAM_STOP -> "Straßenbahnhaltestelle"
FUEL -> "Tankstelle/Ladestation"
PHARMACY -> "Apotheke"
HOSPITAL -> "Krankenhaus/Klinik"
TOILETS -> "Toiletten"
DRINKING_WATER -> "Trinkwasser"
GENERIC_SIGHTS -> "Sehenswürdigkeit"
GENERIC_GASTRONOMY -> "Gastronomie"
GENERIC_MOBILITY -> "Mobilität"
GENERIC_INFRASTRUCTURE -> "Infrastruktur"
}
}
/**
* Ein Explorer-POI aus Overpass/OpenStreetMap.
*
@@ -18,6 +99,8 @@ import kotlin.math.*
* - osmId / osmType für Attribution/Detail-Link
* - rawTags für das POI-Detail-Sheet
* - category aus [PoiCategory]
*
* v2.5.45 — Subtype-Feld ergänzt (kein Backup-/Persistenzmodell-Einfluss).
*/
data class ExplorerPoiResult(
val osmId: Long,
@@ -25,6 +108,7 @@ data class ExplorerPoiResult(
val displayName: String,
val category: PoiCategory,
val categoryLabel: String, // lesbarer Label wie "Restaurant", "Museum" …
val subtype: PoiSubtype = PoiSubtype.GENERIC_SIGHTS, // v2.5.45
val latitude: Double,
val longitude: Double,
val rawTags: Map<String, String> = emptyMap(),
@@ -39,52 +123,33 @@ data class LatLngBbox(
)
/**
* Explorer-Overpass-Client — v2.5.44 (POI Category Fix).
* Explorer-Overpass-Client — v2.5.45 (Explorer POI Autoload).
*
* Rate-Limit-Strategie:
* - KEIN automatisches Polling.
* - Abfragen nur auf explizite Nutzer-Aktion ("POIs laden").
* - Automatisches Polling auf explizite Nutzerwahl — standardmäßig aktiviert,
* aber kontrolliert:
* • Debounce 1.5 s nach Kartenbewegung
* • Nur wenn POI-Layer aktiv
* • Mindest-Zoom ≥ 12 (kein Auto-Laden bei sehr weitem Ausschnitt)
* • Cache-basiert: neue Anfrage nur bei deutlicher Bewegung (> MOVE_THRESHOLD_M)
* oder Zoom-/Kategorie-Wechsel
* • Max-Radius ≤ 5 km (wie bisher)
* - Manueller Lade-Button als Fallback bleibt erhalten.
* - In-Memory-Cache je Kategorie + BBox-Fingerprint.
* - Radius auf ≤ 5 km begrenzt; bei größerem Kartenausschnitt Warnung.
* - Overpass timeout auf 25 s gesetzt; max. 100 Ergebnisse je Kategorie.
*
* v2.5.44 Bug-Fix: Mapping-Priorität korrigiert.
* - GASTRONOMY, MOBILITY und INFRASTRUCTURE-Tags werden nun ZUERST geprüft,
* bevor historic=* greift. Damit landen Restaurants, Bushaltestellen etc.
* nicht mehr in der Sehenswürdigkeiten-Kategorie.
* - humanizeTag() gibt für alle Kategorien einen sinnvollen Fallback zurück,
* sodass auch namenlose Einträge (z. B. Parkplätze, Bushaltestellen) nicht
* mehr verworfen werden.
* - Default PoiLayerSettings: alle vier Kategorien standardmäßig aktiv
* (vorher nur Sehenswürdigkeiten = Root Cause des gemeldeten Bugs).
* - Erweiterte Overpass-Filter: ferry_terminal, aeroway=aerodrome,
* amenity=ferry_terminal, amenity=shelter, amenity=bench (optional),
* amenity=drinking_water für Infrastruktur.
* - Logcat-Zusammenfassung nach POI-Laden ("POI-Load: X Sehenswürdigkeiten …").
* v2.5.45 — Subtype-Auflösung aus OSM-Tags (kein Persistenzmodell-Einfluss).
*
* OSM-Tags je Kategorie:
* OSM-Tags je Kategorie: (unverändert zu v2.5.44)
*
* SIGHTS:
* tourism = attraction | viewpoint | museum | gallery | artwork | zoo | theme_park
* historic = * (jeder Wert)
* amenity = theatre | arts_centre | cinema | place_of_worship
*
* GASTRONOMY:
* amenity = restaurant | cafe | bar | pub | fast_food | ice_cream |
* biergarten | food_court
*
* MOBILITY:
* amenity = parking | bicycle_parking | fuel | charging_station |
* bus_station | ferry_terminal
* highway = bus_stop
* railway = station | halt | tram_stop
* public_transport = station | platform
* aeroway = aerodrome
*
* INFRASTRUCTURE:
* amenity = toilets | hospital | clinic | pharmacy | bank | atm |
* post_office | police | fire_station | library |
* drinking_water | shelter
* SIGHTS: tourism=attraction|viewpoint|museum|gallery|artwork|zoo|theme_park,
* historic=*, amenity=theatre|arts_centre|cinema|place_of_worship
* GASTRONOMY: amenity=restaurant|cafe|bar|pub|fast_food|ice_cream|biergarten|food_court
* MOBILITY: amenity=parking|bicycle_parking|fuel|charging_station|bus_station|ferry_terminal,
* highway=bus_stop, railway=station|halt|tram_stop,
* public_transport=station|platform, aeroway=aerodrome
* INFRASTRUCTURE: amenity=toilets|hospital|clinic|pharmacy|bank|atm|post_office|
* police|fire_station|library|drinking_water|shelter
*
* Attribution: POI-Daten © OpenStreetMap-Mitwirkende, ODbL.
* Overpass API: overpass-api.de
@@ -95,12 +160,45 @@ 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
/** 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
// In-Memory-Cache: key = "kategorie|bboxFingerprint"
private val cache = mutableMapOf<String, List<ExplorerPoiResult>>()
/** Cache leeren — z. B. beim Verlassen des Explorers. */
/** Cache leeren — z. B. beim Verlassen des Explorers oder bei Kategorie-Wechsel. */
fun clearCache() = cache.clear()
/**
* Prüft ob ein Auto-Load sinnvoll ist.
* Gibt true zurück wenn:
* - Mindestens ein POI-Layer aktiv ist
* - Zoom ≥ AUTO_LOAD_MIN_ZOOM
* - Karte hat sich um > MOVE_THRESHOLD_M vom letzten Auto-Load-Zentrum entfernt
* ODER Zoom hat sich um > 0.8 verändert ODER Kategorien haben sich geändert.
*/
fun shouldAutoLoad(
zoom: Double,
centerLat: Double,
centerLon: Double,
lastAutoLat: Double?,
lastAutoLon: Double?,
lastAutoZoom: Double?,
lastAutoCategories: Set<PoiCategory>?,
activeCategories: Set<PoiCategory>,
): Boolean {
if (activeCategories.isEmpty()) return false
if (zoom < AUTO_LOAD_MIN_ZOOM) return false
if (lastAutoLat == null || lastAutoLon == null || lastAutoZoom == null || lastAutoCategories == null) return true
if (activeCategories != lastAutoCategories) return true
val moved = haversineM(centerLat, centerLon, lastAutoLat, lastAutoLon)
val zoomDelta = kotlin.math.abs(zoom - lastAutoZoom)
return moved > MOVE_THRESHOLD_M || zoomDelta > 0.8
}
/**
* Lädt POIs für [categories] im Kartenausschnitt [bbox].
*
@@ -212,7 +310,6 @@ object ExplorerPoiClient {
): String {
val around = "around:$radiusM,$lat,$lon"
val filters = categoryFilters(category)
// Jeder Filter wird als node/way/relation-Union gebaut
val unionParts = filters.joinToString("\n") { (key, value) ->
if (value == "*") {
""" node[$key]($around);
@@ -231,14 +328,6 @@ $unionParts
out center 100;""".trimIndent()
}
/**
* OSM-Tag-Filter je Kategorie. Wert "*" bedeutet: jeder Wert.
*
* v2.5.44 Fix: Erweitert um ferry_terminal, tram_stop, aerodrome,
* drinking_water, shelter. Reihenfolge innerhalb SIGHTS unverändert
* (historic bleibt als letzter breitester Filter — gibt aber keine
* Kollision, da jede Kategorie eine eigene Overpass-Anfrage hat).
*/
private fun categoryFilters(cat: PoiCategory): List<Pair<String, String>> = when (cat) {
PoiCategory.SIGHTS -> listOf(
"tourism" to "attraction",
@@ -302,14 +391,14 @@ out center 100;""".trimIndent()
val root = JSONObject(json)
val elements = root.optJSONArray("elements") ?: return emptyList()
val out = ArrayList<ExplorerPoiResult>(elements.length())
val seen = HashSet<Long>() // Doppelmarker-Schutz via osmId
val seen = HashSet<Long>()
for (i in 0 until elements.length()) {
val el = elements.optJSONObject(i) ?: continue
val osmId = el.optLong("id", -1L)
val osmType = el.optString("type", "node")
if (osmId < 0) continue
if (!seen.add(osmId)) continue // Duplikat
if (!seen.add(osmId)) continue
val lat = el.optDouble("lat", Double.NaN).let {
if (it.isNaN()) el.optJSONObject("center")?.optDouble("lat", Double.NaN) ?: Double.NaN
@@ -330,18 +419,13 @@ out center 100;""".trimIndent()
}
}
// v2.5.44 Fix: Kategorie-Mapping-Priorität korrigiert.
// resolveCategoryLabel() prüft zuerst spezifische Nicht-SIGHTS-Tags
// (amenity=restaurant usw.) BEVOR historic=* greift. Da jede
// Kategorie eine eigene Anfrage abschickt, ist das Mapping hier
// nur für den categoryLabel relevant — die category selbst stammt
// immer aus dem Funktionsparameter (korrekt).
val catLabel = resolveCategoryLabel(tagsMap, category)
val subtype = resolveSubtype(tagsMap, category) // v2.5.45
val name = tagsMap["name"]?.takeIf { it.isNotBlank() }
?: tagsMap["ref"]?.takeIf { it.isNotBlank() }
?: humanizeTag(tagsMap, category)
?: continue // namenloser POI ohne Fallback → überspringen
?: continue
out.add(
ExplorerPoiResult(
@@ -350,6 +434,7 @@ out center 100;""".trimIndent()
displayName = name,
category = category,
categoryLabel = catLabel,
subtype = subtype,
latitude = lat,
longitude = lon,
rawTags = tagsMap,
@@ -360,16 +445,56 @@ out center 100;""".trimIndent()
}
/**
* Menschenlesbarer Kategorie-Label für den POI (z. B. "Restaurant", "Museum").
*
* v2.5.44 Fix: Reihenfolge der Prüfungen in SIGHTS korrigiert.
* Gastronomie- und Mobilitäts-spezifische amenity-Werte (restaurant, cafe,
* parking usw.) werden ZUERST auf GASTRONOMY/MOBILITY/INFRASTRUCTURE
* gemappt und kommen nie als Sehenswürdigkeit durch. Da jedoch jede
* Kategorie eine separate Overpass-Anfrage hat, trifft dies in der Praxis
* nur Elemente, die mehrere Tag-Kombinationen tragen (z. B. restaurant +
* tourism=attraction). Für diese gilt: spezifisches Tag gewinnt.
* v2.5.45 — Subtype aus OSM-Tags ableiten (In-Memory only, kein Backup-Einfluss).
*/
private fun resolveSubtype(tags: Map<String, String>, cat: PoiCategory): PoiSubtype {
val amenity = tags["amenity"]
val tourism = tags["tourism"]
val historic = tags["historic"]
val railway = tags["railway"]
val highway = tags["highway"]
return when (cat) {
PoiCategory.SIGHTS -> when {
historic == "castle" || historic == "palace" || historic == "fort" -> PoiSubtype.CASTLE
historic == "ruins" || historic == "archaeological_site" -> PoiSubtype.RUIN
historic == "monastery" -> PoiSubtype.MONASTERY
historic == "church" -> PoiSubtype.CHURCH
historic == "monument" || historic == "memorial" -> PoiSubtype.MONUMENT
historic != null -> PoiSubtype.HISTORIC_OTHER
amenity == "place_of_worship" -> PoiSubtype.CHURCH
tourism == "museum" -> PoiSubtype.MUSEUM
tourism == "viewpoint" -> PoiSubtype.VIEWPOINT
tourism == "gallery" || tourism == "artwork" -> PoiSubtype.GALLERY
tourism == "zoo" || tourism == "theme_park" -> PoiSubtype.ZOO
amenity == "theatre" || amenity == "cinema" || amenity == "arts_centre" -> PoiSubtype.THEATRE
tourism == "attraction" -> PoiSubtype.ATTRACTION
else -> PoiSubtype.GENERIC_SIGHTS
}
PoiCategory.GASTRONOMY -> when (amenity) {
"restaurant" -> PoiSubtype.RESTAURANT
"cafe" -> PoiSubtype.CAFE
"bar", "pub", "biergarten" -> PoiSubtype.BAR
"fast_food", "food_court", "ice_cream" -> PoiSubtype.FAST_FOOD
else -> PoiSubtype.GENERIC_GASTRONOMY
}
PoiCategory.MOBILITY -> when {
amenity == "parking" || amenity == "bicycle_parking" -> PoiSubtype.PARKING
highway == "bus_stop" || amenity == "bus_station" -> PoiSubtype.BUS_STOP
railway == "station" || railway == "halt" -> PoiSubtype.TRAIN_STATION
railway == "tram_stop" -> PoiSubtype.TRAM_STOP
amenity == "fuel" || amenity == "charging_station" -> PoiSubtype.FUEL
else -> PoiSubtype.GENERIC_MOBILITY
}
PoiCategory.INFRASTRUCTURE -> when (amenity) {
"pharmacy" -> PoiSubtype.PHARMACY
"hospital", "clinic" -> PoiSubtype.HOSPITAL
"toilets" -> PoiSubtype.TOILETS
"drinking_water" -> PoiSubtype.DRINKING_WATER
else -> PoiSubtype.GENERIC_INFRASTRUCTURE
}
}
}
private fun resolveCategoryLabel(tags: Map<String, String>, cat: PoiCategory): String {
val amenity = tags["amenity"]
val tourism = tags["tourism"]
@@ -379,9 +504,6 @@ out center 100;""".trimIndent()
val publicTransport = tags["public_transport"]
val aeroway = tags["aeroway"]
// v2.5.44 Fix: Gastronomie/Mobilität/Infrastruktur haben Vorrang,
// auch wenn ein Element gleichzeitig tourism=* oder historic=* trägt.
// Prüfe zuerst die spezifischen Nicht-SIGHTS-Tags.
if (cat != PoiCategory.SIGHTS) {
when (cat) {
PoiCategory.GASTRONOMY -> return when (amenity) {
@@ -446,41 +568,25 @@ out center 100;""".trimIndent()
amenity == "place_of_worship" -> "Gotteshaus"
else -> "Sehenswürdigkeit"
}
// Fallbacks für den else-Fall (sollten durch obige when-Blöcke nicht erreicht werden)
PoiCategory.GASTRONOMY -> "Gastronomie"
PoiCategory.MOBILITY -> "Mobilität"
PoiCategory.INFRASTRUCTURE-> "Infrastruktur"
}
}
/**
* Fallback-Name für namenlose POIs, wenn kein [name]-Tag vorhanden ist.
*
* v2.5.44 Fix: Gibt nun für alle Kategorien einen sinnvollen Fallback
* zurück (nicht mehr nur für SIGHTS+historic und Non-SIGHTS). Damit
* werden z. B. namenlose Bushaltestellen, Parkplätze und Toiletten
* nicht mehr verworfen — sie erhalten den Kategorie-Label als Anzeigename.
*
* Ausnahme: Für SIGHTS ohne tourism/historic/spez. amenity-Tag wäre
* "Sehenswürdigkeit" als Name wenig aussagekräftig — diese werden
* nur dann behalten, wenn historic gesetzt ist (Denkmal, Monument etc.)
* oder ein eindeutiger tourism-Wert vorliegt.
*/
private fun humanizeTag(tags: Map<String, String>, cat: PoiCategory): String? {
return when (cat) {
PoiCategory.SIGHTS -> {
// Für SIGHTS nur dann Fallback-Name, wenn ein spezifisches Tag vorhanden ist
val label = resolveCategoryLabel(tags, cat)
when {
tags["historic"] != null -> label // Denkmal, Ruine etc. immer behalten
tags["tourism"] != null -> label // tourism=viewpoint/museum etc.
tags["historic"] != null -> label
tags["tourism"] != null -> label
tags["amenity"] in setOf(
"theatre","arts_centre","cinema","place_of_worship"
) -> label
else -> null // zu generisch → überspringen
else -> null
}
}
// GASTRONOMY, MOBILITY, INFRASTRUCTURE: immer Fallback-Label zurückgeben
PoiCategory.GASTRONOMY -> resolveCategoryLabel(tags, cat)
PoiCategory.MOBILITY -> resolveCategoryLabel(tags, cat)
PoiCategory.INFRASTRUCTURE -> resolveCategoryLabel(tags, cat)
@@ -503,6 +609,9 @@ out center 100;""".trimIndent()
"locomotive" -> "Lokomotive (hist.)"
"aircraft" -> "Flugzeug (hist.)"
"ship" -> "Schiff (hist.)"
"monastery" -> "Kloster"
"palace" -> "Schloss/Palast"
"fort" -> "Festung"
else -> "Historisch"
}
@@ -0,0 +1,205 @@
# GPS2Audio v2.5.45 — Explorer POI Autoload — Handoff
## Build-Ergebnis
| Artefakt | Status |
|---|---|
| APK | ✅ BUILD SUCCESSFUL |
| Signing | ✅ APK Signature Scheme v2 verified |
| applicationId | `de.waypointaudio` |
| versionCode | 99 |
| versionName | 2.5.45 |
| APK-Datei | `GPS2Audio_v2_5_45_explorer_poi_autoload_universal_signed.apk` (57 MB) |
| Quelle | `GPS2Audio_v2_5_45_explorer_poi_autoload_source.zip` |
Build-Basis: `GPS2Audio_v2_5_44_poi_category_fix_source.zip`
---
## 1. Automatisches POI-Nachladen
### Verhalten
- **Standardmäßig aktiviert** — kein manueller Start nötig.
- POIs werden nach Kartenbewegung automatisch geladen (1.5 Sekunden Debounce nach `onCameraIdle`).
- Neue Anfrage **nur** wenn:
- Mindest-Zoom ≥ 12.0 (`ExplorerPoiClient.AUTO_LOAD_MIN_ZOOM`)
- Karte hat sich um > 300 m bewegt (`MOVE_THRESHOLD_M`) **oder** Zoom hat sich um > 0.8 geändert **oder** Kategorien haben sich geändert
- POI-Layer aktiv (mindestens eine Kategorie)
- Kein laufendes Load in progress
### Schutzmechanismen
| Schutz | Wert |
|---|---|
| Debounce | 1.5 Sekunden nach `onCameraIdle` |
| Minimaler Zoom | ≥ 12.0 (darunter: keine Auto-Anfrage) |
| Maximaler Radius | 5 000 m (wie bisher, `MAX_RADIUS_M`) |
| Cache-Schutz | Cache per Kategorie + BBox-Fingerprint; neue Anfrage nur bei > 300 m Bewegung oder Zoom-/Kategorie-Wechsel |
| Fehlerverhalten | Bei Fehler: alte POIs bleiben, dezente Snackbar-Meldung; kein White-Screen |
| Parallel-Schutz | `explorerPoiLoading`-Flag; laufende Auto-Load-Jobs werden bei neuem Trigger gecancelt |
| Kategorie-Wechsel | Cache + Cursor werden geleert, nächster Idle lädt frisch |
### Manueller Fallback
Der manuelle Reload-Button (`↻`) ist **erhalten** und erzwingt immer einen frischen Load (Cache + Cursor werden geleert).
### Statusanzeige (POI-Bar, BottomCenter)
| Zustand | Anzeige |
|---|---|
| Laden läuft | Spinner + „POIs werden aktualisiert…" |
| Auto-Load aktiv, Ergebnis vorhanden | „POIs: 42" |
| Auto-Load aktiv, Zoom zu klein | „Hereinzoomen für Auto-Load" |
| Auto-Load aktiv, keine Ergebnisse geladen | „POIs: Auto-Laden aktiv" |
| Auto-Load deaktiviert | „POIs: manuell laden" |
### Toggle im Layer-Sheet
Im Ebenen-Panel (Layers-FAB) im POI-Abschnitt:
**„POIs automatisch nachladen"** — Switch, Standard: EIN
Beschriftungshinweis: „Auto-Laden: POIs werden beim Kartenverschieben aktualisiert (ab Zoom 12)"
---
## 2. POI-Untertypen (Subtypes)
### Architektur
Neues Enum `PoiSubtype` in `ExplorerPoiClient.kt`**nur In-Memory/Runtime, kein Persistenz-/Backup-Einfluss**.
`ExplorerPoiResult` erhält ein neues Feld `subtype: PoiSubtype` (Default: `GENERIC_*`).
### Enthaltene Untertypen
| Kategorie | Subtype | OSM-Tags | Piktogramm |
|---|---|---|---|
| SIGHTS | CASTLE | historic=castle/palace/fort | Zinnenkranz + Tor |
| SIGHTS | RUIN | historic=ruins/archaeological_site | Gebrochene Mauer |
| SIGHTS | CHURCH | amenity=place_of_worship, historic=church | Kreuz |
| SIGHTS | MONASTERY | historic=monastery | Kleines Kreuz + Bogen |
| SIGHTS | MUSEUM | tourism=museum | Giebelhaus |
| SIGHTS | VIEWPOINT | tourism=viewpoint | Fernglas |
| SIGHTS | MONUMENT | historic=monument/memorial | Säule |
| SIGHTS | GALLERY | tourism=gallery/artwork | Bilderrahmen |
| SIGHTS | THEATRE | amenity=theatre/cinema/arts_centre | Drama-Bögen |
| SIGHTS | ZOO | tourism=zoo/theme_park | Pfotenabdruck |
| SIGHTS | ATTRACTION | tourism=attraction | Monument-Nadel |
| SIGHTS | HISTORIC_OTHER | historic=* (sonstige) | Säule |
| GASTRONOMY | RESTAURANT | amenity=restaurant | Messer + Gabel |
| GASTRONOMY | CAFE | amenity=cafe | Tasse + Dampf |
| GASTRONOMY | BAR | amenity=bar/pub/biergarten | Bierglas |
| GASTRONOMY | FAST_FOOD | amenity=fast_food/food_court/ice_cream | Burger-Silhouette |
| MOBILITY | PARKING | amenity=parking | „P"-Piktogramm |
| MOBILITY | BUS_STOP | highway=bus_stop, amenity=bus_station | Bus-Silhouette |
| MOBILITY | TRAIN_STATION | railway=station/halt | Zug |
| MOBILITY | TRAM_STOP | railway=tram_stop | Straßenbahn + Stromabnehmer |
| MOBILITY | FUEL | amenity=fuel/charging_station | Zapfsäule |
| INFRASTRUCTURE | PHARMACY | amenity=pharmacy | Apotheken-Kreuz |
| INFRASTRUCTURE | HOSPITAL | amenity=hospital/clinic | H-Symbol |
| INFRASTRUCTURE | TOILETS | amenity=toilets | WC-Piktogramm |
| INFRASTRUCTURE | DRINKING_WATER | amenity=drinking_water | Wassertropfen |
Alle nicht spezifisch gemappten POIs erhalten ihr Kategorie-Fallback-Icon (wie v2.5.44).
### Marker-Design
- **Hauptkategorie-Farbe** bleibt erhalten (Teal/Amber/Dunkelblau/Dunkelgrün).
- **Hintergrundform** bleibt per Kategorie (Stern/Kreis/Abger. Rechteck/Kreis mit Rand).
- **Inneres Piktogramm** variiert per Subtype (Canvas-Zeichnung, wie v2.5.43-Stil).
- **Sentinel-Pixel** berücksichtigt Kategorie + Subtype-Ordinal, verhindert MapLibre-Cache-Kollisionen.
### Detail-Sheet
Zeigt jetzt `poi.subtype.displayLabel` statt `poi.categoryLabel`, z. B. „Burg/Schloss" statt „Sehenswürdigkeit".
### Legende
- Kompakt-Ansicht: Farbpunkte wie bisher.
- Ausgeklappt: Hinweis **„Symbole zeigen Untertypen"** (neu in v2.5.45).
---
## 3. Explorer-UX-Aufräumungen
### Vorgenommen
| Bereich | Änderung |
|---|---|
| POI-Statusbar | Redesign von reinem Lade-Button zu kompakter Status+Reload-Bar (BottomCenter) |
| POI-Bar Text | Kontextabhängiger Statustext (Auto-Laden aktiv / Zoom-Hinweis / Fehlerzustand) |
| Layer-Sheet | Auto-Load-Toggle im POI-Abschnitt ergänzt; Beschreibungshinweis |
| Legende | Hinweis „Symbole zeigen Untertypen" im aufgeklappten Zustand |
| Detail-Sheet | Konkreterer Typ-Label (Subtype statt Kategorie) |
### Nicht verändert
- Kein Screen, Dialog oder Sheet entfernt.
- TopBar, FAB-Layout, Attribution, Kompass, Nordausrichtung unverändert.
- Routenplaner/Storyboard-UI nicht umgebaut (wie gefordert).
- Phantom-Track-Fixes, Waypoint-Logik, Follow-Location unverändert.
---
## 4. Was NICHT verändert wurde
| Bereich | Status |
|---|---|
| Storyboard/Routenplaner-Datenmodell | ❌ Nicht angefasst |
| Backup/Import/Export (ZIP, GPX) | ❌ Nicht angefasst |
| Playback/Multi-Clip/Player | ❌ Nicht angefasst |
| Lizenzsystem (ECDSA-Validierung, Storage) | ❌ Nicht angefasst |
| Waypoints/Touren/Tracks | ❌ Nicht angefasst |
| GPS-Aufzeichnung | ❌ Nicht angefasst |
| PoiCategory-Enum | ❌ Nicht angefasst (rückwärtskompatibel) |
| PoiLayerSettings-Persistenz | ❌ Nicht angefasst (keine neuen Keys) |
| MapStyleSettings | ❌ Nicht angefasst |
| applicationId | `de.waypointaudio` — unverändert |
| Keystore-Kompatibilität | Gleicher Keystore wie v2.5.44 |
| Canvas-Piktogramm-System (v2.5.43) | Erhalten und erweitert |
---
## 5. Geänderte Dateien
| Datei | Art der Änderung |
|---|---|
| `app/build.gradle.kts` | versionCode 98→99, versionName 2.5.44→2.5.45 |
| `util/ExplorerPoiClient.kt` | Neues `PoiSubtype`-Enum; `ExplorerPoiResult.subtype`; `resolveSubtype()`; `shouldAutoLoad()`-Funktion |
| `ui/ExplorerScreenMapLibre.kt` | Auto-Load-State + Debounce-Logik in cameraIdleListener; Subtype-Icons in Marker-Drawing; Detail-Sheet Subtype-Label; neue POI-Statusbar; MapLayerFab Auto-Load-Callbacks |
| `ui/MapLayerPanel.kt` | `poiAutoLoadEnabled`/`onPoiAutoLoadChange`-Parameter; Toggle-Row im POI-Abschnitt |
---
## 6. Smoke-Tests (manuell simuliert / Compile-Test)
- ✅ Build: `assembleRelease` erfolgreich (43 tasks executed)
- ✅ APK-Signing: v2 scheme verifiziert
- ✅ versionCode = 99, versionName = 2.5.45, applicationId = de.waypointaudio
- ✅ Keine Kompilier-Fehler (nur Pre-existing deprecation-Warnings aus älteren Dateien)
-`PoiSubtype` mit allen 25 Werten kompiliert
- ✅ Auto-Load-Logik: `shouldAutoLoad()` in `ExplorerPoiClient`, verwendet in `cameraIdleListener`
- ✅ Subtype-Icons: `buildExplorerPoiIcon(category, subtype)` mit When-Exhaustive-Abdeckung
- ⚠️ Duplicate-Branch-Warning in `ExplorerPoiClient.kt` (Z. 614: `"fort"` doppelt in `humanizeHistoric`) — funktional ohne Auswirkung, nur Compiler-Hinweis
---
## 7. Bekannte Grenzen
| Thema | Beschreibung |
|---|---|
| Kein APK-Größen-Split | Universal APK (57 MB), kein ABI-Split — wie gefordert |
| Auto-Load nur online | Kein Offline-Cache; bei Netzwerkfehler bleiben alte POIs erhalten |
| Subtype-Piktogramme | 25 Subtypes; sehr kleine Piktogramme (56×56 px) können auf kleinen Displays unter Details leiden — aber alle klar erkennbar |
| MapLibre Icon-Cache | Sentinel-Pixel-System verhindert Cache-Kollisionen; bei sehr vielen POIs kann es zu Marker-Redraws beim ersten Laden kommen |
| Auto-Load-Debouce | 1.5 s Debounce — bei sehr schnellem Scrolling löst erst das finale Idle aus |
| Storyboard/v2.6.0 | Kein Datenmodell-Umbau in v2.5.45. Vorarbeitung für v2.6.0: `PoiSubtype` als erweiterungsfähiges Enum vorhanden |
| Duplicate `"fort"` | In `humanizeHistoric()`: `"fort"` erscheint zweimal (Zeile 614 Warning). Zweiter Eintrag wird nie erreicht — keine funktionale Auswirkung |
---
## 8. Upgrade-Pfad
- Update von v2.5.44 auf v2.5.45 mit demselben Keystore (`GPS2Audio_release_keystore_v2.jks`) ist als In-Place-Update möglich.
- Alle gespeicherten Einstellungen (POI-Layer, Kartenstil, Follow-Location) bleiben erhalten.
- Keine Datenbank-Migration notwendig.