summaryrefslogtreecommitdiff
path: root/android-app/app
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt18
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt18
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt63
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt100
-rw-r--r--android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml122
5 files changed, 287 insertions, 34 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 476fb0d..ea45c36 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
@@ -388,9 +388,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
private fun buildLayerPickerSheet(): LayerPickerSheet {
val currentStyle = loadedStyleFlow.value
return LayerPickerSheet(
- manager = layerManager,
- onBaseChanged = { preset -> currentStyle?.let { layerManager.setBasePreset(it, preset) } },
- onWindChanged = { enabled -> currentStyle?.let { layerManager.setWindEnabled(it, enabled) } },
+ manager = layerManager,
+ onBaseChanged = { preset -> currentStyle?.let { layerManager.setBasePreset(it, preset) } },
+ onWindChanged = { enabled -> currentStyle?.let { layerManager.setWindEnabled(it, enabled) } },
onParticlesChanged = { enabled ->
layerManager.setParticlesEnabled(enabled)
particleWindView?.visibility = if (enabled) View.VISIBLE else View.GONE
@@ -401,8 +401,15 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
val past = if (enabled) viewModel.pastTracks.value else emptyList()
mapHandler?.updateTrackLayer(style, viewModel.trackPoints.value, past)
},
- unitPrefs = unitPrefs,
- onUnitsChanged = { refreshUnits() }
+ onSeamarksChanged = { enabled -> currentStyle?.let { layerManager.setSeamarksEnabled(it, enabled) } },
+ onDepthChanged = { enabled -> currentStyle?.let { layerManager.setDepthEnabled(it, enabled) } },
+ onFadsChanged = { enabled ->
+ layerManager.setFadsEnabled(enabled)
+ val style = loadedStyleFlow.value ?: return@LayerPickerSheet
+ mapHandler?.setFadLayerVisible(style, enabled)
+ },
+ unitPrefs = unitPrefs,
+ onUnitsChanged = { refreshUnits() }
)
}
@@ -657,6 +664,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
val windArrowBitmap = rasterizeDrawable(R.drawable.ic_wind_arrow)
mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap, userBitmap)
mapHandler?.setupWindLayer(style, windArrowBitmap)
+ mapHandler?.setupFadLayer(style, layerManager.fadsEnabled)
// Eagerly load conditions at style-ready time so the HUD is never blank
// on startup — the camera-idle may fire before GPS fixes, this is the backstop.
maplibreMap.cameraPosition.target?.let { center ->
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt
index bd84961..a9dcca8 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt
@@ -20,6 +20,9 @@ class LayerPickerSheet(
private val onWindChanged: (Boolean) -> Unit,
private val onParticlesChanged: (Boolean) -> Unit,
private val onPastTracksChanged: (Boolean) -> Unit,
+ private val onSeamarksChanged: (Boolean) -> Unit,
+ private val onDepthChanged: (Boolean) -> Unit,
+ private val onFadsChanged: (Boolean) -> Unit,
private val unitPrefs: UnitPrefs,
private val onUnitsChanged: () -> Unit
) : BottomSheetDialogFragment() {
@@ -60,6 +63,21 @@ class LayerPickerSheet(
pastTracksSwitch.isChecked = manager.pastTracksEnabled
pastTracksSwitch.setOnCheckedChangeListener { _, checked -> onPastTracksChanged(checked) }
+ // ── Seamarks switch ───────────────────────────────────────────────────
+ val seamarksSwitch = view.findViewById<SwitchMaterial>(R.id.switch_seamarks)
+ seamarksSwitch.isChecked = manager.seamarksEnabled
+ seamarksSwitch.setOnCheckedChangeListener { _, checked -> onSeamarksChanged(checked) }
+
+ // ── Depth contours switch ─────────────────────────────────────────────
+ val depthSwitch = view.findViewById<SwitchMaterial>(R.id.switch_depth)
+ depthSwitch.isChecked = manager.depthEnabled
+ depthSwitch.setOnCheckedChangeListener { _, checked -> onDepthChanged(checked) }
+
+ // ── FADs switch ───────────────────────────────────────────────────────
+ val fadsSwitch = view.findViewById<SwitchMaterial>(R.id.switch_fads)
+ fadsSwitch.isChecked = manager.fadsEnabled
+ fadsSwitch.setOnCheckedChangeListener { _, checked -> onFadsChanged(checked) }
+
// ── Temperature chips ─────────────────────────────────────────────────
val chipTemp = view.findViewById<ChipGroup>(R.id.chip_group_temp)
chipTemp.check(when (unitPrefs.tempUnit) {
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 a3c1eac..99c8ffc 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
@@ -90,6 +90,12 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
private val TACK_SOURCE_ID = "tack-source"
private val TACK_LAYER_ID = "tack-layer"
+ private val FAD_SOURCE_ID = "fad-source"
+ private val FAD_LAYER_ID = "fad-layer"
+ private val FAD_LABEL_LAYER_ID = "fad-label-layer"
+
+ private var fadSource: GeoJsonSource? = null
+
private var anchorPointSource: GeoJsonSource? = null
private var anchorCircleSource: GeoJsonSource? = null
private var tidalCurrentSource: GeoJsonSource? = null
@@ -421,6 +427,63 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
tackSource?.setGeoJson(FeatureCollection.fromFeatures(features))
}
+ /**
+ * Initialises the FAD (Fish Aggregating Device) layer.
+ * Renders FADs as orange circles with ID labels at zoom 10+.
+ * Call once after style loads, then call [updateFadLayer] to populate.
+ */
+ fun setupFadLayer(style: Style, visible: Boolean) {
+ fadSource = GeoJsonSource(FAD_SOURCE_ID)
+ style.addSource(fadSource!!)
+
+ style.addLayer(CircleLayer(FAD_LAYER_ID, FAD_SOURCE_ID).apply {
+ setProperties(
+ PropertyFactory.circleColor("rgba(255, 140, 0, 0.85)"),
+ PropertyFactory.circleRadius(6f),
+ PropertyFactory.circleStrokeColor("rgba(200, 100, 0, 1)"),
+ PropertyFactory.circleStrokeWidth(1.5f),
+ PropertyFactory.visibility(if (visible) "visible" else "none")
+ )
+ })
+
+ style.addLayer(SymbolLayer(FAD_LABEL_LAYER_ID, FAD_SOURCE_ID).apply {
+ setProperties(
+ PropertyFactory.textField(Expression.get("id")),
+ PropertyFactory.textSize(10f),
+ PropertyFactory.textOffset(arrayOf(0f, -1.4f)),
+ PropertyFactory.textColor("rgba(255, 255, 255, 1)"),
+ PropertyFactory.textHaloColor("rgba(0, 0, 0, 0.8)"),
+ PropertyFactory.textHaloWidth(1.2f),
+ PropertyFactory.textAllowOverlap(false),
+ PropertyFactory.visibility(if (visible) "visible" else "none"),
+ // Only show labels at zoom 10+ to avoid clutter
+ PropertyFactory.textOpacity(
+ Expression.step(
+ Expression.zoom(),
+ Expression.literal(0f),
+ Expression.stop(10.0, 1f)
+ )
+ )
+ )
+ })
+ }
+
+ /** Shows or hides the FAD layer (and its label layer) on a live style. */
+ fun setFadLayerVisible(style: Style, visible: Boolean) {
+ val v = if (visible) "visible" else "none"
+ style.getLayer(FAD_LAYER_ID)?.setProperties(PropertyFactory.visibility(v))
+ style.getLayer(FAD_LABEL_LAYER_ID)?.setProperties(PropertyFactory.visibility(v))
+ }
+
+ /**
+ * Replaces FAD markers from a list of [Feature] points.
+ * Each feature must have a string property "id" (e.g. "K-15") and
+ * optionally "name" (e.g. "Kona 15") and "depth_m" (number).
+ */
+ fun updateFadLayer(features: List<Feature>) {
+ fadSource?.setGeoJson(FeatureCollection.fromFeatures(features))
+ }
+
/** Clears all tack markers. */
fun clearTackLayer() {
tackSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList()))
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt
index 1c46248..33072c4 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt
@@ -10,10 +10,13 @@ import org.maplibre.android.style.sources.TileSet
enum class MapBasePreset { SATELLITE, CHARTS, HYBRID }
/**
- * Manages the raster layer stack: base imagery (satellite / NOAA charts / hybrid)
- * and the wind overlay. Persists selections across sessions.
+ * Manages the raster layer stack: base imagery (satellite / NOAA charts / hybrid),
+ * wind overlay, OpenSeaMap seamarks, and depth contours.
*
- * All sources are registered at style-build time; this class only toggles
+ * FADs are a vector (GeoJSON) layer managed by MapHandler — this class only
+ * stores the `fadsEnabled` preference so LayerPickerSheet and MainActivity can read it.
+ *
+ * All raster sources are registered at style-build time; this class toggles
* layer visibility — no runtime source swapping required.
*/
class MapLayerManager(context: Context) {
@@ -23,32 +26,41 @@ class MapLayerManager(context: Context) {
var basePreset: MapBasePreset = loadBasePreset()
private set
- var windEnabled: Boolean = prefs.getBoolean(KEY_WIND, true)
+ var windEnabled: Boolean = prefs.getBoolean(KEY_WIND, true)
private set
-
- var particlesEnabled: Boolean = prefs.getBoolean(KEY_PARTICLES, true)
+ var particlesEnabled: Boolean = prefs.getBoolean(KEY_PARTICLES, true)
private set
-
var pastTracksEnabled: Boolean = prefs.getBoolean(KEY_PAST_TRACKS, true)
private set
+ var seamarksEnabled: Boolean = prefs.getBoolean(KEY_SEAMARKS, true)
+ private set
+ var depthEnabled: Boolean = prefs.getBoolean(KEY_DEPTH, false)
+ private set
+ var fadsEnabled: Boolean = prefs.getBoolean(KEY_FADS, false)
+ private set
// ── Source / Layer IDs ────────────────────────────────────────────────────
companion object {
- const val SOURCE_SATELLITE = "satellite-source"
- const val SOURCE_CHARTS = "charts-source"
- const val SOURCE_WIND = "wind-source"
- const val SOURCE_SEAMARKS = "openseamap-source"
-
- const val LAYER_SATELLITE = "satellite-layer"
- const val LAYER_CHARTS = "charts-layer"
- const val LAYER_WIND = "wind-layer"
- const val LAYER_SEAMARKS = "openseamap-layer"
+ const val SOURCE_SATELLITE = "satellite-source"
+ const val SOURCE_CHARTS = "charts-source"
+ const val SOURCE_WIND = "wind-source"
+ const val SOURCE_SEAMARKS = "openseamap-source"
+ const val SOURCE_DEPTH = "osm-depth-source"
+
+ const val LAYER_SATELLITE = "satellite-layer"
+ const val LAYER_CHARTS = "charts-layer"
+ const val LAYER_WIND = "wind-layer"
+ const val LAYER_SEAMARKS = "openseamap-layer"
+ const val LAYER_DEPTH = "osm-depth-layer"
private const val KEY_BASE = "base_preset"
private const val KEY_WIND = "wind_enabled"
private const val KEY_PARTICLES = "particles_enabled"
private const val KEY_PAST_TRACKS = "past_tracks_enabled"
+ private const val KEY_SEAMARKS = "seamarks_enabled"
+ private const val KEY_DEPTH = "depth_enabled"
+ private const val KEY_FADS = "fads_enabled"
// Tile URLs
private const val URL_SATELLITE =
@@ -59,12 +71,15 @@ class MapLayerManager(context: Context) {
"https://tile.openweathermap.org/map/wind_new/{z}/{x}/{y}.png?appid=ae2a038149aa0900d1bc74160aa2a37e"
private const val URL_SEAMARKS =
"https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png"
+ // Depth soundings + contours from OpenSeaMap (zoom 3–18, coverage varies by region)
+ private const val URL_DEPTH =
+ "https://tiles.openseamap.org/depth/{z}/{x}/{y}.png"
}
/**
- * Registers all raster sources and layers into the [Style.Builder].
- * Call this before [Style.Builder.build]. Layers are added in the correct
- * order; MapHandler's vector layers go on top after style loads.
+ * Registers all raster sources and layers into [Style.Builder].
+ * Layer order (bottom → top): satellite → charts → wind → depth → seamarks.
+ * MapHandler's vector layers go on top after the style loads.
*/
fun addToStyleBuilder(builder: Style.Builder) {
// ── Sources ───────────────────────────────────────────────────────────
@@ -80,6 +95,9 @@ class MapLayerManager(context: Context) {
builder.withSource(RasterSource(SOURCE_SEAMARKS,
TileSet("2.2.0", URL_SEAMARKS).also { it.setMaxZoom(18f) }, 256))
+ builder.withSource(RasterSource(SOURCE_DEPTH,
+ TileSet("2.2.0", URL_DEPTH).also { it.setMinZoom(3f); it.setMaxZoom(18f) }, 256))
+
// ── Layers (bottom → top within raster stack) ─────────────────────────
builder.withLayer(RasterLayer(LAYER_SATELLITE, SOURCE_SATELLITE).apply {
setProperties(PropertyFactory.rasterOpacity(1f))
@@ -96,8 +114,14 @@ class MapLayerManager(context: Context) {
setProperties(PropertyFactory.visibility(if (windEnabled) "visible" else "none"))
})
+ // Depth contours sit below seamarks so soundings don't obscure nav aid symbols
+ builder.withLayer(RasterLayer(LAYER_DEPTH, SOURCE_DEPTH).apply {
+ setProperties(PropertyFactory.rasterOpacity(0.85f))
+ setProperties(PropertyFactory.visibility(if (depthEnabled) "visible" else "none"))
+ })
+
builder.withLayer(RasterLayer(LAYER_SEAMARKS, SOURCE_SEAMARKS).apply {
- setProperties(PropertyFactory.visibility("visible"))
+ setProperties(PropertyFactory.visibility(if (seamarksEnabled) "visible" else "none"))
})
}
@@ -114,7 +138,33 @@ class MapLayerManager(context: Context) {
}
}
- /** Toggle wind particles (Canvas overlay — no style interaction needed). */
+ fun setWindEnabled(style: Style, enabled: Boolean) {
+ windEnabled = enabled
+ prefs.edit().putBoolean(KEY_WIND, enabled).apply()
+ style.getLayer(LAYER_WIND)?.setProperties(
+ PropertyFactory.visibility(if (enabled) "visible" else "none"))
+ }
+
+ fun setSeamarksEnabled(style: Style, enabled: Boolean) {
+ seamarksEnabled = enabled
+ prefs.edit().putBoolean(KEY_SEAMARKS, enabled).apply()
+ style.getLayer(LAYER_SEAMARKS)?.setProperties(
+ PropertyFactory.visibility(if (enabled) "visible" else "none"))
+ }
+
+ fun setDepthEnabled(style: Style, enabled: Boolean) {
+ depthEnabled = enabled
+ prefs.edit().putBoolean(KEY_DEPTH, enabled).apply()
+ style.getLayer(LAYER_DEPTH)?.setProperties(
+ PropertyFactory.visibility(if (enabled) "visible" else "none"))
+ }
+
+ /** FADs are a GeoJSON vector layer in MapHandler; this just persists the pref. */
+ fun setFadsEnabled(enabled: Boolean) {
+ fadsEnabled = enabled
+ prefs.edit().putBoolean(KEY_FADS, enabled).apply()
+ }
+
fun setParticlesEnabled(enabled: Boolean) {
particlesEnabled = enabled
prefs.edit().putBoolean(KEY_PARTICLES, enabled).apply()
@@ -125,14 +175,6 @@ class MapLayerManager(context: Context) {
prefs.edit().putBoolean(KEY_PAST_TRACKS, enabled).apply()
}
- /** Toggle wind overlay on a live style. */
- fun setWindEnabled(style: Style, enabled: Boolean) {
- windEnabled = enabled
- prefs.edit().putBoolean(KEY_WIND, enabled).apply()
- style.getLayer(LAYER_WIND)?.setProperties(
- PropertyFactory.visibility(if (enabled) "visible" else "none"))
- }
-
// ── Helpers ───────────────────────────────────────────────────────────────
private fun visibilityFor(preset: MapBasePreset, layerId: String): String = when (layerId) {
diff --git a/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml b/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml
index 2e6f982..065b720 100644
--- a/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml
+++ b/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml
@@ -176,6 +176,128 @@
</LinearLayout>
+ <!-- ── OCEAN LAYERS ───────────────────────────────────────── -->
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="16dp"
+ android:background="@color/md_theme_surfaceVariant" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="12dp"
+ android:text="OCEAN LAYERS"
+ android:textSize="11sp"
+ android:textAllCaps="true"
+ android:letterSpacing="0.12"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/instrument_text_secondary" />
+
+ <!-- Seamarks / nav aids -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="12dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Seamarks / nav aids"
+ android:textSize="15sp"
+ android:textColor="@color/instrument_text_normal" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Buoys, lights, hazards (OpenSeaMap)"
+ android:textSize="12sp"
+ android:textColor="@color/instrument_text_secondary" />
+ </LinearLayout>
+
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/switch_seamarks"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
+ <!-- Depth contours -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="12dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Depth contours"
+ android:textSize="15sp"
+ android:textColor="@color/instrument_text_normal" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Soundings &amp; contour lines (OpenSeaMap)"
+ android:textSize="12sp"
+ android:textColor="@color/instrument_text_secondary" />
+ </LinearLayout>
+
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/switch_depth"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
+ <!-- FADs -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="8dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="FADs"
+ android:textSize="15sp"
+ android:textColor="@color/instrument_text_normal" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Fish aggregating devices (Hawaii DLNR)"
+ android:textSize="12sp"
+ android:textColor="@color/instrument_text_secondary" />
+ </LinearLayout>
+
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/switch_fads"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
<!-- ── UNITS ───────────────────────────────────────────────── -->
<View