diff --git a/CHANGES.md b/CHANGES.md index aa14d918ed4f..d49c045474ee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ Change Log ### 1.11 - 2015-07-01 +* Improved the algorithm that `Camera.viewRectangle` uses to select the position of the camera, so that the specified rectangle is now better centered on the screen. * The performance statistics displayed by setting `scene.debugShowFramesPerSecond` to `true` can now be styled using the `cesium-performanceDisplay` CSS classes in `shared.css`. ### 1.10 - 2015-06-01 diff --git a/Source/Core/EllipsoidGeodesic.js b/Source/Core/EllipsoidGeodesic.js index d382bd0c296f..c77c6fc9ad31 100644 --- a/Source/Core/EllipsoidGeodesic.js +++ b/Source/Core/EllipsoidGeodesic.js @@ -224,7 +224,18 @@ define([ defineProperties(EllipsoidGeodesic.prototype, { /** - * The surface distance between the start and end point + * Gets the ellipsoid. + * @memberof EllipsoidGeodesic.prototype + * @type {Ellipsoid} + */ + ellipsoid : { + get : function() { + return this._ellipsoid; + } + }, + + /** + * Gets the surface distance between the start and end point * @memberof EllipsoidGeodesic.prototype * @type {Number} */ @@ -241,7 +252,7 @@ define([ }, /** - * The initial planetodetic point on the path. + * Gets the initial planetodetic point on the path. * @memberof EllipsoidGeodesic.prototype * @type {Cartographic} */ @@ -252,7 +263,7 @@ define([ }, /** - * The final planetodetic point on the path. + * Gets the final planetodetic point on the path. * @memberof EllipsoidGeodesic.prototype * @type {Cartographic} */ @@ -263,7 +274,7 @@ define([ }, /** - * The heading at the initial point. + * Gets the heading at the initial point. * @memberof EllipsoidGeodesic.prototype * @type {Number} */ @@ -280,7 +291,7 @@ define([ }, /** - * The heading at the final point. + * Gets the heading at the final point. * @memberof EllipsoidGeodesic.prototype * @type {Number} */ diff --git a/Source/Scene/Camera.js b/Source/Scene/Camera.js index fed7d13feaf6..dd47c2eea875 100644 --- a/Source/Scene/Camera.js +++ b/Source/Scene/Camera.js @@ -10,6 +10,7 @@ define([ '../Core/DeveloperError', '../Core/EasingFunction', '../Core/Ellipsoid', + '../Core/EllipsoidGeodesic', '../Core/Event', '../Core/IntersectionTests', '../Core/Math', @@ -34,6 +35,7 @@ define([ DeveloperError, EasingFunction, Ellipsoid, + EllipsoidGeodesic, Event, IntersectionTests, CesiumMath, @@ -1656,13 +1658,19 @@ define([ Cartesian3.normalize(this.up, this.up); }; - var viewRectangle3DCartographic = new Cartographic(); + var viewRectangle3DCartographic1 = new Cartographic(); + var viewRectangle3DCartographic2 = new Cartographic(); var viewRectangle3DNorthEast = new Cartesian3(); var viewRectangle3DSouthWest = new Cartesian3(); var viewRectangle3DNorthWest = new Cartesian3(); var viewRectangle3DSouthEast = new Cartesian3(); + var viewRectangle3DNorthCenter = new Cartesian3(); + var viewRectangle3DSouthCenter = new Cartesian3(); var viewRectangle3DCenter = new Cartesian3(); + var viewRectangle3DEquator = new Cartesian3(); var defaultRF = {direction: new Cartesian3(), right: new Cartesian3(), up: new Cartesian3()}; + var viewRectangle3DEllipsoidGeodesic; + function rectangleCameraPosition3D (camera, rectangle, ellipsoid, result, positionOnly) { if (!defined(result)) { result = new Cartesian3(); @@ -1672,6 +1680,7 @@ define([ if (positionOnly) { cameraRF = defaultRF; } + var north = rectangle.north; var south = rectangle.south; var east = rectangle.east; @@ -1682,59 +1691,115 @@ define([ east += CesiumMath.TWO_PI; } - var cart = viewRectangle3DCartographic; + // Find the midpoint latitude. + // + // EllipsoidGeodesic will fail if the north and south edges are very close to being on opposite sides of the ellipsoid. + // Ideally we'd just call EllipsoidGeodesic.setEndPoints and let it throw when it detects this case, but sadly it doesn't + // even look for this case in optimized builds, so we have to test for it here instead. + // + // Fortunately, this case can only happen (here) when north is very close to the north pole and south is very close to the south pole, + // so handle it just by using 0 latitude as the center. It's certainliy possible to use a smaller tolerance + // than one degree here, but one degree is safe and putting the center at 0 latitude should be good enough for any + // rectangle that spans 178+ of the 180 degrees of latitude. + var longitude = (west + east) * 0.5; + var latitude; + if (south < -CesiumMath.PI_OVER_TWO + CesiumMath.RADIANS_PER_DEGREE && north > CesiumMath.PI_OVER_TWO - CesiumMath.RADIANS_PER_DEGREE) { + latitude = 0.0; + } else { + var northCartographic = viewRectangle3DCartographic1; + northCartographic.longitude = longitude; + northCartographic.latitude = north; + northCartographic.height = 0.0; + + var southCartographic = viewRectangle3DCartographic2; + southCartographic.longitude = longitude; + southCartographic.latitude = south; + southCartographic.height = 0.0; + + var ellipsoidGeodesic = viewRectangle3DEllipsoidGeodesic; + if (!defined(ellipsoidGeodesic) || ellipsoidGeodesic.ellipsoid !== ellipsoid) { + viewRectangle3DEllipsoidGeodesic = ellipsoidGeodesic = new EllipsoidGeodesic(undefined, undefined, ellipsoid); + } + + ellipsoidGeodesic.setEndPoints(northCartographic, southCartographic); + latitude = ellipsoidGeodesic.interpolateUsingFraction(0.5, viewRectangle3DCartographic1).latitude; + } + + var centerCartographic = viewRectangle3DCartographic1; + centerCartographic.longitude = longitude; + centerCartographic.latitude = latitude; + centerCartographic.height = 0.0; + + var center = ellipsoid.cartographicToCartesian(centerCartographic, viewRectangle3DCenter); + + var cart = viewRectangle3DCartographic1; cart.longitude = east; cart.latitude = north; var northEast = ellipsoid.cartographicToCartesian(cart, viewRectangle3DNorthEast); + cart.longitude = west; + var northWest = ellipsoid.cartographicToCartesian(cart, viewRectangle3DNorthWest); + cart.longitude = longitude; + var northCenter = ellipsoid.cartographicToCartesian(cart, viewRectangle3DNorthCenter); cart.latitude = south; + var southCenter = ellipsoid.cartographicToCartesian(cart, viewRectangle3DSouthCenter); + cart.longitude = east; var southEast = ellipsoid.cartographicToCartesian(cart, viewRectangle3DSouthEast); cart.longitude = west; var southWest = ellipsoid.cartographicToCartesian(cart, viewRectangle3DSouthWest); - cart.latitude = north; - var northWest = ellipsoid.cartographicToCartesian(cart, viewRectangle3DNorthWest); - - var center = Cartesian3.subtract(northEast, southWest, viewRectangle3DCenter); - Cartesian3.multiplyByScalar(center, 0.5, center); - Cartesian3.add(southWest, center, center); - - var mag = Cartesian3.magnitude(center); - if (mag < CesiumMath.EPSILON6) { - cart.longitude = (east + west) * 0.5; - cart.latitude = (north + south) * 0.5; - ellipsoid.cartographicToCartesian(cart, center); - } Cartesian3.subtract(northWest, center, northWest); Cartesian3.subtract(southEast, center, southEast); Cartesian3.subtract(northEast, center, northEast); Cartesian3.subtract(southWest, center, southWest); + Cartesian3.subtract(northCenter, center, northCenter); + Cartesian3.subtract(southCenter, center, southCenter); - var direction = Cartesian3.negate(center, cameraRF.direction); - Cartesian3.normalize(direction, direction); + var direction = ellipsoid.geodeticSurfaceNormal(center, cameraRF.direction); + Cartesian3.negate(direction, direction); var right = Cartesian3.cross(direction, Cartesian3.UNIT_Z, cameraRF.right); Cartesian3.normalize(right, right); var up = Cartesian3.cross(right, direction, cameraRF.up); - var height = Math.max( - Math.abs(Cartesian3.dot(up, northWest)), - Math.abs(Cartesian3.dot(up, southEast)), - Math.abs(Cartesian3.dot(up, northEast)), - Math.abs(Cartesian3.dot(up, southWest)) - ); - var width = Math.max( - Math.abs(Cartesian3.dot(right, northWest)), - Math.abs(Cartesian3.dot(right, southEast)), - Math.abs(Cartesian3.dot(right, northEast)), - Math.abs(Cartesian3.dot(right, southWest)) - ); - var tanPhi = Math.tan(camera.frustum.fovy * 0.5); var tanTheta = camera.frustum.aspectRatio * tanPhi; - var d = Math.max(width / tanTheta, height / tanPhi); - var scalar = mag + d; - Cartesian3.normalize(center, center); - return Cartesian3.multiplyByScalar(center, scalar, result); + function computeD(direction, upOrRight, corner, tanThetaOrPhi) { + var opposite = Math.abs(Cartesian3.dot(upOrRight, corner)); + return opposite / tanThetaOrPhi - Cartesian3.dot(direction, corner); + } + + var d = Math.max( + computeD(direction, up, northWest, tanPhi), + computeD(direction, up, southEast, tanPhi), + computeD(direction, up, northEast, tanPhi), + computeD(direction, up, southWest, tanPhi), + computeD(direction, up, northCenter, tanPhi), + computeD(direction, up, southCenter, tanPhi), + computeD(direction, right, northWest, tanTheta), + computeD(direction, right, southEast, tanTheta), + computeD(direction, right, northEast, tanTheta), + computeD(direction, right, southWest, tanTheta), + computeD(direction, right, northCenter, tanTheta), + computeD(direction, right, southCenter, tanTheta)); + + // If the rectangle crosses the equator, compute D at the equator, too, because that's the + // widest part of the rectangle when projected onto the globe. + if (south < 0 && north > 0) { + var equatorCartographic = viewRectangle3DCartographic1; + equatorCartographic.longitude = west; + equatorCartographic.latitude = 0.0; + equatorCartographic.height = 0.0; + var equatorPosition = ellipsoid.cartographicToCartesian(equatorCartographic, viewRectangle3DEquator); + Cartesian3.subtract(equatorPosition, center, equatorPosition); + d = Math.max(d, computeD(direction, up, equatorPosition, tanPhi), computeD(direction, right, equatorPosition, tanTheta)); + + equatorCartographic.longitude = east; + equatorPosition = ellipsoid.cartographicToCartesian(equatorCartographic, viewRectangle3DEquator); + Cartesian3.subtract(equatorPosition, center, equatorPosition); + d = Math.max(d, computeD(direction, up, equatorPosition, tanPhi), computeD(direction, right, equatorPosition, tanTheta)); + } + + return Cartesian3.add(center, Cartesian3.multiplyByScalar(direction, -d, viewRectangle3DEquator), result); } var viewRectangleCVCartographic = new Cartographic(); diff --git a/Specs/Scene/CameraSpec.js b/Specs/Scene/CameraSpec.js index 1a0157a14158..79a3beec4f59 100644 --- a/Specs/Scene/CameraSpec.js +++ b/Specs/Scene/CameraSpec.js @@ -1294,10 +1294,10 @@ defineSuite([ CesiumMath.toRadians(21.51), CesiumMath.toRadians(41.38)); camera.viewRectangle(rectangle, Ellipsoid.WGS84); - expect(camera.position).toEqualEpsilon(new Cartesian3(4481581.054168208, 1754494.5938935655, 4200573.072090136), CesiumMath.EPSILON6); - expect(camera.direction).toEqualEpsilon(new Cartesian3(-0.7015530983057745, -0.2746510892984876, -0.6575637074875123), CesiumMath.EPSILON10); - expect(camera.up).toEqualEpsilon(new Cartesian3(-0.6123128513437499, -0.23971441651266895, 0.7533989451779698), CesiumMath.EPSILON10); - expect(camera.right).toEqualEpsilon(new Cartesian3(-0.36454934142973716, 0.9311840729217532, 0.0), CesiumMath.EPSILON10); + expect(camera.position).toEqualEpsilon(new Cartesian3(4481555.454147325, 1754498.0086281248, 4200627.581953675), CesiumMath.EPSILON6); + expect(camera.direction).toEqualEpsilon(new Cartesian3(-0.6995108433013301, -0.27385366401082994, -0.6600672320390594), CesiumMath.EPSILON10); + expect(camera.up).toEqualEpsilon(new Cartesian3(-0.6146434679470263, -0.24062867269250837, 0.75120652898407), CesiumMath.EPSILON10); + expect(camera.right).toEqualEpsilon(new Cartesian3(-0.36455176232452213, 0.9311831251617939, 0), CesiumMath.EPSILON10); }); it('views rectangle in 3D (3)', function() { @@ -1307,10 +1307,23 @@ defineSuite([ CesiumMath.toRadians(157.0), CesiumMath.toRadians(0.0)); camera.viewRectangle(rectangle); - expect(camera.position).toEqualEpsilon(new Cartesian3(-7210721.873278953, 8105929.1576369405, -5972336.199381728), CesiumMath.EPSILON6); - expect(camera.direction).toEqualEpsilon(new Cartesian3(0.5822498554483325, -0.6545358652367963, 0.48225294913469874), CesiumMath.EPSILON10); - expect(camera.up).toEqualEpsilon(new Cartesian3(-0.32052676705406324, 0.3603199946588929, 0.8760320159964963), CesiumMath.EPSILON10); - expect(camera.right).toEqualEpsilon(new Cartesian3(-0.7471597536218517, -0.6646444933705039, 0.0), CesiumMath.EPSILON10); + expect(camera.position).toEqualEpsilon(new Cartesian3(-6017603.25625715, 9091606.78076493, -5075070.862292178), CesiumMath.EPSILON6); + expect(camera.direction).toEqualEpsilon(new Cartesian3(0.5000640869795608, -0.7555144216716235, 0.4232420909591703), CesiumMath.EPSILON10); + expect(camera.up).toEqualEpsilon(new Cartesian3(-0.23360296374117637, 0.35293557895291494, 0.9060166292295686), CesiumMath.EPSILON10); + expect(camera.right).toEqualEpsilon(new Cartesian3(-0.8338858220671682, -0.5519369853120581, 0), CesiumMath.EPSILON10); + }); + + it('views rectangle in 3D (4)', function() { + var rectangle = new Rectangle( + CesiumMath.toRadians(90.0), + CesiumMath.toRadians(-62.0), + CesiumMath.toRadians(174.0), + CesiumMath.toRadians(-4.0)); + camera.viewRectangle(rectangle); + expect(camera.position).toEqualEpsilon(new Cartesian3(-7307919.685704952, 8116267.060310548, -7085995.891547672), CesiumMath.EPSILON6); + expect(camera.direction).toEqualEpsilon(new Cartesian3(0.5607858365117034, -0.622815768168856, 0.5455453826109309), CesiumMath.EPSILON10); + expect(camera.up).toEqualEpsilon(new Cartesian3(-0.3650411126627274, 0.4054192281503986, 0.8380812821629494), CesiumMath.EPSILON10); + expect(camera.right).toEqualEpsilon(new Cartesian3(-0.7431448254773944, -0.6691306063588581, 0), CesiumMath.EPSILON10); }); it('views rectangle in 3D across IDL', function() {