
commonMain.earth.worldwind.ogc.Wcs201ElevationCoverage.kt Maven / Gradle / Ivy
package earth.worldwind.ogc
import com.eygraber.uri.Uri
import earth.worldwind.WorldWind
import earth.worldwind.geom.Angle
import earth.worldwind.geom.Sector
import earth.worldwind.geom.Sector.Companion.fromDegrees
import earth.worldwind.geom.TileMatrix
import earth.worldwind.geom.TileMatrixSet
import earth.worldwind.geom.TileMatrixSet.Companion.fromTilePyramid
import earth.worldwind.globe.elevation.ElevationSource
import earth.worldwind.globe.elevation.ElevationSourceFactory
import earth.worldwind.globe.elevation.coverage.TiledElevationCoverage
import earth.worldwind.globe.elevation.coverage.WebElevationCoverage
import earth.worldwind.ogc.gml.GmlRectifiedGrid
import earth.worldwind.ogc.gml.serializersModule
import earth.worldwind.ogc.wcs.Wcs201CoverageDescription
import earth.worldwind.ogc.wcs.Wcs201CoverageDescriptions
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.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import nl.adaptivity.xmlutil.serialization.XML
/**
* Generates elevations from OGC Web Coverage Service (WCS) version 2.0.1.
* Wcs201ElevationCoverage requires the WCS service address, coverage name, and coverage bounding sector. Get Coverage
* requests generated for retrieving data use the WCS version 2.0.1 protocol and are limited to the "image/tiff" format
* and the EPSG:4326 coordinate system. Wcs201ElevationCoverage does not perform version negotiation and assumes the
* service supports the format and coordinate system parameters detailed here. The subset CRS is configured as EPSG:4326
* and the axis labels are set as "Lat" and "Long". The scaling axis labels are set as:
* http://www.opengis.net/def/axis/OGC/1/i and http://www.opengis.net/def/axis/OGC/1/j
*/
open class Wcs201ElevationCoverage private constructor(
final override val serviceAddress: String,
final override val coverageName: String,
final override val outputFormat: String,
tileMatrixSet: TileMatrixSet,
elevationSourceFactory: ElevationSourceFactory,
serviceMetadata: String? = null
): TiledElevationCoverage(tileMatrixSet, elevationSourceFactory), WebElevationCoverage {
final override val serviceType = SERVICE_TYPE
final override var serviceMetadata = serviceMetadata
private set
protected val xml = XML(serializersModule) { defaultPolicy { ignoreUnknownChildren() } }
/**
* Constructs a Web Coverage Service (WCS) elevation coverage with specified WCS configuration values.
*
* @param serviceAddress the WCS service address
* @param coverageName the WCS coverage name
* @param outputFormat the WCS source data format
* @param sector the coverage's geographic bounding sector
* @param resolution the target resolution in angular value of latitude per texel
* 90-degree tiles containing 256x256 elevation pixels
*
* @throws IllegalArgumentException If any argument is null or if the number of levels is less than 0
*/
constructor(serviceAddress: String, coverageName: String, outputFormat: String, sector: Sector, resolution: Angle): this(
serviceAddress, coverageName, outputFormat,
fromTilePyramid(sector, if (sector.isFullSphere) 2 else 1, 1, 256, 256, resolution),
Wcs201ElevationSourceFactory(serviceAddress, coverageName, outputFormat)
)
/**
* Attempts to construct a Web Coverage Service (WCS) elevation coverage with the provided service address and
* coverage id. This constructor initiates an asynchronous request for the DescribeCoverage document and then uses
* the information provided to determine a suitable Sector and level count. If the coverage id doesn't match the
* available coverages or there is another error, no data will be provided and the error will be logged.
*
* @param serviceAddress the WCS service address
* @param coverageName the WCS coverage name
* @param outputFormat the WCS source data format
* @param serviceMetadata optional WCS coverage description XML string to avoid online coverages request
*/
constructor(serviceAddress: String, coverageName: String, outputFormat: String, serviceMetadata: String? = null): this(
serviceAddress, coverageName, outputFormat, TileMatrixSet(), dummyElevationSource(), serviceMetadata
) {
mainScope.launch {
try {
// Fetch the DescribeCoverage document and determine the bounding box and number of levels
val coverageDescription = if (serviceMetadata != null) decodeCoverageDescription(serviceMetadata)
else describeCoverage(serviceAddress, coverageName).getCoverageDescription(coverageName) ?: error(
makeMessage(
"Wcs201ElevationCoverage", "constructor",
"WCS coverage is undefined: $coverageName"
)
)
val axisLabels = coverageDescription.boundedBy.envelope.axisLabelsList
require(axisLabels.size >= 2) {
makeMessage(
"Wcs201ElevationCoverage", "constructor",
"WCS coverage axis labels are undefined: $coverageName"
)
}
[email protected] = serviceMetadata ?: xml.encodeToString(coverageDescription)
elevationSourceFactory = Wcs201ElevationSourceFactory(serviceAddress, coverageName, outputFormat, axisLabels)
tileMatrixSet = tileMatrixSetFromCoverageDescription(coverageDescription)
WorldWind.requestRedraw()
} catch (logged: Throwable) {
logMessage(
ERROR, "Wcs201ElevationCoverage", "constructor",
"Exception initializing WCS coverage serviceAddress:$serviceAddress coverage:$coverageName", logged
)
}
}
}
override fun clone() = Wcs201ElevationCoverage(
serviceAddress, coverageName, outputFormat, tileMatrixSet, elevationSourceFactory, serviceMetadata
).also {
it.displayName = displayName
it.sector.copy(sector)
}
protected open fun tileMatrixSetFromCoverageDescription(coverageDescription: Wcs201CoverageDescription): TileMatrixSet {
val srsName = coverageDescription.boundedBy.envelope.srsName
require(srsName != null && srsName.contains("4326")) {
makeMessage(
"Wcs201ElevationCoverage", "tileMatrixSetFromCoverageDescription",
"WCS Envelope SRS is incompatible: $srsName"
)
}
val lowerCorner = coverageDescription.boundedBy.envelope.lowerCorner.values
val upperCorner = coverageDescription.boundedBy.envelope.upperCorner.values
require(lowerCorner.size == 2 && upperCorner.size == 2) {
makeMessage(
"Wcs201ElevationCoverage", "tileMatrixSetFromCoverageDescription",
"WCS Envelope is invalid"
)
}
// Determine the number of data points in the i and j directions
val geometry = coverageDescription.domainSet.geometry
require(geometry is GmlRectifiedGrid) {
makeMessage(
"Wcs201ElevationCoverage", "tileMatrixSetFromCoverageDescription",
"WCS domainSet Geometry is incompatible:$geometry"
)
}
val gridLow = geometry.limits.gridEnvelope.low.values
val gridHigh = geometry.limits.gridEnvelope.high.values
require(gridLow.size == 2 && gridHigh.size == 2) {
makeMessage(
"Wcs201ElevationCoverage", "tileMatrixSetFromCoverageDescription",
"WCS GridEnvelope is invalid"
)
}
val boundingSector = fromDegrees(
lowerCorner[0], lowerCorner[1],
upperCorner[0] - lowerCorner[0],
upperCorner[1] - lowerCorner[1]
)
val tileWidth = 256
val tileHeight = 256
val resolution = boundingSector.deltaLatitude.div(gridHigh[1] - gridLow[1])
return TileMatrixSet.fromTilePyramid(
boundingSector, if (boundingSector.isFullSphere) 2 else 1, 1, tileWidth, tileHeight, resolution
)
}
@Throws(OwsException::class)
protected open suspend fun describeCoverage(serviceAddress: String, coverageId: String) = DefaultHttpClient().use {
val serviceUri = Uri.parse(serviceAddress).buildUpon()
.appendQueryParameter("VERSION", "2.0.1")
.appendQueryParameter("SERVICE", "WCS")
.appendQueryParameter("REQUEST", "DescribeCoverage")
.appendQueryParameter("COVERAGEID", coverageId)
.build()
val response = it.get(serviceUri.toString())
response.status to response.bodyAsText()
}.let { (status, xmlText) ->
withContext(Dispatchers.Default) {
if (status == HttpStatusCode.OK) xml.decodeFromString(xmlText)
else throw OwsException(xml.decodeFromString(xmlText))
}
}
protected open suspend fun decodeCoverageDescription(xmlText: String) = withContext(Dispatchers.Default) {
xml.decodeFromString(xmlText)
}
companion object {
const val SERVICE_TYPE = "WCS 2.0.1"
/**
* This is a dummy workaround for asynchronously defined ElevationSourceFactory
*/
private fun dummyElevationSource() = object : ElevationSourceFactory {
override val contentType = "Dummy"
override fun createElevationSource(tileMatrix: TileMatrix, row: Int, column: Int) =
ElevationSource.fromUnrecognized(Any())
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy