Release GPS2Audio v2.5.45 Explorer POI autoload
This commit is contained in:
@@ -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.
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user