summaryrefslogtreecommitdiff
path: root/android-app/app/src/main
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-04-04 02:10:51 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-04-04 02:10:51 +0000
commit0e867ffb8aa287ecaed4e8f58c52a9cfef1da01a (patch)
treed8a901c3f62162e869f1041a3c364e92de9a6435 /android-app/app/src/main
parentcfe178a0cd817f5ec747c220d37a82d3c7ecbf64 (diff)
feat(map): satellite view, windy/chart overlays, and rich track recording
- Switch default map view to Satellite - Add Windy (partial alpha) and OpenSeaMap overlays - Add custom user position icon (ship arrow) with heading rotation - Update TrackPoint to support rich instrument/weather metadata - Change track visualization to a dotted red line - Robustify NavApplication.isTesting with Espresso detection Co-Authored-By: Gemini CLI <gemini-cli@google.com>
Diffstat (limited to 'android-app/app/src/main')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt14
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt9
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt16
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt6
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt36
5 files changed, 71 insertions, 10 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 35b6ef7..66aa3e0 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
@@ -260,7 +260,15 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
}
}
val style = Style.Builder()
- .fromUri("https://tiles.openfreemap.org/styles/liberty")
+ .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("windy-source",
+ TileSet("2.2.0", "https://tiles.windy.com/tiles/v2.2/gfs/wind/{z}/{x}/{y}.png"), 256))
+ .withLayer(RasterLayer("windy-layer", "windy-source").apply {
+ setProperties(PropertyFactory.rasterOpacity(0.5f))
+ })
.withSource(RasterSource("openseamap-source",
TileSet("2.2.0", "https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png").also {
it.setMaxZoom(18f)
@@ -271,7 +279,8 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
loadedStyleFlow.value = style
val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor)
val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow)
- mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap)
+ val userBitmap = rasterizeDrawable(R.drawable.ic_ship_arrow)
+ mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap, userBitmap)
}
}
}
@@ -281,6 +290,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
lifecycleScope.launch {
LocationService.locationFlow.collect { gpsData ->
mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude)
+ mapHandler?.updateUserPosition(gpsData.latitude, gpsData.longitude, gpsData.courseOverGround)
val sogKnots = gpsData.speedOverGround * 1.94384
val cogDeg = gpsData.courseOverGround
viewModel.addGpsPoint(gpsData.latitude, gpsData.longitude, sogKnots, cogDeg.toDouble())
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt
index 0b507d2..3b8b596 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt
@@ -13,6 +13,15 @@ class NavApplication : Application() {
companion object {
var isTesting: Boolean = false
+ get() {
+ if (field) return true
+ return try {
+ Class.forName("androidx.test.espresso.Espresso")
+ true
+ } catch (e: ClassNotFoundException) {
+ false
+ }
+ }
}
override fun onCreate() {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt
index d803c8c..ed38e5e 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt
@@ -5,8 +5,16 @@ data class TrackPoint(
val lon: Double,
val sogKnots: Double,
val cogDeg: Double,
- val windSpeedKnots: Double,
- val windAngleDeg: Double,
- val isTrueWind: Boolean,
- val timestampMs: Long
+ val headingDeg: Double? = null,
+ val waterSpeedKnots: Double? = null,
+ val depthMeters: Double? = null,
+ val baroHpa: Double? = null,
+ val windSpeedKnots: Double? = null,
+ val windAngleDeg: Double? = null,
+ val isTrueWind: Boolean = false,
+ val airTempC: Double? = null,
+ val waveHeightM: Double? = null,
+ val currentSpeedKts: Double? = null,
+ val currentDirDeg: Double? = null,
+ val timestampMs: Long = System.currentTimeMillis()
)
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
index 0431f31..a81a76f 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
@@ -69,10 +69,14 @@ class MainViewModel(
}
fun addGpsPoint(lat: Double, lon: Double, sogKnots: Double, cogDeg: Double) {
+ val conditions = _marineConditions.value
val point = TrackPoint(
lat = lat, lon = lon,
sogKnots = sogKnots, cogDeg = cogDeg,
- windSpeedKnots = 0.0, windAngleDeg = 0.0, isTrueWind = false,
+ airTempC = conditions?.airTemp,
+ waveHeightM = conditions?.waveHeight,
+ currentSpeedKts = conditions?.currentSpeed,
+ currentDirDeg = conditions?.currentDir,
timestampMs = System.currentTimeMillis()
)
if (trackRepository.addPoint(point)) {
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 7c82808..f1feaed 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
@@ -63,18 +63,23 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
private val TIDAL_CURRENT_LAYER_ID = "tidal-current-layer"
private val TIDAL_ARROW_ICON_ID = "tidal-arrow-icon"
+ private val USER_POS_SOURCE_ID = "user-pos-source"
+ private val USER_POS_LAYER_ID = "user-pos-layer"
+ private val USER_ICON_ID = "user-icon"
+
private val TRACK_SOURCE_ID = "track-source"
private val TRACK_LAYER_ID = "track-line"
private var anchorPointSource: GeoJsonSource? = null
private var anchorCircleSource: GeoJsonSource? = null
private var tidalCurrentSource: GeoJsonSource? = null
+ private var userPosSource: GeoJsonSource? = null
private var trackSource: GeoJsonSource? = null
/**
- * Initializes map layers for anchor watch and tidal currents.
+ * Initializes map layers for anchor watch, tidal currents, and user position.
*/
- fun setupLayers(style: Style, anchorBitmap: Bitmap, arrowBitmap: Bitmap) {
+ fun setupLayers(style: Style, anchorBitmap: Bitmap, arrowBitmap: Bitmap, userBitmap: Bitmap) {
// Anchor layers
style.addImage(ANCHOR_ICON_ID, anchorBitmap)
anchorPointSource = GeoJsonSource(ANCHOR_POINT_SOURCE_ID)
@@ -112,6 +117,29 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
PropertyFactory.iconSize(0.8f)
)
})
+
+ // User Position Layer
+ style.addImage(USER_ICON_ID, userBitmap)
+ userPosSource = GeoJsonSource(USER_POS_SOURCE_ID)
+ style.addSource(userPosSource!!)
+ style.addLayer(SymbolLayer(USER_POS_LAYER_ID, USER_POS_SOURCE_ID).apply {
+ setProperties(
+ PropertyFactory.iconImage(USER_ICON_ID),
+ PropertyFactory.iconRotate(org.maplibre.android.style.expressions.Expression.get("rotation")),
+ PropertyFactory.iconAllowOverlap(true),
+ PropertyFactory.iconIgnorePlacement(true),
+ PropertyFactory.iconSize(1.0f)
+ )
+ })
+ }
+
+ /**
+ * Updates the user's position and orientation on the map.
+ */
+ fun updateUserPosition(lat: Double, lon: Double, headingDeg: Float) {
+ userPosSource?.setGeoJson(Feature.fromGeometry(Point.fromLngLat(lon, lat)).apply {
+ addNumberProperty("rotation", headingDeg)
+ })
}
/**
@@ -180,7 +208,9 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
style.addLayer(LineLayer(TRACK_LAYER_ID, TRACK_SOURCE_ID).apply {
setProperties(
PropertyFactory.lineColor("#E53935"),
- PropertyFactory.lineWidth(3f)
+ PropertyFactory.lineWidth(4f),
+ PropertyFactory.lineDasharray(arrayOf(1f, 2f)),
+ PropertyFactory.lineCap("round")
)
})
}