summaryrefslogtreecommitdiff
path: root/docs/superpowers/plans/2026-04-03-map-interaction.md
blob: 9f8fa131634f553785a0e49492521a0b344cc604 (plain)
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
# Map Interaction 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 manual pan/zoom with auto-follow GPS centering, a Recenter button that appears when the user pans away, and full UI fade-out in manual mode.

**Architecture:** `MapHandler` owns an `isFollowing: StateFlow<Boolean>` and registers a `MapLibreMap.OnCameraMoveStartedListener` to detect gesture-driven camera moves. `MainActivity` collects the flow and animates the bottom sheet, nav bar, FABs, and recenter button in/out.

**Tech Stack:** MapLibre Android SDK (`MapLibreMap.OnCameraMoveStartedListener`, `REASON_API_GESTURE`), Kotlin `StateFlow`, Android `View.animate()`.

---

## File Map

| File | What changes |
|------|-------------|
| `android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt` | Add `isFollowing` StateFlow, `lastLat`/`lastLon`, gesture listener, `recenter()`, guard in `centerOnLocation()` |
| `android-app/app/src/main/res/layout/activity_main.xml` | Add `fab_recenter` pill button inside the map ConstraintLayout |
| `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt` | Observe `mapHandler.isFollowing`, animate UI in/out, wire `fab_recenter` click |

---

### Task 1: Extend MapHandler with follow state and gesture detection

**Files:**
- Modify: `android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt`

- [ ] **Step 1: Add imports and new fields**

Open `MapHandler.kt`. Add these imports at the top (after existing imports):

```kotlin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.maplibre.android.maps.MapLibreMap
```

Add these fields inside the `MapHandler` class, before `setupLayers`:

```kotlin
private val _isFollowing = MutableStateFlow(true)
val isFollowing: StateFlow<Boolean> = _isFollowing.asStateFlow()

private var lastLat: Double = 0.0
private var lastLon: Double = 0.0
```

- [ ] **Step 2: Register the gesture listener in the constructor**

Replace the class declaration line:
```kotlin
class MapHandler(private val maplibreMap: MapLibreMap) {
```
with:
```kotlin
class MapHandler(private val maplibreMap: MapLibreMap) {

    init {
        maplibreMap.addOnCameraMoveStartedListener { reason ->
            if (reason == MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE) {
                _isFollowing.value = false
            }
        }
    }
```

- [ ] **Step 3: Guard centerOnLocation and store last position**

Replace the existing `centerOnLocation` method:
```kotlin
fun centerOnLocation(lat: Double, lon: Double, zoom: Double = 14.0) {
    lastLat = lat
    lastLon = lon
    if (!_isFollowing.value) return
    val position = CameraPosition.Builder()
        .target(LatLng(lat, lon))
        .zoom(zoom)
        .build()
    maplibreMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000)
}
```

- [ ] **Step 4: Add recenter()**

Add this method after `centerOnLocation`:
```kotlin
fun recenter() {
    _isFollowing.value = true
    centerOnLocation(lastLat, lastLon)
}
```

- [ ] **Step 5: Commit**

```bash
git add android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt
git commit -m "feat(map): add isFollowing state and gesture-driven manual mode to MapHandler"
```

---

### Task 2: Add the Recenter button to the layout

**Files:**
- Modify: `android-app/app/src/main/res/layout/activity_main.xml`

- [ ] **Step 1: Add fab_recenter inside the ConstraintLayout**

In `activity_main.xml`, find the ConstraintLayout that wraps `mapView` (around line 10). It currently contains `mapView` and `fragment_container`. Add the recenter button as a third child, before the closing `</androidx.constraintlayout.widget.ConstraintLayout>` tag:

```xml
    <com.google.android.material.button.MaterialButton
        android:id="@+id/fab_recenter"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:text="⊙ Recenter"
        android:textSize="13sp"
        android:paddingStart="20dp"
        android:paddingEnd="20dp"
        android:visibility="gone"
        app:cornerRadius="20dp"
        app:elevation="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_marginBottom="24dp" />
```

- [ ] **Step 2: Commit**

```bash
git add android-app/app/src/main/res/layout/activity_main.xml
git commit -m "feat(ui): add fab_recenter pill button to map layout"
```

---

### Task 3: Wire MainActivity to animate UI on follow state changes

**Files:**
- Modify: `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt`

- [ ] **Step 1: Add imports**

Add these imports to `MainActivity.kt` (with existing imports):
```kotlin
import androidx.cardview.widget.CardView
import com.google.android.material.button.MaterialButton
```

- [ ] **Step 2: Add view fields**

In the `MainActivity` class body, alongside the existing `fabRecordTrack` declaration, add:
```kotlin
private lateinit var fabMob: FloatingActionButton
private lateinit var fabRecenter: MaterialButton
private lateinit var bottomSheet: CardView
private lateinit var bottomNav: BottomNavigationView
```

- [ ] **Step 3: Add the fade helpers**

Add these two private methods to `MainActivity` (before `onStart`):

```kotlin
private fun fadeOut(vararg views: View, gone: Boolean = false) {
    views.forEach { v ->
        v.animate().alpha(0f).setDuration(150).withEndAction {
            v.visibility = if (gone) View.GONE else View.INVISIBLE
        }.start()
    }
}

private fun fadeIn(vararg views: View) {
    views.forEach { v ->
        v.alpha = 0f
        v.visibility = View.VISIBLE
        v.animate().alpha(1f).setDuration(150).start()
    }
}
```

- [ ] **Step 4: Wire up views and observe isFollowing in initializeUI**

In `initializeUI()`, after the existing `fabRecordTrack` setup, add:

```kotlin
fabMob = findViewById(R.id.fab_mob)
fabRecenter = findViewById(R.id.fab_recenter)
bottomSheet = findViewById(R.id.instrument_bottom_sheet)
bottomNav = findViewById(R.id.bottom_navigation)

fabRecenter.setOnClickListener {
    mapHandler?.recenter()
}
```

- [ ] **Step 5: Observe isFollowing after mapHandler is created**

In `setupMap()`, inside the `getMapAsync` lambda, after `mapHandler = MapHandler(maplibreMap)`, add:

```kotlin
lifecycleScope.launch {
    mapHandler!!.isFollowing.collect { following ->
        if (following) {
            fadeOut(fabRecenter, gone = true)
            fadeIn(bottomSheet, bottomNav, fabMob, fabRecordTrack)
        } else {
            fadeOut(bottomSheet, bottomNav, fabMob, fabRecordTrack, gone = true)
            fadeIn(fabRecenter)
        }
    }
}
```

- [ ] **Step 6: Manual smoke test**

Build and install. Verify:
1. App opens — bottom sheet, nav, FABs visible; no Recenter button
2. Pan the map — all UI fades out, Recenter button fades in
3. Tap Recenter — UI fades back in, map animates to GPS position, Recenter gone
4. GPS updates while in manual mode — map does NOT jump back to GPS position

- [ ] **Step 7: Commit**

```bash
git add android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
git commit -m "feat(map): wire UI fade-out and recenter button to MapHandler.isFollowing"
```

---

### Task 4: Request Gemini review, fix issues, loop until clean, merge

- [ ] **Step 1: Push branch and open PR**
```bash
git push local main
git push github main
```
Then open a PR (or request review inline if working on main).

- [ ] **Step 2: Request code review using code-review:code-review skill**

Invoke `code-review:code-review` skill against the PR. Address all issues with confidence ≥ 80.

- [ ] **Step 3: Loop until review returns clean**

Re-request review after each fix. Stop when no issues ≥ 80 are found.

- [ ] **Step 4: Merge and push to both remotes**
```bash
gh pr merge <number> --repo thepeterstone/nav --squash --delete-branch
git checkout main && git pull github main
git push local main
```