commonMain.earth.worldwind.shape.TiledSurfaceImage.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.shape
import earth.worldwind.draw.DrawableSurfaceTexture
import earth.worldwind.geom.Angle
import earth.worldwind.geom.Matrix3
import earth.worldwind.geom.Sector
import earth.worldwind.globe.Globe
import earth.worldwind.render.AbstractRenderable
import earth.worldwind.render.RenderContext
import earth.worldwind.render.Texture
import earth.worldwind.render.image.ImageOptions
import earth.worldwind.render.image.ImageSource
import earth.worldwind.render.image.ImageTile
import earth.worldwind.render.program.SurfaceTextureProgram
import earth.worldwind.util.*
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.seconds
open class TiledSurfaceImage(tileFactory: TileFactory, levelSet: LevelSet): AbstractRenderable("Tiled Surface Image") {
/**
* Tile factory implementation.
*/
var tileFactory = tileFactory
set(value) {
field = value
invalidateTiles()
}
/**
* Tile pyramid representation.
*/
var levelSet = levelSet
set(value) {
field = value
invalidateTiles()
}
/**
* Additional image texture options.
*/
var imageOptions: ImageOptions? = null
set(value) {
field = value
invalidateTiles()
}
/**
* Define imagery level of details. It controls tile pixel density on the screen.
*/
var detailControl = 1.0
/**
* Retrieve top level tiles to avoid black holes when navigating and zooming out camera
*/
var retrieveTopLevelTiles = true
/**
* Use ancestor tile texture as a fallback for descendants
*/
var useAncestorTileTexture = true
/**
* Define cache tiles factory implementation.
*/
var cacheTileFactory: CacheTileFactory? = null
/**
* Configures tiled surface image to retrieve only the cache source.
*/
var isCacheOnly = false
/**
* Memory cache for this layer's subdivision tiles. Each entry contains an array of four image tiles corresponding
* to the subdivision of the group's common parent tile. The cache is configured to hold 1200 groups, a number
* empirically determined to be sufficient for storing the tiles needed to navigate a small region.
*/
protected var tileCache = LruMemoryCache>(1200)
protected var activeProgram: SurfaceTextureProgram? = null
protected var ancestorTile: ImageTile? = null
protected var ancestorTexture: Texture? = null
protected val ancestorTexCoordMatrix = Matrix3()
protected val topLevelTiles = mutableListOf()
protected var lastGlobeState: Globe.State? = null
/**
* Makes a copy of this tiled surface image
*/
open fun clone() = TiledSurfaceImage(tileFactory, LevelSet(levelSet)).also {
it.imageOptions = imageOptions
}
/**
* Cache size should be adjusted in case of levelSet or detailControl changed.
*/
fun setupTileCache(capacity: Long, lowWater: Long = (capacity * 0.75).toLong()) {
tileCache = LruMemoryCache(capacity, lowWater)
}
override fun doRender(rc: RenderContext) {
if (rc.terrain.sector.isEmpty) return // no terrain surface to render on
checkGlobeState(rc)
determineActiveProgram(rc)
assembleTiles(rc)
activeProgram = null // clear the active program to avoid leaking render resources
ancestorTile = null // clear the ancestor tile and texture
ancestorTexture = null
}
@Throws(IllegalStateException::class, IllegalArgumentException::class)
open fun launchBulkRetrieval(
scope: CoroutineScope, sector: Sector, resolution: ClosedRange, onProgress: ((Long, Long, Long) -> Unit)?,
retrieveTile: suspend (imageSource: ImageSource, cacheSource: ImageSource, options: ImageOptions?) -> Boolean
): Job {
val cacheTileFactory = cacheTileFactory ?: error("Cache not configured")
require(sector.intersect(levelSet.sector)) { "Sector does not intersect tiled surface image sector" }
return scope.launch(Dispatchers.Default) {
val minLevel = levelSet.levelForResolution(resolution.endInclusive)
val maxLevel = levelSet.levelForResolution(resolution.start)
val tileCount = levelSet.tileCount(sector, minLevel, maxLevel)
var downloaded = 0L
var skipped = 0L
if (topLevelTiles.isEmpty()) createTopLevelTiles()
topLevelTiles.forEach { topLevelTile ->
processAndSubdivideTile(topLevelTile as ImageTile, sector, minLevel, maxLevel) { tile ->
var attempt = 0
// Retry download attempts till success or 404 not fond or job canceled
while(true) {
// Check if a job canceled
ensureActive()
// No image source indicates an empty level or an image missing from the tiled data store
val imageSource = tile.imageSource ?: break
// If cache tile factory is specified, then create a cache source and store it in tile
val cacheSource = tile.cacheSource
?: (cacheTileFactory.createTile(tile.sector, tile.level, tile.row, tile.column) as ImageTile)
.imageSource?.also { tile.cacheSource = it } ?: break
// Attempt to download tile
try {
++attempt
if (retrieveTile(imageSource, cacheSource, imageOptions)) {
// Tile successfully downloaded
onProgress?.invoke(++downloaded, skipped, tileCount)
} else {
// Received data cannot be decoded as image
onProgress?.invoke(downloaded, ++skipped, tileCount)
}
break // Continue downloading the next tile
} catch (throwable: Throwable) {
delay(if (attempt % makeLocalRetries == 0) makeLocalTimeoutLong else makeLocalTimeoutShort)
}
}
}
}
}
}
protected open suspend fun processAndSubdivideTile(
tile: ImageTile, sector: Sector, minLevel: Level, maxLevel: Level, process: suspend (ImageTile) -> Unit
) {
if (!tile.intersectsSector(sector)) return // Ignore tiles and its descendants outside the specified sector
val levelNumber = tile.level.levelNumber
// Skip tiles with level less than specified offset from the process
if (levelNumber >= minLevel.levelNumber && levelNumber >= levelSet.levelOffset) process(tile)
// Do not subdivide if specified level or last available level reached
if (levelNumber < maxLevel.levelNumber && !tile.level.isLastLevel) {
tile.subdivide(tileFactory).forEach { processAndSubdivideTile(it as ImageTile, sector, minLevel, maxLevel, process) }
}
}
protected open fun determineActiveProgram(rc: RenderContext) {
activeProgram = rc.getShaderProgram { SurfaceTextureProgram() }
}
protected open fun assembleTiles(rc: RenderContext) {
// TODO
// The need to create Tiles with a defined image source couples the need to determine a tile's visibility with
// he need to know its image source. Decoupling the two would mean we only need to know the image source when
// the texture is actually requested Could the tile-based operations done here be implicit on level/row/column,
// or use transient pooled tile objects not tied to an image source?
if (topLevelTiles.isEmpty()) createTopLevelTiles()
for (i in topLevelTiles.indices) addTileOrDescendants(rc, topLevelTiles[i] as ImageTile)
}
protected open fun createTopLevelTiles() = Tile.assembleTilesForLevel(levelSet.firstLevel, tileFactory, topLevelTiles)
protected open fun addTileOrDescendants(rc: RenderContext, tile: ImageTile) {
// ignore tiles which soes not fit projection limits
if (rc.globe.projectionLimits?.let { tile.intersectsSector(it) } == false) return
// ignore the tile and its descendants if it's not needed or not visible
if (!tile.intersectsSector(levelSet.sector) || !tile.intersectsSector(rc.terrain.sector) || !tile.intersectsFrustum(rc)) return
val retrieveCurrentLevel = tile.level.levelNumber >= levelSet.levelOffset
if (tile.level.isLastLevel || !tile.mustSubdivide(rc, detailControl)) {
if (retrieveCurrentLevel) addTile(rc, tile)
return // use the tile if it does not need to be subdivided
}
val currentAncestorTile = ancestorTile
val currentAncestorTexture = ancestorTexture
getTexture(rc, tile, retrieveTopLevelTiles && retrieveCurrentLevel)?.let { tileTexture ->
// tile has a texture; use it as a fallback tile for descendants
if (useAncestorTileTexture) {
ancestorTile = tile
ancestorTexture = tileTexture
}
}
// each tile has a cached size of 1, recursively process the tile's children
val children = tile.subdivideToCache(tileFactory, tileCache, 4)
for (i in children.indices) addTileOrDescendants(rc, children[i] as ImageTile)
ancestorTile = currentAncestorTile // restore the last fallback tile, even if it was null
ancestorTexture = currentAncestorTexture
}
protected open fun addTile(rc: RenderContext, tile: ImageTile) {
val texture = getTexture(rc, tile)
val ancestorTile = ancestorTile
val ancestorTexture = ancestorTexture
val opacity = if (rc.isPickMode) 1f else rc.currentLayer.opacity
if (texture != null) { // use the tile's own texture
val pool = rc.getDrawablePool()
val drawable = DrawableSurfaceTexture.obtain(pool).set(
activeProgram, tile.sector, opacity, texture, texture.coordTransform, rc.globe.offset
)
rc.offerSurfaceDrawable(drawable, 0.0 /*z-order*/)
} else if (ancestorTile != null && ancestorTexture != null) { // use the ancestor tile's texture, transformed to fill the tile sector
ancestorTexCoordMatrix.copy(ancestorTexture.coordTransform)
ancestorTexCoordMatrix.multiplyByTileTransform(tile.sector, ancestorTile.sector)
val pool = rc.getDrawablePool()
val drawable = DrawableSurfaceTexture.obtain(pool).set(
activeProgram, tile.sector, opacity, ancestorTexture, ancestorTexCoordMatrix, rc.globe.offset
)
rc.offerSurfaceDrawable(drawable, 0.0 /*z-order*/)
}
}
// TODO If cache source retrieved but it is outdated, than try to retrieve original image source anyway to refresh cache
protected open fun getTexture(rc: RenderContext, tile: ImageTile, retrieve: Boolean = true): Texture? {
// No image source indicates an empty level or an image missing from the tiled data store
val imageSource = tile.imageSource ?: return null
// If cache tile factory is specified, then create a cache source and store it in tile
val cacheSource = tile.cacheSource ?: cacheTileFactory?.run {
(createTile(tile.sector, tile.level, tile.row, tile.column) as ImageTile).imageSource?.also { tile.cacheSource = it }
}
// Read cache only in case of tile factory and cache factory are the same
val isCacheOnly = isCacheOnly || tileFactory == cacheTileFactory
// If a cache source is not absent, then retrieve it instead of an original image source
val isCacheAbsent = cacheSource == null || rc.renderResourceCache.absentResourceList.isResourceAbsent(cacheSource.hashCode())
return rc.getTexture(
if (isCacheAbsent) imageSource else cacheSource!!, imageOptions, retrieve && (!isCacheOnly || !isCacheAbsent)
)
}
protected open fun checkGlobeState(rc: RenderContext) {
// Invalidate tiles cache when globe state changes
if (rc.globeState != lastGlobeState) {
invalidateTiles()
lastGlobeState = rc.globeState
}
}
open fun invalidateTiles() {
topLevelTiles.clear()
tileCache.clear()
}
companion object {
/**
* Number of reties of bulk tile retrieval before long timeout
*/
var makeLocalRetries = 3
/**
* Short timeout on bulk tile retrieval failed
*/
var makeLocalTimeoutShort = 5.seconds
/**
* Long timeout on bulk tile retrieval failed
*/
var makeLocalTimeoutLong = 15.seconds
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy