summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/plans/2026-04-03-map-interaction.md256
-rw-r--r--docs/superpowers/specs/2026-04-03-map-interaction-design.md59
2 files changed, 315 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-04-03-map-interaction.md b/docs/superpowers/plans/2026-04-03-map-interaction.md
new file mode 100644
index 0000000..9f8fa13
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-03-map-interaction.md
@@ -0,0 +1,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
+```
diff --git a/docs/superpowers/specs/2026-04-03-map-interaction-design.md b/docs/superpowers/specs/2026-04-03-map-interaction-design.md
new file mode 100644
index 0000000..02e38e1
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-03-map-interaction-design.md
@@ -0,0 +1,59 @@
+# Map Interaction Design
+_2026-04-03_
+
+## Overview
+
+Enable free map pan/zoom while keeping GPS auto-follow as the default. When the user gestures on the map, the UI hides and a Recenter button appears. Tapping Recenter restores auto-follow and the full UI.
+
+## State
+
+A single `isFollowing: Boolean` flag owned by `MapHandler`. Starts `true`. This is the only piece of new state.
+
+## Entering Manual Mode
+
+`MapLibreMap.addOnCameraMoveStartedListener` fires with `REASON_API_GESTURE` when the user initiates a pan, pinch-zoom, or rotate. On that event:
+
+- `MapHandler.isFollowing` → `false`
+- `MapHandler` emits via a new `StateFlow<Boolean> isFollowing` exposed to MainActivity
+- `MapHandler.centerOnLocation()` becomes a no-op while `!isFollowing` — GPS updates still arrive, last position is stored
+
+## Returning to Auto-Follow
+
+`fab_recenter` click handler:
+
+- Calls `MapHandler.recenter()` — sets `isFollowing = true`, calls `centerOnLocation(lastLat, lastLon)`
+- `isFollowing` StateFlow emits `true` → MainActivity restores UI
+
+## UI Changes
+
+### New element: `fab_recenter`
+
+- Pill-shaped button (`wrap_content` width, 40dp height, 20dp corner radius)
+- Label: "⊙ Recenter"
+- Position: centered horizontally, `24dp` above the bottom of the map `ConstraintLayout`
+- `android:visibility="gone"` by default
+- Elevation: 20dp (above the instrument sheet)
+
+### Visibility toggling (MainActivity)
+
+When `isFollowing` → `false`:
+- Fade out (alpha 0, 150ms): `instrument_bottom_sheet`, `bottom_navigation`, `fab_mob`, `fab_record_track`
+- Show `fab_recenter` (visibility VISIBLE, fade in 150ms)
+
+When `isFollowing` → `true`:
+- Hide `fab_recenter` (fade out 150ms, then GONE)
+- Fade in (alpha 1, 150ms): `instrument_bottom_sheet`, `bottom_navigation`, `fab_mob`, `fab_record_track`
+
+## Files to Change
+
+| File | Change |
+|------|--------|
+| `MapHandler.kt` | Add `isFollowing` StateFlow, `lastLat`/`lastLon` storage, `recenter()`, guard in `centerOnLocation()`, register `OnCameraMoveStartedListener` |
+| `activity_main.xml` | Add `fab_recenter` pill button |
+| `MainActivity.kt` | Observe `mapHandler.isFollowing`, animate UI in/out, wire `fab_recenter` click |
+
+## Out of Scope
+
+- Tapping the map (without gesture) to restore UI — not requested
+- Timeout to auto-restore UI — not requested
+- Zoom-level persistence across recenter — not requested