Source: util/measure/MeasurerUtils.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.
 */

define([
        '../../geom/Location',
        '../../geom/Position'
    ],
    function (Location,
              Position) {
        'use strict';

        /**
         * Provides utilities for Measurements.
         * @exports MeasurerUtils
         */
        var MeasurerUtils = {

            /**
             * Subdivide a list of positions so that no segment is longer then the provided maxLength.
             * <p>If needed, new intermediate positions will be created along lines that follow the given pathType one
             * of WorldWind.LINEAR, WorldWind.RHUMB_LINE or WorldWind.GREAT_CIRCLE.
             * All position elevations will be either at the terrain surface if followTerrain is true, or interpolated
             * according to the original elevations.</p>
             *
             * @param {Globe} globe
             * @param {Position[]} positions
             * @param {Boolean} followTerrain
             * @param {String} pathType One of WorldWind.LINEAR, WorldWind.RHUMB_LINE or WorldWind.GREAT_CIRCLE
             * @param {Number} maxLength The maximum length for one segment
             *
             * @return {Position[]} a list of positions with no segment longer then maxLength and elevations following
             * terrain or not.
             */
            subdividePositions: function (globe, positions, followTerrain, pathType, maxLength) {
                var subdividedPositions = [];
                var loc = new Location(0, 0);
                var destLatLon = new Location(0, 0);
                var pos1 = positions[0];
                var elevation;

                this.addPosition(globe, subdividedPositions, pos1, followTerrain);

                for (var i = 1; i < positions.length; i++) {
                    var pos2 = positions[i];
                    var arcLengthRadians = Location.greatCircleDistance(pos1, pos2);
                    loc = Location.interpolateAlongPath(pathType, 0.5, pos1, pos2, loc);
                    var arcLength = arcLengthRadians * globe.radiusAt(loc.latitude, loc.longitude);
                    if (arcLength > maxLength) {
                        // if necessary subdivide segment at regular intervals smaller then maxLength
                        var segmentAzimuth = null;
                        var segmentDistance = null;
                        var steps = Math.ceil(arcLength / maxLength); // number of intervals - at least two
                        for (var j = 1; j < steps; j++) {
                            var s = j / steps;
                            if (pathType === WorldWind.LINEAR) {
                                destLatLon = Location.interpolateLinear(s, pos1, pos2, destLatLon);
                            }
                            else if (pathType === WorldWind.RHUMB_LINE) {
                                if (segmentAzimuth == null) {
                                    segmentAzimuth = Location.rhumbAzimuth(pos1, pos2);
                                    segmentDistance = Location.rhumbDistance(pos1, pos2);
                                }
                                destLatLon = Location.rhumbLocation(pos1, segmentAzimuth, s * segmentDistance,
                                    destLatLon);
                            }
                            else {
                                //GREAT_CIRCLE
                                if (segmentAzimuth == null) {
                                    segmentAzimuth = Location.greatCircleAzimuth(pos1, pos2); //degrees
                                    segmentDistance = Location.greatCircleDistance(pos1, pos2); //radians
                                }
                                //Location, degrees, radians, Location
                                destLatLon = Location.greatCircleLocation(pos1, segmentAzimuth, s * segmentDistance,
                                    destLatLon);
                            }

                            // Set elevation
                            if (followTerrain) {
                                elevation = globe.elevationAtLocation(destLatLon.latitude, destLatLon.longitude);
                            }
                            else {
                                elevation = pos1.altitude * (1 - s) + pos2.altitude * s;
                            }

                            subdividedPositions.push(new Position(destLatLon.latitude, destLatLon.longitude, elevation));
                        }
                    }

                    // Finally add the segment end position
                    this.addPosition(globe, subdividedPositions, pos2, followTerrain);

                    // Prepare for next segment
                    pos1 = pos2;
                }

                return subdividedPositions;
            },

            /**
             * Adds a position to a list of positions.
             * If the path is following the terrain the elevation is also computed.
             *
             * @param {Globe} globe
             * @param {Position[]} positions The list of positions to add to
             * @param {Position} position The position to add to the list
             * @param {Boolean} followTerrain
             *
             * @return {Position[]} The list of positions
             */
            addPosition: function (globe, positions, position, followTerrain) {
                var elevation = position.altitude;
                if (followTerrain) {
                    elevation = globe.elevationAtLocation(position.latitude, position.longitude);
                }
                positions.push(new Position(position.latitude, position.longitude, elevation));
                return positions;
            },

            /**
             * Determines whether a location is located inside a given polygon.
             *
             * @param {Location} location
             * @param {Location[]}locations The list of positions describing the polygon.
             * Last one should be the same as the first one.
             *
             * @return {Boolean} true if the location is inside the polygon.
             */
            isLocationInside: function (location, locations) {
                var result = false;
                var p1 = locations[0];
                for (var i = 1, len = locations.length; i < len; i++) {
                    var p2 = locations[i];
                    if (((p2.latitude <= location.latitude && location.latitude < p1.latitude) ||
                        (p1.latitude <= location.latitude && location.latitude < p2.latitude)) &&
                        (location.longitude < (p1.longitude - p2.longitude) * (location.latitude - p2.latitude) /
                        (p1.latitude - p2.latitude) + p2.longitude)) {
                        result = !result;
                    }
                    p1 = p2;
                }
                return result;
            },

            /**
             * Computes the angle between two Vec3 in radians.
             *
             * @param {Vec3} v1
             * @param {Vec3} v2
             *
             * @return {Number} The ange in radians
             */
            angleBetweenVectors: function (v1, v2) {
                var dot = v1.dot(v2);
                // Compute the sum of magnitudes.
                var length = v1.magnitude() * v2.magnitude();
                // Normalize the dot product, if necessary.
                if (!(length === 0) && (length !== 1.0)) {
                    dot /= length;
                }

                // The normalized dot product should be in the range [-1, 1]. Otherwise the result is an error from
                // floating point roundoff. So if dot is less than -1 or greater than +1, we treat it as -1 and +1
                // respectively.
                if (dot < -1.0) {
                    dot = -1.0;
                }
                else if (dot > 1.0) {
                    dot = 1.0;
                }

                // Angle is arc-cosine of normalized dot product.
                return Math.acos(dot);
            }

        };

        return MeasurerUtils;

    });