summaryrefslogtreecommitdiff
path: root/android-app/app/src/main
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-14 00:46:14 +0000
committerClaudomator Agent <agent@claudomator>2026-03-14 00:46:14 +0000
commit9fb49aebfc01b5df68e67e97ee088318a3621c26 (patch)
tree21b5f4010ce9806689d96d5622954d876a935f6b /android-app/app/src/main
parentb78bdb5c8b858fdab3abd9b759fce6519c8ad5b0 (diff)
Implement tidal current overlay on the chart with toggle FAB
Diffstat (limited to 'android-app/app/src/main')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt38
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt89
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/TidalCurrentData.kt17
-rw-r--r--android-app/app/src/main/res/drawable/ic_tidal_arrow.xml9
-rw-r--r--android-app/app/src/main/res/layout/activity_main.xml12
5 files changed, 165 insertions, 0 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt
index 4b59139..22290a5 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt
@@ -61,6 +61,15 @@ class LocationService : Service() {
anchorAlarmManager = AnchorAlarmManager(this) // Initialize with service context
createNotificationChannel()
+ // Mock tidal current data generator
+ serviceScope.launch {
+ while (true) {
+ val currents = generateMockCurrents()
+ _tidalCurrentState.update { it.copy(currents = currents) }
+ kotlinx.coroutines.delay(60000) // Update every minute (or as needed)
+ }
+ }
+
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { location ->
@@ -132,6 +141,10 @@ class LocationService : Service() {
val radius = intent.getDoubleExtra(EXTRA_WATCH_RADIUS, AnchorWatchState.DEFAULT_WATCH_CIRCLE_RADIUS_METERS)
updateWatchCircleRadius(radius)
}
+ ACTION_TOGGLE_TIDAL_VISIBILITY -> {
+ val isVisible = intent.getBooleanExtra(EXTRA_TIDAL_VISIBILITY, false)
+ _tidalCurrentState.update { it.copy(isVisible = isVisible) }
+ }
}
return START_NOT_STICKY
}
@@ -234,21 +247,46 @@ class LocationService : Service() {
Log.d("AnchorWatch", "Watch circle radius updated to ${radiusMeters}m.")
}
+ private fun generateMockCurrents(): List<TidalCurrent> {
+ // Generate a grid of currents around common coordinates
+ val centerLat = 41.5
+ val centerLon = -71.3
+ val currents = mutableListOf<TidalCurrent>()
+ val currentTime = System.currentTimeMillis()
+
+ for (i in -5..5) {
+ for (j in -5..5) {
+ val lat = centerLat + i * 0.05
+ val lon = centerLon + j * 0.05
+ // Mock speed and direction based on position and time
+ val speed = 0.5 + Math.random() * 2.0 // 0.5 to 2.5 knots
+ val dir = (Math.sin(currentTime / 3600000.0 + lat + lon) * 180.0 + 180.0) % 360.0
+ currents.add(TidalCurrent(lat, lon, speed, dir, currentTime))
+ }
+ }
+ return currents
+ }
+
companion object {
const val ACTION_START_FOREGROUND_SERVICE = "ACTION_START_FOREGROUND_SERVICE"
const val ACTION_STOP_FOREGROUND_SERVICE = "ACTION_STOP_FOREGROUND_SERVICE"
const val ACTION_START_ANCHOR_WATCH = "ACTION_START_ANCHOR_WATCH"
const val ACTION_STOP_ANCHOR_WATCH = "ACTION_STOP_ANCHOR_WATCH"
const val ACTION_UPDATE_WATCH_RADIUS = "ACTION_UPDATE_WATCH_RADIUS"
+ const val ACTION_TOGGLE_TIDAL_VISIBILITY = "ACTION_TOGGLE_TIDAL_VISIBILITY"
const val EXTRA_WATCH_RADIUS = "extra_watch_radius"
+ const val EXTRA_TIDAL_VISIBILITY = "extra_tidal_visibility"
// Publicly accessible flows
val locationFlow: SharedFlow<GpsData>
get() = _locationFlow
val anchorWatchState: StateFlow<AnchorWatchState>
get() = _anchorWatchState
+ val tidalCurrentState: StateFlow<TidalCurrentState>
+ get() = _tidalCurrentState
private val _locationFlow = MutableSharedFlow<GpsData>(replay = 1)
private val _anchorWatchState = MutableStateFlow(AnchorWatchState())
+ private val _tidalCurrentState = MutableStateFlow(TidalCurrentState())
}
}
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 a32fb18..b638136 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
@@ -59,6 +59,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var instrumentDisplayContainer: ConstraintLayout
private lateinit var fabToggleInstruments: FloatingActionButton
private lateinit var fabMob: FloatingActionButton
+ private lateinit var fabTidal: FloatingActionButton
// MapLibreMap instance
private var maplibreMap: MapLibreMap? = null
@@ -73,6 +74,13 @@ class MainActivity : AppCompatActivity() {
private var anchorPointSource: GeoJsonSource? = null
private var anchorCircleSource: GeoJsonSource? = null
+ // MapLibre Layers and Sources for Tidal Current
+ private val TIDAL_CURRENT_SOURCE_ID = "tidal-current-source"
+ private val TIDAL_CURRENT_LAYER_ID = "tidal-current-layer"
+ private val TIDAL_ARROW_ICON_ID = "tidal-arrow-icon"
+
+ private var tidalCurrentSource: GeoJsonSource? = null
+
// MOB UI elements
private lateinit var mobNavigationContainer: ConstraintLayout
private lateinit var mobValueDistance: TextView
@@ -167,12 +175,15 @@ class MainActivity : AppCompatActivity() {
this.maplibreMap = maplibreMap // Assign to class member
maplibreMap.setStyle(Style.Builder().fromUri("https://tiles.openseamap.org/seamark/osm-bright/style.json")) { style ->
setupAnchorMapLayers(style)
+ setupTidalCurrentMapLayers(style)
+ observeTidalCurrentState() // Start observing tidal current state
}
}
instrumentDisplayContainer = findViewById(R.id.instrument_display_container)
fabToggleInstruments = findViewById(R.id.fab_toggle_instruments)
fabMob = findViewById(R.id.fab_mob)
+ fabTidal = findViewById(R.id.fab_tidal)
// Initialize MOB UI elements
mobNavigationContainer = findViewById(R.id.mob_navigation_container)
@@ -261,6 +272,10 @@ class MainActivity : AppCompatActivity() {
}
}
+ fabTidal.setOnClickListener {
+ toggleTidalCurrentVisibility()
+ }
+
fabMob.setOnClickListener {
activateMob()
}
@@ -456,6 +471,80 @@ class MainActivity : AppCompatActivity() {
return Polygon.fromLngLats(listOf(coordinates))
}
+ private fun setupTidalCurrentMapLayers(style: Style) {
+ // Add tidal arrow icon
+ style.addImage(TIDAL_ARROW_ICON_ID, BitmapFactory.decodeResource(resources, R.drawable.ic_tidal_arrow))
+
+ // Create source
+ tidalCurrentSource = GeoJsonSource(TIDAL_CURRENT_SOURCE_ID)
+ tidalCurrentSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList<Feature>()))
+ style.addSource(tidalCurrentSource!!)
+
+ // Create layer for arrows
+ val tidalCurrentLayer = SymbolLayer(TIDAL_CURRENT_LAYER_ID, TIDAL_CURRENT_SOURCE_ID).apply {
+ setProperties(
+ PropertyFactory.iconImage(TIDAL_ARROW_ICON_ID),
+ PropertyFactory.iconRotate(PropertyFactory.get("rotation")),
+ PropertyFactory.iconSize(PropertyFactory.get("size")),
+ PropertyFactory.iconColor(PropertyFactory.get("color")),
+ PropertyFactory.iconAllowOverlap(true),
+ PropertyFactory.iconIgnorePlacement(true)
+ )
+ }
+ style.addLayer(tidalCurrentLayer)
+ }
+
+ private fun toggleTidalCurrentVisibility() {
+ val newState = !LocationService.tidalCurrentState.value.isVisible
+ // Since we cannot update the flow directly from MainActivity (it's owned by LocationService),
+ // we should ideally send an intent or use a shared state.
+ // For this mock, we'll use a local update to the flow if it was a MutableStateFlow,
+ // but it's a StateFlow in LocationService.
+ // Let's add a public update method or an action to LocationService.
+ val intent = Intent(this, LocationService::class.java).apply {
+ action = LocationService.ACTION_TOGGLE_TIDAL_VISIBILITY
+ putExtra(LocationService.EXTRA_TIDAL_VISIBILITY, newState)
+ }
+ startService(intent)
+ val toastMsg = if (newState) "Tidal current overlay enabled" else "Tidal current overlay disabled"
+ Toast.makeText(this, toastMsg, Toast.LENGTH_SHORT).show()
+ }
+
+ private fun updateTidalCurrentMapLayers(state: TidalCurrentState) {
+ maplibreMap?.getStyle { style ->
+ val layer = style.getLayer(TIDAL_CURRENT_LAYER_ID)
+ if (state.isVisible) {
+ layer?.setProperties(PropertyFactory.visibility(org.maplibre.android.style.layers.Property.VISIBLE))
+ val features = state.currents.map { current ->
+ Feature.fromGeometry(
+ Point.fromLngLat(current.longitude, current.latitude)
+ ).apply {
+ addNumberProperty("rotation", current.directionDegrees.toFloat())
+ // Scale arrow size based on speed (knots)
+ val size = (0.5 + current.speedKnots / 2.0).coerceAtMost(2.0).toFloat()
+ addNumberProperty("size", size)
+ // Optionally change color based on speed
+ val color = if (current.speedKnots > 2.0) "#FF0000" else "#0000FF"
+ addStringProperty("color", color)
+ }
+ }
+ tidalCurrentSource?.setGeoJson(FeatureCollection.fromFeatures(features))
+ } else {
+ layer?.setProperties(PropertyFactory.visibility(org.maplibre.android.style.layers.Property.NONE))
+ }
+ }
+ }
+
+ private fun observeTidalCurrentState() {
+ lifecycleScope.launch {
+ LocationService.tidalCurrentState.collect { state ->
+ withContext(Dispatchers.Main) {
+ updateTidalCurrentMapLayers(state)
+ }
+ }
+ }
+ }
+
private fun observeLocationUpdates() {
lifecycleScope.launch {
// Observe from the static locationFlow in LocationService
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/TidalCurrentData.kt b/android-app/app/src/main/kotlin/org/terst/nav/TidalCurrentData.kt
new file mode 100644
index 0000000..9ddd5e8
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/TidalCurrentData.kt
@@ -0,0 +1,17 @@
+package org.terst.nav
+
+import android.location.Location
+
+data class TidalCurrent(
+ val latitude: Double,
+ val longitude: Double,
+ val speedKnots: Double,
+ val directionDegrees: Double, // Direction the current is flowing TOWARDS
+ val timestampMillis: Long
+)
+
+data class TidalCurrentState(
+ val currents: List<TidalCurrent> = emptyList(),
+ val isVisible: Boolean = false,
+ val selectedTimeMillis: Long = System.currentTimeMillis()
+)
diff --git a/android-app/app/src/main/res/drawable/ic_tidal_arrow.xml b/android-app/app/src/main/res/drawable/ic_tidal_arrow.xml
new file mode 100644
index 0000000..973b3ea
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_tidal_arrow.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#0000FF"
+ android:pathData="M12,2L4.5,20.29L5.21,21L12,18L18.79,21L19.5,20.29L12,2Z" />
+</vector>
diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml
index 746fe32..cfeea6c 100644
--- a/android-app/app/src/main/res/layout/activity_main.xml
+++ b/android-app/app/src/main/res/layout/activity_main.xml
@@ -243,6 +243,18 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fab_tidal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:clickable="true"
+ android:focusable="true"
+ android:contentDescription="Toggle Tidal Current Overlay"
+ app:srcCompat="@android:drawable/ic_menu_directions"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_bottom_toTopOf="@+id/fab_toggle_instruments" />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_toggle_instruments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"