summaryrefslogtreecommitdiff
path: root/android-app/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt41
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt48
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt132
-rw-r--r--android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml104
4 files changed, 303 insertions, 22 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 06c45ca..de1f4dd 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
@@ -30,10 +30,6 @@ import java.util.Locale
import org.maplibre.android.MapLibre
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.Style
-import org.maplibre.android.style.layers.PropertyFactory
-import org.maplibre.android.style.layers.RasterLayer
-import org.maplibre.android.style.sources.RasterSource
-import org.maplibre.android.style.sources.TileSet
import org.terst.nav.ui.*
import org.terst.nav.ui.doc.DocFragment
import org.terst.nav.ui.safety.SafetyFragment
@@ -48,6 +44,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
private var instrumentHandler: InstrumentHandler? = null
private var mapHandler: MapHandler? = null
private val loadedStyleFlow = MutableStateFlow<Style?>(null)
+ private lateinit var layerManager: MapLayerManager
private lateinit var bottomSheetBehavior: BottomSheetBehavior<View>
private lateinit var fragmentContainer: FrameLayout
@@ -265,9 +262,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
}
private fun setupMap() {
+ layerManager = MapLayerManager(this)
mapView = findViewById(R.id.mapView)
if (NavApplication.isTesting) return
-
+
mapView?.onCreate(null)
mapView?.getMapAsync { maplibreMap ->
mapHandler = MapHandler(maplibreMap)
@@ -282,29 +280,28 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
}
}
}
- val style = Style.Builder()
- .fromUri("https://tiles.openfreemap.org/styles/bright") // Base for labels if needed, or use satellite only
- .withSource(RasterSource("satellite-source",
- TileSet("2.2.0", "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}"), 256))
- .withLayer(RasterLayer("satellite-layer", "satellite-source"))
- .withSource(RasterSource("wind-source",
- TileSet("2.2.0", "https://tile.openweathermap.org/map/wind_new/{z}/{x}/{y}.png?appid=ae2a038149aa0900d1bc74160aa2a37e"), 256))
- .withLayer(RasterLayer("wind-layer", "wind-source").apply {
- setProperties(PropertyFactory.rasterOpacity(0.6f))
- })
- .withSource(RasterSource("openseamap-source",
- TileSet("2.2.0", "https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png").also {
- it.setMaxZoom(18f)
- }, 256))
- .withLayer(RasterLayer("openseamap-layer", "openseamap-source"))
-
- maplibreMap.setStyle(style) { style ->
+
+ val styleBuilder = Style.Builder()
+ .fromUri("https://tiles.openfreemap.org/styles/bright")
+ layerManager.addToStyleBuilder(styleBuilder)
+
+ maplibreMap.setStyle(styleBuilder) { style ->
loadedStyleFlow.value = style
val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor)
val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow)
val userBitmap = rasterizeDrawable(R.drawable.ic_ship_arrow)
mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap, userBitmap)
}
+
+ maplibreMap.addOnMapLongClickListener { _ ->
+ val currentStyle = loadedStyleFlow.value ?: return@addOnMapLongClickListener true
+ LayerPickerSheet(
+ manager = layerManager,
+ onBaseChanged = { preset -> layerManager.setBasePreset(currentStyle, preset) },
+ onWindChanged = { enabled -> layerManager.setWindEnabled(currentStyle, enabled) }
+ ).show(supportFragmentManager, "layer_picker")
+ true
+ }
}
}
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
new file mode 100644
index 0000000..48dc808
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt
@@ -0,0 +1,48 @@
+package org.terst.nav.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.chip.ChipGroup
+import com.google.android.material.switchmaterial.SwitchMaterial
+import org.terst.nav.R
+
+class LayerPickerSheet(
+ private val manager: MapLayerManager,
+ private val onBaseChanged: (MapBasePreset) -> Unit,
+ private val onWindChanged: (Boolean) -> Unit
+) : BottomSheetDialogFragment() {
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ inflater.inflate(R.layout.layout_layer_picker_sheet, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val chipGroup = view.findViewById<ChipGroup>(R.id.chip_group_base)
+ val windSwitch = view.findViewById<SwitchMaterial>(R.id.switch_wind)
+
+ // Set initial state from manager
+ val chipId = when (manager.basePreset) {
+ MapBasePreset.SATELLITE -> R.id.chip_satellite
+ MapBasePreset.CHARTS -> R.id.chip_charts
+ MapBasePreset.HYBRID -> R.id.chip_hybrid
+ }
+ chipGroup.check(chipId)
+ windSwitch.isChecked = manager.windEnabled
+
+ chipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
+ val preset = when (checkedIds.firstOrNull()) {
+ R.id.chip_satellite -> MapBasePreset.SATELLITE
+ R.id.chip_charts -> MapBasePreset.CHARTS
+ R.id.chip_hybrid -> MapBasePreset.HYBRID
+ else -> return@setOnCheckedStateChangeListener
+ }
+ onBaseChanged(preset)
+ }
+
+ windSwitch.setOnCheckedChangeListener { _, checked ->
+ onWindChanged(checked)
+ }
+ }
+}
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
new file mode 100644
index 0000000..cf78ec2
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt
@@ -0,0 +1,132 @@
+package org.terst.nav.ui
+
+import android.content.Context
+import org.maplibre.android.maps.Style
+import org.maplibre.android.style.layers.PropertyFactory
+import org.maplibre.android.style.layers.RasterLayer
+import org.maplibre.android.style.sources.RasterSource
+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.
+ *
+ * All sources are registered at style-build time; this class only toggles
+ * layer visibility — no runtime source swapping required.
+ */
+class MapLayerManager(context: Context) {
+
+ private val prefs = context.getSharedPreferences("map_layers", Context.MODE_PRIVATE)
+
+ var basePreset: MapBasePreset = loadBasePreset()
+ private set
+
+ var windEnabled: Boolean = prefs.getBoolean(KEY_WIND, true)
+ 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"
+
+ private const val KEY_BASE = "base_preset"
+ private const val KEY_WIND = "wind_enabled"
+
+ // Tile URLs
+ private const val URL_SATELLITE =
+ "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}"
+ private const val URL_CHARTS =
+ "https://tileservice.charts.noaa.gov/tiles/50000_1/{z}/{x}/{y}.png"
+ private const val URL_WIND =
+ "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"
+ }
+
+ /**
+ * 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.
+ */
+ fun addToStyleBuilder(builder: Style.Builder) {
+ // ── Sources ───────────────────────────────────────────────────────────
+ builder.withSource(RasterSource(SOURCE_SATELLITE,
+ TileSet("2.2.0", URL_SATELLITE), 256))
+
+ builder.withSource(RasterSource(SOURCE_CHARTS,
+ TileSet("2.2.0", URL_CHARTS), 256))
+
+ builder.withSource(RasterSource(SOURCE_WIND,
+ TileSet("2.2.0", URL_WIND), 256))
+
+ builder.withSource(RasterSource(SOURCE_SEAMARKS,
+ TileSet("2.2.0", URL_SEAMARKS).also { it.setMaxZoom(18f) }, 256))
+
+ // ── Layers (bottom → top within raster stack) ─────────────────────────
+ builder.withLayer(RasterLayer(LAYER_SATELLITE, SOURCE_SATELLITE).apply {
+ setProperties(PropertyFactory.rasterOpacity(1f))
+ setProperties(PropertyFactory.visibility(visibilityFor(basePreset, LAYER_SATELLITE)))
+ })
+
+ builder.withLayer(RasterLayer(LAYER_CHARTS, SOURCE_CHARTS).apply {
+ setProperties(PropertyFactory.rasterOpacity(opacityFor(basePreset)))
+ setProperties(PropertyFactory.visibility(visibilityFor(basePreset, LAYER_CHARTS)))
+ })
+
+ builder.withLayer(RasterLayer(LAYER_WIND, SOURCE_WIND).apply {
+ setProperties(PropertyFactory.rasterOpacity(0.6f))
+ setProperties(PropertyFactory.visibility(if (windEnabled) "visible" else "none"))
+ })
+
+ builder.withLayer(RasterLayer(LAYER_SEAMARKS, SOURCE_SEAMARKS).apply {
+ setProperties(PropertyFactory.visibility("visible"))
+ })
+ }
+
+ /** Apply a new base preset to a live style. */
+ fun setBasePreset(style: Style, preset: MapBasePreset) {
+ basePreset = preset
+ prefs.edit().putString(KEY_BASE, preset.name).apply()
+
+ style.getLayer(LAYER_SATELLITE)?.setProperties(
+ PropertyFactory.visibility(visibilityFor(preset, LAYER_SATELLITE)))
+ style.getLayer(LAYER_CHARTS)?.let {
+ it.setProperties(PropertyFactory.visibility(visibilityFor(preset, LAYER_CHARTS)))
+ (it as? RasterLayer)?.setProperties(PropertyFactory.rasterOpacity(opacityFor(preset)))
+ }
+ }
+
+ /** 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) {
+ LAYER_SATELLITE -> if (preset == MapBasePreset.SATELLITE || preset == MapBasePreset.HYBRID) "visible" else "none"
+ LAYER_CHARTS -> if (preset == MapBasePreset.CHARTS || preset == MapBasePreset.HYBRID) "visible" else "none"
+ else -> "visible"
+ }
+
+ private fun opacityFor(preset: MapBasePreset): Float =
+ if (preset == MapBasePreset.HYBRID) 0.75f else 1f
+
+ private fun loadBasePreset(): MapBasePreset =
+ prefs.getString(KEY_BASE, null)?.let {
+ runCatching { MapBasePreset.valueOf(it) }.getOrNull()
+ } ?: MapBasePreset.SATELLITE
+}
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
new file mode 100644
index 0000000..c424606
--- /dev/null
+++ b/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:paddingBottom="32dp"
+ android:background="?attr/colorSurface">
+
+ <View
+ android:layout_width="36dp"
+ android:layout_height="4dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="20dp"
+ android:background="@color/md_theme_outline" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:text="MAP LAYERS"
+ android:textSize="11sp"
+ android:textAllCaps="true"
+ android:letterSpacing="0.12"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/instrument_text_secondary" />
+
+ <!-- Base map selection -->
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/chip_group_base"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="24dp"
+ app:singleSelection="true"
+ app:selectionRequired="true"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_satellite"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Satellite" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_charts"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Charts" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_hybrid"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Hybrid" />
+
+ </com.google.android.material.chip.ChipGroup>
+
+ <!-- Wind divider -->
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginBottom="16dp"
+ android:background="@color/md_theme_surfaceVariant" />
+
+ <!-- Wind toggle -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <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="Wind overlay"
+ android:textSize="15sp"
+ android:textColor="@color/instrument_text_normal" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="OpenWeatherMap wind speed"
+ android:textSize="12sp"
+ android:textColor="@color/instrument_text_secondary" />
+ </LinearLayout>
+
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/switch_wind"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
+</LinearLayout>