Files
GPS2Audio/app/src/main/kotlin/de/waypointaudio/ui/MapLayerPanel.kt
T
2026-05-30 18:04:55 +00:00

949 lines
41 KiB
Kotlin

package de.waypointaudio.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AltRoute
import androidx.compose.material.icons.filled.Explore
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.GpsFixed
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Landscape
import androidx.compose.material.icons.filled.Layers
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.LocationCity
import androidx.compose.material.icons.filled.Map
import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.filled.Nightlife
import androidx.compose.material.icons.filled.PinDrop
import androidx.compose.material.icons.filled.Route
import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.material.icons.filled.Timeline
import androidx.compose.material.icons.filled.Label
import androidx.compose.material.icons.filled.LabelOff
import androidx.compose.material.icons.filled.Place
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Slider
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.waypointaudio.R
import de.waypointaudio.data.MapBaseStyle
import de.waypointaudio.data.PoiLayerSettings
/**
* Daten-Container für die Layer-Schalter eines Kartenscreens.
*
* v2.4.1 — Erweitertes Basis-Stil-Menü:
* - Modern hell, Dunkel, Topografisch, Terrain/3D, Satellit
* v2.5.38 — POI-Layer-Kategorien ergänzt.
*/
data class MapLayerState(
val followLocation: Boolean = true,
val showMarkers: Boolean = true,
val showRadius: Boolean = true,
val showTrack: Boolean = true,
val showAudioLength: Boolean = true,
val baseStyle: MapBaseStyle = MapBaseStyle.MODERN_2D,
val showAudioLines: Boolean = true,
val poiLayers: PoiLayerSettings = PoiLayerSettings(),
)
/**
* Floating-Button + BottomSheet für Karten-Ebenen.
*
* v2.4.1 — Neue auswählbare Basis-Stile: Modern hell, Dunkel, Topo, Terrain/3D,
* Satellit. Auswahl wird über [MapStyleStore] persistiert.
* v2.5.38 — POI-Layer-Abschnitt (Sehenswürdigkeiten, Infrastruktur, Gastronomie,
* Mobilität) als Chips. Karteninfo-Dialog (Attribution). Kartentyp persistiert.
* v2.5.39 — Sheet-Inhalt scrollbar (verticalScroll), damit POI-Kategorien und
* Karteninfo auch auf kleinen Displays erreichbar sind.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MapLayerFab(
state: MapLayerState,
onStateChange: (MapLayerState) -> Unit,
customSatelliteUrl: String = "",
onCustomSatelliteUrlChange: (String) -> Unit = {},
customTopoUrl: String = "",
onCustomTopoUrlChange: (String) -> Unit = {},
customTerrainDemUrl: String = "",
onCustomTerrainDemUrlChange: (String) -> Unit = {},
perspectiveTilt: Int = 35,
onPerspectiveTiltChange: (Int) -> Unit = {},
onShowRoutingInfo: () -> Unit = {},
onShowRoutePlanner: () -> Unit = {},
// v2.5.45 — Auto-POI-Load-Toggle
poiAutoLoadEnabled: Boolean = true,
onPoiAutoLoadChange: (Boolean) -> Unit = {},
modifier: Modifier = Modifier
) {
var showSheet by remember { mutableStateOf(false) }
var showStyleHint by remember { mutableStateOf<MapBaseStyle?>(null) }
var showAttributionDialog by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
Box(modifier = modifier) {
SmallFloatingActionButton(
onClick = { showSheet = true },
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.primary
) {
Icon(
Icons.Filled.Layers,
contentDescription = stringResource(R.string.map_layers_open)
)
}
}
if (showSheet) {
ModalBottomSheet(
onDismissRequest = { showSheet = false },
sheetState = sheetState
) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 600.dp)
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Layers, contentDescription = null, modifier = Modifier.size(20.dp))
Spacer(Modifier.width(8.dp))
Text(
stringResource(R.string.map_layers_title),
style = MaterialTheme.typography.titleMedium
)
}
Text(
stringResource(R.string.map_layers_base_section),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// v2.5.37 — Erste Zeile: Modern, Dunkel, Outdoor
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
BaseStyleChip(
label = stringResource(R.string.map_layers_base_2d),
icon = Icons.Filled.LightMode,
selected = state.baseStyle == MapBaseStyle.MODERN_2D,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.MODERN_2D))
}
)
BaseStyleChip(
label = stringResource(R.string.map_layers_base_dark),
icon = Icons.Filled.DarkMode,
selected = state.baseStyle == MapBaseStyle.DARK,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.DARK))
}
)
BaseStyleChip(
label = stringResource(R.string.map_layers_base_outdoor),
icon = Icons.Filled.Landscape,
selected = state.baseStyle == MapBaseStyle.OUTDOOR,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.OUTDOOR))
}
)
}
// v2.5.37 — Zweite Zeile: 3D Stadt, Klassisch
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
BaseStyleChip(
label = stringResource(R.string.map_layers_base_city3d),
icon = Icons.Filled.LocationCity,
selected = state.baseStyle == MapBaseStyle.CITY_3D,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.CITY_3D))
}
)
BaseStyleChip(
label = stringResource(R.string.map_layers_base_classic),
icon = Icons.Filled.Map,
selected = state.baseStyle == MapBaseStyle.CLASSIC,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.CLASSIC))
}
)
// Platzhalter rechts für symmetrisches Layout
Spacer(Modifier.weight(1f))
}
// v2.5.37 — Info-Block für Dunkel-Stil
if (state.baseStyle == MapBaseStyle.DARK) {
StyleConfigSurface(
tone = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.55f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Filled.DarkMode,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.map_layers_dark_note),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
TextButton(onClick = { showStyleHint = MapBaseStyle.DARK }) {
Text(stringResource(R.string.map_layers_satellite_info_button))
}
}
}
}
// v2.5.37 — Info-Block für 3D-Stadt-Stil
if (state.baseStyle == MapBaseStyle.CITY_3D) {
StyleConfigSurface(
tone = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.55f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Filled.LocationCity,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.map_layers_city3d_note),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
TextButton(onClick = { showStyleHint = MapBaseStyle.CITY_3D }) {
Text(stringResource(R.string.map_layers_satellite_info_button))
}
}
Spacer(Modifier.height(8.dp))
Text(
stringResource(R.string.map_layers_terrain_pitch_label) +
": " + perspectiveTilt + "°",
style = MaterialTheme.typography.labelMedium
)
Slider(
value = perspectiveTilt.coerceIn(0, 60).toFloat(),
valueRange = 0f..60f,
steps = 11,
onValueChange = { onPerspectiveTiltChange(it.toInt()) }
)
}
}
// v2.5.37 — Info-Block für Outdoor-Stil
if (state.baseStyle == MapBaseStyle.OUTDOOR) {
StyleConfigSurface(
tone = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.4f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Filled.Landscape,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.map_layers_outdoor_note),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
TextButton(onClick = { showStyleHint = MapBaseStyle.OUTDOOR }) {
Text(stringResource(R.string.map_layers_satellite_info_button))
}
}
}
}
Text(
stringResource(R.string.map_layers_overlays_section),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
LayerSwitchRow(
icon = Icons.Filled.GpsFixed,
label = stringResource(R.string.map_layers_follow),
checked = state.followLocation,
onCheckedChange = { onStateChange(state.copy(followLocation = it)) }
)
LayerSwitchRow(
icon = Icons.Filled.PinDrop,
label = stringResource(R.string.map_layers_markers),
checked = state.showMarkers,
onCheckedChange = { onStateChange(state.copy(showMarkers = it)) }
)
LayerSwitchRow(
icon = Icons.Filled.RadioButtonChecked,
label = stringResource(R.string.map_layers_radius),
checked = state.showRadius,
onCheckedChange = { onStateChange(state.copy(showRadius = it)) }
)
LayerSwitchRow(
icon = Icons.Filled.MusicNote,
label = stringResource(R.string.map_layers_audio_length),
checked = state.showAudioLength,
onCheckedChange = { onStateChange(state.copy(showAudioLength = it)) }
)
LayerSwitchRow(
icon = Icons.Filled.Timeline,
label = stringResource(R.string.map_layers_audio_lines),
checked = state.showAudioLines,
onCheckedChange = { onStateChange(state.copy(showAudioLines = it)) }
)
Text(
stringResource(R.string.map_layers_audio_lines_hint),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f),
modifier = Modifier.padding(start = 28.dp)
)
if (state.showAudioLines) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 28.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
AudioLineLegendChip(
color = 0xFF66BB6A.toInt(),
label = stringResource(R.string.map_layers_audio_lines_legend_short)
)
AudioLineLegendChip(
color = 0xFFFFA726.toInt(),
label = stringResource(R.string.map_layers_audio_lines_legend_medium)
)
AudioLineLegendChip(
color = 0xFFE53935.toInt(),
label = stringResource(R.string.map_layers_audio_lines_legend_long)
)
}
}
LayerSwitchRow(
icon = Icons.Filled.Timeline,
label = stringResource(R.string.map_layers_track),
checked = state.showTrack,
onCheckedChange = { onStateChange(state.copy(showTrack = it)) }
)
// ─── v2.5.38 — POI-Layer-Abschnitt ───────────────────────────
Spacer(Modifier.height(2.dp))
Text(
stringResource(R.string.poi_section_title),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (state.baseStyle == MapBaseStyle.CLASSIC) {
Text(
stringResource(R.string.poi_unavailable_classic),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
modifier = Modifier.padding(start = 4.dp)
)
} else {
Text(
stringResource(R.string.poi_section_subtitle),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f),
modifier = Modifier.padding(start = 4.dp)
)
// Zeile 1: Sehenswürdigkeiten + Infrastruktur
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
PoiCategoryChip(
label = stringResource(R.string.poi_cat_sights),
icon = Icons.Filled.Place,
selected = state.poiLayers.showSights,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showSights = !state.poiLayers.showSights
)
)
)
}
)
PoiCategoryChip(
label = stringResource(R.string.poi_cat_infrastructure),
icon = Icons.Filled.LocationOn,
selected = state.poiLayers.showInfrastructure,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showInfrastructure = !state.poiLayers.showInfrastructure
)
)
)
}
)
}
// Zeile 2: Gastronomie + Mobilität
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
PoiCategoryChip(
label = stringResource(R.string.poi_cat_gastronomy),
icon = Icons.Filled.Nightlife,
selected = state.poiLayers.showGastronomy,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showGastronomy = !state.poiLayers.showGastronomy
)
)
)
}
)
PoiCategoryChip(
label = stringResource(R.string.poi_cat_mobility),
icon = Icons.Filled.Route,
selected = state.poiLayers.showMobility,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showMobility = !state.poiLayers.showMobility
)
)
)
}
)
}
// v2.5.42 — POI-Namen-Toggle: standardmäßig deaktiviert
val anyPoiActive = state.poiLayers.showSights ||
state.poiLayers.showGastronomy ||
state.poiLayers.showMobility ||
state.poiLayers.showInfrastructure
if (anyPoiActive) {
LayerSwitchRow(
icon = if (state.poiLayers.showPoiNames)
Icons.Filled.Label else Icons.Filled.LabelOff,
label = "POI-Namen anzeigen",
checked = state.poiLayers.showPoiNames,
onCheckedChange = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showPoiNames = it
)
)
)
}
)
// 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)
)
}
}
// ─── v2.5.38 — Karteninfo / Attribution ──────────────────────
Spacer(Modifier.height(2.dp))
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Filled.Info,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.map_info_dialog_title),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"OpenFreeMap · OpenMapTiles · © OpenStreetMap-Mitwirkende",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.78f)
)
}
TextButton(onClick = { showAttributionDialog = true }) {
Text(stringResource(R.string.map_info_open))
}
}
}
// v2.4.2 — Routenplaner-Eintrag im Layer-Panel.
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.55f),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Filled.AltRoute,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.route_planner_open_short),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
stringResource(R.string.route_planner_subtitle),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.78f)
)
}
TextButton(onClick = onShowRoutePlanner) {
Text(stringResource(R.string.route_planner_open))
}
}
}
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Filled.AltRoute,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.routing_poi_open),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
stringResource(R.string.routing_poi_search_attribution),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f)
)
}
TextButton(onClick = onShowRoutingInfo) {
Text(stringResource(R.string.routing_poi_open))
}
}
}
TextButton(
onClick = { showSheet = false },
modifier = Modifier.align(Alignment.End)
) {
Text(stringResource(R.string.map_layers_close))
}
}
}
}
// v2.5.38 — Stil-Info-Dialog (unverändert aus v2.5.37)
showStyleHint?.let { style ->
val title: String
val body: String
when (style) {
MapBaseStyle.DARK -> {
title = stringResource(R.string.map_layers_base_dark)
body = stringResource(R.string.map_layers_dark_info)
}
MapBaseStyle.OUTDOOR -> {
title = stringResource(R.string.map_layers_base_outdoor)
body = stringResource(R.string.map_layers_outdoor_info)
}
MapBaseStyle.CITY_3D -> {
title = stringResource(R.string.map_layers_base_city3d)
body = stringResource(R.string.map_layers_city3d_info)
}
MapBaseStyle.CLASSIC -> {
title = stringResource(R.string.map_layers_base_classic)
body = stringResource(R.string.map_layers_classic_info)
}
else -> {
title = stringResource(R.string.map_layers_base_2d)
body = stringResource(R.string.map_layers_modern_info)
}
}
AlertDialog(
onDismissRequest = { showStyleHint = null },
title = { Text(title) },
text = { Text(body) },
confirmButton = {
TextButton(onClick = { showStyleHint = null }) {
Text(stringResource(R.string.version_notice_ok))
}
}
)
}
// v2.5.38 — Karteninfo / Attribution-Dialog
if (showAttributionDialog) {
MapAttributionDialog(onDismiss = { showAttributionDialog = false })
}
}
/**
* v2.5.38 — Vollständiger Karteninfo-/Attribution-Dialog.
*
* Zeigt rechtlich korrekte Namensnennung für:
* - OpenFreeMap (openfreemap.org)
* - OpenMapTiles (openmaptiles.org)
* - OpenStreetMap-Mitwirkende (ODbL)
*
* Links werden als anklickbare Texte dargestellt.
*/
@Composable
fun MapAttributionDialog(onDismiss: () -> Unit) {
val uriHandler = LocalUriHandler.current
AlertDialog(
onDismissRequest = onDismiss,
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Filled.Info,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.map_info_dialog_title))
}
},
text = {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
// Kartenstil
Text(
stringResource(R.string.map_info_style_label),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
stringResource(R.string.map_info_style_source),
style = MaterialTheme.typography.bodyMedium
)
// Kartendaten
Text(
stringResource(R.string.map_info_data_label),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
stringResource(R.string.map_info_data_text),
style = MaterialTheme.typography.bodyMedium
)
// Links
Text(
stringResource(R.string.map_info_links_label),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// openfreemap.org
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f),
modifier = Modifier
.fillMaxWidth()
.clickable {
runCatching {
uriHandler.openUri("https://openfreemap.org")
}
}
) {
Text(
"" + stringResource(R.string.map_info_link_ofm),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
)
}
// openmaptiles.org
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f),
modifier = Modifier
.fillMaxWidth()
.clickable {
runCatching {
uriHandler.openUri("https://openmaptiles.org")
}
}
) {
Text(
"" + stringResource(R.string.map_info_link_omt),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
)
}
// openstreetmap.org/copyright
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f),
modifier = Modifier
.fillMaxWidth()
.clickable {
runCatching {
uriHandler.openUri("https://www.openstreetmap.org/copyright")
}
}
) {
Text(
"" + stringResource(R.string.map_info_link_osm),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
)
}
Spacer(Modifier.height(2.dp))
Text(
"Lizenz: ODbL (Open Database License). Namensnennung erforderlich.",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f)
)
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.map_info_close))
}
}
)
}
@Composable
private fun StyleConfigSurface(
tone: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit
) {
Surface(
shape = RoundedCornerShape(12.dp),
color = tone,
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
content = content
)
}
}
@Composable
private fun BaseStyleChip(
label: String,
icon: ImageVector,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val container = if (selected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f)
}
val content = if (selected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Surface(
shape = RoundedCornerShape(14.dp),
color = container,
modifier = modifier.clickable(onClick = onClick)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp, horizontal = 6.dp)
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = content)
Spacer(Modifier.height(4.dp))
Text(
label,
style = MaterialTheme.typography.labelMedium,
color = content
)
}
}
}
/**
* v2.5.38 — POI-Kategorie-Chip (kompaktes Toggle-Chip-Design).
*/
@Composable
private fun PoiCategoryChip(
label: String,
icon: ImageVector,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val container = if (selected) {
MaterialTheme.colorScheme.secondaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f)
}
val content = if (selected) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Surface(
shape = RoundedCornerShape(12.dp),
color = container,
modifier = modifier.clickable(onClick = onClick)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp)
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(16.dp), tint = content)
Spacer(Modifier.width(4.dp))
Text(
label,
style = MaterialTheme.typography.labelSmall,
color = content
)
}
}
}
@Composable
private fun AudioLineLegendChip(color: Int, label: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Surface(
shape = RoundedCornerShape(2.dp),
color = androidx.compose.ui.graphics.Color(color),
modifier = Modifier
.width(14.dp)
.height(4.dp)
) {}
Spacer(Modifier.width(4.dp))
Text(label, style = MaterialTheme.typography.labelSmall)
}
}
@Composable
private fun LayerSwitchRow(
icon: ImageVector,
label: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f)
)
Spacer(Modifier.width(10.dp))
Text(
label,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium
)
Switch(checked = checked, onCheckedChange = onCheckedChange)
}
}