From 1c393bac51cad92bb01e1ea3f08cdf841cf4f437 Mon Sep 17 00:00:00 2001 From: Dale Hawkins <107309+dkhawk@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:01:52 -0600 Subject: [PATCH] feat(samples): add Cloud-Based Map Styling demo and visual tests --- .../res/layout/activity_cloud_styling.xml | 133 ++++++++++++++++++ .../layout/control_panel_cloud_styling.xml | 82 +++++++++++ .../maps3djava/CloudStylingVisualTest.java | 79 +++++++++++ .../cloudstyling/CloudStylingActivity.java | 85 ++++++++++- .../maps3dkotlin/CloudStylingVisualTest.kt | 82 +++++++++++ .../cloudstyling/CloudStylingActivity.kt | 73 +++++++++- .../com/example/composedemos/MainActivity.kt | 5 + .../cloudstyling/CloudStylingActivity.kt | 81 ++++++++--- 8 files changed, 590 insertions(+), 30 deletions(-) create mode 100644 Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_cloud_styling.xml create mode 100644 Maps3DSamples/ApiDemos/common/src/main/res/layout/control_panel_cloud_styling.xml create mode 100644 Maps3DSamples/ApiDemos/java-app/src/androidTest/java/com/example/maps3djava/CloudStylingVisualTest.java create mode 100644 Maps3DSamples/ApiDemos/kotlin-app/src/androidTest/java/com/example/maps3dkotlin/CloudStylingVisualTest.kt diff --git a/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_cloud_styling.xml b/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_cloud_styling.xml new file mode 100644 index 00000000..61ebe477 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/res/layout/activity_cloud_styling.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Maps3DSamples/ApiDemos/common/src/main/res/layout/control_panel_cloud_styling.xml b/Maps3DSamples/ApiDemos/common/src/main/res/layout/control_panel_cloud_styling.xml new file mode 100644 index 00000000..ec83a2bd --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/res/layout/control_panel_cloud_styling.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Maps3DSamples/ApiDemos/java-app/src/androidTest/java/com/example/maps3djava/CloudStylingVisualTest.java b/Maps3DSamples/ApiDemos/java-app/src/androidTest/java/com/example/maps3djava/CloudStylingVisualTest.java new file mode 100644 index 00000000..c20904d3 --- /dev/null +++ b/Maps3DSamples/ApiDemos/java-app/src/androidTest/java/com/example/maps3djava/CloudStylingVisualTest.java @@ -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.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.cloudstyling.CloudStylingActivity; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * A premium visual regression test for the View-based Java Cloud-Based Map Styling sample. + * + * Demonstrates robust programmatic testing of custom cloud-styled 3D map features by launching the Java-based + * [CloudStylingActivity], waiting for cloud-styled map tiles over San Francisco to load, capturing a screenshot of the + * active map scene, and verifying visual correctness using the Gemini API. + */ +@RunWith(AndroidJUnit4.class) +public class CloudStylingVisualTest extends BaseVisualTest { + + @Test + public void verifyCloudStylingRenders() { + // Launch CloudStylingActivity + Intent intent = new Intent(context, CloudStylingActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + + // Wait for the activity to be displayed in the foreground + uiDevice.wait(Until.hasObject(By.pkg(context.getPackageName()).depth(0)), 10000); + + // Wait for Cloud-styled 3D map tiles to render and feature styling to settle + waitForMapRendering(15); + + // Capture high-resolution screenshot of the active 3D map scene + Bitmap screenshotBitmap = captureScreenshot("cloud_styling_screenshot.png"); + + // Define the verification prompt for the visual testing agent + String prompt = "Please act as a UI tester and analyze this screenshot.\n" + + "1. Confirm that a 3D map view is visible over San Francisco.\n" + + "2. Confirm that custom cloud-based stylized map tiles / elements are displayed on the 3D map scene.\n" + + "3. Confirm that the Map Mode selector card (with radio options for Roadmap, Hybrid, and Satellite) is visible at the bottom of the screen.\n" + + "\n" + + "If and ONLY IF you can clearly see the 3D Cloud-styled map view and bottom Map Mode selection card, reply with \"PASSED\".\n" + + "If you cannot see the 3D map scene or control card, reply with \"FAILED: 3D map scene or Map Mode controls not visible\".\n" + + "Report what you see in detail."; + + // Analyze the image using Gemini (using blocking wrapper) + String geminiResponse = helper.analyzeImageBlocking(screenshotBitmap, prompt, geminiApiKey); + System.out.println("Gemini's analysis: " + geminiResponse); + + // Assert on Gemini's response + assertTrue( + "Visual verification failed. Gemini response: " + geminiResponse, + geminiResponse != null && geminiResponse.toUpperCase().contains("PASSED") + ); + } +} diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/cloudstyling/CloudStylingActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/cloudstyling/CloudStylingActivity.java index 6997460a..a8599bed 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/cloudstyling/CloudStylingActivity.java +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/cloudstyling/CloudStylingActivity.java @@ -16,15 +16,88 @@ package com.example.maps3djava.cloudstyling; +import static com.example.maps3d.common.UtilitiesKt.toValidCamera; + +import android.os.Bundle; +import android.widget.RadioGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.example.maps3dcommon.R; -import com.example.maps3djava.common.BaseSkeletonActivity; +import com.example.maps3djava.sampleactivity.SampleBaseActivity; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps3d.GoogleMap3D; +import com.google.android.gms.maps3d.model.Camera; +import com.google.android.gms.maps3d.model.LatLngAltitude; +import com.google.android.gms.maps3d.model.Map3DMode; +import com.google.android.material.appbar.MaterialToolbar; /** - * Skeleton activity for CloudStylingActivity. + * Showcases **Cloud-Based Map Styling** in Google Maps 3D SDK (Java implementation). Uses + * standalone layout [R.layout.activity_cloud_styling] with declarative custom Map ID. */ -public class CloudStylingActivity extends BaseSkeletonActivity { - @Override - protected int getTitleResId() { - return R.string.feature_title_cloud_styling; +public class CloudStylingActivity extends SampleBaseActivity { + + public static final LatLng SF_LOCATION = new LatLng(37.7915, -122.4010); + + private int currentMapMode = Map3DMode.ROADMAP; + + @NonNull + @Override + public String getTAG() { + return getClass().getSimpleName(); + } + + @NonNull + @Override + public Camera getInitialCamera() { + return toValidCamera(new Camera( + new LatLngAltitude(SF_LOCATION.latitude, SF_LOCATION.longitude, 250.0), + 45.0, + 65.0, + 0.0, + 800.0 + )); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Set dedicated standalone layout with declarative mapId="9a35234a36da44d2c47bf626" + setContentView(R.layout.activity_cloud_styling); + + MaterialToolbar topBar = findViewById(R.id.top_bar); + if (topBar != null) { + topBar.setTitle(R.string.feature_title_cloud_styling); + topBar.setNavigationOnClickListener(v -> finish()); + } + + map3DView = findViewById(R.id.map3dView); + if (map3DView != null) { + map3DView.onCreate(savedInstanceState); + map3DView.getMap3DViewAsync(this); } + + RadioGroup rgMapMode = findViewById(R.id.rg_map_mode); + if (rgMapMode != null) { + rgMapMode.setOnCheckedChangeListener((group, checkedId) -> { + int newMode = Map3DMode.ROADMAP; + if (checkedId == R.id.rb_hybrid) { + newMode = Map3DMode.HYBRID; + } else if (checkedId == R.id.rb_satellite) { + newMode = Map3DMode.SATELLITE; + } + currentMapMode = newMode; + if (googleMap3D != null) { + googleMap3D.setMapMode(newMode); + } + }); + } + } + + @Override + public void onMap3DViewReady(@NonNull GoogleMap3D googleMap3D) { + super.onMap3DViewReady(googleMap3D); + googleMap3D.setMapMode(currentMapMode); + } } diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/androidTest/java/com/example/maps3dkotlin/CloudStylingVisualTest.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/androidTest/java/com/example/maps3dkotlin/CloudStylingVisualTest.kt new file mode 100644 index 00000000..e83306ce --- /dev/null +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/androidTest/java/com/example/maps3dkotlin/CloudStylingVisualTest.kt @@ -0,0 +1,82 @@ +/* + * 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.cloudstyling.CloudStylingActivity +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +/** + * A premium visual regression test for the View-based Kotlin Cloud-Based Map Styling sample. + * + * This test automates launching the [CloudStylingActivity], waiting for cloud-styled 3D map tiles configured via custom + * Map ID over San Francisco to initialize, capturing a screenshot of the live rendering scene, and verifying visual + * correctness using the Gemini API. + */ +@RunWith(AndroidJUnit4::class) +class CloudStylingVisualTest : BaseVisualTest() { + + @Test + fun verifyCloudStylingRenders() { + runBlocking { + // Launch CloudStylingActivity + val intent = Intent(context, CloudStylingActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + + // Wait for the activity to be displayed in the foreground + uiDevice.wait(Until.hasObject(By.pkg(context.packageName).depth(0)), 10000) + + // Wait for Cloud-styled 3D map tiles to render and feature styling to settle + waitForMapRendering(15) + + // Capture high-resolution screenshot of the active 3D map scene + val screenshotBitmap = captureScreenshot("cloud_styling_screenshot.png") + + // Define the verification prompt for the visual testing agent + val prompt = """ + Please act as a UI tester and analyze this screenshot. + 1. Confirm that a 3D map view is visible over San Francisco. + 2. Confirm that custom cloud-based stylized map tiles / elements are displayed on the 3D map scene. + 3. Confirm that the Map Mode selector card (with radio options for Roadmap, Hybrid, and Satellite) is visible at the bottom of the screen. + + If and ONLY IF you can clearly see the 3D Cloud-styled map view and bottom Map Mode selection card, reply with "PASSED". + If you cannot see the 3D map scene or control card, reply with "FAILED: 3D map scene or Map Mode controls not visible". + Report what you see in detail. + """.trimIndent() + + // Analyze the image using Gemini + val geminiResponse = helper.analyzeImage(screenshotBitmap, prompt, geminiApiKey) + println("Gemini's analysis: ${'$'}geminiResponse") + + // Assert on Gemini's response + assertTrue( + "Visual verification failed. Gemini response: ${'$'}geminiResponse", + geminiResponse?.contains("PASSED", ignoreCase = true) == true + ) + } + } +} diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/cloudstyling/CloudStylingActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/cloudstyling/CloudStylingActivity.kt index 4584108f..7c713ceb 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/cloudstyling/CloudStylingActivity.kt +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/cloudstyling/CloudStylingActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Google LLC + * 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. @@ -16,14 +16,75 @@ package com.example.maps3dkotlin.cloudstyling +import android.os.Bundle +import android.widget.RadioGroup +import androidx.activity.enableEdgeToEdge +import com.example.maps3d.common.toValidCamera import com.example.maps3dcommon.R -import com.example.maps3dkotlin.common.BaseSkeletonActivity +import com.example.maps3dkotlin.sampleactivity.SampleBaseActivity +import com.google.android.gms.maps.model.LatLng +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.gms.maps3d.model.camera +import com.google.android.gms.maps3d.model.latLngAltitude +import com.google.android.material.appbar.MaterialToolbar /** - * Skeleton activity for CloudStylingActivity. + * Showcases **Cloud-Based Map Styling** in Google Maps 3D SDK. + * Uses standalone layout [R.layout.activity_cloud_styling] with declarative custom Map ID. */ -class CloudStylingActivity : BaseSkeletonActivity() { - override fun getTitleResId(): Int { - return R.string.feature_title_cloud_styling +class CloudStylingActivity : SampleBaseActivity() { + + override val TAG: String = this::class.java.simpleName + + override val initialCamera: Camera = camera { + center = latLngAltitude { + latitude = SF_LOCATION.latitude + longitude = SF_LOCATION.longitude + altitude = 250.0 + } + heading = 45.0 + tilt = 65.0 + range = 800.0 + }.toValidCamera() + + @Map3DMode + private var currentMapMode: Int = Map3DMode.ROADMAP + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_cloud_styling) + + findViewById(R.id.top_bar)?.apply { + title = getString(R.string.feature_title_cloud_styling) + setNavigationOnClickListener { finish() } + } + + map3DView = findViewById(R.id.map3dView).apply { + onCreate(savedInstanceState) + getMap3DViewAsync(this@CloudStylingActivity) + } + + findViewById(R.id.rg_map_mode)?.setOnCheckedChangeListener { _, checkedId -> + val newMode = when (checkedId) { + R.id.rb_hybrid -> Map3DMode.HYBRID + R.id.rb_satellite -> Map3DMode.SATELLITE + else -> Map3DMode.ROADMAP + } + currentMapMode = newMode + googleMap3D?.setMapMode(newMode) + } + } + + override fun onMapReady(googleMap3D: GoogleMap3D) { + super.onMapReady(googleMap3D) + googleMap3D.setMapMode(currentMapMode) + } + + companion object { + val SF_LOCATION = LatLng(37.7915, -122.4010) } } diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt index 5e475d91..b9705841 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt @@ -207,6 +207,11 @@ fun CatalogScreen() { context.startActivity(Intent(context, FieldOfViewActivity::class.java)) } } + item { + SampleItem("Cloud Map Styling") { + context.startActivity(Intent(context, CloudStylingActivity::class.java)) + } + } } } diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cloudstyling/CloudStylingActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cloudstyling/CloudStylingActivity.kt index 802b7f50..ac803e86 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cloudstyling/CloudStylingActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cloudstyling/CloudStylingActivity.kt @@ -24,13 +24,18 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.ui.Alignment +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps3d.Map3DInitConfig +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 com.google.maps.android.compose3d.GoogleMap3D class CloudStylingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -38,20 +43,60 @@ class CloudStylingActivity : ComponentActivity() { enableEdgeToEdge() setContent { MaterialTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), - contentAlignment = Alignment.Center, - ) { - Text( - text = "TODO: Cloud Map Styling sample will be implemented here.", - style = MaterialTheme.typography.headlineSmall, - ) - } + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + CloudStylingScreen() } } } } } + +@Composable +fun CloudStylingScreen() { + val initialLocation = LatLng(37.7915, -122.4010) + val currentCameraState by remember { + mutableStateOf( + camera { + center = latLngAltitude { + latitude = initialLocation.latitude + longitude = initialLocation.longitude + altitude = 250.0 + } + heading = 45.0 + tilt = 65.0 + range = 800.0 + }, + ) + } + + Box(modifier = Modifier.fillMaxSize()) { + GoogleMap3D( + camera = currentCameraState, + mapMode = Map3DMode.ROADMAP, + options = Map3DInitConfig.create( + centerLat = initialLocation.latitude, + centerLng = initialLocation.longitude, + centerAlt = 0.0, + heading = 45.0, + tilt = 65.0, + roll = 0.0, + range = 800.0, + minAltitude = 0.0, + maxAltitude = 1000000.0, + minHeading = 0.0, + maxHeading = 360.0, + minTilt = 0.0, + maxTilt = 90.0, + bounds = null, + mapMode = Map3DMode.ROADMAP, + mapId = "9a35234a36da44d2c47bf626", + language = java.util.Locale.getDefault().language, + region = java.util.Locale.getDefault().country, + ), + modifier = Modifier.fillMaxSize(), + ) + } +}