Source: globe/ElevationModel.js

/*
 * 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 ElevationModel
 */
define([
        '../util/AbsentResourceList',
        '../geom/Angle',
        '../error/ArgumentError',
        '../globe/ElevationImage',
        '../globe/ElevationTile',
        '../util/LevelSet',
        '../util/Logger',
        '../cache/MemoryCache',
        '../geom/Sector',
        '../util/Tile',
        '../util/WWMath'],
    function (AbsentResourceList,
              Angle,
              ArgumentError,
              ElevationImage,
              ElevationTile,
              LevelSet,
              Logger,
              MemoryCache,
              Sector,
              Tile,
              WWMath) {
        "use strict";

        /**
         * Constructs an elevation model.
         * @alias ElevationModel
         * @constructor
         * @classdesc Represents the elevations for an area, often but not necessarily the whole globe.
         * <p>
         *     While this class can be used as-is, it is intended to be a base class for more concrete elevation
         *     models, such as {@link EarthElevationModel}.
         * @param {Sector} coverageSector The sector this elevation model spans.
         * @param {Location} levelZeroDelta The size of top-level tiles, in degrees.
         * @param {Number} numLevels The number of levels used to represent this elevation model's resolution pyramid.
         * @param {String} retrievalImageFormat The mime type of the elevation data retrieved by this elevation model.
         * @param {String} cachePath A string unique to this elevation model relative to other elevation models used by
         * the application.
         * @param {Number} tileWidth The number of intervals (cells) in the longitudinal direction of this elevation
         * model's elevation tiles.
         * @param {Number} tileHeight The number of intervals (cells) in the latitudinal direction of this elevation
         * model's elevation tiles.
         * @throws {ArgumentError} If any argument is null or undefined, if the number of levels specified is less
         * than one, or if either the tile width or tile height are less than one.
         */
        var ElevationModel = function (coverageSector, levelZeroDelta, numLevels, retrievalImageFormat, cachePath,
                                       tileWidth, tileHeight) {
            if (!coverageSector) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "constructor", "missingSector"));
            }

            if (!levelZeroDelta) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "constructor",
                        "The specified level-zero delta is null or undefined."));
            }

            if (!retrievalImageFormat) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "constructor",
                        "The specified image format is null or undefined."));
            }

            if (!cachePath) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "constructor",
                        "The specified cache path is null or undefined."));
            }

            if (!numLevels || numLevels < 1) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "constructor",
                        "The specified number of levels is not greater than zero."));
            }

            if (!tileWidth || !tileHeight || tileWidth < 1 || tileHeight < 1) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "constructor",
                        "The specified tile width or height is not greater than zero."));
            }

            /**
             * The sector this elevation model spans.
             * @type {Sector}
             * @readonly
             */
            this.coverageSector = coverageSector;

            /**
             * The mime type to use when retrieving elevations.
             * @type {String}
             * @readonly
             */
            this.retrievalImageFormat = retrievalImageFormat;

            /** A unique string identifying this elevation model relative to other elevation models in use.
             * @type {String}
             * @readonly
             */
            this.cachePath = cachePath;

            /**
             * Indicates this elevation model's display name.
             * @type {String}
             * @default "Elevations"
             */
            this.displayName = "Elevations";

            /**
             * Indicates the last time this elevation model changed, in milliseconds since midnight Jan 1, 1970.
             * @type {Number}
             * @readonly
             * @default Date.now() at construction
             */
            this.timestamp = Date.now();

            /**
             * This elevation model's minimum elevation in meters.
             * @type {Number}
             * @default 0
             */
            this.minElevation = 0;

            /**
             * This elevation model's maximum elevation in meters.
             * @type {Number}
             */
            this.maxElevation = 0;

            /**
             * Indicates whether the data associated with this elevation model is point data. A value of false
             * indicates that the data is area data (pixel is area).
             * @type {Boolean}
             * @default true
             */
            this.pixelIsPoint = true;

            /**
             * The {@link LevelSet} created during construction of this elevation model.
             * @type {LevelSet}
             * @readonly
             */
            this.levels = new LevelSet(this.coverageSector, levelZeroDelta, numLevels, tileWidth, tileHeight);

            // These are internal and intentionally not documented.
            this.currentTiles = []; // holds assembled tiles
            this.currentSector = new Sector(0, 0, 0, 0); // a scratch variable
            this.tileCache = new MemoryCache(1000000, 800000); // for elevation tiles
            this.imageCache = new MemoryCache(10000000, 8000000); // for the elevations, themselves
            this.currentRetrievals = []; // Identifies elevation retrievals in progress
            this.absentResourceList = new AbsentResourceList(3, 5e3);
            this.id = ++ElevationModel.idPool;

            /**
             * A string identifying this elevation model's current state. Used to compare states during rendering to
             * determine whether globe-state dependent cached values must be updated. Applications typically do not
             * interact with this property. It is primarily used by shapes and terrain generators.
             * @memberof ElevationModel.prototype
             * @readonly
             * @type {String}
             */
            this.stateKey = "elevationModel " + this.id.toString() + " ";
        };

        ElevationModel.idPool = 0; // Used to assign unique IDs to elevation models for use in their state key.

        /**
         * Returns the minimum and maximum elevations within a specified sector.
         * @param {Sector} sector The sector for which to determine extreme elevations.
         * @returns {Number[]} An array containing the minimum and maximum elevations within the specified sector,
         * or null if the specified sector is outside this elevation model's coverage area.
         * @throws {ArgumentError} If the specified sector is null or undefined.
         */
        ElevationModel.prototype.minAndMaxElevationsForSector = function (sector) {
            if (!sector) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "minAndMaxElevationsForSector", "missingSector"));
            }

            var level = this.levels.levelForTexelSize(sector.deltaLatitude() * Angle.DEGREES_TO_RADIANS / 64);
            this.assembleTiles(level, sector, false);

            if (this.currentTiles.length == 0) {
                return null; // Sector is outside the elevation model's coverage area. Do not modify the result array.
            }

            // Assign the output extreme elevations to the largest and smallest double values, respectively. This has the effect
            // of expanding the extremes with each subsequent tile as needed. If we initialized this array with zeros then the
            // output extreme elevations would always contain zero, even when the range of the image's extreme elevations in the
            // sector does not contain zero.
            var min = Number.MAX_VALUE,
                max = -min,
                image,
                imageMin,
                imageMax,
                result = [];

            for (var i = 0, len = this.currentTiles.length; i < len; i++) {
                image = this.currentTiles[i].image();
                if (image) {
                    imageMin = image.minElevation;
                    if (min > imageMin) {
                        min = imageMin;
                    }

                    imageMax = image.maxElevation;
                    if (max < imageMax) {
                        max = imageMax;
                    }
                } else {
                    result[0] = this.minElevation;
                    result[1] = this.maxElevation;
                    return result; // At least one tile image is not in memory; return the model's extreme elevations.
                }
            }

            result[0] = min;
            result[1] = max;

            return result;
        };

        /**
         * Returns the elevation at a specified location.
         * @param {Number} latitude The location's latitude in degrees.
         * @param {Number} longitude The location's longitude in degrees.
         * @returns {Number} The elevation at the specified location, in meters. Returns zero if the location is
         * outside the coverage area of this elevation model.
         */
        ElevationModel.prototype.elevationAtLocation = function (latitude, longitude) {
            if (!this.coverageSector.containsLocation(latitude, longitude)) {
                return 0; // location is outside the elevation model's coverage
            }

            return this.pointElevationForLocation(latitude, longitude);
        };

        /**
         * Returns the elevations at locations within a specified sector.
         * @param {Sector} sector The sector for which to determine the elevations.
         * @param {Number} numLat The number of latitudinal sample locations within the sector.
         * @param {Number} numLon The number of longitudinal sample locations within the sector.
         * @param {Number} targetResolution The desired elevation resolution, in radians. (To compute radians from
         * meters, divide the number of meters by the globe's radius.)
         * @param {Number[]} result An array in which to return the requested elevations.
         * @returns {Number} The resolution actually achieved, which may be greater than that requested if the
         * elevation data for the requested resolution is not currently available.
         * @throws {ArgumentError} If the specified sector or result array is null or undefined, or if either of the
         * specified numLat or numLon values is less than one.
         */
        ElevationModel.prototype.elevationsForGrid = function (sector, numLat, numLon, targetResolution, result) {
            if (!sector) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "elevationsForSector", "missingSector"));
            }

            if (!result) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "elevationsForSector", "missingResult"));
            }

            if (!numLat || !numLon || numLat < 1 || numLon < 1) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "constructor",
                        "The specified number of latitudinal or longitudinal positions is less than one."));
            }

            var level = this.levels.levelForTexelSize(targetResolution);
            if (this.pixelIsPoint) {
                return this.pointElevationsForGrid(sector, numLat, numLon, level, result);
            } else {
                return this.areaElevationsForGrid(sector, numLat, numLon, level, result);
            }
        };

        // Intentionally not documented.
        ElevationModel.prototype.pointElevationForLocation = function (latitude, longitude) {
            var level = this.levels.lastLevel(),
                deltaLat = level.tileDelta.latitude,
                deltaLon = level.tileDelta.longitude,
                r = Tile.computeRow(deltaLat, latitude),
                c = Tile.computeColumn(deltaLon, longitude),
                tile,
                image = null;

            for (var i = level.levelNumber; i >= 0; i--) {
                tile = this.tileCache.entryForKey(i + "." + r + "." + c);
                if (tile) {
                    image = tile.image();
                    if (image) {
                        return image.elevationAtLocation(latitude, longitude);
                    }
                }

                r = Math.floor(r / 2);
                c = Math.floor(c / 2);
            }

            return 0; // did not find a tile with an image
        };

        // Intentionally not documented.
        ElevationModel.prototype.pointElevationsForGrid = function (sector, numLat, numLon, level, result) {
            var maxResolution = 0,
                resolution;

            this.assembleTiles(level, sector, true);
            if (this.currentTiles.length === 0) {
                return 0; // Sector is outside the elevation model's coverage area. Do not modify the results array.
            }

            // Sort from lowest resolution to highest so that higher resolutions override lower resolutions in the
            // loop below.
            this.currentTiles.sort(function (tileA, tileB) {
                return tileA.level.levelNumber - tileB.level.levelNumber;
            });

            for (var i = 0, len = this.currentTiles.length; i < len; i++) {
                var tile = this.currentTiles[i],
                    image = tile.image();

                if (image) {
                    image.elevationsForGrid(sector, numLat, numLon, result);
                    resolution = tile.level.texelSize;

                    if (maxResolution < resolution) {
                        maxResolution = resolution;
                    }
                } else {
                    maxResolution = Number.MAX_VALUE;
                }
            }

            return maxResolution;
        };

        // Internal. Returns elevations for a grid assuming pixel-is-area.
        ElevationModel.prototype.areaElevationsForGrid = function (sector, numLat, numLon, level, result) {
            var minLat = sector.minLatitude,
                maxLat = sector.maxLatitude,
                minLon = sector.minLongitude,
                maxLon = sector.maxLongitude,
                deltaLat = sector.deltaLatitude() / (numLat > 1 ? numLat - 1 : 1),
                deltaLon = sector.deltaLongitude() / (numLon > 1 ? numLon - 1 : 1),
                lat, lon, s, t,
                latIndex, lonIndex, resultIndex = 0;

            for (latIndex = 0, lat = minLat; latIndex < numLat; latIndex += 1, lat += deltaLat) {
                if (latIndex === numLat - 1) {
                    lat = maxLat; // explicitly set the last lat to the max latitude ensure alignment
                }

                for (lonIndex = 0, lon = minLon; lonIndex < numLon; lonIndex += 1, lon += deltaLon) {
                    if (lonIndex === numLon - 1) {
                        lon = maxLon; // explicitly set the last lon to the max longitude ensure alignment
                    }

                    if (this.coverageSector.containsLocation(lat, lon)) { // ignore locations outside of the model
                        s = (lon + 180) / 360;
                        t = (lat + 90) / 180;
                        this.areaElevationForCoord(s, t, level.levelNumber, result, resultIndex);
                    }

                    resultIndex++;
                }
            }

            return level.texelSize; // TODO: return the actual achieved
        };

        // Internal. Returns an elevation for a location assuming pixel-is-area.
        ElevationModel.prototype.areaElevationForCoord = function (s, t, levelNumber, result, resultIndex) {
            var level, levelWidth, levelHeight,
                tMin, tMax,
                vMin, vMax,
                u, v,
                x0, x1, y0, y1,
                xf, yf,
                retrieveTiles,
                pixels = new Float64Array(4);

            for (var i = levelNumber; i >= 0; i--) {
                level = this.levels.level(i);
                levelWidth = Math.round(level.tileWidth * 360 / level.tileDelta.longitude);
                levelHeight = Math.round(level.tileHeight * 180 / level.tileDelta.latitude);
                tMin = 1 / (2 * levelHeight);
                tMax = 1 - tMin;
                vMin = 0;
                vMax = levelHeight - 1;
                u = levelWidth * WWMath.fract(s); // wrap the horizontal coordinate
                v = levelHeight * WWMath.clamp(t, tMin, tMax); // clamp the vertical coordinate to the level edge
                x0 = WWMath.mod(Math.floor(u - 0.5), levelWidth);
                x1 = WWMath.mod((x0 + 1), levelWidth);
                y0 = WWMath.clamp(Math.floor(v - 0.5), vMin, vMax);
                y1 = WWMath.clamp(y0 + 1, vMin, vMax);
                xf = WWMath.fract(u - 0.5);
                yf = WWMath.fract(v - 0.5);
                retrieveTiles = (i == levelNumber) || (i == 0);

                if (this.lookupPixels(x0, x1, y0, y1, level, retrieveTiles, pixels)) {
                    result[resultIndex] = (1 - xf) * (1 - yf) * pixels[0] +
                        xf * (1 - yf) * pixels[1] +
                        (1 - xf) * yf * pixels[2] +
                        xf * yf * pixels[3];
                    return;
                }
            }
        };

        // Internal. Bilinearly interpolates tile-image elevations.
        ElevationModel.prototype.lookupPixels = function (x0, x1, y0, y1, level, retrieveTiles, result) {
            var levelNumber = level.levelNumber,
                tileWidth = level.tileWidth,
                tileHeight = level.tileHeight,
                row0 = Math.floor(y0 / tileHeight),
                row1 = Math.floor(y1 / tileHeight),
                col0 = Math.floor(x0 / tileWidth),
                col1 = Math.floor(x1 / tileWidth),
                r0c0, r0c1, r1c0, r1c1;

            if (row0 == row1 && row0 == this.cachedRow && col0 == col1 && col0 == this.cachedCol) {
                r0c0 = r0c1 = r1c0 = r1c1 = this.cachedImage; // use results from previous lookup
            } else if (row0 == row1 && col0 == col1) {
                r0c0 = this.lookupImage(levelNumber, row0, col0, retrieveTiles); // only need to lookup one image
                r0c1 = r1c0 = r1c1 = r0c0; // re-use the single image
                this.cachedRow = row0;
                this.cachedCol = col0;
                this.cachedImage = r0c0; // note the results for subsequent lookups
            } else {
                r0c0 = this.lookupImage(levelNumber, row0, col0, retrieveTiles);
                r0c1 = this.lookupImage(levelNumber, row0, col1, retrieveTiles);
                r1c0 = this.lookupImage(levelNumber, row1, col0, retrieveTiles);
                r1c1 = this.lookupImage(levelNumber, row1, col1, retrieveTiles);
            }

            if (r0c0 && r0c1 && r1c0 && r1c1) {
                result[0] = r0c0.pixel(x0 % tileWidth, y0 % tileHeight);
                result[1] = r0c1.pixel(x1 % tileWidth, y0 % tileHeight);
                result[2] = r1c0.pixel(x0 % tileWidth, y1 % tileHeight);
                result[3] = r1c1.pixel(x1 % tileWidth, y1 % tileHeight);
                return true;
            }

            return false;
        };

        // Internal. Intentionally not documented.
        ElevationModel.prototype.lookupImage = function (levelNumber, row, column, retrieveTiles) {
            var tile = this.tileForLevel(levelNumber, row, column),
                image = tile.image();

            // If the tile's elevations have expired, cause it to be re-retrieved. Note that the current,
            // expired elevations are still used until the updated ones arrive.
            if (image == null && retrieveTiles) {
                this.retrieveTileImage(tile);
            }

            return image;
        };

        // Intentionally not documented.
        ElevationModel.prototype.createTile = function (sector, level, row, column) {
            var imagePath = this.cachePath + "/" + level.levelNumber + "/" + row + "/" + row + "_" + column + ".bil";

            return new ElevationTile(sector, level, row, column, imagePath, this.imageCache);
        };

        // Intentionally not documented.
        ElevationModel.prototype.assembleTiles = function (level, sector, retrieveTiles) {
            this.currentTiles = [];

            // Intersect the requested sector with the elevation model's coverage area. This avoids attempting to assemble tiles
            // that are outside the coverage area.
            this.currentSector.copy(sector);
            this.currentSector.intersection(this.coverageSector);

            if (this.currentSector.isEmpty())
                return; // sector is outside the elevation model's coverage area

            var deltaLat = level.tileDelta.latitude,
                deltaLon = level.tileDelta.longitude,
                firstRow = Tile.computeRow(deltaLat, this.currentSector.minLatitude),
                lastRow = Tile.computeLastRow(deltaLat, this.currentSector.maxLatitude),
                firstCol = Tile.computeColumn(deltaLon, this.currentSector.minLongitude),
                lastCol = Tile.computeLastColumn(deltaLon, this.currentSector.maxLongitude);

            for (var row = firstRow; row <= lastRow; row++) {
                for (var col = firstCol; col <= lastCol; col++) {
                    this.addTileOrAncestor(level, row, col, retrieveTiles);
                }
            }
        };

        // Intentionally not documented.
        ElevationModel.prototype.addTileOrAncestor = function (level, row, column, retrieveTiles) {
            var tile = this.tileForLevel(level.levelNumber, row, column);

            if (this.isTileImageInMemory(tile)) {
                this.addToCurrentTiles(tile);
            } else {
                if (retrieveTiles) {
                    this.retrieveTileImage(tile);
                }

                if (level.isFirstLevel()) {
                    this.currentTiles.push(tile); // no ancestor tile to add
                } else {
                    this.addAncestor(level, row, column, retrieveTiles);
                }
            }
        };

        // Intentionally not documented.
        ElevationModel.prototype.addAncestor = function (level, row, column, retrieveTiles) {
            var tile = null,
                r = Math.floor(row / 2),
                c = Math.floor(column / 2);

            for (var i = level.levelNumber - 1; i >= 0; i--) {
                tile = this.tileForLevel(i, r, c);
                if (this.isTileImageInMemory(tile)) {
                    this.addToCurrentTiles(tile);
                    return;
                }

                r = Math.floor(r / 2);
                c = Math.floor(c / 2);
            }

            // No ancestor tiles have an in-memory image. Retrieve the ancestor tile corresponding for the first level, and
            // add it. We add the necessary tiles to provide coverage over the requested sector in order to accurately return
            // whether or not this elevation model has data for the entire sector.
            this.addToCurrentTiles(tile);

            if (retrieveTiles) {
                this.retrieveTileImage(tile);
            }
        };

        // Intentionally not documented.
        ElevationModel.prototype.addToCurrentTiles = function (tile) {
            this.currentTiles.push(tile);
        };

        // Intentionally not documented.
        ElevationModel.prototype.tileForLevel = function (levelNumber, row, column) {
            var tileKey = levelNumber + "." + row + "." + column,
                tile = this.tileCache.entryForKey(tileKey);

            if (tile) {
                return tile;
            }

            var level = this.levels.level(levelNumber),
                sector = Tile.computeSector(level, row, column);

            tile = this.createTile(sector, level, row, column);
            this.tileCache.putEntry(tileKey, tile, tile.size());

            return tile;
        };

        // Intentionally not documented.
        ElevationModel.prototype.isTileImageInMemory = function (tile) {
            return this.imageCache.containsKey(tile.imagePath);
        };

        // Intentionally not documented.
        ElevationModel.prototype.resourceUrlForTile = function (tile) {
            return this.urlBuilder.urlForTile(tile, this.retrievalImageFormat);
        };

        // Intentionally not documented.
        ElevationModel.prototype.retrieveTileImage = function (tile) {
            if (this.currentRetrievals.indexOf(tile.imagePath) < 0) {
                var url = this.resourceUrlForTile(tile, this.retrievalImageFormat),
                    xhr = new XMLHttpRequest(),
                    elevationModel = this;

                if (!url)
                    return;

                xhr.open("GET", url, true);
                xhr.responseType = 'arraybuffer';
                xhr.onreadystatechange = function () {
                    if (xhr.readyState === 4) {
                        elevationModel.removeFromCurrentRetrievals(tile.imagePath);

                        var contentType = xhr.getResponseHeader("content-type");

                        if (xhr.status === 200) {
                            if (contentType === elevationModel.retrievalImageFormat
                                || contentType === "text/plain"
                                || contentType === "application/octet-stream") {
                                Logger.log(Logger.LEVEL_INFO, "Elevations retrieval succeeded: " + url);
                                elevationModel.loadElevationImage(tile, xhr);
                                elevationModel.absentResourceList.unmarkResourceAbsent(tile.imagePath);

                                // Send an event to request a redraw.
                                var e = document.createEvent('Event');
                                e.initEvent(WorldWind.REDRAW_EVENT_TYPE, true, true);
                                window.dispatchEvent(e);
                            } else if (contentType === "text/xml") {
                                elevationModel.absentResourceList.markResourceAbsent(tile.imagePath);
                                Logger.log(Logger.LEVEL_WARNING,
                                    "Elevations retrieval failed (" + xhr.statusText + "): " + url + ".\n "
                                    + String.fromCharCode.apply(null, new Uint8Array(xhr.response)));
                            } else {
                                elevationModel.absentResourceList.markResourceAbsent(tile.imagePath);
                                Logger.log(Logger.LEVEL_WARNING,
                                    "Elevations retrieval failed: " + url + ". " + "Unexpected content type "
                                    + contentType);
                            }
                        } else {
                            elevationModel.absentResourceList.markResourceAbsent(tile.imagePath);
                            Logger.log(Logger.LEVEL_WARNING,
                                "Elevations retrieval failed (" + xhr.statusText + "): " + url);
                        }
                    }
                };

                xhr.onerror = function () {
                    elevationModel.removeFromCurrentRetrievals(tile.imagePath);
                    elevationModel.absentResourceList.markResourceAbsent(tile.imagePath);
                    Logger.log(Logger.LEVEL_WARNING, "Elevations retrieval failed: " + url);
                };

                xhr.ontimeout = function () {
                    elevationModel.removeFromCurrentRetrievals(tile.imagePath);
                    elevationModel.absentResourceList.markResourceAbsent(tile.imagePath);
                    Logger.log(Logger.LEVEL_WARNING, "Elevations retrieval timed out: " + url);
                };

                xhr.send(null);

                this.currentRetrievals.push(tile.imagePath);
            }
        };

        // Intentionally not documented.
        ElevationModel.prototype.removeFromCurrentRetrievals = function (imagePath) {
            var index = this.currentRetrievals.indexOf(imagePath);
            if (index > -1) {
                this.currentRetrievals.splice(index, 1);
            }
        };

        // Intentionally not documented.
        ElevationModel.prototype.loadElevationImage = function (tile, xhr) {
            var elevationImage = new ElevationImage(tile.imagePath, tile.sector, tile.tileWidth, tile.tileHeight);

            if (this.retrievalImageFormat == "application/bil16") {
                elevationImage.imageData = new Int16Array(xhr.response);
                elevationImage.size = elevationImage.imageData.length * 2;
            } else if (this.retrievalImageFormat == "application/bil32") {
                elevationImage.imageData = new Float32Array(xhr.response);
                elevationImage.size = elevationImage.imageData.length * 4;
            }

            if (elevationImage.imageData) {
                elevationImage.findMinAndMaxElevation();
                this.imageCache.putEntry(tile.imagePath, elevationImage, elevationImage.size);
                this.timestamp = Date.now();
            }
        };

        return ElevationModel;

    });