diff options
Diffstat (limited to 'android-app/app/src/main/kotlin')
3 files changed, 144 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() +) |
