Release GPS2Audio v2.5.44 POI category fix

This commit is contained in:
Perplexity Computer
2026-05-30 05:42:44 +00:00
parent 18ea77b21d
commit 014e6c1bf1
100 changed files with 28584 additions and 1083 deletions
@@ -29,8 +29,12 @@ import androidx.compose.material.icons.filled.AddLocation
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FiberManualRecord
import androidx.compose.material.icons.filled.Layers
import androidx.compose.material.icons.filled.MyLocation
import androidx.compose.material.icons.filled.PlayCircle
import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.Timeline
import androidx.compose.material3.AlertDialog
@@ -72,13 +76,14 @@ import com.google.android.gms.location.Priority
import com.google.android.gms.tasks.CancellationTokenSource
import de.waypointaudio.R
import de.waypointaudio.data.GpsTrackPoint
import de.waypointaudio.data.MapProvider
import de.waypointaudio.data.MapProviderStore
import de.waypointaudio.data.Waypoint
import de.waypointaudio.viewmodel.WaypointViewModel
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import org.osmdroid.config.Configuration
import org.osmdroid.events.MapEventsReceiver
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.MapEventsOverlay
@@ -116,7 +121,12 @@ import kotlin.math.sqrt
@Composable
fun MapScreen(
viewModel: WaypointViewModel,
onNavigateBack: () -> Unit
onNavigateBack: () -> Unit,
// v2.2.1: Wenn gesetzt, zeigt die TopAppBar einen Explorer/Editor-Modus-
// Umschalter im Titel-Slot. Dadurch wirken Explorer und Editor als ein
// gemeinsames Kartenmodul.
currentMode: MapMode? = null,
onModeChange: ((MapMode) -> Unit)? = null
) {
val allWaypoints by viewModel.waypoints.collectAsState()
val selectedTour by viewModel.selectedTour.collectAsState()
@@ -149,6 +159,14 @@ fun MapScreen(
var currentLocation by remember { mutableStateOf<Pair<Double, Double>?>(null) }
var showAddWaypointAction by remember { mutableStateOf(false) }
// Karten-Anbieter (Phase 1): persistent via DataStore, app-weit gespeichert.
// Hinweis: Eine Pro-Tour-Auswahl ist mit der bisherigen Tour-Settings-Architektur
// nicht direkt vereinbar (Touren halten Audio-Einstellungen, keine Karten-Settings),
// daher bewusst app-weit. Die Auswahl ist über das Karten-Menü jederzeit umstellbar.
val providerStore = remember { MapProviderStore(context.applicationContext) }
val selectedProvider by providerStore.provider.collectAsState(initial = MapProvider.DEFAULT)
var showProviderDialog by remember { mutableStateOf(false) }
// Editor-Dialog-State
var editDialogWaypoint by remember { mutableStateOf<Waypoint?>(null) }
var editDialogPrefill by remember { mutableStateOf<Pair<Double, Double>?>(null) }
@@ -167,6 +185,15 @@ fun MapScreen(
// Bestätigungsdialog für das Löschen eines Wegpunkts
var deleteConfirmWaypoint by remember { mutableStateOf<Waypoint?>(null) }
// Ortssuche (Nominatim/OpenStreetMap) — temporärer roter Pin auf der Karte.
// Verändert weder Wegpunkte, Tracks, Drafts noch den Aufnahmezustand.
var showSearchDialog by remember { mutableStateOf(false) }
var searchPin by remember { mutableStateOf<GeoPoint?>(null) }
// v2.2.1: Layer-Panel-Status. Die Switches steuern aktuell vorwiegend
// Sichtbarkeit; markers/radius/track werden in drawMapOverlays gefiltert.
var layerState by remember { mutableStateOf(MapLayerState()) }
// Bitmap für Standort-Puck (einmalig erstellt)
val puckBitmap = remember { createLocationPuckBitmap(context) }
@@ -174,17 +201,27 @@ fun MapScreen(
LocationServices.getFusedLocationProviderClient(context)
}
// ─── Karte neu zeichnen wenn Wegpunkte / Track / Farben sich ändern ────────
LaunchedEffect(waypoints, primaryColor, currentTrack) {
// ─── Karten-Anbieter Wechsel: Tile-Source aktualisieren und Cache invalidieren ─
LaunchedEffect(selectedProvider) {
val mv = mapView ?: return@LaunchedEffect
mv.setTileSource(selectedProvider.toTileSource())
mv.invalidate()
}
// ─── Karte neu zeichnen wenn Wegpunkte / Track / Farben / Auswahl / Such-Pin / Layer sich ändern ─
LaunchedEffect(waypoints, primaryColor, currentTrack, actionMenuWaypoint, searchPin, layerState) {
val mv = mapView ?: return@LaunchedEffect
drawMapOverlays(
mv = mv,
waypoints = waypoints,
trackPoints = currentTrack,
waypoints = if (layerState.showMarkers) waypoints else emptyList(),
trackPoints = if (layerState.showTrack) currentTrack else emptyList(),
primaryArgb = primaryColor.toArgb(),
errorArgb = errorColor.toArgb(),
trackArgb = trackColor.toArgb(),
onWaypointTapped = { wp -> actionMenuWaypoint = wp }
selectedWaypointId = actionMenuWaypoint?.id,
onWaypointTapped = { wp -> actionMenuWaypoint = wp },
searchPin = searchPin,
showRadius = layerState.showRadius
)
}
@@ -223,6 +260,10 @@ fun MapScreen(
showEditDialog = false
editDialogWaypoint = null
editDialogPrefill = null
},
// v2.5.16 — Kopieren auch aus dem Karten-Bearbeiten-Flow möglich.
onCopyToOtherTour = editDialogWaypoint?.let { wp ->
{ targetTour -> viewModel.copyWaypointToTour(wp.id, targetTour) }
}
)
}
@@ -350,18 +391,54 @@ fun MapScreen(
)
}
// ─── Karten-Anbieter Auswahl-Dialog ─────────────────────────────────────
if (showProviderDialog) {
MapProviderDialog(
selected = selectedProvider,
onSelect = { provider ->
scope.launch {
providerStore.setProvider(provider)
}
showProviderDialog = false
},
onDismiss = { showProviderDialog = false }
)
}
// ─── Ortssuche Dialog (Nominatim/OSM) ────────────────────────────────────
if (showSearchDialog) {
PlaceSearchDialog(
onDismiss = { showSearchDialog = false },
onResultSelected = { result ->
val gp = GeoPoint(result.latitude, result.longitude)
searchPin = gp
mapView?.controller?.animateTo(gp)
mapView?.controller?.setZoom(15.0)
showSearchDialog = false
}
)
}
Scaffold(
topBar = {
TopAppBar(
title = {
Column {
Text(stringResource(R.string.map_view))
if (tourList.size > 1) {
Text(
text = selectedTour,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
)
if (currentMode != null && onModeChange != null) {
MapModeSegmentedHeader(
current = currentMode,
onChange = onModeChange,
subtitle = if (tourList.size > 1) selectedTour else null
)
} else {
Column {
Text(stringResource(R.string.map_view))
if (tourList.size > 1) {
Text(
text = selectedTour,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
)
}
}
}
},
@@ -373,10 +450,33 @@ fun MapScreen(
)
}
},
actions = {
IconButton(onClick = { showProviderDialog = true }) {
Icon(
Icons.Filled.Layers,
contentDescription = stringResource(R.string.map_provider_menu)
)
}
IconButton(onClick = { showSearchDialog = true }) {
Icon(
Icons.Filled.Search,
contentDescription = stringResource(R.string.search_place_action)
)
}
if (searchPin != null) {
IconButton(onClick = { searchPin = null }) {
Icon(
Icons.Filled.Delete,
contentDescription = stringResource(R.string.search_place_close)
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
)
)
},
@@ -518,7 +618,7 @@ fun MapScreen(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
MapView(ctx).apply {
setTileSource(TileSourceFactory.MAPNIK)
setTileSource(selectedProvider.toTileSource())
setMultiTouchControls(true)
controller.setZoom(12.0)
@@ -557,12 +657,15 @@ fun MapScreen(
mapView = this
drawMapOverlays(
mv = this,
waypoints = waypoints,
trackPoints = currentTrack,
waypoints = if (layerState.showMarkers) waypoints else emptyList(),
trackPoints = if (layerState.showTrack) currentTrack else emptyList(),
primaryArgb = primaryColor.toArgb(),
errorArgb = errorColor.toArgb(),
trackArgb = trackColor.toArgb(),
onWaypointTapped = { wp -> actionMenuWaypoint = wp }
selectedWaypointId = actionMenuWaypoint?.id,
onWaypointTapped = { wp -> actionMenuWaypoint = wp },
searchPin = searchPin,
showRadius = layerState.showRadius
)
}
},
@@ -570,12 +673,15 @@ fun MapScreen(
mapView = mv
drawMapOverlays(
mv = mv,
waypoints = waypoints,
trackPoints = currentTrack,
waypoints = if (layerState.showMarkers) waypoints else emptyList(),
trackPoints = if (layerState.showTrack) currentTrack else emptyList(),
primaryArgb = primaryColor.toArgb(),
errorArgb = errorColor.toArgb(),
trackArgb = trackColor.toArgb(),
onWaypointTapped = { wp -> actionMenuWaypoint = wp }
selectedWaypointId = actionMenuWaypoint?.id,
onWaypointTapped = { wp -> actionMenuWaypoint = wp },
searchPin = searchPin,
showRadius = layerState.showRadius
)
}
)
@@ -618,11 +724,12 @@ fun MapScreen(
}
// ─── Track-Statistik-Panel ───────────────────────────────────────
// Bottom padding leaves a clear strip for the OSM attribution badge.
AnimatedVisibility(
visible = isRecording || currentTrack.isNotEmpty(),
modifier = Modifier
.align(Alignment.BottomStart)
.padding(start = 12.dp, bottom = 12.dp),
.padding(start = 12.dp, bottom = 40.dp),
enter = fadeIn() + slideInVertically(initialOffsetY = { it }),
exit = fadeOut() + slideOutVertically(targetOffsetY = { it })
) {
@@ -710,11 +817,12 @@ fun MapScreen(
}
// ─── Editor-Hinweis (unten mittig) ──────────────────────────────
// Raised above the OSM attribution strip so the mandatory badge is never covered.
AnimatedVisibility(
visible = !isRecording && currentTrack.isEmpty(),
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp),
.padding(bottom = 40.dp),
enter = fadeIn(),
exit = fadeOut()
) {
@@ -731,6 +839,26 @@ fun MapScreen(
)
}
}
// ─── Layer-/3D-Overlay-FAB (v2.2.1) ──────────────────────────────
// Schwebt rechts oben über der Karte, öffnet das Material-3-Panel
// mit Layer-Switches und 3D-Vorbereitungs-Hinweis.
MapLayerFab(
state = layerState,
onStateChange = { layerState = it },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 8.dp, end = 8.dp)
)
// ─── OSM-Attribution (Pflicht) ───────────────────────────────────
// Moved to bottom-left so the FAB stack at bottom-right cannot overlap it.
// The track-stats card and editor hint above leave a clear strip for this badge.
OsmAttributionBadge(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(start = 6.dp, bottom = 6.dp)
)
}
}
@@ -752,42 +880,73 @@ private fun drawMapOverlays(
primaryArgb: Int,
errorArgb: Int,
trackArgb: Int,
onWaypointTapped: (Waypoint) -> Unit
selectedWaypointId: String?,
onWaypointTapped: (Waypoint) -> Unit,
searchPin: GeoPoint? = null,
showRadius: Boolean = true
) {
val overlays = mv.overlays
// Radius-Polygone, Wegpunkt-Marker und Track-Polylines entfernen
// Radius-Polygone, Wegpunkt-Marker und Track-Polylines entfernen.
// Mein-Standort-Puck überlebt (eigener Marker mit title="Mein Standort").
overlays.removeAll(overlays.filterIsInstance<Polygon>().toSet())
val waypointMarkers = overlays.filterIsInstance<Marker>()
.filter { it.title != "Mein Standort" }
overlays.removeAll(waypointMarkers.toSet())
overlays.removeAll(overlays.filterIsInstance<Polyline>().toSet())
// GPS-Track zeichnen
val density = mv.resources.displayMetrics.density
// GPS-Track zeichnen — kräftiger, mit weißem Halo für Kontrast auf jedem Untergrund.
if (trackPoints.size >= 2) {
val pts = trackPoints.map { GeoPoint(it.latitude, it.longitude) }
// Halo (heller Rand)
val halo = Polyline(mv).apply {
setPoints(pts)
outlinePaint.color = 0xFFFFFFFF.toInt()
outlinePaint.strokeWidth = 18f * density / 2f
outlinePaint.strokeCap = Paint.Cap.ROUND
outlinePaint.strokeJoin = Paint.Join.ROUND
outlinePaint.alpha = 230
}
overlays.add(halo)
// Hauptlinie
val polyline = Polyline(mv).apply {
setPoints(trackPoints.map { GeoPoint(it.latitude, it.longitude) })
setPoints(pts)
outlinePaint.color = trackArgb
outlinePaint.strokeWidth = 6f
outlinePaint.alpha = 200
outlinePaint.strokeWidth = 12f * density / 2f
outlinePaint.strokeCap = Paint.Cap.ROUND
outlinePaint.strokeJoin = Paint.Join.ROUND
outlinePaint.alpha = 235
}
overlays.add(polyline)
}
// Wegpunkte zeichnen
// Wegpunkte zeichnen — größere, gut sichtbare Streckenpunkt-Handles
for (wp in waypoints) {
val geoPoint = GeoPoint(wp.latitude, wp.longitude)
// Radius-Kreis
val circle = buildRadiusCircle(geoPoint, wp.radiusMeters.toDouble(), primaryArgb)
overlays.add(circle)
// Radius-Kreis (nur wenn Layer aktiv)
if (showRadius) {
val circle = buildRadiusCircle(geoPoint, wp.radiusMeters.toDouble(), primaryArgb)
overlays.add(circle)
}
// Marker mit Tap-Handler
val isSelected = selectedWaypointId != null && wp.id == selectedWaypointId
// Größerer Marker mit Kontrast-Halo. Ausgewählter Punkt: andere Farbe + größer.
val handleBitmap = createWaypointHandleBitmap(
context = mv.context,
fillArgb = if (isSelected) errorArgb else primaryArgb,
isSelected = isSelected
)
val marker = Marker(mv).apply {
position = geoPoint
title = wp.name
snippet = "%.6f, %.6f · %dm".format(Locale.US, wp.latitude, wp.longitude, wp.radiusMeters.toInt())
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
alpha = if (wp.isActive) 1.0f else 0.4f
// ANCHOR_CENTER, ANCHOR_CENTER → Touch-Trefferfläche entspricht der Bitmap-Größe und
// sitzt zentriert über dem Punkt (deutlich größer als der frühere Default-Pin).
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
alpha = if (wp.isActive) 1.0f else 0.55f
icon = android.graphics.drawable.BitmapDrawable(mv.context.resources, handleBitmap)
// infoWindow = null → Tap löst onMarkerClick aus
setOnMarkerClickListener { _, _ ->
onWaypointTapped(wp)
@@ -797,9 +956,79 @@ private fun drawMapOverlays(
overlays.add(marker)
}
// Temporärer roter Such-Treffer-Pin (Nominatim/OSM). Verändert keine Daten.
if (searchPin != null) {
val pinBm = makeSearchPinBitmapShared(96)
val pinMarker = Marker(mv).apply {
position = searchPin
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = android.graphics.drawable.BitmapDrawable(mv.context.resources, pinBm)
title = null
snippet = null
subDescription = null
infoWindow = null
setOnMarkerClickListener { _, _ -> true }
}
overlays.add(pinMarker)
}
mv.invalidate()
}
/**
* Zeichnet einen großen, kontraststarken Kreis-Handle für einen Streckenpunkt.
* Die Bitmap-Größe bestimmt zugleich die Touch-Trefferfläche des osmdroid-Markers.
* - Normaler Punkt: ca. 44dp Trefferfläche, sichtbarer Kreis ~26dp mit weißem Halo
* - Ausgewählter Punkt: ca. 56dp Trefferfläche, sichtbarer Kreis ~36dp mit dunklem Außenrand
*/
private fun createWaypointHandleBitmap(
context: android.content.Context,
fillArgb: Int,
isSelected: Boolean
): Bitmap {
val density = context.resources.displayMetrics.density
// Touch-Trefferfläche (Bitmap-Kantenlänge) mind. 44dp/56dp lt. Material Touch-Targets
val touchDp = if (isSelected) 56f else 44f
val sizePx = (touchDp * density).toInt()
val bmp = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
val cx = sizePx / 2f
val cy = sizePx / 2f
// Sichtbarer Kreis-Radius
val visibleRadiusDp = if (isSelected) 18f else 13f
val visibleRadius = visibleRadiusDp * density
val haloThicknessDp = if (isSelected) 4.5f else 3.5f
val haloThickness = haloThicknessDp * density
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
// Weicher Schatten-/Halo-Ring (weiß), damit der Punkt auf jedem Kartenuntergrund sichtbar bleibt
paint.style = Paint.Style.FILL
paint.color = 0xFFFFFFFF.toInt()
canvas.drawCircle(cx, cy, visibleRadius + haloThickness, paint)
// Füllung
paint.color = fillArgb
canvas.drawCircle(cx, cy, visibleRadius, paint)
// Kontrast-Außenrand (dunkel) beim ausgewählten Punkt deutlich kräftiger
paint.style = Paint.Style.STROKE
paint.strokeWidth = (if (isSelected) 3f else 2f) * density
paint.color = 0xFF202020.toInt()
canvas.drawCircle(cx, cy, visibleRadius, paint)
if (isSelected) {
// Innerer heller Ring als zusätzliche „Selected"-Markierung
paint.style = Paint.Style.STROKE
paint.strokeWidth = 2f * density
paint.color = 0xFFFFFFFF.toInt()
canvas.drawCircle(cx, cy, visibleRadius - 4f * density, paint)
}
return bmp
}
// ---------------------------------------------------------------------------
// Hilfsfunktionen: Standort-Puck, Marker, Radius-Kreis, Distanz
// ---------------------------------------------------------------------------