# 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` 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 = _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 `` tag: ```xml ``` - [ ] **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 --repo thepeterstone/nav --squash --delete-branch git checkout main && git pull github main git push local main ```