From 9ec663b4aa79c48829cc3796be90dc130fa5f097 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 1 Jul 2026 06:37:01 +0000 Subject: feat(layers): ATON CircleLayer + TSS FillLayer in MapHandler --- .../src/main/kotlin/org/terst/nav/ui/MapHandler.kt | 103 +++++++++++++++++++++ 1 file changed, 103 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 6d408ed..02c9fca 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 @@ -1,5 +1,6 @@ package org.terst.nav.ui +import android.content.Context import android.graphics.Bitmap import org.maplibre.android.camera.CameraPosition import org.maplibre.android.camera.CameraUpdateFactory @@ -9,6 +10,7 @@ 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.FillLayer import org.maplibre.android.style.layers.LineLayer import org.maplibre.android.style.layers.PropertyFactory import org.maplibre.android.style.layers.RasterLayer @@ -21,6 +23,7 @@ import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.LineString import org.maplibre.geojson.Point import org.maplibre.geojson.Polygon +import org.terst.nav.R import org.terst.nav.safety.AnchorWatchState import org.terst.nav.TidalCurrentState import org.terst.nav.data.model.WindArrow @@ -96,6 +99,11 @@ class MapHandler(private val maplibreMap: MapLibreMap) { private var fadSource: GeoJsonSource? = null + private val ATON_SOURCE_ID = "aton-source" + private val ATON_LAYER_ID = "aton-layer" + private val ATON_LABELS_ID = "aton-labels" + private var atonSource: 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 @@ -489,6 +497,101 @@ class MapHandler(private val maplibreMap: MapLibreMap) { fadSource?.setGeoJson(FeatureCollection.fromFeatures(features)) } + /** + * Initialises the ATON (Aid to Navigation) layer. + * Renders ATONs as colored circles with name labels at zoom 12+. + * Call once after style loads, then call [updateAtonLayer] to populate. + */ + fun setupAtonLayer(style: Style, visible: Boolean) { + val src = GeoJsonSource(ATON_SOURCE_ID) + atonSource = src + style.addSource(src) + val v = if (visible) "visible" else "none" + style.addLayer(CircleLayer(ATON_LAYER_ID, ATON_SOURCE_ID).apply { + setProperties( + PropertyFactory.circleColor(Expression.get("color")), + PropertyFactory.circleRadius(4f), + PropertyFactory.circleStrokeColor("rgba(0,0,0,0.6)"), + PropertyFactory.circleStrokeWidth(1f), + PropertyFactory.visibility(v) + ) + }) + style.addLayer(SymbolLayer(ATON_LABELS_ID, ATON_SOURCE_ID).apply { + setProperties( + PropertyFactory.textField(Expression.get("name")), + PropertyFactory.textSize(9f), + PropertyFactory.textOffset(arrayOf(0f, -1.2f)), + PropertyFactory.textColor("rgba(255,255,255,1)"), + PropertyFactory.textHaloColor("rgba(0,0,0,0.7)"), + PropertyFactory.textHaloWidth(1f), + PropertyFactory.textAllowOverlap(false), + PropertyFactory.visibility(v), + PropertyFactory.textOpacity( + Expression.step(Expression.zoom(), Expression.literal(0f), + Expression.stop(12.0, 1f)) + ) + ) + }) + } + + /** Replaces ATON markers from a list of [Feature] points. */ + fun updateAtonLayer(features: List) { + atonSource?.setGeoJson(FeatureCollection.fromFeatures(features)) + } + + /** Shows or hides the ATON layer (and its label layer) on a live style. */ + fun setAtonLayerVisible(style: Style, visible: Boolean) { + val v = if (visible) "visible" else "none" + style.getLayer(ATON_LAYER_ID)?.setProperties(PropertyFactory.visibility(v)) + style.getLayer(ATON_LABELS_ID)?.setProperties(PropertyFactory.visibility(v)) + } + + /** + * Initialises the TSS (Traffic Separation Scheme) layer from the bundled GeoJSON asset. + * Renders traffic lanes as green (inbound) / red (outbound) fills and the separation + * zone as a yellow fill. Call once after style loads. + */ + fun setupTssLayer(style: Style, context: Context, visible: Boolean) { + val json = context.resources.openRawResource(R.raw.pacific_tss) + .bufferedReader().readText() + val src = GeoJsonSource("tss-source", FeatureCollection.fromJson(json)) + style.addSource(src) + val v = if (visible) "visible" else "none" + style.addLayer(FillLayer("tss-lanes-layer", "tss-source").apply { + setFilter(Expression.neq( + Expression.get("type"), Expression.literal("separation_zone"))) + setProperties( + PropertyFactory.fillColor(Expression.switchCase( + Expression.eq(Expression.get("type"), Expression.literal("lane_inbound")), + Expression.literal("rgba(0,180,0,0.12)"), + Expression.literal("rgba(200,0,0,0.12)") + )), + PropertyFactory.fillOutlineColor(Expression.switchCase( + Expression.eq(Expression.get("type"), Expression.literal("lane_inbound")), + Expression.literal("rgba(0,180,0,0.5)"), + Expression.literal("rgba(200,0,0,0.5)") + )), + PropertyFactory.visibility(v) + ) + }) + style.addLayer(FillLayer("tss-sep-layer", "tss-source").apply { + setFilter(Expression.eq( + Expression.get("type"), Expression.literal("separation_zone"))) + setProperties( + PropertyFactory.fillColor("rgba(255,200,0,0.25)"), + PropertyFactory.fillOutlineColor("rgba(255,200,0,0.7)"), + PropertyFactory.visibility(v) + ) + }) + } + + /** Shows or hides the TSS lanes and separation zone layers on a live style. */ + fun setTssLayerVisible(style: Style, visible: Boolean) { + val v = if (visible) "visible" else "none" + style.getLayer("tss-lanes-layer")?.setProperties(PropertyFactory.visibility(v)) + style.getLayer("tss-sep-layer")?.setProperties(PropertyFactory.visibility(v)) + } + /** Clears all tack markers. */ fun clearTackLayer() { tackSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList())) -- cgit v1.2.3