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" }, 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 925ff33a..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 @@ -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,10 +28,18 @@ 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, 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, @@ -41,9 +48,17 @@ public class WmsUrlTileProvider( zoom: Int ) -> String ) : 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,9 +69,11 @@ 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 EARTH_CIRCUMFERENCE = 2 * 20037508.34789244 + private const val BOUND = 20037508.34789244 + private const val EARTH_CIRCUMFERENCE = 2 * BOUND + } /** @@ -65,16 +82,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 = -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 = BOUND - (y * tileSizeMeters) + val yMin = yMax - tileSizeMeters return doubleArrayOf(xMin, yMin, xMax, yMax) } 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..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 { @@ -62,4 +64,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..