1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
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 & 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
|