From 5019c83b80706d4633ac66053967dd464006fbbd Mon Sep 17 00:00:00 2001 From: Thorsten Date: Tue, 28 Apr 2026 11:52:27 +0200 Subject: [PATCH] Wegpunkt-Pfeile mit Richtung und Info-Fenster; GPX-Schreib-Performance fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TrackDetailActivity: Zeigt zoom-adaptive Richtungspfeile (Bearing) als Marker auf der Karte; Klick öffnet Info-Fenster mit Geschwindigkeit und Kurs - GPXManager: appendLocation schreibt nur noch per appendText statt Datei komplett neu zu schreiben (O(1) statt O(n)) - TrackingService: finalizeSession() beim Stoppen aufrufen, um GPX-Footer korrekt abzuschließen Co-Authored-By: Claude Sonnet 4.6 --- .../gpstracker/TrackDetailActivity.kt | 92 +++++++++++++++++++ .../de/tstratmann/gpstracker/GPXManager.kt | 8 +- .../tstratmann/gpstracker/TrackingService.kt | 6 +- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/mobile/src/main/java/de/tstratmann/gpstracker/TrackDetailActivity.kt b/mobile/src/main/java/de/tstratmann/gpstracker/TrackDetailActivity.kt index ec57c90..998333c 100644 --- a/mobile/src/main/java/de/tstratmann/gpstracker/TrackDetailActivity.kt +++ b/mobile/src/main/java/de/tstratmann/gpstracker/TrackDetailActivity.kt @@ -1,6 +1,10 @@ package de.tstratmann.gpstracker +import android.graphics.Bitmap +import android.graphics.Canvas import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -18,6 +22,7 @@ class TrackDetailActivity : AppCompatActivity(), OnMapReadyCallback { private lateinit var binding: ActivityTrackDetailBinding private lateinit var googleMap: GoogleMap private lateinit var track: Track + private val waypointMarkers = mutableListOf() companion object { const val EXTRA_TRACK_NAME = "track_name" @@ -86,6 +91,93 @@ class TrackDetailActivity : AppCompatActivity(), OnMapReadyCallback { drawTrackWithSpeedGradient() addStartEndMarkers() fitMapToTrack() + + googleMap.setOnCameraIdleListener { + displayWaypoints(googleMap.cameraPosition.zoom) + } + + googleMap.setOnMarkerClickListener { marker -> + val point = marker.tag as? TrackPoint ?: return@setOnMarkerClickListener false + marker.title = formatWaypointSpeed(point.speed) + marker.snippet = "Kurs: ${point.bearing.toInt()}°" + marker.showInfoWindow() + true + } + } + + private fun getWaypointStride(zoom: Float): Int = when { + zoom < 12f -> Int.MAX_VALUE + zoom < 13f -> 100 + zoom < 14f -> 50 + zoom < 15f -> 20 + zoom < 16f -> 10 + zoom < 17f -> 5 + zoom < 18f -> 3 + else -> 1 + } + + private fun createArrowBitmap(bearing: Float): BitmapDescriptor { + val size = 48 + val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + val cx = size / 2f + val cy = size / 2f + + val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + style = Paint.Style.FILL + } + val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.argb(220, 30, 100, 200) + style = Paint.Style.STROKE + strokeWidth = 3f + } + + val path = Path().apply { + moveTo(cx, cy - size * 0.38f) + lineTo(cx + size * 0.22f, cy + size * 0.30f) + lineTo(cx, cy + size * 0.12f) + lineTo(cx - size * 0.22f, cy + size * 0.30f) + close() + } + + canvas.rotate(bearing, cx, cy) + canvas.drawPath(path, fillPaint) + canvas.drawPath(path, strokePaint) + + return BitmapDescriptorFactory.fromBitmap(bitmap) + } + + private fun displayWaypoints(zoom: Float) { + waypointMarkers.forEach { it.remove() } + waypointMarkers.clear() + + val stride = getWaypointStride(zoom) + if (stride == Int.MAX_VALUE) return + + track.points.forEachIndexed { index, point -> + if (index % stride == 0) { + val marker = googleMap.addMarker( + MarkerOptions() + .position(LatLng(point.latitude, point.longitude)) + .icon(createArrowBitmap(point.bearing)) + .anchor(0.5f, 0.5f) + .flat(true) + ) ?: return@forEachIndexed + marker.tag = point + waypointMarkers.add(marker) + } + } + } + + private fun formatWaypointSpeed(speedMs: Float): String { + val useKnots = track.activityType?.useKnots == true + return if (useKnots) { + val knots = speedMs * 1.94384f + "%.1f kn".format(knots) + } else { + "${(speedMs * 3.6f).toInt()} km/h" + } } private fun drawTrackWithSpeedGradient() { diff --git a/wear/src/main/java/de/tstratmann/gpstracker/GPXManager.kt b/wear/src/main/java/de/tstratmann/gpstracker/GPXManager.kt index 3808239..e268e82 100644 --- a/wear/src/main/java/de/tstratmann/gpstracker/GPXManager.kt +++ b/wear/src/main/java/de/tstratmann/gpstracker/GPXManager.kt @@ -19,6 +19,11 @@ class GPXManager(private val context: Context) { return file } + fun finalizeSession() { + currentFile?.appendText(getGpxFooter()) + currentFile = null + } + fun appendLocation(location: Location) { currentFile?.let { file -> val time = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { @@ -37,8 +42,7 @@ class GPXManager(private val context: Context) { } sb.append("") - val content = file.readText().removeSuffix(getGpxFooter()) - file.writeText(content + sb.toString() + getGpxFooter()) + file.appendText(sb.toString()) } } diff --git a/wear/src/main/java/de/tstratmann/gpstracker/TrackingService.kt b/wear/src/main/java/de/tstratmann/gpstracker/TrackingService.kt index 069c45f..4292e12 100644 --- a/wear/src/main/java/de/tstratmann/gpstracker/TrackingService.kt +++ b/wear/src/main/java/de/tstratmann/gpstracker/TrackingService.kt @@ -100,8 +100,9 @@ class TrackingService : Service() { speedCount = 0 _trackingState.value = TrackingData() - val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) - .setMinUpdateIntervalMillis(1000) + val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000) + .setMinUpdateIntervalMillis(5000) + .setMinUpdateDistanceMeters(10f) .build() try { @@ -122,6 +123,7 @@ class TrackingService : Service() { fun stopTracking() { fusedLocationClient.removeLocationUpdates(locationCallback) job?.cancel() + gpxManager.finalizeSession() stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() }