UNPKG

cesium

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

1,417 lines (1,272 loc) 76.4 kB
/** * Cesium - https://github.com/CesiumGS/cesium * * Copyright 2011-2020 Cesium Contributors * * 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. * * Columbus View (Pat. Pend.) * * Portions licensed separately. * See https://github.com/CesiumGS/cesium/blob/main/LICENSE.md for full licensing details. */ define(['./Transforms-f5d400d6', './Matrix2-57f130bc', './RuntimeError-1349fdaf', './when-4bbc8319', './ComponentDatatype-17ffa790', './ArcType-fc72c06c', './arrayRemoveDuplicates-04f4e20a', './EllipsoidGeodesic-bd191ae8', './EllipsoidRhumbLine-e39900fb', './EncodedCartesian3-7b753db7', './GeometryAttribute-48d0e89b', './IntersectionTests-e14e2851', './Plane-0f8ffca6', './WebMercatorProjection-3b4197b5', './combine-e9466e32', './WebGLConstants-508b9636'], (function (Transforms, Matrix2, RuntimeError, when, ComponentDatatype, ArcType, arrayRemoveDuplicates, EllipsoidGeodesic, EllipsoidRhumbLine, EncodedCartesian3, GeometryAttribute, IntersectionTests, Plane, WebMercatorProjection, combine, WebGLConstants) { 'use strict'; /** * A tiling scheme for geometry referenced to a simple {@link GeographicProjection} where * longitude and latitude are directly mapped to X and Y. This projection is commonly * known as geographic, equirectangular, equidistant cylindrical, or plate carrée. * * @alias GeographicTilingScheme * @constructor * * @param {Object} [options] Object with the following properties: * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid whose surface is being tiled. Defaults to * the WGS84 ellipsoid. * @param {Rectangle} [options.rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the tiling scheme. * @param {Number} [options.numberOfLevelZeroTilesX=2] The number of tiles in the X direction at level zero of * the tile tree. * @param {Number} [options.numberOfLevelZeroTilesY=1] The number of tiles in the Y direction at level zero of * the tile tree. */ function GeographicTilingScheme(options) { options = when.defaultValue(options, when.defaultValue.EMPTY_OBJECT); this._ellipsoid = when.defaultValue(options.ellipsoid, Matrix2.Ellipsoid.WGS84); this._rectangle = when.defaultValue(options.rectangle, Matrix2.Rectangle.MAX_VALUE); this._projection = new Transforms.GeographicProjection(this._ellipsoid); this._numberOfLevelZeroTilesX = when.defaultValue( options.numberOfLevelZeroTilesX, 2 ); this._numberOfLevelZeroTilesY = when.defaultValue( options.numberOfLevelZeroTilesY, 1 ); } Object.defineProperties(GeographicTilingScheme.prototype, { /** * Gets the ellipsoid that is tiled by this tiling scheme. * @memberof GeographicTilingScheme.prototype * @type {Ellipsoid} */ ellipsoid: { get: function () { return this._ellipsoid; }, }, /** * Gets the rectangle, in radians, covered by this tiling scheme. * @memberof GeographicTilingScheme.prototype * @type {Rectangle} */ rectangle: { get: function () { return this._rectangle; }, }, /** * Gets the map projection used by this tiling scheme. * @memberof GeographicTilingScheme.prototype * @type {MapProjection} */ projection: { get: function () { return this._projection; }, }, }); /** * Gets the total number of tiles in the X direction at a specified level-of-detail. * * @param {Number} level The level-of-detail. * @returns {Number} The number of tiles in the X direction at the given level. */ GeographicTilingScheme.prototype.getNumberOfXTilesAtLevel = function (level) { return this._numberOfLevelZeroTilesX << level; }; /** * Gets the total number of tiles in the Y direction at a specified level-of-detail. * * @param {Number} level The level-of-detail. * @returns {Number} The number of tiles in the Y direction at the given level. */ GeographicTilingScheme.prototype.getNumberOfYTilesAtLevel = function (level) { return this._numberOfLevelZeroTilesY << level; }; /** * Transforms a rectangle specified in geodetic radians to the native coordinate system * of this tiling scheme. * * @param {Rectangle} rectangle The rectangle to transform. * @param {Rectangle} [result] The instance to which to copy the result, or undefined if a new instance * should be created. * @returns {Rectangle} The specified 'result', or a new object containing the native rectangle if 'result' * is undefined. */ GeographicTilingScheme.prototype.rectangleToNativeRectangle = function ( rectangle, result ) { //>>includeStart('debug', pragmas.debug); RuntimeError.Check.defined("rectangle", rectangle); //>>includeEnd('debug'); const west = ComponentDatatype.CesiumMath.toDegrees(rectangle.west); const south = ComponentDatatype.CesiumMath.toDegrees(rectangle.south); const east = ComponentDatatype.CesiumMath.toDegrees(rectangle.east); const north = ComponentDatatype.CesiumMath.toDegrees(rectangle.north); if (!when.defined(result)) { return new Matrix2.Rectangle(west, south, east, north); } result.west = west; result.south = south; result.east = east; result.north = north; return result; }; /** * Converts tile x, y coordinates and level to a rectangle expressed in the native coordinates * of the tiling scheme. * * @param {Number} x The integer x coordinate of the tile. * @param {Number} y The integer y coordinate of the tile. * @param {Number} level The tile level-of-detail. Zero is the least detailed. * @param {Object} [result] The instance to which to copy the result, or undefined if a new instance * should be created. * @returns {Rectangle} The specified 'result', or a new object containing the rectangle * if 'result' is undefined. */ GeographicTilingScheme.prototype.tileXYToNativeRectangle = function ( x, y, level, result ) { const rectangleRadians = this.tileXYToRectangle(x, y, level, result); rectangleRadians.west = ComponentDatatype.CesiumMath.toDegrees(rectangleRadians.west); rectangleRadians.south = ComponentDatatype.CesiumMath.toDegrees(rectangleRadians.south); rectangleRadians.east = ComponentDatatype.CesiumMath.toDegrees(rectangleRadians.east); rectangleRadians.north = ComponentDatatype.CesiumMath.toDegrees(rectangleRadians.north); return rectangleRadians; }; /** * Converts tile x, y coordinates and level to a cartographic rectangle in radians. * * @param {Number} x The integer x coordinate of the tile. * @param {Number} y The integer y coordinate of the tile. * @param {Number} level The tile level-of-detail. Zero is the least detailed. * @param {Object} [result] The instance to which to copy the result, or undefined if a new instance * should be created. * @returns {Rectangle} The specified 'result', or a new object containing the rectangle * if 'result' is undefined. */ GeographicTilingScheme.prototype.tileXYToRectangle = function ( x, y, level, result ) { const rectangle = this._rectangle; const xTiles = this.getNumberOfXTilesAtLevel(level); const yTiles = this.getNumberOfYTilesAtLevel(level); const xTileWidth = rectangle.width / xTiles; const west = x * xTileWidth + rectangle.west; const east = (x + 1) * xTileWidth + rectangle.west; const yTileHeight = rectangle.height / yTiles; const north = rectangle.north - y * yTileHeight; const south = rectangle.north - (y + 1) * yTileHeight; if (!when.defined(result)) { result = new Matrix2.Rectangle(west, south, east, north); } result.west = west; result.south = south; result.east = east; result.north = north; return result; }; /** * Calculates the tile x, y coordinates of the tile containing * a given cartographic position. * * @param {Cartographic} position The position. * @param {Number} level The tile level-of-detail. Zero is the least detailed. * @param {Cartesian2} [result] The instance to which to copy the result, or undefined if a new instance * should be created. * @returns {Cartesian2} The specified 'result', or a new object containing the tile x, y coordinates * if 'result' is undefined. */ GeographicTilingScheme.prototype.positionToTileXY = function ( position, level, result ) { const rectangle = this._rectangle; if (!Matrix2.Rectangle.contains(rectangle, position)) { // outside the bounds of the tiling scheme return undefined; } const xTiles = this.getNumberOfXTilesAtLevel(level); const yTiles = this.getNumberOfYTilesAtLevel(level); const xTileWidth = rectangle.width / xTiles; const yTileHeight = rectangle.height / yTiles; let longitude = position.longitude; if (rectangle.east < rectangle.west) { longitude += ComponentDatatype.CesiumMath.TWO_PI; } let xTileCoordinate = ((longitude - rectangle.west) / xTileWidth) | 0; if (xTileCoordinate >= xTiles) { xTileCoordinate = xTiles - 1; } let yTileCoordinate = ((rectangle.north - position.latitude) / yTileHeight) | 0; if (yTileCoordinate >= yTiles) { yTileCoordinate = yTiles - 1; } if (!when.defined(result)) { return new Matrix2.Cartesian2(xTileCoordinate, yTileCoordinate); } result.x = xTileCoordinate; result.y = yTileCoordinate; return result; }; const scratchDiagonalCartesianNE = new Matrix2.Cartesian3(); const scratchDiagonalCartesianSW = new Matrix2.Cartesian3(); const scratchDiagonalCartographic = new Matrix2.Cartographic(); const scratchCenterCartesian = new Matrix2.Cartesian3(); const scratchSurfaceCartesian = new Matrix2.Cartesian3(); const scratchBoundingSphere = new Transforms.BoundingSphere(); const tilingScheme = new GeographicTilingScheme(); const scratchCorners = [ new Matrix2.Cartographic(), new Matrix2.Cartographic(), new Matrix2.Cartographic(), new Matrix2.Cartographic(), ]; const scratchTileXY = new Matrix2.Cartesian2(); /** * A collection of functions for approximating terrain height * @private */ const ApproximateTerrainHeights = {}; /** * Initializes the minimum and maximum terrain heights * @return {Promise<void>} */ ApproximateTerrainHeights.initialize = function () { let initPromise = ApproximateTerrainHeights._initPromise; if (when.defined(initPromise)) { return initPromise; } initPromise = Transforms.Resource.fetchJson( Transforms.buildModuleUrl("Assets/approximateTerrainHeights.json") ).then(function (json) { ApproximateTerrainHeights._terrainHeights = json; }); ApproximateTerrainHeights._initPromise = initPromise; return 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.getMinimumMaximumHeights = function ( rectangle, ellipsoid ) { //>>includeStart('debug', pragmas.debug); RuntimeError.Check.defined("rectangle", rectangle); if (!when.defined(ApproximateTerrainHeights._terrainHeights)) { throw new RuntimeError.DeveloperError( "You must call ApproximateTerrainHeights.initialize and wait for the promise to resolve before using this function" ); } //>>includeEnd('debug'); ellipsoid = when.defaultValue(ellipsoid, Matrix2.Ellipsoid.WGS84); const xyLevel = getTileXYLevel(rectangle); // Get the terrain min/max for that tile let minTerrainHeight = ApproximateTerrainHeights._defaultMinTerrainHeight; let maxTerrainHeight = ApproximateTerrainHeights._defaultMaxTerrainHeight; if (when.defined(xyLevel)) { const key = xyLevel.level + "-" + xyLevel.x + "-" + xyLevel.y; const heights = ApproximateTerrainHeights._terrainHeights[key]; if (when.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( Matrix2.Rectangle.northeast(rectangle, scratchDiagonalCartographic), scratchDiagonalCartesianNE ); ellipsoid.cartographicToCartesian( Matrix2.Rectangle.southwest(rectangle, scratchDiagonalCartographic), scratchDiagonalCartesianSW ); Matrix2.Cartesian3.midpoint( scratchDiagonalCartesianSW, scratchDiagonalCartesianNE, scratchCenterCartesian ); const surfacePosition = ellipsoid.scaleToGeodeticSurface( scratchCenterCartesian, scratchSurfaceCartesian ); if (when.defined(surfacePosition)) { const distance = Matrix2.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.getBoundingSphere = function (rectangle, ellipsoid) { //>>includeStart('debug', pragmas.debug); RuntimeError.Check.defined("rectangle", rectangle); if (!when.defined(ApproximateTerrainHeights._terrainHeights)) { throw new RuntimeError.DeveloperError( "You must call ApproximateTerrainHeights.initialize and wait for the promise to resolve before using this function" ); } //>>includeEnd('debug'); ellipsoid = when.defaultValue(ellipsoid, Matrix2.Ellipsoid.WGS84); const xyLevel = getTileXYLevel(rectangle); // Get the terrain max for that tile let maxTerrainHeight = ApproximateTerrainHeights._defaultMaxTerrainHeight; if (when.defined(xyLevel)) { const key = xyLevel.level + "-" + xyLevel.x + "-" + xyLevel.y; const heights = ApproximateTerrainHeights._terrainHeights[key]; if (when.defined(heights)) { maxTerrainHeight = heights[1]; } } const result = Transforms.BoundingSphere.fromRectangle3D(rectangle, ellipsoid, 0.0); Transforms.BoundingSphere.fromRectangle3D( rectangle, ellipsoid, maxTerrainHeight, scratchBoundingSphere ); return Transforms.BoundingSphere.union(result, scratchBoundingSphere, result); }; function getTileXYLevel(rectangle) { Matrix2.Cartographic.fromRadians( rectangle.east, rectangle.north, 0.0, scratchCorners[0] ); Matrix2.Cartographic.fromRadians( rectangle.west, rectangle.north, 0.0, scratchCorners[1] ); Matrix2.Cartographic.fromRadians( rectangle.east, rectangle.south, 0.0, scratchCorners[2] ); Matrix2.Cartographic.fromRadians( rectangle.west, rectangle.south, 0.0, scratchCorners[3] ); // Determine which tile the bounding rectangle is in let lastLevelX = 0, lastLevelY = 0; let currentX = 0, currentY = 0; const maxLevel = ApproximateTerrainHeights._terrainHeightsMaxLevel; let i; for (i = 0; i <= maxLevel; ++i) { let failed = false; for (let j = 0; j < 4; ++j) { const 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; Object.defineProperties(ApproximateTerrainHeights, { /** * Determines if the terrain heights are initialized and ready to use. To initialize the terrain heights, * call {@link ApproximateTerrainHeights#initialize} and wait for the returned promise to resolve. * @type {Boolean} * @readonly * @memberof ApproximateTerrainHeights */ initialized: { get: function () { return when.defined(ApproximateTerrainHeights._terrainHeights); }, }, }); const PROJECTIONS = [Transforms.GeographicProjection, WebMercatorProjection.WebMercatorProjection]; const PROJECTION_COUNT = PROJECTIONS.length; const MITER_BREAK_SMALL = Math.cos(ComponentDatatype.CesiumMath.toRadians(30.0)); const MITER_BREAK_LARGE = Math.cos(ComponentDatatype.CesiumMath.toRadians(150.0)); // Initial heights for constructing the wall. // Keeping WALL_INITIAL_MIN_HEIGHT near the ellipsoid surface helps // prevent precision problems with planes in the shader. // Putting the start point of a plane at ApproximateTerrainHeights._defaultMinTerrainHeight, // which is a highly conservative bound, usually puts the plane origin several thousands // of meters away from the actual terrain, causing floating point problems when checking // fragments on terrain against the plane. // Ellipsoid height is generally much closer. // The initial max height is arbitrary. // Both heights are corrected using ApproximateTerrainHeights for computing the actual volume geometry. const WALL_INITIAL_MIN_HEIGHT = 0.0; const WALL_INITIAL_MAX_HEIGHT = 1000.0; /** * A description of a polyline on terrain or 3D Tiles. Only to be used with {@link GroundPolylinePrimitive}. * * @alias GroundPolylineGeometry * @constructor * * @param {Object} options Options with the following properties: * @param {Cartesian3[]} options.positions An array of {@link Cartesian3} defining the polyline's points. Heights above the ellipsoid will be ignored. * @param {Number} [options.width=1.0] The screen space width in pixels. * @param {Number} [options.granularity=9999.0] The distance interval in meters used for interpolating options.points. Defaults to 9999.0 meters. Zero indicates no interpolation. * @param {Boolean} [options.loop=false] Whether during geometry creation a line segment will be added between the last and first line positions to make this Polyline a loop. * @param {ArcType} [options.arcType=ArcType.GEODESIC] The type of line the polyline segments must follow. Valid options are {@link ArcType.GEODESIC} and {@link ArcType.RHUMB}. * * @exception {DeveloperError} At least two positions are required. * * @see GroundPolylinePrimitive * * @example * const positions = Cesium.Cartesian3.fromDegreesArray([ * -112.1340164450331, 36.05494287836128, * -112.08821010582645, 36.097804071380715, * -112.13296079730024, 36.168769146801104 * ]); * * const geometry = new Cesium.GroundPolylineGeometry({ * positions : positions * }); */ function GroundPolylineGeometry(options) { options = when.defaultValue(options, when.defaultValue.EMPTY_OBJECT); const positions = options.positions; //>>includeStart('debug', pragmas.debug); if (!when.defined(positions) || positions.length < 2) { throw new RuntimeError.DeveloperError("At least two positions are required."); } if ( when.defined(options.arcType) && options.arcType !== ArcType.ArcType.GEODESIC && options.arcType !== ArcType.ArcType.RHUMB ) { throw new RuntimeError.DeveloperError( "Valid options for arcType are ArcType.GEODESIC and ArcType.RHUMB." ); } //>>includeEnd('debug'); /** * The screen space width in pixels. * @type {Number} */ this.width = when.defaultValue(options.width, 1.0); // Doesn't get packed, not necessary for computing geometry. this._positions = positions; /** * The distance interval used for interpolating options.points. Zero indicates no interpolation. * Default of 9999.0 allows centimeter accuracy with 32 bit floating point. * @type {Boolean} * @default 9999.0 */ this.granularity = when.defaultValue(options.granularity, 9999.0); /** * Whether during geometry creation a line segment will be added between the last and first line positions to make this Polyline a loop. * If the geometry has two positions this parameter will be ignored. * @type {Boolean} * @default false */ this.loop = when.defaultValue(options.loop, false); /** * The type of path the polyline must follow. Valid options are {@link ArcType.GEODESIC} and {@link ArcType.RHUMB}. * @type {ArcType} * @default ArcType.GEODESIC */ this.arcType = when.defaultValue(options.arcType, ArcType.ArcType.GEODESIC); this._ellipsoid = Matrix2.Ellipsoid.WGS84; // MapProjections can't be packed, so store the index to a known MapProjection. this._projectionIndex = 0; this._workerName = "createGroundPolylineGeometry"; // Used by GroundPolylinePrimitive to signal worker that scenemode is 3D only. this._scene3DOnly = false; } Object.defineProperties(GroundPolylineGeometry.prototype, { /** * The number of elements used to pack the object into an array. * @memberof GroundPolylineGeometry.prototype * @type {Number} * @readonly * @private */ packedLength: { get: function () { return ( 1.0 + this._positions.length * 3 + 1.0 + 1.0 + 1.0 + Matrix2.Ellipsoid.packedLength + 1.0 + 1.0 ); }, }, }); /** * Set the GroundPolylineGeometry's projection and ellipsoid. * Used by GroundPolylinePrimitive to signal scene information to the geometry for generating 2D attributes. * * @param {GroundPolylineGeometry} groundPolylineGeometry GroundPolylinGeometry describing a polyline on terrain or 3D Tiles. * @param {Projection} mapProjection A MapProjection used for projecting cartographic coordinates to 2D. * @private */ GroundPolylineGeometry.setProjectionAndEllipsoid = function ( groundPolylineGeometry, mapProjection ) { let projectionIndex = 0; for (let i = 0; i < PROJECTION_COUNT; i++) { if (mapProjection instanceof PROJECTIONS[i]) { projectionIndex = i; break; } } groundPolylineGeometry._projectionIndex = projectionIndex; groundPolylineGeometry._ellipsoid = mapProjection.ellipsoid; }; const cart3Scratch1 = new Matrix2.Cartesian3(); const cart3Scratch2 = new Matrix2.Cartesian3(); const cart3Scratch3 = new Matrix2.Cartesian3(); function computeRightNormal(start, end, maxHeight, ellipsoid, result) { const startBottom = getPosition(ellipsoid, start, 0.0, cart3Scratch1); const startTop = getPosition(ellipsoid, start, maxHeight, cart3Scratch2); const endBottom = getPosition(ellipsoid, end, 0.0, cart3Scratch3); const up = direction(startTop, startBottom, cart3Scratch2); const forward = direction(endBottom, startBottom, cart3Scratch3); Matrix2.Cartesian3.cross(forward, up, result); return Matrix2.Cartesian3.normalize(result, result); } const interpolatedCartographicScratch = new Matrix2.Cartographic(); const interpolatedBottomScratch = new Matrix2.Cartesian3(); const interpolatedTopScratch = new Matrix2.Cartesian3(); const interpolatedNormalScratch = new Matrix2.Cartesian3(); function interpolateSegment( start, end, minHeight, maxHeight, granularity, arcType, ellipsoid, normalsArray, bottomPositionsArray, topPositionsArray, cartographicsArray ) { if (granularity === 0.0) { return; } let ellipsoidLine; if (arcType === ArcType.ArcType.GEODESIC) { ellipsoidLine = new EllipsoidGeodesic.EllipsoidGeodesic(start, end, ellipsoid); } else if (arcType === ArcType.ArcType.RHUMB) { ellipsoidLine = new EllipsoidRhumbLine.EllipsoidRhumbLine(start, end, ellipsoid); } const surfaceDistance = ellipsoidLine.surfaceDistance; if (surfaceDistance < granularity) { return; } // Compute rightwards normal applicable at all interpolated points const interpolatedNormal = computeRightNormal( start, end, maxHeight, ellipsoid, interpolatedNormalScratch ); const segments = Math.ceil(surfaceDistance / granularity); const interpointDistance = surfaceDistance / segments; let distanceFromStart = interpointDistance; const pointsToAdd = segments - 1; let packIndex = normalsArray.length; for (let i = 0; i < pointsToAdd; i++) { const interpolatedCartographic = ellipsoidLine.interpolateUsingSurfaceDistance( distanceFromStart, interpolatedCartographicScratch ); const interpolatedBottom = getPosition( ellipsoid, interpolatedCartographic, minHeight, interpolatedBottomScratch ); const interpolatedTop = getPosition( ellipsoid, interpolatedCartographic, maxHeight, interpolatedTopScratch ); Matrix2.Cartesian3.pack(interpolatedNormal, normalsArray, packIndex); Matrix2.Cartesian3.pack(interpolatedBottom, bottomPositionsArray, packIndex); Matrix2.Cartesian3.pack(interpolatedTop, topPositionsArray, packIndex); cartographicsArray.push(interpolatedCartographic.latitude); cartographicsArray.push(interpolatedCartographic.longitude); packIndex += 3; distanceFromStart += interpointDistance; } } const heightlessCartographicScratch = new Matrix2.Cartographic(); function getPosition(ellipsoid, cartographic, height, result) { Matrix2.Cartographic.clone(cartographic, heightlessCartographicScratch); heightlessCartographicScratch.height = height; return Matrix2.Cartographic.toCartesian( heightlessCartographicScratch, ellipsoid, result ); } /** * Stores the provided instance into the provided array. * * @param {PolygonGeometry} value The value to pack. * @param {Number[]} array The array to pack into. * @param {Number} [startingIndex=0] The index into the array at which to start packing the elements. * * @returns {Number[]} The array that was packed into */ GroundPolylineGeometry.pack = function (value, array, startingIndex) { //>>includeStart('debug', pragmas.debug); RuntimeError.Check.typeOf.object("value", value); RuntimeError.Check.defined("array", array); //>>includeEnd('debug'); let index = when.defaultValue(startingIndex, 0); const positions = value._positions; const positionsLength = positions.length; array[index++] = positionsLength; for (let i = 0; i < positionsLength; ++i) { const cartesian = positions[i]; Matrix2.Cartesian3.pack(cartesian, array, index); index += 3; } array[index++] = value.granularity; array[index++] = value.loop ? 1.0 : 0.0; array[index++] = value.arcType; Matrix2.Ellipsoid.pack(value._ellipsoid, array, index); index += Matrix2.Ellipsoid.packedLength; array[index++] = value._projectionIndex; array[index++] = value._scene3DOnly ? 1.0 : 0.0; return array; }; /** * Retrieves an instance from a packed array. * * @param {Number[]} array The packed array. * @param {Number} [startingIndex=0] The starting index of the element to be unpacked. * @param {PolygonGeometry} [result] The object into which to store the result. */ GroundPolylineGeometry.unpack = function (array, startingIndex, result) { //>>includeStart('debug', pragmas.debug); RuntimeError.Check.defined("array", array); //>>includeEnd('debug'); let index = when.defaultValue(startingIndex, 0); const positionsLength = array[index++]; const positions = new Array(positionsLength); for (let i = 0; i < positionsLength; i++) { positions[i] = Matrix2.Cartesian3.unpack(array, index); index += 3; } const granularity = array[index++]; const loop = array[index++] === 1.0; const arcType = array[index++]; const ellipsoid = Matrix2.Ellipsoid.unpack(array, index); index += Matrix2.Ellipsoid.packedLength; const projectionIndex = array[index++]; const scene3DOnly = array[index++] === 1.0; if (!when.defined(result)) { result = new GroundPolylineGeometry({ positions: positions, }); } result._positions = positions; result.granularity = granularity; result.loop = loop; result.arcType = arcType; result._ellipsoid = ellipsoid; result._projectionIndex = projectionIndex; result._scene3DOnly = scene3DOnly; return result; }; function direction(target, origin, result) { Matrix2.Cartesian3.subtract(target, origin, result); Matrix2.Cartesian3.normalize(result, result); return result; } function tangentDirection(target, origin, up, result) { result = direction(target, origin, result); // orthogonalize result = Matrix2.Cartesian3.cross(result, up, result); result = Matrix2.Cartesian3.normalize(result, result); result = Matrix2.Cartesian3.cross(up, result, result); return result; } const toPreviousScratch = new Matrix2.Cartesian3(); const toNextScratch = new Matrix2.Cartesian3(); const forwardScratch = new Matrix2.Cartesian3(); const vertexUpScratch = new Matrix2.Cartesian3(); const cosine90 = 0.0; const cosine180 = -1.0; function computeVertexMiterNormal( previousBottom, vertexBottom, vertexTop, nextBottom, result ) { const up = direction(vertexTop, vertexBottom, vertexUpScratch); // Compute vectors pointing towards neighboring points but tangent to this point on the ellipsoid const toPrevious = tangentDirection( previousBottom, vertexBottom, up, toPreviousScratch ); const toNext = tangentDirection(nextBottom, vertexBottom, up, toNextScratch); // Check if tangents are almost opposite - if so, no need to miter. if ( ComponentDatatype.CesiumMath.equalsEpsilon( Matrix2.Cartesian3.dot(toPrevious, toNext), cosine180, ComponentDatatype.CesiumMath.EPSILON5 ) ) { result = Matrix2.Cartesian3.cross(up, toPrevious, result); result = Matrix2.Cartesian3.normalize(result, result); return result; } // Average directions to previous and to next in the plane of Up result = Matrix2.Cartesian3.add(toNext, toPrevious, result); result = Matrix2.Cartesian3.normalize(result, result); // Flip the normal if it isn't pointing roughly bound right (aka if forward is pointing more "backwards") const forward = Matrix2.Cartesian3.cross(up, result, forwardScratch); if (Matrix2.Cartesian3.dot(toNext, forward) < cosine90) { result = Matrix2.Cartesian3.negate(result, result); } return result; } const XZ_PLANE = Plane.Plane.fromPointNormal(Matrix2.Cartesian3.ZERO, Matrix2.Cartesian3.UNIT_Y); const previousBottomScratch = new Matrix2.Cartesian3(); const vertexBottomScratch = new Matrix2.Cartesian3(); const vertexTopScratch = new Matrix2.Cartesian3(); const nextBottomScratch = new Matrix2.Cartesian3(); const vertexNormalScratch = new Matrix2.Cartesian3(); const intersectionScratch = new Matrix2.Cartesian3(); const cartographicScratch0 = new Matrix2.Cartographic(); const cartographicScratch1 = new Matrix2.Cartographic(); const cartographicIntersectionScratch = new Matrix2.Cartographic(); /** * Computes shadow volumes for the ground polyline, consisting of its vertices, indices, and a bounding sphere. * Vertices are "fat," packing all the data needed in each volume to describe a line on terrain or 3D Tiles. * Should not be called independent of {@link GroundPolylinePrimitive}. * * @param {GroundPolylineGeometry} groundPolylineGeometry * @private */ GroundPolylineGeometry.createGeometry = function (groundPolylineGeometry) { const compute2dAttributes = !groundPolylineGeometry._scene3DOnly; let loop = groundPolylineGeometry.loop; const ellipsoid = groundPolylineGeometry._ellipsoid; const granularity = groundPolylineGeometry.granularity; const arcType = groundPolylineGeometry.arcType; const projection = new PROJECTIONS[groundPolylineGeometry._projectionIndex]( ellipsoid ); const minHeight = WALL_INITIAL_MIN_HEIGHT; const maxHeight = WALL_INITIAL_MAX_HEIGHT; let index; let i; const positions = groundPolylineGeometry._positions; const positionsLength = positions.length; if (positionsLength === 2) { loop = false; } // Split positions across the IDL and the Prime Meridian as well. // Split across prime meridian because very large geometries crossing the Prime Meridian but not the IDL // may get split by the plane of IDL + Prime Meridian. let p0; let p1; let c0; let c1; const rhumbLine = new EllipsoidRhumbLine.EllipsoidRhumbLine(undefined, undefined, ellipsoid); let intersection; let intersectionCartographic; let intersectionLongitude; const splitPositions = [positions[0]]; for (i = 0; i < positionsLength - 1; i++) { p0 = positions[i]; p1 = positions[i + 1]; intersection = IntersectionTests.IntersectionTests.lineSegmentPlane( p0, p1, XZ_PLANE, intersectionScratch ); if ( when.defined(intersection) && !Matrix2.Cartesian3.equalsEpsilon(intersection, p0, ComponentDatatype.CesiumMath.EPSILON7) && !Matrix2.Cartesian3.equalsEpsilon(intersection, p1, ComponentDatatype.CesiumMath.EPSILON7) ) { if (groundPolylineGeometry.arcType === ArcType.ArcType.GEODESIC) { splitPositions.push(Matrix2.Cartesian3.clone(intersection)); } else if (groundPolylineGeometry.arcType === ArcType.ArcType.RHUMB) { intersectionLongitude = ellipsoid.cartesianToCartographic( intersection, cartographicScratch0 ).longitude; c0 = ellipsoid.cartesianToCartographic(p0, cartographicScratch0); c1 = ellipsoid.cartesianToCartographic(p1, cartographicScratch1); rhumbLine.setEndPoints(c0, c1); intersectionCartographic = rhumbLine.findIntersectionWithLongitude( intersectionLongitude, cartographicIntersectionScratch ); intersection = ellipsoid.cartographicToCartesian( intersectionCartographic, intersectionScratch ); if ( when.defined(intersection) && !Matrix2.Cartesian3.equalsEpsilon(intersection, p0, ComponentDatatype.CesiumMath.EPSILON7) && !Matrix2.Cartesian3.equalsEpsilon(intersection, p1, ComponentDatatype.CesiumMath.EPSILON7) ) { splitPositions.push(Matrix2.Cartesian3.clone(intersection)); } } } splitPositions.push(p1); } if (loop) { p0 = positions[positionsLength - 1]; p1 = positions[0]; intersection = IntersectionTests.IntersectionTests.lineSegmentPlane( p0, p1, XZ_PLANE, intersectionScratch ); if ( when.defined(intersection) && !Matrix2.Cartesian3.equalsEpsilon(intersection, p0, ComponentDatatype.CesiumMath.EPSILON7) && !Matrix2.Cartesian3.equalsEpsilon(intersection, p1, ComponentDatatype.CesiumMath.EPSILON7) ) { if (groundPolylineGeometry.arcType === ArcType.ArcType.GEODESIC) { splitPositions.push(Matrix2.Cartesian3.clone(intersection)); } else if (groundPolylineGeometry.arcType === ArcType.ArcType.RHUMB) { intersectionLongitude = ellipsoid.cartesianToCartographic( intersection, cartographicScratch0 ).longitude; c0 = ellipsoid.cartesianToCartographic(p0, cartographicScratch0); c1 = ellipsoid.cartesianToCartographic(p1, cartographicScratch1); rhumbLine.setEndPoints(c0, c1); intersectionCartographic = rhumbLine.findIntersectionWithLongitude( intersectionLongitude, cartographicIntersectionScratch ); intersection = ellipsoid.cartographicToCartesian( intersectionCartographic, intersectionScratch ); if ( when.defined(intersection) && !Matrix2.Cartesian3.equalsEpsilon(intersection, p0, ComponentDatatype.CesiumMath.EPSILON7) && !Matrix2.Cartesian3.equalsEpsilon(intersection, p1, ComponentDatatype.CesiumMath.EPSILON7) ) { splitPositions.push(Matrix2.Cartesian3.clone(intersection)); } } } } let cartographicsLength = splitPositions.length; let cartographics = new Array(cartographicsLength); for (i = 0; i < cartographicsLength; i++) { const cartographic = Matrix2.Cartographic.fromCartesian( splitPositions[i], ellipsoid ); cartographic.height = 0.0; cartographics[i] = cartographic; } cartographics = arrayRemoveDuplicates.arrayRemoveDuplicates( cartographics, Matrix2.Cartographic.equalsEpsilon ); cartographicsLength = cartographics.length; if (cartographicsLength < 2) { return undefined; } /**** Build heap-side arrays for positions, interpolated cartographics, and normals from which to compute vertices ****/ // We build a "wall" and then decompose it into separately connected component "volumes" because we need a lot // of information about the wall. Also, this simplifies interpolation. // Convention: "next" and "end" are locally forward to each segment of the wall, // and we are computing normals pointing towards the local right side of the vertices in each segment. const cartographicsArray = []; const normalsArray = []; const bottomPositionsArray = []; const topPositionsArray = []; let previousBottom = previousBottomScratch; let vertexBottom = vertexBottomScratch; let vertexTop = vertexTopScratch; let nextBottom = nextBottomScratch; let vertexNormal = vertexNormalScratch; // First point - either loop or attach a "perpendicular" normal const startCartographic = cartographics[0]; const nextCartographic = cartographics[1]; const prestartCartographic = cartographics[cartographicsLength - 1]; previousBottom = getPosition( ellipsoid, prestartCartographic, minHeight, previousBottom ); nextBottom = getPosition(ellipsoid, nextCartographic, minHeight, nextBottom); vertexBottom = getPosition( ellipsoid, startCartographic, minHeight, vertexBottom ); vertexTop = getPosition(ellipsoid, startCartographic, maxHeight, vertexTop); if (loop) { vertexNormal = computeVertexMiterNormal( previousBottom, vertexBottom, vertexTop, nextBottom, vertexNormal ); } else { vertexNormal = computeRightNormal( startCartographic, nextCartographic, maxHeight, ellipsoid, vertexNormal ); } Matrix2.Cartesian3.pack(vertexNormal, normalsArray, 0); Matrix2.Cartesian3.pack(vertexBottom, bottomPositionsArray, 0); Matrix2.Cartesian3.pack(vertexTop, topPositionsArray, 0); cartographicsArray.push(startCartographic.latitude); cartographicsArray.push(startCartographic.longitude); interpolateSegment( startCartographic, nextCartographic, minHeight, maxHeight, granularity, arcType, ellipsoid, normalsArray, bottomPositionsArray, topPositionsArray, cartographicsArray ); // All inbetween points for (i = 1; i < cartographicsLength - 1; ++i) { previousBottom = Matrix2.Cartesian3.clone(vertexBottom, previousBottom); vertexBottom = Matrix2.Cartesian3.clone(nextBottom, vertexBottom); const vertexCartographic = cartographics[i]; getPosition(ellipsoid, vertexCartographic, maxHeight, vertexTop); getPosition(ellipsoid, cartographics[i + 1], minHeight, nextBottom); computeVertexMiterNormal( previousBottom, vertexBottom, vertexTop, nextBottom, vertexNormal ); index = normalsArray.length; Matrix2.Cartesian3.pack(vertexNormal, normalsArray, index); Matrix2.Cartesian3.pack(vertexBottom, bottomPositionsArray, index); Matrix2.Cartesian3.pack(vertexTop, topPositionsArray, index); cartographicsArray.push(vertexCartographic.latitude); cartographicsArray.push(vertexCartographic.longitude); interpolateSegment( cartographics[i], cartographics[i + 1], minHeight, maxHeight, granularity, arcType, ellipsoid, normalsArray, bottomPositionsArray, topPositionsArray, cartographicsArray ); } // Last point - either loop or attach a normal "perpendicular" to the wall. const endCartographic = cartographics[cartographicsLength - 1]; const preEndCartographic = cartographics[cartographicsLength - 2]; vertexBottom = getPosition( ellipsoid, endCartographic, minHeight, vertexBottom ); vertexTop = getPosition(ellipsoid, endCartographic, maxHeight, vertexTop); if (loop) { const postEndCartographic = cartographics[0]; previousBottom = getPosition( ellipsoid, preEndCartographic, minHeight, previousBottom ); nextBottom = getPosition( ellipsoid, postEndCartographic, minHeight, nextBottom ); vertexNormal = computeVertexMiterNormal( previousBottom, vertexBottom, vertexTop, nextBottom, vertexNormal ); } else { vertexNormal = computeRightNormal( preEndCartographic, endCartographic, maxHeight, ellipsoid, vertexNormal ); } index = normalsArray.length; Matrix2.Cartesian3.pack(vertexNormal, normalsArray, index); Matrix2.Cartesian3.pack(vertexBottom, bottomPositionsArray, index); Matrix2.Cartesian3.pack(vertexTop, topPositionsArray, index); cartographicsArray.push(endCartographic.latitude); cartographicsArray.push(endCartographic.longitude); if (loop) { interpolateSegment( endCartographic, startCartographic, minHeight, maxHeight, granularity, arcType, ellipsoid, normalsArray, bottomPositionsArray, topPositionsArray, cartographicsArray ); index = normalsArray.length; for (i = 0; i < 3; ++i) { normalsArray[index + i] = normalsArray[i]; bottomPositionsArray[index + i] = bottomPositionsArray[i]; topPositionsArray[index + i] = topPositionsArray[i]; } cartographicsArray.push(startCartographic.latitude); cartographicsArray.push(startCartographic.longitude); } return generateGeometryAttributes( loop, projection, bottomPositionsArray, topPositionsArray, normalsArray, cartographicsArray, compute2dAttributes ); }; // If the end normal angle is too steep compared to the direction of the line segment, // "break" the miter by rotating the normal 90 degrees around the "up" direction at the point // For ultra precision we would want to project into a plane, but in practice this is sufficient. const lineDirectionScratch = new Matrix2.Cartesian3(); const matrix3Scratch = new Matrix2.Matrix3(); const quaternionScratch = new Transforms.Quaternion(); function breakMiter(endGeometryNormal, startBottom, endBottom, endTop) { const lineDirection = direction(endBottom, startBottom, lineDirectionScratch); const dot = Matrix2.Cartesian3.dot(lineDirection, endGeometryNormal); if (dot > MITER_BREAK_SMALL || dot < MITER_BREAK_LARGE) { const vertexUp = direction(endTop, endBottom, vertexUpScratch); const angle = dot < MITER_BREAK_LARGE ? ComponentDatatype.CesiumMath.PI_OVER_TWO : -ComponentDatatype.CesiumMath.PI_OVER_TWO; const quaternion = Transforms.Quaternion.fromAxisAngle( vertexUp, angle, quaternionScratch ); const rotationMatrix = Matrix2.Matrix3.fromQuaternion(quaternion, matrix3Scratch); Matrix2.Matrix3.multiplyByVector( rotationMatrix, endGeometryNormal, endGeometryNormal ); return true; } return false; } const endPosCartographicScratch = new Matrix2.Cartographic(); const normalStartpointScratch = new Matrix2.Cartesian3(); const normalEndpointScratch = new Matrix2.Cartesian3(); function projectNormal( projection, cartographic, normal, projectedPosition, result ) { const position = Matrix2.Cartographic.toCartesian( cartographic, projection._ellipsoid, normalStartpointScratch ); let normalEndpoint = Matrix2.Cartesian3.add(position, normal, normalEndpointScratch); let flipNormal = false; const ellipsoid = projection._ellipsoid; let normalEndpointCartographic = ellipsoid.cartesianToCartographic( normalEndpoint, endPosCartographicScratch ); // If normal crosses the IDL, go the other way and flip the result. // In practice this almost never happens because the cartographic start // and end points of each segment are "nudged" to be on the same side // of the IDL and slightly away from the IDL. if ( Math.abs(cartographic.longitude - normalEndpointCartographic.longitude) > ComponentDatatype.CesiumMath.PI_OVER_TWO ) { flipNormal = true; normalEndpoint = Matrix2.Cartesian3.subtract( position, normal, normalEndpointScratch ); normalEndpointCartographic = ellipsoid.cartesianToCartographic( normalEndpoint, endPosCartographicScratch ); } normalEndpointCartographic.height = 0.0; const normalEndpointProjected = projection.project( normalEndpointCartographic, result ); result = Matrix2.Cartesian3.subtract( normalEndpointProjected, projectedPosition, result ); result.z = 0.0; result = Matrix2.Cartesian3.normalize(result, result); if (flipNormal) { Matrix2.Cartesian3.negate(result, result); } return result; } const adjustHeightNormalScratch = new Matrix2.Cartesian3(); const adjustHeightOffsetScratch = new Matrix2.Cartesian3(); function adjustHeights( bottom, top, minHeight, maxHeight, adjustHeightBottom, adjustHeightTop ) { // bottom and top should be at WALL_INITIAL_MIN_HEIGHT and WALL_INITIAL_MAX_HEIGHT, respectively const adjustHeightNormal = Matrix2.Cartesian3.subtract( top, bottom, adjustHeightNormalScratch ); Matrix2.Cartesian3.normalize(adjustHeightNormal, adjustHeightNormal); const distanceForBottom = minHeight - WALL_INITIAL_MIN_HEIGHT; let adjustHeightOffset = Matrix2.Cartesian3.multiplyByScalar( adjustHeightNormal, distanceForBottom, adjustHeightOffsetScratch ); Matrix2.Cartesian3.add(bottom, adjustHeightOffset, adjustHeightBottom); const distanceForTop = maxHeight - WALL_INITIAL_MAX_HEIGHT; adjustHeightOffset = Matrix2.Cartesian3.multiplyByScalar( adjustHeightNormal, distanceForTop, adjustHeightOffsetScratch ); Matrix2.Cartesian3.add(top, adjustHeightOffset, adjustHeightTop); } const nudgeDirectionScratch = new Matrix2.Cartesian3(); function nudgeXZ(start, end) { const startToXZdistance = Plane.Plane.getPointDistance(XZ_PLANE, start); const endToXZdistance = Plane.Plane.getPointDistance(XZ_PLANE, end); let offset = nudgeDirectionScratch; // Larger epsilon than what's used in GeometryPipeline, a centimeter in world space if (ComponentDatatype.CesiumMath.equalsEpsilon(startToXZdistance, 0.0, ComponentDatatype.CesiumM