From 4fcd0d62e562032d238d3667e142045832292248 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 22:43:05 +0000 Subject: feat(wind): Level 0 — single wind arrow at GPS position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MapHandler.setupWindLayer: registers ic_wind_arrow bitmap, adds GeoJSON source + SymbolLayer with rotation/size driven by direction/speed_kt props - MapHandler.updateWindLayer(arrow): pushes updated feature to the source - MainActivity: calls setupWindLayer after style load; fires loadWeather once on first GPS fix (weatherLoaded flag); observes windArrow StateFlow to drive updateWindLayer https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 12 ++++++ .../src/main/kotlin/org/terst/nav/ui/MapHandler.kt | 43 ++++++++++++++++++++++ 2 files changed, 55 insertions(+) (limited to 'android-app/app/src/main') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index f9d4dbd..5950844 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -49,6 +49,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private var mapHandler: MapHandler? = null private var anchorWatchHandler: AnchorWatchHandler? = null private val loadedStyleFlow = MutableStateFlow(null) + private var weatherLoaded = false private lateinit var bottomSheetBehavior: BottomSheetBehavior private lateinit var fragmentContainer: FrameLayout @@ -244,7 +245,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { loadedStyleFlow.value = style val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor) val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow) + val windArrowBitmap = rasterizeDrawable(R.drawable.ic_wind_arrow) mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap) + mapHandler?.setupWindLayer(style, windArrowBitmap) } } } @@ -255,6 +258,15 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude) val sogKnots = gpsData.speedOverGround * 1.94384 viewModel.addGpsPoint(gpsData.latitude, gpsData.longitude, sogKnots, gpsData.courseOverGround.toDouble()) + if (!weatherLoaded) { + weatherLoaded = true + viewModel.loadWeather(gpsData.latitude, gpsData.longitude) + } + } + } + lifecycleScope.launch { + viewModel.windArrow.collect { arrow -> + if (arrow != null) mapHandler?.updateWindLayer(arrow) } } lifecycleScope.launch { 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 cbc2e90..cdf0c25 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 @@ -6,6 +6,7 @@ import org.maplibre.android.camera.CameraUpdateFactory import org.maplibre.android.geometry.LatLng import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.Style +import org.maplibre.android.style.expressions.Expression import org.maplibre.android.style.layers.CircleLayer import org.maplibre.android.style.layers.LineLayer import org.maplibre.android.style.layers.PropertyFactory @@ -21,6 +22,7 @@ import org.maplibre.geojson.Point import org.maplibre.geojson.Polygon import org.terst.nav.AnchorWatchState import org.terst.nav.TidalCurrentState +import org.terst.nav.data.model.WindArrow import org.terst.nav.track.TrackPoint import kotlin.math.cos import kotlin.math.sin @@ -43,10 +45,15 @@ class MapHandler(private val maplibreMap: MapLibreMap) { private val TRACK_SOURCE_ID = "track-source" private val TRACK_LAYER_ID = "track-line" + private val WIND_SOURCE_ID = "wind-source" + private val WIND_LAYER_ID = "wind-arrows" + private val WIND_ARROW_ICON_ID = "wind-arrow" + private var anchorPointSource: GeoJsonSource? = null private var anchorCircleSource: GeoJsonSource? = null private var tidalCurrentSource: GeoJsonSource? = null private var trackSource: GeoJsonSource? = null + private var windSource: GeoJsonSource? = null /** * Initializes map layers for anchor watch and tidal currents. @@ -134,6 +141,42 @@ class MapHandler(private val maplibreMap: MapLibreMap) { } } + /** + * Registers the wind-arrow bitmap in the style. Call once after style loads. + */ + fun setupWindLayer(style: Style, arrowBitmap: Bitmap) { + style.addImage(WIND_ARROW_ICON_ID, arrowBitmap) + windSource = GeoJsonSource(WIND_SOURCE_ID) + style.addSource(windSource!!) + style.addLayer(SymbolLayer(WIND_LAYER_ID, WIND_SOURCE_ID).apply { + setProperties( + PropertyFactory.iconImage(WIND_ARROW_ICON_ID), + PropertyFactory.iconRotate(Expression.get("direction")), + PropertyFactory.iconRotationAlignment("map"), + PropertyFactory.iconAllowOverlap(true), + PropertyFactory.iconSize( + Expression.interpolate( + Expression.linear(), + Expression.get("speed_kt"), + Expression.stop(0, 0.6f), + Expression.stop(30, 1.4f) + ) + ) + ) + }) + } + + /** + * Places or moves the single wind arrow at the GPS position. + */ + fun updateWindLayer(arrow: WindArrow) { + val feature = Feature.fromGeometry(Point.fromLngLat(arrow.lon, arrow.lat)).apply { + addNumberProperty("direction", arrow.directionDeg) + addNumberProperty("speed_kt", arrow.speedKt) + } + windSource?.setGeoJson(FeatureCollection.fromFeature(feature)) + } + /** * Updates the GPS track polyline on the map. Lazily initialises the layer on first call. */ -- cgit v1.2.3