import { EarthConstants , sphereProjection } from "@here/harp-geoutils" ; import { Math2D } from "@here/harp-utils" ; import * as THREE from "three" ; import { CameraUtils } from "./CameraUtils" ; import { MapViewUtils } from "./Utils" ; const twoPi = Math . PI * 2 ; export enum CanvasSide { Bottom , Right , Top , Left } export function nextCanvasSide ( side ) { return ( side + 1 ) % 4 ; } export function previousCanvasSide ( side ) { return ( side + 3 ) % 4 ; } remarks internal export class SphereHorizon { private readonly m_matrix : THREE . Matrix4 ; private readonly m_radius : number ; private readonly m_normalToTangentAngle : number ; private readonly m_distanceToHorizonCenter : number ; private readonly m_intersections : number [ ] [ ] = [ ] ; private m_isFullyVisible : boolean = true ; private readonly m_cameraPitch : number ; param m_camera param m_cornerIntersects constructor ( private readonly m_camera , private readonly m_cornerIntersects ) { const earthRadiusSq = EarthConstants . EQUATORIAL_RADIUS * EarthConstants . EQUATORIAL_RADIUS ; const xAxis = new THREE . Vector3 ( ) . setFromMatrixColumn ( m_camera . matrixWorld , 0 ) . normalize ( ) ; const zAxis = m_camera . position . clone ( ) . normalize ( ) ; const yAxis = new THREE . Vector3 ( ) . crossVectors ( zAxis , xAxis ) ; const cameraHeight = m_camera . position . length ( ) ; this . m_normalToTangentAngle = Math . asin ( EarthConstants . EQUATORIAL_RADIUS / cameraHeight ) ; const tangentDistance = Math . sqrt ( cameraHeight * cameraHeight - earthRadiusSq ) ; this . m_distanceToHorizonCenter = tangentDistance * Math . cos ( this . m_normalToTangentAngle ) ; const horizonCenterLength = cameraHeight - this . m_distanceToHorizonCenter ; this . m_radius = Math . sqrt ( earthRadiusSq - horizonCenterLength * horizonCenterLength ) ; this . m_cameraPitch = MapViewUtils . extractAttitude ( { projection : sphereProjection } , this . m_camera ) . pitch ; const horizonCenter = new THREE . Vector3 ( ) . copy ( zAxis ) . setLength ( horizonCenterLength ) ; this . m_matrix = new THREE . Matrix4 ( ) . makeBasis ( xAxis , yAxis , zAxis ) . setPosition ( horizonCenter ) ; this . computeIntersections ( ) ; } param t param arcStart param arcEnd param target returns getPoint ( t , arcStart = 0 , arcEnd = 1 , target = new THREE . Vector3 ( ) ) { const startAngle = arcStart * twoPi ; const endAngle = arcEnd >= arcStart ? arcEnd * twoPi : ( arcEnd + 1 ) * twoPi ; const deltaAngle = endAngle - startAngle ; const angle = startAngle + t * deltaAngle ; target . set ( this . m_radius * Math . cos ( angle ) , this . m_radius * Math . sin ( angle ) , 0 ) ; target . applyMatrix4 ( this . m_matrix ) ; return target ; } param callback param tStart param tEnd param maxNumPoints getDivisionPoints ( callback , tStart = 0 , tEnd = 1 , maxNumPoints = 10 ) { const numPoints = Math . max ( Math . ceil ( ( ( tEnd < tStart ? 1 + tEnd : tEnd ) - tStart ) * maxNumPoints ) , 1 ) ; for ( let d = 0 ; d < numPoints ; d ++ ) { callback ( this . getPoint ( d / numPoints , tStart , tEnd ) ) ; } } returns get isFullyVisible ( ) : boolean { return this . m_isFullyVisible ; } returns getSideIntersections ( side ) { return this . m_intersections [ side ] ; } private isTangentVisible ( side ) { switch ( side ) { case CanvasSide . Top : { const eyeToTangentAngle = this . m_normalToTangentAngle - this . m_cameraPitch ; return CameraUtils . getTopFov ( this . m_camera ) >= Math . abs ( eyeToTangentAngle ) ; } case CanvasSide . Bottom : { const eyeToTangentAngle = this . m_normalToTangentAngle + this . m_cameraPitch ; return CameraUtils . getBottomFov ( this . m_camera ) >= Math . abs ( eyeToTangentAngle ) ; } case CanvasSide . Left : { const eyeToTangentAngle = this . m_normalToTangentAngle ; return ( CameraUtils . getLeftFov ( this . m_camera ) >= Math . abs ( eyeToTangentAngle ) && this . m_cameraPitch <= CameraUtils . getBottomFov ( this . m_camera ) ) ; } case CanvasSide . Right : { const eyeToTangentAngle = this . m_normalToTangentAngle ; return ( CameraUtils . getRightFov ( this . m_camera ) >= Math . abs ( eyeToTangentAngle ) && this . m_cameraPitch <= CameraUtils . getBottomFov ( this . m_camera ) ) ; } } } private getTangentOnSide ( side ) { switch ( side ) { case CanvasSide . Bottom : return 0.75 ; case CanvasSide . Right : return 0 ; case CanvasSide . Top : return 0.25 ; case CanvasSide . Left : return 0.5 ; } } private computeIntersections ( ) { const yBottom = this . m_distanceToHorizonCenter * Math . tan ( this . m_cameraPitch - CameraUtils . getBottomFov ( this . m_camera ) ) ; let tTopRight ; let tBottomRight ; for ( let side = CanvasSide . Bottom ; side < 4 ; side ++ ) { if ( this . isTangentVisible ( side ) ) { this . m_intersections . push ( [ this . getTangentOnSide ( side ) ] ) ; continue ; } const sideIntersections = new Array < number | undefined > ( ) ; this . m_isFullyVisible = false ; switch ( side ) { case CanvasSide . Bottom : { sideIntersections . push ( ... this . computeTBIntersections ( yBottom ) ) ; break ; } case CanvasSide . Right : { const rightFov = CameraUtils . getRightFov ( this . m_camera ) ; const intersections = this . computeLRIntersections ( yBottom , rightFov ) ; if ( intersections ) { [ tTopRight , tBottomRight ] = intersections ; sideIntersections . push ( tBottomRight !== undefined ? 1 + tBottomRight : undefined , tTopRight ) ; } break ; } case CanvasSide . Top : { const yTop = this . m_distanceToHorizonCenter * Math . tan ( this . m_cameraPitch + CameraUtils . getTopFov ( this . m_camera ) ) ; sideIntersections . push ( ... this . computeTBIntersections ( yTop ) . reverse ( ) ) ; break ; } case CanvasSide . Left : { const leftFov = CameraUtils . getLeftFov ( this . m_camera ) ; if ( leftFov === CameraUtils . getRightFov ( this . m_camera ) ) { sideIntersections . push ( tTopRight !== undefined ? 0.5 - tTopRight : undefined , tBottomRight !== undefined ? 0.5 - tBottomRight : undefined ) ; } else { const isections = this . computeLRIntersections ( yBottom , leftFov ) ; if ( isections ) { sideIntersections . push ( 0.5 - isections [ 0 ] , isections [ 1 ] !== undefined ? 0.5 - isections [ 1 ] : undefined ) ; } } break ; } } const hasCorners = [ this . m_cornerIntersects [ side ] , this . m_cornerIntersects [ nextCanvasSide ( side ) ] ] ; this . m_intersections . push ( sideIntersections . filter ( ( val , i ) => val !== undefined && ! hasCorners [ i ] ) as number [ ] ) ; } } returns private computeTBIntersections ( y ) { const radiusSq = this . m_radius * this . m_radius ; const x = Math . sqrt ( radiusSq - y * y ) ; const t = Math . atan2 ( y , x ) / twoPi ; return [ 0.5 - t , t > 0 ? t : 1 + t ] ; } returns private computeLRIntersections ( yBottom , sideFov ) { const eyeToHorizon = this . m_distanceToHorizonCenter / Math . cos ( this . m_cameraPitch ) ; const yMiddle = this . m_distanceToHorizonCenter * Math . tan ( this . m_cameraPitch ) ; const xMiddle = eyeToHorizon * Math . tan ( sideFov ) ; const bottomFov = CameraUtils . getBottomFov ( this . m_camera ) ; const eyeToBottom = ( this . m_distanceToHorizonCenter * Math . cos ( bottomFov ) ) / Math . cos ( this . m_cameraPitch - bottomFov ) ; const xBottom = ( xMiddle * eyeToBottom ) / eyeToHorizon ; const intersections = Math2D . intersectLineAndCircle ( xBottom , yBottom , xMiddle , yMiddle , this . m_radius ) ; if ( ! intersections ) { return undefined ; } const yTopRight = intersections . y1 ; const tTop = Math . atan2 ( yTopRight , intersections . x1 ) / twoPi ; const hasBottomIntersection = - yTopRight >= yBottom && intersections . x2 !== undefined ; const tBottom = hasBottomIntersection ? Math . atan2 ( intersections . y2 ! , intersections . x2 ! ) / twoPi : undefined ; return [ tTop , tBottom ] ; } }