import axios , { AxiosRequestConfig } from 'axios' ; import moment from 'moment' ; import area from '@turf/area' ; import { union , intersection , Geom } from 'polygon-clipping' ; import { BBox } from '../bbox' ; import { CRS_EPSG4326 } from '../crs' ; import { GetMapParams , ApiType , PaginatedTiles , FlyoverInterval , PreviewMode , MimeType , MimeTypes , } from './const' ; import { Dataset } from './dataset' ; import { getAxiosReqParams , RequestConfiguration } from '../utils/cancelRequests' ; import { ensureTimeout } from '../utils/ensureTimeout' ; import { Effects } from '../mapDataManipulation/const' ; import { runEffectFunctions } from '../mapDataManipulation/runEffectFunctions' ; import { drawBlobOnCanvas , canvasToBlob , validateCanvasDimensions } from '../utils/canvas' ; import { CACHE_CONFIG_30MIN } from '../utils/cacheHandlers' ; interface ConstructorParameters { title ? : string | null ; description ? : string | null ; legendUrl ? : string | null ; } export class AbstractLayer { public title : string | null = null ; public description : string | null = null ; public readonly dataset : Dataset | null = null ; public legendUrl : string | null = null ; public constructor ( { title = null , description = null , legendUrl = null } ) { this . title = title ; this . description = description ; this . legendUrl = legendUrl ; } public async getMap ( params , api , reqConfig ? ) { const blob = await ensureTimeout ( async ( innerReqConfig ) => { switch ( api ) { case ApiType . WMS : if ( params . outputResponseId || params . outputResponseId === '' ) { throw new Error ( 'outputResponseId is only available with Processing API' ) ; } const paramsWithoutEffects = { ... params } ; delete paramsWithoutEffects . gain ; delete paramsWithoutEffects . gamma ; delete paramsWithoutEffects . effects ; const url = this . getMapUrl ( paramsWithoutEffects , api ) ; const requestConfig = { responseType : typeof window !== 'undefined' && window . Blob ? 'blob' : 'arraybuffer' , ... getAxiosReqParams ( innerReqConfig , CACHE_CONFIG_30MIN ) , } ; const response = await axios . get ( url , requestConfig ) ; let blob = response . data ; const effects = { gain : params . gain , gamma : params . gamma , ... params . effects } ; blob = await runEffectFunctions ( blob , effects ) ; return blob ; default : const className = this . constructor . name ; throw new Error ( `API type "${ api }" not supported in ${ className }` ) ; } } , reqConfig ) ; return blob ; } protected async decideJpegOrPng ( params , reqConfig ) { if ( params . format !== MimeTypes . JPEG_OR_PNG ) { return params ; } const res = await this . findFlyovers ( params . bbox , params . fromTime , params . toTime , null , null , reqConfig , Number . POSITIVE_INFINITY ) ; if ( res . length === 0 ) { return { ... params , format : MimeTypes . PNG } ; } const { coveragePercent } = res [ 0 ] ; const format = coveragePercent === 100 ? MimeTypes . JPEG : MimeTypes . PNG ; return { ... params , format : format } ; } public async getHugeMap ( params , api , reqConfig ? ) { return await ensureTimeout ( async ( innerReqConfig ) => { const { width , height , bbox } = params ; if ( ! width || ! height ) { throw new Error ( 'Method getHugeMap() requests that width and height of resulting image are specified' ) ; } if ( params . format !== MimeTypes . JPEG && params . format !== MimeTypes . PNG && params . format !== MimeTypes . JPEG_OR_PNG ) { throw new Error ( 'Format ' + params . format + ' not supported, only ' + MimeTypes . PNG + ' and ' + MimeTypes . JPEG + ' are allowed' ) ; } const LIMIT_DIM = 2500 ; if ( width <= LIMIT_DIM && height <= LIMIT_DIM ) { return await this . getMap ( params , api , innerReqConfig ) ; } const canvas = document . createElement ( 'canvas' ) ; canvas . width = width ; canvas . height = height ; const ctx = canvas . getContext ( '2d' ) ; const isCanvasValid = await validateCanvasDimensions ( canvas ) ; if ( ! isCanvasValid ) { throw new Error ( `Image dimensions (${ width }x${ height }) exceed the canvas size limit for this browser.` ) ; } const xSplitBy = Math . ceil ( width / LIMIT_DIM ) ; const chunkWidth = Math . ceil ( width / xSplitBy ) ; const ySplitBy = Math . ceil ( height / LIMIT_DIM ) ; const chunkHeight = Math . ceil ( height / ySplitBy ) ; const { minX : lng0 , minY : lat0 , maxX : lng1 , maxY : lat1 } = bbox ; const xToLng = ( x ) : number => Math . min ( lng0 , lng1 ) + ( x / width ) * Math . abs ( lng1 - lng0 ) ; const yToLat = ( y ) : number => Math . max ( lat0 , lat1 ) - ( y / height ) * Math . abs ( lat1 - lat0 ) ; for ( let x = 0 ; x < width ; x += chunkWidth ) { const xTo = Math . min ( x + chunkWidth , width ) ; for ( let y = 0 ; y < height ; y += chunkHeight ) { const yTo = Math . min ( y + chunkHeight , height ) ; const paramsChunk = { ... params , width : xTo - x , height : yTo - y , bbox : new BBox ( bbox . crs , xToLng ( x ) , yToLat ( yTo ) , xToLng ( xTo ) , yToLat ( y ) ) , preview : PreviewMode . EXTENDED_PREVIEW , } ; const blob = await this . getMap ( paramsChunk , api , innerReqConfig ) ; await drawBlobOnCanvas ( ctx , blob , x , y ) ; } } const outputFormat = ( params . format === MimeTypes . JPEG_OR_PNG ? MimeTypes . PNG : params . format ) as MimeType ; return await canvasToBlob ( canvas , outputFormat ) ; } , reqConfig ) ; } public supportsApiType ( api ) { return api === ApiType . WMS ; } public getMapUrl ( params , api ) { throw new Error ( 'getMapUrl() not implemented yet' ) ; } public setEvalscript ( evalscript ) { throw new Error ( 'Evalscript is only supported on Sentinel Hub layers' ) ; } public setEvalscriptUrl ( evalscriptUrl ) { throw new Error ( 'EvalscriptUrl is only supported on Sentinel Hub layers' ) ; } protected async findTilesInner ( bbox , fromTime , toTime , maxCount = null , offset = null , reqConfig ? ) { throw new Error ( 'findTilesInner() not implemented yet' ) ; } public async findTiles ( bbox , fromTime , toTime , maxCount = null , offset = null , reqConfig ? ) { const findTilesResponse = await ensureTimeout ( async ( innerReqConfig ) => await this . findTilesInner ( bbox , fromTime , toTime , maxCount , offset , innerReqConfig ) , reqConfig ) ; return findTilesResponse ; } public async findFlyovers ( bbox , fromTime , toTime , maxFindTilesRequests = 50 , tilesPerRequest = 50 , reqConfig ? , overrideOrbitTimeMinutes = null ) { const flyOvers = await ensureTimeout ( async ( innerReqConfig ) => { if ( overrideOrbitTimeMinutes === null && ( ! this . dataset || ! this . dataset . orbitTimeMinutes ) ) { throw new Error ( 'Orbit time is needed for grouping tiles into flyovers.' ) ; } if ( bbox . crs !== CRS_EPSG4326 ) { throw new Error ( 'Currently, only EPSG:4326 in findFlyovers' ) ; } if ( maxFindTilesRequests === null ) { maxFindTilesRequests = 50 ; } if ( tilesPerRequest === null ) { tilesPerRequest = 50 ; } const orbitTimeS = overrideOrbitTimeMinutes === null ? this . dataset . orbitTimeMinutes * 60 : overrideOrbitTimeMinutes * 60 ; const bboxGeometry = this . roundCoordinates ( [ [ [ bbox . minX , bbox . minY ] , [ bbox . maxX , bbox . minY ] , [ bbox . maxX , bbox . maxY ] , [ bbox . minX , bbox . maxY ] , [ bbox . minX , bbox . minY ] , ] , ] ) ; const bboxArea = area ( { type : 'Polygon' , coordinates : bboxGeometry , } ) ; let flyovers = [ ] ; let flyoverIndex = 0 ; let currentFlyoverGeometry = null ; let nTilesInFlyover ; let sumCloudCoverPercent ; for ( let i = 0 ; i < maxFindTilesRequests ; i ++ ) { const { tiles , hasMore } = await this . findTiles ( bbox , fromTime , toTime , tilesPerRequest , i * tilesPerRequest , innerReqConfig ) ; for ( let tileIndex = 0 ; tileIndex < tiles . length ; tileIndex ++ ) { if ( flyovers . length === 0 ) { flyovers [ flyoverIndex ] = { fromTime : tiles [ tileIndex ] . sensingTime , toTime : tiles [ tileIndex ] . sensingTime , coveragePercent : 0 , meta : { } , } ; currentFlyoverGeometry = tiles [ tileIndex ] . geometry . coordinates as Geom ; sumCloudCoverPercent = tiles [ tileIndex ] . meta . cloudCoverPercent ; nTilesInFlyover = 1 ; continue ; } const prevDateS = moment . utc ( flyovers [ flyoverIndex ] . fromTime ) . unix ( ) ; const currDateS = moment . utc ( tiles [ tileIndex ] . sensingTime ) . unix ( ) ; const diffS = Math . abs ( prevDateS - currDateS ) ; if ( diffS > orbitTimeS ) { try { flyovers [ flyoverIndex ] . coveragePercent = this . calculateCoveragePercent ( bboxGeometry , bboxArea , currentFlyoverGeometry ) ; } catch ( err ) { flyovers [ flyoverIndex ] . coveragePercent = null ; } if ( sumCloudCoverPercent !== undefined ) { flyovers [ flyoverIndex ] . meta . averageCloudCoverPercent = sumCloudCoverPercent / nTilesInFlyover ; } flyoverIndex ++ ; flyovers [ flyoverIndex ] = { fromTime : tiles [ tileIndex ] . sensingTime , toTime : tiles [ tileIndex ] . sensingTime , coveragePercent : 0 , meta : { } , } ; currentFlyoverGeometry = tiles [ tileIndex ] . geometry . coordinates as Geom ; sumCloudCoverPercent = tiles [ tileIndex ] . meta . cloudCoverPercent ; nTilesInFlyover = 1 ; } else { flyovers [ flyoverIndex ] . fromTime = tiles [ tileIndex ] . sensingTime ; currentFlyoverGeometry = union ( this . roundCoordinates ( currentFlyoverGeometry ) , this . roundCoordinates ( tiles [ tileIndex ] . geometry . coordinates as Geom ) ) ; sumCloudCoverPercent = sumCloudCoverPercent !== undefined ? sumCloudCoverPercent + tiles [ tileIndex ] . meta . cloudCoverPercent : undefined ; nTilesInFlyover ++ ; } } if ( ! hasMore ) { break ; } if ( i + 1 === maxFindTilesRequests ) { throw new Error ( `Could not fetch all the tiles in [${ maxFindTilesRequests }] requests for [${ tilesPerRequest }] tiles` ) ; } } if ( flyovers . length > 0 ) { try { flyovers [ flyoverIndex ] . coveragePercent = this . calculateCoveragePercent ( bboxGeometry , bboxArea , currentFlyoverGeometry ) ; } catch ( err ) { flyovers [ flyoverIndex ] . coveragePercent = null ; } if ( sumCloudCoverPercent !== undefined ) { flyovers [ flyoverIndex ] . meta . averageCloudCoverPercent = sumCloudCoverPercent / nTilesInFlyover ; } } return flyovers ; } , reqConfig ) ; return flyOvers ; } private calculateCoveragePercent ( bboxGeometry , bboxArea , flyoverGeometry ) { let bboxedFlyoverGeometry ; try { bboxedFlyoverGeometry = intersection ( this . roundCoordinates ( bboxGeometry ) , this . roundCoordinates ( flyoverGeometry ) ) ; } catch ( ex ) { console . error ( { msg : 'intersection() failed' , ex , bboxGeometry , flyoverGeometry } ) ; throw ex ; } try { const coveredArea = area ( { type : 'MultiPolygon' , coordinates : bboxedFlyoverGeometry , } ) ; return ( coveredArea / bboxArea ) * 100 ; } catch ( ex ) { console . error ( { msg : 'Turf.js area() division failed' , ex , bboxedFlyoverGeometry , flyoverGeometry } ) ; throw ex ; } } private roundCoordinates ( geometry ) { if ( typeof geometry === 'number' ) { const shift = 1000000 ; return Math . round ( geometry * shift ) / shift ; } return geometry . map ( ( x ) => this . roundCoordinates ( x ) ) ; } public async findDatesUTC ( bbox , fromTime , toTime , reqConfig ? ) { throw new Error ( 'findDatesUTC() not implemented yet' ) ; } deprecated public async findDates ( bbox , fromTime , toTime ) { console . warn ( 'Method findDates() is deprecated and will be removed, please use findDatesUTC() instead' ) ; return await this . findDatesUTC ( bbox , fromTime , toTime ) ; } public async getStats ( payload , reqConfig ? ) { throw new Error ( 'getStats() not implemented for this dataset' ) ; } public async updateLayerFromServiceIfNeeded ( reqConfig ? ) { } }