diff --git a/CHANGES.md b/CHANGES.md index 21639ecac8ab..0e481e64659c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ Change Log * Added `classificationType` property to `PolylineGraphics` and `GroundPolylinePrimitive` which specifies whether a polyline clamped to ground should be clamped to terrain, 3D Tiles, or both. [#7437](https://github.com/AnalyticalGraphicsInc/cesium/pull/7437) * Added the ability to specify the width of the intersection volume for `Scene.sampleHeight`, `Scene.clampToHeight`, `Scene.sampleHeightMostDetailed`, and `Scene.clampToHeightMostDetailed`. [#7287](https://github.com/AnalyticalGraphicsInc/cesium/pull/7287) * Added a [new Sandcastle example](https://cesiumjs.org/Cesium/Build/Apps/Sandcastle/?src=Time%20Dynamic%20Wheels.html) on using `nodeTransformations` to rotate a model's wheels based on its velocity. [#7361](https://github.com/AnalyticalGraphicsInc/cesium/pull/7361) +* Added `EllipsoidRhumbLine` class as a rhumb line counterpart to `EllipsoidGeodesic`. [#7484](https://github.com/AnalyticalGraphicsInc/cesium/pull/7484) ##### Fixes :wrench: * Fixed 3D Tiles performance regression. [#7482](https://github.com/AnalyticalGraphicsInc/cesium/pull/7482) diff --git a/Source/Core/EllipsoidRhumbLine.js b/Source/Core/EllipsoidRhumbLine.js new file mode 100644 index 000000000000..9c1273f90b31 --- /dev/null +++ b/Source/Core/EllipsoidRhumbLine.js @@ -0,0 +1,507 @@ +define([ + './Cartesian3', + './Cartographic', + './Check', + './defaultValue', + './defined', + './defineProperties', + './DeveloperError', + './Ellipsoid', + './Math' + ], function( + Cartesian3, + Cartographic, + Check, + defaultValue, + defined, + defineProperties, + DeveloperError, + Ellipsoid, + CesiumMath) { + 'use strict'; + + function calculateM(ellipticity, major, latitude) { + if (ellipticity === 0.0) { // sphere + return major * latitude; + } + + var e2 = ellipticity * ellipticity; + var e4 = e2 * e2; + var e6 = e4 * e2; + var e8 = e6 * e2; + var e10 = e8 * e2; + var e12 = e10 * e2; + var phi = latitude; + var sin2Phi = Math.sin(2 * phi); + var sin4Phi = Math.sin(4 * phi); + var sin6Phi = Math.sin(6 * phi); + var sin8Phi = Math.sin(8 * phi); + var sin10Phi = Math.sin(10 * phi); + var sin12Phi = Math.sin(12 * phi); + + return major * ((1 - e2 / 4 - 3 * e4 / 64 - 5 * e6 / 256 - 175 * e8 / 16384 - 441 * e10 / 65536 - 4851 * e12 / 1048576) * phi + - (3 * e2 / 8 + 3 * e4 / 32 + 45 * e6 / 1024 + 105 * e8 / 4096 + 2205 * e10 / 131072 + 6237 * e12 / 524288) * sin2Phi + + (15 * e4 / 256 + 45 * e6 / 1024 + 525 * e8 / 16384 + 1575 * e10 / 65536 + 155925 * e12 / 8388608) * sin4Phi + - (35 * e6 / 3072 + 175 * e8 / 12288 + 3675 * e10 / 262144 + 13475 * e12 / 1048576) * sin6Phi + + (315 * e8 / 131072 + 2205 * e10 / 524288 + 43659 * e12 / 8388608) * sin8Phi + - (693 * e10 / 1310720 + 6237 * e12 / 5242880) * sin10Phi + + 1001 * e12 / 8388608 * sin12Phi); + } + + function calculateInverseM(M, ellipticity, major) { + var d = M / major; + + if (ellipticity === 0.0) { // sphere + return d; + } + + var d2 = d * d; + var d3 = d2 * d; + var d4 = d3 * d; + var e = ellipticity; + var e2 = e * e; + var e4 = e2 * e2; + var e6 = e4 * e2; + var e8 = e6 * e2; + var e10 = e8 * e2; + var e12 = e10 * e2; + var sin2D = Math.sin(2 * d); + var cos2D = Math.cos(2 * d); + var sin4D = Math.sin(4 * d); + var cos4D = Math.cos(4 * d); + var sin6D = Math.sin(6 * d); + var cos6D = Math.cos(6 * d); + var sin8D = Math.sin(8 * d); + var cos8D = Math.cos(8 * d); + var sin10D = Math.sin(10 * d); + var cos10D = Math.cos(10 * d); + var sin12D = Math.sin(12 * d); + + return d + d * e2 / 4 + 7 * d * e4 / 64 + 15 * d * e6 / 256 + 579 * d * e8 / 16384 + 1515 * d * e10 / 65536 + 16837 * d * e12 / 1048576 + + (3 * d * e4 / 16 + 45 * d * e6 / 256 - d * (32 * d2 - 561) * e8 / 4096 - d * (232 * d2 - 1677) * e10 / 16384 + d * (399985 - 90560 * d2 + 512 * d4) * e12 / 5242880) * cos2D + + (21 * d * e6 / 256 + 483 * d * e8 / 4096 - d * (224 * d2 - 1969) * e10 / 16384 - d * (33152 * d2 - 112599) * e12 / 1048576) * cos4D + + (151 * d * e8 / 4096 + 4681 * d * e10 / 65536 + 1479 * d * e12 / 16384 - 453 * d3 * e12 / 32768) * cos6D + + (1097 * d * e10 / 65536 + 42783 * d * e12 / 1048576) * cos8D + + 8011 * d * e12 / 1048576 * cos10D + + (3 * e2 / 8 + 3 * e4 / 16 + 213 * e6 / 2048 - 3 * d2 * e6 / 64 + 255 * e8 / 4096 - 33 * d2 * e8 / 512 + 20861 * e10 / 524288 - 33 * d2 * e10 / 512 + d4 * e10 / 1024 + 28273 * e12 / 1048576 - 471 * d2 * e12 / 8192 + 9 * d4 * e12 / 4096) * sin2D + + (21 * e4 / 256 + 21 * e6 / 256 + 533 * e8 / 8192 - 21 * d2 * e8 / 512 + 197 * e10 / 4096 - 315 * d2 * e10 / 4096 + 584039 * e12 / 16777216 - 12517 * d2 * e12 / 131072 + 7 * d4 * e12 / 2048) * sin4D + + (151 * e6 / 6144 + 151 * e8 / 4096 + 5019 * e10 / 131072 - 453 * d2 * e10 / 16384 + 26965 * e12 / 786432 - 8607 * d2 * e12 / 131072) * sin6D + + (1097 * e8 / 131072 + 1097 * e10 / 65536 + 225797 * e12 / 10485760 - 1097 * d2 * e12 / 65536) * sin8D + + (8011 * e10 / 2621440 + 8011 * e12 / 1048576) * sin10D + + 293393 * e12 / 251658240 * sin12D; + } + + function calculateSigma(ellipticity, latitude) { + if (ellipticity === 0.0) { // sphere + return Math.log(Math.tan(0.5 * (CesiumMath.PI_OVER_TWO + latitude))); + } + + var eSinL = ellipticity * Math.sin(latitude); + return Math.log(Math.tan(0.5 * (CesiumMath.PI_OVER_TWO + latitude))) - (ellipticity / 2.0 * Math.log((1 + eSinL) / (1 - eSinL))); + } + + function calculateHeading(ellipsoidRhumbLine, firstLongitude, firstLatitude, secondLongitude, secondLatitude) { + var sigma1 = calculateSigma(ellipsoidRhumbLine._ellipticity, firstLatitude); + var sigma2 = calculateSigma(ellipsoidRhumbLine._ellipticity, secondLatitude); + return Math.atan2(CesiumMath.negativePiToPi(secondLongitude - firstLongitude), sigma2 - sigma1); + } + + function calculateArcLength(ellipsoidRhumbLine, major, minor, firstLongitude, firstLatitude, secondLongitude, secondLatitude) { + var heading = ellipsoidRhumbLine._heading; + var deltaLongitude = secondLongitude - firstLongitude; + + var distance = 0.0; + + //Check to see if the rhumb line has constant latitude + //This equation will diverge if heading gets close to 90 degrees + if (CesiumMath.equalsEpsilon(Math.abs(heading), CesiumMath.PI_OVER_TWO, CesiumMath.EPSILON8)) { //If heading is close to 90 degrees + if (major === minor) { + distance = major * Math.cos(firstLatitude) * CesiumMath.negativePiToPi(deltaLongitude); + } else { + var sinPhi = Math.sin(firstLatitude); + distance = major * Math.cos(firstLatitude) * CesiumMath.negativePiToPi(deltaLongitude) / Math.sqrt(1 - ellipsoidRhumbLine._ellipticitySquared * sinPhi * sinPhi); + } + } else { + var M1 = calculateM(ellipsoidRhumbLine._ellipticity, major, firstLatitude); + var M2 = calculateM(ellipsoidRhumbLine._ellipticity, major, secondLatitude); + + distance = (M2 - M1) / Math.cos(heading); + } + return Math.abs(distance); + } + + var scratchCart1 = new Cartesian3(); + var scratchCart2 = new Cartesian3(); + + function computeProperties(ellipsoidRhumbLine, start, end, ellipsoid) { + var firstCartesian = Cartesian3.normalize(ellipsoid.cartographicToCartesian(start, scratchCart2), scratchCart1); + var lastCartesian = Cartesian3.normalize(ellipsoid.cartographicToCartesian(end, scratchCart2), scratchCart2); + + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.greaterThanOrEquals('value', Math.abs(Math.abs(Cartesian3.angleBetween(firstCartesian, lastCartesian)) - Math.PI), 0.0125); + //>>includeEnd('debug'); + + var major = ellipsoid.maximumRadius; + var minor = ellipsoid.minimumRadius; + var majorSquared = major * major; + var minorSquared = minor * minor; + ellipsoidRhumbLine._ellipticitySquared = (majorSquared - minorSquared) / majorSquared; + ellipsoidRhumbLine._ellipticity = Math.sqrt(ellipsoidRhumbLine._ellipticitySquared); + + ellipsoidRhumbLine._start = Cartographic.clone(start, ellipsoidRhumbLine._start); + ellipsoidRhumbLine._start.height = 0; + + ellipsoidRhumbLine._end = Cartographic.clone(end, ellipsoidRhumbLine._end); + ellipsoidRhumbLine._end.height = 0; + + ellipsoidRhumbLine._heading = calculateHeading(ellipsoidRhumbLine, start.longitude, start.latitude, end.longitude, end.latitude); + ellipsoidRhumbLine._distance = calculateArcLength(ellipsoidRhumbLine, ellipsoid.maximumRadius, ellipsoid.minimumRadius, + start.longitude, start.latitude, end.longitude, end.latitude); + } + + function interpolateUsingSurfaceDistance(start, heading, distance, major, ellipticity, result) + { + var ellipticitySquared = ellipticity * ellipticity; + + var longitude; + var latitude; + var deltaLongitude; + + //Check to see if the rhumb line has constant latitude + //This won't converge if heading is close to 90 degrees + if (Math.abs(CesiumMath.PI_OVER_TWO - Math.abs(heading)) > CesiumMath.EPSILON8) { + //Calculate latitude of the second point + var M1 = calculateM(ellipticity, major, start.latitude); + var deltaM = distance * Math.cos(heading); + var M2 = M1 + deltaM; + latitude = calculateInverseM(M2, ellipticity, major); + + //Now find the longitude of the second point + var sigma1 = calculateSigma(ellipticity, start.latitude); + var sigma2 = calculateSigma(ellipticity, latitude); + deltaLongitude = Math.tan(heading) * (sigma2 - sigma1); + longitude = CesiumMath.negativePiToPi(start.longitude + deltaLongitude); + } else { //If heading is close to 90 degrees + latitude = start.latitude; + var localRad; + + if (ellipticity === 0.0) { // sphere + localRad = major * Math.cos(start.latitude); + } else { + var sinPhi = Math.sin(start.latitude); + localRad = major * Math.cos(start.latitude) / Math.sqrt(1 - ellipticitySquared * sinPhi * sinPhi); + } + + deltaLongitude = distance / localRad; + if (heading > 0.0) { + longitude = CesiumMath.negativePiToPi(start.longitude + deltaLongitude); + } else { + longitude = CesiumMath.negativePiToPi(start.longitude - deltaLongitude); + } + } + + if (defined(result)) { + result.longitude = longitude; + result.latitude = latitude; + result.height = 0; + + return result; + } + + return new Cartographic(longitude, latitude, 0); + } + + /** + * Initializes a rhumb line on the ellipsoid connecting the two provided planetodetic points. + * + * @alias EllipsoidRhumbLine + * @constructor + * + * @param {Cartographic} [start] The initial planetodetic point on the path. + * @param {Cartographic} [end] The final planetodetic point on the path. + * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid on which the rhumb line lies. + * + * @exception {DeveloperError} angle between start and end must be at least 0.0125 radians. + */ + function EllipsoidRhumbLine(start, end, ellipsoid) { + var e = defaultValue(ellipsoid, Ellipsoid.WGS84); + this._ellipsoid = e; + this._start = new Cartographic(); + this._end = new Cartographic(); + + this._heading = undefined; + this._distance = undefined; + this._ellipticity = undefined; + this._ellipticitySquared = undefined; + + if (defined(start) && defined(end)) { + computeProperties(this, start, end, e); + } + } + + defineProperties(EllipsoidRhumbLine.prototype, { + /** + * Gets the ellipsoid. + * @memberof EllipsoidRhumbLine.prototype + * @type {Ellipsoid} + * @readonly + */ + ellipsoid : { + get : function() { + return this._ellipsoid; + } + }, + + /** + * Gets the surface distance between the start and end point + * @memberof EllipsoidRhumbLine.prototype + * @type {Number} + * @readonly + */ + surfaceDistance : { + get : function() { + //>>includeStart('debug', pragmas.debug); + Check.defined('distance', this._distance); + //>>includeEnd('debug'); + + return this._distance; + } + }, + + /** + * Gets the initial planetodetic point on the path. + * @memberof EllipsoidRhumbLine.prototype + * @type {Cartographic} + * @readonly + */ + start : { + get : function() { + return this._start; + } + }, + + /** + * Gets the final planetodetic point on the path. + * @memberof EllipsoidRhumbLine.prototype + * @type {Cartographic} + * @readonly + */ + end : { + get : function() { + return this._end; + } + }, + + /** + * Gets the heading from the start point to the end point. + * @memberof EllipsoidRhumbLine.prototype + * @type {Number} + * @readonly + */ + heading : { + get : function() { + //>>includeStart('debug', pragmas.debug); + Check.defined('distance', this._distance); + //>>includeEnd('debug'); + + return this._heading; + } + } + }); + + /** + * Create a rhumb line using an initial position with a heading and distance. + * + * @param {Cartographic} start The initial planetodetic point on the path. + * @param {Number} heading The heading in radians. + * @param {Number} distance The rhumb line distance between the start and end point. + * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid on which the rhumb line lies. + * @param {EllipsoidRhumbLine} [result] The object in which to store the result. + * @returns {EllipsoidRhumbLine} The EllipsoidRhumbLine object. + */ + EllipsoidRhumbLine.fromStartHeadingDistance = function(start, heading, distance, ellipsoid, result) { + //>>includeStart('debug', pragmas.debug); + Check.defined('start', start); + Check.defined('heading', heading); + Check.defined('distance', distance); + Check.typeOf.number.greaterThan('distance', distance, 0.0); + //>>includeEnd('debug'); + + var e = defaultValue(ellipsoid, Ellipsoid.WGS84); + var major = e.maximumRadius; + var minor = e.minimumRadius; + var majorSquared = major * major; + var minorSquared = minor * minor; + var ellipticity = Math.sqrt((majorSquared - minorSquared) / majorSquared); + + heading = CesiumMath.negativePiToPi(heading); + var end = interpolateUsingSurfaceDistance(start, heading, distance, e.maximumRadius, ellipticity); + + if (!defined(result) || (defined(ellipsoid) && !ellipsoid.equals(result.ellipsoid))) { + return new EllipsoidRhumbLine(start, end, e); + } + + result.setEndPoints(start, end); + return result; + }; + + /** + * Sets the start and end points of the rhumb line. + * + * @param {Cartographic} start The initial planetodetic point on the path. + * @param {Cartographic} end The final planetodetic point on the path. + */ + EllipsoidRhumbLine.prototype.setEndPoints = function(start, end) { + //>>includeStart('debug', pragmas.debug); + Check.defined('start', start); + Check.defined('end', end); + //>>includeEnd('debug'); + + computeProperties(this, start, end, this._ellipsoid); + }; + + /** + * Provides the location of a point at the indicated portion along the rhumb line. + * + * @param {Number} fraction The portion of the distance between the initial and final points. + * @param {Cartographic} [result] The object in which to store the result. + * @returns {Cartographic} The location of the point along the rhumb line. + */ + EllipsoidRhumbLine.prototype.interpolateUsingFraction = function(fraction, result) { + return this.interpolateUsingSurfaceDistance(fraction * this._distance, result); + }; + + /** + * Provides the location of a point at the indicated distance along the rhumb line. + * + * @param {Number} distance The distance from the inital point to the point of interest along the rhumbLine. + * @param {Cartographic} [result] The object in which to store the result. + * @returns {Cartographic} The location of the point along the rhumb line. + * + * @exception {DeveloperError} start and end must be set before calling function interpolateUsingSurfaceDistance + */ + EllipsoidRhumbLine.prototype.interpolateUsingSurfaceDistance = function(distance, result) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('distance', distance); + if (!defined(this._distance) || this._distance === 0.0) { + throw new DeveloperError('EllipsoidRhumbLine must have distinct start and end set.'); + } + //>>includeEnd('debug'); + + return interpolateUsingSurfaceDistance(this._start, this._heading, distance, this._ellipsoid.maximumRadius, this._ellipticity, result); + }; + + /** + * Provides the location of a point at the indicated longitude along the rhumb line. + * If the longitude is outside the range of start and end points, the first intersection with the longitude from the start point in the direction of the heading is returned. This follows the spiral property of a rhumb line. + * + * @param {Number} intersectionLongitude The longitude, in radians, at which to find the intersection point from the starting point using the heading. + * @param {Cartographic} [result] The object in which to store the result. + * @returns {Cartographic} The location of the intersection point along the rhumb line, undefined if there is no intersection or infinite intersections. + * + * @exception {DeveloperError} start and end must be set before calling function findIntersectionWithLongitude. + */ + EllipsoidRhumbLine.prototype.findIntersectionWithLongitude = function(intersectionLongitude, result) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('intersectionLongitude', intersectionLongitude); + if (!defined(this._distance) || this._distance === 0.0) { + throw new DeveloperError('EllipsoidRhumbLine must have distinct start and end set.'); + } + //>>includeEnd('debug'); + + var ellipticity = this._ellipticity; + var heading = this._heading; + var absHeading = Math.abs(heading); + var start = this._start; + + intersectionLongitude = CesiumMath.negativePiToPi(intersectionLongitude); + + if (!defined(result)) { + result = new Cartographic(); + } + + // If heading is -PI/2 or PI/2, this is an E-W rhumb line + // If heading is 0 or PI, this is an N-S rhumb line + if (Math.abs(CesiumMath.PI_OVER_TWO - absHeading) <= CesiumMath.EPSILON8) { + result.longitude = intersectionLongitude; + result.latitude = start.latitude; + result.height = 0; + return result; + } else if (CesiumMath.equalsEpsilon(Math.abs(CesiumMath.PI_OVER_TWO - absHeading), CesiumMath.PI_OVER_TWO, CesiumMath.EPSILON8)) { + if (CesiumMath.equalsEpsilon(intersectionLongitude, start.longitude, CesiumMath.EPSILON12)) { + return undefined; + } + + result.longitude = intersectionLongitude; + result.latitude = CesiumMath.PI_OVER_TWO * Math.sign(CesiumMath.PI_OVER_TWO - heading); + result.height = 0; + return result; + } + + // Use iterative solver from Equation 9 from http://edwilliams.org/ellipsoid/ellipsoid.pdf + var phi1 = start.latitude; + var eSinPhi1 = ellipticity * Math.sin(phi1); + var leftComponent = Math.tan(0.5 * (CesiumMath.PI_OVER_TWO + phi1)) * Math.exp((intersectionLongitude - start.longitude) / Math.tan(heading)); + var denominator = (1 + eSinPhi1) / (1 - eSinPhi1); + + var newPhi = start.latitude; + var phi; + do { + phi = newPhi; + var eSinPhi = ellipticity * Math.sin(phi); + var numerator = (1 + eSinPhi) / (1 - eSinPhi); + newPhi = 2 * Math.atan(leftComponent * Math.pow(numerator / denominator, ellipticity / 2)) - CesiumMath.PI_OVER_TWO; + } while (!CesiumMath.equalsEpsilon(newPhi, phi, CesiumMath.EPSILON12)); + + result.longitude = intersectionLongitude; + result.latitude = phi; + result.height = 0; + return result; + }; + + /** + * Provides the location of a point at the indicated latitude along the rhumb line. + * If the latitude is outside the range of start and end points, the first intersection with the latitude from that start point in the direction of the heading is returned. This follows the spiral property of a rhumb line. + * + * @param {Number} intersectionLatitude The latitude, in radians, at which to find the intersection point from the starting point using the heading. + * @param {Cartographic} [result] The object in which to store the result. + * @returns {Cartographic} The location of the intersection point along the rhumb line, undefined if there is no intersection or infinite intersections. + * + * @exception {DeveloperError} start and end must be set before calling function findIntersectionWithLongitude. + */ + EllipsoidRhumbLine.prototype.findIntersectionWithLatitude = function(intersectionLatitude, result) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('intersectionLatitude', intersectionLatitude); + if (!defined(this._distance) || this._distance === 0.0) { + throw new DeveloperError('EllipsoidRhumbLine must have distinct start and end set.'); + } + //>>includeEnd('debug'); + + var ellipticity = this._ellipticity; + var heading = this._heading; + var start = this._start; + + // If start and end have same latitude, return undefined since it's either no intersection or infinite intersections + if (CesiumMath.equalsEpsilon(Math.abs(heading), CesiumMath.PI_OVER_TWO, CesiumMath.EPSILON8)) { + return; + } + + // Can be solved using the same equations from interpolateUsingSurfaceDistance + var sigma1 = calculateSigma(ellipticity, start.latitude); + var sigma2 = calculateSigma(ellipticity, intersectionLatitude); + var deltaLongitude = Math.tan(heading) * (sigma2 - sigma1); + var longitude = CesiumMath.negativePiToPi(start.longitude + deltaLongitude); + + if (defined(result)) { + result.longitude = longitude; + result.latitude = intersectionLatitude; + result.height = 0; + + return result; + } + + return new Cartographic(longitude, intersectionLatitude, 0); + }; + + return EllipsoidRhumbLine; +}); diff --git a/Specs/Core/EllipsoidRhumbLineSpec.js b/Specs/Core/EllipsoidRhumbLineSpec.js new file mode 100644 index 000000000000..364a56402c5c --- /dev/null +++ b/Specs/Core/EllipsoidRhumbLineSpec.js @@ -0,0 +1,629 @@ +defineSuite([ + 'Core/EllipsoidRhumbLine', + 'Core/Cartographic', + 'Core/Ellipsoid', + 'Core/EllipsoidGeodesic', + 'Core/Math' + ], function( + EllipsoidRhumbLine, + Cartographic, + Ellipsoid, + EllipsoidGeodesic, + CesiumMath) { + 'use strict'; + + var oneDegree = CesiumMath.RADIANS_PER_DEGREE; + var fifteenDegrees = Math.PI / 12; + var thirtyDegrees = Math.PI / 6; + var fortyfiveDegrees = Math.PI / 4; + + it('throws without start', function() { + expect(function() { + var rhumb = new EllipsoidRhumbLine(); + return rhumb.interpolateUsingSurfaceDistance(0); + }).toThrowDeveloperError(); + }); + + it('throws without end', function() { + expect(function() { + var rhumb = new EllipsoidRhumbLine(new Cartographic(Math.PI, Math.PI)); + return rhumb.interpolateUsingSurfaceDistance(0); + }).toThrowDeveloperError(); + }); + + it('throws without unique position', function() { + expect(function() { + var rhumb = new EllipsoidRhumbLine(new Cartographic(Math.PI, Math.PI), new Cartographic(0, Math.PI)); + return rhumb.interpolateUsingSurfaceDistance(0); + }).toThrowDeveloperError(); + }); + + it('setEndPoints throws without start', function() { + expect(function() { + var rhumb = new EllipsoidRhumbLine(); + rhumb.setEndPoints(); + }).toThrowDeveloperError(); + }); + + it('setEndPoints throws without end', function() { + expect(function() { + var start = new Cartographic(CesiumMath.PI_OVER_TWO, 0); + var rhumb = new EllipsoidRhumbLine(); + rhumb.setEndPoints(start); + return rhumb.interpolateUsingSurfaceDistance(0); + }).toThrowDeveloperError(); + }); + + it('can create using fromStartHeadingDistance function', function() { + var ellipsoid = Ellipsoid.WGS84; + var start = new Cartographic(fifteenDegrees, fifteenDegrees); + var heading = fifteenDegrees; + var distance = fifteenDegrees * ellipsoid.maximumRadius; + + var rhumb = EllipsoidRhumbLine.fromStartHeadingDistance(start, heading, distance, ellipsoid); + expect(start).toEqual(rhumb.start); + expect(distance).toEqualEpsilon(rhumb.surfaceDistance, CesiumMath.EPSILON6); + expect(heading).toEqualEpsilon(rhumb.heading, CesiumMath.EPSILON12); + }); + + it('can create using fromStartHeadingDistance function with result', function() { + var ellipsoid = Ellipsoid.WGS84; + var scratch = new EllipsoidRhumbLine(undefined, undefined, ellipsoid); + + var start = new Cartographic(fifteenDegrees, fifteenDegrees); + var heading = fifteenDegrees; + var distance = fifteenDegrees * ellipsoid.maximumRadius; + + var rhumb = EllipsoidRhumbLine.fromStartHeadingDistance(start, heading, distance, ellipsoid, scratch); + expect(rhumb).toBe(scratch); + expect(rhumb.ellipsoid).toBe(ellipsoid); + expect(start).toEqual(rhumb.start); + expect(distance).toEqualEpsilon(rhumb.surfaceDistance, CesiumMath.EPSILON6); + expect(heading).toEqualEpsilon(rhumb.heading, CesiumMath.EPSILON12); + }); + + it('getSurfaceDistance throws if start or end never defined', function() { + expect(function() { + var rhumb = new EllipsoidRhumbLine(); + return rhumb.surfaceDistance; + }).toThrowDeveloperError(); + }); + + it('getHeading throws if start or end never defined', function() { + expect(function() { + var rhumb = new EllipsoidRhumbLine(); + return rhumb.heading; + }).toThrowDeveloperError(); + }); + + it('works with two points', function() { + var start = new Cartographic(fifteenDegrees, fifteenDegrees); + var end = new Cartographic(thirtyDegrees, thirtyDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end); + expect(start).toEqual(rhumb.start); + expect(end).toEqual(rhumb.end); + }); + + it('sets end points', function() { + var start = new Cartographic(CesiumMath.PI_OVER_TWO, 0); + var end = new Cartographic(CesiumMath.PI_OVER_TWO, CesiumMath.PI_OVER_TWO); + var rhumb = new EllipsoidRhumbLine(); + rhumb.setEndPoints(start, end); + expect(start).toEqual(rhumb.start); + expect(end).toEqual(rhumb.end); + }); + + it('gets heading', function() { + var ellipsoid = new Ellipsoid(6, 6, 3); + var start = new Cartographic(CesiumMath.PI_OVER_TWO, 0); + var end = new Cartographic(Math.PI, 0); + + var rhumb = new EllipsoidRhumbLine(start, end, ellipsoid); + expect(CesiumMath.PI_OVER_TWO).toEqualEpsilon(rhumb.heading, CesiumMath.EPSILON12); + }); + + it('computes heading not going over the pole', function() { + var ellipsoid = Ellipsoid.WGS84; + var start = new Cartographic(0, 1.2); + var end = new Cartographic(Math.PI, 1.5); + + var rhumb = new EllipsoidRhumbLine(start, end, ellipsoid); + + expect(0.0).not.toEqual(rhumb.heading); + }); + + it('computes heading going over the pole', function() { + var ellipsoid = Ellipsoid.WGS84; + var start = new Cartographic(1.3, CesiumMath.PI_OVER_TWO); + var end = new Cartographic(0.0, CesiumMath.PI / 2.4); + + var rhumb = new EllipsoidRhumbLine(start, end, ellipsoid); + + expect(0.0).not.toEqual(rhumb.heading); + }); + + it('heading works when going around the world at constant latitude', function() { + var ellipsoid = new Ellipsoid(6, 6, 6); + var start = new Cartographic(0.0, 0.3); + var end = new Cartographic(CesiumMath.PI_OVER_TWO, 0.3); + + var rhumb = new EllipsoidRhumbLine(start, end, ellipsoid); + + expect(CesiumMath.PI_OVER_TWO).toEqualEpsilon(rhumb.heading, CesiumMath.EPSILON12); + + start = new Cartographic(3 * CesiumMath.PI_OVER_TWO, 0.3); + end = new Cartographic(CesiumMath.PI, 0.3); + var rhumb2 = new EllipsoidRhumbLine(start, end, ellipsoid); + expect(-CesiumMath.PI_OVER_TWO).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + }); + + it('computes heading for vertical lines', function() { + var ellipsoid = Ellipsoid.WGS84; + var start = new Cartographic(0.0, 1.2); + var end = new Cartographic(0.0, 1.5); + + var rhumb = new EllipsoidRhumbLine(start, end, ellipsoid); + expect(0.0).toEqualEpsilon(rhumb.heading, CesiumMath.EPSILON12); + + var rhumb2 = new EllipsoidRhumbLine(end, start, ellipsoid); + expect(CesiumMath.PI).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + }); + + it('computes distance at equator', function() { + var ellipsoid = new Ellipsoid(6, 6, 3); + var start = new Cartographic(-CesiumMath.PI_OVER_FOUR, 0.0); + var end = new Cartographic(CesiumMath.PI_OVER_FOUR, 0.0); + + var rhumb = new EllipsoidRhumbLine(start, end, ellipsoid); + expect(CesiumMath.PI_OVER_TWO * 6).toEqualEpsilon(rhumb.surfaceDistance, CesiumMath.EPSILON12); + }); + + it('computes distance at meridian', function() { + var ellipsoid = new Ellipsoid(6, 6, 6); + var start = new Cartographic(CesiumMath.PI_OVER_TWO, fifteenDegrees); + var end = new Cartographic(CesiumMath.PI_OVER_TWO, fortyfiveDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end, ellipsoid); + expect(thirtyDegrees * 6).toEqualEpsilon(rhumb.surfaceDistance, CesiumMath.EPSILON12); + }); + + it('computes equal distance on sphere for 90 degrees arcs along meridian and equator', function() { + var ellipsoid = new Ellipsoid(6, 6, 6); + var fortyFiveSouth = new Cartographic(0.0, -CesiumMath.PI_OVER_FOUR); + var fortyFiveNorth = new Cartographic(0.0, CesiumMath.PI_OVER_FOUR); + var fortyFiveWest = new Cartographic(-CesiumMath.PI_OVER_FOUR, 0.0); + var fortyFiveEast = new Cartographic(CesiumMath.PI_OVER_FOUR, 0.0); + + var westEastRhumb = new EllipsoidRhumbLine(fortyFiveWest, fortyFiveEast, ellipsoid); + var southNorthRhumb = new EllipsoidRhumbLine(fortyFiveSouth, fortyFiveNorth, ellipsoid); + var eastWestRhumb = new EllipsoidRhumbLine(fortyFiveEast, fortyFiveWest, ellipsoid); + var northSouthRhumb = new EllipsoidRhumbLine(fortyFiveNorth, fortyFiveSouth, ellipsoid); + expect(CesiumMath.PI_OVER_TWO * 6).toEqualEpsilon(westEastRhumb.surfaceDistance, CesiumMath.EPSILON12); + expect(CesiumMath.PI_OVER_TWO * 6).toEqualEpsilon(southNorthRhumb.surfaceDistance, CesiumMath.EPSILON12); + expect(westEastRhumb.surfaceDistance).toEqualEpsilon(southNorthRhumb.surfaceDistance, CesiumMath.EPSILON12); + + expect(CesiumMath.PI_OVER_TWO * 6).toEqualEpsilon(eastWestRhumb.surfaceDistance, CesiumMath.EPSILON12); + expect(CesiumMath.PI_OVER_TWO * 6).toEqualEpsilon(northSouthRhumb.surfaceDistance, CesiumMath.EPSILON12); + expect(eastWestRhumb.surfaceDistance).toEqualEpsilon(northSouthRhumb.surfaceDistance, CesiumMath.EPSILON12); + }); + + it('computes distance at same latitude', function() { + var ellipsoid = new Ellipsoid(6, 6, 6); + var start = new Cartographic(0, -fortyfiveDegrees); + var end = new Cartographic(CesiumMath.PI_OVER_TWO, -fortyfiveDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end, ellipsoid); + var distance = Math.cos(fortyfiveDegrees) * CesiumMath.PI_OVER_TWO * 6; + expect(distance).toEqualEpsilon(rhumb.surfaceDistance, CesiumMath.EPSILON12); + }); + + it('throws when interpolating rhumb line of zero length', function() { + var radius = 6378137.0; + var ellipsoid = new Ellipsoid(radius, radius, radius); + var initial = new Cartographic(fifteenDegrees, fifteenDegrees); + + expect(function() { + var rhumb = EllipsoidRhumbLine.fromStartHeadingDistance(initial, fifteenDegrees, 0.0, ellipsoid); + return rhumb.interpolateUsingSurfaceDistance(0); + }).toThrowDeveloperError(); + }); + + it('computes heading and distance given endpoints on sphere ', function() { + var radius = 6378137.0; + var ellipsoid = new Ellipsoid(radius, radius, radius); + var initial = new Cartographic(fifteenDegrees, fifteenDegrees); + var distance = radius * fifteenDegrees; + + var rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, fifteenDegrees, distance, ellipsoid); + var rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + + expect(fifteenDegrees).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(distance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + }); + + it('computes heading and distance given endpoints on sphereoid', function() { + var ellipsoid = Ellipsoid.WGS84; + var initial = new Cartographic(fifteenDegrees, fifteenDegrees); + var distance = ellipsoid.maximumRadius * fifteenDegrees; + + var rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, fifteenDegrees, distance, ellipsoid); + var rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + + expect(fifteenDegrees).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(distance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + }); + + it('tests sphere close to 90 degrees', function() { + var radius = 6378137.0; + var ellipsoid = new Ellipsoid(radius, radius, radius); + var initial = new Cartographic(fifteenDegrees, fifteenDegrees); + var distance = radius * fifteenDegrees; + + var eightyNineDegrees = 89 * oneDegree; + var eightyNinePointNineDegrees = 89.9 * oneDegree; + var ninetyDegrees = 90 * oneDegree; + var ninetyPointOneDegrees = 90.1 * oneDegree; + var ninetyPointZeroTwoDegrees = 90.02 * oneDegree; + + var rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, eightyNineDegrees, distance, ellipsoid); + var rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + + rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, eightyNinePointNineDegrees, distance, ellipsoid); + rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + + rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, ninetyDegrees, distance, ellipsoid); + rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + + rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, ninetyPointOneDegrees, distance, ellipsoid); + rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + + rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, ninetyPointZeroTwoDegrees, distance, ellipsoid); + rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + }); + + it('tests spheroid close to 90 degrees', function() { + var ellipsoid = Ellipsoid.WGS84; + var initial = new Cartographic(fifteenDegrees, fifteenDegrees); + var distance = ellipsoid.maximumRadius * fifteenDegrees; + + var eightyNineDegrees = 89 * oneDegree; + var eightyNinePointNineDegrees = 89.9 * oneDegree; + var ninetyDegrees = 90 * oneDegree; + var ninetyPointOneDegrees = 90.1 * oneDegree; + var ninetyPointZeroTwoDegrees = 90.02 * oneDegree; + + var rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, eightyNineDegrees, distance, ellipsoid); + var rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + + rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, eightyNinePointNineDegrees, distance, ellipsoid); + rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + + rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, ninetyDegrees, distance, ellipsoid); + rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + + rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, ninetyPointOneDegrees, distance, ellipsoid); + rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + + rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, ninetyPointZeroTwoDegrees, distance, ellipsoid); + rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + }); + + it('test sphereoid across meridian', function() { + var ellipsoid = Ellipsoid.WGS84; + var initial = new Cartographic(-fifteenDegrees, 0.0); + var final = new Cartographic(fifteenDegrees, 0.0); + var distance = ellipsoid.maximumRadius * 2 * fifteenDegrees; + + var rhumb1 = new EllipsoidRhumbLine(initial, final, ellipsoid); + var rhumb2 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, CesiumMath.PI_OVER_TWO, distance, ellipsoid); + + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + }); + + it('test across IDL with -PI to PI range of longitude', function() { + var ellipsoid = Ellipsoid.WGS84; + var initial = new Cartographic(-CesiumMath.PI + fifteenDegrees, 0.0); + var final = new Cartographic(CesiumMath.PI - fifteenDegrees, 0.0); + + var distance = ellipsoid.maximumRadius * 2 * fifteenDegrees; + + var rhumb1 = new EllipsoidRhumbLine(initial, final, ellipsoid); + var rhumb2 = new EllipsoidRhumbLine.fromStartHeadingDistance(initial, 3.0 * CesiumMath.PI_OVER_TWO, distance, ellipsoid); + + expect(-CesiumMath.PI_OVER_TWO).toEqualEpsilon(rhumb1.heading, CesiumMath.EPSILON12); + expect(distance).toEqualEpsilon(rhumb1.surfaceDistance, CesiumMath.EPSILON6); + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + + var rhumb3 = new EllipsoidRhumbLine(final, initial, ellipsoid); + var rhumb4 = new EllipsoidRhumbLine.fromStartHeadingDistance(final, CesiumMath.PI_OVER_TWO, distance, ellipsoid); + expect(CesiumMath.PI_OVER_TWO).toEqualEpsilon(rhumb3.heading, CesiumMath.EPSILON12); + expect(distance).toEqualEpsilon(rhumb3.surfaceDistance, CesiumMath.EPSILON6); + expect(rhumb3.heading).toEqualEpsilon(rhumb4.heading, CesiumMath.EPSILON12); + expect(rhumb3.surfaceDistance).toEqualEpsilon(rhumb4.surfaceDistance, CesiumMath.EPSILON6); + }); + + it('test across equator', function() { + var ellipsoid = Ellipsoid.WGS84; + var initial = new Cartographic(fifteenDegrees, -oneDegree); + var final = new Cartographic(fifteenDegrees, oneDegree); + + //A rhumb line with heading = 0 should be almost the same as a geodesic + var rhumb = new EllipsoidRhumbLine(initial, final, ellipsoid); + var geodesic = new EllipsoidGeodesic(initial, final, ellipsoid); + expect(0.0).toEqualEpsilon(rhumb.heading, CesiumMath.EPSILON12); + expect(geodesic.startHeading).toEqualEpsilon(rhumb.heading, CesiumMath.EPSILON12); + expect(geodesic.surfaceDistance).toEqualEpsilon(rhumb.surfaceDistance, CesiumMath.EPSILON6); + }); + + it('test on equator', function() { + var ellipsoid = Ellipsoid.WGS84; + var initial = new Cartographic(0.0, 0.0); + var final = new Cartographic(CesiumMath.PI - 1, 0.0); + + //A rhumb line on the equator should be the same as a geodesic + var rhumb = new EllipsoidRhumbLine(initial, final, ellipsoid); + var geodesic = new EllipsoidGeodesic(initial, final, ellipsoid); + expect(CesiumMath.PI_OVER_TWO).toEqualEpsilon(rhumb.heading, CesiumMath.EPSILON12); + expect(geodesic.startHeading).toEqualEpsilon(rhumb.heading, CesiumMath.EPSILON12); + expect(geodesic.surfaceDistance).toEqualEpsilon(rhumb.surfaceDistance, CesiumMath.EPSILON4); // Due to computational difference, slightly larger tolerance + }); + + it('test close to poles', function() { + var ellipsoid = Ellipsoid.WGS84; + var fiveDegrees = CesiumMath.PI / 36.0; + var eightyDegrees = 16 * fiveDegrees; + + var distance = fifteenDegrees * ellipsoid.maximumRadius; + + var initial = new Cartographic(0.0, eightyDegrees); + + var rhumb1 = EllipsoidRhumbLine.fromStartHeadingDistance(initial, eightyDegrees, distance, ellipsoid); + var rhumb2 = new EllipsoidRhumbLine(initial, rhumb1.end, ellipsoid); + + expect(rhumb1.heading).toEqualEpsilon(rhumb2.heading, CesiumMath.EPSILON12); + expect(rhumb1.surfaceDistance).toEqualEpsilon(rhumb2.surfaceDistance, CesiumMath.EPSILON6); + }); + + it('test interpolate fraction', function() { + var ellipsoid = Ellipsoid.WGS84; + var initial = new Cartographic(0.0, 0.0); + var final = new Cartographic(CesiumMath.PI_OVER_TWO, 0.0); + var halfway = new Cartographic(CesiumMath.PI_OVER_FOUR, 0.0); + + var rhumb = new EllipsoidRhumbLine(initial, final, ellipsoid); + var interpolatedPoint = rhumb.interpolateUsingFraction(0.5); + + expect(halfway.longitude).toEqualEpsilon(interpolatedPoint.longitude, CesiumMath.EPSILON12); + expect(halfway.latitude).toEqualEpsilon(interpolatedPoint.latitude, CesiumMath.EPSILON12); + }); + + it('test interpolate distance', function() { + var ellipsoid = Ellipsoid.WGS84; + var initial = new Cartographic(0.0, 0.0); + var final = new Cartographic(CesiumMath.PI_OVER_TWO, 0.0); + var halfway = new Cartographic(CesiumMath.PI_OVER_FOUR, 0.0); + + var distance = ellipsoid.maximumRadius * CesiumMath.PI_OVER_FOUR; + + var rhumb = new EllipsoidRhumbLine(initial, final, ellipsoid); + var interpolatedPoint = rhumb.interpolateUsingSurfaceDistance(distance); + + expect(halfway.longitude).toEqualEpsilon(interpolatedPoint.longitude, CesiumMath.EPSILON12); + expect(halfway.latitude).toEqualEpsilon(interpolatedPoint.latitude, CesiumMath.EPSILON12); + }); + + it('interpolates start and end points', function() { + var start = new Cartographic(fifteenDegrees, fifteenDegrees); + var end = new Cartographic(thirtyDegrees, thirtyDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end); + var distance = rhumb.surfaceDistance; + + var first = rhumb.interpolateUsingSurfaceDistance(0.0); + var last = rhumb.interpolateUsingSurfaceDistance(distance); + + expect(start.longitude).toEqualEpsilon(first.longitude, CesiumMath.EPSILON12); + expect(start.latitude).toEqualEpsilon(first.latitude, CesiumMath.EPSILON12); + expect(end.longitude).toEqualEpsilon(last.longitude, CesiumMath.EPSILON12); + expect(end.latitude).toEqualEpsilon(last.latitude, CesiumMath.EPSILON12); + }); + + it('interpolates midpoint', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fortyfiveDegrees, 0.0); + var expectedMid = new Cartographic(thirtyDegrees, 0.0); + + var rhumb = new EllipsoidRhumbLine(start, end); + var distance = Ellipsoid.WGS84.radii.x * fifteenDegrees; + + var midpoint = rhumb.interpolateUsingSurfaceDistance(distance); + + expect(expectedMid.longitude).toEqualEpsilon(midpoint.longitude, CesiumMath.EPSILON12); + expect(expectedMid.latitude).toEqualEpsilon(midpoint.latitude, CesiumMath.EPSILON12); + }); + + it('interpolates start and end points using fraction', function() { + var start = new Cartographic(fifteenDegrees, fifteenDegrees); + var end = new Cartographic(thirtyDegrees, thirtyDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end); + + var first = rhumb.interpolateUsingFraction(0); + var last = rhumb.interpolateUsingFraction(1); + + expect(start.longitude).toEqualEpsilon(first.longitude, CesiumMath.EPSILON12); + expect(start.latitude).toEqualEpsilon(first.latitude, CesiumMath.EPSILON12); + expect(end.longitude).toEqualEpsilon(last.longitude, CesiumMath.EPSILON12); + expect(end.latitude).toEqualEpsilon(last.latitude, CesiumMath.EPSILON12); + }); + + it('interpolates midpoint using fraction', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fortyfiveDegrees, 0.0); + var expectedMid = new Cartographic(thirtyDegrees, 0.0); + + var rhumb = new EllipsoidRhumbLine(start, end); + + var midpoint = rhumb.interpolateUsingFraction(0.5); + + expect(expectedMid.longitude).toEqualEpsilon(midpoint.longitude, CesiumMath.EPSILON12); + expect(expectedMid.latitude).toEqualEpsilon(midpoint.latitude, CesiumMath.EPSILON12); + }); + + it('interpolates midpoint fraction using result parameter', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fortyfiveDegrees, 0.0); + var expectedMid = new Cartographic(thirtyDegrees, 0.0); + + var rhumb = new EllipsoidRhumbLine(start, end); + var result = new Cartographic(); + var midpoint = rhumb.interpolateUsingFraction(0.5, result); + expect(result).toBe(midpoint); + + expect(expectedMid.longitude).toEqualEpsilon(result.longitude, CesiumMath.EPSILON12); + expect(expectedMid.latitude).toEqualEpsilon(result.latitude, CesiumMath.EPSILON12); + }); + + it('interpolates midpoint using result parameter', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fortyfiveDegrees, 0.0); + var expectedMid = new Cartographic(thirtyDegrees, 0.0); + + var rhumb = new EllipsoidRhumbLine(start, end); + var distance = Ellipsoid.WGS84.radii.x * fifteenDegrees; + + var result = new Cartographic(); + var midpoint = rhumb.interpolateUsingSurfaceDistance(distance, result); + + expect(result).toBe(midpoint); + + expect(expectedMid.longitude).toEqualEpsilon(result.longitude, CesiumMath.EPSILON12); + expect(expectedMid.latitude).toEqualEpsilon(result.latitude, CesiumMath.EPSILON12); + }); + + it('finds midpoint and other points using intersection with longitude', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fortyfiveDegrees, thirtyDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end); + + var midpointUsingInterpolation = rhumb.interpolateUsingFraction(0.5); + var midpointUsingIntersection = rhumb.findIntersectionWithLongitude(midpointUsingInterpolation.longitude); + expect(Cartographic.equalsEpsilon(midpointUsingInterpolation, midpointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + + var pointUsingInterpolation = rhumb.interpolateUsingFraction(0.1); + var pointUsingIntersection = rhumb.findIntersectionWithLongitude(pointUsingInterpolation.longitude); + expect(Cartographic.equalsEpsilon(pointUsingInterpolation, pointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + + pointUsingInterpolation = rhumb.interpolateUsingFraction(0.75); + pointUsingIntersection = rhumb.findIntersectionWithLongitude(pointUsingInterpolation.longitude); + expect(Cartographic.equalsEpsilon(pointUsingInterpolation, pointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + + pointUsingInterpolation = rhumb.interpolateUsingFraction(1.1); + pointUsingIntersection = rhumb.findIntersectionWithLongitude(pointUsingInterpolation.longitude); + expect(Cartographic.equalsEpsilon(pointUsingInterpolation, pointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + }); + + it('intersection with longitude handles E-W lines', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fortyfiveDegrees, 0.0); + + var rhumb = new EllipsoidRhumbLine(start, end); + + var midpointUsingInterpolation = rhumb.interpolateUsingFraction(0.5); + var midpointUsingIntersection = rhumb.findIntersectionWithLongitude(midpointUsingInterpolation.longitude); + expect(Cartographic.equalsEpsilon(midpointUsingInterpolation, midpointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + }); + + it('intersection with longitude handles N-S lines', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fifteenDegrees, thirtyDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end); + + var midpointUsingInterpolation = rhumb.interpolateUsingFraction(0.5); + var midpointUsingIntersection = rhumb.findIntersectionWithLongitude(midpointUsingInterpolation.longitude); + + expect(midpointUsingIntersection).not.toBeDefined(); + }); + + it('intersection with longitude handles N-S lines with different longitude', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fifteenDegrees, thirtyDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end); + + var midpointUsingIntersection = rhumb.findIntersectionWithLongitude(thirtyDegrees); + + expect(midpointUsingIntersection.latitude).toEqualEpsilon(CesiumMath.PI_OVER_TWO, CesiumMath.EPSILON12); + }); + + it('finds midpoint and other points using intersection with latitude', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fortyfiveDegrees, thirtyDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end); + + var midpointUsingInterpolation = rhumb.interpolateUsingFraction(0.5); + var midpointUsingIntersection = rhumb.findIntersectionWithLatitude(midpointUsingInterpolation.latitude); + expect(Cartographic.equalsEpsilon(midpointUsingInterpolation, midpointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + + var pointUsingInterpolation = rhumb.interpolateUsingFraction(0.1); + var pointUsingIntersection = rhumb.findIntersectionWithLatitude(pointUsingInterpolation.latitude); + expect(Cartographic.equalsEpsilon(pointUsingInterpolation, pointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + + pointUsingInterpolation = rhumb.interpolateUsingFraction(0.75); + pointUsingIntersection = rhumb.findIntersectionWithLatitude(pointUsingInterpolation.latitude); + expect(Cartographic.equalsEpsilon(pointUsingInterpolation, pointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + + pointUsingInterpolation = rhumb.interpolateUsingFraction(1.1); + pointUsingIntersection = rhumb.findIntersectionWithLatitude(pointUsingInterpolation.latitude); + expect(Cartographic.equalsEpsilon(pointUsingInterpolation, pointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + }); + + it('intersection with latitude handles E-W lines', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fortyfiveDegrees, 0.0); + + var rhumb = new EllipsoidRhumbLine(start, end); + + var midpointUsingInterpolation = rhumb.interpolateUsingFraction(0.5); + var midpointUsingIntersection = rhumb.findIntersectionWithLatitude(midpointUsingInterpolation.latitude); + + expect(midpointUsingIntersection).not.toBeDefined(); + }); + + it('intersection with latitude handles N-S lines', function() { + var start = new Cartographic(fifteenDegrees, 0.0); + var end = new Cartographic(fifteenDegrees, thirtyDegrees); + + var rhumb = new EllipsoidRhumbLine(start, end); + + var midpointUsingInterpolation = rhumb.interpolateUsingFraction(0.5); + var midpointUsingIntersection = rhumb.findIntersectionWithLatitude(midpointUsingInterpolation.latitude); + expect(Cartographic.equalsEpsilon(midpointUsingInterpolation, midpointUsingIntersection, CesiumMath.EPSILON12)).toBe(true); + }); +});