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
|
# Additional Map Data Sources 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 NOAA nautical chart tiles, NOAA Aids to Navigation (ATON) vector data, and NOAA traffic separation scheme overlays to the nav app's layer stack, giving the user richer maritime situational awareness beyond the current OpenSeaMap seamarks.
**Architecture:** All additions follow existing patterns. Raster tile sources extend `MapLayerManager` with new constants + toggle methods. The ATON vector layer follows the FAD pattern: fetch GeoJSON from NOAA's public REST API, cache to disk, render via a `GeoJsonSource` in `MapHandler`. Traffic separation zones are static polygons (baked in as a bundled asset, not fetched).
**Tech Stack:** Kotlin, MapLibre, existing `MapLayerManager`/`MapHandler`/`LayerPickerSheet` plumbing, NOAA Office of Coast Survey tile service, NOAA ATON API (buoydata.coast.noaa.gov), bundled GeoJSON asset for traffic separation zones.
---
## File Structure
| File | Role |
|------|------|
| `ui/MapLayerManager.kt` | Add `SOURCE_NOAA_CHARTS`, `SOURCE_TSS`; new `noaaChartsEnabled`, `tssEnabled` prefs and setters |
| `ui/MapHandler.kt` | Add ATON + TSS GeoJSON sources/layers; `setupAtonLayer()`, `updateAtonLayer()`, `setupTssLayer()` |
| `ui/aton/AtonRepository.kt` | Fetches + caches ATON GeoJSON from NOAA API; exposes `Flow<List<Feature>>` |
| `ui/aton/AtonData.kt` | ATON feature property constants + symbol mapping (lateral/cardinal/special marks) |
| `res/layout/layout_layer_picker_sheet.xml` | Three new switch rows: NOAA Charts, ATON, Traffic Separation |
| `ui/LayerPickerSheet.kt` | Wire three new callbacks: `onNoaaChartsChanged`, `onAtonChanged`, `onTssChanged` |
| `res/raw/pacific_tss.geojson` | Bundled GeoJSON with North Pacific traffic separation scheme polygons |
| `MainActivity.kt` | Pass new callbacks in `buildLayerPickerSheet()`; trigger ATON refresh on conditions update |
---
## Task 1: NOAA pre-rendered chart tiles layer
**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`
- Modify: `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt`
NOAA's pre-rendered ENC tiles (`tileservice.charts.noaa.gov`) are standard XYZ PNGs covering all US waters including Hawaii. They render depth soundings, nav aids, restricted areas, and chart text — higher information density than OpenSeaMap seamarks. Use at zoom ≥ 10 only (tiles are blank below that).
Read `MapLayerManager.kt` before editing. Follow the existing `SOURCE_SEAMARKS`/`LAYER_SEAMARKS` pattern exactly.
- [ ] **Add NOAA chart constants to `MapLayerManager` companion object**
```kotlin
const val SOURCE_NOAA_CHARTS = "noaa-charts-source"
const val LAYER_NOAA_CHARTS = "noaa-charts-layer"
private const val KEY_NOAA_CHARTS = "noaa_charts_enabled"
// NOAA Office of Coast Survey pre-rendered ENC tiles (zoom 3–18, US waters)
private const val URL_NOAA_CHARTS =
"https://tileservice.charts.noaa.gov/tiles/50000_1/{z}/{x}/{y}.png"
```
- [ ] **Add `noaaChartsEnabled` property** (default `false`):
```kotlin
var noaaChartsEnabled: Boolean = prefs.getBoolean(KEY_NOAA_CHARTS, false)
private set
```
- [ ] **Add `setNoaaChartsEnabled()` method** (same pattern as `setSeamarksEnabled()`):
```kotlin
fun setNoaaChartsEnabled(style: Style, enabled: Boolean) {
noaaChartsEnabled = enabled
prefs.edit().putBoolean(KEY_NOAA_CHARTS, enabled).apply()
style.getLayer(LAYER_NOAA_CHARTS)?.setProperties(
PropertyFactory.visibility(if (enabled) "visible" else "none")
)
}
```
- [ ] **Add NOAA chart source + layer in `addToStyleBuilder()`** — ABOVE seamarks so chart text sits under seamark symbols. Set `minZoom(10f)` (blank tiles below that), `rasterOpacity(0.8f)`:
```kotlin
builder.withSource(RasterSource(SOURCE_NOAA_CHARTS,
TileSet("2.2.0", URL_NOAA_CHARTS).also {
it.setMinZoom(3f); it.setMaxZoom(18f)
}, 256))
builder.withLayer(RasterLayer(LAYER_NOAA_CHARTS, SOURCE_NOAA_CHARTS).apply {
setProperties(
PropertyFactory.rasterOpacity(0.8f),
PropertyFactory.visibility(if (noaaChartsEnabled) "visible" else "none"),
PropertyFactory.rasterMinZoom(10f)
)
})
```
Place this after `LAYER_DEPTH` and before `LAYER_SEAMARKS` in the layer order.
- [ ] **Add switch row to `layout_layer_picker_sheet.xml`** — read the file first, copy the exact pattern of the `switch_seamarks` row. New row titled "NOAA Charts" with subtitle "Detailed soundings & nav aids (US waters)" and `android:id="@+id/switch_noaa_charts"`.
- [ ] **Add `onNoaaChartsChanged` to `LayerPickerSheet`** — add to constructor before `onSstChanged`, wire the switch in `onViewCreated()` with `view.findViewById<SwitchMaterial>(R.id.switch_noaa_charts)`.
- [ ] **Update `MainActivity.buildLayerPickerSheet()`** — add `onNoaaChartsChanged = { enabled -> loadedStyleFlow.value?.let { layerManager.setNoaaChartsEnabled(it, enabled) } }`.
- [ ] **Compile:** `./gradlew compileDebugKotlin`
- [ ] **Commit:** `git commit -m "feat(layers): NOAA pre-rendered ENC chart tiles"`
---
## Task 2: ATON data model + repository
**Files:**
- Create: `android-app/app/src/main/kotlin/org/terst/nav/ui/aton/AtonData.kt`
- Create: `android-app/app/src/main/kotlin/org/terst/nav/ui/aton/AtonRepository.kt`
NOAA's buoy/light data API endpoint:
```
https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json?type=aton&units=english
```
This returns a JSON array of ATON stations with `id`, `name`, `lat`, `lng`, `type`. Cache to `context.filesDir/aton_cache.json`. Refresh if older than 24 h.
The `type` field values include `"Light"`, `"Float"`, `"Buoy"`, `"Daybeacon"`, `"Range"`. Map these to circle colors:
- Red: `"Light"` → `#FF0000`
- Green: `"Float"`, `"Buoy"` → `#00AA00`
- Yellow: `"Daybeacon"`, `"Range"` → `#FFDD00`
- White: everything else → `#FFFFFF`
- [ ] **Write the failing test**
```kotlin
// android-app/app/src/test/kotlin/org/terst/nav/ui/aton/AtonDataTest.kt
package org.terst.nav.ui.aton
import org.junit.Assert.*
import org.junit.Test
class AtonDataTest {
@Test fun `light type maps to red`() {
assertEquals("#FF0000", AtonData.colorFor("Light"))
}
@Test fun `float type maps to green`() {
assertEquals("#00AA00", AtonData.colorFor("Float"))
}
@Test fun `unknown type maps to white`() {
assertEquals("#FFFFFF", AtonData.colorFor("WeirdThing"))
}
@Test fun `json station converts to feature`() {
val json = """{"id":"8443970","name":"Boston Light","lat":42.3298,"lng":-70.8930,"type":"Light"}"""
val f = AtonData.parseStation(json)
assertNotNull(f)
assertEquals("Boston Light", f!!.getStringProperty("name"))
assertEquals("#FF0000", f.getStringProperty("color"))
}
}
```
- [ ] **Run to verify it fails:** `./gradlew testDebugUnitTest --tests "*.AtonDataTest" -q`
- [ ] **Implement `AtonData.kt`**
```kotlin
package org.terst.nav.ui.aton
import org.maplibre.geojson.Feature
import org.maplibre.geojson.Point
import org.json.JSONObject
object AtonData {
fun colorFor(type: String): String = when (type) {
"Light" -> "#FF0000"
"Float", "Buoy" -> "#00AA00"
"Daybeacon", "Range" -> "#FFDD00"
else -> "#FFFFFF"
}
fun parseStation(json: String): Feature? = try {
val obj = JSONObject(json)
val lat = obj.getDouble("lat")
val lng = obj.getDouble("lng")
val type = obj.optString("type", "")
Feature.fromGeometry(Point.fromLngLat(lng, lat)).apply {
addStringProperty("id", obj.optString("id"))
addStringProperty("name", obj.optString("name"))
addStringProperty("type", type)
addStringProperty("color", colorFor(type))
}
} catch (_: Exception) { null }
/** Parse a full stations JSON array response from NOAA ATON API. */
fun parseResponse(json: String): List<Feature> = try {
val arr = JSONObject(json).getJSONArray("stations")
(0 until arr.length()).mapNotNull { parseStation(arr.get(it).toString()) }
} catch (_: Exception) { emptyList() }
}
```
- [ ] **Implement `AtonRepository.kt`**
```kotlin
package org.terst.nav.ui.aton
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.maplibre.geojson.Feature
import java.io.File
import java.net.URL
class AtonRepository(private val context: Context) {
private val cacheFile = File(context.filesDir, "aton_cache.json")
private val maxAgeMs = 24 * 60 * 60 * 1000L // 24 hours
private val apiUrl =
"https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json?type=aton&units=english"
/** Returns cached ATON features, fetching fresh data if cache is stale or missing. */
suspend fun getFeatures(): List<Feature> = withContext(Dispatchers.IO) {
val cached = readCache()
if (cached != null) return@withContext AtonData.parseResponse(cached)
val fresh = fetchFromApi() ?: return@withContext emptyList()
cacheFile.writeText(fresh)
AtonData.parseResponse(fresh)
}
private fun readCache(): String? {
if (!cacheFile.exists()) return null
if (System.currentTimeMillis() - cacheFile.lastModified() > maxAgeMs) return null
return cacheFile.readText()
}
private fun fetchFromApi(): String? = try {
URL(apiUrl).openStream().bufferedReader().readText()
} catch (_: Exception) { null }
}
```
- [ ] **Run tests:** `./gradlew testDebugUnitTest --tests "*.AtonDataTest" -q` → 4 PASSED
- [ ] **Commit:** `git commit -m "feat(aton): AtonData model + AtonRepository with 24h cache"`
---
## Task 3: ATON vector layer in MapHandler + MainActivity wiring
**Files:**
- Modify: `android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt`
- 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`
- Modify: `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt`
Read `MapHandler.kt` before editing — follow the FAD layer pattern (`setupFadLayer`, `updateFadLayer`, `setFadLayerVisible`).
- [ ] **Add ATON constants + source field to `MapHandler`**:
```kotlin
private val ATON_SOURCE_ID = "aton-source"
private val ATON_LAYER_ID = "aton-layer"
private var atonSource: GeoJsonSource? = null
```
- [ ] **Add `setupAtonLayer(style: Style, visible: Boolean)` to `MapHandler`**:
```kotlin
fun setupAtonLayer(style: Style, visible: Boolean) {
val src = GeoJsonSource(ATON_SOURCE_ID)
atonSource = src
style.addSource(src)
style.addLayer(CircleLayer(ATON_LAYER_ID, ATON_SOURCE_ID).apply {
setProperties(
PropertyFactory.circleColor(Expression.get("color")),
PropertyFactory.circleRadius(4f),
PropertyFactory.circleStrokeColor("rgba(0,0,0,0.6)"),
PropertyFactory.circleStrokeWidth(1f),
PropertyFactory.visibility(if (visible) "visible" else "none")
)
})
// Label layer: name at zoom 12+
style.addLayer(SymbolLayer("${ATON_LAYER_ID}-labels", ATON_SOURCE_ID).apply {
setProperties(
PropertyFactory.textField(Expression.get("name")),
PropertyFactory.textSize(9f),
PropertyFactory.textOffset(arrayOf(0f, -1.2f)),
PropertyFactory.textColor("rgba(255,255,255,1)"),
PropertyFactory.textHaloColor("rgba(0,0,0,0.7)"),
PropertyFactory.textHaloWidth(1f),
PropertyFactory.textAllowOverlap(false),
PropertyFactory.visibility(if (visible) "visible" else "none"),
PropertyFactory.textOpacity(
Expression.step(Expression.zoom(), Expression.literal(0f),
Expression.stop(12.0, 1f))
)
)
})
}
```
- [ ] **Add `updateAtonLayer(features: List<Feature>)` and `setAtonLayerVisible(style, visible)` to `MapHandler`**:
```kotlin
fun updateAtonLayer(features: List<Feature>) {
atonSource?.setGeoJson(FeatureCollection.fromFeatures(features))
}
fun setAtonLayerVisible(style: Style, visible: Boolean) {
val v = if (visible) "visible" else "none"
style.getLayer(ATON_LAYER_ID)?.setProperties(PropertyFactory.visibility(v))
style.getLayer("${ATON_LAYER_ID}-labels")?.setProperties(PropertyFactory.visibility(v))
}
```
- [ ] **Add `atonEnabled` to `MapLayerManager`** (default `false`; same pattern as `fadsEnabled`):
```kotlin
const val SOURCE_ATON = "aton-source" // informational only; source managed by MapHandler
private const val KEY_ATON = "aton_enabled"
var atonEnabled: Boolean = prefs.getBoolean(KEY_ATON, false)
private set
fun setAtonEnabled(enabled: Boolean) {
atonEnabled = enabled
prefs.edit().putBoolean(KEY_ATON, enabled).apply()
}
```
- [ ] **Add ATON switch row to `layout_layer_picker_sheet.xml`** — read the file first, copy existing row pattern. Title: "NOAA ATON", subtitle: "Buoys, lights, daybeacons". `android:id="@+id/switch_aton"`.
- [ ] **Add `onAtonChanged` callback to `LayerPickerSheet`**, wire the switch.
- [ ] **In `MainActivity`**:
- Add `private val atonRepository by lazy { AtonRepository(this) }` field
- Call `mapHandler?.setupAtonLayer(style, layerManager.atonEnabled)` in style-load callback (after `setupFadLayer`)
- In `buildLayerPickerSheet()`, add:
```kotlin
onAtonChanged = { enabled ->
layerManager.setAtonEnabled(enabled)
val style = loadedStyleFlow.value ?: return@LayerPickerSheet
mapHandler?.setAtonLayerVisible(style, enabled)
if (enabled) {
lifecycleScope.launch {
val features = atonRepository.getFeatures()
mapHandler?.updateAtonLayer(features)
}
}
}
```
- Call `atonRepository.getFeatures()` on first GPS fix if `atonEnabled` is true — in `observeDataSources()` near the GPS collect block:
```kotlin
if (layerManager.atonEnabled && !atonLoaded) {
atonLoaded = true
lifecycleScope.launch {
mapHandler?.updateAtonLayer(atonRepository.getFeatures())
}
}
```
Add `private var atonLoaded = false` field.
- [ ] **Compile:** `./gradlew compileDebugKotlin`
- [ ] **Commit:** `git commit -m "feat(aton): NOAA ATON vector layer (buoys + lights) with 24h cache"`
---
## Task 4: Traffic Separation Scheme (TSS) overlay
**Files:**
- Create: `android-app/app/src/main/res/raw/pacific_tss.geojson`
- Modify: `android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt`
- 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`
- Modify: `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt`
Traffic separation schemes define inbound/outbound shipping lanes and separation zones. Vessels under COLREGS must avoid impeding traffic in TSS lanes. For Hawaiian waters, the relevant schemes are the approaches to Honolulu and Kawaihae. Source: NOAA / IHO.
The GeoJSON asset is bundled (no network required, static data). It contains `Polygon` features with a `"type"` property: `"lane_inbound"`, `"lane_outbound"`, `"separation_zone"`.
- [ ] **Create `res/raw/pacific_tss.geojson`** — minimal GeoJSON with the two Hawaii TSS schemes:
```json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "name": "Honolulu Inbound", "type": "lane_inbound" },
"geometry": {
"type": "Polygon",
"coordinates": [[
[-157.95, 21.30], [-157.90, 21.28], [-157.87, 21.32],
[-157.92, 21.35], [-157.95, 21.30]
]]
}
},
{
"type": "Feature",
"properties": { "name": "Honolulu Outbound", "type": "lane_outbound" },
"geometry": {
"type": "Polygon",
"coordinates": [[
[-157.90, 21.25], [-157.85, 21.23], [-157.82, 21.27],
[-157.87, 21.29], [-157.90, 21.25]
]]
}
},
{
"type": "Feature",
"properties": { "name": "Honolulu Separation Zone", "type": "separation_zone" },
"geometry": {
"type": "Polygon",
"coordinates": [[
[-157.90, 21.27], [-157.88, 21.26], [-157.87, 21.28],
[-157.89, 21.29], [-157.90, 21.27]
]]
}
}
]
}
```
**Note:** These coordinates are approximate placeholders based on publicly available NOAA chart data. Before shipping, verify against the official IHO/NOAA TSS publication for the actual polygon boundaries. If exact data is unavailable, this layer can be omitted from the initial release and added when verified coordinates are confirmed.
- [ ] **Add TSS constants + source to `MapLayerManager` companion object**:
```kotlin
const val LAYER_TSS_LANES = "tss-lanes-layer"
const val LAYER_TSS_SEP = "tss-sep-layer"
private const val KEY_TSS = "tss_enabled"
```
- [ ] **Add `tssEnabled` property and `setTssEnabled()` method** to `MapLayerManager`.
- [ ] **Add `setupTssLayer(style: Style, context: Context, visible: Boolean)` to `MapHandler`**:
```kotlin
fun setupTssLayer(style: Style, context: Context, visible: Boolean) {
val json = context.resources.openRawResource(R.raw.pacific_tss)
.bufferedReader().readText()
val src = GeoJsonSource("tss-source", FeatureCollection.fromJson(json))
style.addSource(src)
val v = if (visible) "visible" else "none"
// Lanes: semi-transparent green (inbound) / red (outbound)
style.addLayer(FillLayer("tss-lanes-layer", "tss-source").apply {
setFilter(Expression.neq(Expression.get("type"), Expression.literal("separation_zone")))
setProperties(
PropertyFactory.fillColor(
Expression.switchCase(
Expression.eq(Expression.get("type"), Expression.literal("lane_inbound")),
Expression.literal("rgba(0,180,0,0.12)"),
Expression.literal("rgba(200,0,0,0.12)")
)
),
PropertyFactory.fillOutlineColor(
Expression.switchCase(
Expression.eq(Expression.get("type"), Expression.literal("lane_inbound")),
Expression.literal("rgba(0,180,0,0.5)"),
Expression.literal("rgba(200,0,0,0.5)")
)
),
PropertyFactory.visibility(v)
)
})
// Separation zone: yellow hatch-style (solid semi-transparent yellow)
style.addLayer(FillLayer("tss-sep-layer", "tss-source").apply {
setFilter(Expression.eq(Expression.get("type"), Expression.literal("separation_zone")))
setProperties(
PropertyFactory.fillColor("rgba(255,200,0,0.25)"),
PropertyFactory.fillOutlineColor("rgba(255,200,0,0.7)"),
PropertyFactory.visibility(v)
)
})
}
```
Check `MapHandler` imports — you'll need `FillLayer` added to imports.
- [ ] **Add `setTssLayerVisible(style, visible)` to `MapHandler`**:
```kotlin
fun setTssLayerVisible(style: Style, visible: Boolean) {
val v = if (visible) "visible" else "none"
style.getLayer("tss-lanes-layer")?.setProperties(PropertyFactory.visibility(v))
style.getLayer("tss-sep-layer")?.setProperties(PropertyFactory.visibility(v))
}
```
- [ ] **Add TSS switch row to `layout_layer_picker_sheet.xml`** — title "Traffic Separation", subtitle "Shipping lane zones (COLREGS)". `android:id="@+id/switch_tss"`.
- [ ] **Add `onTssChanged` callback to `LayerPickerSheet`**.
- [ ] **Wire in `MainActivity.buildLayerPickerSheet()`** — call `setupTssLayer(style, this, ...)` in style-load callback. Pass `onTssChanged` lambda.
- [ ] **Compile:** `./gradlew compileDebugKotlin`
- [ ] **Commit:** `git commit -m "feat(layers): traffic separation scheme overlay (TSS/COLREGS lanes)"`
---
## Task 5: Update `.agent/code-map.md`
- [ ] **Add new layer sources to the `MapLayerManager` entry** in the `ui/` table.
- [ ] **Add `ui/aton/` subsection** with `AtonData.kt` and `AtonRepository.kt`.
- [ ] **Add ATON test** to the testing guide table.
- [ ] **Commit:** `git commit -m "docs(agent): update code-map for ATON + NOAA charts + TSS layers"`
---
## Testing
```bash
./gradlew testDebugUnitTest --tests "*.AtonDataTest" -q # 4 tests
./gradlew compileDebugKotlin # compile all
```
Manual test:
1. Open layer picker → enable "NOAA Charts" → chart renders at zoom ≥ 10
2. Enable "NOAA ATON" → colored dots appear at buoy/light positions; wait ~5s for API response
3. Enable "Traffic Separation" → colored lane polygons appear in Hawaiian waters
4. Disable each → layers hide correctly
---
## Notes
**NOAA chart tile scale:** `50000_1` in the URL is the chart scale code for 1:50,000. NOAA provides multiple scales: `20000_1` (1:20,000, detailed), `80000_1` (1:80,000, coastal overview). A future enhancement could auto-select scale based on zoom level, but single-scale is sufficient for V1.
**ATON API coverage:** The NOAA ATON API covers all US waters. For international waters (e.g. non-US Pacific islands), the IHO S-125 standard is the eventual authoritative source but is not yet widely available as a public API.
**TSS coordinates:** The bundled `pacific_tss.geojson` contains approximate coordinates as placeholders. If the user wants precise TSS boundaries, they should be verified against NOAA chart publication No. 1 or the `nais.gdacs.org` database and updated before shipping.
|