949 lines
41 KiB
Kotlin
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)
|
|
}
|
|
}
|