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
@@ -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)
)
}
}