Skip to content

feat(samples): add 2D-to-3D Store Locator sample and seamless 2D/3D mode switching across showcase apps - #88

Merged
dkhawk merged 4 commits into
mainfrom
feat/2D-3D-Map-Samples
Oct 21, 2026
Merged

dkhawk merged 4 commits into
mainfrom
feat/2D-3D-Map-Samples

Conversation

@LoyalAbbas

Copy link
Copy Markdown
Collaborator

Summary

Introduces a canonical Store Locator (2D & 3D) sample with 100% 3-way parity across Kotlin Views, Java Views, and Jetpack Compose, and adds seamless Switch to 2D / Switch to 3D mode transitions across all 2D/abstract-capable samples and PlacesUIKit3D.

Key Changes

1. Canonical Store Locator Sample (StoreLocatorActivity & StoreLocatorViewModel)

  • Shared Clean Architecture / MVVM (Maps3DSamples/ApiDemos/common):
    • Added StoreLocation with 8 curated San Francisco locations, Haversine distance sorting from Union Square, multi-amenity filtering (OPEN_NOW, DRIVE_THRU, OPEN_24_HOURS, EV_CHARGING), and dedicated 2D (tilt = 0.0°) and 3D (tilt = 55.0°) camera presets.
    • Added StoreLocatorViewModel exposing immutable StoreLocatorUiState via StateFlow (for Kotlin/Compose) and LiveData via asLiveData() (for Java Views), preserving the active 2D/3D mode when selecting stores or filtering amenities.
  • 3-Way UI Parity (kotlin-app, java-app, ComposeDemos):
    • Implemented collapsible bottom sheet cards with a Switch to 3D / Switch to 2D toggle button, filter chips, horizontal store carousels, scaled 3D markers (1.35f on selection), and interactive 3D Popover info cards.
    • Added 2D & 3D catalog badges in MainActivity across all three sample apps for StoreLocatorActivity, RoutesActivity, RoadmapModeActivity, CloudStylingActivity, and DataVisualizationActivity.

2. Seamless 2D ↔ 3D Mode Switching & Camera Perspective Alignment

  • 2D/3D Toggle Controls: Added Switch to 3D / Switch to 2D buttons to the control headers of StoreLocatorActivity, RoutesActivity, RoadmapModeActivity, CloudStylingActivity, and DataVisualizationActivity across Kotlin, Java, and Compose, plus the top bar of PlacesUIKit3D.
  • Zero-Tilt 2D & Tilted 3D Perspectives:
    • Standardized 2D Map3DMode.ROADMAP views on tilt = 0.0° with zoomed-out city/neighborhood camera ranges (2200m–4500m) so roadmap mode renders as a clean 2D street map.
    • Anchored camera centers at ground level (altitude = 0.0) so toggling between 2D (tilt = 0.0°) and 3D (tilt = 45°–65°) pivots around the exact same geographic location without raycast drift.
    • Synchronized session-scoped Map3DSettingsController before initial camera setup so landing on a sample in 3D mode immediately applies a 3D tilted camera without redundant startup reloads.
  • Compose Map3DState Initial Sync: Seeded lastSyncedCamera and lastSyncedMapMode from Map3DInitConfig in maps3d-compose (GoogleMap3D / Map3DState) to prevent first-toggle recompositions from resetting active flyCameraTo animations.

3. Quick Settings Dialog Layout Polish

  • Updated dialog_map3d_quick_settings.xml and Map3DQuickSettingsDialog (Views) to pin the Reset to Defaults and Done action bar below a bounded <ScrollView> (layout_weight="1"), preventing bottom button clipping on smaller screens or immersive window insets.
  • Bounded the scrollable content height in Map3DQuickSettingsWidget (Compose) and made full checkbox rows touch-toggleable.

Testing

@LoyalAbbas
LoyalAbbas requested review from dkhawk and kikoso and removed request for dkhawk October 19, 2026 15:51
Comment thread Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml Fixed
Comment thread Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml Fixed

@dkhawk dkhawk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: 2D-to-3D Store Locator Sample & Mode Switching

Thank you for putting this together, @LoyalAbbas! The architecture and craftsmanship across this PR are fantastic:

  • True 3-Way Parity: Achieving identical feature sets, styling, and kinematics across Kotlin Views, Java Views, and Jetpack Compose is a high benchmark and executed cleanly.
  • MVVM Architecture: The domain separation in ApiDemos/common (StoreLocation and StoreLocatorViewModel) providing immutable StateFlow alongside asLiveData() for Java interoperability is well-structured and easy to follow.
  • Comprehensive ViewModel Testing: The 8 unit tests in StoreLocatorViewModelTest thoroughly validate filtering, selection, and mode synchronization.

We tested the PR end-to-end on device (including millisecond logcat timing and autonomous UI exploration with Artemis). Here is our feedback and suggested refinements:


1. Map Readiness & 300ms Fallback Delay (SampleBaseActivity & GoogleMap3D)

  • Observations: In SampleBaseActivity and GoogleMap3D.kt, the readiness fallback delay was reduced from 2000L to 300L.
  • Hardware Test Results: On cold launch testing on a Pixel device, onMap3DViewReady fired followed ~303ms later by onMapReady (via delay(300)). The native googleMap3D.setOnMapReadyListener never fired during the session. The 300ms delay effectively worked around this SDK quirk, and the map surface, pins, and controls loaded cleanly behind the initial loading spinner.
  • Recommendation: Because setOnMapReadyListener is largely a no-op or one-shot in play-services-maps3d:16.0.1, please add a brief code comment documenting why the 300ms fallback exists, so future maintainers understand it is an intentional workaround for the native callback.

