summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/plans/2026-06-30-fishing-mode.md572
1 files changed, 572 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-06-30-fishing-mode.md b/docs/superpowers/plans/2026-06-30-fishing-mode.md
new file mode 100644
index 0000000..e863cd3
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-30-fishing-mode.md
@@ -0,0 +1,572 @@
+# Fishing Mode Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add a "Fishing Mode" to the nav app that highlights productive fishing lanes, recommends rigs and approach tactics, and gives live steering guidance for fast pelagic species (Ono/wahoo and mahi-mahi).
+
+**Architecture:** Fishing mode is a persistent overlay state toggled from the map screen. When active, it adds SST (sea surface temperature) raster tiles to the map, computes target zones from the conditions API response, and displays a floating card with rig suggestions and steering bearing. A `FishingModeManager` owns the state and is wired through `MapLayerManager` for the map layer and through a new `FishingOverlayView` for the heads-up card.
+
+**Tech Stack:** Kotlin, MapLibre raster layers (NOAA CoastWatch SST tiles), existing `MarineConditions` model, existing `LayerPickerSheet`, new `FishingModeManager` + `FishingOverlayView`.
+
+---
+
+## File Structure
+
+| File | Role |
+|------|------|
+| `ui/fishing/FishingModeManager.kt` | State singleton: active flag, SST layer visibility, target zone computation |
+| `ui/fishing/FishingOverlayView.kt` | Floating card drawn over the map: target bearing, species, rig, speed cue |
+| `ui/fishing/RigAdvisor.kt` | Pure function: conditions → `RigRecommendation` |
+| `ui/fishing/FishingTarget.kt` | Data class: bearing, distance, zone description |
+| `ui/MapLayerManager.kt` | Add `SOURCE_SST`/`LAYER_SST` and `sstEnabled` pref |
+| `res/layout/view_fishing_overlay.xml` | Card layout for the fishing HUD |
+| `res/layout/activity_main.xml` | Add `FishingOverlayView` stacked over the MapView |
+| `MainActivity.kt` | Wire fishing FAB, observe conditions→overlay, pass bearing to map |
+| `res/drawable/ic_fish.xml` | Fish icon for FAB (24dp vector) |
+
+---
+
+## Task 1: FishingTarget + RigAdvisor (pure logic, no Android)
+
+**Files:**
+- Create: `android-app/app/src/main/kotlin/org/terst/nav/ui/fishing/FishingTarget.kt`
+- Create: `android-app/app/src/main/kotlin/org/terst/nav/ui/fishing/RigAdvisor.kt`
+
+- [ ] **Write the failing test**
+
+```kotlin
+// test/kotlin/org/terst/nav/ui/fishing/RigAdvisorTest.kt
+class RigAdvisorTest {
+ @Test fun `warm water fast current favors Ono rig`() {
+ val rec = RigAdvisor.recommend(tempC = 26.0, currentKt = 1.5, windKt = 10.0)
+ assertEquals(FishingSpecies.ONO, rec.primarySpecies)
+ assertTrue(rec.trollingSpeedKt in 7.0..10.0)
+ }
+ @Test fun `cooler water moderate current favors mahi`() {
+ val rec = RigAdvisor.recommend(tempC = 23.0, currentKt = 0.5, windKt = 8.0)
+ assertEquals(FishingSpecies.MAHI, rec.primarySpecies)
+ }
+}
+```
+
+- [ ] **Run test to verify it fails:** `./gradlew testDebugUnitTest --tests "*.RigAdvisorTest" -q`
+
+- [ ] **Implement FishingTarget.kt**
+
+```kotlin
+package org.terst.nav.ui.fishing
+
+enum class FishingSpecies(val displayName: String) {
+ ONO("Ono (Wahoo)"), MAHI("Mahi-Mahi"), BOTH("Ono & Mahi")
+}
+
+data class RigRecommendation(
+ val primarySpecies: FishingSpecies,
+ val rigDescription: String, // e.g. "Blue/white 9" lure, mono 80# leader"
+ val trollingSpeedKt: Double, // optimal trolling speed
+ val approachNote: String // one-line tactic
+)
+
+data class FishingTarget(
+ val bearingDeg: Double, // magnetic bearing toward best zone
+ val distanceNm: Double, // estimated distance to zone
+ val zoneDescription: String, // "temp break 76°F / color edge"
+ val rig: RigRecommendation
+)
+```
+
+- [ ] **Implement RigAdvisor.kt**
+
+```kotlin
+package org.terst.nav.ui.fishing
+
+object RigAdvisor {
+ /**
+ * Returns a rig recommendation based on current conditions.
+ * Ono (wahoo): prefer 75–82°F, current edges, troll 8–10 kt with fast lures.
+ * Mahi: prefer 70–78°F, floating debris/weed lines, troll 6–9 kt with skirts.
+ */
+ fun recommend(tempC: Double, currentKt: Double, windKt: Double): RigRecommendation {
+ val tempF = tempC * 9.0 / 5.0 + 32.0
+ val species = when {
+ tempF >= 75.0 && currentKt >= 1.0 -> FishingSpecies.ONO
+ tempF in 70.0..78.0 -> FishingSpecies.MAHI
+ else -> FishingSpecies.BOTH
+ }
+ return when (species) {
+ FishingSpecies.ONO -> RigRecommendation(
+ primarySpecies = FishingSpecies.ONO,
+ rigDescription = "Blue/white 9\" Rapala or jet head, 80# mono leader, 50–80# stand-up",
+ trollingSpeedKt = if (currentKt > 1.0) 9.0 else 8.0,
+ approachNote = "Troll along current edge; watch for color change (blue→green). Ono hit fast."
+ )
+ FishingSpecies.MAHI -> RigRecommendation(
+ primarySpecies = FishingSpecies.MAHI,
+ rigDescription = "Green/yellow skirt or ballyhoo, 50# mono, medium-action",
+ trollingSpeedKt = 7.0,
+ approachNote = "Look for floating debris or weed lines. Mahi school; keep one hooked in water."
+ )
+ FishingSpecies.BOTH -> RigRecommendation(
+ primarySpecies = FishingSpecies.BOTH,
+ rigDescription = "Mix: one Ono lure (fast), one mahi skirt (slow)",
+ trollingSpeedKt = 8.0,
+ approachNote = "Spread lures at different depths. Watch for birds and color changes."
+ )
+ }
+ }
+}
+```
+
+- [ ] **Run tests:** `./gradlew testDebugUnitTest --tests "*.RigAdvisorTest" -q`
+ Expected: 2 PASSED
+
+- [ ] **Commit**
+```bash
+git add android-app/app/src/main/kotlin/org/terst/nav/ui/fishing/ \
+ android-app/app/src/test/kotlin/org/terst/nav/ui/fishing/
+git commit -m "feat(fishing): FishingTarget + RigAdvisor logic"
+```
+
+---
+
+## Task 2: SST raster layer in MapLayerManager
+
+**Files:**
+- Modify: `android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt`
+- Modify: `android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml`
+- Modify: `android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt`
+
+SST tile source: NOAA CoastWatch ERDDAP griddap XYZ tiles — `https://coastwatch.pfeg.noaa.gov/erddap/griddap/jplMURSST41.largePng?analysed_sst%5B(2024-01-01T00:00:00Z)%5D%5B({lat_min}):({lat_max})%5D%5B({lon_min}):({lon_max})%5D&.draw=surface&.vars=longitude|latitude|analysed_sst&.colorBar=KT_thermal|||19|32|` — but this is a WMS-style endpoint, not XYZ tiles. Use NOAA Tiler instead: `https://api.tidetopography.com/tiles/sst/{z}/{x}/{y}.png` — this is a fictional URL. The real approach for SST tiles:
+
+**Recommended source:** NOAA Environmental Research Division's CoastWatch WMS:
+`https://coastwatch.pfeg.noaa.gov/erddap/wms/jplMURSST41/request?SERVICE=WMS&REQUEST=GetMap&LAYERS=jplMURSST41:analysed_sst&BBOX={bbox-epsg-3857}&FORMAT=image/png&TRANSPARENT=TRUE&CRS=EPSG:3857&WIDTH=256&HEIGHT=256&VERSION=1.3.0&STYLES=`
+
+This is a WMS endpoint, which MapLibre supports as a raster source with `TileSet` using the WMS URL pattern.
+
+- [ ] **Add SST constants to MapLayerManager companion object**
+
+```kotlin
+const val SOURCE_SST = "sst-source"
+const val LAYER_SST = "sst-layer"
+private const val KEY_SST = "sst_enabled"
+private const val URL_SST =
+ "https://coastwatch.pfeg.noaa.gov/erddap/wms/jplMURSST41/request" +
+ "?SERVICE=WMS&REQUEST=GetMap&LAYERS=jplMURSST41:analysed_sst" +
+ "&BBOX={bbox-epsg-3857}&FORMAT=image/png&TRANSPARENT=TRUE" +
+ "&CRS=EPSG:3857&WIDTH=256&HEIGHT=256&VERSION=1.3.0&STYLES="
+```
+
+- [ ] **Add `sstEnabled` property and `setSstEnabled()` method** (same pattern as `depthEnabled`/`setDepthEnabled`)
+
+- [ ] **Add SST layer in `addToStyleBuilder()`** — `RasterLayer(LAYER_SST, SOURCE_SST)` with `rasterOpacity(0.65f)`, above wind layer, below depth layer. Visibility set from `sstEnabled`.
+
+- [ ] **Add SST toggle to `layout_layer_picker_sheet.xml`** — new switch row after "Past tracks" titled "SST (sea surface temp)" with subtitle "Temperature overlay — NOAA CoastWatch".
+
+- [ ] **Wire SST switch in `LayerPickerSheet.kt`** — same pattern as `onDepthChanged`.
+
+- [ ] **Compile:** `./gradlew compileDebugKotlin`
+
+- [ ] **Commit:** `git commit -m "feat(fishing): SST raster layer from NOAA CoastWatch"`
+
+---
+
+## Task 3: FishingModeManager
+
+**Files:**
+- Create: `android-app/app/src/main/kotlin/org/terst/nav/ui/fishing/FishingModeManager.kt`
+
+- [ ] **Write the failing test**
+
+```kotlin
+// FishingModeManagerTest.kt
+class FishingModeManagerTest {
+ @Test fun `target zone computed from current bearing and temp break`() {
+ val mgr = FishingModeManager()
+ // conditions: heading 270°, current flowing 180°, temp 25°C
+ val target = mgr.computeTarget(
+ boatLat = 19.5, boatLon = -156.0, boatCogDeg = 270.0,
+ tempC = 25.0, currentKt = 1.2, currentDirDeg = 180.0
+ )
+ assertNotNull(target)
+ // Target should be roughly perpendicular to current (cross-current) for temp breaks
+ assertTrue(target!!.distanceNm in 0.5..20.0)
+ }
+}
+```
+
+- [ ] **Run test to verify it fails**
+
+- [ ] **Implement FishingModeManager.kt**
+
+```kotlin
+package org.terst.nav.ui.fishing
+
+import android.content.Context
+import kotlin.math.*
+
+class FishingModeManager(context: Context) {
+
+ private val prefs = context.getSharedPreferences("fishing_mode", Context.MODE_PRIVATE)
+
+ var isActive: Boolean
+ get() = prefs.getBoolean("active", false)
+ set(v) { prefs.edit().putBoolean("active", v).apply() }
+
+ /**
+ * Computes a target fishing zone based on current conditions.
+ * Strategy: steer perpendicular to current flow toward the nearest estimated
+ * temperature break (assumed 3–8 nm away across the current axis).
+ * Returns null when conditions are insufficient.
+ */
+ fun computeTarget(
+ boatLat: Double, boatLon: Double, boatCogDeg: Double,
+ tempC: Double, currentKt: Double, currentDirDeg: Double
+ ): FishingTarget? {
+ if (currentKt < 0.1) return null // no current data — can't predict break
+
+ // Temp break tends to be perpendicular to current flow and typically 3–8 nm away
+ val crossCurrentBearing = (currentDirDeg + 90.0) % 360.0
+ val estimatedDistNm = (3.0 + currentKt * 2.5).coerceIn(3.0, 8.0)
+
+ val rig = RigAdvisor.recommend(tempC, currentKt, windKt = 0.0)
+
+ val tempF = tempC * 9.0 / 5.0 + 32.0
+ val zoneDesc = "%.0f°F water, %.1f kt current — steer to color edge".format(tempF, currentKt)
+
+ return FishingTarget(
+ bearingDeg = crossCurrentBearing,
+ distanceNm = estimatedDistNm,
+ zoneDescription = zoneDesc,
+ rig = rig
+ )
+ }
+}
+```
+
+- [ ] **Run tests:** `./gradlew testDebugUnitTest --tests "*.FishingModeManagerTest" -q` → PASSED
+
+- [ ] **Commit:** `git commit -m "feat(fishing): FishingModeManager with target zone computation"`
+
+---
+
+## Task 4: FishingOverlayView layout + view class
+
+**Files:**
+- Create: `android-app/app/src/main/res/layout/view_fishing_overlay.xml`
+- Create: `android-app/app/src/main/kotlin/org/terst/nav/ui/fishing/FishingOverlayView.kt`
+
+- [ ] **Create `view_fishing_overlay.xml`**
+
+```xml
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ app:cardCornerRadius="12dp"
+ app:cardElevation="4dp"
+ android:background="#CC004D40">
+
+ <LinearLayout
+ android:layout_width="220dp"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="12dp">
+
+ <!-- Species + bearing row -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="6dp">
+
+ <TextView
+ android:id="@+id/tv_fish_species"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Ono &amp; Mahi"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:textColor="#FFFFFFFF" />
+
+ <TextView
+ android:id="@+id/tv_fish_bearing"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="270°"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ android:textColor="#FF80CBC4" />
+
+ </LinearLayout>
+
+ <!-- Zone description -->
+ <TextView
+ android:id="@+id/tv_fish_zone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="76°F water, 1.2 kt current"
+ android:textSize="11sp"
+ android:textColor="#99FFFFFF"
+ android:layout_marginBottom="8dp" />
+
+ <!-- Rig description -->
+ <TextView
+ android:id="@+id/tv_fish_rig"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Blue/white 9″ jet head, 80# mono"
+ android:textSize="11sp"
+ android:textColor="#CCFFFFFF"
+ android:layout_marginBottom="4dp" />
+
+ <!-- Speed + approach -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/tv_fish_speed"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Troll 8–10 kt"
+ android:textSize="11sp"
+ android:textColor="#CCFFFFFF" />
+
+ <TextView
+ android:id="@+id/tv_fish_distance"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="~5 nm"
+ android:textSize="11sp"
+ android:textColor="#99FFFFFF" />
+
+ </LinearLayout>
+
+ <!-- Approach note -->
+ <TextView
+ android:id="@+id/tv_fish_approach"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Troll along current edge; watch for color change"
+ android:textSize="10sp"
+ android:textColor="#88FFFFFF"
+ android:layout_marginTop="6dp" />
+
+ </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
+```
+
+- [ ] **Create `FishingOverlayView.kt`**
+
+```kotlin
+package org.terst.nav.ui.fishing
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.FrameLayout
+import android.widget.TextView
+import org.terst.nav.R
+
+class FishingOverlayView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null
+) : FrameLayout(context, attrs) {
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.view_fishing_overlay, this, true)
+ }
+
+ fun bind(target: FishingTarget) {
+ findViewById<TextView>(R.id.tv_fish_species).text = target.rig.primarySpecies.displayName
+ findViewById<TextView>(R.id.tv_fish_bearing).text = "%.0f°".format(target.bearingDeg)
+ findViewById<TextView>(R.id.tv_fish_zone).text = target.zoneDescription
+ findViewById<TextView>(R.id.tv_fish_rig).text = target.rig.rigDescription
+ findViewById<TextView>(R.id.tv_fish_speed).text = "Troll %.0f kt".format(target.rig.trollingSpeedKt)
+ findViewById<TextView>(R.id.tv_fish_distance).text = "~%.0f nm".format(target.distanceNm)
+ findViewById<TextView>(R.id.tv_fish_approach).text = target.rig.approachNote
+ }
+}
+```
+
+- [ ] **Compile:** `./gradlew compileDebugKotlin`
+- [ ] **Commit:** `git commit -m "feat(fishing): FishingOverlayView card UI"`
+
+---
+
+## Task 5: Wire fishing mode in MainActivity
+
+**Files:**
+- Modify: `android-app/app/src/main/res/layout/activity_main.xml`
+- Modify: `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt`
+
+- [ ] **Add `FishingOverlayView` to `activity_main.xml`** — inside the root `FrameLayout`, after the MapView, gravity `bottom|start`, initial `visibility="gone"`:
+
+```xml
+<org.terst.nav.ui.fishing.FishingOverlayView
+ android:id="@+id/fishing_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|start"
+ android:layout_marginBottom="120dp"
+ android:visibility="gone" />
+```
+
+- [ ] **Add fishing FAB to `activity_main.xml`** — `FloatingActionButton` with `ic_fish` icon, `gravity="bottom|start"`, `layout_marginStart="16dp"`, `layout_marginBottom="80dp"`:
+
+```xml
+<com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fab_fishing"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|start"
+ android:layout_marginStart="16dp"
+ android:layout_marginBottom="80dp"
+ android:src="@drawable/ic_fish"
+ app:fabSize="mini"
+ android:contentDescription="Fishing mode" />
+```
+
+- [ ] **Add fishing mode fields to `MainActivity`**:
+
+```kotlin
+private lateinit var fishingOverlay: org.terst.nav.ui.fishing.FishingOverlayView
+private lateinit var fabFishing: FloatingActionButton
+private lateinit var fishingModeManager: org.terst.nav.ui.fishing.FishingModeManager
+```
+
+- [ ] **Initialize in `initializeUI()`** — bind views, init `FishingModeManager`, set FAB click listener:
+
+```kotlin
+fishingOverlay = findViewById(R.id.fishing_overlay)
+fabFishing = findViewById(R.id.fab_fishing)
+fishingModeManager = org.terst.nav.ui.fishing.FishingModeManager(this)
+
+fabFishing.setOnClickListener {
+ val active = !fishingModeManager.isActive
+ fishingModeManager.isActive = active
+ fishingOverlay.visibility = if (active) View.VISIBLE else View.GONE
+ // Also enable SST layer when fishing mode activates
+ loadedStyleFlow.value?.let { style ->
+ layerManager.setSstEnabled(style, active)
+ }
+ updateFishingOverlay()
+}
+
+// Restore fishing overlay if it was active
+if (fishingModeManager.isActive) {
+ fishingOverlay.visibility = View.VISIBLE
+}
+```
+
+- [ ] **Add `updateFishingOverlay()` helper**:
+
+```kotlin
+private fun updateFishingOverlay() {
+ if (!fishingModeManager.isActive) return
+ val cond = viewModel.marineConditions.value ?: return
+ val pos = viewModel.currentPosition.value ?: return
+ val target = fishingModeManager.computeTarget(
+ boatLat = pos.first,
+ boatLon = pos.second,
+ boatCogDeg = lastCogDeg, // add var lastCogDeg in observeDataSources GPS collect
+ tempC = cond.tempC ?: return,
+ currentKt = cond.currentSpeedKt ?: 0.0,
+ currentDirDeg = cond.currentDirDeg ?: 0.0
+ ) ?: return
+ fishingOverlay.bind(target)
+}
+```
+
+- [ ] **Call `updateFishingOverlay()` inside `marineConditions.collect { }` block**
+
+- [ ] **Compile:** `./gradlew compileDebugKotlin`
+
+- [ ] **Commit:** `git commit -m "feat(fishing): wire fishing mode FAB and overlay in MainActivity"`
+
+---
+
+## Task 6: Fishing mode asset — ic_fish.xml
+
+**Files:**
+- Create: `android-app/app/src/main/res/drawable/ic_fish.xml`
+
+- [ ] **Create vector drawable**
+
+```xml
+<!-- ic_fish.xml — simple silhouette fish icon 24×24dp -->
+<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="@android:color/white"
+ android:pathData="M20,12 C18,9 14,7 10,8 L8,5 L6,8 C4,8.5 2,10 2,12 C2,14 4,15.5 6,16 L8,19 L10,16 C14,17 18,15 20,12 Z M7,12 C6.4,12 6,11.6 6,11 C6,10.4 6.4,10 7,10 C7.6,10 8,10.4 8,11 C8,11.6 7.6,12 7,12 Z" />
+</vector>
+```
+
+- [ ] **Compile and visual check:** `./gradlew compileDebugKotlin`
+- [ ] **Commit:** `git commit -m "feat(fishing): add ic_fish vector drawable"`
+
+---
+
+## Task 7: Steering guidance on map — fishing lane bearing line
+
+**Files:**
+- Modify: `android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt`
+- Modify: `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt`
+
+Add a dashed heading line on the map pointing toward the fishing target bearing.
+
+- [ ] **Add constants and source to MapHandler**:
+
+```kotlin
+private val FISHING_LINE_SOURCE_ID = "fishing-line-source"
+private val FISHING_LINE_LAYER_ID = "fishing-line-layer"
+private var fishingLineSource: GeoJsonSource? = null
+```
+
+- [ ] **Add `setupFishingLayer(style: Style)` to MapHandler** — `LineLayer` with dashed stroke (`lineDasharray([4f, 2f])`), `lineColor("#FF6D00")`, `lineWidth(2f)`
+
+- [ ] **Add `updateFishingBearing(lat: Double, lon: Double, bearingDeg: Double, lengthNm: Double = 10.0)` to MapHandler** — compute endpoint from start + bearing + distance (haversine inverse), update `fishingLineSource`
+
+- [ ] **Call `setupFishingLayer()` in style-load callback in MainActivity** (after `setupFadLayer`)
+
+- [ ] **Call `updateFishingBearing()` in `updateFishingOverlay()`**
+
+- [ ] **Compile:** `./gradlew compileDebugKotlin`
+
+- [ ] **Commit:** `git commit -m "feat(fishing): dashed bearing line on map for fishing target"`
+
+---
+
+## Testing
+
+```bash
+./gradlew testDebugUnitTest # unit tests for RigAdvisor + FishingModeManager
+./gradlew assembleDebug # full APK build
+```
+
+Manual test flow:
+1. Open app → tap fish FAB → fishing overlay card appears
+2. SST layer loads on map (requires network; may take a few seconds)
+3. Overlay shows species, bearing, rig, speed
+4. Tap FAB again → overlay hides, SST layer hides
+5. Conditions update (conditions API returns) → overlay auto-refreshes
+
+---
+
+## Notes on SST Tile Quality
+
+The NOAA CoastWatch WMS tile URL may have latency (2–10 sec tile load). Consider caching tiles locally with MapLibre's tile cache or using a CDN proxy. If tile loading is too slow:
+- Alternative: Copernicus Marine Environment (CMEMS) SST — requires API key
+- Fallback: Disable SST by default, show only the rig/bearing overlay from conditions data