'use strict' ; import type { GeoJSONGeometryMultiPoint } from './GeoJSON' ; import type { LonLatArray , MapBounds , ProjectedXYArray } from './MapViewOptions' ; import type { ProjectionDefinition , Projector } from './ProjectionDefinition' ; import type { ProjectionOptions , ProjectionRotationOption } from './ProjectionOptions' ; import type SVGPath from '../Core/Renderer/SVG/SVGPath' ; import registry from './Projections/ProjectionRegistry.js' ; import U from '../Core/Utilities.js' ; const { clamp , erase } = U ; const deg2rad = Math . PI * 2 / 360 ; const floatCorrection = 0.000001 ; const wrapLon = ( lon ) : number => { if ( lon < - 180 ) { lon += 360 ; } if ( lon > 180 ) { lon -= 360 ; } return lon ; } ; export default class Projection { public bounds : MapBounds | undefined ; public options : ProjectionOptions ; public hasCoordinates : boolean = false ; public hasGeoProjection : boolean = false ; public rotator : Projector | undefined ; public def : ProjectionDefinition | undefined ; public static registry = registry ; public static add ( name , definition ) { Projection . registry [ name ] = definition ; } public static greatCircle ( point1 , point2 , inclusive ? ) { const { atan2 , cos , sin , sqrt } = Math ; const lat1 = point1 [ 1 ] * deg2rad ; const lon1 = point1 [ 0 ] * deg2rad ; const lat2 = point2 [ 1 ] * deg2rad ; const lon2 = point2 [ 0 ] * deg2rad ; const deltaLat = lat2 - lat1 ; const deltaLng = lon2 - lon1 ; const calcA = sin ( deltaLat / 2 ) * sin ( deltaLat / 2 ) + cos ( lat1 ) * cos ( lat2 ) * sin ( deltaLng / 2 ) * sin ( deltaLng / 2 ) ; const calcB = 2 * atan2 ( sqrt ( calcA ) , sqrt ( 1 - calcA ) ) ; const distance = calcB * 6371e3 ; const jumps = Math . round ( distance / 500000 ) ; const lineString = [ ] ; if ( inclusive ) { lineString . push ( point1 ) ; } if ( jumps > 1 ) { const step = 1 / jumps ; for ( let fraction = step ; fraction < 0.999 ; fraction += step ) { const A = sin ( ( 1 - fraction ) * calcB ) / sin ( calcB ) ; const B = sin ( fraction * calcB ) / sin ( calcB ) ; const x = A * cos ( lat1 ) * cos ( lon1 ) + B * cos ( lat2 ) * cos ( lon2 ) ; const y = A * cos ( lat1 ) * sin ( lon1 ) + B * cos ( lat2 ) * sin ( lon2 ) ; const z = A * sin ( lat1 ) + B * sin ( lat2 ) ; const lat3 = atan2 ( z , sqrt ( x * x + y * y ) ) ; const lon3 = atan2 ( y , x ) ; lineString . push ( [ lon3 / deg2rad , lat3 / deg2rad ] ) ; } } if ( inclusive ) { lineString . push ( point2 ) ; } return lineString ; } public static insertGreatCircles ( poly ) { let i = poly . length - 1 ; while ( i -- ) { const roughDistance = Math . max ( Math . abs ( poly [ i ] [ 0 ] - poly [ i + 1 ] [ 0 ] ) , Math . abs ( poly [ i ] [ 1 ] - poly [ i + 1 ] [ 1 ] ) ) ; if ( roughDistance > 10 ) { const greatCircle = Projection . greatCircle ( poly [ i ] , poly [ i + 1 ] ) ; if ( greatCircle . length ) { poly . splice ( i + 1 , 0 , ... greatCircle ) ; } } } } public static toString ( options ? ) { const { name , rotation } = options || { } ; return [ name , rotation && rotation . join ( ',' ) ] . join ( ';' ) ; } public constructor ( options = { } ) { this . options = options ; const { name , projectedBounds , rotation } = options ; this . rotator = rotation ? this . getRotator ( rotation ) : void 0 ; const ProjectionDefinition = name ? Projection . registry [ name ] : void 0 ; if ( ProjectionDefinition ) { this . def = new ProjectionDefinition ( options ) ; } const { def , rotator } = this ; if ( def ) { this . maxLatitude = def . maxLatitude || 90 ; this . hasGeoProjection = true ; } if ( rotator && def ) { this . forward = ( lonLat ) : LonLatArray => def . forward ( rotator . forward ( lonLat ) ) ; this . inverse = ( xy ) : ProjectedXYArray => rotator . inverse ( def . inverse ( xy ) ) ; } else if ( def ) { this . forward = ( lonLat ) : LonLatArray => def . forward ( lonLat ) ; this . inverse = ( xy ) : ProjectedXYArray => def . inverse ( xy ) ; } else if ( rotator ) { this . forward = rotator . forward ; this . inverse = rotator . inverse ; } this . bounds = projectedBounds === 'world' ? def && def . bounds : projectedBounds ; } public lineIntersectsBounds ( line ) { const { x1 , x2 , y1 , y2 } = this . bounds || { } ; const getIntersect = ( line , dim , val ? ) : ProjectedXYArray | undefined => { const [ p1 , p2 ] = line , otherDim = dim ? 0 : 1 ; if ( typeof val === 'number' && p1 [ dim ] >= val !== p2 [ dim ] >= val ) { const fraction = ( ( val - p1 [ dim ] ) / ( p2 [ dim ] - p1 [ dim ] ) ) , crossingVal = p1 [ otherDim ] + fraction * ( p2 [ otherDim ] - p1 [ otherDim ] ) ; return dim ? [ crossingVal , val ] : [ val , crossingVal ] ; } } ; let intersection , ret = line [ 0 ] ; if ( ( intersection = getIntersect ( line , 0 , x1 ) ) ) { ret = intersection ; line [ 1 ] = intersection ; } else if ( ( intersection = getIntersect ( line , 0 , x2 ) ) ) { ret = intersection ; line [ 1 ] = intersection ; } if ( ( intersection = getIntersect ( line , 1 , y1 ) ) ) { ret = intersection ; } else if ( ( intersection = getIntersect ( line , 1 , y2 ) ) ) { ret = intersection ; } return ret ; } public getBoundsPerimeter ( p1 , p2 ) { const corners = [ ] ; if ( p1 [ 0 ] === p2 [ 0 ] || p1 [ 1 ] === p2 [ 1 ] ) { return corners ; } let addCorner = false ; if ( this . bounds ) { const { x1 , x2 , y1 , y2 } = this . bounds , sides = [ x1 , y2 , x2 , y1 , x1 , y2 , x2 ] ; sides . forEach ( ( val , side ) : void => { const dim = side % 2 ; if ( addCorner ) { corners . push ( dim === 0 ? [ val , sides [ side - 1 ] ] : [ sides [ side - 1 ] , val ] ) ; } if ( Math . abs ( p1 [ dim ] - val ) < 0.5 ) { addCorner = true ; } if ( Math . abs ( p2 [ dim ] - val ) < 0.5 ) { addCorner = false ; } } ) ; } if ( corners . length > 2 ) { return this . getBoundsPerimeter ( p2 , p1 ) ; } return corners ; } public getRotator ( rotation ) { const deltaLambda = rotation [ 0 ] * deg2rad , deltaPhi = ( rotation [ 1 ] || 0 ) * deg2rad , deltaGamma = ( rotation [ 2 ] || 0 ) * deg2rad ; const cosDeltaPhi = Math . cos ( deltaPhi ) , sinDeltaPhi = Math . sin ( deltaPhi ) , cosDeltaGamma = Math . cos ( deltaGamma ) , sinDeltaGamma = Math . sin ( deltaGamma ) ; if ( deltaLambda === 0 && deltaPhi === 0 && deltaGamma === 0 ) { return ; } return { forward : ( lonLat ) : [ number , number ] => { const lon = lonLat [ 0 ] * deg2rad + deltaLambda ; const lat = lonLat [ 1 ] * deg2rad , cosLat = Math . cos ( lat ) , x = Math . cos ( lon ) * cosLat , y = Math . sin ( lon ) * cosLat , sinLat = Math . sin ( lat ) , k = sinLat * cosDeltaPhi + x * sinDeltaPhi ; return [ Math . atan2 ( y * cosDeltaGamma - k * sinDeltaGamma , x * cosDeltaPhi - sinLat * sinDeltaPhi ) / deg2rad , Math . asin ( k * cosDeltaGamma + y * sinDeltaGamma ) / deg2rad ] ; } , inverse : ( rLonLat ) : [ number , number ] => { const lon = rLonLat [ 0 ] * deg2rad ; const lat = rLonLat [ 1 ] * deg2rad , cosLat = Math . cos ( lat ) , x = Math . cos ( lon ) * cosLat , y = Math . sin ( lon ) * cosLat , sinLat = Math . sin ( lat ) , k = sinLat * cosDeltaGamma - y * sinDeltaGamma ; return [ ( Math . atan2 ( y * cosDeltaGamma + sinLat * sinDeltaGamma , x * cosDeltaPhi + k * sinDeltaPhi ) - deltaLambda ) / deg2rad , Math . asin ( k * cosDeltaPhi - x * sinDeltaPhi ) / deg2rad ] ; } } ; } public forward ( lonLat ) { return lonLat ; } public inverse ( xy ) { return xy ; } public maxLatitude = 90 ; private cutOnAntimeridian ( poly , isPolygon ) { const antimeridian = 180 ; const intersections = [ ] ; const polygons = [ poly ] ; poly . forEach ( ( lonLat , i ) : void => { let previousLonLat = poly [ i - 1 ] ; if ( ! i ) { if ( ! isPolygon ) { return ; } previousLonLat = poly [ poly . length - 1 ] ; } const lon1 = previousLonLat [ 0 ] , lon2 = lonLat [ 0 ] ; if ( ( lon1 < - 90 || lon1 > 90 ) && ( lon2 < - 90 || lon2 > 90 ) && ( lon1 > 0 ) !== ( lon2 > 0 ) ) { const fraction = clamp ( ( antimeridian - ( lon1 + 360 ) % 360 ) / ( ( lon2 + 360 ) % 360 - ( lon1 + 360 ) % 360 ) , 0 , 1 ) , lat = ( previousLonLat [ 1 ] + fraction * ( lonLat [ 1 ] - previousLonLat [ 1 ] ) ) ; intersections . push ( { i , lat , direction : lon1 < 0 ? 1 : - 1 , previousLonLat , lonLat } ) ; } } ) ; let polarIntersection ; if ( intersections . length ) { if ( isPolygon ) { if ( intersections . length % 2 === 1 ) { polarIntersection = intersections . slice ( ) . sort ( ( a , b ) : number => Math . abs ( b . lat ) - Math . abs ( a . lat ) ) [ 0 ] ; erase ( intersections , polarIntersection ) ; } let i = intersections . length - 2 ; while ( i >= 0 ) { const index = intersections [ i ] . i ; const lonPlus = wrapLon ( antimeridian + intersections [ i ] . direction * floatCorrection ) ; const lonMinus = wrapLon ( antimeridian - intersections [ i ] . direction * floatCorrection ) ; const slice = poly . splice ( index , intersections [ i + 1 ] . i - index , ... Projection . greatCircle ( [ lonPlus , intersections [ i ] . lat ] , [ lonPlus , intersections [ i + 1 ] . lat ] , true ) ) ; slice . push ( ... Projection . greatCircle ( [ lonMinus , intersections [ i + 1 ] . lat ] , [ lonMinus , intersections [ i ] . lat ] , true ) ) ; polygons . push ( slice ) ; i -= 2 ; } if ( polarIntersection ) { for ( let i = 0 ; i < polygons . length ; i ++ ) { const poly = polygons [ i ] ; const indexOf = poly . indexOf ( polarIntersection . lonLat ) ; if ( indexOf > - 1 ) { const polarLatitude = ( polarIntersection . lat < 0 ? - 1 : 1 ) * this . maxLatitude ; const lon1 = wrapLon ( antimeridian + polarIntersection . direction * floatCorrection ) ; const lon2 = wrapLon ( antimeridian - polarIntersection . direction * floatCorrection ) ; const polarSegment = Projection . greatCircle ( [ lon1 , polarIntersection . lat ] , [ lon1 , polarLatitude ] , true ) . concat ( Projection . greatCircle ( [ lon2 , polarLatitude ] , [ lon2 , polarIntersection . lat ] , true ) ) ; poly . splice ( indexOf , 0 , ... polarSegment ) ; break ; } } } } else { let i = intersections . length ; while ( i -- ) { const index = intersections [ i ] . i ; const slice = poly . splice ( index , poly . length , [ wrapLon ( antimeridian + intersections [ i ] . direction * floatCorrection ) , intersections [ i ] . lat ] ) ; slice . unshift ( [ wrapLon ( antimeridian - intersections [ i ] . direction * floatCorrection ) , intersections [ i ] . lat ] ) ; polygons . push ( slice ) ; } } } return polygons ; } public path ( geometry ) { const { def , rotator } = this ; const antimeridian = 180 ; const path = [ ] ; const isPolygon = geometry . type === 'Polygon' || geometry . type === 'MultiPolygon' ; const hasGeoProjection = this . hasGeoProjection ; const projectingToPlane = this . options . name !== 'Orthographic' ; const preclip = projectingToPlane ? rotator : void 0 ; const postclip = projectingToPlane ? ( def || this ) : this ; const addToPath = ( polygon ) : void => { const poly = polygon . map ( ( lonLat ) : LonLatArray => { if ( projectingToPlane ) { if ( preclip ) { lonLat = preclip . forward ( lonLat ) ; } let lon = lonLat [ 0 ] ; if ( Math . abs ( lon - antimeridian ) < floatCorrection ) { if ( lon < antimeridian ) { lon = antimeridian - floatCorrection ; } else { lon = antimeridian + floatCorrection ; } } lonLat = [ lon , lonLat [ 1 ] ] ; } return lonLat ; } ) ; let polygons = [ poly ] ; if ( hasGeoProjection ) { Projection . insertGreatCircles ( poly ) ; if ( projectingToPlane ) { polygons = this . cutOnAntimeridian ( poly , isPolygon ) ; } } polygons . forEach ( ( poly ) : void => { if ( poly . length < 2 ) { return ; } let movedTo = false , firstValidLonLat , lastValidLonLat , lastValidPoint , lastInvalidPoint ; const pushToPath = ( point ) : void => { if ( ! movedTo ) { path . push ( [ 'M' , point [ 0 ] , point [ 1 ] ] ) ; movedTo = true ; } else { path . push ( [ 'L' , point [ 0 ] , point [ 1 ] ] ) ; } } ; for ( let i = 0 ; i < poly . length ; i ++ ) { const lonLat = poly [ i ] , point = postclip . forward ( lonLat ) ; if ( ! point . outside ) { if ( isPolygon && ! firstValidLonLat ) { firstValidLonLat = lonLat ; if ( poly [ i - 1 ] ) { poly . push ( poly [ i - 1 ] ) ; } poly . push ( lonLat ) ; } if ( lastInvalidPoint ) { const intersection = ( this . bounds && projectingToPlane && this . lineIntersectsBounds ( [ point , lastInvalidPoint ] ) ) ; if ( isPolygon && hasGeoProjection ) { if ( intersection ) { if ( lastValidPoint ) { this . getBoundsPerimeter ( lastValidPoint , intersection ) . forEach ( pushToPath ) ; } } else if ( lastValidLonLat ) { const greatCircle = Projection . greatCircle ( lastValidLonLat , lonLat ) ; greatCircle . forEach ( ( lonLat ) : void => { pushToPath ( postclip . forward ( lonLat ) ) ; } ) ; } } else { movedTo = false ; } if ( intersection ) { pushToPath ( intersection ) ; } } pushToPath ( point ) ; lastValidLonLat = lonLat ; lastValidPoint = point ; lastInvalidPoint = void 0 ; } else { if ( lastValidPoint && ! lastInvalidPoint ) { lastValidPoint = this . lineIntersectsBounds ( [ lastValidPoint , point ] ) ; pushToPath ( lastValidPoint ) ; } lastInvalidPoint = point ; } } } ) ; } ; if ( geometry . type === 'LineString' ) { addToPath ( geometry . coordinates ) ; } else if ( geometry . type === 'MultiLineString' ) { geometry . coordinates . forEach ( ( c ) : void => addToPath ( c ) ) ; } else if ( geometry . type === 'Polygon' ) { geometry . coordinates . forEach ( ( c ) : void => addToPath ( c ) ) ; if ( path . length ) { path . push ( [ 'Z' ] ) ; } } else if ( geometry . type === 'MultiPolygon' ) { geometry . coordinates . forEach ( ( polygons ) : void => { polygons . forEach ( ( c ) : void => addToPath ( c ) ) ; } ) ; if ( path . length ) { path . push ( [ 'Z' ] ) ; } } return path ; } }