diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/activity_main.xml b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/activity_main.xml index 53a6692d..f1f25395 100644 --- a/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/activity_main.xml +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/activity_main.xml @@ -11,7 +11,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" map3d:mapId="your_map_id" - map3d:mode="hybrid" + map3d:mapMode="hybrid" map3d:centerLat="40.748392" map3d:centerLng="-73.986060" map3d:centerAlt="175" diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml index 53a6692d..f1f25395 100644 --- a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml @@ -11,7 +11,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" map3d:mapId="your_map_id" - map3d:mode="hybrid" + map3d:mapMode="hybrid" map3d:centerLat="40.748392" map3d:centerLng="-73.986060" map3d:centerAlt="175" diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/LocalizationController.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/LocalizationController.kt new file mode 100644 index 00000000..be865969 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/LocalizationController.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.maps3d.common + +import com.google.android.gms.maps3d.model.Map3DMode + +/** + * Pure Kotlin state machine controller for Maps Localization. + * + * Manages transitions between [MapLocalePreset] configurations and map rendering modes + * ([Map3DMode.HYBRID] vs [Map3DMode.ROADMAP]). + */ +class LocalizationController( + initialPreset: MapLocalePreset = MapLocalePreset.ENGLISH_US, +) { + private var state = LocalizationState( + selectedPreset = initialPreset, + ) + + /** Returns the current immutable [LocalizationState]. */ + fun getState(): LocalizationState = state + + /** + * Selects a new [MapLocalePreset] and updates language and region. + */ + fun selectPreset(preset: MapLocalePreset): LocalizationState { + state = state.copy( + selectedPreset = preset, + languageCode = preset.language, + regionCode = preset.region, + ) + return state + } + + /** + * Updates the 3D map rendering mode (`Map3DMode.HYBRID` or `Map3DMode.ROADMAP`). + */ + fun setMapMode(@Map3DMode mapMode: Int): LocalizationState { + state = state.copy(mapMode = mapMode) + return state + } +} diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/LocalizationData.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/LocalizationData.kt new file mode 100644 index 00000000..5725c163 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/LocalizationData.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.maps3d.common + +import com.google.android.gms.maps3d.model.Camera +import com.google.android.gms.maps3d.model.LocaleOptions +import com.google.android.gms.maps3d.model.Map3DMode +import com.google.android.gms.maps3d.model.camera +import com.google.android.gms.maps3d.model.latLngAltitude +import java.util.Locale + +private const val SYSTEM_DEFAULT_CODE = "default" + +val DEFAULT_LOCALIZATION_CAMERA: Camera = camera { + center = latLngAltitude { + latitude = 37.8080 + longitude = -122.4177 + altitude = 0.0 + } + tilt = 45.0 + heading = 340.0 + range = 18500.0 + roll = 0.0 +} + +/** + * Curated language and region presets for the Maps Localization sample. + */ +enum class MapLocalePreset( + val displayName: String, + val nativeLabel: String, + val language: String, + val region: String, +) { + ENGLISH_US( + displayName = "English (US)", + nativeLabel = "en-US", + language = "en", + region = "US", + ), + CHINESE_CHINA( + displayName = "Chinese (China)", + nativeLabel = "中文 (zh-CN)", + language = "zh", + region = "CN", + ), + FRENCH_FRANCE( + displayName = "French (France)", + nativeLabel = "Français (fr-FR)", + language = "fr", + region = "FR", + ), + JAPANESE_JAPAN( + displayName = "Japanese (Japan)", + nativeLabel = "日本語 (ja-JP)", + language = "ja", + region = "JP", + ), + SPANISH_SPAIN( + displayName = "Spanish (Spain)", + nativeLabel = "Español (es-ES)", + language = "es", + region = "ES", + ), + ARABIC_SAUDI_ARABIA( + displayName = "Arabic (Saudi Arabia)", + nativeLabel = "العربية (ar-SA)", + language = "ar", + region = "SA", + ), + GERMAN_GERMANY( + displayName = "German (Germany)", + nativeLabel = "Deutsch (de-DE)", + language = "de", + region = "DE", + ), + SYSTEM_DEFAULT( + displayName = "System Default", + nativeLabel = SYSTEM_DEFAULT_CODE, + language = Locale.getDefault().language, + region = Locale.getDefault().country, + ) +} + +/** + * Immutable UI state for the Maps Localization sample across Kotlin Views, Java Views, and Compose. + * + * @property selectedPreset Currently active [MapLocalePreset]. + * @property languageCode Active ISO-639 language code (`"en"`, `"zh"`, `"fr"`, etc.). + * @property regionCode Active ISO-3166-1 alpha-2 region code (`"US"`, `"CN"`, `"FR"`, etc.). + * @property mapMode Current [Map3DMode] (`HYBRID` or `ROADMAP` to display localized labels). + * @property camera Target [Camera] position. + */ +data class LocalizationState( + val selectedPreset: MapLocalePreset = MapLocalePreset.ENGLISH_US, + val languageCode: String = selectedPreset.language, + val regionCode: String = selectedPreset.region, + @param:Map3DMode val mapMode: Int = Map3DMode.HYBRID, + val camera: Camera = DEFAULT_LOCALIZATION_CAMERA, +) { + /** Formatted locale badge text (e.g. `"zh-CN"` or `"default (en_US)"`). */ + val localeBadgeText: String + get() = if (selectedPreset == MapLocalePreset.SYSTEM_DEFAULT) { + "default (${Locale.getDefault()})" + } else { + "$languageCode-$regionCode" + } + + /** + * Builds a [LocaleOptions] instance matching the current [languageCode] and [regionCode] + * for runtime locale updates via [com.google.android.gms.maps3d.GoogleMap3D.setLocale]. + */ + fun toLocaleOptions(): LocaleOptions = LocaleOptions().apply { + language = languageCode + region = regionCode + } +} diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/MapsLocalizationViewModel.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/MapsLocalizationViewModel.kt new file mode 100644 index 00000000..d1821932 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/MapsLocalizationViewModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.maps3d.common + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import com.google.android.gms.maps3d.model.Map3DMode +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Shared Architecture ViewModel for Maps Localization across Kotlin Views, Java Views, and + * Jetpack Compose. + * + * Exposes immutable [LocalizationState] via [StateFlow] (for Kotlin/Compose) and [LiveData] + * (for Java Views via [asLiveData]). + */ +class MapsLocalizationViewModel( + initialPreset: MapLocalePreset = MapLocalePreset.ENGLISH_US, +) : ViewModel() { + + private val controller = LocalizationController(initialPreset) + + private val _uiState = MutableStateFlow(controller.getState()) + val uiState: StateFlow = _uiState.asStateFlow() + + val localizationStateData: LiveData = _uiState.asLiveData() + + val currentState: LocalizationState + get() = _uiState.value + + /** Selects a [MapLocalePreset] and updates the language and region. */ + fun selectPreset(preset: MapLocalePreset) { + _uiState.value = controller.selectPreset(preset) + } + + /** Switches between [Map3DMode.HYBRID] and [Map3DMode.ROADMAP] to inspect localized labels. */ + fun setMapMode(@Map3DMode mapMode: Int) { + _uiState.value = controller.setMapMode(mapMode) + } +} diff --git a/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_camera_controls.xml b/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_camera_controls.xml index d1f1c658..8b3ebd45 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_camera_controls.xml +++ b/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_camera_controls.xml @@ -53,7 +53,7 @@ map3d:minAltitude="0" map3d:minHeading="0" map3d:minTilt="0" - map3d:mode="satellite" + map3d:mapMode="satellite" map3d:range="4000" map3d:roll="0" map3d:tilt="67" /> diff --git a/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_common_map.xml b/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_common_map.xml index c7452983..175d9f17 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_common_map.xml +++ b/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_common_map.xml @@ -47,7 +47,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml b/Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml index 138b4de3..b31206e6 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml +++ b/Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml @@ -44,6 +44,7 @@ Cloud Map Styling Roadmap Mode Field Of View + Maps Localization Coming soon! @@ -246,4 +247,11 @@ 3D Model Animation Mode: Synchronized (High CPU) Midpoint Jump (Low CPU) + + + 3D Map Localization Controls + Change Language & Region + %1$s [%2$s] + Applied %1$s [%2$s] + Location: San Francisco Bay, USA diff --git a/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/LocalizationControllerTest.kt b/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/LocalizationControllerTest.kt new file mode 100644 index 00000000..5780e950 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/LocalizationControllerTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.maps3d.common + +import com.google.android.gms.maps3d.model.Map3DMode +import com.google.common.truth.Truth.assertThat +import java.util.Locale +import org.junit.Before +import org.junit.Test + +/** + * Tier 2 unit tests for [LocalizationController] and [MapLocalePreset]. + */ +class LocalizationControllerTest { + + private lateinit var controller: LocalizationController + + @Before + fun setUp() { + controller = LocalizationController() + } + + @Test + fun `getState defaults to English US in HYBRID mode`() { + val state = controller.getState() + + assertThat(state.selectedPreset).isEqualTo(MapLocalePreset.ENGLISH_US) + assertThat(state.languageCode).isEqualTo("en") + assertThat(state.regionCode).isEqualTo("US") + assertThat(state.localeBadgeText).isEqualTo("en-US") + assertThat(state.mapMode).isEqualTo(Map3DMode.HYBRID) + } + + @Test + fun `selectPreset updates language and region options`() { + val updated = controller.selectPreset(MapLocalePreset.CHINESE_CHINA) + + assertThat(updated.selectedPreset).isEqualTo(MapLocalePreset.CHINESE_CHINA) + assertThat(updated.languageCode).isEqualTo("zh") + assertThat(updated.regionCode).isEqualTo("CN") + assertThat(updated.localeBadgeText).isEqualTo("zh-CN") + + val localeOptions = updated.toLocaleOptions() + assertThat(localeOptions.language).isEqualTo("zh") + assertThat(localeOptions.region).isEqualTo("CN") + } + + @Test + fun `selectPreset SYSTEM_DEFAULT resolves default LocaleOptions`() { + controller.selectPreset(MapLocalePreset.FRENCH_FRANCE) + + val resetState = controller.selectPreset(MapLocalePreset.SYSTEM_DEFAULT) + val expectedDefaultLocale = Locale.getDefault() + + assertThat(resetState.selectedPreset).isEqualTo(MapLocalePreset.SYSTEM_DEFAULT) + assertThat(resetState.languageCode).isEqualTo(expectedDefaultLocale.language) + assertThat(resetState.regionCode).isEqualTo(expectedDefaultLocale.country) + assertThat(resetState.localeBadgeText).startsWith("default (") + + val localeOptions = resetState.toLocaleOptions() + assertThat(localeOptions.language).isEqualTo(expectedDefaultLocale.language) + assertThat(localeOptions.region).isEqualTo(expectedDefaultLocale.country) + } + + @Test + fun `setMapMode updates mapMode cleanly`() { + val updatedState = controller.setMapMode(Map3DMode.ROADMAP) + assertThat(updatedState.mapMode).isEqualTo(Map3DMode.ROADMAP) + } +} diff --git a/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/MapsLocalizationViewModelTest.kt b/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/MapsLocalizationViewModelTest.kt new file mode 100644 index 00000000..73ebf6ce --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/MapsLocalizationViewModelTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.maps3d.common + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.google.android.gms.maps3d.model.Map3DMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +/** + * Tier 3 JVM Unit Tests for [MapsLocalizationViewModel]. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class MapsLocalizationViewModelTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private val testDispatcher = StandardTestDispatcher() + private lateinit var viewModel: MapsLocalizationViewModel + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + viewModel = MapsLocalizationViewModel() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun selectPreset_updatesStateFlowAndLiveData() = runTest { + val observedStates = mutableListOf() + viewModel.localizationStateData.observeForever { observedStates.add(it) } + + viewModel.selectPreset(MapLocalePreset.CHINESE_CHINA) + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(MapLocalePreset.CHINESE_CHINA, state.selectedPreset) + assertEquals("zh", state.languageCode) + assertEquals("CN", state.regionCode) + assertEquals(MapLocalePreset.CHINESE_CHINA, observedStates.last().selectedPreset) + } + + @Test + fun setMapMode_dispatchesCleanly() = runTest { + viewModel.selectPreset(MapLocalePreset.GERMAN_GERMANY) + + viewModel.setMapMode(Map3DMode.ROADMAP) + assertEquals(Map3DMode.ROADMAP, viewModel.currentState.mapMode) + } +} diff --git a/Maps3DSamples/ApiDemos/java-app/src/androidTest/java/com/example/maps3djava/MapsLocalizationVisualTest.java b/Maps3DSamples/ApiDemos/java-app/src/androidTest/java/com/example/maps3djava/MapsLocalizationVisualTest.java new file mode 100644 index 00000000..a1a34de8 --- /dev/null +++ b/Maps3DSamples/ApiDemos/java-app/src/androidTest/java/com/example/maps3djava/MapsLocalizationVisualTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.maps3djava; + +import static org.junit.Assert.assertTrue; + +import android.content.Intent; +import android.graphics.Bitmap; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.Until; +import com.example.maps3djava.mapslocalization.MapsLocalizationActivity; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tier 4 Visual Regression Test for the Java Views {@link MapsLocalizationActivity} sample. + */ +@RunWith(AndroidJUnit4.class) +public class MapsLocalizationVisualTest extends BaseVisualTest { + + @Test + public void verifyMapsLocalizationRenders() { + Intent intent = new Intent(context, MapsLocalizationActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + + uiDevice.wait(Until.hasObject(By.pkg(context.getPackageName()).depth(0)), 10000); + waitForMapRendering(15); + + Bitmap screenshotBitmap = captureScreenshot("maps_localization_screenshot.png"); + + String prompt = + "Please act as a UI tester and analyze this screenshot.\n" + + "1. Confirm that a 3D map view is visible in Hybrid or Roadmap mode with place labels.\n" + + "2. Confirm that the bottom 3D Map Localization Controls card is visible (showing the language selection button and location label below it).\n" + + "\n" + + "If and ONLY IF you can clearly see the 3D map scene and the bottom Localization Controls card, reply with \"PASSED\".\n" + + "If you cannot see the 3D map scene or control card, reply with \"FAILED: 3D Map Localization scene or controls not visible\".\n" + + "Report what you see in detail."; + + String geminiResponse = helper.analyzeImageBlocking(screenshotBitmap, prompt, geminiApiKey); + System.out.println("Gemini's analysis: " + geminiResponse); + + assertTrue( + "Visual verification failed. Gemini response: " + geminiResponse, + geminiResponse != null && geminiResponse.toUpperCase().contains("PASSED")); + } +} diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml b/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml index 7649c6cd..fb241e1e 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml +++ b/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml @@ -200,6 +200,13 @@ android:exported="true" /> + + diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mainactivity/MainActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mainactivity/MainActivity.java index 7e4b9f0d..7efcefb7 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mainactivity/MainActivity.java +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mainactivity/MainActivity.java @@ -51,6 +51,7 @@ import com.example.maps3djava.cloudstyling.CloudStylingActivity; import com.example.maps3djava.roadmapmode.RoadmapModeActivity; import com.example.maps3djava.fieldofview.FieldOfViewActivity; +import com.example.maps3djava.mapslocalization.MapsLocalizationActivity; import com.google.android.material.appbar.MaterialToolbar; import java.util.LinkedHashMap; @@ -81,6 +82,7 @@ public class MainActivity extends AppCompatActivity { put(R.string.feature_title_cloud_styling, CloudStylingActivity.class); put(R.string.feature_title_roadmap_mode, RoadmapModeActivity.class); put(R.string.feature_title_field_of_view, FieldOfViewActivity.class); + put(R.string.feature_title_maps_localization, MapsLocalizationActivity.class); }}; @Override diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mapinteractions/MapInteractionsActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mapinteractions/MapInteractionsActivity.java index bed8625e..ca6eba21 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mapinteractions/MapInteractionsActivity.java +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mapinteractions/MapInteractionsActivity.java @@ -72,7 +72,7 @@ protected void onCreate(Bundle savedInstanceState) { @Override public void onMap3DViewReady(GoogleMap3D googleMap3D) { super.onMap3DViewReady(googleMap3D); - + googleMap3D.setOnMapReadyListener((map) -> { googleMap3D.setOnMapReadyListener(null); initializeMap(googleMap3D); @@ -94,18 +94,21 @@ private void initializeMap(@NonNull GoogleMap3D googleMap3D) { googleMap3D.setMapMode(Map3DMode.HYBRID); // Listeners for map clicks. - googleMap3D.setMap3DClickListener((location, placeId) -> { + googleMap3D.setMap3DClickListener((event) -> { runOnUiThread(() -> { String message; - if (placeId != null) { - message = "Clicked Place ID: " + placeId; + if (event.getPlaceId() != null) { + message = "Clicked Place ID: " + event.getPlaceId(); } else { - message = "Clicked Location: " + location.getLatitude() + ", " + location.getLongitude(); + message = "Clicked Location: " + event.getLocation().getLatitude() + ", " + + event.getLocation().getLongitude(); } clickedInfoText.setText(message); clickedInfoText.setContentDescription(message); showToast(message); + }); + return true; }); } diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mapslocalization/MapsLocalizationActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mapslocalization/MapsLocalizationActivity.java new file mode 100644 index 00000000..50757f02 --- /dev/null +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mapslocalization/MapsLocalizationActivity.java @@ -0,0 +1,235 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.maps3djava.mapslocalization; + +import android.os.Bundle; +import android.transition.TransitionManager; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RadioGroup; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.lifecycle.ViewModelProvider; +import com.example.maps3d.common.LocalizationDataKt; +import com.example.maps3d.common.LocalizationState; +import com.example.maps3d.common.MapLocalePreset; +import com.example.maps3d.common.MapsLocalizationViewModel; +import com.example.maps3dcommon.R; +import com.example.maps3djava.sampleactivity.SampleBaseActivity; +import com.google.android.gms.maps3d.GoogleMap3D; +import com.google.android.gms.maps3d.Map3DView; +import com.google.android.gms.maps3d.model.Camera; +import com.google.android.gms.maps3d.model.Map3DMode; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import java.util.List; + +/** + * Java Views implementation of the 3D Map Localization sample (`language` and `region` via {@link + * com.google.android.gms.maps3d.model.LocaleOptions} and {@link GoogleMap3D#setLocale}). + * + *

Dynamically updates the map language and region in-place on the live {@link GoogleMap3D} + * instance without recreating {@link Map3DView} or reloading the map scene. + */ +public class MapsLocalizationActivity extends SampleBaseActivity { + + private static final String TAG = "MapsLocalizationActivity"; + + @Override + public Camera getInitialCamera() { + return LocalizationDataKt.getDEFAULT_LOCALIZATION_CAMERA(); + } + + @Override + public String getTAG() { + return TAG; + } + + private MapsLocalizationViewModel viewModel; + + private LinearLayout collapsibleContent; + private MaterialButton collapseToggleButton; + private MaterialButton changeLanguageButton; + private RadioGroup mapModeRadioGroup; + + private boolean isCollapsed = false; + private boolean isUpdatingUiFromState = false; + private MapLocalePreset activePreset = MapLocalePreset.ENGLISH_US; + private int activeMapMode = Map3DMode.HYBRID; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + viewModel = new ViewModelProvider(this).get(MapsLocalizationViewModel.class); + + ConstraintLayout rootContainer = findViewById(R.id.map_container); + View controlPanel = + LayoutInflater.from(this) + .inflate(R.layout.control_panel_maps_localization, rootContainer, false); + rootContainer.addView(controlPanel); + + ViewCompat.setOnApplyWindowInsetsListener( + controlPanel, + (view, windowInsets) -> { + Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + ConstraintLayout.LayoutParams layoutParams = + (ConstraintLayout.LayoutParams) view.getLayoutParams(); + int margin12dp = (int) (12 * getResources().getDisplayMetrics().density); + layoutParams.bottomMargin = insets.bottom + margin12dp; + view.setLayoutParams(layoutParams); + return windowInsets; + }); + + bindControlPanelViews(controlPanel); + setupControlListeners(); + observeViewModel(); + } + + + @Override + public void onMap3DViewReady(@NonNull GoogleMap3D googleMap3D) { + super.onMap3DViewReady(googleMap3D); + LocalizationState state = viewModel.getCurrentState(); + activePreset = state.getSelectedPreset(); + activeMapMode = state.getMapMode(); + googleMap3D.setMapMode(state.getMapMode()); + googleMap3D.setLocale(state.toLocaleOptions()); + } + + private void bindControlPanelViews(View panel) { + collapsibleContent = panel.findViewById(R.id.card_content); + collapseToggleButton = panel.findViewById(R.id.btn_collapse); + changeLanguageButton = panel.findViewById(R.id.btn_change_language); + mapModeRadioGroup = panel.findViewById(R.id.rg_localization_map_mode); + + panel.findViewById(R.id.card_header).setOnClickListener(v -> togglePanelCollapse()); + collapseToggleButton.setOnClickListener(v -> togglePanelCollapse()); + } + + private void setupControlListeners() { + changeLanguageButton.setOnClickListener(v -> showLanguageSelectionDialog()); + + mapModeRadioGroup.setOnCheckedChangeListener( + (group, checkedId) -> { + if (isUpdatingUiFromState) { + return; + } + int targetMode = + (checkedId == R.id.rb_mode_roadmap) + ? Map3DMode.ROADMAP + : Map3DMode.HYBRID; + viewModel.setMapMode(targetMode); + }); + } + + private void showLanguageSelectionDialog() { + List presets = MapLocalePreset.getEntries(); + String[] labels = new String[presets.size()]; + for (int i = 0; i < presets.size(); i++) { + MapLocalePreset preset = presets.get(i); + labels[i] = preset.getDisplayName() + " — " + preset.getNativeLabel(); + } + + MapLocalePreset currentPreset = viewModel.getCurrentState().getSelectedPreset(); + int currentIndex = Math.max(0, presets.indexOf(currentPreset)); + + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.change_language_button) + .setSingleChoiceItems( + labels, + currentIndex, + (dialog, which) -> { + MapLocalePreset chosenPreset = presets.get(which); + dialog.dismiss(); + if (chosenPreset != currentPreset) { + viewModel.selectPreset(chosenPreset); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void observeViewModel() { + viewModel.getLocalizationStateData() + .observe( + this, + state -> { + renderUiState(state); + GoogleMap3D map = this.googleMap3D; + if (map != null) { + applyStateToMap(map, state); + } + }); + } + + private void renderUiState(LocalizationState state) { + isUpdatingUiFromState = true; + try { + changeLanguageButton.setText( + getString( + R.string.language_button_format, + state.getSelectedPreset().getDisplayName(), + state.getLocaleBadgeText())); + + int expectedRadioId = + (state.getMapMode() == Map3DMode.ROADMAP) + ? R.id.rb_mode_roadmap + : R.id.rb_mode_hybrid; + if (mapModeRadioGroup.getCheckedRadioButtonId() != expectedRadioId) { + mapModeRadioGroup.check(expectedRadioId); + } + } finally { + isUpdatingUiFromState = false; + } + } + + private void applyStateToMap(GoogleMap3D map, LocalizationState state) { + if (state.getMapMode() != activeMapMode) { + activeMapMode = state.getMapMode(); + map.setMapMode(state.getMapMode()); + } + + if (state.getSelectedPreset() != activePreset) { + activePreset = state.getSelectedPreset(); + map.setLocale(state.toLocaleOptions()); + Toast.makeText( + this, + getString( + R.string.localization_applied_format, + state.getSelectedPreset().getDisplayName(), + state.getLocaleBadgeText()), + Toast.LENGTH_SHORT) + .show(); + } + } + + private void togglePanelCollapse() { + ConstraintLayout rootContainer = findViewById(R.id.map_container); + TransitionManager.beginDelayedTransition(rootContainer); + isCollapsed = !isCollapsed; + collapsibleContent.setVisibility(isCollapsed ? View.GONE : View.VISIBLE); + collapseToggleButton.setIconResource( + isCollapsed ? R.drawable.expand_less_24px : R.drawable.expand_more_24px); + } +} diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/markers/MarkersActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/markers/MarkersActivity.java index e8963123..e49f12cb 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/markers/MarkersActivity.java +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/markers/MarkersActivity.java @@ -14,32 +14,24 @@ package com.example.maps3djava.markers; -import com.google.android.gms.maps3d.model.FlyAroundOptions; -import com.google.android.gms.maps3d.model.PopoverOptions; -import com.google.android.gms.maps3d.Popover; -import org.json.JSONObject; -import org.json.JSONArray; -import android.graphics.Color; -import android.util.Log; -import android.widget.TextView; -import android.widget.PopupMenu; -import java.util.concurrent.CompletableFuture; -import android.os.Looper; -import android.os.Handler; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Random; import static com.example.maps3d.common.UtilitiesKt.toValidCamera; +import android.graphics.Color; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; import android.view.View; import android.widget.Button; - +import android.widget.PopupMenu; +import android.widget.TextView; import com.example.maps3dcommon.R; import com.example.maps3djava.sampleactivity.SampleBaseActivity; import com.google.android.gms.maps3d.GoogleMap3D; +import com.google.android.gms.maps3d.Popover; import com.google.android.gms.maps3d.model.AltitudeMode; import com.google.android.gms.maps3d.model.Camera; import com.google.android.gms.maps3d.model.CollisionBehavior; +import com.google.android.gms.maps3d.model.FlyAroundOptions; import com.google.android.gms.maps3d.model.FlyToOptions; import com.google.android.gms.maps3d.model.Glyph; import com.google.android.gms.maps3d.model.ImageView; @@ -48,9 +40,13 @@ import com.google.android.gms.maps3d.model.Marker; import com.google.android.gms.maps3d.model.MarkerOptions; import com.google.android.gms.maps3d.model.PinConfiguration; - -import java.util.List; +import com.google.android.gms.maps3d.model.PopoverOptions; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CompletableFuture; /** * Demonstrates the use of different altitude modes for markers in a 3D map. @@ -111,9 +107,9 @@ public final Camera getInitialCamera() { @Override public void onMap3DViewReady(GoogleMap3D googleMap3D) { super.onMap3DViewReady(googleMap3D); - + Log.d(getTAG(), "onMap3DViewReady called"); - + googleMap3D.setOnMapReadyListener((map) -> { Log.w(getTAG(), "on map ready listener fired"); googleMap3D.setOnMapReadyListener(null); @@ -201,13 +197,17 @@ private void initializeMap(GoogleMap3D googleMap3D) { stopButton.setOnClickListener(v -> stopMonsterTour(googleMap3D)); } - googleMap3D.setMap3DClickListener((location, placeId) -> { - runOnUiThread(() -> { - if (activePopover != null) { - activePopover.remove(); - activePopover = null; - } - }); + googleMap3D.setMap3DClickListener((event) -> { + boolean hadActivePopover = activePopover != null; + if (hadActivePopover) { + runOnUiThread(() -> { + if (activePopover != null) { + activePopover.remove(); + activePopover = null; + } + }); + } + return hadActivePopover; }); // Marker 1: Absolute Altitude @@ -594,4 +594,4 @@ private int getMonsterBlurbResId(String monsterId) { return 0; } } -} \ No newline at end of file +} diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/androidTest/java/com/example/maps3dkotlin/MapsLocalizationVisualTest.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/androidTest/java/com/example/maps3dkotlin/MapsLocalizationVisualTest.kt new file mode 100644 index 00000000..86b6b378 --- /dev/null +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/androidTest/java/com/example/maps3dkotlin/MapsLocalizationVisualTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.maps3dkotlin + +import android.content.Intent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.example.maps3dkotlin.mapslocalization.MapsLocalizationActivity +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tier 4 Visual Regression Test for the Kotlin Views [MapsLocalizationActivity] sample. + */ +@RunWith(AndroidJUnit4::class) +class MapsLocalizationVisualTest : BaseVisualTest() { + + @Test + fun verifyMapsLocalizationRenders() { + runBlocking { + val intent = Intent(context, MapsLocalizationActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + + uiDevice.wait(Until.hasObject(By.pkg(context.packageName).depth(0)), 10000) + waitForMapRendering(15) + + val screenshotBitmap = captureScreenshot("maps_localization_screenshot.png") + + val prompt = """ + Please act as a UI tester and analyze this screenshot. + 1. Confirm that a 3D map view is visible in Hybrid or Roadmap mode with labels. + 2. Confirm that the bottom 3D Map Localization Controls card is visible + (showing the language selection button and location label below it). + + If and ONLY IF you can clearly see the 3D map scene and the bottom Localization + Controls card, reply with "PASSED". + If you cannot see the 3D map scene or control card, reply with + "FAILED: 3D Map Localization scene or controls not visible". + Report what you see in detail. + """.trimIndent() + + val geminiResponse = helper.analyzeImage(screenshotBitmap, prompt, geminiApiKey) + println("Gemini's analysis: $geminiResponse") + + assertTrue( + "Visual verification failed. Gemini response: $geminiResponse", + geminiResponse?.contains("PASSED", ignoreCase = true) == true, + ) + } + } +} diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/AndroidManifest.xml b/Maps3DSamples/ApiDemos/kotlin-app/src/main/AndroidManifest.xml index 5de5d795..f4762c49 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/AndroidManifest.xml +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/AndroidManifest.xml @@ -203,6 +203,13 @@ android:exported="true" /> + + diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt index 00fd104b..6a43c0da 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt @@ -67,6 +67,7 @@ import com.example.maps3dkotlin.datavisualization.DataVisualizationActivity import com.example.maps3dkotlin.cloudstyling.CloudStylingActivity import com.example.maps3dkotlin.roadmapmode.RoadmapModeActivity import com.example.maps3dkotlin.fieldofview.FieldOfViewActivity +import com.example.maps3dkotlin.mapslocalization.MapsLocalizationActivity import com.example.maps3dkotlin.theme.Maps3DSamplesTheme import kotlinx.coroutines.launch @@ -114,6 +115,7 @@ class MainActivity : ComponentActivity() { Sample(R.string.feature_title_cloud_styling, CloudStylingActivity::class.java), Sample(R.string.feature_title_roadmap_mode, RoadmapModeActivity::class.java), Sample(R.string.feature_title_field_of_view, FieldOfViewActivity::class.java), + Sample(R.string.feature_title_maps_localization, MapsLocalizationActivity::class.java), ) @OptIn(ExperimentalMaterial3Api::class) diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mapinteractions/MapInteractionsActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mapinteractions/MapInteractionsActivity.kt index 8a42f1f9..0eee3827 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mapinteractions/MapInteractionsActivity.kt +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mapinteractions/MapInteractionsActivity.kt @@ -62,17 +62,18 @@ class MapInteractionsActivity : SampleBaseActivity() { // Listeners for map clicks. We use lifecycleScope to ensure coroutines are cancelled when the activity is destroyed. lifecycleScope.launch { - googleMap3D.setMap3DClickListener { location, placeId -> + googleMap3D.setMap3DClickListener { event -> runOnUiThread { - val message = if (placeId != null) { - "Clicked Place ID: $placeId" + val message = if (event.placeId != null) { + "Clicked Place ID: ${event.placeId}" } else { - "Clicked Location: ${location.latitude}, ${location.longitude}" + "Clicked Location: ${event.location.latitude}, ${event.location.longitude}" } clickedInfoText.text = message clickedInfoText.contentDescription = message showToast(message) } + return@setMap3DClickListener true } } } diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mapslocalization/MapsLocalizationActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mapslocalization/MapsLocalizationActivity.kt new file mode 100644 index 00000000..1868a905 --- /dev/null +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mapslocalization/MapsLocalizationActivity.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.maps3dkotlin.mapslocalization + +import android.os.Bundle +import android.transition.TransitionManager +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import android.widget.RadioGroup +import android.widget.Toast +import androidx.activity.viewModels +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.example.maps3d.common.DEFAULT_LOCALIZATION_CAMERA +import com.example.maps3d.common.LocalizationState +import com.example.maps3d.common.MapLocalePreset +import com.example.maps3d.common.MapsLocalizationViewModel +import com.example.maps3dcommon.R +import com.example.maps3dkotlin.sampleactivity.SampleBaseActivity +import com.google.android.gms.maps3d.GoogleMap3D +import com.google.android.gms.maps3d.model.Camera +import com.google.android.gms.maps3d.model.Map3DMode +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.launch + +/** + * Demonstrates 3D Map Localization (`language` and `region` via + * [com.google.android.gms.maps3d.model.LocaleOptions] and [GoogleMap3D.setLocale]). + * + * Dynamically updates the map language and region in-place on the live [GoogleMap3D] instance + * without recreating `Map3DView` or reloading the map scene. + */ +class MapsLocalizationActivity : SampleBaseActivity() { + + override val TAG = "MapsLocalizationActivity" + override val initialCamera: Camera = DEFAULT_LOCALIZATION_CAMERA + + private val viewModel: MapsLocalizationViewModel by viewModels() + + private lateinit var collapsibleContent: LinearLayout + private lateinit var collapseToggleButton: MaterialButton + private lateinit var changeLanguageButton: MaterialButton + private lateinit var mapModeRadioGroup: RadioGroup + + private var isCollapsed = false + private var isUpdatingUiFromState = false + private var activePreset: MapLocalePreset = MapLocalePreset.ENGLISH_US + private var activeMapMode: Int = Map3DMode.HYBRID + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val rootContainer = findViewById(R.id.map_container) + val controlPanel = LayoutInflater.from(this).inflate( + R.layout.control_panel_maps_localization, + rootContainer, + false, + ) + rootContainer.addView(controlPanel) + + ViewCompat.setOnApplyWindowInsetsListener(controlPanel) { view, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val layoutParams = view.layoutParams as ConstraintLayout.LayoutParams + val margin12dp = (12 * resources.displayMetrics.density).toInt() + layoutParams.bottomMargin = insets.bottom + margin12dp + view.layoutParams = layoutParams + windowInsets + } + + bindControlPanelViews(controlPanel) + setupControlListeners() + observeViewModel() + } + + override fun onMapReady(googleMap3D: GoogleMap3D) { + super.onMapReady(googleMap3D) + val state = viewModel.uiState.value + activePreset = state.selectedPreset + activeMapMode = state.mapMode + googleMap3D.setMapMode(state.mapMode) + googleMap3D.setLocale(state.toLocaleOptions()) + } + + private fun bindControlPanelViews(panel: View) { + collapsibleContent = panel.findViewById(R.id.card_content) + collapseToggleButton = panel.findViewById(R.id.btn_collapse) + changeLanguageButton = panel.findViewById(R.id.btn_change_language) + mapModeRadioGroup = panel.findViewById(R.id.rg_localization_map_mode) + + panel.findViewById(R.id.card_header).setOnClickListener { + togglePanelCollapse() + } + collapseToggleButton.setOnClickListener { + togglePanelCollapse() + } + } + + private fun setupControlListeners() { + changeLanguageButton.setOnClickListener { + showLanguageSelectionDialog() + } + + mapModeRadioGroup.setOnCheckedChangeListener { _, checkedId -> + if (isUpdatingUiFromState) return@setOnCheckedChangeListener + val targetMode = if (checkedId == R.id.rb_mode_roadmap) { + Map3DMode.ROADMAP + } else { + Map3DMode.HYBRID + } + viewModel.setMapMode(targetMode) + } + } + + private fun showLanguageSelectionDialog() { + val presets = MapLocalePreset.entries + val labels = presets.map { "${it.displayName} — ${it.nativeLabel}" }.toTypedArray() + val currentState = viewModel.uiState.value + val currentIndex = presets.indexOf(currentState.selectedPreset).coerceAtLeast(0) + + MaterialAlertDialogBuilder(this) + .setTitle(R.string.change_language_button) + .setSingleChoiceItems(labels, currentIndex) { dialog, which -> + val chosenPreset = presets[which] + dialog.dismiss() + if (chosenPreset != currentState.selectedPreset) { + viewModel.selectPreset(chosenPreset) + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun observeViewModel() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { state -> + renderUiState(state) + val map = googleMap3D + if (map != null) { + applyStateToMap(map, state) + } + } + } + } + } + + private fun renderUiState(state: LocalizationState) { + isUpdatingUiFromState = true + try { + changeLanguageButton.text = getString( + R.string.language_button_format, + state.selectedPreset.displayName, + state.localeBadgeText, + ) + + val expectedRadioId = if (state.mapMode == Map3DMode.ROADMAP) { + R.id.rb_mode_roadmap + } else { + R.id.rb_mode_hybrid + } + if (mapModeRadioGroup.checkedRadioButtonId != expectedRadioId) { + mapModeRadioGroup.check(expectedRadioId) + } + } finally { + isUpdatingUiFromState = false + } + } + + private fun applyStateToMap(map: GoogleMap3D, state: LocalizationState) { + if (state.mapMode != activeMapMode) { + activeMapMode = state.mapMode + map.setMapMode(state.mapMode) + } + + if (state.selectedPreset != activePreset) { + activePreset = state.selectedPreset + map.setLocale(state.toLocaleOptions()) + Toast.makeText( + this, + getString( + R.string.localization_applied_format, + state.selectedPreset.displayName, + state.localeBadgeText, + ), + Toast.LENGTH_SHORT, + ).show() + } + } + + private fun togglePanelCollapse() { + val rootContainer = findViewById(R.id.map_container) + TransitionManager.beginDelayedTransition(rootContainer) + isCollapsed = !isCollapsed + collapsibleContent.visibility = if (isCollapsed) View.GONE else View.VISIBLE + collapseToggleButton.setIconResource( + if (isCollapsed) R.drawable.expand_less_24px else R.drawable.expand_more_24px, + ) + } +} diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/markers/MarkersActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/markers/MarkersActivity.kt index 30e1de1d..39f1f8ec 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/markers/MarkersActivity.kt +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/markers/MarkersActivity.kt @@ -21,29 +21,29 @@ import android.widget.Button import android.widget.Toast import androidx.lifecycle.lifecycleScope import com.example.maps3dcommon.R +import com.example.maps3dkotlin.common.awaitCameraAnimation +import com.example.maps3dkotlin.common.awaitMapSteady +import com.example.maps3dkotlin.markers.data.MonsterParser import com.example.maps3dkotlin.sampleactivity.SampleBaseActivity import com.google.android.gms.maps3d.GoogleMap3D import com.google.android.gms.maps3d.model.AltitudeMode +import com.google.android.gms.maps3d.model.Camera import com.google.android.gms.maps3d.model.CollisionBehavior import com.google.android.gms.maps3d.model.Glyph import com.google.android.gms.maps3d.model.ImageView import com.google.android.gms.maps3d.model.Map3DMode import com.google.android.gms.maps3d.model.Marker -import com.google.android.gms.maps3d.model.Camera import com.google.android.gms.maps3d.model.camera import com.google.android.gms.maps3d.model.flyToOptions import com.google.android.gms.maps3d.model.latLngAltitude import com.google.android.gms.maps3d.model.markerOptions import com.google.android.gms.maps3d.model.pinConfiguration import com.google.android.gms.maps3d.model.popoverOptions +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.delay import kotlinx.coroutines.isActive -import com.example.maps3dkotlin.common.awaitCameraAnimation -import com.example.maps3dkotlin.common.awaitMapSteady -import com.example.maps3dkotlin.markers.data.MonsterParser -import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.launch /** * This activity demonstrates the various altitude modes available for markers on a 3D map. @@ -92,7 +92,7 @@ class MarkersActivity : SampleBaseActivity() { private var monsterMarkers: List = emptyList() private var monsterIds: List = emptyList() private var monsterLabels: List = emptyList() - + private var tourJob: kotlinx.coroutines.Job? = null // The initial camera position is defined declaratively, providing a clear overview of @@ -174,7 +174,7 @@ class MarkersActivity : SampleBaseActivity() { } } } - + findViewById