Skip to content

Commit 884b142

Browse files
committed
feat(wms): implement WMS tile overlay with math optimizations and dynamic toggles
- Extract hardcoded Web Mercator extent into precise PI-derived constants - Use efficient bitwise shifts for tile count calculations - Document Web Mercator projection mechanics step-by-step - Add interactive toggle buttons for base map and overlay visibility
1 parent 34e4b3e commit 884b142

3 files changed

Lines changed: 86 additions & 21 deletions

File tree

maps-app/src/main/java/com/google/maps/android/compose/WmsTileOverlayActivity.kt

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,24 @@ package com.google.maps.android.compose
1919
import android.os.Bundle
2020
import androidx.activity.ComponentActivity
2121
import androidx.activity.compose.setContent
22+
import androidx.compose.foundation.layout.Arrangement
23+
import androidx.compose.foundation.layout.Box
24+
import androidx.compose.foundation.layout.Column
2225
import androidx.compose.foundation.layout.fillMaxSize
26+
import androidx.compose.foundation.layout.padding
27+
import androidx.compose.material3.Button
28+
import androidx.compose.material3.Text
29+
import androidx.compose.runtime.getValue
30+
import androidx.compose.runtime.mutableStateOf
31+
import androidx.compose.runtime.remember
32+
import androidx.compose.runtime.setValue
33+
import androidx.compose.ui.Alignment
2334
import androidx.compose.ui.Modifier
35+
import androidx.compose.ui.unit.dp
2436
import com.google.android.gms.maps.model.CameraPosition
2537
import com.google.android.gms.maps.model.LatLng
2638
import com.google.maps.android.compose.wms.WmsTileOverlay
39+
import androidx.core.net.toUri
2740

2841
/**
2942
* This activity demonstrates how to use [WmsTileOverlay] to display a Web Map Service (WMS)
@@ -38,23 +51,62 @@ class WmsTileOverlayActivity : ComponentActivity() {
3851
val cameraPositionState = rememberCameraPositionState {
3952
position = CameraPosition.fromLatLngZoom(center, 4f)
4053
}
54+
var mapType by remember { mutableStateOf(MapType.NORMAL) }
55+
var overlayVisible by remember { mutableStateOf(true) }
4156

42-
GoogleMap(
43-
modifier = Modifier.fillMaxSize(),
44-
cameraPositionState = cameraPositionState
45-
) {
57+
Box(modifier = Modifier.fillMaxSize()) {
58+
GoogleMap(
59+
modifier = Modifier.fillMaxSize(),
60+
cameraPositionState = cameraPositionState,
61+
properties = MapProperties(mapType = mapType)
62+
) {
4663
// Example: USGS National Map Shaded Relief (WMS)
4764
WmsTileOverlay(
4865
urlFormatter = { xMin, yMin, xMax, yMax, _ ->
49-
"https://basemap.nationalmap.gov/arcgis/services/USGSShadedReliefOnly/MapServer/WmsServer?" +
50-
"SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap" +
51-
"&FORMAT=image/png&TRANSPARENT=true&LAYERS=0" +
52-
"&SRS=EPSG:3857&WIDTH=256&HEIGHT=256" +
53-
"&BBOX=$xMin,$yMin,$xMax,$yMax"
66+
"https://basemap.nationalmap.gov/arcgis/services/USGSShadedReliefOnly/MapServer/WmsServer".toUri()
67+
.buildUpon()
68+
.appendQueryParameter("SERVICE", "WMS")
69+
.appendQueryParameter("VERSION", "1.1.1")
70+
.appendQueryParameter("REQUEST", "GetMap")
71+
.appendQueryParameter("FORMAT", "image/png")
72+
.appendQueryParameter("TRANSPARENT", "true")
73+
.appendQueryParameter("LAYERS", "0")
74+
.appendQueryParameter("SRS", "EPSG:3857")
75+
.appendQueryParameter("WIDTH", "256")
76+
.appendQueryParameter("HEIGHT", "256")
77+
.appendQueryParameter("STYLES", "")
78+
.appendQueryParameter("BBOX", "$xMin,$yMin,$xMax,$yMax")
79+
.build()
80+
.toString()
5481
},
55-
transparency = 0.5f
82+
transparency = 0.5f,
83+
visible = overlayVisible
5684
)
5785
}
86+
87+
Column(
88+
modifier = Modifier
89+
.align(Alignment.TopEnd)
90+
.padding(16.dp),
91+
verticalArrangement = Arrangement.spacedBy(8.dp)
92+
) {
93+
Button(
94+
onClick = {
95+
mapType = if (mapType == MapType.NONE) MapType.NORMAL else MapType.NONE
96+
}
97+
) {
98+
Text(if (mapType == MapType.NONE) "Show Base Map" else "Hide Base Map")
99+
}
100+
101+
Button(
102+
onClick = {
103+
overlayVisible = !overlayVisible
104+
}
105+
) {
106+
Text(if (overlayVisible) "Hide WMS Overlay" else "Show WMS Overlay")
107+
}
108+
}
58109
}
59110
}
111+
}
60112
}

maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.google.maps.android.compose.wms
1919
import com.google.android.gms.maps.model.UrlTileProvider
2020
import java.net.MalformedURLException
2121
import java.net.URL
22+
import kotlin.math.PI
2223
import kotlin.math.pow
2324

2425
/**
@@ -54,9 +55,16 @@ public class WmsUrlTileProvider(
5455

5556
private companion object {
5657
/**
57-
* The Earth's circumference in meters at the equator according to EPSG:3857.
58+
* The maximum extent of the Web Mercator projection (EPSG:3857) in meters.
59+
* This is the distance from the origin (0,0) to the edge of the world map.
60+
* Calculated as semi-major axis of Earth (6378137.0) * PI.
5861
*/
59-
private const val EARTH_CIRCUMFERENCE = 2 * 20037508.34789244
62+
private const val WORLD_EXTENT = (6378137.0) * PI
63+
64+
/**
65+
* The total width/height of the world map in meters.
66+
*/
67+
private const val WORLD_SIZE_METERS = 2 * WORLD_EXTENT
6068
}
6169

