summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-04-08 22:43:05 +0000
committerClaude <noreply@anthropic.com>2026-04-08 22:43:05 +0000
commit4fcd0d62e562032d238d3667e142045832292248 (patch)
treebea78ce2a5edfa476de22b2116925dff5bec8092
parent5416c8ccc9220bc36e7af3febcbdd9d86e88cf30 (diff)
feat(wind): Level 0 — single wind arrow at GPS position
- 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
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt12
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt43
2 files changed, 55 insertions, 0 deletions
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<Style?>(null)
+ private var weatherLoaded = false
private lateinit var bottomSheetBehavior: BottomSheetBehavior<View>
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.
@@ -135,6 +142,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.
*/
fun updateTrackLayer(style: Style, points: List<TrackPoint>) {