diff options
Diffstat (limited to 'android-app/app/src/main')
5 files changed, 67 insertions, 15 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt index 9668fd1..b0d256b 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt @@ -14,11 +14,11 @@ object TackDetector { if (points.size < 2 * HALF_WIN + 1) return emptyList() val results = mutableListOf<TackEvent>() - var lastTackMs = Long.MIN_VALUE + var lastTackMs: Long? = null for (i in HALF_WIN until points.size - HALF_WIN) { if (points[i].timestampMs - points.first().timestampMs < START_SKIP_MS) continue - if (points[i].timestampMs - lastTackMs < MIN_GAP_MS) continue + if (lastTackMs != null && points[i].timestampMs - lastTackMs < MIN_GAP_MS) continue val spreadBefore = abs(angleDiff(points[i - 2].cogDeg, points[i - 1].cogDeg)) val spreadAfter = abs(angleDiff(points[i + 1].cogDeg, points[i + 2].cogDeg)) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt new file mode 100644 index 0000000..292240a --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt @@ -0,0 +1,38 @@ +package org.terst.nav.track + +import org.maplibre.android.style.expressions.Expression +import org.maplibre.geojson.Feature +import org.maplibre.geojson.LineString +import org.maplibre.geojson.Point + +private fun speedToHex(speedKt: Double): String = when { + speedKt >= 13 -> "#F44336" // red + speedKt >= 10 -> "#FFC107" // amber + speedKt >= 7 -> "#4CAF50" // green + speedKt >= 4 -> "#00BCD4" // cyan + else -> "#2196F3" // blue +} + +/** + * Breaks [points] into consecutive 2-point LineString Features. + * Each feature carries "speed" (knots) and "color" (CSS hex) properties. + */ +fun speedSegments(points: List<TrackPoint>): List<Feature> = + (0 until points.size - 1).map { i -> + val sog = (points[i].sogKnots + points[i + 1].sogKnots) / 2.0 + Feature.fromGeometry( + LineString.fromLngLats(listOf( + Point.fromLngLat(points[i].lon, points[i].lat), + Point.fromLngLat(points[i + 1].lon, points[i + 1].lat) + )) + ).apply { + addNumberProperty("speed", sog) + addStringProperty("color", speedToHex(sog)) + } + } + +/** + * Returns the lineColor expression for speed-colored segments. + * Reads the pre-computed "color" string property from each feature. + */ +fun speedColorExpression(): Expression = Expression.get("color") diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index 0cc098b..0bf1dff 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -148,7 +148,11 @@ class TrackRepository(context: Context) { suspend fun getSavedTracks(): List<SavedTrack> = getPastTracks() .filter { it.isNotEmpty() } - .map { it.toSavedTrack() } + .mapNotNull { points -> + runCatching { points.toSavedTrack() } + .onFailure { e -> NavLogger.e("track", "toSavedTrack failed: ${e.javaClass.simpleName}: ${e.message}") } + .getOrNull() + } fun safState(): SafState = storage.safState() diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt index 49ce0f5..49de36f 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt @@ -106,7 +106,9 @@ class MainViewModel( runCatching { trackRepository.getPastTracks() } .onSuccess { _pastTracks.value = it - _savedTracks.value = trackRepository.getSavedTracks() + runCatching { trackRepository.getSavedTracks() } + .onSuccess { saved -> _savedTracks.value = saved } + .onFailure { e -> NavLogger.e("track", "init: getSavedTracks failed: ${e.javaClass.simpleName}: ${e.message}") } } .onFailure { e -> NavLogger.e("track", "init: failed to load past tracks: ${e.javaClass.simpleName}: ${e.message}") } } @@ -170,7 +172,9 @@ class MainViewModel( viewModelScope.launch { val summary = trackRepository.stopTrack() _pastTracks.value = trackRepository.getPastTracks() - _savedTracks.value = trackRepository.getSavedTracks() + runCatching { trackRepository.getSavedTracks() } + .onSuccess { _savedTracks.value = it } + .onFailure { e -> NavLogger.e("track", "stopTrack: getSavedTracks failed: ${e.javaClass.simpleName}: ${e.message}") } _trackPoints.value = emptyList() _isRecording.value = false _trackStats.value = null @@ -187,7 +191,9 @@ class MainViewModel( points.forEach { trackRepository.addPoint(it) } val summary = trackRepository.stopTrack() _pastTracks.value = trackRepository.getPastTracks() - _savedTracks.value = trackRepository.getSavedTracks() + runCatching { trackRepository.getSavedTracks() } + .onSuccess { _savedTracks.value = it } + .onFailure { e -> NavLogger.e("track", "injectTestTrack: getSavedTracks failed: ${e.javaClass.simpleName}: ${e.message}") } _trackPoints.value = emptyList() _isRecording.value = false _trackStats.value = null @@ -430,7 +436,9 @@ class MainViewModel( viewModelScope.launch { trackRepository.importTrack(points) _pastTracks.value = trackRepository.getPastTracks() - _savedTracks.value = trackRepository.getSavedTracks() + runCatching { trackRepository.getSavedTracks() } + .onSuccess { _savedTracks.value = it } + .onFailure { e -> NavLogger.e("track", "addImportedTrack: getSavedTracks failed: ${e.javaClass.simpleName}: ${e.message}") } } } @@ -444,7 +452,9 @@ class MainViewModel( runCatching { trackRepository.reloadTracks() } .onSuccess { tracks -> _pastTracks.value = tracks - _savedTracks.value = trackRepository.getSavedTracks() + runCatching { trackRepository.getSavedTracks() } + .onSuccess { saved -> _savedTracks.value = saved } + .onFailure { e -> NavLogger.e("track", "reloadPastTracks: getSavedTracks failed: ${e.javaClass.simpleName}: ${e.message}") } NavLogger.i("track", "reloadPastTracks: ${tracks.size} tracks loaded") } .onFailure { e -> diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt index 7614a49..a3c1eac 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt @@ -26,6 +26,8 @@ import org.terst.nav.TidalCurrentState import org.terst.nav.data.model.WindArrow import org.terst.nav.track.TackEvent import org.terst.nav.track.TrackPoint +import org.terst.nav.track.speedColorExpression +import org.terst.nav.track.speedSegments import kotlin.math.cos import kotlin.math.sin import kotlin.math.sqrt @@ -328,11 +330,10 @@ class MapHandler(private val maplibreMap: MapLibreMap) { style.addSource(trackPastFocusSource!!) style.addLayer(LineLayer(TRACK_PAST_FOCUS_LAYER_ID, TRACK_PAST_FOCUS_SOURCE_ID).apply { setProperties( - PropertyFactory.lineColor("#2B8FC4"), - PropertyFactory.lineOpacity(1.0f), PropertyFactory.lineWidth(4f), PropertyFactory.lineCap("round"), - PropertyFactory.lineJoin("round") + PropertyFactory.lineJoin("round"), + PropertyFactory.lineColor(speedColorExpression()) ) }) } @@ -363,12 +364,10 @@ class MapHandler(private val maplibreMap: MapLibreMap) { } } - /** Highlights [points] in the focus layer (full-opacity bright blue over the dim layer). */ + /** Highlights [points] in the focus layer, colored by boat speed. */ fun setFocusedTrackPoints(points: List<TrackPoint>?) { if (points != null && points.size >= 2) { - trackPastFocusSource?.setGeoJson( - Feature.fromGeometry(LineString.fromLngLats(points.map { Point.fromLngLat(it.lon, it.lat) })) - ) + trackPastFocusSource?.setGeoJson(FeatureCollection.fromFeatures(speedSegments(points))) } else { trackPastFocusSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList())) } @@ -443,3 +442,4 @@ class MapHandler(private val maplibreMap: MapLibreMap) { return Polygon.fromLngLats(listOf(points)) } } + |
