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(),
+ )
+ }
+}