Source: globe/ElevationModel.js

/*
 * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
 * National Aeronautics and Space Administration. All rights reserved.
 *
 * The NASAWorldWind/WebWorldWind platform is 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(['../error/ArgumentError',
        '../geom/Angle',
        '../geom/Location',
        '../util/Logger'],
    function (ArgumentError,
              Angle,
              Location,
              Logger) {
        "use strict";

        /**
         * Constructs an elevation model.
         * @alias ElevationModel
         * @constructor
         * @classdesc Represents the elevations for an area, often but not necessarily the whole globe.
         */
        var ElevationModel = function () {

            /**
             * Internal use only
             * The unique ID of this model.
             * @type {Array}
             * @ignore
             */
            this.id = 0;

            /**
             * 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 = "";

            /**
             * The list of all elevation coverages usable by this model.
             * @type {Array}
             */
            this.coverages = [];

            this.scratchLocation = new Location(0, 0);

            this.computeStateKey();

        };

        Object.defineProperties(ElevationModel.prototype, {
            /**
             * Indicates the last time the coverages changed, in milliseconds since midnight Jan 1, 1970.
             * @type {Number}
             * @readonly
             */
            timestamp: {
                get: function () {
                    var maxTimestamp = 0;

                    var i, len;
                    for (i = 0, len = this.coverages.length; i < len; i++) {
                        var coverage = this.coverages[i];
                        if (maxTimestamp < coverage.timestamp) {
                            maxTimestamp = coverage.timestamp;
                        }
                    }

                    return maxTimestamp;
                }
            },

            /**
             * This model's minimum elevation in meters across all enabled coverages.
             * @type {Number}
             * @readonly
             */
            minElevation: {
                get: function () {
                    var minElevation = Number.MAX_VALUE;

                    for (var i = 0, len = this.coverages.length; i < len; i++) {
                        var coverage = this.coverages[i];
                        if (coverage.enabled && coverage.minElevation < minElevation) {
                            minElevation = coverage.minElevation;
                        }
                    }

                    return (minElevation !== Number.MAX_VALUE) ? minElevation : 0; // no coverages or all coverages disabled
                }
            },

            /**
             * This model's maximum elevation in meters across all enabled coverages.
             * @type {Number}
             * @readonly
             */
            maxElevation: {
                get: function () {
                    var maxElevation = -Number.MAX_VALUE;

                    for (var i = 0, len = this.coverages.length; i < len; i++) {
                        var coverage = this.coverages[i];
                        if (coverage.enabled && coverage.maxElevation > maxElevation) {
                            maxElevation = coverage.maxElevation;
                        }
                    }

                    return (maxElevation !== -Number.MAX_VALUE) ? maxElevation : 0; // no coverages or all coverages disabled
                }
            }
        });

        /**
         * Internal use only
         * Used to assign unique IDs to elevation models for use in their state key.
         * @type {Number}
         * @ignore
         */
        ElevationModel.idPool = 0;

        /**
         * Internal use only
         * Sets the state key to a new unique value.
         * @ignore
         */
        ElevationModel.prototype.computeStateKey = function () {
            this.id = ++ElevationModel.idPool;
            this.stateKey = "elevationModel " + this.id.toString() + " ";
        };

        /**
         * Internal use only
         * The comparison function used for sorting elevation coverages.
         * @ignore
         */
        ElevationModel.prototype.coverageComparator = function (coverage1, coverage2) {
            var res1 = coverage1.resolution;
            var res2 = coverage2.resolution;
            // sort from lowest resolution to highest
            return res1 > res2 ? -1 : res1 === res2 ? 0 : 1;
        };

        /**
         * Internal use only
         * Perform common actions required when the list of available coverages changes.
         * @ignore
         */
        ElevationModel.prototype.performCoverageListChangedActions = function () {
            if (this.coverages.length > 1) {
                this.coverages.sort(this.coverageComparator);
            }

            this.computeStateKey();
        };

        /**
         * Adds an elevation coverage to this elevation model and sorts the list. Duplicate coverages will be ignored.
         *
         * @param coverage The elevation coverage to add.
         * @return {Boolean} true if the ElevationCoverage as added; false if the coverage was a duplicate.
         * @throws ArgumentError if the specified elevation coverage is null.
         */
        ElevationModel.prototype.addCoverage = function (coverage) {
            if (!coverage) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "addCoverage", "missingCoverage"));
            }

            if (!this.containsCoverage(coverage)) {
                this.coverages.push(coverage);
                this.performCoverageListChangedActions();
                return true;
            }

            return false;
        };

        /**
         * Removes all elevation coverages from this elevation model.
         */
        ElevationModel.prototype.removeAllCoverages = function () {
            if (this.coverages.length > 0) {
                this.coverages = [];
                this.performCoverageListChangedActions();
            }
        };

        /**
         * Removes a specific elevation coverage from this elevation model.
         *
         * @param coverage The elevation model to remove.
         *
         * @throws ArgumentError if the specified elevation coverage is null.
         */
        ElevationModel.prototype.removeCoverage = function (coverage) {
            if (!coverage) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "removeCoverage", "missingCoverage"));
            }

            var index = this.coverages.indexOf(coverage);
            if (index >= 0) {
                this.coverages.splice(index, 1);
                this.performCoverageListChangedActions();
            }
        };

        /**
         * Returns true if this ElevationModel contains the specified ElevationCoverage, and false otherwise.
         *
         * @param coverage the ElevationCoverage to test.
         * @return {Boolean} true if the ElevationCoverage is in this ElevationModel; false otherwise.
         * @throws ArgumentError if the ElevationCoverage is null.
         */
        ElevationModel.prototype.containsCoverage = function (coverage) {
            if (!coverage) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "containsCoverage", "missingCoverage"));
            }
            var index = this.coverages.indexOf(coverage);
            return index >= 0;
        };

        /**
         * 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. If no coverage
         * can satisfy the request, a min and max of zero is returned.
         * @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"));
            }

            // Initialize the min and max elevations to the largest and smallest numbers, respectively. This has the
            // effect of moving the extremes with each subsequent coverage as needed, without unintentionally capturing
            // zero elevation. If we initialized this array with zeros the result would always contain zero, even when
            // elevations in the sector are all above or below zero. This is critical for tile bounding boxes.

            var result = [Number.MAX_VALUE, -Number.MAX_VALUE];

            for (var i = this.coverages.length - 1; i >= 0; i--) {
                var coverage = this.coverages[i];
                if (coverage.enabled && coverage.coverageSector.intersects(sector)) {
                    if (coverage.minAndMaxElevationsForSector(sector, result)) {
                        break; // coverage completely fills the sector, ignore the remaining coverages
                    }
                }
            }

            return (result[0] !== Number.MAX_VALUE) ? result : [0, 0]; // no coverages, all coverages disabled, or no coverages intersect the sector
        };

        /**
         * 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 model.
         */
        ElevationModel.prototype.elevationAtLocation = function (latitude, longitude) {
            var i, n = this.coverages.length;
            for (i = n - 1; i >= 0; i--) {
                var coverage = this.coverages[i];
                if (coverage.enabled && coverage.coverageSector.containsLocation(latitude, longitude)) {
                    var elevation = coverage.elevationAtLocation(latitude, longitude);
                    if (elevation !== null) {
                        return elevation;
                    }
                }
            }

            return 0;
        };

        /**
         * Internal use only
         * Returns the index of the coverage most closely matching the supplied resolution and overlapping the supplied
         * sector or point area of interest. At least one area of interest parameter must be non-null.
         * @param {Sector} sector An optional sector area of interest. Setting this parameter to null will cause it to be ignored.
         * @param {Location} location An optional point area of interest. Setting this parameter to null will cause it to be ignored.
         * @param {Number} targetResolution The desired elevation resolution, in degrees. (To compute degrees from
         * meters, divide the number of meters by the globe's radius to obtain radians and convert the result to degrees.)
         * @returns {Number} The index of the coverage most closely matching the requested resolution.
         * @ignore
         */
        ElevationModel.prototype.preferredCoverageIndex = function (sector, location, targetResolution) {

            var i,
                n = this.coverages.length,
                minResDiff = Number.MAX_VALUE,
                minDiffIdx = -1;

            for (i = 0; i < n; i++) {
                var coverage = this.coverages[i],
                    validCoverage = coverage.enabled && ((sector !== null && coverage.coverageSector.intersects(sector)) ||
                        (location !== null && coverage.coverageSector.containsLocation(location.latitude, location.longitude)));
                if (validCoverage) {
                    var resDiff = Math.abs(coverage.resolution - targetResolution);
                    if (resDiff > minResDiff) {
                        return minDiffIdx;
                    }
                    minResDiff = resDiff;
                    minDiffIdx = i;
                }
            }

            return minDiffIdx;
        };

        /**
         * Returns the best coverage available for a particular resolution,
         * @param {Number} latitude The location's latitude in degrees.
         * @param {Number} longitude The location's longitude in degrees.
         * @param {Number} targetResolution The desired elevation resolution, in degrees. (To compute degrees from
         * meters, divide the number of meters by the globe's radius to obtain radians and convert the result to degrees.)
         * @returns {ElevationCoverage} The coverage most closely matching the requested resolution. Returns null if no coverage is available at this
         * location.
         * @throws {ArgumentError} If the specified resolution is not positive.
         */
        ElevationModel.prototype.bestCoverageAtLocation = function (latitude, longitude, targetResolution) {

            if (!targetResolution || targetResolution < 0) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "bestCoverageAtLocation", "invalidResolution"));
            }

            this.scratchLocation.set(latitude, longitude);
            var preferredIndex = this.preferredCoverageIndex(null, this.scratchLocation, targetResolution);
            if (preferredIndex >= 0) {
                return this.coverages[preferredIndex];
            }

            return null;
        };

        /**
         * 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 degrees. (To compute degrees from
         * meters, divide the number of meters by the globe's radius to obtain radians and convert the result to degrees.)
         * @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, targetResolution, 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", "elevationsForGrid", "missingSector"));
            }

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

            if (!targetResolution) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "elevationsForGrid", "missingTargetResolution"));
            }

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

            result.fill(NaN);
            var resolution = Number.MAX_VALUE,
                resultFilled = false,
                preferredIndex = this.preferredCoverageIndex(sector, null, targetResolution);

            if (preferredIndex >= 0) {
                for (var i = preferredIndex; !resultFilled && i >= 0; i--) {
                    var coverage = this.coverages[i];
                    if (coverage.enabled && coverage.coverageSector.intersects(sector)) {
                        resultFilled = coverage.elevationsForGrid(sector, numLat, numLon, result);
                        if (resultFilled) {
                            resolution = coverage.resolution;
                        }
                    }
                }
            }

            if (!resultFilled) {
                var n = result.length;
                for (i = 0; i < n; i++) {
                    if (isNaN(result[i])) {
                        result[i] = 0;
                    }
                }
            }

            return resolution;
        };

        return ElevationModel;
    });