/*
* Copyright 2015-2017 WorldWind Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @exports TiledImageLayer
*/
define([
'../util/AbsentResourceList',
'../error/ArgumentError',
'../render/ImageTile',
'../layer/Layer',
'../util/LevelSet',
'../util/Logger',
'../cache/MemoryCache',
'../render/Texture',
'../util/Tile',
'../util/WWUtil'
],
function (AbsentResourceList,
ArgumentError,
ImageTile,
Layer,
LevelSet,
Logger,
MemoryCache,
Texture,
Tile,
WWUtil) {
"use strict";
/**
* Constructs a tiled image layer.
* @alias TiledImageLayer
* @constructor
* @classdesc
* Provides a layer that displays multi-resolution imagery arranged as adjacent tiles in a pyramid.
* This is the primary WorldWind base class for displaying imagery of this type. While it may be used as a
* stand-alone class, it is typically subclassed by classes that identify the remote image server.
* <p>
* While the image tiles for this class are typically drawn from a remote server such as a WMS server. The actual
* retrieval protocol is independent of this class and encapsulated by a class implementing the {@link UrlBuilder}
* interface and associated with instances of this class as a property.
* <p>
* There is no requirement that image tiles of this class be remote, they may be local or procedurally generated. For
* such cases the subclass overrides this class' [retrieveTileImage]{@link TiledImageLayer#retrieveTileImage} method.
* <p>
* Layers of this type are by default not pickable. Their pick-enabled flag is initialized to false.
*
* @augments Layer
* @param {Sector} sector The sector this layer covers.
* @param {Location} levelZeroDelta The size in latitude and longitude of level zero (lowest resolution) tiles.
* @param {Number} numLevels The number of levels to define for the layer. Each level is successively one power
* of two higher resolution than the next lower-numbered level. (0 is the lowest resolution level, 1 is twice
* that resolution, etc.)
* Each level contains four times as many tiles as the next lower-numbered level, each 1/4 the geographic size.
* @param {String} imageFormat The mime type of the image format for the layer's tiles, e.g., <em>image/png</em>.
* @param {String} cachePath A string uniquely identifying this layer relative to other layers.
* @param {Number} tileWidth The horizontal size of image tiles in pixels.
* @param {Number} tileHeight The vertical size of image tiles in pixels.
* @throws {ArgumentError} If any of the specified sector, level-zero delta, cache path or image format arguments are
* null or undefined, or if the specified number of levels, tile width or tile height is less than 1.
*
*/
var TiledImageLayer = function (sector, levelZeroDelta, numLevels, imageFormat, cachePath, tileWidth, tileHeight) {
if (!sector) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor", "missingSector"));
}
if (!levelZeroDelta) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor",
"The specified level-zero delta is null or undefined."));
}
if (!imageFormat) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor",
"The specified image format is null or undefined."));
}
if (!cachePath) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor",
"The specified cache path is null or undefined."));
}
if (!numLevels || numLevels < 1) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor",
"The specified number of levels is less than one."));
}
if (!tileWidth || !tileHeight || tileWidth < 1 || tileHeight < 1) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor",
"The specified tile width or height is less than one."));
}
Layer.call(this, "Tiled Image Layer");
this.retrievalImageFormat = imageFormat;
this.cachePath = cachePath;
this.levels = new LevelSet(sector, levelZeroDelta, numLevels, tileWidth, tileHeight);
/**
* Controls the level of detail switching for this layer. The next highest resolution level is
* used when an image's texel size is greater than this number of pixels, up to the maximum resolution
* of this layer.
* @type {Number}
* @default 1.75
*/
this.detailControl = 1.75;
/**
* Indicates whether credentials are sent when requesting images from a different origin.
*
* Allowed values are anonymous and use-credentials.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-crossorigin
* @type {string}
* @default anonymous
*/
this.crossOrigin = 'anonymous';
/* Intentionally not documented.
* Indicates the time at which this layer's imagery expire. Expired images are re-retrieved
* when the current time exceeds the specified expiry time. If null, images do not expire.
* @type {Date}
*/
this.expiration = null;
this.currentTiles = [];
this.currentTilesInvalid = true;
this.tileCache = new MemoryCache(500000, 400000);
this.currentRetrievals = [];
this.absentResourceList = new AbsentResourceList(3, 50e3);
this.pickEnabled = false;
};
TiledImageLayer.prototype = Object.create(Layer.prototype);
// Inherited from Layer.
TiledImageLayer.prototype.refresh = function () {
this.expiration = new Date();
this.currentTilesInvalid = true;
};
/**
* Initiates retrieval of this layer's level 0 images. Use
* [isPrePopulated]{@link TiledImageLayer#isPrePopulated} to determine when the images have been retrieved
* and associated with the level 0 tiles.
* Pre-populating is not required. It is used to eliminate the visual effect of loading tiles incrementally,
* but only for level 0 tiles. An application might pre-populate a layer in order to delay displaying it
* within a time series until all the level 0 images have been retrieved and added to memory.
* @param {WorldWindow} wwd The WorldWindow for which to pre-populate this layer.
* @throws {ArgumentError} If the specified WorldWindow is null or undefined.
*/
TiledImageLayer.prototype.prePopulate = function (wwd) {
if (!wwd) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "prePopulate", "missingWorldWindow"));
}
var dc = wwd.drawContext;
if (!this.topLevelTiles || (this.topLevelTiles.length === 0)) {
this.createTopLevelTiles(dc);
}
for (var i = 0; i < this.topLevelTiles.length; i++) {
var tile = this.topLevelTiles[i];
if (!this.isTileTextureInMemory(dc, tile)) {
this.retrieveTileImage(dc, tile, true); // suppress redraw upon successful retrieval
}
}
};
/**
* Initiates retrieval of this layer's tiles that are visible in the specified WorldWindow. Pre-populating is
* not required. It is used to eliminate the visual effect of loading tiles incrementally.
* @param {WorldWindow} wwd The WorldWindow for which to pre-populate this layer.
* @throws {ArgumentError} If the specified WorldWindow is null or undefined.
*/
TiledImageLayer.prototype.prePopulateCurrentTiles = function (wwd) {
if (!wwd) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "prePopulate", "missingWorldWindow"));
}
var dc = wwd.drawContext;
this.assembleTiles(dc);
for (var i = 0, len = this.currentTiles.length; i < len; i++) {
var tile = this.currentTiles[i];
if (!this.isTileTextureInMemory(dc, tile)) {
this.retrieveTileImage(dc, tile, true); // suppress redraw upon successful retrieval
}
}
};
/**
* Indicates whether this layer's level 0 tile images have been retrieved and associated with the tiles.
* Use [prePopulate]{@link TiledImageLayer#prePopulate} to initiate retrieval of level 0 images.
* @param {WorldWindow} wwd The WorldWindow associated with this layer.
* @returns {Boolean} true if all level 0 images have been retrieved, otherwise false.
* @throws {ArgumentError} If the specified WorldWindow is null or undefined.
*/
TiledImageLayer.prototype.isPrePopulated = function (wwd) {
if (!wwd) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "isPrePopulated", "missingWorldWindow"));
}
for (var i = 0; i < this.topLevelTiles.length; i++) {
if (!this.isTileTextureInMemory(wwd.drawContext, this.topLevelTiles[i])) {
return false;
}
}
return true;
};
// Intentionally not documented.
TiledImageLayer.prototype.createTile = function (sector, level, row, column) {
var path = this.cachePath + "-layer/" + level.levelNumber + "/" + row + "/" + row + "_" + column + "."
+ WWUtil.suffixForMimeType(this.retrievalImageFormat);
return new ImageTile(sector, level, row, column, path);
};
// Documented in superclass.
TiledImageLayer.prototype.doRender = function (dc) {
if (!dc.terrain)
return;
if (this.currentTilesInvalid
|| !this.lasTtMVP || !dc.navigatorState.modelviewProjection.equals(this.lasTtMVP)
|| dc.globeStateKey != this.lastGlobeStateKey) {
this.currentTilesInvalid = false;
// Tile fading works visually only when the surface tiles are opaque, otherwise the surface flashes
// when two tiles are drawn over the same area, even though one of them is semi-transparent.
// So do not provide fading when the surface opacity is less than 1;
if (dc.surfaceOpacity >= 1 && this.opacity >= 1) {
// Fading of outgoing tiles requires determination of the those tiles. Prepare an object with all of
// the preceding frame's tiles so that we can subsequently compare the list of newly selected tiles
// with the previously selected tiles.
this.previousTiles = {};
for (var j = 0; j < this.currentTiles.length; j++) {
this.previousTiles[this.currentTiles[j].imagePath] = this.currentTiles[j];
}
this.assembleTiles(dc);
this.fadeOutgoingTiles(dc);
} else {
this.assembleTiles(dc);
}
}
this.lasTtMVP = dc.navigatorState.modelviewProjection;
this.lastGlobeStateKey = dc.globeStateKey;
if (this.currentTiles.length > 0) {
dc.surfaceTileRenderer.renderTiles(dc, this.currentTiles, this.opacity, dc.surfaceOpacity >= 1);
dc.frameStatistics.incrementImageTileCount(this.currentTiles.length);
this.inCurrentFrame = true;
}
};
TiledImageLayer.prototype.fadeOutgoingTiles = function (dc) {
// Determine which files are outgoing and fade their disappearance. Must be called after this frame's
// current tiles for this layer have been determined.
var visibilityDelta = (dc.timestamp - dc.previousRedrawTimestamp) / dc.fadeTime;
// Create a hash table of the current tiles so that we can check for tile inclusion below.
var current = {};
for (var i = 0; i < this.currentTiles.length; i++) {
var tile = this.currentTiles[i];
current[tile.imagePath] = tile;
}
// Determine whether the tile was in the previous frame but is not in this one. If that's the case,
// then the tile is outgoing and its opacity needs to be reduced.
for (var tileImagePath in this.previousTiles) {
if (this.previousTiles.hasOwnProperty(tileImagePath)) {
tile = this.previousTiles[tileImagePath];
if (tile.opacity > 0 && !current[tile.imagePath]) {
// Compute the reduced.
tile.opacity = Math.max(0, tile.opacity - visibilityDelta);
// If not fully faded, add the tile to the list of current tiles and request a redraw so that
// we'll be called continuously until all tiles have faded completely. Note that order in the
// current tiles list is important: the non-opaque tiles must be drawn after the opaque tiles.
if (tile.opacity > 0) {
this.currentTiles.push(tile);
this.currentTilesInvalid = true;
dc.redrawRequested = true;
}
}
}
}
};
// Documented in superclass.
TiledImageLayer.prototype.isLayerInView = function (dc) {
return dc.terrain && dc.terrain.sector && dc.terrain.sector.intersects(this.levels.sector);
};
// Documented in superclass.
TiledImageLayer.prototype.createTopLevelTiles = function (dc) {
this.topLevelTiles = [];
Tile.createTilesForLevel(this.levels.firstLevel(), this, this.topLevelTiles);
};
// Intentionally not documented.
TiledImageLayer.prototype.assembleTiles = function (dc) {
this.currentTiles = [];
if (!this.topLevelTiles || (this.topLevelTiles.length === 0)) {
this.createTopLevelTiles(dc);
}
for (var i = 0, len = this.topLevelTiles.length; i < len; i++) {
var tile = this.topLevelTiles[i];
tile.update(dc);
this.currentAncestorTile = null;
if (this.isTileVisible(dc, tile)) {
this.addTileOrDescendants(dc, tile);
}
}
};
// Intentionally not documented.
TiledImageLayer.prototype.addTileOrDescendants = function (dc, tile) {
if (this.tileMeetsRenderingCriteria(dc, tile)) {
this.addTile(dc, tile);
return;
}
var ancestorTile = null;
try {
if (this.isTileTextureInMemory(dc, tile) || tile.level.levelNumber === 0) {
ancestorTile = this.currentAncestorTile;
this.currentAncestorTile = tile;
}
var nextLevel = this.levels.level(tile.level.levelNumber + 1),
subTiles = tile.subdivideToCache(nextLevel, this, this.tileCache);
for (var i = 0, len = subTiles.length; i < len; i++) {
var child = subTiles[i];
child.update(dc);
if (this.levels.sector.intersects(child.sector) && this.isTileVisible(dc, child)) {
this.addTileOrDescendants(dc, child);
}
}
} finally {
if (ancestorTile) {
this.currentAncestorTile = ancestorTile;
}
}
};
// Intentionally not documented.
TiledImageLayer.prototype.addTile = function (dc, tile) {
tile.fallbackTile = null;
var texture = dc.gpuResourceCache.resourceForKey(tile.imagePath);
if (texture) {
tile.opacity = 1;;
this.currentTiles.push(tile);
// If the tile's texture has expired, cause it to be re-retrieved. Note that the current,
// expired texture is still used until the updated one arrives.
if (this.expiration && this.isTextureExpired(texture)) {
this.retrieveTileImage(dc, tile);
}
return;
}
this.retrieveTileImage(dc, tile);
if (this.currentAncestorTile) {
if (this.isTileTextureInMemory(dc, this.currentAncestorTile)) {
// Set up to map the ancestor tile into the current one.
tile.fallbackTile = this.currentAncestorTile;
tile.fallbackTile.opacity = 1;
this.currentTiles.push(tile);
}
}
};
// Intentionally not documented.
TiledImageLayer.prototype.isTileVisible = function (dc, tile) {
if (dc.globe.projectionLimits && !tile.sector.overlaps(dc.globe.projectionLimits)) {
return false;
}
return tile.extent.intersectsFrustum(dc.navigatorState.frustumInModelCoordinates);
};
// Intentionally not documented.
TiledImageLayer.prototype.tileMeetsRenderingCriteria = function (dc, tile) {
var s = this.detailControl;
if (tile.sector.minLatitude >= 75 || tile.sector.maxLatitude <= -75) {
s *= 1.2;
}
return tile.level.isLastLevel() || !tile.mustSubdivide(dc, s);
};
// Intentionally not documented.
TiledImageLayer.prototype.isTileTextureInMemory = function (dc, tile) {
return dc.gpuResourceCache.containsResource(tile.imagePath);
};
// Intentionally not documented.
TiledImageLayer.prototype.isTextureExpired = function (texture) {
return this.expiration && (texture.creationTime.getTime() <= this.expiration.getTime());
};
/**
* Retrieves the image for the specified tile. Subclasses should override this method in order to retrieve,
* compute or otherwise create the image.
* @param {DrawContext} dc The current draw context.
* @param {ImageTile} tile The tile for which to retrieve the resource.
* @param {Boolean} suppressRedraw true to suppress generation of redraw events when an image is successfully
* retrieved, otherwise false.
* @protected
*/
TiledImageLayer.prototype.retrieveTileImage = function (dc, tile, suppressRedraw) {
if (this.currentRetrievals.indexOf(tile.imagePath) < 0) {
if (this.absentResourceList.isResourceAbsent(tile.imagePath)) {
return;
}
var url = this.resourceUrlForTile(tile, this.retrievalImageFormat),
image = new Image(),
imagePath = tile.imagePath,
cache = dc.gpuResourceCache,
canvas = dc.currentGlContext.canvas,
layer = this;
if (!url) {
this.currentTilesInvalid = true;
return;
}
image.onload = function () {
Logger.log(Logger.LEVEL_INFO, "Image retrieval succeeded: " + url);
var texture = layer.createTexture(dc, tile, image);
layer.removeFromCurrentRetrievals(imagePath);
if (texture) {
cache.putResource(imagePath, texture, texture.size);
layer.currentTilesInvalid = true;
layer.absentResourceList.unmarkResourceAbsent(imagePath);
if (!suppressRedraw) {
// Send an event to request a redraw.
var e = document.createEvent('Event');
e.initEvent(WorldWind.REDRAW_EVENT_TYPE, true, true);
canvas.dispatchEvent(e);
}
}
};
image.onerror = function () {
layer.removeFromCurrentRetrievals(imagePath);
layer.absentResourceList.markResourceAbsent(imagePath);
Logger.log(Logger.LEVEL_WARNING, "Image retrieval failed: " + url);
};
this.currentRetrievals.push(imagePath);
image.crossOrigin = this.crossOrigin;
image.src = url;
}
};
// Intentionally not documented.
TiledImageLayer.prototype.createTexture = function (dc, tile, image) {
return new Texture(dc.currentGlContext, image);
};
// Intentionally not documented.
TiledImageLayer.prototype.removeFromCurrentRetrievals = function (imagePath) {
var index = this.currentRetrievals.indexOf(imagePath);
if (index > -1) {
this.currentRetrievals.splice(index, 1);
}
};
/**
* Returns the URL string for the resource.
* @param {ImageTile} tile The tile whose image is returned
* @param {String} imageFormat The mime type of the image format desired.
* @returns {String} The URL string, or null if the string can not be formed.
* @protected
*/
TiledImageLayer.prototype.resourceUrlForTile = function (tile, imageFormat) {
if (this.urlBuilder) {
return this.urlBuilder.urlForTile(tile, imageFormat);
} else {
return null;
}
};
return TiledImageLayer;
});