import { OrientedBox3 , Projection , ProjectionType , TileKey , TileKeyUtils , TilingScheme } from "@here/harp-geoutils" ; import { assert } from "@here/harp-utils" ; import * as THREE from "three" ; import { DataSource } from "./DataSource" ; import { CalculationStatus , ElevationRange , ElevationRangeSource } from "./ElevationRangeSource" ; import { MapTileCuller } from "./MapTileCuller" ; import { MapView } from "./MapView" ; import { MapViewUtils } from "./Utils" ; const tmpVectors3 = [ new THREE . Vector3 ( ) , new THREE . Vector3 ( ) ] ; const tmpVector4 = new THREE . Vector4 ( ) ; /**
 * Represents a unique TileKey and the area it takes up on screen.
 *
 * Note, in certain tiling projections, it is possible to have an offset, which represents a tile
 * which has fully wrapped around, hence this defaults to 0 to simplify usage for projections which
 * don't require it.
 */ export class TileKeyEntry { constructor ( public tileKey , public area , public offset = 0 , public elevationRange ? , public distance = 0 ) { } } function getGeoBox ( tilingScheme , childTileKey , offset ) { const geoBox = tilingScheme . getGeoBox ( childTileKey ) ; const longitudeOffset = 360.0 * offset ; geoBox . northEast . longitude += longitudeOffset ; geoBox . southWest . longitude += longitudeOffset ; return geoBox ; } /**
 * Map tile keys to TileKeyEntry.
 * Keys are a combination of morton code and tile offset,
 * see [[TileOffsetUtils.getKeyForTileKeyAndOffset]].
 */ type TileKeyEntries = Map < number , TileKeyEntry > ; /**
 * Map zoom level to map of visible tile key entries
 */ type ZoomLevelTileKeyMap = Map < number , TileKeyEntries > ; /**
 * Result of frustum intersection
 */ interface IntersectionResult { /**
     * Tiles intersected by the frustum per zoom level.
     */ readonly tileKeyEntries : ZoomLevelTileKeyMap ; /**
     * True if the intersection was calculated using precise elevation data, false if it's an
     * approximation.
     */ calculationFinal : boolean ; } /**
 * Computes the tiles intersected by the frustum defined by the current camera setup.
 */ export class FrustumIntersection { private readonly m_frustum : THREE . Frustum = new THREE . Frustum ( ) ; private readonly m_viewProjectionMatrix = new THREE . Matrix4 ( ) ; private readonly m_mapTileCuller : MapTileCuller ; private m_rootTileKeys : TileKeyEntry [ ] = [ ] ; private readonly m_tileKeyEntries : ZoomLevelTileKeyMap = new Map ( ) ; constructor ( private readonly m_camera , readonly mapView , private readonly m_extendedFrustumCulling , private readonly m_tileWrappingEnabled , private readonly m_enableMixedLod , private readonly m_tilePixelSize = 256 ) { this . m_mapTileCuller = new MapTileCuller ( m_camera ) ; } /**
     * Return camera used for generating frustum.
     */ get camera ( ) : THREE . PerspectiveCamera { return this . m_camera ; } /**
     * Return projection used to convert geo coordinates to world coordinates.
     */ get projection ( ) : Projection { return this . mapView . projection ; } /**
     * Updates the frustum to match the current camera setup.
     */ updateFrustum ( projectionMatrixOverride ? ) { this . m_viewProjectionMatrix . multiplyMatrices ( projectionMatrixOverride !== undefined ? projectionMatrixOverride : this . m_camera . projectionMatrix , this . m_camera . matrixWorldInverse ) ; this . m_frustum . setFromProjectionMatrix ( this . m_viewProjectionMatrix ) ; if ( this . m_extendedFrustumCulling ) { this . m_mapTileCuller . setup ( ) ; } this . computeRequiredInitialRootTileKeys ( this . m_camera . position ) ; } param tilingScheme param elevationRangeSource param zoomLevels param dataSources returns compute ( tilingScheme , elevationRangeSource , zoomLevels , dataSources ) { this . m_tileKeyEntries . clear ( ) ; assert ( this . mapView . viewportHeight !== 0 ) ; const targetTileArea = Math . pow ( this . m_tilePixelSize / this . mapView . viewportHeight , 2 ) ; const useElevationRangeSource = elevationRangeSource !== undefined && elevationRangeSource . getTilingScheme ( ) === tilingScheme ; const obbIntersections = this . mapView . projection . type === ProjectionType . Spherical || useElevationRangeSource ; const uniqueZoomLevels = new Set ( zoomLevels ) ; let minGeometryHeight = 0 ; let maxGeometryHeight = 0 ; dataSources . forEach ( dataSource => { minGeometryHeight = Math . min ( minGeometryHeight , dataSource . minGeometryHeight ) ; maxGeometryHeight = Math . max ( maxGeometryHeight , dataSource . maxGeometryHeight ) ; } ) ; const cache = { calculationFinal : true , tileBounds : obbIntersections ? new OrientedBox3 ( ) : new THREE . Box3 ( ) } ; for ( const zoomLevel of uniqueZoomLevels ) { this . m_tileKeyEntries . set ( zoomLevel , new Map ( ) ) ; } for ( const tileEntry of this . m_rootTileKeys ) { const tileKey = tileEntry . tileKey ; const offset = tileEntry . offset ; const tileKeyEntry = this . getTileKeyEntry ( tileKey , offset , tilingScheme , cache , minGeometryHeight , maxGeometryHeight , useElevationRangeSource ? elevationRangeSource : undefined ) ; if ( tileKeyEntry !== undefined ) { for ( const zoomLevel of uniqueZoomLevels ) { const tileKeyEntries = this . m_tileKeyEntries . get ( zoomLevel ) ! ; tileKeyEntries . set ( TileKeyUtils . getKeyForTileKeyAndOffset ( tileKey , offset ) , tileKeyEntry ) ; } } } const workList = [ ... this . m_rootTileKeys . values ( ) ] ; while ( workList . length > 0 ) { const tileEntry = workList . pop ( ) ; if ( tileEntry === undefined ) { break ; } const tileKey = tileEntry . tileKey ; const offset = tileEntry . offset ; const subdivide = dataSources . some ( ( ds , i ) => ds . shouldSubdivide ( zoomLevels [ i ] , tileKey ) ) ; if ( ! subdivide ) { continue ; } if ( this . m_enableMixedLod && tileEntry . area < targetTileArea ) { continue ; } const tileKeyAndOffset = TileKeyUtils . getKeyForTileKeyAndOffset ( tileKey , offset ) ; for ( const zoomLevel of uniqueZoomLevels ) { if ( tileKey . level >= zoomLevel ) { continue ; } const tileKeyEntries = this . m_tileKeyEntries . get ( zoomLevel ) ! ; tileKeyEntries . delete ( tileKeyAndOffset ) ; } for ( const subTileKey of tilingScheme . getSubTileKeys ( tileKey ) ) { const subTileEntry = this . getTileKeyEntry ( subTileKey , offset , tilingScheme , cache , minGeometryHeight , maxGeometryHeight , useElevationRangeSource ? elevationRangeSource : undefined ) ; if ( subTileEntry !== undefined ) { for ( const zoomLevel of uniqueZoomLevels ) { if ( subTileEntry . tileKey . level > zoomLevel ) { continue ; } const subTileKeyAndOffset = TileKeyUtils . getKeyForTileKeyAndOffset ( subTileKey , offset ) ; this . m_tileKeyEntries . get ( zoomLevel ) ! . set ( subTileKeyAndOffset , subTileEntry ) ; } workList . push ( subTileEntry ) ; } } } return { tileKeyEntries : this . m_tileKeyEntries , calculationFinal : cache . calculationFinal } ; } private getTileKeyEntry ( tileKey , offset , tilingScheme , cache , minGeometryHeight , maxGeometryHeight , elevationRangeSource ? ) { const geoBox = getGeoBox ( tilingScheme , tileKey , offset ) ; if ( elevationRangeSource !== undefined ) { const range = elevationRangeSource ! . getElevationRange ( tileKey ) ; geoBox . southWest . altitude = range . minElevation ; geoBox . northEast . altitude = range . maxElevation ; cache . calculationFinal = cache . calculationFinal && range . calculationStatus === CalculationStatus . FinalPrecise ; } geoBox . southWest . altitude = ( geoBox . southWest . altitude ?? 0 ) + minGeometryHeight ; geoBox . northEast . altitude = ( geoBox . northEast . altitude ?? 0 ) + maxGeometryHeight ; this . mapView . projection . projectBox ( geoBox , cache . tileBounds ) ; const { area , distance } = this . computeTileAreaAndDistance ( cache . tileBounds ) ; if ( area > 0 ) { return new TileKeyEntry ( tileKey , area , offset , { minElevation : geoBox . southWest . altitude , maxElevation : geoBox . northEast . altitude } , distance ) ; } return undefined ; } param tileBounds return private computeTileAreaAndDistance ( tileBounds ) { if ( tileBounds instanceof THREE . Box3 ) { if ( ( this . m_extendedFrustumCulling && ! this . m_mapTileCuller . frustumIntersectsTileBox ( tileBounds ) ) || ! this . m_frustum . intersectsBox ( tileBounds ) ) { return { area : 0 , distance : Infinity } ; } } else if ( ! tileBounds . intersects ( this . m_frustum ) ) { return { area : 0 , distance : Infinity } ; } const center = tileBounds . getCenter ( tmpVectors3 [ 0 ] ) ; const projectedPoint = tmpVector4 . set ( center . x , center . y , center . z , 1.0 ) . applyMatrix4 ( this . m_viewProjectionMatrix ) ; const size = tileBounds . getSize ( tmpVectors3 [ 1 ] ) ; const objectSize = ( 0.5 * size . length ( ) ) / projectedPoint . w ; return { area : objectSize * objectSize , distance : projectedPoint . z <= - projectedPoint . w ? - 1 : projectedPoint . z >= projectedPoint . w ? 1 : projectedPoint . z / projectedPoint . w } ; } /**
     * Create a list of root nodes to test against the frustum. The root nodes each start at level 0
     * and have an offset (see  Tile ) based on:
     * - the current position [[worldCenter]].
     * - the height of the camera above the world.
     * - the field of view of the camera (the maximum value between the horizontal / vertical
     *   values)
     * - the tilt of the camera (because we see more tiles when tilted).
     *
     *  param worldCenter private computeRequiredInitialRootTileKeys ( worldCenter ) { this . m_rootTileKeys = [ ] ; const rootTileKey = TileKey . fromRowColumnLevel ( 0 , 0 , 0 ) ; const tileWrappingEnabled = this . mapView . projection . type === ProjectionType . Planar ; if ( ! tileWrappingEnabled || ! this . m_tileWrappingEnabled ) { this . m_rootTileKeys . push ( new TileKeyEntry ( rootTileKey , Infinity , 0 ) ) ; return ; } const worldGeoPoint = this . mapView . projection . unprojectPoint ( worldCenter ) ; const startOffset = Math . round ( worldGeoPoint . longitude / 360.0 ) ; const camera = this . m_camera ; const cameraPitch = MapViewUtils . extractAttitude ( this . mapView , camera ) . pitch ; const aspect = camera . aspect > 1 ? camera . aspect : 1 / camera . aspect ; const totalAngleRad = THREE . MathUtils . degToRad ( ( camera . fov * aspect ) / 2 ) + cameraPitch ; const worldLengthHorizontalFull = Math . tan ( totalAngleRad ) * camera . position . z ; const worldLengthHorizontalSmallerHalf = Math . tan ( cameraPitch ) * camera . position . z ; const worldLengthHorizontal = worldLengthHorizontalFull - worldLengthHorizontalSmallerHalf ; const worldLeftPoint = new THREE . Vector3 ( worldCenter . x - worldLengthHorizontal , worldCenter . y , worldCenter . z ) ; const worldLeftGeoPoint = this . mapView . projection . unprojectPoint ( worldLeftPoint ) ; const offsetRange = THREE . MathUtils . clamp ( Math . ceil ( Math . abs ( ( worldGeoPoint . longitude - worldLeftGeoPoint . longitude ) / 360 ) * Math . SQRT2 ) , 0 , 7 ) ; for ( let offset = - offsetRange + startOffset ; offset <= offsetRange + startOffset ; offset ++ ) { this . m_rootTileKeys . push ( new TileKeyEntry ( rootTileKey , Infinity , offset ) ) ; } } }