2. Marker Re-Creation & Selection Performance in Views (StoreLocatorActivity)

  • Location: StoreLocatorActivity.kt (lines 281–352) & StoreLocatorActivity.java (lines 321–391)
  • The Issue: In syncMapMarkersAndCamera:
    if (currentIds != lastRenderedStoreIds || selectedId != lastRenderedSelectedStoreId)
    This conflates dataset changes (filtering amenities) with selection changes (user tapped a store card or pin).
  • Impact: Whenever any store is clicked, rebuildStoreMarkers executes activeMarkers.values.forEach { it.remove() }, clears the map, and invokes map.addMarker(...) for all 8 stores from scratch. Destroying and re-creating 3D extruded pins causes noticeable pin flickering on the map and unnecessary native allocations at the exact moment the camera begins its flyCameraTo animation.
  • Recommendation: Separate filtering from selection:
    1. Dataset changes (currentIds != lastRenderedStoreIds): Diff and add/remove only the markers that entered or exited the filtered list.
    2. Selection changes (selectedId != lastRenderedSelectedStoreId): Selectively update only the previously selected marker (reverting scale to 1.0f, zIndex = 1, and blue/gray color) and the newly selected marker (elevating scale to 1.35f, zIndex = 10, and Google Yellow #F4B400). Keep the other 6 markers completely untouched.
    3. Lifecycle: Add onDestroy() overrides in both StoreLocatorActivity classes to explicitly remove activePopover and activeMarkers.

3. Product Identity: Default Mode in PlacesUIKit3D

  • Location: PlacesUIKit3D/src/main/java/com/example/placesuikit3d/ui/viewmodel/PlaceSearch3DViewModel.kt
  • The Issue: Changing the default launch state of PlacesUIKit3D from 3D Hybrid view to 2D Roadmap (tilt = 0.0).
  • Feedback: This module is our flagship showcase for PlacesUIKit3D—demonstrating photorealistic 3D building meshes, orbital camera maneuvers, and 3D pins anchored to building roofs (AltitudeMode.RELATIVE_TO_MESH). Defaulting to a flat 2D roadmap hides the core 3D value proposition on first launch. Having a 2D switch is great, but PlacesUIKit3D should likely remain 3D by default on startup.

4. Duplicate Quick Settings Dialog in PlacesUIKit3D

  • Location: PlacesUIKit3D/src/main/java/com/example/placesuikit3d/MainActivity.kt
  • Feedback: The PR adds ~180 lines implementing an ad-hoc AlertDialog with RadioButtons and Switches for map modes, color schemes, and camera controls. This duplicates the exact layout and logic of Map3DQuickSettingsWidget (Compose) and Map3DQuickSettingsDialog (Views). Maintaining two parallel copies of settings UI across sample modules creates tech debt when SDK control APIs evolve.

5. Scope & Granularity

  • Feedback: This PR touches 56 files across 5 distinct modules (Store Locator across 3 frameworks, 2D/3D mode-switching in 5 existing samples, PlacesUIKit3D feature work, and maps3d-compose library changes). Consider splitting into:
    • PR 1: Canonical Store Locator sample (Kotlin, Java, Compose) and its core dependencies.
    • PR 2: Cross-app 2D/3D mode-switching in existing showcase apps and PlacesUIKit3D.
    • Benefit: Greatly simplifies review, minimizes blast radius, and preserves clean git bisect history.

6. Android Lint & Code Polish

  • GitHub Advanced Security / Lint:
    • Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml:325: Flagged spelling warning for "Drive-Thru". Please add tools:ignore="Spelling" if intentional.
    • Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml:333: Flagged Potential Plurals for "%1$d of %2$d stores". Please use <plurals> or add tools:ignore="PluralsCandidate".
  • Hardcoded Strings:
    • In MainActivity.kt (Kotlin & Compose), text = "2D / ROADMAP" is hardcoded in Compose rather than referencing a string resource.
    • In StoreLocation.kt, StoreAmenity hardcodes English label strings (val label: String) instead of string resource IDs (@StringRes val labelResId: Int).
    • In PlacesRepositoryImpl.kt (line 441), category matching uses inline string regex heuristics (cleaned.contains("coffee"), "park", etc.). It would be cleaner to map standard Places API types (Place.Type) or use a structured enum map.
  • Redundant Startup Invocations:
    • In SampleBaseActivity.kt and SampleBaseActivity.java, onSampleEntered(), applyTo(), and googleMap3D.setCamera(initialCamera) are called during onMap3DViewReady and then executed again inside onMapReady. Consolidating these avoids duplicate camera snapping or interrupting flight animations on startup.

@dkhawk
dkhawk enabled auto-merge (squash) October 20, 2026 17:05
Comment thread Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml Fixed
LoyalAbbas and others added 3 commits October 20, 2026 12:43
…nt warnings

- Decouple dataset filtering reconciliation from marker selection state
- Selectively update only previous and newly selected markers to prevent full marker teardown churn on store card tap
- Add explicit onDestroy cleanup for active popovers and markers in Kotlin and Java StoreLocatorActivity
- Resolve strings.xml lint warnings for 'Drive-Thru' spelling and plurals candidate
@dkhawk
dkhawk force-pushed the feat/2D-3D-Map-Samples branch from 0b2e4e9 to 8f83cc9 Compare October 20, 2026 19:05
@dkhawk
dkhawk disabled auto-merge October 21, 2026 17:26
@dkhawk
dkhawk merged commit 2bd2fe7 into main Oct 21, 2026
13 checks passed
@dkhawk
dkhawk deleted the feat/2D-3D-Map-Samples branch October 21, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants