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 = 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 } } }