commonMain.earth.worldwind.ogc.WmsLayerFactory.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of worldwind-jvm Show documentation
Show all versions of worldwind-jvm Show documentation
The WorldWind Kotlin SDK (WWK) includes the library, examples and tutorials for building multiplatform 3D virtual globe applications for Android, Web and Java.
The newest version!
package earth.worldwind.ogc
import com.eygraber.uri.Uri
import earth.worldwind.geom.Ellipsoid
import earth.worldwind.geom.Sector
import earth.worldwind.layer.TiledImageLayer
import earth.worldwind.ogc.wms.WmsCapabilities
import earth.worldwind.ogc.wms.WmsLayer
import earth.worldwind.shape.TiledSurfaceImage
import earth.worldwind.util.LevelSet
import earth.worldwind.util.LevelSetConfig
import earth.worldwind.util.Logger.ERROR
import earth.worldwind.util.Logger.logMessage
import earth.worldwind.util.Logger.makeMessage
import earth.worldwind.util.http.DefaultHttpClient
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import nl.adaptivity.xmlutil.serialization.XML
object WmsLayerFactory {
private const val DEFAULT_WMS_NUM_LEVELS = 20
private val compatibleImageFormats = listOf("image/png", "image/jpg", "image/jpeg", "image/gif", "image/bmp")
private val xml = XML { defaultPolicy { ignoreUnknownChildren() } }
/**
* Create tiled image layer based on WMS layer metadata retrieved from server capabilities or decoded from parameter
*
* @param serviceAddress WMS service address
* @param layerNames Optional list of WMS layer names to be requested and combined into the resulting image layer
* @param serviceMetadata Optional WMS capabilities XML string to avoid online capabilities request
* @param displayName Optional layer display name
*/
suspend fun createLayer(
serviceAddress: String, layerNames: List, serviceMetadata: String? = null, displayName: String? = null
): TiledImageLayer {
require(serviceAddress.isNotEmpty()) {
logMessage(ERROR, "WmsLayerFactory", "createLayer", "missingServiceAddress")
}
require(layerNames.isNotEmpty()) {
logMessage(ERROR, "WmsLayerFactory", "createLayer", "missingLayerNames")
}
val wmsCapabilitiesText = serviceMetadata ?: retrieveWmsCapabilities(serviceAddress)
val wmsCapabilities = decodeWmsCapabilities(wmsCapabilitiesText)
val wmsLayers = wmsCapabilities.getNamedLayers(layerNames)
require(wmsLayers.isNotEmpty()) {
makeMessage("WmsLayerFactory", "createLayer", "Provided layer names did not match available layers")
}
return createWmsImageLayer(serviceAddress, wmsCapabilitiesText, wmsLayers, displayName)
}
private suspend fun retrieveWmsCapabilities(serviceAddress: String) = DefaultHttpClient().use { httpClient ->
val serviceUri = Uri.parse(serviceAddress).buildUpon()
.appendQueryParameter("VERSION", "1.3.0")
.appendQueryParameter("SERVICE", "WMS")
.appendQueryParameter("REQUEST", "GetCapabilities")
.build()
httpClient.get(serviceUri.toString()) { expectSuccess = true }.bodyAsText()
}
private suspend fun decodeWmsCapabilities(xmlText: String) = withContext(Dispatchers.Default) {
xml.decodeFromString(xmlText)
}
private fun createWmsImageLayer(
serviceAddress: String, serviceMetadata: String, wmsLayers: List, name: String?
) = WmsImageLayer(
serviceAddress, serviceMetadata, wmsLayers.mapNotNull { lc -> lc.name }.joinToString(","),
name ?: wmsLayers.joinToString(",") { lc -> lc.title }, createWmsSurfaceImage(wmsLayers)
)
private fun createWmsSurfaceImage(wmsLayers: List): TiledSurfaceImage {
// Check if the server supports multiple layer request
val layerLimit = wmsLayers[0].capability?.capabilities?.service?.layerLimit
require (layerLimit == null || layerLimit >= wmsLayers.size) {
makeMessage(
"WmsLayerFactory", "createFromWmsAsync",
"The number of layers specified exceeds the services limit"
)
}
val wmsLayerConfig = getLayerConfigFromWmsCapabilities(wmsLayers)
val levelSetConfig = getLevelSetConfigFromWmsCapabilities(wmsLayers)
return TiledSurfaceImage(WmsTileFactory(wmsLayerConfig), LevelSet(levelSetConfig))
}
internal fun getLayerConfigFromWmsCapabilities(wmsLayers: List): WmsLayerConfig {
// Construct the WmsTiledImage renderable from the WMS Capabilities properties
val wmsCapabilities = wmsLayers[0].capability?.capabilities
val serviceAddress = wmsCapabilities?.capability?.request?.getMap?.getUrl ?: error(
makeMessage("WmsLayerFactory", "getLayerConfigFromWmsCapabilities", "Unable to resolve GetMap URL")
)
var matchingCoordinateSystems: MutableSet? = null
for (wmsLayer in wmsLayers) {
val wmsLayerCoordinateSystems = wmsLayer.referenceSystems
if (matchingCoordinateSystems == null) matchingCoordinateSystems = wmsLayerCoordinateSystems.toMutableSet()
else matchingCoordinateSystems.retainAll(wmsLayerCoordinateSystems.toSet())
}
val wmsLayerConfig = WmsLayerConfig(serviceAddress, wmsLayers.joinToString(",") { l -> l.name ?: "" })
when (val wmsVersion = wmsCapabilities.version) {
"1.3.0" -> wmsLayerConfig.wmsVersion = wmsVersion
else -> error(
makeMessage("WmsLayerFactory", "getLayerConfigFromWmsCapabilities", "Version not compatible")
)
}
if (matchingCoordinateSystems?.contains("EPSG:4326") == true) {
wmsLayerConfig.coordinateSystem = "EPSG:4326"
} else if (matchingCoordinateSystems?.contains("CRS:84") == true) {
wmsLayerConfig.coordinateSystem = "CRS:84"
} else error(
makeMessage("WmsLayerFactory", "getLayerConfigFromWmsCapabilities", "Coordinate systems not compatible")
)
// Negotiate Image Formats
val imageFormats = wmsCapabilities.capability.request.getMap.formats
wmsLayerConfig.imageFormat = compatibleImageFormats.firstOrNull { format -> imageFormats.contains(format) } ?: error(
makeMessage("WmsLayerFactory", "getLayerConfigFromWmsCapabilities", "Image Formats Not Compatible")
)
// Setup transparency. If at least one layer is opaque then transparency is disabled
wmsLayerConfig.isTransparent = !wmsLayers.any { layer -> layer.isOpaque }
return wmsLayerConfig
}
internal fun getLevelSetConfigFromWmsCapabilities(layerCapabilities: List): LevelSetConfig {
val levelSetConfig = LevelSetConfig()
var minScaleDenominator = Double.MAX_VALUE
val sector = Sector()
for (layerCapability in layerCapabilities) {
layerCapability.minScaleDenominator?.let { minScaleDenominator = minScaleDenominator.coerceAtMost(it) }
layerCapability.geographicBoundingBox?.let { sector.union(it) }
}
if (!sector.isEmpty) levelSetConfig.sector.copy(sector) else error(
makeMessage(
"WmsLayerFactory", "getLevelSetConfigFromWmsCapabilities", "Geographic Bounding Box Not Defined"
)
)
levelSetConfig.numLevels = when {
minScaleDenominator != Double.MAX_VALUE -> {
// WMS 1.3.0 scale configuration. Based on the WMS 1.3.0 spec page 28. The hard coded value 0.00028 is
// detailed in the spec as the common pixel size of 0.28mm x 0.28mm. Configures the maximum level not to
// exceed the specified min scale denominator.
val minMetersPerPixel = minScaleDenominator * 0.00028
val minRadiansPerPixel = minMetersPerPixel / Ellipsoid.WGS84.semiMajorAxis
levelSetConfig.numLevelsForMinResolution(minRadiansPerPixel)
}
else -> DEFAULT_WMS_NUM_LEVELS // Default scale configuration when no minimum scale denominator or scale hint is provided.
}
return levelSetConfig
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy