Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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

}

/**
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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..<halfOfRes(z)) { //since xMinBound is the line 1.0 the boxes made to reach 0.0 will not intersect
for (y in 0..2 * halfOfRes(z)) {
val tileUrl = provider.getTileUrl(x, y, z)
assertNull(tileUrl)
}
}
}
}

@Test
public fun testGetTileUrlWithinBounds() {
val provider = WmsUrlTileProvider(datasetXMaxBound = -1.0) { _, _, _, _, _ -> "https://example.com" }
val halfOfRes = {zoom : Int -> 1 shl (zoom - 1)}
for (z in 1..3) {
for (x in 0..<halfOfRes(z)) { //since xMaxBound is the line 1.0 the boxes made to reach 0.0 will be contained
for (y in 0..2 * halfOfRes(z)) {
val tileUrl = provider.getTileUrl(x, y, z)
assertNotNull(tileUrl)
}
}
}
}

}