Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2eeb086481 | |||
| 990b66e24f | |||
| 5cfc5aee1d |
@@ -157,8 +157,8 @@ android {
|
|||||||
// • Keine Regressionen: Phantom-Layer-Fix, Lizenzsystem, Multi-Clip,
|
// • Keine Regressionen: Phantom-Layer-Fix, Lizenzsystem, Multi-Clip,
|
||||||
// PTT, Tour-Zähler, Backup/Cover, GPX-Import, Explorer/Routing,
|
// PTT, Tour-Zähler, Backup/Cover, GPX-Import, Explorer/Routing,
|
||||||
// Tour-/Waypoint-Kopien, Hintergrundwiedergabe.
|
// Tour-/Waypoint-Kopien, Hintergrundwiedergabe.
|
||||||
versionCode = 98
|
versionCode = 102
|
||||||
versionName = "2.5.44"
|
versionName = "2.5.48"
|
||||||
}
|
}
|
||||||
|
|
||||||
val storeFilePath = resolveSigningValue("storeFile", "GPS2AUDIO_RELEASE_STORE_FILE")
|
val storeFilePath = resolveSigningValue("storeFile", "GPS2AUDIO_RELEASE_STORE_FILE")
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ enum class MapBaseStyle(val key: String) {
|
|||||||
PERSPECTIVE("perspective");
|
PERSPECTIVE("perspective");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val DEFAULT = MODERN_2D
|
val DEFAULT = CITY_3D
|
||||||
fun fromKey(key: String?): MapBaseStyle {
|
fun fromKey(key: String?): MapBaseStyle {
|
||||||
return when (key) {
|
return when (key) {
|
||||||
"modern_2d" -> MODERN_2D
|
"modern_2d" -> MODERN_2D
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import de.waypointaudio.data.TourAudioSettings
|
|||||||
import de.waypointaudio.data.TourCover
|
import de.waypointaudio.data.TourCover
|
||||||
import de.waypointaudio.data.TourPlaybackDefaults
|
import de.waypointaudio.data.TourPlaybackDefaults
|
||||||
import de.waypointaudio.data.TourRoute
|
import de.waypointaudio.data.TourRoute
|
||||||
|
import de.waypointaudio.data.TrackDraft
|
||||||
import de.waypointaudio.data.Waypoint
|
import de.waypointaudio.data.Waypoint
|
||||||
import de.waypointaudio.data.effectiveClips
|
import de.waypointaudio.data.effectiveClips
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
@@ -68,6 +69,10 @@ object BackupExportManager {
|
|||||||
// v2.5.5 — Tour-Cover (Icon + Palette) sind ein reines UI-Detail pro Tour
|
// v2.5.5 — Tour-Cover (Icon + Palette) sind ein reines UI-Detail pro Tour
|
||||||
// und wandern mit dem Tour-Export mit.
|
// und wandern mit dem Tour-Export mit.
|
||||||
val tourCovers: Map<String, TourCover> = emptyMap(),
|
val tourCovers: Map<String, TourCover> = emptyMap(),
|
||||||
|
// v2.5.48 — Echte TrackDraft-Objekte (Name, Notiz, Markierungen, Punkte).
|
||||||
|
// gpsTracks bleibt für die Live-Aufzeichnung weiterhin im Backup-Input,
|
||||||
|
// wird aber NICHT mehr in track_drafts.json geschrieben.
|
||||||
|
val trackDrafts: List<TrackDraft> = emptyList(),
|
||||||
val appVersionName: String = "",
|
val appVersionName: String = "",
|
||||||
val appVersionCode: Int = 0
|
val appVersionCode: Int = 0
|
||||||
)
|
)
|
||||||
@@ -197,13 +202,17 @@ object BackupExportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----- data/track_drafts.json -----
|
// ----- data/track_drafts.json -----
|
||||||
|
// v2.5.48 FIX: Exportiert die echten TrackDraft-Objekte (Name, Notiz,
|
||||||
|
// Markierungen, Punkte) statt der gpsTracks-Map. Altes Format
|
||||||
|
// ("tracks": { tourName: [GpsTrackPoint] }) wird beim Import
|
||||||
|
// weiterhin toleriert (Abwärtskompatibilität).
|
||||||
if (selection.trackDrafts) {
|
if (selection.trackDrafts) {
|
||||||
val trackPayload = mapOf(
|
val trackPayload = mapOf(
|
||||||
"tracks" to input.gpsTracks
|
"drafts" to input.trackDrafts
|
||||||
)
|
)
|
||||||
writeJsonEntry(zip, "data/track_drafts.json", trackPayload)
|
writeJsonEntry(zip, "data/track_drafts.json", trackPayload)
|
||||||
val pointCount = input.gpsTracks.values.sumOf { it.size }
|
val pointCount = input.trackDrafts.sumOf { it.points.size }
|
||||||
sectionsWritten.add("Track-Entwürfe (${input.gpsTracks.size} Touren, $pointCount Punkte)")
|
sectionsWritten.add("Track-Entwürfe (${input.trackDrafts.size} Entwürfe, $pointCount Punkte)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- data/music_settings.json + media/... -----
|
// ----- data/music_settings.json + media/... -----
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import de.waypointaudio.data.TourAudioSettings
|
|||||||
import de.waypointaudio.data.TourPlaybackDefaults
|
import de.waypointaudio.data.TourPlaybackDefaults
|
||||||
import de.waypointaudio.data.TourCover
|
import de.waypointaudio.data.TourCover
|
||||||
import de.waypointaudio.data.TourRoute
|
import de.waypointaudio.data.TourRoute
|
||||||
|
import de.waypointaudio.data.TrackDraft
|
||||||
import de.waypointaudio.data.Waypoint
|
import de.waypointaudio.data.Waypoint
|
||||||
import de.waypointaudio.data.WaypointMusicBehavior
|
import de.waypointaudio.data.WaypointMusicBehavior
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -102,6 +103,8 @@ object BackupImportManager {
|
|||||||
val tourRoutesToWrite: Map<String, TourRoute> = emptyMap(),
|
val tourRoutesToWrite: Map<String, TourRoute> = emptyMap(),
|
||||||
// v2.5.5 — Tour-Cover, die geschrieben werden sollen (Map endgültiger Tourname → Cover).
|
// v2.5.5 — Tour-Cover, die geschrieben werden sollen (Map endgültiger Tourname → Cover).
|
||||||
val tourCoversToWrite: Map<String, TourCover> = emptyMap(),
|
val tourCoversToWrite: Map<String, TourCover> = emptyMap(),
|
||||||
|
// v2.5.48 — Echte TrackDraft-Objekte, die als neue Drafts hinzugefügt werden sollen.
|
||||||
|
val trackDraftsToAdd: List<TrackDraft> = emptyList(),
|
||||||
val trackMinDistanceMeters: Int?,
|
val trackMinDistanceMeters: Int?,
|
||||||
val newTourSelection: String?,
|
val newTourSelection: String?,
|
||||||
val warnings: List<String>,
|
val warnings: List<String>,
|
||||||
@@ -514,42 +517,71 @@ object BackupImportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Track-Entwürfe ----
|
// ---- Track-Entwürfe ----
|
||||||
|
// v2.5.48 FIX: Importiert echte TrackDraft-Objekte aus dem "drafts"-Array.
|
||||||
|
// Abwärtskompatibilität: Alte Backups schrieben fälschlicherweise die
|
||||||
|
// gpsTracks-Map unter "tracks" in track_drafts.json. Falls "drafts" fehlt
|
||||||
|
// aber "tracks" vorhanden ist, wird das alte Format erkannt und toleriert
|
||||||
|
// (kein Crash, keine Datenverluste an bestehenden Touren/Waypoints).
|
||||||
val tracksToWrite = mutableMapOf<String, List<GpsTrackPoint>>()
|
val tracksToWrite = mutableMapOf<String, List<GpsTrackPoint>>()
|
||||||
|
val trackDraftsToAdd = mutableListOf<TrackDraft>()
|
||||||
var tracksImported = 0
|
var tracksImported = 0
|
||||||
if (selection.trackDrafts && draftsJson != null) {
|
if (selection.trackDrafts && draftsJson != null) {
|
||||||
val obj = runCatching {
|
val obj = runCatching {
|
||||||
JsonParser.parseString(draftsJson).asJsonObject
|
JsonParser.parseString(draftsJson).asJsonObject
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
val map: Map<String, List<GpsTrackPoint>> = if (obj != null) {
|
|
||||||
runCatching {
|
|
||||||
gson.fromJson<Map<String, List<GpsTrackPoint>>>(
|
|
||||||
obj.get("tracks"),
|
|
||||||
object : TypeToken<Map<String, List<GpsTrackPoint>>>() {}.type
|
|
||||||
) ?: emptyMap()
|
|
||||||
}.getOrDefault(emptyMap())
|
|
||||||
} else emptyMap()
|
|
||||||
|
|
||||||
map.forEach { (origTour, points) ->
|
if (obj != null) {
|
||||||
|
// Neues Format (v2.5.48+): "drafts" enthält eine Liste echter TrackDraft-Objekte.
|
||||||
|
val hasDraftsKey = obj.has("drafts") && !obj.get("drafts").isJsonNull
|
||||||
|
|
||||||
|
if (hasDraftsKey) {
|
||||||
|
// Neues Format: Liste von TrackDraft-Objekten
|
||||||
|
val importedDrafts: List<TrackDraft> = runCatching {
|
||||||
|
gson.fromJson<List<TrackDraft>>(
|
||||||
|
obj.get("drafts"),
|
||||||
|
object : TypeToken<List<TrackDraft>>() {}.type
|
||||||
|
) ?: emptyList()
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
|
||||||
|
importedDrafts.forEach { draft ->
|
||||||
|
// Tourname-Mapping anwenden (für den Fall, dass Tour umbenannt wurde).
|
||||||
|
val origTour = draft.sourceTourName
|
||||||
val mappedTour = tourRename[origTour]
|
val mappedTour = tourRename[origTour]
|
||||||
?: run {
|
?: run {
|
||||||
// Tour war nicht im Tour-Import (z. B. wenn nur Drafts importiert werden):
|
if (origTour == Waypoint.DEFAULT_TOUR_NAME ||
|
||||||
// prüfen, ob Name kollidiert.
|
origTour in resultTours
|
||||||
if (origTour == Waypoint.DEFAULT_TOUR_NAME) {
|
) {
|
||||||
origTour
|
|
||||||
} else if (origTour in resultTours) {
|
|
||||||
origTour
|
origTour
|
||||||
} else {
|
} else {
|
||||||
resultTours.add(origTour)
|
resultTours.add(origTour)
|
||||||
origTour
|
origTour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val existingPoints = existingTracks[mappedTour] ?: emptyList()
|
// Neue UUID vergeben, damit importierte Drafts nie mit
|
||||||
if (existingPoints.isEmpty()) {
|
// vorhandenen Drafts-IDs kollidieren.
|
||||||
// Bestehenden Track NICHT überschreiben.
|
val newDraft = draft.copy(
|
||||||
tracksToWrite[mappedTour] = points
|
id = java.util.UUID.randomUUID().toString(),
|
||||||
|
sourceTourName = mappedTour,
|
||||||
|
updatedAt = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
trackDraftsToAdd.add(newDraft)
|
||||||
tracksImported++
|
tracksImported++
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
warnings.add("Track-Entwurf für Tour „$mappedTour\" bereits vorhanden – nicht überschrieben.")
|
// Altes (fehlerhaftes) Format: "tracks" enthält gpsTracks-Map.
|
||||||
|
// Tolerieren ohne Crash, aber nicht in Tracks oder Drafts importieren,
|
||||||
|
// da der Inhalt GPS-Track-Daten (nicht Draft-Metadaten) sind.
|
||||||
|
// Hinweis in Warnings ausgeben.
|
||||||
|
val hasTracksKey = obj.has("tracks") && !obj.get("tracks").isJsonNull
|
||||||
|
if (hasTracksKey) {
|
||||||
|
warnings.add(
|
||||||
|
"Hinweis: track_drafts.json aus älterem Backup-Format erkannt " +
|
||||||
|
"(enthält GPS-Tracks statt Draft-Objekte). " +
|
||||||
|
"TrackDraft-Namen/Notizen können nicht wiederhergestellt werden. " +
|
||||||
|
"Bestehende Touren und Wegpunkte bleiben unverändert."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Keine Daten importieren aus altem, fehlerh. Format — kein Crash.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -636,6 +668,8 @@ object BackupImportManager {
|
|||||||
musicToWrite = musicToWrite,
|
musicToWrite = musicToWrite,
|
||||||
tourRoutesToWrite = tourRoutesToWrite,
|
tourRoutesToWrite = tourRoutesToWrite,
|
||||||
tourCoversToWrite = tourCoversToWrite,
|
tourCoversToWrite = tourCoversToWrite,
|
||||||
|
// v2.5.48 — Echte TrackDraft-Objekte weitergeben.
|
||||||
|
trackDraftsToAdd = trackDraftsToAdd,
|
||||||
trackMinDistanceMeters = trackMinDistanceMeters,
|
trackMinDistanceMeters = trackMinDistanceMeters,
|
||||||
newTourSelection = newTourSelection,
|
newTourSelection = newTourSelection,
|
||||||
warnings = warnings,
|
warnings = warnings,
|
||||||
|
|||||||
@@ -121,8 +121,11 @@ import de.waypointaudio.util.LatLngBbox
|
|||||||
import de.waypointaudio.util.WaypointAudioDuration
|
import de.waypointaudio.util.WaypointAudioDuration
|
||||||
import de.waypointaudio.viewmodel.WaypointViewModel
|
import de.waypointaudio.viewmodel.WaypointViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import de.waypointaudio.util.PoiSubtype
|
||||||
import org.maplibre.android.annotations.IconFactory
|
import org.maplibre.android.annotations.IconFactory
|
||||||
import org.maplibre.android.annotations.Marker
|
import org.maplibre.android.annotations.Marker
|
||||||
import org.maplibre.android.annotations.MarkerOptions
|
import org.maplibre.android.annotations.MarkerOptions
|
||||||
@@ -324,6 +327,15 @@ fun ExplorerScreenMapLibre(
|
|||||||
var currentMapZoom by remember { mutableStateOf(13.0) }
|
var currentMapZoom by remember { mutableStateOf(13.0) }
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
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 lastLat by remember { mutableStateOf<Double?>(null) }
|
||||||
var lastLng by remember { mutableStateOf<Double?>(null) }
|
var lastLng by remember { mutableStateOf<Double?>(null) }
|
||||||
var lastAccuracyM by remember { mutableStateOf<Float?>(null) }
|
var lastAccuracyM by remember { mutableStateOf<Float?>(null) }
|
||||||
@@ -455,10 +467,14 @@ fun ExplorerScreenMapLibre(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// v2.5.38 — POI-Layer-Auswahl persistent speichern.
|
// 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) {
|
LaunchedEffect(layerState.poiLayers) {
|
||||||
if (styleHydrated && layerState.poiLayers != mapStyleSettings.poiLayers) {
|
if (styleHydrated && layerState.poiLayers != mapStyleSettings.poiLayers) {
|
||||||
runCatching { mapStyleStore.setPoiLayers(layerState.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.
|
// v2.5.38 — POI-Sichtbarkeit auf den geladenen MapLibre-Style anwenden.
|
||||||
// Greift auf heuristische Layer-ID-Präfixe des OpenMapTiles-Schemas zurück.
|
// Greift auf heuristische Layer-ID-Präfixe des OpenMapTiles-Schemas zurück.
|
||||||
@@ -487,33 +503,92 @@ fun ExplorerScreenMapLibre(
|
|||||||
// Schlüssel inkl. showPoiNames: Marker werden bei Toggle-Änderung neu gezeichnet.
|
// Schlüssel inkl. showPoiNames: Marker werden bei Toggle-Änderung neu gezeichnet.
|
||||||
// Fix: Kategorie-Bitmap wird per buildExplorerPoiIcon(cat, showPoiNames, name) mit
|
// Fix: Kategorie-Bitmap wird per buildExplorerPoiIcon(cat, showPoiNames, name) mit
|
||||||
// eindeutiger Kategorie-Kennung erzeugt — verhindert Icon-Cache-Kollisionen in MapLibre.
|
// eindeutiger Kategorie-Kennung erzeugt — verhindert Icon-Cache-Kollisionen in MapLibre.
|
||||||
|
// v2.5.46 — Atomare POI-Marker-Aktualisierung: erst neue Marker hinzufügen,
|
||||||
|
// dann alte entfernen. So gibt es keine sichtbare Lücke (kein Flackern) beim
|
||||||
|
// Kartennachladen. Außerdem: maximale Anzahl angezeigter Marker auf 280 begrenzt,
|
||||||
|
// benannte und wichtige Subtypen werden priorisiert.
|
||||||
LaunchedEffect(explorerPoiResults, styleReady, showPoiNames) {
|
LaunchedEffect(explorerPoiResults, styleReady, showPoiNames) {
|
||||||
if (!styleReady) return@LaunchedEffect
|
if (!styleReady) return@LaunchedEffect
|
||||||
val ml = mapLibreMap ?: return@LaunchedEffect
|
val ml = mapLibreMap ?: return@LaunchedEffect
|
||||||
// Alte Explorer-POI-Marker entfernen
|
if (explorerPoiResults.isEmpty()) {
|
||||||
|
// Nur leeren wenn wirklich leer (z.B. manueller Clear)
|
||||||
explorerPoiMarkers.forEach { runCatching { ml.removeMarker(it) } }
|
explorerPoiMarkers.forEach { runCatching { ml.removeMarker(it) } }
|
||||||
explorerPoiMarkers.clear()
|
explorerPoiMarkers.clear()
|
||||||
explorerPoiMarkerIdToResult.clear()
|
explorerPoiMarkerIdToResult.clear()
|
||||||
if (explorerPoiResults.isEmpty()) return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
val iconFactory = IconFactory.getInstance(context)
|
val iconFactory = IconFactory.getInstance(context)
|
||||||
for ((cat, pois) in explorerPoiResults) {
|
|
||||||
for (poi in pois) {
|
// v2.5.46 — POI-Dichte-Begrenzung: max. 280 Marker pro Viewport,
|
||||||
|
// priorisiere benannte POIs und wichtige Subtypen.
|
||||||
|
val MAX_VISIBLE_POIS = 280
|
||||||
|
val importantSubtypes = setOf(
|
||||||
|
de.waypointaudio.util.PoiSubtype.CASTLE,
|
||||||
|
de.waypointaudio.util.PoiSubtype.CHURCH,
|
||||||
|
de.waypointaudio.util.PoiSubtype.MUSEUM,
|
||||||
|
de.waypointaudio.util.PoiSubtype.VIEWPOINT,
|
||||||
|
de.waypointaudio.util.PoiSubtype.MONUMENT,
|
||||||
|
de.waypointaudio.util.PoiSubtype.TRAIN_STATION,
|
||||||
|
de.waypointaudio.util.PoiSubtype.HOSPITAL,
|
||||||
|
de.waypointaudio.util.PoiSubtype.PHARMACY,
|
||||||
|
)
|
||||||
|
// Alle POIs flach aufsammeln, sortieren (benannte + wichtige zuerst)
|
||||||
|
val allPois: List<Pair<de.waypointaudio.data.PoiCategory, de.waypointaudio.util.ExplorerPoiResult>> =
|
||||||
|
explorerPoiResults.flatMap { (cat, pois) -> pois.map { cat to it } }
|
||||||
|
val totalCount = allPois.size
|
||||||
|
val sortedPois = allPois.sortedWith(
|
||||||
|
compareByDescending<Pair<de.waypointaudio.data.PoiCategory, de.waypointaudio.util.ExplorerPoiResult>> {
|
||||||
|
val poi = it.second
|
||||||
|
// Priorität: benannter POI + wichtiger Subtype = höchste Priorität
|
||||||
|
val isNamed = poi.displayName.isNotBlank() && poi.displayName != poi.subtype.displayLabel
|
||||||
|
val isImportant = poi.subtype in importantSubtypes
|
||||||
|
when {
|
||||||
|
isNamed && isImportant -> 3
|
||||||
|
isNamed -> 2
|
||||||
|
isImportant -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val visiblePois = if (totalCount > MAX_VISIBLE_POIS) sortedPois.take(MAX_VISIBLE_POIS) else sortedPois
|
||||||
|
val shownCount = visiblePois.size
|
||||||
|
|
||||||
|
// v2.5.46 — Status-Text (zeigt "N von M" wenn begrenzt)
|
||||||
|
if (totalCount > MAX_VISIBLE_POIS) {
|
||||||
|
autoPoiStatus = "POIs: $shownCount von $totalCount angezeigt"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Marker aufbauen (noch nicht auf der Karte)
|
||||||
|
val newMarkers = mutableListOf<org.maplibre.android.annotations.Marker>()
|
||||||
|
val newMarkerIdToResult = mutableMapOf<Long, de.waypointaudio.util.ExplorerPoiResult>()
|
||||||
|
|
||||||
|
for ((cat, poi) in visiblePois) {
|
||||||
|
// v2.5.45 — Subtype-Icon: Piktogramm variiert nach Untertyp, Farbe bleibt Kategorie
|
||||||
val iconBitmap = if (showPoiNames) {
|
val iconBitmap = if (showPoiNames) {
|
||||||
buildExplorerPoiIconWithLabel(cat, poi.displayName)
|
buildExplorerPoiIconWithLabel(cat, poi.subtype, poi.displayName)
|
||||||
} else {
|
} else {
|
||||||
buildExplorerPoiIcon(cat)
|
buildExplorerPoiIcon(cat, poi.subtype)
|
||||||
}
|
}
|
||||||
val icon = iconFactory.fromBitmap(iconBitmap)
|
val icon = iconFactory.fromBitmap(iconBitmap)
|
||||||
val mo = MarkerOptions()
|
val mo = MarkerOptions()
|
||||||
.position(LatLng(poi.latitude, poi.longitude))
|
.position(LatLng(poi.latitude, poi.longitude))
|
||||||
.title(poi.displayName)
|
.title(poi.displayName)
|
||||||
.snippet(poi.categoryLabel)
|
.snippet(poi.subtype.displayLabel) // v2.5.45: Subtype-Label statt categoryLabel
|
||||||
.icon(icon)
|
.icon(icon)
|
||||||
val marker = runCatching { ml.addMarker(mo) }.getOrNull() ?: continue
|
val marker = runCatching { ml.addMarker(mo) }.getOrNull() ?: continue
|
||||||
explorerPoiMarkers.add(marker)
|
newMarkers.add(marker)
|
||||||
explorerPoiMarkerIdToResult[marker.id] = poi
|
newMarkerIdToResult[marker.id] = poi
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v2.5.46 — ERST nach Hinzufügen der neuen Marker die alten entfernen
|
||||||
|
// → atomare Aktualisierung ohne sichtbare Lücke / Flackern
|
||||||
|
explorerPoiMarkers.forEach { runCatching { ml.removeMarker(it) } }
|
||||||
|
explorerPoiMarkers.clear()
|
||||||
|
explorerPoiMarkerIdToResult.clear()
|
||||||
|
|
||||||
|
explorerPoiMarkers.addAll(newMarkers)
|
||||||
|
explorerPoiMarkerIdToResult.putAll(newMarkerIdToResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
var showSearchDialog by remember { mutableStateOf(false) }
|
var showSearchDialog by remember { mutableStateOf(false) }
|
||||||
@@ -2131,8 +2206,65 @@ fun ExplorerScreenMapLibre(
|
|||||||
override fun onMoveEnd(detector: org.maplibre.android.gestures.MoveGestureDetector) {}
|
override fun onMoveEnd(detector: org.maplibre.android.gestures.MoveGestureDetector) {}
|
||||||
})
|
})
|
||||||
// v2.5.41 — Zoom-Tracking für zoom-abhängige POI-Label-Anzeige
|
// 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 {
|
ml.addOnCameraIdleListener {
|
||||||
currentMapZoom = ml.cameraPosition.zoom
|
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
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// v2.5.46: Debounce erhöht auf 2.5 s (vorher 1.5 s)
|
||||||
|
poiAutoLoadJob?.cancel()
|
||||||
|
poiAutoLoadJob = scope.launch {
|
||||||
|
delay(2500L)
|
||||||
|
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 "POIs: $total"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Alten Bestand behalten, nur dezente Fehlermeldung
|
||||||
|
autoPoiStatus = null
|
||||||
|
explorerPoiSnackbar = "POI-Aktualisierung fehlgeschlagen."
|
||||||
|
} finally {
|
||||||
|
explorerPoiLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mv.onStart()
|
mv.onStart()
|
||||||
@@ -2274,6 +2406,12 @@ fun ExplorerScreenMapLibre(
|
|||||||
},
|
},
|
||||||
onShowRoutingInfo = { showRoutingInfo = true },
|
onShowRoutingInfo = { showRoutingInfo = true },
|
||||||
onShowRoutePlanner = { showRoutePlanner = true },
|
onShowRoutePlanner = { showRoutePlanner = true },
|
||||||
|
// v2.5.45 — Auto-Load-Toggle
|
||||||
|
poiAutoLoadEnabled = autoPoiLoadEnabled,
|
||||||
|
onPoiAutoLoadChange = { enabled ->
|
||||||
|
autoPoiLoadEnabled = enabled
|
||||||
|
if (!enabled) poiAutoLoadJob?.cancel()
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(top = 8.dp, end = 8.dp)
|
.padding(top = 8.dp, end = 8.dp)
|
||||||
@@ -2320,42 +2458,73 @@ fun ExplorerScreenMapLibre(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// v2.5.40 — Explorer-POI-Lade-Button (mittig unten, über Attribution)
|
// v2.5.45 — Explorer-POI-Bar (Auto-Load-Status + manueller Fallback)
|
||||||
if (layerState.poiLayers.let {
|
// 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
|
it.showSights || it.showGastronomy || it.showMobility || it.showInfrastructure
|
||||||
}) {
|
}
|
||||||
|
if (anyPoiLayerActiveForBar) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.95f),
|
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.92f),
|
||||||
shadowElevation = 3.dp,
|
shadowElevation = 3.dp,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.padding(bottom = 32.dp)
|
.padding(bottom = 32.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
|
// Status-Icon/Spinner
|
||||||
if (explorerPoiLoading) {
|
if (explorerPoiLoading) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(13.dp),
|
||||||
strokeWidth = 2.dp,
|
strokeWidth = 1.8.dp,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Explore,
|
if (autoPoiLoadEnabled) Icons.Filled.Explore else Icons.Filled.Explore,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(13.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
tint = MaterialTheme.colorScheme.onSecondaryContainer.copy(
|
||||||
|
alpha = if (autoPoiLoadEnabled) 1f else 0.55f
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// v2.5.46 — Status-Text: nur bei echtem Laden "wird geladen",
|
||||||
|
// sonst ruhiger Status-Text
|
||||||
|
val statusText = when {
|
||||||
|
explorerPoiLoading -> "POIs laden…"
|
||||||
|
autoPoiStatus != null -> autoPoiStatus!!
|
||||||
|
autoPoiLoadEnabled && currentMapZoom < de.waypointaudio.util.ExplorerPoiClient.AUTO_LOAD_MIN_ZOOM
|
||||||
|
-> "Hereinzoomen für Auto-Load"
|
||||||
|
autoPoiLoadEnabled -> "Auto-Laden aktiv"
|
||||||
|
else -> "POIs: manuell"
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = if (explorerPoiLoading) "POIs werden geladen …"
|
text = statusText,
|
||||||
else "POIs im Kartenbereich laden",
|
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,
|
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) {
|
modifier = Modifier.clickable(enabled = !explorerPoiLoading) {
|
||||||
val ml = mapLibreMap ?: return@clickable
|
val ml = mapLibreMap ?: return@clickable
|
||||||
val bounds = ml.projection.visibleRegion.latLngBounds
|
val bounds = ml.projection.visibleRegion.latLngBounds
|
||||||
@@ -2371,7 +2540,11 @@ fun ExplorerScreenMapLibre(
|
|||||||
if (layerState.poiLayers.showMobility) add(PoiCategory.MOBILITY)
|
if (layerState.poiLayers.showMobility) add(PoiCategory.MOBILITY)
|
||||||
if (layerState.poiLayers.showInfrastructure) add(PoiCategory.INFRASTRUCTURE)
|
if (layerState.poiLayers.showInfrastructure) add(PoiCategory.INFRASTRUCTURE)
|
||||||
}
|
}
|
||||||
|
// Manueller Reload: Cache leeren, Cursor zurücksetzen
|
||||||
|
de.waypointaudio.util.ExplorerPoiClient.clearCache()
|
||||||
|
lastAutoLoadCats = null
|
||||||
explorerPoiLoading = true
|
explorerPoiLoading = true
|
||||||
|
autoPoiStatus = null
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val results = ExplorerPoiClient.loadPois(
|
val results = ExplorerPoiClient.loadPois(
|
||||||
@@ -2379,17 +2552,21 @@ fun ExplorerScreenMapLibre(
|
|||||||
bbox = bbox,
|
bbox = bbox,
|
||||||
categories = cats,
|
categories = cats,
|
||||||
onRadiusClamped = {
|
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 }
|
val total = results.values.sumOf { it.size }
|
||||||
explorerPoiResults = results
|
explorerPoiResults = results
|
||||||
// v2.5.44 — POI-Zusammenfassung im Snackbar
|
val cp = ml.cameraPosition
|
||||||
explorerPoiSnackbar = if (total == 0) {
|
lastAutoLoadLat = cp.target?.latitude
|
||||||
"Keine POIs in diesem Bereich gefunden."
|
lastAutoLoadLon = cp.target?.longitude
|
||||||
} else {
|
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)
|
ExplorerPoiClient.summarize(results)
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
explorerPoiSnackbar = "POI-Laden fehlgeschlagen: ${e.message?.take(60) ?: "Netzwerkfehler"}"
|
explorerPoiSnackbar = "POI-Laden fehlgeschlagen: ${e.message?.take(60) ?: "Netzwerkfehler"}"
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2446,12 +2623,13 @@ fun ExplorerScreenMapLibre(
|
|||||||
else "%.1f km entfernt".format(d / 1000.0)
|
else "%.1f km entfernt".format(d / 1000.0)
|
||||||
} else null
|
} else null
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
// v2.5.45 — Subtype-Label (konkreter) statt nur Kategorie
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer
|
color = MaterialTheme.colorScheme.secondaryContainer
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
poi.categoryLabel,
|
poi.subtype.displayLabel, // z.B. "Burg/Schloss" statt "Sehenswürdigkeit"
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp)
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp)
|
||||||
@@ -3506,9 +3684,22 @@ fun MapCompactAttributionBadge(modifier: Modifier = Modifier) {
|
|||||||
* MOBILITY — Indigo (#3F51B5) + Abgerundetes Quadrat + Bus-Symbol (B)
|
* MOBILITY — Indigo (#3F51B5) + Abgerundetes Quadrat + Bus-Symbol (B)
|
||||||
* INFRASTRUCTURE — Koralle (#E53935) + Kreis + Plus-Symbol (+)
|
* 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.43 — Echte Canvas-Piktogramme statt Buchstaben/Zeichen
|
||||||
val sizePx = 56
|
// v2.5.45 — Subtype-Piktogramme für Sights/Gastro/Mobil/Infra
|
||||||
|
// v2.5.46 — Größe von 56 auf 68 px erhöht (bessere Lesbarkeit auf Smartphone-Screens)
|
||||||
|
val sizePx = 68
|
||||||
|
|
||||||
// Kategorie-Farben — dezent, App-Teal-Stil passend
|
// Kategorie-Farben — dezent, App-Teal-Stil passend
|
||||||
val fillColor = when (category) {
|
val fillColor = when (category) {
|
||||||
@@ -3518,13 +3709,15 @@ private fun buildExplorerPoiIcon(category: de.waypointaudio.data.PoiCategory): B
|
|||||||
de.waypointaudio.data.PoiCategory.INFRASTRUCTURE-> 0xFF558B2F.toInt() // Dunkelgrün (Infrastruktur/Info)
|
de.waypointaudio.data.PoiCategory.INFRASTRUCTURE-> 0xFF558B2F.toInt() // Dunkelgrün (Infrastruktur/Info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sentinel-Byte-Wert je Kategorie (eindeutige Pixel-Markierung für MapLibre-Cache)
|
// v2.5.45 — Sentinel-Wert berücksichtigt Kategorie + Subtype-Ordinal,
|
||||||
val sentinelAlpha = when (category) {
|
// um MapLibre-Icon-Cache-Kollisionen zwischen verschiedenen Subtypen derselben Kategorie zu verhindern.
|
||||||
de.waypointaudio.data.PoiCategory.SIGHTS -> 0xFC
|
val categoryBase = when (category) {
|
||||||
de.waypointaudio.data.PoiCategory.GASTRONOMY -> 0xFD
|
de.waypointaudio.data.PoiCategory.SIGHTS -> 0
|
||||||
de.waypointaudio.data.PoiCategory.MOBILITY -> 0xFE
|
de.waypointaudio.data.PoiCategory.GASTRONOMY -> 32
|
||||||
de.waypointaudio.data.PoiCategory.INFRASTRUCTURE-> 0xFF
|
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 bmp = android.graphics.Bitmap.createBitmap(sizePx, sizePx, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
val canvas = android.graphics.Canvas(bmp)
|
val canvas = android.graphics.Canvas(bmp)
|
||||||
@@ -3605,101 +3798,270 @@ private fun buildExplorerPoiIcon(category: de.waypointaudio.data.PoiCategory): B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Weißes Piktogramm per Canvas ─────────────────────────────────────────
|
|
||||||
|
// ── Weißes Piktogramm per Canvas (v2.5.45: Subtype-Varianten) ──────────────────
|
||||||
|
// v2.5.46: Strichstärke auf 2.8 erhöht (Icon-Größe 68 px statt 56 px)
|
||||||
paint.color = 0xFFFFFFFF.toInt()
|
paint.color = 0xFFFFFFFF.toInt()
|
||||||
paint.style = android.graphics.Paint.Style.FILL
|
paint.style = android.graphics.Paint.Style.FILL
|
||||||
paint.strokeWidth = 2.2f
|
paint.strokeWidth = 2.8f
|
||||||
|
|
||||||
when (category) {
|
when (subtype) {
|
||||||
de.waypointaudio.data.PoiCategory.SIGHTS -> {
|
// ─── SIGHTS Subtypes ────────────────────────────────────────────────────────
|
||||||
// Stern-Marker: Stern-Hintergrund ist schon der Rahmen.
|
de.waypointaudio.util.PoiSubtype.CASTLE -> {
|
||||||
// Piktogramm: kleiner innerer Punkt + Strahllinien wie eine Landmarke/Monument-Nadel
|
// Turm/Burg: Zinnenkranz + Tor
|
||||||
// Vertikaler Stab (Monument-Pin)
|
paint.style = android.graphics.Paint.Style.FILL
|
||||||
val stW = 3.5f
|
val tw = 8f; val th = 14f
|
||||||
val stH = 14f
|
val tl = cx - tw/2f; val tt = cy - th/2f - 1f
|
||||||
val stTop = cy - 10f
|
canvas.drawRect(tl, tt + 4f, tl + tw, tt + th, paint)
|
||||||
val stBot = cy + 4f
|
for (zi in 0..2) {
|
||||||
val stLeft = cx - stW / 2f
|
val zx = tl + zi * (tw/2f) - 0.5f
|
||||||
canvas.drawRect(stLeft, stTop, stLeft + stW, stBot, paint)
|
canvas.drawRect(zx, tt, zx + 2.5f, tt + 4f, paint)
|
||||||
// Kleines Dreieck-Dach oben (Turm/Monument)
|
}
|
||||||
|
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 {
|
val roofPath = android.graphics.Path().apply {
|
||||||
moveTo(cx, stTop - 5f)
|
moveTo(cx, stTop - 5f); lineTo(cx - 5f, stTop); lineTo(cx + 5f, stTop); close()
|
||||||
lineTo(cx - 5f, stTop)
|
|
||||||
lineTo(cx + 5f, stTop)
|
|
||||||
close()
|
|
||||||
}
|
}
|
||||||
canvas.drawPath(roofPath, paint)
|
canvas.drawPath(roofPath, paint)
|
||||||
// Sockel/Basis unten
|
|
||||||
canvas.drawRect(cx - 6f, stBot, cx + 6f, stBot + 2.5f, paint)
|
canvas.drawRect(cx - 6f, stBot, cx + 6f, stBot + 2.5f, paint)
|
||||||
}
|
}
|
||||||
de.waypointaudio.data.PoiCategory.GASTRONOMY -> {
|
// ─── GASTRONOMY Subtypes ────────────────────────────────────────────────────
|
||||||
// Tasse mit Henkel und Dampf-Linie
|
de.waypointaudio.util.PoiSubtype.RESTAURANT -> {
|
||||||
val cupLeft = cx - 9f
|
// Messer + Gabel
|
||||||
val cupRight = cx + 9f
|
paint.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.0f
|
||||||
val cupTop = cy - 3f
|
canvas.drawLine(cx - 5f, cy + 9f, cx - 5f, cy - 10f, paint)
|
||||||
val cupBot = cy + 9f
|
paint.strokeWidth = 1.5f
|
||||||
// Tassenform: Trapez (breiter unten)
|
canvas.drawLine(cx - 7f, cy - 10f, cx - 7f, cy - 4f, paint)
|
||||||
val cupPath = android.graphics.Path().apply {
|
canvas.drawLine(cx - 3f, cy - 10f, cx - 3f, cy - 4f, paint)
|
||||||
moveTo(cupLeft + 2f, cupTop)
|
paint.strokeWidth = 2.0f
|
||||||
lineTo(cupLeft, cupBot)
|
canvas.drawLine(cx + 5f, cy + 9f, cx + 5f, cy - 4f, paint)
|
||||||
lineTo(cupRight, cupBot)
|
val knifePath = android.graphics.Path().apply {
|
||||||
lineTo(cupRight - 2f, cupTop)
|
moveTo(cx + 5f, cy - 4f); lineTo(cx + 8f, cy - 10f); lineTo(cx + 5f, cy - 10f); close()
|
||||||
close()
|
|
||||||
}
|
}
|
||||||
paint.style = android.graphics.Paint.Style.STROKE
|
paint.style = android.graphics.Paint.Style.FILL; canvas.drawPath(knifePath, paint)
|
||||||
paint.strokeWidth = 2.3f
|
}
|
||||||
|
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)
|
canvas.drawPath(cupPath, paint)
|
||||||
// Untertasse
|
paint.strokeWidth = 2f; canvas.drawLine(cupLeft - 2f, cupBot + 2f, cupRight + 2f, cupBot + 2f, paint)
|
||||||
paint.strokeWidth = 2f
|
canvas.drawArc(android.graphics.RectF(cupRight - 1f, cupTop + 4f, cupRight + 6f, cupBot - 2f), -90f, 180f, false, paint)
|
||||||
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 = 1.8f
|
paint.strokeWidth = 1.8f
|
||||||
val steamPath = android.graphics.Path().apply {
|
val steamPath = android.graphics.Path().apply {
|
||||||
moveTo(cx - 3f, cupTop - 3f)
|
moveTo(cx - 3f, cupTop - 3f); quadTo(cx - 1f, cupTop - 7f, cx + 1f, cupTop - 3f)
|
||||||
quadTo(cx - 1f, cupTop - 7f, cx + 1f, cupTop - 3f)
|
}; canvas.drawPath(steamPath, paint)
|
||||||
quadTo(cx + 3f, cupTop + 1f, cx + 5f, cupTop - 3f)
|
|
||||||
}
|
}
|
||||||
canvas.drawPath(steamPath, paint)
|
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()
|
||||||
}
|
}
|
||||||
de.waypointaudio.data.PoiCategory.MOBILITY -> {
|
canvas.drawPath(glassPath, paint)
|
||||||
// Bus-Silhouette: rechteckiger Körper, Räder, Fenster
|
canvas.drawArc(android.graphics.RectF(cx + 7f, cy - 5f, cx + 13f, cy + 5f), -90f, 180f, false, paint)
|
||||||
val busLeft = cx - 10f
|
}
|
||||||
val busRight = cx + 10f
|
de.waypointaudio.util.PoiSubtype.FAST_FOOD -> {
|
||||||
val busTop = cy - 8f
|
// Burger-Silhouette
|
||||||
val busBot = cy + 6f
|
paint.style = android.graphics.Paint.Style.FILL
|
||||||
// Buskarosserie
|
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.style = android.graphics.Paint.Style.STROKE; paint.strokeWidth = 2.8f
|
||||||
paint.strokeWidth = 2.2f
|
canvas.drawLine(cx - 9f, cy + 2f, cx + 9f, cy + 2f, paint)
|
||||||
val busRect = android.graphics.RectF(busLeft, busTop, busRight, busBot)
|
canvas.drawLine(cx - 9f, cy + 6f, cx + 9f, cy + 6f, paint)
|
||||||
canvas.drawRoundRect(busRect, 3f, 3f, paint)
|
paint.style = android.graphics.Paint.Style.FILL
|
||||||
// Frontscheibe (linke Seite)
|
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)
|
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
|
paint.strokeWidth = 1.5f
|
||||||
canvas.drawRoundRect(w1, 1f, 1f, paint)
|
canvas.drawRoundRect(android.graphics.RectF(busLeft + 4f, busTop + 2f, busLeft + 9f, busTop + 6f), 1f, 1f, paint)
|
||||||
// Fenster 2
|
canvas.drawRoundRect(android.graphics.RectF(busLeft + 11f, busTop + 2f, busLeft + 16f, busTop + 6f), 1f, 1f, paint)
|
||||||
val w2 = android.graphics.RectF(busLeft + 11f, busTop + 2f, busLeft + 16f, busTop + 6f)
|
|
||||||
canvas.drawRoundRect(w2, 1f, 1f, paint)
|
|
||||||
// Rad links
|
|
||||||
paint.strokeWidth = 2f
|
paint.strokeWidth = 2f
|
||||||
canvas.drawCircle(busLeft + 5f, busBot + 1f, 3f, paint)
|
canvas.drawCircle(busLeft + 5f, busBot + 1f, 3f, paint)
|
||||||
// Rad rechts
|
|
||||||
canvas.drawCircle(busRight - 5f, busBot + 1f, 3f, paint)
|
canvas.drawCircle(busRight - 5f, busBot + 1f, 3f, paint)
|
||||||
}
|
}
|
||||||
de.waypointaudio.data.PoiCategory.INFRASTRUCTURE -> {
|
de.waypointaudio.util.PoiSubtype.TRAIN_STATION -> {
|
||||||
// Info-"i"-Piktogramm: großer Punkt oben, vertikaler Stab unten
|
// 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
|
paint.style = android.graphics.Paint.Style.FILL
|
||||||
// Punkt oben
|
|
||||||
canvas.drawCircle(cx, cy - 8f, 3.5f, paint)
|
canvas.drawCircle(cx, cy - 8f, 3.5f, paint)
|
||||||
// Stab unten — leicht breiter, abgerundet
|
|
||||||
val stW2 = 4.5f
|
val stW2 = 4.5f
|
||||||
val rect2 = android.graphics.RectF(cx - stW2 / 2f, cy - 2f, cx + stW2 / 2f, cy + 9f)
|
canvas.drawRoundRect(android.graphics.RectF(cx - stW2/2f, cy - 2f, cx + stW2/2f, cy + 9f), 2f, 2f, paint)
|
||||||
canvas.drawRoundRect(rect2, 2f, 2f, paint)
|
|
||||||
// Basis-Strich
|
|
||||||
canvas.drawRoundRect(android.graphics.RectF(cx - 7f, cy + 8f, cx + 7f, cy + 10f), 2f, 2f, paint)
|
canvas.drawRoundRect(android.graphics.RectF(cx - 7f, cy + 8f, cx + 7f, cy + 10f), 2f, 2f, paint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3712,9 +4074,10 @@ private fun buildExplorerPoiIcon(category: de.waypointaudio.data.PoiCategory): B
|
|||||||
|
|
||||||
private fun buildExplorerPoiIconWithLabel(
|
private fun buildExplorerPoiIconWithLabel(
|
||||||
category: de.waypointaudio.data.PoiCategory,
|
category: de.waypointaudio.data.PoiCategory,
|
||||||
|
subtype: de.waypointaudio.util.PoiSubtype = de.waypointaudio.util.PoiSubtype.GENERIC_SIGHTS,
|
||||||
name: String
|
name: String
|
||||||
): Bitmap {
|
): Bitmap {
|
||||||
val iconSize = 56 // gleiche Größe wie buildExplorerPoiIcon
|
val iconSize = 68 // v2.5.46: gleiche Größe wie buildExplorerPoiIcon (erhöht auf 68 px)
|
||||||
val labelMaxChars = 14
|
val labelMaxChars = 14
|
||||||
val displayName = if (name.length > labelMaxChars) name.take(labelMaxChars - 1) + "…" else name
|
val displayName = if (name.length > labelMaxChars) name.take(labelMaxChars - 1) + "…" else name
|
||||||
|
|
||||||
@@ -3736,7 +4099,7 @@ private fun buildExplorerPoiIconWithLabel(
|
|||||||
val paint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
|
val paint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
// Zeichne Kategorie-Icon zentriert oben
|
// Zeichne Kategorie-Icon zentriert oben
|
||||||
val iconBmp = buildExplorerPoiIcon(category)
|
val iconBmp = buildExplorerPoiIcon(category, subtype)
|
||||||
val iconLeft = (totalW - iconSize) / 2f
|
val iconLeft = (totalW - iconSize) / 2f
|
||||||
canvas.drawBitmap(iconBmp, iconLeft, 0f, null)
|
canvas.drawBitmap(iconBmp, iconLeft, 0f, null)
|
||||||
iconBmp.recycle()
|
iconBmp.recycle()
|
||||||
@@ -3844,11 +4207,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(
|
Text(
|
||||||
"antippen zum Einklappen",
|
"antippen zum Einklappen",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f),
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f),
|
||||||
modifier = Modifier.padding(top = 1.dp)
|
modifier = Modifier.padding(top = 0.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AltRoute
|
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.DarkMode
|
||||||
import androidx.compose.material.icons.filled.GpsFixed
|
import androidx.compose.material.icons.filled.GpsFixed
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
@@ -105,6 +106,9 @@ fun MapLayerFab(
|
|||||||
onPerspectiveTiltChange: (Int) -> Unit = {},
|
onPerspectiveTiltChange: (Int) -> Unit = {},
|
||||||
onShowRoutingInfo: () -> Unit = {},
|
onShowRoutingInfo: () -> Unit = {},
|
||||||
onShowRoutePlanner: () -> Unit = {},
|
onShowRoutePlanner: () -> Unit = {},
|
||||||
|
// v2.5.45 — Auto-POI-Load-Toggle
|
||||||
|
poiAutoLoadEnabled: Boolean = true,
|
||||||
|
onPoiAutoLoadChange: (Boolean) -> Unit = {},
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var showSheet by remember { mutableStateOf(false) }
|
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 java.net.URLEncoder
|
||||||
import kotlin.math.*
|
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.
|
* Ein Explorer-POI aus Overpass/OpenStreetMap.
|
||||||
*
|
*
|
||||||
@@ -18,6 +99,8 @@ import kotlin.math.*
|
|||||||
* - osmId / osmType für Attribution/Detail-Link
|
* - osmId / osmType für Attribution/Detail-Link
|
||||||
* - rawTags für das POI-Detail-Sheet
|
* - rawTags für das POI-Detail-Sheet
|
||||||
* - category aus [PoiCategory]
|
* - category aus [PoiCategory]
|
||||||
|
*
|
||||||
|
* v2.5.45 — Subtype-Feld ergänzt (kein Backup-/Persistenzmodell-Einfluss).
|
||||||
*/
|
*/
|
||||||
data class ExplorerPoiResult(
|
data class ExplorerPoiResult(
|
||||||
val osmId: Long,
|
val osmId: Long,
|
||||||
@@ -25,6 +108,7 @@ data class ExplorerPoiResult(
|
|||||||
val displayName: String,
|
val displayName: String,
|
||||||
val category: PoiCategory,
|
val category: PoiCategory,
|
||||||
val categoryLabel: String, // lesbarer Label wie "Restaurant", "Museum" …
|
val categoryLabel: String, // lesbarer Label wie "Restaurant", "Museum" …
|
||||||
|
val subtype: PoiSubtype = PoiSubtype.GENERIC_SIGHTS, // v2.5.45
|
||||||
val latitude: Double,
|
val latitude: Double,
|
||||||
val longitude: Double,
|
val longitude: Double,
|
||||||
val rawTags: Map<String, String> = emptyMap(),
|
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:
|
* Rate-Limit-Strategie:
|
||||||
* - KEIN automatisches Polling.
|
* - Automatisches Polling auf explizite Nutzerwahl — standardmäßig aktiviert,
|
||||||
* - Abfragen nur auf explizite Nutzer-Aktion ("POIs laden").
|
* 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.
|
* - 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.
|
* - Overpass timeout auf 25 s gesetzt; max. 100 Ergebnisse je Kategorie.
|
||||||
*
|
*
|
||||||
* v2.5.44 Bug-Fix: Mapping-Priorität korrigiert.
|
* v2.5.45 — Subtype-Auflösung aus OSM-Tags (kein Persistenzmodell-Einfluss).
|
||||||
* - 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 …").
|
|
||||||
*
|
*
|
||||||
* OSM-Tags je Kategorie:
|
* OSM-Tags je Kategorie: (unverändert zu v2.5.44)
|
||||||
*
|
*
|
||||||
* SIGHTS:
|
* SIGHTS: tourism=attraction|viewpoint|museum|gallery|artwork|zoo|theme_park,
|
||||||
* tourism = attraction | viewpoint | museum | gallery | artwork | zoo | theme_park
|
* historic=*, amenity=theatre|arts_centre|cinema|place_of_worship
|
||||||
* historic = * (jeder Wert)
|
* GASTRONOMY: amenity=restaurant|cafe|bar|pub|fast_food|ice_cream|biergarten|food_court
|
||||||
* amenity = theatre | arts_centre | cinema | place_of_worship
|
* MOBILITY: amenity=parking|bicycle_parking|fuel|charging_station|bus_station|ferry_terminal,
|
||||||
*
|
* highway=bus_stop, railway=station|halt|tram_stop,
|
||||||
* GASTRONOMY:
|
* public_transport=station|platform, aeroway=aerodrome
|
||||||
* amenity = restaurant | cafe | bar | pub | fast_food | ice_cream |
|
* INFRASTRUCTURE: amenity=toilets|hospital|clinic|pharmacy|bank|atm|post_office|
|
||||||
* biergarten | food_court
|
* police|fire_station|library|drinking_water|shelter
|
||||||
*
|
|
||||||
* 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.
|
* Attribution: POI-Daten © OpenStreetMap-Mitwirkende, ODbL.
|
||||||
* Overpass API: overpass-api.de
|
* Overpass API: overpass-api.de
|
||||||
@@ -95,12 +160,49 @@ object ExplorerPoiClient {
|
|||||||
private const val OVERPASS_URL = "https://overpass-api.de/api/interpreter"
|
private const val OVERPASS_URL = "https://overpass-api.de/api/interpreter"
|
||||||
private const val MAX_RADIUS_M = 5000
|
private const val MAX_RADIUS_M = 5000
|
||||||
|
|
||||||
|
// v2.5.46 — Auto-Load-Parameter (erhöhte Schwellen für weniger Flackern)
|
||||||
|
/** Minimaler Zoom für automatisches Laden (unter diesem Zoom: kein Auto-Load). */
|
||||||
|
const val AUTO_LOAD_MIN_ZOOM = 12.0
|
||||||
|
/** Mindest-Kartenbewegung in Metern, bevor ein neuer Auto-Load ausgelöst wird.
|
||||||
|
* v2.5.46: erhöht von 300 m auf 600 m, um überflüssige Nachlades bei kleinen
|
||||||
|
* Kamerabewegungen (Kippen, Heading-Änderungen, kleiner Zoom) zu vermeiden. */
|
||||||
|
const val MOVE_THRESHOLD_M = 600.0
|
||||||
|
|
||||||
// In-Memory-Cache: key = "kategorie|bboxFingerprint"
|
// In-Memory-Cache: key = "kategorie|bboxFingerprint"
|
||||||
private val cache = mutableMapOf<String, List<ExplorerPoiResult>>()
|
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()
|
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)
|
||||||
|
// v2.5.46: Zoom-Delta-Schwelle erhöht auf 1.2, damit minimales Reinzoomen
|
||||||
|
// (z. B. Pitch-Änderung oder sehr kleiner Zoom) keinen Reload auslöst.
|
||||||
|
val zoomDelta = kotlin.math.abs(zoom - lastAutoZoom)
|
||||||
|
return moved > MOVE_THRESHOLD_M || zoomDelta > 1.2
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt POIs für [categories] im Kartenausschnitt [bbox].
|
* Lädt POIs für [categories] im Kartenausschnitt [bbox].
|
||||||
*
|
*
|
||||||
@@ -212,7 +314,6 @@ object ExplorerPoiClient {
|
|||||||
): String {
|
): String {
|
||||||
val around = "around:$radiusM,$lat,$lon"
|
val around = "around:$radiusM,$lat,$lon"
|
||||||
val filters = categoryFilters(category)
|
val filters = categoryFilters(category)
|
||||||
// Jeder Filter wird als node/way/relation-Union gebaut
|
|
||||||
val unionParts = filters.joinToString("\n") { (key, value) ->
|
val unionParts = filters.joinToString("\n") { (key, value) ->
|
||||||
if (value == "*") {
|
if (value == "*") {
|
||||||
""" node[$key]($around);
|
""" node[$key]($around);
|
||||||
@@ -231,14 +332,6 @@ $unionParts
|
|||||||
out center 100;""".trimIndent()
|
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) {
|
private fun categoryFilters(cat: PoiCategory): List<Pair<String, String>> = when (cat) {
|
||||||
PoiCategory.SIGHTS -> listOf(
|
PoiCategory.SIGHTS -> listOf(
|
||||||
"tourism" to "attraction",
|
"tourism" to "attraction",
|
||||||
@@ -302,14 +395,14 @@ out center 100;""".trimIndent()
|
|||||||
val root = JSONObject(json)
|
val root = JSONObject(json)
|
||||||
val elements = root.optJSONArray("elements") ?: return emptyList()
|
val elements = root.optJSONArray("elements") ?: return emptyList()
|
||||||
val out = ArrayList<ExplorerPoiResult>(elements.length())
|
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()) {
|
for (i in 0 until elements.length()) {
|
||||||
val el = elements.optJSONObject(i) ?: continue
|
val el = elements.optJSONObject(i) ?: continue
|
||||||
val osmId = el.optLong("id", -1L)
|
val osmId = el.optLong("id", -1L)
|
||||||
val osmType = el.optString("type", "node")
|
val osmType = el.optString("type", "node")
|
||||||
if (osmId < 0) continue
|
if (osmId < 0) continue
|
||||||
if (!seen.add(osmId)) continue // Duplikat
|
if (!seen.add(osmId)) continue
|
||||||
|
|
||||||
val lat = el.optDouble("lat", Double.NaN).let {
|
val lat = el.optDouble("lat", Double.NaN).let {
|
||||||
if (it.isNaN()) el.optJSONObject("center")?.optDouble("lat", Double.NaN) ?: Double.NaN
|
if (it.isNaN()) el.optJSONObject("center")?.optDouble("lat", Double.NaN) ?: Double.NaN
|
||||||
@@ -330,18 +423,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 catLabel = resolveCategoryLabel(tagsMap, category)
|
||||||
|
val subtype = resolveSubtype(tagsMap, category) // v2.5.45
|
||||||
|
|
||||||
val name = tagsMap["name"]?.takeIf { it.isNotBlank() }
|
val name = tagsMap["name"]?.takeIf { it.isNotBlank() }
|
||||||
?: tagsMap["ref"]?.takeIf { it.isNotBlank() }
|
?: tagsMap["ref"]?.takeIf { it.isNotBlank() }
|
||||||
?: humanizeTag(tagsMap, category)
|
?: humanizeTag(tagsMap, category)
|
||||||
?: continue // namenloser POI ohne Fallback → überspringen
|
?: continue
|
||||||
|
|
||||||
out.add(
|
out.add(
|
||||||
ExplorerPoiResult(
|
ExplorerPoiResult(
|
||||||
@@ -350,6 +438,7 @@ out center 100;""".trimIndent()
|
|||||||
displayName = name,
|
displayName = name,
|
||||||
category = category,
|
category = category,
|
||||||
categoryLabel = catLabel,
|
categoryLabel = catLabel,
|
||||||
|
subtype = subtype,
|
||||||
latitude = lat,
|
latitude = lat,
|
||||||
longitude = lon,
|
longitude = lon,
|
||||||
rawTags = tagsMap,
|
rawTags = tagsMap,
|
||||||
@@ -360,16 +449,56 @@ out center 100;""".trimIndent()
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menschenlesbarer Kategorie-Label für den POI (z. B. "Restaurant", "Museum").
|
* v2.5.45 — Subtype aus OSM-Tags ableiten (In-Memory only, kein Backup-Einfluss).
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
|
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 {
|
private fun resolveCategoryLabel(tags: Map<String, String>, cat: PoiCategory): String {
|
||||||
val amenity = tags["amenity"]
|
val amenity = tags["amenity"]
|
||||||
val tourism = tags["tourism"]
|
val tourism = tags["tourism"]
|
||||||
@@ -379,9 +508,6 @@ out center 100;""".trimIndent()
|
|||||||
val publicTransport = tags["public_transport"]
|
val publicTransport = tags["public_transport"]
|
||||||
val aeroway = tags["aeroway"]
|
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) {
|
if (cat != PoiCategory.SIGHTS) {
|
||||||
when (cat) {
|
when (cat) {
|
||||||
PoiCategory.GASTRONOMY -> return when (amenity) {
|
PoiCategory.GASTRONOMY -> return when (amenity) {
|
||||||
@@ -446,41 +572,25 @@ out center 100;""".trimIndent()
|
|||||||
amenity == "place_of_worship" -> "Gotteshaus"
|
amenity == "place_of_worship" -> "Gotteshaus"
|
||||||
else -> "Sehenswürdigkeit"
|
else -> "Sehenswürdigkeit"
|
||||||
}
|
}
|
||||||
// Fallbacks für den else-Fall (sollten durch obige when-Blöcke nicht erreicht werden)
|
|
||||||
PoiCategory.GASTRONOMY -> "Gastronomie"
|
PoiCategory.GASTRONOMY -> "Gastronomie"
|
||||||
PoiCategory.MOBILITY -> "Mobilität"
|
PoiCategory.MOBILITY -> "Mobilität"
|
||||||
PoiCategory.INFRASTRUCTURE-> "Infrastruktur"
|
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? {
|
private fun humanizeTag(tags: Map<String, String>, cat: PoiCategory): String? {
|
||||||
return when (cat) {
|
return when (cat) {
|
||||||
PoiCategory.SIGHTS -> {
|
PoiCategory.SIGHTS -> {
|
||||||
// Für SIGHTS nur dann Fallback-Name, wenn ein spezifisches Tag vorhanden ist
|
|
||||||
val label = resolveCategoryLabel(tags, cat)
|
val label = resolveCategoryLabel(tags, cat)
|
||||||
when {
|
when {
|
||||||
tags["historic"] != null -> label // Denkmal, Ruine etc. immer behalten
|
tags["historic"] != null -> label
|
||||||
tags["tourism"] != null -> label // tourism=viewpoint/museum etc.
|
tags["tourism"] != null -> label
|
||||||
tags["amenity"] in setOf(
|
tags["amenity"] in setOf(
|
||||||
"theatre","arts_centre","cinema","place_of_worship"
|
"theatre","arts_centre","cinema","place_of_worship"
|
||||||
) -> label
|
) -> label
|
||||||
else -> null // zu generisch → überspringen
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// GASTRONOMY, MOBILITY, INFRASTRUCTURE: immer Fallback-Label zurückgeben
|
|
||||||
PoiCategory.GASTRONOMY -> resolveCategoryLabel(tags, cat)
|
PoiCategory.GASTRONOMY -> resolveCategoryLabel(tags, cat)
|
||||||
PoiCategory.MOBILITY -> resolveCategoryLabel(tags, cat)
|
PoiCategory.MOBILITY -> resolveCategoryLabel(tags, cat)
|
||||||
PoiCategory.INFRASTRUCTURE -> resolveCategoryLabel(tags, cat)
|
PoiCategory.INFRASTRUCTURE -> resolveCategoryLabel(tags, cat)
|
||||||
@@ -503,6 +613,9 @@ out center 100;""".trimIndent()
|
|||||||
"locomotive" -> "Lokomotive (hist.)"
|
"locomotive" -> "Lokomotive (hist.)"
|
||||||
"aircraft" -> "Flugzeug (hist.)"
|
"aircraft" -> "Flugzeug (hist.)"
|
||||||
"ship" -> "Schiff (hist.)"
|
"ship" -> "Schiff (hist.)"
|
||||||
|
"monastery" -> "Kloster"
|
||||||
|
"palace" -> "Schloss/Palast"
|
||||||
|
"fort" -> "Festung"
|
||||||
else -> "Historisch"
|
else -> "Historisch"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1212,6 +1212,8 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
val tourRoutesMap = runCatching { repository.tourRoutes.first() }.getOrDefault(emptyMap())
|
val tourRoutesMap = runCatching { repository.tourRoutes.first() }.getOrDefault(emptyMap())
|
||||||
val tourCoversMap = runCatching { repository.tourCovers.first() }.getOrDefault(emptyMap())
|
val tourCoversMap = runCatching { repository.tourCovers.first() }.getOrDefault(emptyMap())
|
||||||
|
// v2.5.48 — Echte TrackDraft-Objekte für Backup lesen (nicht gpsTracks).
|
||||||
|
val trackDraftsSnapshot = runCatching { repository.trackDrafts.first() }.getOrDefault(emptyList())
|
||||||
|
|
||||||
val input = de.waypointaudio.io.BackupExportManager.BackupInput(
|
val input = de.waypointaudio.io.BackupExportManager.BackupInput(
|
||||||
waypoints = _waypoints.value,
|
waypoints = _waypoints.value,
|
||||||
@@ -1221,6 +1223,7 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
|||||||
musicSettings = musicSettingsMap,
|
musicSettings = musicSettingsMap,
|
||||||
tourRoutes = tourRoutesMap,
|
tourRoutes = tourRoutesMap,
|
||||||
tourCovers = tourCoversMap,
|
tourCovers = tourCoversMap,
|
||||||
|
trackDrafts = trackDraftsSnapshot,
|
||||||
appVersionName = pkgInfo?.versionName ?: "",
|
appVersionName = pkgInfo?.versionName ?: "",
|
||||||
appVersionCode = versionCode
|
appVersionCode = versionCode
|
||||||
)
|
)
|
||||||
@@ -1366,6 +1369,13 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trackDrafts) {
|
if (trackDrafts) {
|
||||||
|
// v2.5.48 FIX: Echte TrackDraft-Objekte importieren statt gpsTracks-Map.
|
||||||
|
// trackDraftsToAdd enthält bereits neue UUIDs (vergeben im ImportManager).
|
||||||
|
payload.trackDraftsToAdd.forEach { draft ->
|
||||||
|
repository.addTrackDraft(draft)
|
||||||
|
}
|
||||||
|
// tracksToWrite ist für die Live-GPS-Track-Map (gpsTracksFlow).
|
||||||
|
// Bei neuem Format leer; bei altem Format auch leer (kein Import).
|
||||||
payload.tracksToWrite.forEach { (tour, points) ->
|
payload.tracksToWrite.forEach { (tour, points) ->
|
||||||
repository.saveGpsTrack(tour, points)
|
repository.saveGpsTrack(tour, points)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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