diff --git a/Source/Core/ApproximateTerrainHeights.js b/Source/Core/ApproximateTerrainHeights.js new file mode 100644 index 000000000000..e295abe57a39 --- /dev/null +++ b/Source/Core/ApproximateTerrainHeights.js @@ -0,0 +1,203 @@ +define([ + './buildModuleUrl', + './defaultValue', + './defined', + './BoundingSphere', + './Cartesian2', + './Cartesian3', + './Cartographic', + './Check', + './DeveloperError', + './Ellipsoid', + './GeographicTilingScheme', + './Rectangle', + './Resource' + ], function( + buildModuleUrl, + defaultValue, + defined, + BoundingSphere, + Cartesian2, + Cartesian3, + Cartographic, + Check, + DeveloperError, + Ellipsoid, + GeographicTilingScheme, + Rectangle, + Resource) { + 'use strict'; + + var scratchDiagonalCartesianNE = new Cartesian3(); + var scratchDiagonalCartesianSW = new Cartesian3(); + var scratchDiagonalCartographic = new Cartographic(); + var scratchCenterCartesian = new Cartesian3(); + var scratchSurfaceCartesian = new Cartesian3(); + + var scratchBoundingSphere = new BoundingSphere(); + var tilingScheme = new GeographicTilingScheme(); + var scratchCorners = [new Cartographic(), new Cartographic(), new Cartographic(), new Cartographic()]; + var scratchTileXY = new Cartesian2(); + + /** + * A collection of functions for approximating terrain height + * @private + */ + var ApproximateTerrainHeights = {}; + + /** + * Initializes the minimum and maximum terrain heights + * @return {Promise} + */ + ApproximateTerrainHeights.initialize = function() { + var initPromise = ApproximateTerrainHeights._initPromise; + if (defined(initPromise)) { + return initPromise; + } + + ApproximateTerrainHeights._initPromise = Resource.fetchJson(buildModuleUrl('Assets/approximateTerrainHeights.json')).then(function(json) { + ApproximateTerrainHeights._terrainHeights = json; + }); + + return ApproximateTerrainHeights._initPromise; + }; + + /** + * Computes the minimum and maximum terrain heights for a given rectangle + * @param {Rectangle} rectangle THe bounding rectangle + * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid + * @return {{minimumTerrainHeight: Number, maximumTerrainHeight: Number}} + */ + ApproximateTerrainHeights.getApproximateTerrainHeights = function(rectangle, ellipsoid) { + //>>includeStart('debug', pragmas.debug); + Check.defined('rectangle', rectangle); + if (!defined(ApproximateTerrainHeights._terrainHeights)) { + throw new DeveloperError('You must call ApproximateTerrainHeights.initialize and wait for the promise to resolve before using this function'); + } + //>>includeEnd('debug'); + ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); + + var xyLevel = getTileXYLevel(rectangle); + + // Get the terrain min/max for that tile + var minTerrainHeight = ApproximateTerrainHeights._defaultMinTerrainHeight; + var maxTerrainHeight = ApproximateTerrainHeights._defaultMaxTerrainHeight; + if (defined(xyLevel)) { + var key = xyLevel.level + '-' + xyLevel.x + '-' + xyLevel.y; + var heights = ApproximateTerrainHeights._terrainHeights[key]; + if (defined(heights)) { + minTerrainHeight = heights[0]; + maxTerrainHeight = heights[1]; + } + + // Compute min by taking the center of the NE->SW diagonal and finding distance to the surface + ellipsoid.cartographicToCartesian(Rectangle.northeast(rectangle, scratchDiagonalCartographic), + scratchDiagonalCartesianNE); + ellipsoid.cartographicToCartesian(Rectangle.southwest(rectangle, scratchDiagonalCartographic), + scratchDiagonalCartesianSW); + + Cartesian3.subtract(scratchDiagonalCartesianSW, scratchDiagonalCartesianNE, scratchCenterCartesian); + Cartesian3.add(scratchDiagonalCartesianNE, + Cartesian3.multiplyByScalar(scratchCenterCartesian, 0.5, scratchCenterCartesian), scratchCenterCartesian); + var surfacePosition = ellipsoid.scaleToGeodeticSurface(scratchCenterCartesian, scratchSurfaceCartesian); + if (defined(surfacePosition)) { + var distance = Cartesian3.distance(scratchCenterCartesian, surfacePosition); + minTerrainHeight = Math.min(minTerrainHeight, -distance); + } else { + minTerrainHeight = ApproximateTerrainHeights._defaultMinTerrainHeight; + } + } + + minTerrainHeight = Math.max(ApproximateTerrainHeights._defaultMinTerrainHeight, minTerrainHeight); + + return { + minimumTerrainHeight: minTerrainHeight, + maximumTerrainHeight: maxTerrainHeight + }; + }; + + /** + * Computes the bounding sphere based on the tile heights in the rectangle + * @param {Rectangle} rectangle The bounding rectangle + * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid + * @return {BoundingSphere} The result bounding sphere + */ + ApproximateTerrainHeights.getInstanceBoundingSphere = function(rectangle, ellipsoid) { + //>>includeStart('debug', pragmas.debug); + Check.defined('rectangle', rectangle); + if (!defined(ApproximateTerrainHeights._terrainHeights)) { + throw new DeveloperError('You must call ApproximateTerrainHeights.initialize and wait for the promise to resolve before using this function'); + } + //>>includeEnd('debug'); + ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); + + var xyLevel = getTileXYLevel(rectangle); + + // Get the terrain max for that tile + var maxTerrainHeight = ApproximateTerrainHeights._defaultMaxTerrainHeight; + if (defined(xyLevel)) { + var key = xyLevel.level + '-' + xyLevel.x + '-' + xyLevel.y; + var heights = ApproximateTerrainHeights._terrainHeights[key]; + if (defined(heights)) { + maxTerrainHeight = heights[1]; + } + } + + var result = BoundingSphere.fromRectangle3D(rectangle, ellipsoid, 0.0); + BoundingSphere.fromRectangle3D(rectangle, ellipsoid, maxTerrainHeight, scratchBoundingSphere); + + return BoundingSphere.union(result, scratchBoundingSphere, result); + }; + + function getTileXYLevel(rectangle) { + Cartographic.fromRadians(rectangle.east, rectangle.north, 0.0, scratchCorners[0]); + Cartographic.fromRadians(rectangle.west, rectangle.north, 0.0, scratchCorners[1]); + Cartographic.fromRadians(rectangle.east, rectangle.south, 0.0, scratchCorners[2]); + Cartographic.fromRadians(rectangle.west, rectangle.south, 0.0, scratchCorners[3]); + + // Determine which tile the bounding rectangle is in + var lastLevelX = 0, lastLevelY = 0; + var currentX = 0, currentY = 0; + var maxLevel = ApproximateTerrainHeights._terrainHeightsMaxLevel; + var i; + for(i = 0; i <= maxLevel; ++i) { + var failed = false; + for(var j = 0; j < 4; ++j) { + var corner = scratchCorners[j]; + tilingScheme.positionToTileXY(corner, i, scratchTileXY); + if (j === 0) { + currentX = scratchTileXY.x; + currentY = scratchTileXY.y; + } else if(currentX !== scratchTileXY.x || currentY !== scratchTileXY.y) { + failed = true; + break; + } + } + + if (failed) { + break; + } + + lastLevelX = currentX; + lastLevelY = currentY; + } + + if (i === 0) { + return undefined; + } + + return { + x : lastLevelX, + y : lastLevelY, + level : (i > maxLevel) ? maxLevel : (i - 1) + }; + } + + ApproximateTerrainHeights._terrainHeightsMaxLevel = 6; + ApproximateTerrainHeights._defaultMaxTerrainHeight = 9000.0; + ApproximateTerrainHeights._defaultMinTerrainHeight = -100000.0; + ApproximateTerrainHeights._terrainHeights = undefined; + ApproximateTerrainHeights._initPromise = undefined; + + return ApproximateTerrainHeights; +}); diff --git a/Source/Scene/GroundPrimitive.js b/Source/Scene/GroundPrimitive.js index f1c60d1e2acd..aee9ddf404e5 100644 --- a/Source/Scene/GroundPrimitive.js +++ b/Source/Scene/GroundPrimitive.js @@ -1,4 +1,5 @@ define([ + '../Core/ApproximateTerrainHeights', '../Core/BoundingSphere', '../Core/buildModuleUrl', '../Core/Cartesian2', @@ -28,6 +29,7 @@ define([ './SceneMode', './ShadowVolumeAppearance' ], function( + ApproximateTerrainHeights, BoundingSphere, buildModuleUrl, Cartesian2, @@ -246,8 +248,8 @@ define([ this._maxHeight = undefined; this._minHeight = undefined; - this._maxTerrainHeight = GroundPrimitive._defaultMaxTerrainHeight; - this._minTerrainHeight = GroundPrimitive._defaultMinTerrainHeight; + this._maxTerrainHeight = ApproximateTerrainHeights._defaultMaxTerrainHeight; + this._minTerrainHeight = ApproximateTerrainHeights._defaultMinTerrainHeight; this._boundingSpheresKeys = []; this._boundingSpheres = []; @@ -408,12 +410,6 @@ define([ */ GroundPrimitive.isSupported = ClassificationPrimitive.isSupported; - GroundPrimitive._defaultMaxTerrainHeight = 9000.0; - GroundPrimitive._defaultMinTerrainHeight = -100000.0; - - GroundPrimitive._terrainHeights = undefined; - GroundPrimitive._terrainHeightsMaxLevel = 6; - function getComputeMaximumHeightFunction(primitive) { return function(granularity, ellipsoid) { var r = ellipsoid.maximumRadius; @@ -433,9 +429,6 @@ define([ var scratchBVCartesian = new Cartesian3(); var scratchBVCartographic = new Cartographic(); var scratchBVRectangle = new Rectangle(); - var tilingScheme = new GeographicTilingScheme(); - var scratchCorners = [new Cartographic(), new Cartographic(), new Cartographic(), new Cartographic()]; - var scratchTileXY = new Cartesian2(); function getRectangle(frameState, geometry) { var ellipsoid = frameState.mapProjection.ellipsoid; @@ -482,110 +475,11 @@ define([ return rectangle; } - var scratchDiagonalCartesianNE = new Cartesian3(); - var scratchDiagonalCartesianSW = new Cartesian3(); - var scratchDiagonalCartographic = new Cartographic(); - var scratchCenterCartesian = new Cartesian3(); - var scratchSurfaceCartesian = new Cartesian3(); - - function getTileXYLevel(rectangle) { - Cartographic.fromRadians(rectangle.east, rectangle.north, 0.0, scratchCorners[0]); - Cartographic.fromRadians(rectangle.west, rectangle.north, 0.0, scratchCorners[1]); - Cartographic.fromRadians(rectangle.east, rectangle.south, 0.0, scratchCorners[2]); - Cartographic.fromRadians(rectangle.west, rectangle.south, 0.0, scratchCorners[3]); - - // Determine which tile the bounding rectangle is in - var lastLevelX = 0, lastLevelY = 0; - var currentX = 0, currentY = 0; - var maxLevel = GroundPrimitive._terrainHeightsMaxLevel; - var i; - for(i = 0; i <= maxLevel; ++i) { - var failed = false; - for(var j = 0; j < 4; ++j) { - var corner = scratchCorners[j]; - tilingScheme.positionToTileXY(corner, i, scratchTileXY); - if (j === 0) { - currentX = scratchTileXY.x; - currentY = scratchTileXY.y; - } else if(currentX !== scratchTileXY.x || currentY !== scratchTileXY.y) { - failed = true; - break; - } - } - - if (failed) { - break; - } - - lastLevelX = currentX; - lastLevelY = currentY; - } - - if (i === 0) { - return undefined; - } - - return { - x : lastLevelX, - y : lastLevelY, - level : (i > maxLevel) ? maxLevel : (i - 1) - }; - } - function setMinMaxTerrainHeights(primitive, rectangle, ellipsoid) { - var xyLevel = getTileXYLevel(rectangle); - - // Get the terrain min/max for that tile - var minTerrainHeight = GroundPrimitive._defaultMinTerrainHeight; - var maxTerrainHeight = GroundPrimitive._defaultMaxTerrainHeight; - if (defined(xyLevel)) { - var key = xyLevel.level + '-' + xyLevel.x + '-' + xyLevel.y; - var heights = GroundPrimitive._terrainHeights[key]; - if (defined(heights)) { - minTerrainHeight = heights[0]; - maxTerrainHeight = heights[1]; - } - - // Compute min by taking the center of the NE->SW diagonal and finding distance to the surface - ellipsoid.cartographicToCartesian(Rectangle.northeast(rectangle, scratchDiagonalCartographic), - scratchDiagonalCartesianNE); - ellipsoid.cartographicToCartesian(Rectangle.southwest(rectangle, scratchDiagonalCartographic), - scratchDiagonalCartesianSW); - - Cartesian3.subtract(scratchDiagonalCartesianSW, scratchDiagonalCartesianNE, scratchCenterCartesian); - Cartesian3.add(scratchDiagonalCartesianNE, - Cartesian3.multiplyByScalar(scratchCenterCartesian, 0.5, scratchCenterCartesian), scratchCenterCartesian); - var surfacePosition = ellipsoid.scaleToGeodeticSurface(scratchCenterCartesian, scratchSurfaceCartesian); - if (defined(surfacePosition)) { - var distance = Cartesian3.distance(scratchCenterCartesian, surfacePosition); - minTerrainHeight = Math.min(minTerrainHeight, -distance); - } else { - minTerrainHeight = GroundPrimitive._defaultMinTerrainHeight; - } - } + var result = ApproximateTerrainHeights.getApproximateTerrainHeights(rectangle, ellipsoid); - primitive._minTerrainHeight = Math.max(GroundPrimitive._defaultMinTerrainHeight, minTerrainHeight); - primitive._maxTerrainHeight = maxTerrainHeight; - } - - var scratchBoundingSphere = new BoundingSphere(); - function getInstanceBoundingSphere(rectangle, ellipsoid) { - var xyLevel = getTileXYLevel(rectangle); - - // Get the terrain max for that tile - var maxTerrainHeight = GroundPrimitive._defaultMaxTerrainHeight; - if (defined(xyLevel)) { - var key = xyLevel.level + '-' + xyLevel.x + '-' + xyLevel.y; - var heights = GroundPrimitive._terrainHeights[key]; - if (defined(heights)) { - maxTerrainHeight = heights[1]; - } - } - - var result = BoundingSphere.fromRectangle3D(rectangle, ellipsoid, 0.0); - BoundingSphere.fromRectangle3D(rectangle, ellipsoid, maxTerrainHeight, scratchBoundingSphere); - - return BoundingSphere.union(result, scratchBoundingSphere, result); + primitive._minTerrainHeight = result.minimumTerrainHeight; + primitive._maxTerrainHeight = result.maximumTerrainHeight; } function createBoundingVolume(groundPrimitive, frameState, geometry) { @@ -731,10 +625,10 @@ define([ return initPromise; } - GroundPrimitive._initPromise = Resource.fetchJson(buildModuleUrl('Assets/approximateTerrainHeights.json')).then(function(json) { - GroundPrimitive._initialized = true; - GroundPrimitive._terrainHeights = json; - }); + GroundPrimitive._initPromise = ApproximateTerrainHeights.initialize() + .then(function() { + GroundPrimitive._initialized = true; + }); return GroundPrimitive._initPromise; }; @@ -801,7 +695,7 @@ define([ var id = instance.id; if (defined(id) && defined(instanceRectangle)) { - var boundingSphere = getInstanceBoundingSphere(instanceRectangle, ellipsoid); + var boundingSphere = ApproximateTerrainHeights.getInstanceBoundingSphere(instanceRectangle, ellipsoid); this._boundingSpheresKeys.push(id); this._boundingSpheres.push(boundingSphere); } diff --git a/Specs/Core/ApproximateTerrainHeightsSpec.js b/Specs/Core/ApproximateTerrainHeightsSpec.js new file mode 100644 index 000000000000..df85d432a348 --- /dev/null +++ b/Specs/Core/ApproximateTerrainHeightsSpec.js @@ -0,0 +1,70 @@ +defineSuite([ + 'Core/ApproximateTerrainHeights', + 'Core/Cartesian3', + 'Core/Math', + 'Core/Rectangle' +], function( + ApproximateTerrainHeights, + Cartesian3, + CesiumMath, + Rectangle) { + 'use strict'; + + beforeAll(function() { + return ApproximateTerrainHeights.initialize(); + }); + + afterAll(function() { + ApproximateTerrainHeights._initPromise = undefined; + ApproximateTerrainHeights._terrainHeights = undefined; + }); + + it('initializes', function() { + return ApproximateTerrainHeights.initialize() + .then(function() { + expect(ApproximateTerrainHeights._terrainHeights).toBeDefined(); + }); + }); + + it('getApproximateTerrainHeights computes minimum and maximum terrain heights', function() { + var result = ApproximateTerrainHeights.getApproximateTerrainHeights(Rectangle.fromDegrees(-121.0, 10.0, -120.0, 11.0)); + expect(result.minimumTerrainHeight).toEqualEpsilon(-476.125711887558, CesiumMath.EPSILON10); + expect(result.maximumTerrainHeight).toEqualEpsilon(-28.53441619873047, CesiumMath.EPSILON10); + }); + + it('getApproximateTerrainHeights throws with no rectangle', function() { + expect(function() { + return ApproximateTerrainHeights.getApproximateTerrainHeights(); + }).toThrowDeveloperError(); + }); + + it('getApproximateTerrainHeights throws if ApproximateTerrainHeights was not initialized first', function() { + var heights = ApproximateTerrainHeights._terrainHeights; + ApproximateTerrainHeights._terrainHeights = undefined; + expect(function() { + return ApproximateTerrainHeights.getApproximateTerrainHeights(Rectangle.fromDegrees(-121.0, 10.0, -120.0, 11.0)); + }); + ApproximateTerrainHeights._terrainHeights = heights; + }); + + it('getInstanceBoundingSphere computes a bounding sphere', function() { + var result = ApproximateTerrainHeights.getInstanceBoundingSphere(Rectangle.fromDegrees(-121.0, 10.0, -120.0, 11.0)); + expect(result.center).toEqualEpsilon(new Cartesian3(-3183013.8480289434, -5403772.557261968, 1154581.5817616477), CesiumMath.EPSILON10); + expect(result.radius).toEqualEpsilon(77884.16539096291, CesiumMath.EPSILON10); + }); + + it('getInstanceBoundingSphere throws with no rectangle', function() { + expect(function() { + return ApproximateTerrainHeights.getInstanceBoundingSphere(); + }).toThrowDeveloperError(); + }); + + it('getInstanceBoundingSphere throws if ApproximateTerrainHeights was not initialized first', function() { + var heights = ApproximateTerrainHeights._terrainHeights; + ApproximateTerrainHeights._terrainHeights = undefined; + expect(function() { + return ApproximateTerrainHeights.getInstanceBoundingSphere(Rectangle.fromDegrees(-121.0, 10.0, -120.0, 11.0)); + }); + ApproximateTerrainHeights._terrainHeights = heights; + }); +}); diff --git a/Specs/DataSources/DataSourceDisplaySpec.js b/Specs/DataSources/DataSourceDisplaySpec.js index ed0199b589ac..c19fd7539b81 100644 --- a/Specs/DataSources/DataSourceDisplaySpec.js +++ b/Specs/DataSources/DataSourceDisplaySpec.js @@ -1,5 +1,6 @@ defineSuite([ 'DataSources/DataSourceDisplay', + 'Core/ApproximateTerrainHeights', 'Core/BoundingSphere', 'Core/Cartesian3', 'Core/Iso8601', @@ -11,6 +12,7 @@ defineSuite([ 'Specs/MockDataSource' ], function( DataSourceDisplay, + ApproximateTerrainHeights, BoundingSphere, Cartesian3, Iso8601, @@ -38,7 +40,8 @@ defineSuite([ // Leave ground primitive uninitialized GroundPrimitive._initialized = false; GroundPrimitive._initPromise = undefined; - GroundPrimitive._terrainHeights = undefined; + ApproximateTerrainHeights._initPromise = undefined; + ApproximateTerrainHeights._terrainHeights = undefined; }); afterEach(function() { @@ -358,7 +361,8 @@ defineSuite([ it('verify update returns false till terrain heights are initialized', function() { GroundPrimitive._initialized = false; GroundPrimitive._initPromise = undefined; - GroundPrimitive._terrainHeights = undefined; + ApproximateTerrainHeights._initPromise = undefined; + ApproximateTerrainHeights._terrainHeights = undefined; var source1 = new MockDataSource(); var source2 = new MockDataSource(); diff --git a/Specs/DataSources/GeometryVisualizerSpec.js b/Specs/DataSources/GeometryVisualizerSpec.js index 6130986fbc64..f0fd0c5e426c 100644 --- a/Specs/DataSources/GeometryVisualizerSpec.js +++ b/Specs/DataSources/GeometryVisualizerSpec.js @@ -1,5 +1,6 @@ defineSuite([ 'DataSources/GeometryVisualizer', + 'Core/ApproximateTerrainHeights', 'Core/BoundingSphere', 'Core/Cartesian3', 'Core/Color', @@ -30,6 +31,7 @@ defineSuite([ 'Specs/pollToPromise' ], function( GeometryVisualizer, + ApproximateTerrainHeights, BoundingSphere, Cartesian3, Color, @@ -75,7 +77,9 @@ defineSuite([ // Leave ground primitive uninitialized GroundPrimitive._initialized = false; GroundPrimitive._initPromise = undefined; - GroundPrimitive._terrainHeights = undefined; + ApproximateTerrainHeights._initPromise = undefined; + ApproximateTerrainHeights._terrainHeights = undefined; + }); it('Can create and destroy', function() { diff --git a/Specs/DataSources/StaticGroundGeometryColorBatchSpec.js b/Specs/DataSources/StaticGroundGeometryColorBatchSpec.js index 5ca62376cb9e..24c2293e02b8 100644 --- a/Specs/DataSources/StaticGroundGeometryColorBatchSpec.js +++ b/Specs/DataSources/StaticGroundGeometryColorBatchSpec.js @@ -1,6 +1,7 @@ defineSuite([ 'DataSources/StaticGroundGeometryColorBatch', 'Core/defaultValue', + 'Core/ApproximateTerrainHeights', 'Core/Cartesian3', 'Core/Color', 'Core/DistanceDisplayCondition', @@ -20,6 +21,7 @@ defineSuite([ ], function( StaticGroundGeometryColorBatch, defaultValue, + ApproximateTerrainHeights, Cartesian3, Color, DistanceDisplayCondition, @@ -52,7 +54,8 @@ defineSuite([ // Leave ground primitive uninitialized GroundPrimitive._initialized = false; GroundPrimitive._initPromise = undefined; - GroundPrimitive._terrainHeights = undefined; + ApproximateTerrainHeights._initPromise = undefined; + ApproximateTerrainHeights._terrainHeights = undefined; }); function computeKey(color, zIndex) { diff --git a/Specs/Scene/GroundPrimitiveSpec.js b/Specs/Scene/GroundPrimitiveSpec.js index e8ab745fe93a..bc5effd5f25c 100644 --- a/Specs/Scene/GroundPrimitiveSpec.js +++ b/Specs/Scene/GroundPrimitiveSpec.js @@ -1,5 +1,6 @@ defineSuite([ 'Scene/GroundPrimitive', + 'Core/ApproximateTerrainHeights', 'Core/Color', 'Core/ColorGeometryInstanceAttribute', 'Core/destroyObject', @@ -24,6 +25,7 @@ defineSuite([ 'Specs/pollToPromise' ], function( GroundPrimitive, + ApproximateTerrainHeights, Color, ColorGeometryInstanceAttribute, destroyObject, @@ -77,7 +79,8 @@ defineSuite([ // Leave ground primitive uninitialized GroundPrimitive._initialized = false; GroundPrimitive._initPromise = undefined; - GroundPrimitive._terrainHeights = undefined; + ApproximateTerrainHeights._initPromise = undefined; + ApproximateTerrainHeights._terrainHeights = undefined; }); function MockGlobePrimitive(primitive) {