From af1232773fd525ecd6f6ad434346673139d0d183 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Tue, 30 Jun 2026 18:48:43 +0000 Subject: feat(fishing): dashed bearing line on map for fishing target Adds setupFishingLayer() and updateFishingBearing() to MapHandler with destinationPoint() haversine helper. Draws dashed orange line from boat toward the computed fishing target bearing. Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/kotlin/org/terst/nav/ui/MapHandler.kt | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) 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 99c8ffc..6d408ed 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 @@ -96,6 +96,11 @@ class MapHandler(private val maplibreMap: MapLibreMap) { private var fadSource: GeoJsonSource? = null + private val FISHING_LINE_SOURCE_ID = "fishing-line-source" + private val FISHING_LINE_LAYER_ID = "fishing-line-layer" + private var fishingLineSource: GeoJsonSource? = null + private var fishingLineStyle: Style? = null + private var anchorPointSource: GeoJsonSource? = null private var anchorCircleSource: GeoJsonSource? = null private var tidalCurrentSource: GeoJsonSource? = null @@ -489,6 +494,54 @@ class MapHandler(private val maplibreMap: MapLibreMap) { tackSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList())) } + /** + * Initialises the fishing bearing line layer. + * A dashed orange line is drawn from the boat position toward the fishing target bearing. + * Call once after style loads. + */ + fun setupFishingLayer(style: Style) { + fishingLineStyle = style + val src = GeoJsonSource(FISHING_LINE_SOURCE_ID, FeatureCollection.fromFeatures(emptyArray())) + fishingLineSource = src + style.addSource(src) + style.addLayer(LineLayer(FISHING_LINE_LAYER_ID, FISHING_LINE_SOURCE_ID).apply { + setProperties( + PropertyFactory.lineColor("#FF6D00"), + PropertyFactory.lineWidth(2f), + PropertyFactory.lineDasharray(arrayOf(4f, 2f)), + PropertyFactory.visibility("none") + ) + }) + } + + /** + * Updates the fishing bearing line from [lat]/[lon] toward [bearingDeg] for [lengthNm] nautical miles. + */ + fun updateFishingBearing(lat: Double, lon: Double, bearingDeg: Double, lengthNm: Double = 10.0) { + val end = destinationPoint(lat, lon, bearingDeg, lengthNm) + val line = Feature.fromGeometry( + LineString.fromLngLats(listOf( + Point.fromLngLat(lon, lat), + Point.fromLngLat(end.second, end.first) + )) + ) + fishingLineSource?.setGeoJson(FeatureCollection.fromFeature(line)) + fishingLineStyle?.getLayer(FISHING_LINE_LAYER_ID)?.setProperties( + PropertyFactory.visibility("visible") + ) + } + + private fun destinationPoint(lat: Double, lon: Double, bearingDeg: Double, distanceNm: Double): Pair { + val R = 3440.065 + val d = distanceNm / R + val bear = Math.toRadians(bearingDeg) + val lat1r = Math.toRadians(lat) + val lon1r = Math.toRadians(lon) + val lat2r = kotlin.math.asin(kotlin.math.sin(lat1r) * kotlin.math.cos(d) + kotlin.math.cos(lat1r) * kotlin.math.sin(d) * kotlin.math.cos(bear)) + val lon2r = lon1r + kotlin.math.atan2(kotlin.math.sin(bear) * kotlin.math.sin(d) * kotlin.math.cos(lat1r), kotlin.math.cos(d) - kotlin.math.sin(lat1r) * kotlin.math.sin(lat2r)) + return Pair(Math.toDegrees(lat2r), Math.toDegrees(lon2r)) + } + private fun createCirclePolygon(lat: Double, lon: Double, radiusMeters: Double): Polygon { val points = mutableListOf() val degreesBetweenPoints = 8 -- cgit v1.2.3