Files
GPS2Audio/app/src/main/kotlin/de/waypointaudio/data/MapStyleSettings.kt
T
2026-05-30 05:42:44 +00:00

242 lines
10 KiB
Kotlin

package de.waypointaudio.data
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* Basis-Kartenstil — v2.5.38 Explorer Map Styles.
*
* Alle Stile nutzen OpenFreeMap (https://openfreemap.org) für Vektor-Tiles,
* kostenlos ohne API-Key, ohne eigenen Server. Attribution: © OpenFreeMap,
* © OpenMapTiles, Daten: © OpenStreetMap-Mitwirkende.
*
* MODERN_2D — Positron: heller, reduzierter Vektorstil. Ideal als
* neutraler Hintergrund für Wegpunkte/Routen.
* URL: https://tiles.openfreemap.org/styles/positron
*
* DARK — Dark Matter: dunkler Vektorstil.
* URL: https://tiles.openfreemap.org/styles/dark
*
* OUTDOOR — Fiord/Outdoor: Liberty-Vektorstil mit Outdoor-/Geländecharakter.
* URL: https://tiles.openfreemap.org/styles/liberty
* (Liberty ist der vollständigste, lebendigste OpenFreeMap-Style
* mit deutlicher Grün-/Geländeanmutung.)
*
* CITY_3D — 3D Stadt: Positron-Basis + MapLibre fill-extrusion für
* Gebäude-Layer (building). Wenn kein building-Layer in der
* Style-Source, automatischer 2D-Fallback ohne Crash.
* URL: https://tiles.openfreemap.org/styles/positron
* + programmatische fill-extrusion nachträglich per addLayer.
*
* CLASSIC — Klassisch: OSM-Raster-Tiles (tile.openstreetmap.org),
* raster-basierter Fallback. Kein Vektor.
*
* SATELLITE — Legacy aus v2.4.x. Wird auf MODERN_2D migriert.
* Satellit ist ab v2.5.37 nicht mehr im UI angeboten.
* TERRAIN_3D — Legacy aus v2.4.x. Wird auf CITY_3D migriert.
* TOPO — Legacy aus v2.4.x. Wird auf OUTDOOR migriert.
* PERSPECTIVE — Legacy aus v2.3.x. Wird auf CITY_3D migriert.
*/
enum class MapBaseStyle(val key: String) {
MODERN_2D("modern_2d"),
DARK("dark"),
OUTDOOR("outdoor"),
CITY_3D("city_3d"),
CLASSIC("classic"),
// Legacy — für Rückwärtskompatibilität gespeicherter Settings:
SATELLITE("satellite"),
TERRAIN_3D("terrain_3d"),
TOPO("topo"),
PERSPECTIVE("perspective");
companion object {
val DEFAULT = MODERN_2D
fun fromKey(key: String?): MapBaseStyle {
return when (key) {
"modern_2d" -> MODERN_2D
"dark" -> DARK
"outdoor" -> OUTDOOR
"city_3d" -> CITY_3D
"classic" -> CLASSIC
// Legacy migration:
"satellite" -> MODERN_2D
"terrain_3d" -> CITY_3D
"topo" -> OUTDOOR
"perspective"-> CITY_3D
else -> DEFAULT
}
}
}
}
/**
* POI-Kategorie-Auswahl — v2.5.38.
*
* Jede Kategorie schaltet eine Gruppe von MapLibre-Symbol-Layern sichtbar/unsichtbar.
* Wenn der Style keinen passenden Layer enthält, passiert nichts (kein Crash).
*
* Kategorien:
* SIGHTS — Sehenswürdigkeiten / Tourismus (tourism, historic, amenity:place_of_worship)
* INFRASTRUCTURE — Infrastruktur (hospital, pharmacy, police, post, atm, bank, fuel)
* GASTRONOMY — Gastronomie (restaurant, cafe, fast_food, bar, pub, ice_cream)
* MOBILITY — Mobilität (bus_stop, train_station, parking, bicycle, car_sharing)
*/
enum class PoiCategory(val key: String) {
SIGHTS("sights"),
INFRASTRUCTURE("infrastructure"),
GASTRONOMY("gastronomy"),
MOBILITY("mobility");
companion object {
fun fromKey(key: String?): PoiCategory? = values().firstOrNull { it.key == key }
}
}
/**
* Persistierte POI-Sichtbarkeit je Kategorie — v2.5.44.
* Default: Alle vier Hauptkategorien aktiv, damit der Nutzer sofort
* alle POI-Typen sieht (Fix: vorher nur Sehenswürdigkeiten sichtbar).
* Namen-Labels bleiben standardmäßig aus (Performance/Übersichtlichkeit).
*/
data class PoiLayerSettings(
val showSights: Boolean = true,
val showInfrastructure: Boolean = true,
val showGastronomy: Boolean = true,
val showMobility: Boolean = true,
// v2.5.42 — Namen-Labels auf der Karte standardmäßig deaktiviert
val showPoiNames: Boolean = false,
)
/**
* Gespeicherte Karten-Stil-Einstellungen — v2.5.38.
*
* @param baseStyle gewählter Basis-Stil (wird persistent gespeichert)
* @param showAudioLength ob die visuelle Audio-/Trigger-Länge als
* Ring/Indikator je Wegpunkt eingeblendet wird.
* @param perspectiveTilt Kamera-Pitch für CITY_3D (Grad, 0..60).
* @param showAudioLines Clip-Längen als projizierte Liniensegmente.
* @param routingEndpoint optionaler OSRM-Routing-Endpoint.
* @param poiLayers POI-Kategorien-Sichtbarkeit (persistent).
*
* Legacy-Felder (customSatelliteUrl, customTopoUrl, customTerrainDemUrl) werden
* weiterhin gelesen, aber nicht mehr in der UI angeboten — für Rückwärts-
* kompatibilität mit gespeicherten Preferences aus v2.4.x.
*/
data class MapStyleSettings(
val baseStyle: MapBaseStyle = MapBaseStyle.DEFAULT,
val customSatelliteUrl: String = "", // legacy, wird ignoriert
val customTopoUrl: String = "", // legacy, wird ignoriert
val customTerrainDemUrl: String = "", // legacy, wird ignoriert
val showAudioLength: Boolean = true,
val perspectiveTilt: Int = 35,
val showAudioLines: Boolean = true,
val routingEndpoint: String = "",
val poiLayers: PoiLayerSettings = PoiLayerSettings(),
)
private val Context.mapStyleDataStore by preferencesDataStore(name = "map_style_settings")
class MapStyleStore(private val context: Context) {
private companion object {
val KEY_BASE_STYLE = stringPreferencesKey("base_style")
val KEY_SAT_URL = stringPreferencesKey("custom_satellite_url")
val KEY_TOPO_URL = stringPreferencesKey("custom_topo_url")
val KEY_TERRAIN_DEM_URL = stringPreferencesKey("custom_terrain_dem_url")
val KEY_SHOW_AUDIO_LEN = booleanPreferencesKey("show_audio_length")
val KEY_PERSPECTIVE_TILT = stringPreferencesKey("perspective_tilt")
val KEY_SHOW_AUDIO_LINES = booleanPreferencesKey("show_audio_lines")
val KEY_ROUTING_ENDPOINT = stringPreferencesKey("routing_endpoint")
// v2.5.38 — POI-Layer-Persistenz
val KEY_POI_SIGHTS = booleanPreferencesKey("poi_sights")
val KEY_POI_INFRASTRUCTURE = booleanPreferencesKey("poi_infrastructure")
val KEY_POI_GASTRONOMY = booleanPreferencesKey("poi_gastronomy")
val KEY_POI_MOBILITY = booleanPreferencesKey("poi_mobility")
// v2.5.42
val KEY_POI_SHOW_NAMES = booleanPreferencesKey("poi_show_names")
}
val settings: Flow<MapStyleSettings> = context.mapStyleDataStore.data.map { prefs ->
MapStyleSettings(
baseStyle = MapBaseStyle.fromKey(prefs[KEY_BASE_STYLE]),
customSatelliteUrl = prefs[KEY_SAT_URL].orEmpty(),
customTopoUrl = prefs[KEY_TOPO_URL].orEmpty(),
customTerrainDemUrl = prefs[KEY_TERRAIN_DEM_URL].orEmpty(),
showAudioLength = prefs[KEY_SHOW_AUDIO_LEN] ?: true,
perspectiveTilt = prefs[KEY_PERSPECTIVE_TILT]?.toIntOrNull()?.coerceIn(0, 60) ?: 35,
showAudioLines = prefs[KEY_SHOW_AUDIO_LINES] ?: true,
routingEndpoint = prefs[KEY_ROUTING_ENDPOINT].orEmpty(),
poiLayers = PoiLayerSettings(
showSights = prefs[KEY_POI_SIGHTS] ?: true,
// v2.5.44: Default auf true geändert — Nutzer ohne gespeicherte
// Einstellung bekommen jetzt alle Kategorien aktiv
showInfrastructure = prefs[KEY_POI_INFRASTRUCTURE] ?: true,
showGastronomy = prefs[KEY_POI_GASTRONOMY] ?: true,
showMobility = prefs[KEY_POI_MOBILITY] ?: true,
showPoiNames = prefs[KEY_POI_SHOW_NAMES] ?: false,
)
)
}
suspend fun setBaseStyle(style: MapBaseStyle) {
context.mapStyleDataStore.edit { it[KEY_BASE_STYLE] = style.key }
}
suspend fun setShowAudioLength(enabled: Boolean) {
context.mapStyleDataStore.edit { it[KEY_SHOW_AUDIO_LEN] = enabled }
}
suspend fun setPerspectiveTilt(tilt: Int) {
context.mapStyleDataStore.edit {
it[KEY_PERSPECTIVE_TILT] = tilt.coerceIn(0, 60).toString()
}
}
suspend fun setShowAudioLines(enabled: Boolean) {
context.mapStyleDataStore.edit { it[KEY_SHOW_AUDIO_LINES] = enabled }
}
// v2.5.38 — POI-Layer-Persistenz
suspend fun setPoiLayers(poi: PoiLayerSettings) {
context.mapStyleDataStore.edit {
it[KEY_POI_SIGHTS] = poi.showSights
it[KEY_POI_INFRASTRUCTURE] = poi.showInfrastructure
it[KEY_POI_GASTRONOMY] = poi.showGastronomy
it[KEY_POI_MOBILITY] = poi.showMobility
it[KEY_POI_SHOW_NAMES] = poi.showPoiNames
}
}
// v2.5.37 — Legacy-Stubs für Aufrufstellen aus v2.4.x die
// noch customSatelliteUrl / customTopoUrl / customTerrainDemUrl schreiben.
// Diese Werte werden weiterhin gelesen (Preferences bleiben),
// aber im UI nicht mehr angeboten und vom Style-Builder ignoriert.
suspend fun setCustomSatelliteUrl(url: String) {
context.mapStyleDataStore.edit { it[KEY_SAT_URL] = url.trim() }
}
suspend fun setCustomTopoUrl(url: String) {
context.mapStyleDataStore.edit { it[KEY_TOPO_URL] = url.trim() }
}
suspend fun setCustomTerrainDemUrl(url: String) {
context.mapStyleDataStore.edit { it[KEY_TERRAIN_DEM_URL] = url.trim() }
}
/**
* Speichert den Routing-Endpunkt — v2.4.4: nutzt
* [de.waypointaudio.util.OsrmRoutingClient.normalizeBase], sodass der
* Nutzer sowohl die reine Basis-URL (`https://router.project-osrm.org`)
* als auch die vollständige `/route/v1/driving`-Variante speichern darf.
* Intern landet in den Preferences immer die normalisierte Basis-URL,
* was Doppelprefixe bei der eigentlichen Request-URL ausschließt.
*/
suspend fun setRoutingEndpoint(url: String) {
val normalized = de.waypointaudio.util.OsrmRoutingClient.normalizeBase(url)
context.mapStyleDataStore.edit { it[KEY_ROUTING_ENDPOINT] = normalized }
}
}