6270
/**
@@ -65,16 +73,21 @@ public class WmsUrlTileProvider(
6573
* @return an array containing [xMin, yMin, xMax, yMax] in meters.
6674
*/
6775
internal fun getBoundingBox(x: Int, y: Int, zoom: Int): DoubleArray {
68-
val numTiles = 2.0.pow(zoom.toDouble())
69-
val tileSizeMeters = EARTH_CIRCUMFERENCE / numTiles
76+
// 1. Calculate how many tiles exist in each dimension at this zoom level (2^zoom).
77+
val tilesPerDimension = 1 shl zoom
78+
79+
// 2. Divide the total world span by the number of tiles to find the metric size of one tile.
80+
val tileSizeMeters = WORLD_SIZE_METERS / tilesPerDimension.toDouble()
7081

71-
val xMin = -20037508.34789244 + (x * tileSizeMeters)
72-
val xMax = -20037508.34789244 + ((x + 1) * tileSizeMeters)
82+
// 3. X-axis: Starts at the far left (-WORLD_EXTENT) and moves East.
83+
val xMin = -WORLD_EXTENT + (x * tileSizeMeters)
84+
val xMax = -WORLD_EXTENT + ((x + 1) * tileSizeMeters)
7385

74-
// Y is inverted in TMS/Google Maps tiles vs WMS BBOX
75-
// Top of map (y=0) is +20037508.34789244
76-
val yMax = 20037508.34789244 - (y * tileSizeMeters)
77-
val yMin = 20037508.34789244 - ((y + 1) * tileSizeMeters)
86+
// 4. Y-axis: Google Maps/TMS starts at the Top (y=0 is North) and moves South.
87+
// WMS Bounding Box expects yMin to be the southern-most latitude and yMax to be the northern-most.
88+
// Therefore, we subtract the tile distance from the northern-most edge (+WORLD_EXTENT).
89+
val yMax = WORLD_EXTENT - (y * tileSizeMeters)
90+
val yMin = WORLD_EXTENT - ((y + 1) * tileSizeMeters)
7891

7992
return doubleArrayOf(xMin, yMin, xMax, yMax)
8093
}

maps-compose-utils/src/test/java/com/google/maps/android/compose/wms/WmsUrlTileProviderTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import org.junit.Test
2121

2222
public class WmsUrlTileProviderTest {
2323

24-
private val worldSize: Double = 20037508.34789244
24+
private val worldSize: Double = 6378137.0 * kotlin.math.PI
2525

2626
@Test
2727
public fun testGetBoundingBoxZoom0() {

0 commit comments

Comments
 (0)