From 00dd059ec5ef57935825eb18250838702e5ad68a Mon Sep 17 00:00:00 2001 From: David Hovde Date: Thu, 2 Apr 2026 16:13:18 +0200 Subject: [PATCH 1/6] Added STYLES=default parameter to baseUrl of WmsTileOverlayActivity.kt --- .../com/google/maps/android/compose/WmsTileOverlayActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-app/src/main/java/com/google/maps/android/compose/WmsTileOverlayActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/WmsTileOverlayActivity.kt index 9bc11a7e..42cfa1a8 100644 --- a/maps-app/src/main/java/com/google/maps/android/compose/WmsTileOverlayActivity.kt +++ b/maps-app/src/main/java/com/google/maps/android/compose/WmsTileOverlayActivity.kt @@ -48,7 +48,7 @@ class WmsTileOverlayActivity : ComponentActivity() { urlFormatter = { xMin, yMin, xMax, yMax, _ -> "https://basemap.nationalmap.gov/arcgis/services/USGSShadedReliefOnly/MapServer/WmsServer?" + "SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap" + - "&FORMAT=image/png&TRANSPARENT=true&LAYERS=0" + + "&FORMAT=image/png&STYLES=default&TRANSPARENT=true&LAYERS=0" + "&SRS=EPSG:3857&WIDTH=256&HEIGHT=256" + "&BBOX=$xMin,$yMin,$xMax,$yMax" }, From f533f7e521821278f03d38a5210888863fe81de7 Mon Sep 17 00:00:00 2001 From: David Hovde Date: Thu, 2 Apr 2026 16:14:46 +0200 Subject: [PATCH 2/6] Made WMS_BOUND a variable so that tileSize- and xy-calculations have a shared source of truth. Refactored numTiles logic to use constants rather than several declarations of the same number. --- .../maps/android/compose/wms/WmsUrlTileProvider.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt b/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt index 925ff33a..a3ee6942 100644 --- a/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt +++ b/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt @@ -56,7 +56,9 @@ public class WmsUrlTileProvider( /** * The Earth's circumference in meters at the equator according to EPSG:3857. */ - private const val EARTH_CIRCUMFERENCE = 2 * 20037508.34789244 + private const val WMS_BOUND = 20037508.34789244 + private const val EARTH_CIRCUMFERENCE = 2 * WMS_BOUND + } /** @@ -65,16 +67,16 @@ public class WmsUrlTileProvider( * @return an array containing [xMin, yMin, xMax, yMax] in meters. */ internal fun getBoundingBox(x: Int, y: Int, zoom: Int): DoubleArray { - val numTiles = 2.0.pow(zoom.toDouble()) + val numTiles: Int = 1 shl zoom // Powers of 2 are equivalent to bit-shifts val tileSizeMeters = EARTH_CIRCUMFERENCE / numTiles - val xMin = -20037508.34789244 + (x * tileSizeMeters) - val xMax = -20037508.34789244 + ((x + 1) * tileSizeMeters) + val xMin = -WMS_BOUND + (x * tileSizeMeters) + val xMax = xMin + tileSizeMeters // Y is inverted in TMS/Google Maps tiles vs WMS BBOX // Top of map (y=0) is +20037508.34789244 - val yMax = 20037508.34789244 - (y * tileSizeMeters) - val yMin = 20037508.34789244 - ((y + 1) * tileSizeMeters) + val yMax = WMS_BOUND - (y * tileSizeMeters) + val yMin = yMax - tileSizeMeters return doubleArrayOf(xMin, yMin, xMax, yMax) } From b1851b3fd627ac956e3de04b78343c88fe6be563 Mon Sep 17 00:00:00 2001 From: David Hovde Date: Thu, 2 Apr 2026 17:03:58 +0200 Subject: [PATCH 3/6] Added the ability to give the WmsUrlTileProvider the bounds of your WMS-dataset to forego needless network requests. If no bounds are provided the getTileUrl skips the checking and goes straight to urlFormatter. Renamed WMS_BOUND to just BOUND and added it to the comment about EARTH_CIRCUMFERENCE. --- .../android/compose/wms/WmsTileOverlay.kt | 16 ++++++++-- .../android/compose/wms/WmsUrlTileProvider.kt | 31 ++++++++++++++----- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsTileOverlay.kt b/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsTileOverlay.kt index 52f8bcbf..27a94fe1 100644 --- a/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsTileOverlay.kt +++ b/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsTileOverlay.kt @@ -35,6 +35,10 @@ import com.google.maps.android.compose.rememberTileOverlayState * @param onClick a lambda invoked when the tile overlay is clicked. * @param tileWidth the width of the tiles in pixels (default 256). * @param tileHeight the height of the tiles in pixels (default 256). + * @param datasetXMinBound the minimum X coordinate of the dataset in EPSG:3857 (default null). + * @param datasetYMinBound the minimum Y coordinate of the dataset in EPSG:3857 (default null). + * @param datasetXMaxBound the maximum X coordinate of the dataset in EPSG:3857 (default null). + * @param datasetYMaxBound the maximum Y coordinate of the dataset in EPSG:3857 (default null). */ @Composable public fun WmsTileOverlay( @@ -46,13 +50,21 @@ public fun WmsTileOverlay( zIndex: Float = 0f, onClick: (TileOverlay) -> Unit = {}, tileWidth: Int = 256, - tileHeight: Int = 256 + tileHeight: Int = 256, + datasetXMinBound: Double? = null, + datasetYMinBound: Double? = null, + datasetXMaxBound: Double? = null, + datasetYMaxBound: Double? = null ) { val tileProvider = remember(urlFormatter, tileWidth, tileHeight) { WmsUrlTileProvider( width = tileWidth, height = tileHeight, - urlFormatter = urlFormatter + urlFormatter = urlFormatter, + datasetXMinBound = datasetXMinBound, + datasetYMinBound = datasetYMinBound, + datasetXMaxBound = datasetXMaxBound, + datasetYMaxBound = datasetYMaxBound ) } TileOverlay( diff --git a/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt b/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt index a3ee6942..1ed98128 100644 --- a/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt +++ b/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt @@ -19,7 +19,6 @@ package com.google.maps.android.compose.wms import com.google.android.gms.maps.model.UrlTileProvider import java.net.MalformedURLException import java.net.URL -import kotlin.math.pow /** * A [UrlTileProvider] for Web Map Service (WMS) layers that use the EPSG:3857 (Web Mercator) @@ -29,6 +28,10 @@ import kotlin.math.pow * @param height the height of the tile in pixels. * @param urlFormatter a lambda that returns the WMS URL for the given bounding box coordinates * (xMin, yMin, xMax, yMax) and zoom level. + * @param datasetXMinBound the minimum X coordinate of the dataset in EPSG:3857 (default null). + * @param datasetYMinBound the minimum Y coordinate of the dataset in EPSG:3857 (default null). + * @param datasetXMaxBound the maximum X coordinate of the dataset in EPSG:3857 (default null). + * @param datasetYMaxBound the maximum Y coordinate of the dataset in EPSG:3857 (default null). */ public class WmsUrlTileProvider( width: Int = 256, @@ -39,11 +42,23 @@ public class WmsUrlTileProvider( xMax: Double, yMax: Double, zoom: Int - ) -> String + ) -> String, + private val datasetXMinBound: Double? = null, + private val datasetYMinBound: Double? = null, + private val datasetXMaxBound: Double? = null, + private val datasetYMaxBound: Double? = null, ) : UrlTileProvider(width, height) { + private val bounded: Boolean = datasetXMinBound != null || datasetYMinBound != null || datasetXMaxBound != null || datasetYMaxBound != null override fun getTileUrl(x: Int, y: Int, zoom: Int): URL? { - val bbox = getBoundingBox(x, y, zoom) + val bbox = getBoundingBox(x, y, zoom) // doubleArrayOf(xMin, yMin, xMax, yMax) + // Return null if the tile is entirely outside the specified bounds of the dataset + if(bounded && // skip checking for datasets where no bounds are specified + (datasetXMaxBound != null && bbox[0] > datasetXMaxBound) || // xMin greater than datasets xMax. No overlap. + (datasetYMaxBound != null && bbox[1] > datasetYMaxBound) || // yMin greater than datasets yMax. No overlap. + (datasetXMinBound != null && bbox[2] < datasetXMinBound) || // xMax less than datasets xMin. No overlap. + (datasetYMinBound != null && bbox[3] < datasetYMinBound) // yMax less than datasets yMin. No overlap. + ){return null} val urlString = urlFormatter(bbox[0], bbox[1], bbox[2], bbox[3], zoom) return try { URL(urlString) @@ -54,10 +69,10 @@ public class WmsUrlTileProvider( private companion object { /** - * The Earth's circumference in meters at the equator according to EPSG:3857. + * The Earth's bound and circumference in meters at the equator according to EPSG:3857. */ - private const val WMS_BOUND = 20037508.34789244 - private const val EARTH_CIRCUMFERENCE = 2 * WMS_BOUND + private const val BOUND = 20037508.34789244 + private const val EARTH_CIRCUMFERENCE = 2 * BOUND } @@ -70,12 +85,12 @@ public class WmsUrlTileProvider( val numTiles: Int = 1 shl zoom // Powers of 2 are equivalent to bit-shifts val tileSizeMeters = EARTH_CIRCUMFERENCE / numTiles - val xMin = -WMS_BOUND + (x * tileSizeMeters) + val xMin = -BOUND + (x * tileSizeMeters) val xMax = xMin + tileSizeMeters // Y is inverted in TMS/Google Maps tiles vs WMS BBOX // Top of map (y=0) is +20037508.34789244 - val yMax = WMS_BOUND - (y * tileSizeMeters) + val yMax = BOUND - (y * tileSizeMeters) val yMin = yMax - tileSizeMeters return doubleArrayOf(xMin, yMin, xMax, yMax) From 5f0e78c6b3472c03548eadc86b612677279098fb Mon Sep 17 00:00:00 2001 From: David Hovde Date: Thu, 2 Apr 2026 17:47:24 +0200 Subject: [PATCH 4/6] Moved urlFormatter-lambda to be last argument of WmsUrlTileProvider.kt again. --- .../maps/android/compose/wms/WmsUrlTileProvider.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt b/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt index 1ed98128..e80c200b 100644 --- a/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt +++ b/maps-compose-utils/src/main/java/com/google/maps/android/compose/wms/WmsUrlTileProvider.kt @@ -36,17 +36,17 @@ import java.net.URL public class WmsUrlTileProvider( width: Int = 256, height: Int = 256, + private val datasetXMinBound: Double? = null, + private val datasetYMinBound: Double? = null, + private val datasetXMaxBound: Double? = null, + private val datasetYMaxBound: Double? = null, private val urlFormatter: ( xMin: Double, yMin: Double, xMax: Double, yMax: Double, zoom: Int - ) -> String, - private val datasetXMinBound: Double? = null, - private val datasetYMinBound: Double? = null, - private val datasetXMaxBound: Double? = null, - private val datasetYMaxBound: Double? = null, + ) -> String ) : UrlTileProvider(width, height) { private val bounded: Boolean = datasetXMinBound != null || datasetYMinBound != null || datasetXMaxBound != null || datasetYMaxBound != null From 17d76400088f351b8b6edc6a4453a8da2830518d Mon Sep 17 00:00:00 2001 From: David Hovde Date: Thu, 2 Apr 2026 18:20:07 +0200 Subject: [PATCH 5/6] Added testing of bounds by asking for every tile west of centerline. One test with xMin as a smidge above centerline (all should be null since no intersection). One test with xMax as a smidge below centerline (all should return URLs since no tile is entirely outside of bounds) --- .../compose/wms/WmsUrlTileProviderTest.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/maps-compose-utils/src/test/java/com/google/maps/android/compose/wms/WmsUrlTileProviderTest.kt b/maps-compose-utils/src/test/java/com/google/maps/android/compose/wms/WmsUrlTileProviderTest.kt index 751405e7..206981f0 100644 --- a/maps-compose-utils/src/test/java/com/google/maps/android/compose/wms/WmsUrlTileProviderTest.kt +++ b/maps-compose-utils/src/test/java/com/google/maps/android/compose/wms/WmsUrlTileProviderTest.kt @@ -62,4 +62,33 @@ public class WmsUrlTileProviderTest { val expected = doubleArrayOf(-worldSize / 2, 0.0, 0.0, worldSize / 2) assertArrayEquals(expected, bbox, 0.001) } + + @Test + public fun testGetTileUrlBeyondBounds() { + val provider = WmsUrlTileProvider(datasetXMinBound = 1.0) { _, _, _, _, _ -> "https://example.com" } + val halfOfRes = {zoom : Int -> 1 shl (zoom - 1)} + for (z in 1..3) { + for (x in 0.. "https://example.com" } + val halfOfRes = {zoom : Int -> 1 shl (zoom - 1)} + for (z in 1..3) { + for (x in 0.. Date: Thu, 2 Apr 2026 18:50:36 +0200 Subject: [PATCH 6/6] Switched to better assertions for Bounds-testing. --- .../maps/android/compose/wms/WmsUrlTileProviderTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/maps-compose-utils/src/test/java/com/google/maps/android/compose/wms/WmsUrlTileProviderTest.kt b/maps-compose-utils/src/test/java/com/google/maps/android/compose/wms/WmsUrlTileProviderTest.kt index 206981f0..3449e29c 100644 --- a/maps-compose-utils/src/test/java/com/google/maps/android/compose/wms/WmsUrlTileProviderTest.kt +++ b/maps-compose-utils/src/test/java/com/google/maps/android/compose/wms/WmsUrlTileProviderTest.kt @@ -17,6 +17,8 @@ package com.google.maps.android.compose.wms import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Test public class WmsUrlTileProviderTest { @@ -71,7 +73,7 @@ public class WmsUrlTileProviderTest { for (x in 0..