import { BoundingBox , Epsg , EpsgCode , TileMatrixSet , Tile , NamedBounds } from '@basemaps/geo' ; import { BBox , BBoxFeature , BBoxFeatureCollection , MultiPolygon , multiPolygonToWgs84 , toFeatureMultiPolygon , toFeaturePolygon , } from '@linzjs/geojson' ; import { Position } from 'geojson' ; import Proj from 'proj4' ; import { CompositeError } from '../composite.error.js' ; import { Citm2000 } from './citm2000.js' ; import { Nztm2000 } from './nztm2000.js' ; Proj . defs ( Epsg . Nztm2000 . toEpsgString ( ) , Nztm2000 ) ; Proj . defs ( Epsg . Citm2000 . toEpsgString ( ) , Citm2000 ) ; const CodeMap = new Map < EpsgCode , Projection > ( ) ; export interface LatLon { lat : number ; lon : number ; } function getEpsgCode ( epsgCode ? ) { if ( epsgCode == null ) return null ; if ( typeof epsgCode === 'number' ) return epsgCode ; if ( 'code' in epsgCode ) return epsgCode . code ; if ( 'projection' in epsgCode ) return epsgCode . projection . code ; return null ; } export class Projection { epsg : Epsg ; /** Transform coordinates to and from Wgs84 */ private projection : proj4 . Converter ; /**
     * Wrapper around TileMatrixSet with utilities for converting Points and Polygons
     */ private constructor ( epsg ) { this . epsg = epsg ; try { this . projection = Proj ( epsg . toEpsgString ( ) , Epsg . Wgs84 . toEpsgString ( ) ) ; } catch ( err ) { throw new CompositeError ( `Failed to create projection: ${ epsg . toEpsgString ( ) }, ${ Epsg . Wgs84 . toEpsgString ( ) }` , 500 , err ) ; } } param epsgCode static get ( epsgCode ) { const proj = this . tryGet ( epsgCode ) ; if ( proj == null ) throw new Error ( `Invalid projection: ${ epsgCode }` ) ; return proj ; } param epsgCode static tryGet ( unk ? ) { const epsgCode = getEpsgCode ( unk ) ; if ( epsgCode == null ) return null ; let proj = CodeMap . get ( epsgCode ) ; if ( proj != null ) return proj ; const epsg = Epsg . tryGet ( epsgCode ) ; if ( epsg == null ) return null ; proj = new Projection ( epsg ) ; CodeMap . set ( epsgCode , proj ) ; return proj ; } return projectMultipolygon ( multipoly , targetProjection ) { if ( targetProjection . epsg . code === this . epsg . code ) return multipoly ; const { toWgs84 } = this ; const { fromWgs84 } = targetProjection ; return multipoly . map ( ( poly ) => poly . map ( ( ring ) => ring . map ( ( p ) => fromWgs84 ( toWgs84 ( p ) ) ) ) ) ; } /**
     * Convert source `[x, y]` coordinates to `[lon, lat]`
     */ get toWgs84 ( ) : ( coordinates : Position ) => Position { return this . projection . forward ; } /**
     * Convert `[lon, lat]` coordinates to source `[x, y]`
     */ get fromWgs84 ( ) : ( coordinates : Position ) => Position { return this . projection . inverse ; } param source returns boundsToWgs84BoundingBox ( source ) { const sw = this . toWgs84 ( [ source . x , source . y ] ) ; const ne = this . toWgs84 ( [ source . x + source . width , source . y + source . height ] ) ; return [ sw [ 0 ] , sw [ 1 ] , ne [ 0 ] , ne [ 1 ] ] ; } param bounds param properties returns boundsToGeoJsonFeature ( bounds , properties = { } ) { const sw = [ bounds . x , bounds . y ] ; const se = [ bounds . x + bounds . width , bounds . y ] ; const nw = [ bounds . x , bounds . y + bounds . height ] ; const ne = [ bounds . x + bounds . width , bounds . y + bounds . height ] ; const coords = multiPolygonToWgs84 ( [ [ [ sw , nw , ne , se , sw ] ] ] as MultiPolygon , this . toWgs84 ) ; const feature = coords . length === 1 ? toFeaturePolygon ( coords [ 0 ] , properties ) : toFeatureMultiPolygon ( coords , properties ) ; feature . bbox = this . boundsToWgs84BoundingBox ( bounds ) ; return feature as BBoxFeature ; } /** Convert a tile covering to a GeoJSON FeatureCollection */ toGeoJson ( files ) { return { type : 'FeatureCollection' , features : files . map ( ( f ) => this . boundsToGeoJsonFeature ( f , { name : f . name } ) ) , } ; } param gsd param blockFactor static getTiffResZoom ( tms , gsd , blockFactor = 1 ) { let z = 0 ; for ( ; z < tms . zooms . length ; ++ z ) { if ( tms . pixelScale ( z ) <= gsd * blockFactor ) return z ; } if ( z === tms . zooms . length ) return z - 1 ; throw new Error ( 'ResZoom not found' ) ; } /** Convert a tile to the wgs84 bounds */ static tileToWgs84Bbox ( tms , tile ) { const ul = tms . tileToSource ( tile ) ; const lr = tms . tileToSource ( { x : tile . x + 1 , y : tile . y + 1 , z : tile . z } ) ; const proj = this . get ( tms ) ; const [ swLon , swLat ] = proj . toWgs84 ( [ ul . x , lr . y ] ) ; const [ neLon , neLat ] = proj . toWgs84 ( [ lr . x , ul . y ] ) ; return [ swLon , swLat , neLon , neLat ] ; } /**
     * return the `lat`, `lon` of a Tile's center
     */ static tileCenterToLatLon ( tms , tile ) { const point = tms . tileToSource ( { x : tile . x + 0.5 , y : tile . y + 0.5 , z : tile . z } ) ; const [ lon , lat ] = this . get ( tms ) . toWgs84 ( [ point . x , point . y ] ) ; return Projection . wrapLatLon ( lat , lon ) ; } /**
     * Reused from : https://github.com/pelias/api/blob/6a7751f35882698eb885b93635656ec0c2941633/sanitizer/wrap.js
     *
     * Normalize co-ordinates that lie outside of the normal ranges.
     *
     * longitude wrapping simply requires adding +- 360 to the value until it comes in to range.
     * for the latitude values we need to flip the longitude whenever the latitude
     * crosses a pole.
     *
     */ static wrapLatLon ( lat , lon ) { const point = { lat , lon } ; const quadrant = Math . floor ( Math . abs ( lat ) / 90 ) % 4 ; const pole = lat > 0 ? 90 : - 90 ; const offset = lat % 90 ; switch ( quadrant ) { case 0 : point . lat = offset ; break ; case 1 : point . lat = pole - offset ; point . lon += 180 ; break ; case 2 : point . lat = - offset ; point . lon += 180 ; break ; case 3 : point . lat = - pole + offset ; break ; } if ( point . lon > 180 || point . lon < - 180 ) { point . lon -= Math . floor ( ( point . lon + 180 ) / 360 ) * 360 ; } return point ; } param tile param gsd static findAlignmentLevels ( tms , tile , gsd ) { return Math . max ( 0 , this . getTiffResZoom ( tms , gsd , 2 ) - tile . z ) ; } param tile param targetZoom static getImagePixelWidth ( tms , tile , targetZoom ) { const ul = tms . tileToSource ( tile ) ; const lr = tms . tileToSource ( { x : tile . x + 1 , y : tile . y + 1 , z : tile . z } ) ; return Math . round ( ( lr . x - ul . x ) / tms . pixelScale ( targetZoom ) ) * 2 ; } }