import { EarthConstants , GeoCoordinates , GeoPolygon , GeoPolygonCoordinates , isAntimeridianCrossing , Projection , ProjectionType } from "@here/harp-geoutils" ; import { assert } from "@here/harp-utils" ; import { PerspectiveCamera , Vector2 , Vector3 } from "three" ; import { CanvasSide , nextCanvasSide , previousCanvasSide , SphereHorizon } from "./SphereHorizon" ; import { MapViewUtils } from "./Utils" ; import { ViewBounds } from "./ViewBounds" ; export function computeEdgeDivisions ( geoStart , geoEnd ) { const maxLatitudeSpan = 20 ; const maxLongitudeSpan = 5 ; const latitudeSpan = Math . abs ( geoEnd . latitude - geoStart . latitude ) ; const longitudeSpan = geoStart . minLongitudeSpanTo ( geoEnd ) ; return Math . ceil ( Math . max ( latitudeSpan / maxLatitudeSpan , longitudeSpan / maxLongitudeSpan ) ) ; } const ccwCanvasCornersNDC = [ { x : - 1 , y : - 1 } , { x : 1 , y : - 1 } , { x : 1 , y : 1 } , { x : - 1 , y : 1 } ] ; internal export class SphereViewBounds implements ViewBounds { constructor ( readonly camera , readonly projection ) { assert ( projection . type === ProjectionType . Spherical ) ; } @override
      generate ( ) { const coordinates = this . findBoundsIntersections ( ) ; this . wrapAroundPoles ( coordinates ) ; return coordinates . length > 2 ? new GeoPolygon ( coordinates as GeoPolygonCoordinates , false , true ) : undefined ; } private addSideSegmentSubdivisions ( coordinates , NDCStart , NDCEnd , geoStart , geoEnd ) { coordinates . push ( geoStart ) ; const divisionCount = computeEdgeDivisions ( geoStart , geoEnd ) ; if ( divisionCount <= 1 ) { return ; } const NDCStep = new Vector2 ( NDCEnd . x - NDCStart . x , NDCEnd . y - NDCStart . y ) . multiplyScalar ( 1 / divisionCount ) ; const NDCDivision = new Vector2 ( NDCStart . x , NDCStart . y ) ; for ( let i = 0 ; i < divisionCount - 1 ; i ++ ) { NDCDivision . add ( NDCStep ) ; const intersection = MapViewUtils . rayCastWorldCoordinates ( { camera : this . camera , projection : this . projection } , NDCDivision . x , NDCDivision . y ) ; if ( intersection ) { coordinates . push ( this . projection . unprojectPoint ( intersection ) ) ; } } } private addSideIntersections ( coordinates , side , geoStartCorner ? , geoEndCorner ? , horizon ? ) { const startNDCCorner = ccwCanvasCornersNDC [ side ] ; const endNDCCorner = ccwCanvasCornersNDC [ nextCanvasSide ( side ) ] ; if ( geoStartCorner && geoEndCorner ) { this . addSideSegmentSubdivisions ( coordinates , startNDCCorner , endNDCCorner , geoStartCorner , geoEndCorner ) ; return ; } if ( ! horizon ) { return ; } const horizonIntersections = horizon . getSideIntersections ( side ) ; if ( horizonIntersections . length === 0 ) { return ; } if ( geoStartCorner ) { const worldHorizonPoint = horizon . getPoint ( horizonIntersections [ horizonIntersections . length - 1 ] ) ; const geoHorizonPoint = this . projection . unprojectPoint ( worldHorizonPoint ) ; this . addSideSegmentSubdivisions ( coordinates , startNDCCorner , worldHorizonPoint . project ( this . camera ) , geoStartCorner , geoHorizonPoint ) ; } else { const prevSide = previousCanvasSide ( side ) ; let prevSideIntersections = horizon . getSideIntersections ( prevSide ) ; if ( prevSideIntersections . length === 0 ) { prevSideIntersections = horizon . getSideIntersections ( previousCanvasSide ( prevSide ) ) ; } assert ( prevSideIntersections . length > 0 ) ; horizon . getDivisionPoints ( point => { coordinates . push ( this . projection . unprojectPoint ( point ) ) ; } , prevSideIntersections [ prevSideIntersections . length - 1 ] , horizonIntersections [ 0 ] ) ; } if ( horizonIntersections . length > 1 ) { const worldHorizonStart = horizon . getPoint ( horizonIntersections [ 0 ] ) ; const worldHorizonEnd = horizon . getPoint ( horizonIntersections [ 1 ] ) ; const geoHorizonStart = this . projection . unprojectPoint ( worldHorizonStart ) ; const geoHorizonEnd = this . projection . unprojectPoint ( worldHorizonEnd ) ; this . addSideSegmentSubdivisions ( coordinates , worldHorizonStart . project ( this . camera ) , worldHorizonEnd . project ( this . camera ) , geoHorizonStart , geoHorizonEnd ) ; } if ( geoEndCorner ) { const worldHorizonPoint = horizon . getPoint ( horizonIntersections [ 0 ] ) ; const geoHorizonPoint = this . projection . unprojectPoint ( worldHorizonPoint ) ; this . addSideSegmentSubdivisions ( coordinates , worldHorizonPoint . project ( this . camera ) , endNDCCorner , geoHorizonPoint , geoEndCorner ) ; } } private findBoundsIntersections ( ) { const coordinates = [ ] ; const [ cornerCoordinates , numCorners ] = this . addCanvasCornerIntersection ( ) ; const horizon = numCorners < 4 ? new SphereHorizon ( this . camera , cornerCoordinates . map ( value => value !== undefined ) ) : undefined ; if ( numCorners === 0 && horizon ! . isFullyVisible ) { horizon ! . getDivisionPoints ( point => { coordinates . push ( this . projection . unprojectPoint ( point ) ) ; } ) ; return coordinates ; } for ( let side = CanvasSide . Bottom ; side < 4 ; side ++ ) { const startCorner = cornerCoordinates [ side ] ; const endCorner = cornerCoordinates [ nextCanvasSide ( side ) ] ; this . addSideIntersections ( coordinates , side , startCorner , endCorner , horizon ) ; } return coordinates ; } private wrapAroundPoles ( coordinates ) { const northPoleCenter = new Vector3 ( 0 , 0 , EarthConstants . EQUATORIAL_RADIUS ) ; const southPoleCenter = new Vector3 ( 0 , 0 , - EarthConstants . EQUATORIAL_RADIUS ) ; const northPoleInView = MapViewUtils . closeToFrustum ( northPoleCenter , this . camera ) ; const southPoleInView = MapViewUtils . closeToFrustum ( southPoleCenter , this . camera ) ; if ( ! northPoleInView && ! southPoleInView ) { return ; } const camLon = this . projection . unprojectPoint ( this . camera . position ) . lng ; const wrapLat = northPoleInView ? 90 : - 90 ; const wrapLon = northPoleInView ? camLon + 180 : camLon - 180 ; const geoWrapTopRight = new GeoCoordinates ( wrapLat , wrapLon ) ; const geoWrapTopRightNorm = geoWrapTopRight . normalized ( ) ; const crossLon = geoWrapTopRightNorm . lng ; let prevLon = coordinates [ coordinates . length - 1 ] . lng ; let isGwAntimerCross = false ; const hSphereCrossEndIndex = coordinates . findIndex ( ( value ) => { const crossesAntimer = isAntimeridianCrossing ( prevLon , value . lng ) ; const sameSign = Math . sign ( crossLon - value . lng ) === Math . sign ( crossLon - prevLon ) ; if ( sameSign === crossesAntimer ) { isGwAntimerCross = crossesAntimer ; return true ; } prevLon = value . lng ; return false ; } ) ; if ( hSphereCrossEndIndex < 0 ) { return ; } const wrapSideOffset = northPoleInView ? 90 : - 90 ; const wrapCornerOffset = northPoleInView ? 0.00001 : - 0.00001 ; const geoWrapRight = new GeoCoordinates ( wrapLat , camLon + wrapSideOffset ) . normalized ( ) ; const geoWrapBottom = new GeoCoordinates ( wrapLat , camLon ) . normalized ( ) ; const geoWrapLeft = new GeoCoordinates ( wrapLat , camLon - wrapSideOffset ) . normalized ( ) ; const geoWrapTopLeft = new GeoCoordinates ( wrapLat , wrapLon + wrapCornerOffset ) . normalized ( ) ; const hSphereCrossStartIndex = ( hSphereCrossEndIndex + coordinates . length - 1 ) % coordinates . length ; const crossStart = coordinates [ hSphereCrossStartIndex ] ; const crossEnd = coordinates [ hSphereCrossEndIndex ] ; let crossLerp = GeoCoordinates . lerp ( crossStart , crossEnd , 0.01 , isGwAntimerCross ) ; if ( isGwAntimerCross && northPoleInView ) { crossLerp . longitude -= 360 ; } else { crossLerp = crossLerp . normalized ( ) ; } coordinates . splice ( hSphereCrossEndIndex , 0 , wrapLon < - 180 ? geoWrapTopRight : geoWrapTopRightNorm , geoWrapRight , geoWrapBottom , geoWrapLeft , geoWrapTopLeft , crossLerp ) ; } private addCanvasCornerIntersection ( ) { const geoCorners = new Array < GeoCoordinates | undefined > ( ) ; let numIntersections = 0 ; ccwCanvasCornersNDC . forEach ( corner => { const intersection = MapViewUtils . rayCastWorldCoordinates ( { camera : this . camera , projection : this . projection } , corner . x , corner . y ) ; if ( intersection ) { geoCorners . push ( this . projection . unprojectPoint ( intersection ) ) ; ++ numIntersections ; } else { geoCorners . push ( undefined ) ; } } ) ; return [ geoCorners , numIntersections ] ; } }