├── .env.example ├── .eslintrc.cjs ├── .github └── workflows │ ├── on_push.yaml │ └── on_release.yaml ├── .gitignore ├── .prettierrc ├── .storybook └── main.js ├── DatasetsInfo.md ├── LICENSE.md ├── README.md ├── example └── node │ ├── .gitignore │ └── index.js ├── jest-setup.ts ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── auth.ts ├── bbox.ts ├── crs.ts ├── dataimport │ ├── AirbusDataProvider.ts │ ├── MaxarDataProvider.ts │ ├── PlanetDataProvider.ts │ ├── PlanetaryVariablesDataProvider.ts │ ├── TPDI.ts │ ├── TPDProvider.ts │ ├── __tests__ │ │ ├── AirbusDataProvider.ts │ │ ├── MaxarDataProvider.ts │ │ ├── PlanetDataProvider.ts │ │ ├── TPDI.ts │ │ ├── testUtils.AirbusDataProvider.ts │ │ ├── testUtils.MaxarDataProvider.ts │ │ └── testUtils.PlanetDataProvider.ts │ └── const.ts ├── index.ts ├── layer │ ├── AbstractDEMLayer.ts │ ├── AbstractLandsat8Layer.ts │ ├── AbstractLayer.ts │ ├── AbstractSentinelHubV1OrV2Layer.ts │ ├── AbstractSentinelHubV1OrV2WithCCLayer.ts │ ├── AbstractSentinelHubV3Layer.ts │ ├── AbstractSentinelHubV3WithCCLayer.ts │ ├── BYOCLayer.ts │ ├── DEMAWSUSLayer.ts │ ├── DEMCDASLayer.ts │ ├── DEMLayer.ts │ ├── HLSAWSLayer.ts │ ├── Landsat15AWSLMSSL1Layer.ts │ ├── Landsat45AWSLTML1Layer.ts │ ├── Landsat45AWSLTML2Layer.ts │ ├── Landsat7AWSLETML1Layer.ts │ ├── Landsat7AWSLETML2Layer.ts │ ├── Landsat8AWSLOTL1Layer.ts │ ├── Landsat8AWSLOTL2Layer.ts │ ├── Landsat8AWSLayer.ts │ ├── LayersFactory.ts │ ├── MODISLayer.ts │ ├── PlanetNicfi.ts │ ├── ProcessingDataFusionLayer.ts │ ├── S1GRDAWSEULayer.ts │ ├── S1GRDCDASLayer.ts │ ├── S2L1CCDASLayer.ts │ ├── S2L1CLayer.ts │ ├── S2L2ACDASLayer.ts │ ├── S2L2ALayer.ts │ ├── S3OLCICDASLayer.ts │ ├── S3OLCIL2CDASLayer.ts │ ├── S3OLCILayer.ts │ ├── S3SLSTRCDASLayer.ts │ ├── S3SLSTRLayer.ts │ ├── S3SYNL2CDASLayer.ts │ ├── S5PL2CDASLayer.ts │ ├── S5PL2Layer.ts │ ├── WmsLayer.ts │ ├── WmsWmtMsLayer.ts │ ├── WmtsLayer.ts │ ├── __tests__ │ │ ├── BYOCLayer.ts │ │ ├── DEMCDASLayer.ts │ │ ├── HLSAWSLayer.ts │ │ ├── Landsat8AWSLayer.ts │ │ ├── LayersFactory.ts │ │ ├── MODISLayer.ts │ │ ├── ProcessingDataFusionLayer.ts │ │ ├── S1GRDAWSLayer.ts │ │ ├── S2L1CCDASLayer.ts │ │ ├── S2L1CLayer.ts │ │ ├── S2L2ACDASLayer.ts │ │ ├── S2L2ACLayer.ts │ │ ├── S3OLCICDASLayer.ts │ │ ├── S3OLCILayer.ts │ │ ├── S3SLTRCDASLayer.ts │ │ ├── S3SLTRLayer.ts │ │ ├── S5PL2CDASLayer.ts │ │ ├── S5PL2Layer.ts │ │ ├── WmsLayer.ts │ │ ├── WmsWmtMsLayer.ts │ │ ├── WmtsLayer.ts │ │ ├── auth.ts │ │ ├── cache.ts │ │ ├── cancelToken.ts │ │ ├── fixtures.BYOCLayer.ts │ │ ├── fixtures.Landsat8AWSLayer.ts │ │ ├── fixtures.MODISLayer.ts │ │ ├── fixtures.ProcessingDataFusionLayer.ts │ │ ├── fixtures.S1GRDAWSLayer.ts │ │ ├── fixtures.S2L1CCDASLayer.ts │ │ ├── fixtures.S2L1CLayer.ts │ │ ├── fixtures.S2L2ACDASLayer.ts │ │ ├── fixtures.S2L2ALayer.ts │ │ ├── fixtures.S3OLCILayer.ts │ │ ├── fixtures.S3SLTRLayer.ts │ │ ├── fixtures.S5PL2Layer.ts │ │ ├── fixtures.auth.ts │ │ ├── fixtures.findDatesUTC.ts │ │ ├── fixtures.findTiles.ts │ │ ├── fixtures.getCapabilitiesWMS.ts │ │ ├── fixtures.getCapabilitiesWMTS.ts │ │ ├── fixtures.getHugeMap.ts │ │ ├── fixtures.getMap.ts │ │ ├── fixtures.makeLayersSHv3.ts │ │ ├── fixtures.mosaickingOrder.ts │ │ ├── getHugeMap.ts │ │ ├── legacy.ts │ │ ├── mosaickingOrder.ts │ │ ├── outputResponses.ts │ │ ├── retries.ts │ │ ├── testUtils.findDatesUTC.ts │ │ ├── testUtils.findTiles.ts │ │ ├── testUtils.layers.ts │ │ ├── utils.ts │ │ └── wmts.utils.ts │ ├── const.ts │ ├── dataset.ts │ ├── processing.ts │ ├── utils.ts │ ├── wms.ts │ └── wmts.utils.ts ├── legacyCompat.ts ├── mapDataManipulation │ ├── __tests__ │ │ └── effectFunctions.ts │ ├── const.ts │ ├── effectFunctions.ts │ ├── mapDataManipulationUtils.ts │ └── runEffectFunctions.ts ├── statistics │ ├── Fis.ts │ ├── StatisticalApi.ts │ ├── StatisticsProvider.ts │ ├── const.ts │ ├── index.ts │ └── statistics.utils.ts └── utils │ ├── Cache.ts │ ├── axiosInterceptors.ts │ ├── cacheHandlers.ts │ ├── cancelRequests.ts │ ├── canvas.ts │ ├── debug.ts │ ├── defaultReqsConfig.ts │ ├── ensureTimeout.ts │ └── replaceHostnames.ts ├── stories ├── DEM.stories.js ├── TPDI.stories.js ├── byoc.stories.js ├── datafusion.stories.js ├── index.stories.js ├── landsat1.aws.lmssl1.stories.js ├── landsat45.aws.ltml1.stories.js ├── landsat45.aws.ltml2.stories.js ├── landsat7.aws.etml1.stories.js ├── landsat7.aws.etml2.stories.js ├── landsat8.aws.l8l1c.stories.js ├── landsat8.aws.lotl1.stories.js ├── landsat8.aws.lotl2.stories.js ├── legacy.stories.js ├── legacyGetMapFromParams.stories.js ├── legacyGetMapFromUrl.stories.js ├── legacyGetMapWmsUrlFromParams.stories.js ├── s1grdew.stories.js ├── s1grdiw.stories.js ├── s2l1c.stories.js ├── s2l2a.stories.js ├── s3olci.stories.js ├── s3slstr.stories.js ├── s5pl2.stories.js ├── storiesUtils.js ├── wms.gibs.stories.js ├── wms.probav.stories.js ├── wmts.planet.stories.js └── wmts.stories.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | CLIENT_ID= 2 | CLIENT_SECRET= 3 | 4 | ALTERNATIVE_HOSTNAME= 5 | 6 | # Env var INSTANCE_ID should be set to the ID of some instance in Sentinel Hub 7 | # Configurator, which contains all the layers listed below it. 8 | INSTANCE_ID= 9 | S1GRDIW_LAYER_ID= 10 | S1GRDEW_LAYER_ID= 11 | S2L2A_LAYER_ID= 12 | S2L1C_LAYER_ID= 13 | S3SLSTR_LAYER_ID= 14 | S3OLCI_LAYER_ID= 15 | BYOC_LAYER_ID= 16 | BYOC_COLLECTION_ID= 17 | BYOC_BBOX_EPSG3857_MINX= 18 | BYOC_BBOX_EPSG3857_MINY= 19 | BYOC_BBOX_EPSG3857_MAXX= 20 | BYOC_BBOX_EPSG3857_MAXY= 21 | S5PL2_LAYER_ID= 22 | LANDSAT8_LAYER_ID= 23 | LANDSAT8_NDVI_LAYER_ID= 24 | LANDSAT8_LOTL1_LAYER_ID= 25 | LANDSAT8_LOTL1_NDVI_LAYER_ID= 26 | LANDSAT8_LOTL2_LAYER_ID= 27 | LANDSAT8_LOTL2_NDVI_LAYER_ID= 28 | LANDSAT45_LTML1_LAYER_ID= 29 | LANDSAT45_LTML1_NDVI_LAYER_ID= 30 | LANDSAT45_LTML2_LAYER_ID= 31 | LANDSAT45_LTML2_NDVI_LAYER_ID= 32 | 33 | EOC_INSTANCE_ID= 34 | EOC_S1GRDIW_LAYER_ID= 35 | EOC_LANDSAT5_LAYER_ID= 36 | EOC_LANDSAT7_LAYER_ID= 37 | EOC_LANDSAT8_LAYER_ID= 38 | 39 | DEM_MAPZEN_LAYER_ID= 40 | DEM_COPERNICUS_30_LAYER_ID= 41 | DEM_COPERNICUS_90_LAYER_ID= 42 | 43 | PLANET_API_KEY= -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 6 | ], 7 | plugins: ['@typescript-eslint'], 8 | rules: { 9 | '@typescript-eslint/no-explicit-any': 'off', 10 | '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], 11 | '@typescript-eslint/no-non-null-assertion': 'off', 12 | '@typescript-eslint/no-use-before-define': 'off', 13 | '@typescript-eslint/prefer-interface': 'off', 14 | '@typescript-eslint/ban-types': 'off', 15 | 'prefer-const': 'off', 16 | '@typescript-eslint/no-unsafe-function-type': 'off', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/on_push.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | 8 | strategy: 9 | matrix: 10 | node-version: [18] 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | 15 | - name: Use Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | 20 | - name: Run linter 21 | run: | 22 | npm ci 23 | npm run lint 24 | npm run build 25 | 26 | - name: Run tests 27 | run: | 28 | npm run test 29 | -------------------------------------------------------------------------------- /.github/workflows/on_release.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to NPM 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | 12 | - name: Use Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: 18 16 | 17 | - name: Install packages 18 | if: success() && startsWith(github.ref, 'refs/tags/v') 19 | run: | 20 | npm install 21 | 22 | - name: Build and deploy 23 | if: success() && startsWith(github.ref, 'refs/tags/v') 24 | env: 25 | GITHUB_REF_TAG: ${{ github.ref }} 26 | NPMJS_REGISTRY: registry.npmjs.org 27 | NPMJS_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | run: | 29 | echo "Extracting version from GITHUB_REF_TAG: $GITHUB_REF_TAG" 30 | export VERSION=$(echo "$GITHUB_REF_TAG" | sed -e "s#^refs/tags/##") 31 | echo "Version is $VERSION" 32 | npm version --no-git-tag-version "${VERSION}" 33 | npm run build 34 | echo "//${NPMJS_REGISTRY}/:_authToken=${NPMJS_TOKEN}" > .npmrc 35 | [ $(echo "${VERSION}" | grep rc) ] && echo "publishing with next tag to npm - ${VERSION}" || echo "publishing with latest tag to npm - ${VERSION}" 36 | [ $(echo "${VERSION}" | grep rc) ] && npm publish --access public --tag next || npm publish --access public 37 | echo "Published to npm registry: ${NPMJS_REGISTRY}" 38 | tar -cvzf /tmp/dist.tar.gz dist/ 39 | 40 | - name: Create artifacts 41 | if: success() && startsWith(github.ref, 'refs/tags/v') 42 | uses: actions/upload-artifact@master 43 | with: 44 | name: dist.tar.gz 45 | path: /tmp/dist.tar.gz 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | dist/ 3 | doc/ 4 | # dependencies 5 | node_modules 6 | .env 7 | 8 | # misc 9 | .DS_Store 10 | .idea 11 | npm-debug.log 12 | *.log 13 | .rpt2_cache 14 | *.sublime-project 15 | *.sublime-workspace 16 | 17 | 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 110, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/**/*.stories.js'], 3 | }; 4 | -------------------------------------------------------------------------------- /DatasetsInfo.md: -------------------------------------------------------------------------------- 1 | # Dataset information 2 | 3 | Most important info is Period / orbit time which is used for finding flyover times of orbits over a chosen area. 4 | Orbit time is the time in which the satellite makes 1 circle around the Earth (or in the missions with 2 satellites, half the circle around the Earth as both satellites travel on the same track with approximately 180° difference). 5 | 6 | # Copernicus 7 | 8 | Copernicus missions consist of 1 or 2 satellites. 9 | In mission with 2 satellites, the satellites are approximately 180° apart. 10 | In that case the orbit time needs to be halfed as both satellites are traveling on (almost) the same track. 11 | 12 | ## Sentinel-1 13 | 14 | Consists of 2 satellites: Sentinel-1A, Sentinel-1B 15 | 16 | - 1 satellite: 17 | - Orbital cycle: 12 days 18 | - Orbits per cycle: 175 19 | - Calculated time for 1 orbit: ~98.742 min (should be the same as Period) 20 | - Period (time to circle the Earth 1 time = 1 orbit): 98.6 min 21 | - Revisit time (of the same area) on the equator: 12 days 22 | 23 | - Both satellites: 24 | - Period: 98.6 min / 2 = **‭49.3 min** 25 | - Revisit time (of the same area): 12 days / 2 = 6 days 26 | 27 | - info sources: 28 | - https://www.esa.int/Applications/Observing_the_Earth/Copernicus/Sentinel-1/Facts_and_figures 29 | - https://sentinel.esa.int/web/sentinel/missions/sentinel-1/satellite-description/orbit 30 | 31 | ## Sentinel-2 32 | 33 | Consists of 2 satellites: Sentinel-2A, Sentinel-2B 34 | 35 | - 1 satellite: 36 | - Orbital cycle: 10 days 37 | - Orbits per cycle: 143 38 | - Calculated time for 1 orbit: ~100.699 min (should be the same as Period) 39 | - Period (time to circle the Earth 1 time by = 1 orbit): 100.6 min 40 | - Revisit time (of the same area) on the equator: 10 days 41 | 42 | - Both satellites: 43 | - Period: 100.6 min / 2 = **50.3 min** 44 | - Revisit time (of the same area): 10 days / 2 = 5 days 45 | 46 | - info sources: 47 | - https://www.esa.int/Applications/Observing_the_Earth/Copernicus/Sentinel-2/Facts_and_figures 48 | - https://sentinel.esa.int/web/sentinel/missions/sentinel-2/satellite-description/orbit 49 | 50 | ## Sentinel-3 51 | 52 | Consists of 2 satellites: Sentinel-3A, Sentinel-3B 53 | 54 | - 1 satellite: 55 | - Orbital cycle: 27 days 56 | - Orbits per cycle: 385 57 | - Calculated time for 1 orbit: ~100.987 min (should be the same as Period) 58 | - Period (time to circle the Earth 1 time = 1 orbit): 100.99 min 59 | - Revisit time (of the same area) on the equator : SLSTR ~1 day, OLCI ~2 days 60 | 61 | - Both satellites: 62 | - Period: 100.99 min / 2 = **50.495 min** 63 | 64 | - info sources: 65 | - https://www.esa.int/Applications/Observing_the_Earth/Copernicus/Sentinel-3/Facts_and_figures 66 | - https://sentinel.esa.int/web/sentinel/missions/sentinel-3/satellite-description/orbit 67 | 68 | 69 | ## Sentinel-5P 70 | 71 | Consists of 1 satellite 72 | 73 | - 1 satellite: 74 | - Orbital cycle: 16 days 75 | - Orbits per cycle: 227 (16 per day) 76 | - Calculated time for 1 orbit: ~101.498 min (should be the same as Period) 77 | - Period (time to circle the Earth 1 time = 1 orbit): **101 min** 78 | - Revisit time (of the same area) on the equator: ??? 79 | 80 | - info sources: 81 | - https://www.esa.int/Applications/Observing_the_Earth/Copernicus/Sentinel-5P/Facts_and_figures 82 | - https://sentinel.esa.int/web/sentinel/missions/sentinel-5p/orbit 83 | - https://www.esa.int/Enabling_Support/Operations/Sentinel-5P_operations 84 | 85 | # Landsat 86 | 87 | ## Landsat 5 88 | 89 | Consists of 1 satellite 90 | 91 | - 1 satellite: 92 | - Period (time to circle the Earth 1 time = 1 orbit): **99 min** 93 | - Revisit time (of the same area) at the equator: 16 days 94 | 95 | - info sources: 96 | - https://www.usgs.gov/land-resources/nli/landsat/landsat-5 97 | 98 | ## Landsat 7 99 | 100 | Consists of 1 satellite 101 | 102 | - 1 satellite: 103 | - Period (time to circle the Earth 1 time = 1 orbit): **99 min** 104 | - Revisit time (of the same area) at the equator: 16 days 105 | 106 | - info sources: 107 | - https://www.usgs.gov/land-resources/nli/landsat/landsat-7 108 | 109 | 110 | ## Landsat 8 111 | 112 | Consists of 1 satellite 113 | 114 | - 1 satellite: 115 | - Period (time to circle the Earth 1 time = 1 orbit): **99 min** 116 | - Revisit time (of the same area) at the equator: 16 days 117 | 118 | - info sources: 119 | - https://www.usgs.gov/land-resources/nli/landsat/landsat-8 120 | 121 | # Other 122 | 123 | ## MODIS 124 | 125 | Consists of 1 satellite 126 | 127 | - 1 satellite: 128 | - Period (time to circle the Earth 1 time = 1 orbit): **99 min** 129 | 130 | - info sources: 131 | - https://sos.noaa.gov/datasets/polar-orbiting-aqua-satellite-and-modis-swath/ 132 | 133 | ## Envisat Meris 134 | 135 | Consists of 1 satellite 136 | 137 | - 1 satellite: 138 | - Period (time to circle the Earth 1 time = 1 orbit): **100.16 min** 139 | 140 | - info sources: 141 | - https://en.wikipedia.org/wiki/Envisat 142 | 143 | ## DEM 144 | 145 | - not applicable -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sinergise Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/node/.gitignore: -------------------------------------------------------------------------------- 1 | image.jpeg -------------------------------------------------------------------------------- /jest-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-canvas-mock'; 2 | declare global { 3 | // eslint-disable-next-line @typescript-eslint/no-namespace 4 | namespace jest { 5 | interface Matchers { 6 | toHaveQueryParams(expectedParamsKeys: string[]): R; 7 | toHaveQueryParamsValues(expectedParams: Record): R; 8 | toHaveOrigin(expectedOrigin: string): R; 9 | toHaveBaseUrl(expectedPathName: string): R; 10 | } 11 | } 12 | } 13 | 14 | expect.extend({ 15 | toHaveQueryParams(received, expectedParamsKeys) { 16 | const { params } = parseUrl(received); 17 | for (let k of expectedParamsKeys) { 18 | if (params[k] === undefined) { 19 | return { 20 | message: () => `URL query parameter [${k}] should exist, but it doesn't`, 21 | pass: false, 22 | }; 23 | } 24 | } 25 | return { 26 | message: () => 27 | `URL [${received}] should not include all of the parameters ${JSON.stringify( 28 | expectedParamsKeys, 29 | )}, but it does`, 30 | pass: true, 31 | }; 32 | }, 33 | 34 | toHaveQueryParamsValues(received, expectedParams) { 35 | const { params } = parseUrl(received); 36 | for (let k in expectedParams) { 37 | if (String(params[k]) !== String(expectedParams[k])) { 38 | return { 39 | message: () => 40 | `URL query parameter [${k}] should have value [${expectedParams[k]}], instead it has value [${params[k]}]`, 41 | pass: false, 42 | }; 43 | } 44 | } 45 | return { 46 | message: () => 47 | `URL [${received}] should not include all of the values [${JSON.stringify( 48 | expectedParams, 49 | )}], but it does`, 50 | pass: true, 51 | }; 52 | }, 53 | 54 | toHaveOrigin(received, expectedOrigin) { 55 | const { origin } = parseUrl(received); 56 | if (origin !== expectedOrigin) { 57 | return { 58 | message: () => `URL hostname should have value [${expectedOrigin}], instead it has value [${origin}]`, 59 | pass: false, 60 | }; 61 | } 62 | return { 63 | message: () => `URL hostname should not have value [${expectedOrigin}], but it does`, // if .not is used 64 | pass: true, 65 | }; 66 | }, 67 | 68 | toHaveBaseUrl(received, expectedBaseUrl) { 69 | const { baseUrl } = parseUrl(received); 70 | if (baseUrl !== expectedBaseUrl) { 71 | return { 72 | message: () => 73 | `URL baseUrl should have value [${expectedBaseUrl}], instead it has value [${baseUrl}]`, 74 | pass: false, 75 | }; 76 | } 77 | return { 78 | message: () => `URL baseUrl should not have value [${expectedBaseUrl}], but it does`, // if .not is used 79 | pass: true, 80 | }; 81 | }, 82 | }); 83 | 84 | /* ************************ */ 85 | 86 | function parseUrl( 87 | urlWithQueryParams: string, 88 | ): { 89 | origin: string; 90 | baseUrl: string; 91 | params: Record; 92 | } { 93 | const url = new URL(urlWithQueryParams); 94 | let params: Record = {}; 95 | url.searchParams.forEach((value, key) => { 96 | params[key] = value; 97 | }); 98 | const baseUrl = `${url.origin}${url.pathname}`; 99 | return { 100 | origin: url.origin, 101 | baseUrl: baseUrl, 102 | params: params, 103 | }; 104 | } 105 | 106 | if (typeof window.URL.createObjectURL === 'undefined') { 107 | window.URL.createObjectURL = () => `blob:https://mocked-create-object-url.com/${Math.random()}`; 108 | window.URL.revokeObjectURL = () => {}; 109 | } 110 | 111 | document.createElement = (function(create) { 112 | return function() { 113 | const element: HTMLElement = create.apply(this, arguments); 114 | 115 | if (element.tagName === 'IMG') { 116 | setTimeout(() => { 117 | element.onload(new Event('load')); 118 | }, 100); 119 | } 120 | return element; 121 | }; 122 | })(document.createElement); 123 | 124 | export default undefined; 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sentinel-hub/sentinelhub-js", 3 | "version": "1.0.0", 4 | "main": "dist/sentinelHub.cjs", 5 | "module": "dist/sentinelHub.js", 6 | "browser": "dist/sentinelHub.umd.js", 7 | "type": "module", 8 | "peerDependencies": { 9 | "@turf/area": "^6.0.1", 10 | "@turf/helpers": "^6.1.4", 11 | "@types/proj4": "^2.5.2", 12 | "axios": "^0.21.1", 13 | "fast-xml-parser": "^4.4.1", 14 | "moment": "^2.24.0", 15 | "polygon-clipping": "^0.14.3", 16 | "proj4": "^2.9.0", 17 | "query-string": "^6.4.2", 18 | "terraformer-wkt-parser": "^1.2.1" 19 | }, 20 | "engines": { 21 | "node": ">=16" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.8.3", 25 | "@rollup/plugin-commonjs": "^26.0.1", 26 | "@rollup/plugin-json": "^6.1.0", 27 | "@rollup/plugin-node-resolve": "^15.2.3", 28 | "@storybook/html": "^5.3.3", 29 | "@types/jest": "^25.1.3", 30 | "@types/node-fetch": "^2.5.7", 31 | "@types/service-worker-mock": "^2.0.4", 32 | "@typescript-eslint/eslint-plugin": "^7.17.0", 33 | "@typescript-eslint/parser": "^7.17.0", 34 | "axios-mock-adapter": "^1.18.1", 35 | "babel-loader": "^8.0.6", 36 | "concurrently": "^5.1.0", 37 | "dotenv": "^8.2.0", 38 | "eslint": "^8.57.0", 39 | "eslint-config-prettier": "^9.1.0", 40 | "eslint-plugin-prettier": "^5.2.1", 41 | "jest": "^29.7.0", 42 | "jest-canvas-mock": "^2.3.1", 43 | "jest-environment-jsdom": "^29.7.0", 44 | "node-fetch": "^2.6.0", 45 | "prettier": "^3.3.3", 46 | "rollup": "^4.19.0", 47 | "rollup-plugin-commonjs": "^10.1.0", 48 | "rollup-plugin-node-resolve": "^5.2.0", 49 | "rollup-plugin-typescript2": "^0.36.0", 50 | "service-worker-mock": "^2.0.5", 51 | "ts-jest": "29.2.3", 52 | "typedoc": "^0.26.5", 53 | "typescript": "5.5.4" 54 | }, 55 | "scripts": { 56 | "build": "rollup -c", 57 | "dev": "rollup -c -w", 58 | "test": "TZ=Europe/Ljubljana jest", 59 | "lint": "eslint --max-warnings 0 --ext .ts,js src/", 60 | "prettier": "prettier --write \"{src,example,stories}/**/*.{ts,js}\"", 61 | "storybook": "concurrently --kill-others \"rollup -c -w\" \"start-storybook -p 6006 --quiet\"", 62 | "build-storybook": "build-storybook", 63 | "build-doc": "typedoc --excludePrivate --excludeProtected --exclude '**/*__*' --out doc/ src/" 64 | }, 65 | "jest": { 66 | "moduleFileExtensions": [ 67 | "ts", 68 | "tsx", 69 | "js" 70 | ], 71 | "transform": { 72 | "\\.(ts|tsx)$": "ts-jest" 73 | }, 74 | "moduleDirectories": [ 75 | "node_modules", 76 | "" 77 | ], 78 | "testRegex": "/__tests__/.*\\.(ts|tsx|js)$", 79 | "testPathIgnorePatterns": [ 80 | "/node_modules/", 81 | "/dist/", 82 | "fixtures[.].*[.]ts", 83 | "testUtils[.].*[.]ts" 84 | ], 85 | "testEnvironment": "jsdom" 86 | }, 87 | "types": "dist/src/index.d.ts", 88 | "files": [ 89 | "dist", 90 | "dist/src/index.d.ts" 91 | ], 92 | "optionalDependencies": { 93 | "@rollup/rollup-linux-x64-gnu": "^4.19.2" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import pkg from './package.json' with { type: "json" }; 3 | 4 | export default { 5 | input: 'src/index.ts', 6 | output: [ 7 | { 8 | file: pkg.main, 9 | format: 'cjs', 10 | }, 11 | { 12 | file: pkg.module, 13 | format: 'es', 14 | } 15 | ], 16 | plugins: [ 17 | typescript(), 18 | ], 19 | external: [ 20 | ...Object.keys(pkg.peerDependencies || {}), 21 | ], 22 | onwarn: function(warning) { 23 | // Skip certain warnings 24 | 25 | // should intercept ... but doesn't in some rollup versions 26 | if (warning.code === 'THIS_IS_UNDEFINED') { 27 | return; 28 | } 29 | 30 | // console.warn everything else 31 | console.warn(warning.message); 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | let authToken: string | null = null; 4 | 5 | export function getAuthToken(): string | null { 6 | return authToken; 7 | } 8 | 9 | export function setAuthToken(newAuthToken: string | null): void { 10 | authToken = newAuthToken; 11 | } 12 | 13 | export function isAuthTokenSet(): boolean { 14 | return authToken !== null; 15 | } 16 | 17 | export async function requestAuthToken(clientId: string, clientSecret: string): Promise { 18 | const response = await axios({ 19 | method: 'post', 20 | url: 'https://services.sentinel-hub.com/auth/realms/main/protocol/openid-connect/token', 21 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 22 | data: `grant_type=client_credentials&client_id=${encodeURIComponent( 23 | clientId, 24 | )}&client_secret=${encodeURIComponent(clientSecret)}`, 25 | }); 26 | return response.data.access_token; 27 | } 28 | -------------------------------------------------------------------------------- /src/bbox.ts: -------------------------------------------------------------------------------- 1 | import { CRS } from './crs'; 2 | import { Polygon } from '@turf/helpers'; 3 | 4 | export class BBox { 5 | public crs: CRS; 6 | public minX: number; 7 | public minY: number; 8 | public maxX: number; 9 | public maxY: number; 10 | 11 | public constructor(crs: CRS, minX: number, minY: number, maxX: number, maxY: number) { 12 | if (minX >= maxX) { 13 | throw new Error('MinX should be lower than maxX'); 14 | } 15 | if (minY >= maxY) { 16 | throw new Error('MinY should be lower than maxY'); 17 | } 18 | this.crs = crs; 19 | this.minX = minX; 20 | this.maxX = maxX; 21 | this.minY = minY; 22 | this.maxY = maxY; 23 | } 24 | 25 | // Note that Turf's Polygon type (which is basically what we are returning) doesn't 26 | // allow 'crs' property, so we must return type :any. 27 | public toGeoJSON(): Polygon { 28 | return { 29 | type: 'Polygon', 30 | crs: { type: 'name', properties: { name: this.crs.urn } }, 31 | coordinates: [ 32 | [ 33 | [this.minX, this.minY], 34 | [this.maxX, this.minY], 35 | [this.maxX, this.maxY], 36 | [this.minX, this.maxY], 37 | [this.minX, this.minY], 38 | ], 39 | ], 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/crs.ts: -------------------------------------------------------------------------------- 1 | export type CRS = { 2 | authId: CRS_IDS; 3 | auth: string; 4 | srid: number; 5 | urn: CRS_URN; 6 | opengisUrl: string; 7 | }; 8 | 9 | export type CRS_IDS = 'EPSG:3857' | 'CRS:84' | 'EPSG:4326'; 10 | 11 | export type CRS_URN = 12 | | 'urn:ogc:def:crs:EPSG::3857' 13 | | 'urn:ogc:def:crs:EPSG::4326' 14 | | 'urn:ogc:def:crs:OGC:1.3:CRS84'; 15 | 16 | /** 17 | * The most common CRS for online maps, used by almost all free and commercial tile providers. Uses Spherical Mercator projection. 18 | */ 19 | export const CRS_EPSG3857: CRS = { 20 | authId: 'EPSG:3857', 21 | auth: 'EPSG', 22 | srid: 3857, 23 | urn: 'urn:ogc:def:crs:EPSG::3857', 24 | opengisUrl: 'http://www.opengis.net/def/crs/EPSG/0/3857', 25 | }; 26 | 27 | /** 28 | * EPSG:4326 is identifier of World Geodetic System (WGS84) which comprises of a reference ellipsoid, a standard coordinate system, altitude data and a geoid. 29 | */ 30 | export const CRS_EPSG4326: CRS = { 31 | authId: 'EPSG:4326', 32 | auth: 'EPSG', 33 | srid: 4326, 34 | urn: 'urn:ogc:def:crs:EPSG::4326', 35 | opengisUrl: 'http://www.opengis.net/def/crs/EPSG/0/4326', 36 | }; 37 | 38 | /** 39 | * Same as EPSG:4326, but with a switched coordinate order. 40 | */ 41 | export const CRS_WGS84: CRS = { 42 | authId: 'CRS:84', 43 | auth: 'CRS', 44 | srid: 84, 45 | urn: 'urn:ogc:def:crs:OGC:1.3:CRS84', 46 | opengisUrl: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', 47 | }; 48 | 49 | export const SUPPORTED_CRS_OBJ = { 50 | [CRS_EPSG3857.authId]: CRS_EPSG3857, 51 | [CRS_EPSG4326.authId]: CRS_EPSG4326, 52 | [CRS_WGS84.authId]: CRS_WGS84, 53 | }; 54 | 55 | declare module '@turf/helpers' { 56 | export interface GeometryObject { 57 | crs?: { 58 | type: 'name'; 59 | properties: { 60 | name: CRS_URN; 61 | }; 62 | }; 63 | } 64 | } 65 | 66 | export function findCrsFromUrn(urn: CRS_URN): CRS { 67 | switch (urn) { 68 | case 'urn:ogc:def:crs:EPSG::3857': 69 | return CRS_EPSG3857; 70 | case 'urn:ogc:def:crs:EPSG::4326': 71 | return CRS_EPSG4326; 72 | case 'urn:ogc:def:crs:OGC:1.3:CRS84': 73 | return CRS_WGS84; 74 | default: 75 | throw new Error('CRS not found'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/dataimport/AirbusDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { TPDProvider, TPDISearchParams } from './const'; 2 | import { AbstractTPDProvider } from './TPDProvider'; 3 | 4 | export class AirbusDataProvider extends AbstractTPDProvider { 5 | public constructor() { 6 | super(); 7 | this.provider = TPDProvider.AIRBUS; 8 | } 9 | 10 | protected getAdditionalSearchParams(params: TPDISearchParams): any { 11 | const data: any = {}; 12 | 13 | //constellation is a required parameter 14 | if (!params.constellation) { 15 | throw new Error('Parameter constellation must be specified'); 16 | } 17 | data.constellation = params.constellation; 18 | 19 | //datafilter 20 | const dataFilter: any = {}; 21 | 22 | if (!params.fromTime) { 23 | throw new Error('Parameter fromTime must be specified'); 24 | } 25 | 26 | if (!params.toTime) { 27 | throw new Error('Parameter toTime must be specified'); 28 | } 29 | 30 | dataFilter.timeRange = { 31 | from: params.fromTime.toISOString(), 32 | to: params.toTime.toISOString(), 33 | }; 34 | 35 | if (!isNaN(params.maxCloudCoverage)) { 36 | dataFilter.maxCloudCoverage = params.maxCloudCoverage; 37 | } 38 | 39 | if (!!params.processingLevel) { 40 | dataFilter.processingLevel = params.processingLevel; 41 | } 42 | 43 | if (!isNaN(params.maxSnowCoverage)) { 44 | dataFilter.maxSnowCoverage = params.maxSnowCoverage; 45 | } 46 | 47 | if (!isNaN(params.maxIncidenceAngle)) { 48 | dataFilter.maxIncidenceAngle = params.maxIncidenceAngle; 49 | } 50 | 51 | if (!!params.expiredFromTime && !!params.expiredToTime) { 52 | dataFilter.expirationDate = { 53 | from: params.expiredFromTime.toISOString(), 54 | to: params.expiredToTime.toISOString(), 55 | }; 56 | } 57 | 58 | data.dataFilter = dataFilter; 59 | 60 | return { data: [data] }; 61 | } 62 | 63 | protected getAdditionalTransactionParams(items: string[], searchParams: TPDISearchParams): any { 64 | const input = this.getSearchPayload(searchParams); 65 | if (!!items && items.length) { 66 | const dataObject = input.data[0]; 67 | dataObject.products = items.map((item) => ({ id: item })); 68 | delete dataObject.dataFilter; 69 | } 70 | return input; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/dataimport/MaxarDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { TPDProvider, TPDISearchParams, MaxarProductBands, TPDITransactionParams } from './const'; 2 | import { AbstractTPDProvider } from './TPDProvider'; 3 | 4 | export class MaxarDataProvider extends AbstractTPDProvider { 5 | public constructor() { 6 | super(); 7 | this.provider = TPDProvider.MAXAR; 8 | } 9 | 10 | public addSearchPagination(): void {} 11 | 12 | protected getAdditionalSearchParams(params: TPDISearchParams): any { 13 | const data: any = {}; 14 | 15 | //productBands is a required parameter with value of MaxarProductBands 16 | data.productBands = MaxarProductBands; 17 | 18 | //datafilter 19 | const dataFilter: any = {}; 20 | 21 | if (!params.fromTime) { 22 | throw new Error('Parameter fromTime must be specified'); 23 | } 24 | 25 | if (!params.toTime) { 26 | throw new Error('Parameter toTime must be specified'); 27 | } 28 | 29 | dataFilter.timeRange = { 30 | from: params.fromTime.toISOString(), 31 | to: params.toTime.toISOString(), 32 | }; 33 | 34 | if (!isNaN(params.maxCloudCoverage)) { 35 | dataFilter.maxCloudCoverage = params.maxCloudCoverage; 36 | } 37 | 38 | if (!isNaN(params.minOffNadir)) { 39 | dataFilter.minOffNadir = params.minOffNadir; 40 | } 41 | 42 | if (!isNaN(params.maxOffNadir)) { 43 | dataFilter.maxOffNadir = params.maxOffNadir; 44 | } 45 | 46 | if (!isNaN(params.minSunElevation)) { 47 | dataFilter.minSunElevation = params.minSunElevation; 48 | } 49 | 50 | if (!isNaN(params.maxSunElevation)) { 51 | dataFilter.maxSunElevation = params.maxSunElevation; 52 | } 53 | 54 | if (!!params.sensor) { 55 | dataFilter.sensor = params.sensor; 56 | } 57 | 58 | data.dataFilter = dataFilter; 59 | 60 | return { data: [data] }; 61 | } 62 | 63 | protected getAdditionalTransactionParams( 64 | items: string[], 65 | searchParams: TPDISearchParams, 66 | transactionParams: TPDITransactionParams, 67 | ): any { 68 | const input = this.getSearchPayload(searchParams); 69 | const dataObject = input.data[0]; 70 | 71 | if (transactionParams?.productKernel) { 72 | dataObject.productKernel = transactionParams.productKernel; 73 | } 74 | 75 | if (!!items && items.length) { 76 | dataObject.selectedImages = items; 77 | delete dataObject.dataFilter; 78 | } 79 | 80 | return input; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/dataimport/PlanetDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { TPDProvider, TPDISearchParams, PlanetSupportedProductBundles, TPDITransactionParams } from './const'; 2 | import { AbstractTPDProvider } from './TPDProvider'; 3 | 4 | export class PlanetDataProvider extends AbstractTPDProvider { 5 | public constructor() { 6 | super(); 7 | this.provider = TPDProvider.PLANET; 8 | } 9 | 10 | protected getCommonSearchParams(params: TPDISearchParams): any { 11 | const payload: any = super.getCommonSearchParams(params); 12 | 13 | if (params.planetApiKey) { 14 | payload.planetApiKey = params.planetApiKey; 15 | } 16 | 17 | return payload; 18 | } 19 | 20 | protected getAdditionalSearchParams(params: TPDISearchParams): any { 21 | const data: any = {}; 22 | 23 | //itemType is a required parameter 24 | if (!params.itemType) { 25 | throw new Error('Parameter itemType must be specified'); 26 | } 27 | 28 | data.itemType = params.itemType; 29 | 30 | //productBundle is a required parameter 31 | if (!params.productBundle) { 32 | throw new Error('Parameter productBundle must be specified'); 33 | } 34 | 35 | data.productBundle = params.productBundle; 36 | 37 | //check if productBundle is supported for selected itemType 38 | if ( 39 | PlanetSupportedProductBundles[params.itemType] && 40 | !PlanetSupportedProductBundles[params.itemType].includes(params.productBundle) 41 | ) { 42 | throw new Error(`Product bundle is not supported for selected item type`); 43 | } 44 | 45 | //datafilter 46 | const dataFilter: any = {}; 47 | 48 | if (!params.fromTime) { 49 | throw new Error('Parameter fromTime must be specified'); 50 | } 51 | 52 | if (!params.toTime) { 53 | throw new Error('Parameter toTime must be specified'); 54 | } 55 | 56 | dataFilter.timeRange = { 57 | from: params.fromTime.toISOString(), 58 | to: params.toTime.toISOString(), 59 | }; 60 | 61 | if (!isNaN(params.maxCloudCoverage)) { 62 | dataFilter.maxCloudCoverage = params.maxCloudCoverage; 63 | } 64 | 65 | if (!!params.nativeFilter) { 66 | dataFilter.nativeFilter = params.nativeFilter; 67 | } 68 | 69 | data.dataFilter = dataFilter; 70 | 71 | return { 72 | data: [data], 73 | }; 74 | } 75 | 76 | protected getAdditionalTransactionParams( 77 | items: string[], 78 | searchParams: TPDISearchParams, 79 | transactionParams: TPDITransactionParams, 80 | ): any { 81 | const input = this.getSearchPayload(searchParams); 82 | const dataObject = input.data[0]; 83 | 84 | if (transactionParams?.harmonizeTo) { 85 | dataObject.harmonizeTo = transactionParams.harmonizeTo; 86 | } 87 | 88 | if (!transactionParams?.planetApiKey) { 89 | throw new Error('Parameter planetApiKey must be specified'); 90 | } 91 | 92 | input.planetApiKey = transactionParams.planetApiKey; 93 | 94 | if (!!items && items.length) { 95 | dataObject.itemIds = items; 96 | delete dataObject.dataFilter; 97 | } 98 | return input; 99 | } 100 | public checkSubscriptionsSupported(): boolean { 101 | return true; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/dataimport/PlanetaryVariablesDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { TPDProvider, TPDISearchParams, TPDITransactionParams, PlanetSupportedPVIds } from './const'; 2 | import { AbstractTPDProvider } from './TPDProvider'; 3 | 4 | export class PlanetaryVariablesDataProvider extends AbstractTPDProvider { 5 | public constructor() { 6 | super(); 7 | this.provider = TPDProvider.PLANETARY_VARIABLES; 8 | } 9 | 10 | protected getSearchParamsProvider(): TPDProvider { 11 | return TPDProvider.PLANET; 12 | } 13 | 14 | protected getAdditionalSearchParams(params: TPDISearchParams): any { 15 | const data: any = {}; 16 | 17 | if (!params.type) { 18 | throw new Error('Parameter type must be specified'); 19 | } 20 | 21 | if (!params.id) { 22 | throw new Error('Parameter id must be specified'); 23 | } 24 | 25 | data.type = params.type; 26 | data.id = params.id; 27 | 28 | //check if id is supported for selected type 29 | if (PlanetSupportedPVIds[params.type] && !PlanetSupportedPVIds[params.type].includes(params.id)) { 30 | throw new Error(`Source ID is not supported for selected Source Type`); 31 | } 32 | 33 | //datafilter 34 | const dataFilter: any = {}; 35 | 36 | if (!params.fromTime) { 37 | throw new Error('Parameter fromTime must be specified'); 38 | } 39 | 40 | if (!params.toTime) { 41 | throw new Error('Parameter toTime must be specified'); 42 | } 43 | 44 | dataFilter.timeRange = { 45 | from: params.fromTime.toISOString(), 46 | to: params.toTime.toISOString(), 47 | }; 48 | 49 | data.dataFilter = dataFilter; 50 | 51 | return { 52 | data: [data], 53 | }; 54 | } 55 | 56 | protected getAdditionalTransactionParams( 57 | items: string[], 58 | searchParams: TPDISearchParams, 59 | transactionParams: TPDITransactionParams, 60 | ): any { 61 | const input = this.getSearchPayload(searchParams); 62 | const dataObject = input.data[0]; 63 | 64 | if (!transactionParams?.planetApiKey) { 65 | throw new Error('Parameter planetApiKey must be specified'); 66 | } 67 | 68 | input.planetApiKey = transactionParams.planetApiKey; 69 | 70 | if (!!items && items.length) { 71 | dataObject.itemIds = items; 72 | delete dataObject.dataFilter; 73 | } 74 | return input; 75 | } 76 | public checkSubscriptionsSupported(): boolean { 77 | return true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/dataimport/TPDProvider.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | import { TPDProvider, TPDISearchParams, TPDITransactionParams } from './const'; 3 | 4 | export interface TPDProviderInterface { 5 | getSearchPayload(params: TPDISearchParams): any; 6 | getTransactionPayload( 7 | name: string, 8 | collectionId: string, 9 | items: string[], 10 | searchParams: TPDISearchParams, 11 | transactionParams?: TPDITransactionParams, 12 | ): any; 13 | addSearchPagination(requestConfig: AxiosRequestConfig, count: number, viewtoken: string): void; 14 | checkSubscriptionsSupported(): boolean; 15 | } 16 | 17 | export abstract class AbstractTPDProvider implements TPDProviderInterface { 18 | protected provider: TPDProvider; 19 | 20 | public getProvider(): TPDProvider { 21 | return this.provider; 22 | } 23 | 24 | public addSearchPagination(requestConfig: AxiosRequestConfig, count: number, viewtoken: string): void { 25 | let queryParams: Record = {}; 26 | //set page size 27 | if (!isNaN(count)) { 28 | queryParams.count = count; 29 | } 30 | 31 | //set offset 32 | if (viewtoken) { 33 | queryParams.viewtoken = viewtoken; 34 | } 35 | requestConfig.params = queryParams; 36 | } 37 | 38 | protected getSearchParamsProvider(): TPDProvider { 39 | return this.provider; 40 | } 41 | 42 | protected getCommonSearchParams(params: TPDISearchParams): any { 43 | const payload: any = {}; 44 | //provider 45 | payload['provider'] = this.getSearchParamsProvider(); 46 | 47 | //bounds 48 | //Defines the request bounds by specifying the bounding box and/or geometry for the request. 49 | 50 | if (!params.bbox && !params.geometry) { 51 | throw new Error('Parameter bbox and/or geometry must be specified'); 52 | } 53 | 54 | const bounds: any = {}; 55 | 56 | if (!!params.bbox) { 57 | bounds.bbox = [params.bbox.minX, params.bbox.minY, params.bbox.maxX, params.bbox.maxY]; 58 | bounds.properties = { 59 | crs: params.bbox.crs.opengisUrl, 60 | }; 61 | } 62 | 63 | if (!!params.geometry) { 64 | if (!params.crs) { 65 | throw new Error('Parameter crs must be specified'); 66 | } 67 | bounds.geometry = params.geometry; 68 | bounds.properties = { 69 | crs: params.crs.opengisUrl, 70 | }; 71 | } 72 | 73 | payload.bounds = bounds; 74 | 75 | return payload; 76 | } 77 | 78 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 79 | protected getAdditionalSearchParams(params: TPDISearchParams): any { 80 | return {}; 81 | } 82 | 83 | public getSearchPayload(params: TPDISearchParams): any { 84 | const commonParams = this.getCommonSearchParams(params); 85 | const additionalParams = this.getAdditionalSearchParams(params); 86 | const payload = { ...commonParams, ...additionalParams }; 87 | return payload; 88 | } 89 | 90 | protected getAdditionalTransactionParams( 91 | items: string[], // eslint-disable-line @typescript-eslint/no-unused-vars 92 | searchParams: TPDISearchParams, // eslint-disable-line @typescript-eslint/no-unused-vars 93 | transactionParams: TPDITransactionParams, // eslint-disable-line @typescript-eslint/no-unused-vars 94 | ): any { 95 | return {}; 96 | } 97 | 98 | public getTransactionPayload( 99 | name: string, 100 | collectionId: string, 101 | items: string[], 102 | searchParams: TPDISearchParams, 103 | transactionParams: TPDITransactionParams | null = null, 104 | ): any { 105 | const payload: any = {}; 106 | 107 | if (!!name) { 108 | payload.name = name; 109 | } 110 | 111 | if (!!collectionId) { 112 | payload.collectionId = collectionId; 113 | } 114 | payload.input = this.getAdditionalTransactionParams(items, searchParams, transactionParams); 115 | 116 | return payload; 117 | } 118 | 119 | public checkSubscriptionsSupported(): boolean { 120 | throw new Error('Subscriptions are not supported for selected provider'); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/dataimport/__tests__/testUtils.AirbusDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { TPDISearchParams, TPDProvider } from '../const'; 2 | 3 | export function checkSearchPayload(requestData: any, params: TPDISearchParams): void { 4 | expect(requestData.provider).toStrictEqual(TPDProvider.AIRBUS); 5 | if (!!params.bbox) { 6 | expect(requestData.bounds.bbox).toStrictEqual([ 7 | params.bbox.minX, 8 | params.bbox.minY, 9 | params.bbox.maxX, 10 | params.bbox.maxY, 11 | ]); 12 | } 13 | if (!!params.geometry) { 14 | expect(requestData.bounds.geometry).toStrictEqual(params.geometry); 15 | } 16 | expect(requestData.bounds.properties.crs).toStrictEqual(params.bbox.crs.opengisUrl); 17 | const dataObject = requestData.data[0]; 18 | const { dataFilter } = dataObject; 19 | expect(dataObject.constellation).toStrictEqual(params.constellation); 20 | expect(dataFilter.timeRange.from).toStrictEqual(params.fromTime.toISOString()); 21 | expect(dataFilter.timeRange.to).toStrictEqual(params.toTime.toISOString()); 22 | 23 | if (!isNaN(params.maxCloudCoverage)) { 24 | expect(dataFilter.maxCloudCoverage).toBeDefined(); 25 | expect(dataFilter.maxCloudCoverage).toStrictEqual(params.maxCloudCoverage); 26 | } else { 27 | expect(dataFilter.maxCloudCoverage).toBeUndefined(); 28 | } 29 | 30 | if (!!params.processingLevel) { 31 | expect(dataFilter.processingLevel).toBeDefined(); 32 | expect(dataFilter.processingLevel).toStrictEqual(params.processingLevel); 33 | } else { 34 | expect(dataFilter.processingLevel).toBeUndefined(); 35 | } 36 | 37 | if (!isNaN(params.maxSnowCoverage)) { 38 | expect(dataFilter.maxSnowCoverage).toBeDefined(); 39 | expect(dataFilter.maxSnowCoverage).toStrictEqual(params.maxSnowCoverage); 40 | } else { 41 | expect(dataFilter.maxSnowCoverage).toBeUndefined(); 42 | } 43 | 44 | if (!isNaN(params.maxIncidenceAngle)) { 45 | expect(dataFilter.maxIncidenceAngle).toBeDefined(); 46 | expect(dataFilter.maxIncidenceAngle).toStrictEqual(params.maxIncidenceAngle); 47 | } else { 48 | expect(dataFilter.maxIncidenceAngle).toBeUndefined(); 49 | } 50 | 51 | if (!!params.expiredFromTime && !!params.expiredToTime) { 52 | expect(dataFilter.expirationDate).toBeDefined(); 53 | expect(dataFilter.expirationDate.from).toStrictEqual(params.expiredFromTime.toISOString()); 54 | expect(dataFilter.expirationDate.to).toStrictEqual(params.expiredToTime.toISOString()); 55 | } else { 56 | expect(dataFilter.expirationDate).toBeUndefined(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/dataimport/__tests__/testUtils.MaxarDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { TPDISearchParams, TPDProvider } from '../const'; 2 | 3 | export function checkSearchPayload(requestData: any, params: TPDISearchParams): void { 4 | expect(requestData.provider).toStrictEqual(TPDProvider.MAXAR); 5 | if (!!params.bbox) { 6 | expect(requestData.bounds.bbox).toStrictEqual([ 7 | params.bbox.minX, 8 | params.bbox.minY, 9 | params.bbox.maxX, 10 | params.bbox.maxY, 11 | ]); 12 | } 13 | if (!!params.geometry) { 14 | expect(requestData.bounds.geometry).toStrictEqual(params.geometry); 15 | } 16 | expect(requestData.bounds.properties.crs).toStrictEqual(params.bbox.crs.opengisUrl); 17 | const dataObject = requestData.data[0]; 18 | const { dataFilter } = dataObject; 19 | expect(dataFilter.timeRange.from).toStrictEqual(params.fromTime.toISOString()); 20 | expect(dataFilter.timeRange.to).toStrictEqual(params.toTime.toISOString()); 21 | 22 | if (!isNaN(params.maxCloudCoverage)) { 23 | expect(dataFilter.maxCloudCoverage).toBeDefined(); 24 | expect(dataFilter.maxCloudCoverage).toStrictEqual(params.maxCloudCoverage); 25 | } else { 26 | expect(dataFilter.maxCloudCoverage).toBeUndefined(); 27 | } 28 | 29 | if (!isNaN(params.minOffNadir)) { 30 | expect(dataFilter.minOffNadir).toBeDefined(); 31 | expect(dataFilter.minOffNadir).toStrictEqual(params.minOffNadir); 32 | } else { 33 | expect(dataFilter.minOffNadir).toBeUndefined(); 34 | } 35 | 36 | if (!isNaN(params.maxOffNadir)) { 37 | expect(dataFilter.maxOffNadir).toBeDefined(); 38 | expect(dataFilter.maxOffNadir).toStrictEqual(params.maxOffNadir); 39 | } else { 40 | expect(dataFilter.maxOffNadir).toBeUndefined(); 41 | } 42 | 43 | if (!isNaN(params.minSunElevation)) { 44 | expect(dataFilter.minSunElevation).toBeDefined(); 45 | expect(dataFilter.minSunElevation).toStrictEqual(params.minSunElevation); 46 | } else { 47 | expect(dataFilter.minSunElevation).toBeUndefined(); 48 | } 49 | 50 | if (!isNaN(params.maxSunElevation)) { 51 | expect(dataFilter.maxSunElevation).toBeDefined(); 52 | expect(dataFilter.maxSunElevation).toStrictEqual(params.maxSunElevation); 53 | } else { 54 | expect(dataFilter.maxSunElevation).toBeUndefined(); 55 | } 56 | 57 | if (!!params.sensor) { 58 | expect(dataFilter.sensor).toBeDefined(); 59 | expect(dataFilter.sensor).toStrictEqual(params.sensor); 60 | } else { 61 | expect(dataFilter.sensor).toBeUndefined(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/dataimport/__tests__/testUtils.PlanetDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { TPDISearchParams, TPDProvider } from '../const'; 2 | 3 | export function checkSearchPayload(requestData: any, params: TPDISearchParams): void { 4 | expect(requestData.provider).toStrictEqual(TPDProvider.PLANET); 5 | 6 | if (!!params.bbox) { 7 | expect(requestData.bounds.bbox).toStrictEqual([ 8 | params.bbox.minX, 9 | params.bbox.minY, 10 | params.bbox.maxX, 11 | params.bbox.maxY, 12 | ]); 13 | } 14 | if (!!params.geometry) { 15 | expect(requestData.bounds.geometry).toStrictEqual(params.geometry); 16 | } 17 | expect(requestData.bounds.properties.crs).toStrictEqual(params.bbox.crs.opengisUrl); 18 | const dataObject = requestData.data[0]; 19 | 20 | if (!!params.itemType) { 21 | expect(dataObject.itemType).toStrictEqual(params.itemType); 22 | } 23 | 24 | const { dataFilter } = dataObject; 25 | expect(dataFilter.timeRange.from).toStrictEqual(params.fromTime.toISOString()); 26 | expect(dataFilter.timeRange.to).toStrictEqual(params.toTime.toISOString()); 27 | 28 | if (!isNaN(params.maxCloudCoverage)) { 29 | expect(dataFilter.maxCloudCoverage).toBeDefined(); 30 | expect(dataFilter.maxCloudCoverage).toStrictEqual(params.maxCloudCoverage); 31 | } else { 32 | expect(dataFilter.maxCloudCoverage).toBeUndefined(); 33 | } 34 | 35 | if (!!params.nativeFilter) { 36 | expect(dataFilter.nativeFilter).toBeDefined(); 37 | expect(dataFilter.nativeFilter).toStrictEqual(params.nativeFilter); 38 | } else { 39 | expect(dataFilter.nativeFilter).toBeUndefined(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/layer/AbstractLandsat8Layer.ts: -------------------------------------------------------------------------------- 1 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 2 | import { Link, LinkType } from './const'; 3 | 4 | export class AbstractLandsat8Layer extends AbstractSentinelHubV3WithCCLayer { 5 | protected getTileLinks(tile: Record): Link[] { 6 | return [ 7 | { 8 | target: tile.dataUri, 9 | type: LinkType.AWS, 10 | }, 11 | { 12 | target: `${tile.dataUri}_thumb_small.jpg`, 13 | type: LinkType.PREVIEW, 14 | }, 15 | ]; 16 | } 17 | 18 | protected extractFindTilesMeta(tile: any): Record { 19 | return { 20 | ...super.extractFindTilesMeta(tile), 21 | sunElevation: tile.sunElevation, 22 | }; 23 | } 24 | 25 | protected extractFindTilesMetaFromCatalog(feature: Record): Record { 26 | return { 27 | ...super.extractFindTilesMetaFromCatalog(feature), 28 | sunElevation: feature.properties['view:sun_elevation'], 29 | projEpsg: feature.properties['proj:epsg'], 30 | }; 31 | } 32 | 33 | protected getTileLinksFromCatalog(feature: Record): Link[] { 34 | const { assets } = feature; 35 | let result: Link[] = super.getTileLinksFromCatalog(feature); 36 | 37 | if (assets.data && assets.data.href) { 38 | result.push({ 39 | target: assets.data.href.replace('/index.html', `/${feature.id}_thumb_small.jpg`), 40 | type: LinkType.PREVIEW, 41 | }); 42 | } 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/layer/AbstractSentinelHubV1OrV2WithCCLayer.ts: -------------------------------------------------------------------------------- 1 | import { MosaickingOrder } from './const'; 2 | import { AbstractSentinelHubV1OrV2Layer } from './AbstractSentinelHubV1OrV2Layer'; 3 | 4 | interface ConstructorParameters { 5 | instanceId?: string | null; 6 | layerId?: string | null; 7 | evalscript?: string | null; 8 | evalscriptUrl?: string | null; 9 | title?: string | null; 10 | description?: string | null; 11 | maxCloudCoverPercent?: number | null; 12 | mosaickingOrder?: MosaickingOrder | null; 13 | } 14 | 15 | // same as AbstractSentinelHubV1OrV2Layer, but with maxCloudCoverPercent (useful for Landsat datasets) 16 | export class AbstractSentinelHubV1OrV2WithCCLayer extends AbstractSentinelHubV1OrV2Layer { 17 | public maxCloudCoverPercent: number; 18 | 19 | public constructor({ maxCloudCoverPercent = 100, ...rest }: ConstructorParameters) { 20 | super(rest); 21 | this.maxCloudCoverPercent = maxCloudCoverPercent; 22 | } 23 | 24 | protected getWmsGetMapUrlAdditionalParameters(): Record { 25 | return { 26 | ...super.getWmsGetMapUrlAdditionalParameters(), 27 | maxcc: this.maxCloudCoverPercent, 28 | }; 29 | } 30 | 31 | protected getFindTilesAdditionalParameters(): Record { 32 | return { 33 | maxcc: this.maxCloudCoverPercent / 100, 34 | }; 35 | } 36 | 37 | protected extractFindTilesMeta(tile: any): Record { 38 | return { 39 | cloudCoverPercent: tile.cloudCoverPercentage, 40 | }; 41 | } 42 | 43 | protected async getFindDatesUTCAdditionalParameters(): Promise> { 44 | return { 45 | maxcc: this.maxCloudCoverPercent / 100, 46 | }; 47 | } 48 | 49 | public getStatsAdditionalParameters(): Record { 50 | return { 51 | maxcc: this.maxCloudCoverPercent, 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/layer/AbstractSentinelHubV3WithCCLayer.ts: -------------------------------------------------------------------------------- 1 | import { MosaickingOrder, DataProductId, FindTilesAdditionalParameters } from './const'; 2 | import { AbstractSentinelHubV3Layer } from './AbstractSentinelHubV3Layer'; 3 | import { ProcessingPayload } from './processing'; 4 | import { RequestConfiguration } from '../utils/cancelRequests'; 5 | 6 | interface ConstructorParameters { 7 | instanceId?: string | null; 8 | layerId?: string | null; 9 | evalscript?: string | null; 10 | evalscriptUrl?: string | null; 11 | dataProduct?: DataProductId | null; 12 | mosaickingOrder?: MosaickingOrder | null; 13 | title?: string | null; 14 | description?: string | null; 15 | legendUrl?: string | null; 16 | maxCloudCoverPercent?: number | null; 17 | } 18 | 19 | // same as AbstractSentinelHubV3Layer, but with maxCloudCoverPercent (for layers which support it) 20 | export class AbstractSentinelHubV3WithCCLayer extends AbstractSentinelHubV3Layer { 21 | public maxCloudCoverPercent: number; 22 | 23 | public constructor({ maxCloudCoverPercent = 100, ...rest }: ConstructorParameters) { 24 | super(rest); 25 | this.maxCloudCoverPercent = maxCloudCoverPercent; 26 | } 27 | 28 | protected getWmsGetMapUrlAdditionalParameters(): Record { 29 | return { 30 | ...super.getWmsGetMapUrlAdditionalParameters(), 31 | maxcc: this.maxCloudCoverPercent, 32 | }; 33 | } 34 | 35 | public async _updateProcessingGetMapPayload( 36 | payload: ProcessingPayload, 37 | datasetSeqNo: number = 0, 38 | ): Promise { 39 | payload = await super._updateProcessingGetMapPayload(payload); 40 | payload.input.data[datasetSeqNo].dataFilter.maxCloudCoverage = this.maxCloudCoverPercent; 41 | return payload; 42 | } 43 | 44 | protected extractFindTilesMetaFromCatalog(feature: Record): Record { 45 | let result: Record = {}; 46 | 47 | if (!feature) { 48 | return result; 49 | } 50 | 51 | result = { 52 | ...super.extractFindTilesMetaFromCatalog(feature), 53 | cloudCoverPercent: feature.properties['eo:cloud_cover'], 54 | }; 55 | 56 | return result; 57 | } 58 | 59 | protected async getFindDatesUTCAdditionalParameters( 60 | reqConfig: RequestConfiguration, // eslint-disable-line @typescript-eslint/no-unused-vars 61 | ): Promise> { 62 | return this.maxCloudCoverPercent !== null && this.maxCloudCoverPercent !== undefined 63 | ? { 64 | maxCloudCoverage: this.maxCloudCoverPercent / 100, 65 | } 66 | : {}; 67 | } 68 | public getStatsAdditionalParameters(): Record { 69 | return { 70 | maxcc: this.maxCloudCoverPercent, 71 | }; 72 | } 73 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 74 | protected extractFindTilesMeta(tile: any): Record { 75 | return { 76 | ...super.extractFindTilesMeta(tile), 77 | cloudCoverPercent: tile.cloudCoverPercentage, 78 | }; 79 | } 80 | 81 | protected createCatalogFilterQuery( 82 | maxCloudCoverPercent?: number | null, 83 | datasetParameters?: Record | null, 84 | ): Record { 85 | let result = { ...super.createCatalogFilterQuery(maxCloudCoverPercent, datasetParameters) }; 86 | 87 | if (maxCloudCoverPercent !== null && maxCloudCoverPercent !== undefined) { 88 | result['op'] = '<='; 89 | result['args'] = [ 90 | { 91 | property: 'eo:cloud_cover', 92 | }, 93 | maxCloudCoverPercent, 94 | ]; 95 | } 96 | return result && Object.keys(result).length > 0 ? result : null; 97 | } 98 | 99 | protected getFindTilesAdditionalParameters(): FindTilesAdditionalParameters { 100 | return { 101 | maxCloudCoverPercent: this.maxCloudCoverPercent, 102 | datasetParameters: null, 103 | }; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/layer/DEMAWSUSLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWSUS_DEM } from './dataset'; 2 | import { AbstractDEMLayer, ConstructorParameters } from './AbstractDEMLayer'; 3 | import { DEMInstanceType } from './const'; 4 | 5 | export class DEMAWSUSLayer extends AbstractDEMLayer { 6 | public readonly dataset = DATASET_AWSUS_DEM; 7 | 8 | public constructor({ demInstance, ...rest }: ConstructorParameters) { 9 | super(rest); 10 | if (!demInstance || demInstance === DEMInstanceType.MAPZEN) { 11 | this.demInstance = DEMInstanceType.MAPZEN; 12 | } else { 13 | throw new Error(`DEMAWSUSLayer does not support demInstance ${demInstance}`); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/layer/DEMCDASLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_CDAS_DEM } from './dataset'; 2 | import { AbstractDEMLayer, ConstructorParameters } from './AbstractDEMLayer'; 3 | import { DEMInstanceType } from './const'; 4 | 5 | export class DEMCDASLayer extends AbstractDEMLayer { 6 | public readonly dataset = DATASET_CDAS_DEM; 7 | 8 | public constructor({ demInstance, ...rest }: ConstructorParameters) { 9 | super(rest); 10 | 11 | if (demInstance === DEMInstanceType.MAPZEN) { 12 | throw new Error(`DEMCDASLayer does not support demInstance ${demInstance}`); 13 | } 14 | 15 | this.demInstance = demInstance; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/layer/DEMLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_DEM } from './dataset'; 2 | import { AbstractDEMLayer } from './AbstractDEMLayer'; 3 | 4 | export class DEMLayer extends AbstractDEMLayer { 5 | public readonly dataset = DATASET_AWS_DEM; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/HLSAWSLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_HLS } from './dataset'; 2 | import { DataProductId, FindTilesAdditionalParameters, MosaickingOrder } from './const'; 3 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 4 | import { HLSConstellation } from '../dataimport/const'; 5 | import { ProcessingPayload } from './processing'; 6 | 7 | interface ConstructorParameters { 8 | instanceId?: string | null; 9 | layerId?: string | null; 10 | evalscript?: string | null; 11 | evalscriptUrl?: string | null; 12 | dataProduct?: DataProductId | null; 13 | mosaickingOrder?: MosaickingOrder | null; 14 | title?: string | null; 15 | description?: string | null; 16 | legendUrl?: string | null; 17 | maxCloudCoverPercent?: number | null; 18 | constellation?: HLSConstellation | null; 19 | highlights?: AbstractSentinelHubV3WithCCLayer['highlights']; 20 | } 21 | 22 | type HLSFindTilesDatasetParameters = { 23 | type: string; 24 | constellation: HLSConstellation; 25 | }; 26 | 27 | export class HLSAWSLayer extends AbstractSentinelHubV3WithCCLayer { 28 | public readonly dataset = DATASET_AWS_HLS; 29 | public constellation: HLSConstellation | null; 30 | 31 | public constructor({ constellation = null, ...params }: ConstructorParameters) { 32 | super(params); 33 | this.constellation = constellation; 34 | } 35 | 36 | protected createCatalogFilterQuery( 37 | maxCloudCoverPercent?: number | null, 38 | datasetParameters?: Record | null, 39 | ): Record { 40 | let result = { ...super.createCatalogFilterQuery(maxCloudCoverPercent, datasetParameters) }; 41 | let args: { op: string; args: any[] }[] = []; 42 | 43 | if (maxCloudCoverPercent !== null && maxCloudCoverPercent !== undefined) { 44 | args.push({ 45 | op: '<=', 46 | args: [{ property: 'eo:cloud_cover' }, maxCloudCoverPercent], 47 | }); 48 | } 49 | 50 | if (datasetParameters && datasetParameters.constellation) { 51 | args.push({ 52 | op: '=', 53 | args: [{ property: 'constellation' }, datasetParameters.constellation], 54 | }); 55 | } 56 | 57 | if (args.length > 0) { 58 | result.op = 'and'; 59 | result.args = args; 60 | } 61 | 62 | return result && Object.keys(result).length > 0 ? result : null; 63 | } 64 | 65 | protected getFindTilesAdditionalParameters(): FindTilesAdditionalParameters { 66 | const findTilesDatasetParameters: HLSFindTilesDatasetParameters = { 67 | type: this.dataset.datasetParametersType, 68 | constellation: this.constellation, 69 | }; 70 | 71 | return { 72 | maxCloudCoverPercent: this.maxCloudCoverPercent, 73 | datasetParameters: findTilesDatasetParameters, 74 | }; 75 | } 76 | 77 | public async _updateProcessingGetMapPayload( 78 | payload: ProcessingPayload, 79 | datasetSeqNo: number = 0, 80 | ): Promise { 81 | payload = await super._updateProcessingGetMapPayload(payload); 82 | 83 | if (this.constellation !== null) { 84 | payload.input.data[datasetSeqNo].dataFilter.constellation = this.constellation; 85 | } 86 | 87 | return payload; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/layer/Landsat15AWSLMSSL1Layer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_LMSSL1 } from './dataset'; 2 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 3 | 4 | export class Landsat15AWSLMSSL1Layer extends AbstractSentinelHubV3WithCCLayer { 5 | public readonly dataset = DATASET_AWS_LMSSL1; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/Landsat45AWSLTML1Layer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_LTML1 } from './dataset'; 2 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 3 | 4 | export class Landsat45AWSLTML1Layer extends AbstractSentinelHubV3WithCCLayer { 5 | public readonly dataset = DATASET_AWS_LTML1; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/Landsat45AWSLTML2Layer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_LTML2 } from './dataset'; 2 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 3 | 4 | export class Landsat45AWSLTML2Layer extends AbstractSentinelHubV3WithCCLayer { 5 | public readonly dataset = DATASET_AWS_LTML2; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/Landsat7AWSLETML1Layer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_LETML1 } from './dataset'; 2 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 3 | 4 | export class Landsat7AWSLETML1Layer extends AbstractSentinelHubV3WithCCLayer { 5 | public readonly dataset = DATASET_AWS_LETML1; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/Landsat7AWSLETML2Layer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_LETML2 } from './dataset'; 2 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 3 | 4 | export class Landsat7AWSLETML2Layer extends AbstractSentinelHubV3WithCCLayer { 5 | public readonly dataset = DATASET_AWS_LETML2; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/Landsat8AWSLOTL1Layer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_LOTL1 } from './dataset'; 2 | import { AbstractLandsat8Layer } from './AbstractLandsat8Layer'; 3 | import { Link, LinkType } from './const'; 4 | 5 | export class Landsat8AWSLOTL1Layer extends AbstractLandsat8Layer { 6 | public readonly dataset = DATASET_AWS_LOTL1; 7 | 8 | private getPreviewUrl(productId: string): string { 9 | return `https://landsatlook.usgs.gov/gen-browse?size=thumb&type=refl&product_id=${productId}`; 10 | } 11 | 12 | protected getTileLinks(tile: Record): Link[] { 13 | return [ 14 | { 15 | target: tile.dataUri, 16 | type: LinkType.AWS, 17 | }, 18 | { 19 | target: this.getPreviewUrl(tile.originalId), 20 | type: LinkType.PREVIEW, 21 | }, 22 | ]; 23 | } 24 | 25 | protected getTileLinksFromCatalog(feature: Record): Link[] { 26 | const { assets } = feature; 27 | let result: Link[] = []; 28 | 29 | if (assets && assets.data) { 30 | result.push({ target: assets.data.href, type: LinkType.AWS }); 31 | } 32 | 33 | result.push({ 34 | target: this.getPreviewUrl(feature.id), 35 | type: LinkType.PREVIEW, 36 | }); 37 | 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/layer/Landsat8AWSLOTL2Layer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_LOTL2 } from './dataset'; 2 | import { AbstractLandsat8Layer } from './AbstractLandsat8Layer'; 3 | import { Link, LinkType } from './const'; 4 | 5 | export class Landsat8AWSLOTL2Layer extends AbstractLandsat8Layer { 6 | public readonly dataset = DATASET_AWS_LOTL2; 7 | 8 | protected getTileLinks(tile: Record): Link[] { 9 | return [ 10 | { 11 | target: tile.dataUri, 12 | type: LinkType.AWS, 13 | }, 14 | ]; 15 | } 16 | 17 | protected getTileLinksFromCatalog(feature: Record): Link[] { 18 | const { assets } = feature; 19 | let result: Link[] = []; 20 | 21 | if (assets && assets.data) { 22 | result.push({ target: assets.data.href, type: LinkType.AWS }); 23 | } 24 | 25 | return result; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/layer/Landsat8AWSLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_AWS_L8L1C } from './dataset'; 2 | import { AbstractLandsat8Layer } from './AbstractLandsat8Layer'; 3 | 4 | export class Landsat8AWSLayer extends AbstractLandsat8Layer { 5 | public readonly dataset = DATASET_AWS_L8L1C; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/MODISLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_MODIS } from './dataset'; 2 | import { AbstractSentinelHubV3Layer } from './AbstractSentinelHubV3Layer'; 3 | 4 | export class MODISLayer extends AbstractSentinelHubV3Layer { 5 | public readonly dataset = DATASET_MODIS; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/PlanetNicfi.ts: -------------------------------------------------------------------------------- 1 | import { RequestConfiguration } from '../utils/cancelRequests'; 2 | import { ensureTimeout } from '../utils/ensureTimeout'; 3 | 4 | import { WmtsLayer } from './WmtsLayer'; 5 | import { BBox } from '../bbox'; 6 | import moment from 'moment'; 7 | import { fetchLayersFromWmtsGetCapabilitiesXml } from './wmts.utils'; 8 | 9 | const YYYY_MM_REGEX = /\d{4}-\d{2}/g; 10 | 11 | enum NICFI_LAYER_TYPES { 12 | ANALYTIC = 'analytic', 13 | VISUAL = 'visual', 14 | } 15 | 16 | interface ConstructorParameters { 17 | baseUrl?: string; 18 | layerId?: string; 19 | title?: string | null; 20 | description?: string | null; 21 | legendUrl?: string | null; 22 | resourceUrl?: string | null; 23 | } 24 | 25 | export class PlanetNicfiLayer extends WmtsLayer { 26 | protected baseUrl: string; 27 | protected layerId: string; 28 | protected resourceUrl: string; 29 | protected matrixSet: string; 30 | 31 | public constructor({ 32 | baseUrl, 33 | layerId, 34 | title = null, 35 | description = null, 36 | legendUrl = null, 37 | resourceUrl = null, 38 | }: ConstructorParameters) { 39 | super({ title, description, legendUrl }); 40 | this.baseUrl = baseUrl; 41 | this.layerId = layerId; 42 | this.resourceUrl = resourceUrl; 43 | this.matrixSet = 'GoogleMapsCompatible15'; //only matrixSet available for PlanetNicfi 44 | } 45 | 46 | public async findDatesUTC( 47 | bbox: BBox, 48 | fromTime: Date, 49 | toTime: Date, 50 | reqConfig?: RequestConfiguration, // eslint-disable-line @typescript-eslint/no-unused-vars 51 | ): Promise { 52 | return await ensureTimeout(async (innerReqConfig) => { 53 | const parsedLayers = await fetchLayersFromWmtsGetCapabilitiesXml(this.baseUrl, innerReqConfig); 54 | const applicableLayers = parsedLayers.filter((l) => { 55 | return this.getLayerType(this.layerId) === this.getLayerType(l.Name[0]); 56 | }); 57 | const datesFromApplicableLayers = applicableLayers.map((l) => { 58 | const dateArray = l.Name[0].match(YYYY_MM_REGEX); 59 | return moment.utc(dateArray[dateArray.length - 1]).endOf('month'); 60 | }); 61 | const availableDates = datesFromApplicableLayers.filter((d) => 62 | d.isBetween(moment.utc(fromTime), moment.utc(toTime), null, '[]'), 63 | ); 64 | return availableDates.map((d) => d.toDate()); 65 | }, reqConfig); 66 | } 67 | 68 | private getLayerType(layerId: string): NICFI_LAYER_TYPES { 69 | return layerId.includes(NICFI_LAYER_TYPES.ANALYTIC) 70 | ? NICFI_LAYER_TYPES.ANALYTIC 71 | : NICFI_LAYER_TYPES.VISUAL; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/layer/S1GRDCDASLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_CDAS_S1GRD } from './dataset'; 2 | import { S1GRDAWSEULayer } from './S1GRDAWSEULayer'; 3 | 4 | export class S1GRDCDASLayer extends S1GRDAWSEULayer { 5 | public readonly dataset = DATASET_CDAS_S1GRD; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/S2L1CCDASLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_CDAS_S2L1C } from './dataset'; 2 | import { S2L1CLayer } from './S2L1CLayer'; 3 | 4 | export class S2L1CCDASLayer extends S2L1CLayer { 5 | public readonly dataset = DATASET_CDAS_S2L1C; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/S2L1CLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_S2L1C } from './dataset'; 2 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 3 | import { Link, LinkType } from './const'; 4 | 5 | export class S2L1CLayer extends AbstractSentinelHubV3WithCCLayer { 6 | public readonly dataset = DATASET_S2L1C; 7 | 8 | protected getTileLinks(tile: Record): Link[] { 9 | return [ 10 | { 11 | target: tile.dataUri, 12 | type: LinkType.AWS, 13 | }, 14 | { 15 | target: `https://roda.sentinel-hub.com/sentinel-s2-l1c/tiles${ 16 | tile.dataUri.split('tiles')[1] 17 | }/preview.jpg`, 18 | type: LinkType.PREVIEW, 19 | }, 20 | ]; 21 | } 22 | 23 | protected extractFindTilesMeta(tile: any): Record { 24 | return { 25 | ...super.extractFindTilesMeta(tile), 26 | tileId: tile.id, 27 | MGRSLocation: tile.dataUri.split('/').slice(4, 7).join(''), 28 | }; 29 | } 30 | 31 | protected extractFindTilesMetaFromCatalog(feature: Record): Record { 32 | let result: Record = super.extractFindTilesMetaFromCatalog(feature); 33 | 34 | if (feature.assets && feature.assets.data && feature.assets.data.href) { 35 | result.MGRSLocation = feature.assets.data.href.split('/').slice(4, 7).join(''); 36 | } 37 | return result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/layer/S2L2ACDASLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_CDAS_S2L2A } from './dataset'; 2 | import { S2L2ALayer } from './S2L2ALayer'; 3 | 4 | export class S2L2ACDASLayer extends S2L2ALayer { 5 | public readonly dataset = DATASET_CDAS_S2L2A; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/S2L2ALayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_S2L2A } from './dataset'; 2 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 3 | import { ProcessingPayload } from './processing'; 4 | import { Link, LinkType } from './const'; 5 | 6 | export class S2L2ALayer extends AbstractSentinelHubV3WithCCLayer { 7 | public readonly dataset = DATASET_S2L2A; 8 | 9 | public async _updateProcessingGetMapPayload( 10 | payload: ProcessingPayload, 11 | datasetSeqNo: number = 0, 12 | ): Promise { 13 | payload.input.data[datasetSeqNo].dataFilter.maxCloudCoverage = this.maxCloudCoverPercent; 14 | return payload; 15 | } 16 | 17 | private createPreviewLinkFromDataUri(dataUri: string): Link { 18 | return { 19 | // S-2 L2A doesn't have previews, but we can use corresponding L1C ones instead: 20 | target: `https://roda.sentinel-hub.com/sentinel-s2-l1c/tiles${dataUri}preview.jpg`, 21 | type: LinkType.PREVIEW, 22 | }; 23 | } 24 | 25 | protected getTileLinks(tile: Record): Link[] { 26 | return [ 27 | { 28 | target: tile.dataUri, 29 | type: LinkType.AWS, 30 | }, 31 | this.createPreviewLinkFromDataUri(`${tile.dataUri.split('tiles')[1]}/`), 32 | ]; 33 | } 34 | 35 | protected getTileLinksFromCatalog(feature: Record): Link[] { 36 | let result: Link[] = super.getTileLinksFromCatalog(feature); 37 | 38 | if (feature && feature.assets && feature.assets.data) { 39 | const dataUri = feature.assets.data.href.split('tiles')[1]; 40 | result.push(this.createPreviewLinkFromDataUri(dataUri)); 41 | } 42 | 43 | return result; 44 | } 45 | 46 | protected extractFindTilesMeta(tile: any): Record { 47 | return { 48 | ...super.extractFindTilesMeta(tile), 49 | tileId: tile.id, 50 | MGRSLocation: tile.dataUri.split('/').slice(4, 7).join(''), 51 | }; 52 | } 53 | 54 | protected extractFindTilesMetaFromCatalog(feature: Record): Record { 55 | let result: Record = super.extractFindTilesMetaFromCatalog(feature); 56 | 57 | if (feature.assets && feature.assets.data && feature.assets.data.href) { 58 | result.MGRSLocation = feature.assets.data.href.split('/').slice(4, 7).join(''); 59 | } 60 | return result; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/layer/S3OLCICDASLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_CDAS_S3OLCI } from './dataset'; 2 | import { S3OLCILayer } from './S3OLCILayer'; 3 | 4 | export class S3OLCICDASLayer extends S3OLCILayer { 5 | public readonly dataset = DATASET_CDAS_S3OLCI; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/S3OLCIL2CDASLayer.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { DATASET_CDAS_S3OLCIL2 } from './dataset'; 4 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 5 | import { RequestConfiguration } from '../utils/cancelRequests'; 6 | 7 | import { PaginatedTiles, Link, LinkType, FindTilesAdditionalParameters } from './const'; 8 | 9 | type S3OLCIL2FindTilesDatasetParameters = { 10 | type?: string; 11 | }; 12 | 13 | export class S3OLCIL2CDASLayer extends AbstractSentinelHubV3WithCCLayer { 14 | public readonly dataset = DATASET_CDAS_S3OLCIL2; 15 | 16 | protected convertResponseFromSearchIndex(response: { 17 | data: { tiles: any[]; hasMore: boolean }; 18 | }): PaginatedTiles { 19 | return { 20 | tiles: response.data.tiles.map((tile) => ({ 21 | geometry: tile.dataGeometry, 22 | sensingTime: moment.utc(tile.sensingTime).toDate(), 23 | meta: this.extractFindTilesMeta(tile), 24 | links: this.getTileLinks(tile), 25 | })), 26 | hasMore: response.data.hasMore, 27 | }; 28 | } 29 | 30 | protected getFindTilesAdditionalParameters(): FindTilesAdditionalParameters { 31 | const findTilesDatasetParameters: S3OLCIL2FindTilesDatasetParameters = { 32 | type: this.dataset.shProcessingApiDatasourceAbbreviation, 33 | }; 34 | 35 | return { 36 | maxCloudCoverPercent: this.maxCloudCoverPercent, 37 | datasetParameters: findTilesDatasetParameters, 38 | }; 39 | } 40 | 41 | protected async getFindDatesUTCAdditionalParameters( 42 | reqConfig: RequestConfiguration, // eslint-disable-line @typescript-eslint/no-unused-vars 43 | ): Promise> { 44 | const result: Record = { 45 | datasetParameters: { 46 | type: this.dataset.datasetParametersType, 47 | }, 48 | }; 49 | 50 | if (this.maxCloudCoverPercent !== null) { 51 | result.maxCloudCoverage = this.maxCloudCoverPercent / 100; 52 | } 53 | 54 | return result; 55 | } 56 | 57 | protected getTileLinks(tile: Record): Link[] { 58 | return [ 59 | { 60 | target: tile.originalId.replace('EODATA', '/eodata'), 61 | type: LinkType.CREODIAS, 62 | }, 63 | { 64 | target: `https://finder.creodias.eu/files${tile.originalId.replace( 65 | 'EODATA', 66 | '', 67 | )}/${tile.productName.replace('.SEN3', '')}-ql.jpg`, 68 | type: LinkType.PREVIEW, 69 | }, 70 | ]; 71 | } 72 | 73 | protected getTileLinksFromCatalog(feature: Record): Link[] { 74 | const { assets } = feature; 75 | let result: Link[] = super.getTileLinksFromCatalog(feature); 76 | 77 | if (assets.data && assets.data.href) { 78 | result.push({ target: assets.data.href.replace('s3://DIAS', '/dias'), type: LinkType.CREODIAS }); 79 | } 80 | return result; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/layer/S3OLCILayer.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { PaginatedTiles, Link, LinkType } from './const'; 4 | import { DATASET_S3OLCI } from './dataset'; 5 | import { AbstractSentinelHubV3Layer } from './AbstractSentinelHubV3Layer'; 6 | 7 | export class S3OLCILayer extends AbstractSentinelHubV3Layer { 8 | public readonly dataset = DATASET_S3OLCI; 9 | 10 | protected convertResponseFromSearchIndex(response: { 11 | data: { tiles: any[]; hasMore: boolean }; 12 | }): PaginatedTiles { 13 | return { 14 | tiles: response.data.tiles.map((tile) => ({ 15 | geometry: tile.dataGeometry, 16 | sensingTime: moment.utc(tile.sensingTime).toDate(), 17 | meta: {}, 18 | links: this.getTileLinks(tile), 19 | })), 20 | hasMore: response.data.hasMore, 21 | }; 22 | } 23 | 24 | protected getTileLinks(tile: Record): Link[] { 25 | return [ 26 | { 27 | target: tile.originalId.replace('EODATA', '/eodata'), 28 | type: LinkType.CREODIAS, 29 | }, 30 | { 31 | target: `https://finder.creodias.eu/files${tile.originalId.replace( 32 | 'EODATA', 33 | '', 34 | )}/${tile.productName.replace('.SEN3', '')}-ql.jpg`, 35 | type: LinkType.PREVIEW, 36 | }, 37 | ]; 38 | } 39 | 40 | protected getTileLinksFromCatalog(feature: Record): Link[] { 41 | const { assets } = feature; 42 | let result: Link[] = super.getTileLinksFromCatalog(feature); 43 | 44 | if (assets.data && assets.data.href) { 45 | result.push({ target: assets.data.href.replace('s3://DIAS', '/dias'), type: LinkType.CREODIAS }); 46 | } 47 | return result; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/layer/S3SLSTRCDASLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_CDAS_S3SLSTR } from './dataset'; 2 | import { S3SLSTRLayer } from './S3SLSTRLayer'; 3 | 4 | export class S3SLSTRCDASLayer extends S3SLSTRLayer { 5 | public readonly dataset = DATASET_CDAS_S3SLSTR; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/S3SLSTRLayer.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { 4 | PaginatedTiles, 5 | OrbitDirection, 6 | Link, 7 | LinkType, 8 | DataProductId, 9 | FindTilesAdditionalParameters, 10 | } from './const'; 11 | import { DATASET_S3SLSTR } from './dataset'; 12 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 13 | import { ProcessingPayload } from './processing'; 14 | import { RequestConfiguration } from '../utils/cancelRequests'; 15 | 16 | interface ConstructorParameters { 17 | instanceId?: string | null; 18 | layerId?: string | null; 19 | evalscript?: string | null; 20 | evalscriptUrl?: string | null; 21 | dataProduct?: DataProductId | null; 22 | title?: string | null; 23 | description?: string | null; 24 | legendUrl?: string | null; 25 | maxCloudCoverPercent?: number | null; 26 | view?: S3SLSTRView | null; 27 | highlights?: AbstractSentinelHubV3WithCCLayer['highlights']; 28 | } 29 | 30 | export enum S3SLSTRView { 31 | NADIR = 'NADIR', 32 | OBLIQUE = 'OBLIQUE', 33 | } 34 | 35 | type S3SLSTRFindTilesDatasetParameters = { 36 | type?: string; 37 | orbitDirection?: OrbitDirection; 38 | view: S3SLSTRView; 39 | }; 40 | 41 | export class S3SLSTRLayer extends AbstractSentinelHubV3WithCCLayer { 42 | public readonly dataset = DATASET_S3SLSTR; 43 | public orbitDirection: OrbitDirection | null; 44 | public view: S3SLSTRView; 45 | 46 | public constructor({ view = S3SLSTRView.NADIR, ...rest }: ConstructorParameters) { 47 | super(rest); 48 | // images that are not DESCENDING appear empty: 49 | this.orbitDirection = OrbitDirection.DESCENDING; 50 | this.view = view; 51 | } 52 | 53 | public async _updateProcessingGetMapPayload( 54 | payload: ProcessingPayload, 55 | datasetSeqNo: number = 0, 56 | ): Promise { 57 | payload = await super._updateProcessingGetMapPayload(payload); 58 | payload.input.data[datasetSeqNo].dataFilter.orbitDirection = this.orbitDirection; 59 | payload.input.data[datasetSeqNo].processing.view = this.view; 60 | return payload; 61 | } 62 | 63 | protected convertResponseFromSearchIndex(response: { 64 | data: { tiles: any[]; hasMore: boolean }; 65 | }): PaginatedTiles { 66 | return { 67 | tiles: response.data.tiles.map((tile) => ({ 68 | geometry: tile.dataGeometry, 69 | sensingTime: moment.utc(tile.sensingTime).toDate(), 70 | meta: this.extractFindTilesMeta(tile), 71 | links: this.getTileLinks(tile), 72 | })), 73 | hasMore: response.data.hasMore, 74 | }; 75 | } 76 | 77 | protected getFindTilesAdditionalParameters(): FindTilesAdditionalParameters { 78 | const findTilesDatasetParameters: S3SLSTRFindTilesDatasetParameters = { 79 | type: this.dataset.shProcessingApiDatasourceAbbreviation, 80 | orbitDirection: this.orbitDirection, 81 | view: this.view, 82 | }; 83 | 84 | return { 85 | maxCloudCoverPercent: this.maxCloudCoverPercent, 86 | datasetParameters: findTilesDatasetParameters, 87 | }; 88 | } 89 | 90 | protected async getFindDatesUTCAdditionalParameters( 91 | reqConfig: RequestConfiguration, // eslint-disable-line @typescript-eslint/no-unused-vars 92 | ): Promise> { 93 | const result: Record = { 94 | datasetParameters: { 95 | type: this.dataset.datasetParametersType, 96 | view: this.view, 97 | }, 98 | }; 99 | if (this.orbitDirection !== null) { 100 | result.datasetParameters.orbitDirection = this.orbitDirection; 101 | } 102 | 103 | if (this.maxCloudCoverPercent !== null) { 104 | result.maxCloudCoverage = this.maxCloudCoverPercent / 100; 105 | } 106 | 107 | return result; 108 | } 109 | 110 | protected getTileLinks(tile: Record): Link[] { 111 | return [ 112 | { 113 | target: tile.originalId.replace('EODATA', '/eodata'), 114 | type: LinkType.CREODIAS, 115 | }, 116 | { 117 | target: `https://finder.creodias.eu/files${tile.originalId.replace( 118 | 'EODATA', 119 | '', 120 | )}/${tile.productName.replace('.SEN3', '')}-ql.jpg`, 121 | type: LinkType.PREVIEW, 122 | }, 123 | ]; 124 | } 125 | 126 | protected extractFindTilesMeta(tile: any): Record { 127 | return { 128 | ...super.extractFindTilesMeta(tile), 129 | orbitDirection: tile.orbitDirection, 130 | }; 131 | } 132 | 133 | protected extractFindTilesMetaFromCatalog(feature: Record): Record { 134 | let result: Record = {}; 135 | 136 | if (!feature) { 137 | return result; 138 | } 139 | 140 | result = { 141 | ...super.extractFindTilesMetaFromCatalog(feature), 142 | orbitDirection: feature.properties['sat:orbit_state'], 143 | }; 144 | 145 | return result; 146 | } 147 | 148 | protected getTileLinksFromCatalog(feature: Record): Link[] { 149 | const { assets } = feature; 150 | let result: Link[] = super.getTileLinksFromCatalog(feature); 151 | 152 | if (assets.data && assets.data.href) { 153 | result.push({ target: assets.data.href.replace('s3://EODATA', '/eodata'), type: LinkType.CREODIAS }); 154 | } 155 | return result; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/layer/S3SYNL2CDASLayer.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { DATASET_CDAS_S3SYNERGYL2 } from './dataset'; 4 | import { AbstractSentinelHubV3WithCCLayer } from './AbstractSentinelHubV3WithCCLayer'; 5 | import { RequestConfiguration } from '../utils/cancelRequests'; 6 | 7 | import { PaginatedTiles, Link, LinkType, FindTilesAdditionalParameters } from './const'; 8 | 9 | type S3SYNL2FindTilesDatasetParameters = { 10 | type?: string; 11 | }; 12 | 13 | export class S3SYNL2CDASLayer extends AbstractSentinelHubV3WithCCLayer { 14 | public readonly dataset = DATASET_CDAS_S3SYNERGYL2; 15 | 16 | protected convertResponseFromSearchIndex(response: { 17 | data: { tiles: any[]; hasMore: boolean }; 18 | }): PaginatedTiles { 19 | return { 20 | tiles: response.data.tiles.map((tile) => ({ 21 | geometry: tile.dataGeometry, 22 | sensingTime: moment.utc(tile.sensingTime).toDate(), 23 | meta: this.extractFindTilesMeta(tile), 24 | links: this.getTileLinks(tile), 25 | })), 26 | hasMore: response.data.hasMore, 27 | }; 28 | } 29 | 30 | protected getFindTilesAdditionalParameters(): FindTilesAdditionalParameters { 31 | const findTilesDatasetParameters: S3SYNL2FindTilesDatasetParameters = { 32 | type: this.dataset.shProcessingApiDatasourceAbbreviation, 33 | }; 34 | 35 | return { 36 | maxCloudCoverPercent: this.maxCloudCoverPercent, 37 | datasetParameters: findTilesDatasetParameters, 38 | }; 39 | } 40 | 41 | protected async getFindDatesUTCAdditionalParameters( 42 | reqConfig: RequestConfiguration, // eslint-disable-line @typescript-eslint/no-unused-vars 43 | ): Promise> { 44 | const result: Record = { 45 | datasetParameters: { 46 | type: this.dataset.datasetParametersType, 47 | }, 48 | }; 49 | 50 | if (this.maxCloudCoverPercent !== null) { 51 | result.maxCloudCoverage = this.maxCloudCoverPercent / 100; 52 | } 53 | 54 | return result; 55 | } 56 | 57 | protected getTileLinks(tile: Record): Link[] { 58 | return [ 59 | { 60 | target: tile.originalId.replace('EODATA', '/eodata'), 61 | type: LinkType.CREODIAS, 62 | }, 63 | { 64 | target: `https://finder.creodias.eu/files${tile.originalId.replace( 65 | 'EODATA', 66 | '', 67 | )}/${tile.productName.replace('.SEN3', '')}-ql.jpg`, 68 | type: LinkType.PREVIEW, 69 | }, 70 | ]; 71 | } 72 | 73 | protected getTileLinksFromCatalog(feature: Record): Link[] { 74 | const { assets } = feature; 75 | let result: Link[] = super.getTileLinksFromCatalog(feature); 76 | 77 | if (assets.data && assets.data.href) { 78 | result.push({ target: assets.data.href.replace('s3://DIAS', '/dias'), type: LinkType.CREODIAS }); 79 | } 80 | return result; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/layer/S5PL2CDASLayer.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_CDAS_S5PL2 } from './dataset'; 2 | import { S5PL2Layer } from './S5PL2Layer'; 3 | 4 | export class S5PL2CDASLayer extends S5PL2Layer { 5 | public readonly dataset = DATASET_CDAS_S5PL2; 6 | } 7 | -------------------------------------------------------------------------------- /src/layer/WmsLayer.ts: -------------------------------------------------------------------------------- 1 | import moment, { Moment } from 'moment'; 2 | 3 | import { BBox } from '../bbox'; 4 | import { GetMapParams, ApiType, OgcServiceTypes } from './const'; 5 | import { wmsGetMapUrl } from './wms'; 6 | import { AbstractLayer } from './AbstractLayer'; 7 | import { fetchLayersFromGetCapabilitiesXml } from './utils'; 8 | import { RequestConfiguration } from '../utils/cancelRequests'; 9 | import { ensureTimeout } from '../utils/ensureTimeout'; 10 | 11 | interface ConstructorParameters { 12 | baseUrl?: string; 13 | layerId?: string; 14 | title?: string | null; 15 | description?: string | null; 16 | legendUrl?: string | null; 17 | } 18 | 19 | export class WmsLayer extends AbstractLayer { 20 | // The URL of the WMS service, for example: "https://services.sentinel-hub.com/ogc/wms//" 21 | protected baseUrl: string; 22 | protected layerId: string; 23 | 24 | public constructor({ 25 | baseUrl, 26 | layerId, 27 | title = null, 28 | description = null, 29 | legendUrl = null, 30 | }: ConstructorParameters) { 31 | super({ title, description, legendUrl }); 32 | this.baseUrl = baseUrl; 33 | this.layerId = layerId; 34 | } 35 | 36 | public getMapUrl(params: GetMapParams, api: ApiType): string { 37 | if (api !== ApiType.WMS) { 38 | throw new Error('Only WMS is supported on this layer'); 39 | } 40 | if (params.gain) { 41 | throw new Error('Parameter gain is not supported in getMapUrl. Use getMap method instead.'); 42 | } 43 | if (params.gamma) { 44 | throw new Error('Parameter gamma is not supported in getMapUrl. Use getMap method instead.'); 45 | } 46 | if (params.effects) { 47 | throw new Error('Parameter effects is not supported in getMapUrl. Use getMap method instead.'); 48 | } 49 | return wmsGetMapUrl(this.baseUrl, this.layerId, params); 50 | } 51 | 52 | public async findDatesUTC( 53 | bbox: BBox, // eslint-disable-line @typescript-eslint/no-unused-vars 54 | fromTime: Date, 55 | toTime: Date, 56 | reqConfig?: RequestConfiguration, 57 | ): Promise { 58 | const dates = await ensureTimeout(async (innerReqConfig) => { 59 | // http://cite.opengeospatial.org/OGCTestData/wms/1.1.1/spec/wms1.1.1.html#dims 60 | const parsedLayers = await fetchLayersFromGetCapabilitiesXml( 61 | this.baseUrl, 62 | OgcServiceTypes.WMS, 63 | innerReqConfig, 64 | ); 65 | const layer = parsedLayers.find((layerInfo) => this.layerId === layerInfo.Name[0]); 66 | if (!layer) { 67 | throw new Error('Layer not found'); 68 | } 69 | if (!layer.Dimension) { 70 | throw new Error('Layer does not supply time information (no Dimension field)'); 71 | } 72 | const timeDimension = layer.Dimension.find((d) => d['$'].name === 'time'); 73 | if (!timeDimension) { 74 | throw new Error("Layer does not supply time information (no Dimension field with name === 'time')"); 75 | } 76 | // http://cite.opengeospatial.org/OGCTestData/wms/1.1.1/spec/wms1.1.1.html#date_time 77 | if (timeDimension['$'].units !== 'ISO8601') { 78 | throw new Error('Layer time information is not in ISO8601 format, parsing not supported'); 79 | } 80 | 81 | let allTimesMomentUTC = []; 82 | const times = timeDimension['_'].split(','); 83 | for (let i = 0; i < times.length; i++) { 84 | const timeParts = times[i].split('/'); 85 | switch (timeParts.length) { 86 | case 1: 87 | allTimesMomentUTC.push(moment.utc(timeParts[0])); 88 | break; 89 | case 3: 90 | const [intervalFromTime, intervalToTime, intervalDuration] = timeParts; 91 | const intervalFromTimeMoment = moment.utc(intervalFromTime); 92 | const intervalToTimeMoment = moment.utc(intervalToTime); 93 | const intervalDurationMoment = moment.duration(intervalDuration); 94 | for ( 95 | let t = intervalFromTimeMoment; 96 | t.isSameOrBefore(intervalToTimeMoment); 97 | t.add(intervalDurationMoment) 98 | ) { 99 | allTimesMomentUTC.push(t.clone()); 100 | } 101 | break; 102 | default: 103 | throw new Error('Unable to parse time information'); 104 | } 105 | } 106 | 107 | const found: Moment[] = allTimesMomentUTC.filter((t) => 108 | t.isBetween(moment.utc(fromTime), moment.utc(toTime), null, '[]'), 109 | ); 110 | found.sort((a, b) => b.unix() - a.unix()); 111 | return found.map((m) => m.toDate()); 112 | }, reqConfig); 113 | return dates; 114 | } 115 | 116 | public async updateLayerFromServiceIfNeeded(reqConfig?: RequestConfiguration): Promise { 117 | await ensureTimeout(async (innerReqConfig) => { 118 | if (this.legendUrl) { 119 | return; 120 | } 121 | if (this.baseUrl === null || this.layerId === null) { 122 | throw new Error( 123 | "Additional data can't be fetched from service because baseUrl and layerId are not defined", 124 | ); 125 | } 126 | const parsedLayers = await fetchLayersFromGetCapabilitiesXml( 127 | this.baseUrl, 128 | OgcServiceTypes.WMS, 129 | innerReqConfig, 130 | ); 131 | const layer = parsedLayers.find((layer) => this.layerId === layer.Name[0]); 132 | if (!layer) { 133 | throw new Error('Layer not found'); 134 | } 135 | const legendUrl = 136 | layer.Style && layer.Style[0].LegendURL 137 | ? layer.Style[0].LegendURL[0].OnlineResource[0]['$']['xlink:href'] 138 | : layer.Layer && layer.Layer[0].Style && layer.Layer[0].Style[0].LegendURL 139 | ? layer.Layer[0].Style[0].LegendURL[0].OnlineResource[0]['$']['xlink:href'] 140 | : null; 141 | this.legendUrl = legendUrl; 142 | }, reqConfig); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/layer/WmsWmtMsLayer.ts: -------------------------------------------------------------------------------- 1 | import { WmsLayer } from './WmsLayer'; 2 | import { ApiType, GetMapParams } from './const'; 3 | import { wmsWmtMsGetMapUrl } from './wms'; 4 | 5 | export class WmsWmtMsLayer extends WmsLayer { 6 | public getMapUrl(params: GetMapParams, api: ApiType): string { 7 | if (api !== ApiType.WMS) { 8 | throw new Error('Only WMS is supported on this layer'); 9 | } 10 | if (params.gain) { 11 | throw new Error('Parameter gain is not supported in getMapUrl. Use getMap method instead.'); 12 | } 13 | if (params.gamma) { 14 | throw new Error('Parameter gamma is not supported in getMapUrl. Use getMap method instead.'); 15 | } 16 | if (params.effects) { 17 | throw new Error('Parameter effects is not supported in getMapUrl. Use getMap method instead.'); 18 | } 19 | return wmsWmtMsGetMapUrl(this.baseUrl, this.layerId, params); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/layer/__tests__/DEMCDASLayer.ts: -------------------------------------------------------------------------------- 1 | import { DEMCDASLayer, DEMInstanceType, setAuthToken } from '../../index'; 2 | 3 | import { AUTH_TOKEN, mockNetwork } from './testUtils.findTiles'; 4 | 5 | import { checkLayersParamsEndpoint } from './testUtils.layers'; 6 | 7 | describe('correct endpoint is used for layer params', () => { 8 | beforeEach(async () => { 9 | setAuthToken(AUTH_TOKEN); 10 | mockNetwork.reset(); 11 | }); 12 | 13 | test('updateLayerFromServiceIfNeeded', async () => { 14 | await checkLayersParamsEndpoint(mockNetwork, DEMCDASLayer, 'https://sh.dataspace.copernicus.eu'); 15 | }); 16 | }); 17 | 18 | describe('check supported DEM instances', () => { 19 | test.each([ 20 | [ 21 | { 22 | instanceId: 'INSTANCE_ID', 23 | layerId: 'LAYER_ID', 24 | }, 25 | undefined, 26 | false, 27 | ], 28 | [ 29 | { 30 | instanceId: 'INSTANCE_ID', 31 | layerId: 'LAYER_ID', 32 | demInstance: DEMInstanceType.COPERNICUS_30, 33 | }, 34 | DEMInstanceType.COPERNICUS_30, 35 | false, 36 | ], 37 | [ 38 | { 39 | instanceId: 'INSTANCE_ID', 40 | layerId: 'LAYER_ID', 41 | demInstance: DEMInstanceType.MAPZEN, 42 | }, 43 | undefined, 44 | true, 45 | ], 46 | ])('check supported DEM instances', (params, expectedResult, shouldThrowError) => { 47 | if (shouldThrowError) { 48 | expect(() => new DEMCDASLayer(params)).toThrowError(); 49 | } else { 50 | const layer: DEMCDASLayer = new DEMCDASLayer(params); 51 | expect(layer.getDemInstance()).toBe(expectedResult); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/layer/__tests__/HLSAWSLayer.ts: -------------------------------------------------------------------------------- 1 | import { GetMapParams } from './../const'; 2 | import { HLSAWSLayer } from './../HLSAWSLayer'; 3 | import { ApiType, HLSConstellation, setAuthToken } from '../../index'; 4 | import { BBox, CRS_EPSG4326 } from '../../index'; 5 | 6 | import { AUTH_TOKEN, mockNetwork } from './testUtils.findTiles'; 7 | 8 | const getMapParams: GetMapParams = { 9 | fromTime: new Date(Date.UTC(2020, 4 - 1, 1, 0, 0, 0, 0)), 10 | toTime: new Date(Date.UTC(2020, 5 - 1, 1, 23, 59, 59, 999)), 11 | bbox: new BBox(CRS_EPSG4326, 19, 20, 20, 21), 12 | width: 256, 13 | height: 256, 14 | format: 'image/png', 15 | }; 16 | 17 | describe('Check constellation param is set correctly', () => { 18 | beforeEach(async () => { 19 | setAuthToken(AUTH_TOKEN); 20 | mockNetwork.reset(); 21 | }); 22 | 23 | test.each([[null], [undefined], [HLSConstellation.LANDSAT], [HLSConstellation.SENTINEL]])( 24 | 'processing payload has constellation param', 25 | async (constellation) => { 26 | const layer = new HLSAWSLayer({ 27 | evalscript: '//', 28 | constellation: constellation, 29 | }); 30 | mockNetwork.onAny().reply(200); 31 | try { 32 | await layer.getMap(getMapParams, ApiType.PROCESSING, { 33 | cache: { expiresIn: 0 }, 34 | }); 35 | } catch (err) {} 36 | const request = mockNetwork.history.post[0]; 37 | const data = JSON.parse(request.data); 38 | expect(data.input.data[0].type).toBe('HLS'); 39 | const requestConstellation = data.input.data[0].dataFilter.constellation; 40 | if (constellation) { 41 | expect(requestConstellation).toEqual(constellation); 42 | } else { 43 | expect(requestConstellation).toBeUndefined(); 44 | } 45 | }, 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /src/layer/__tests__/MODISLayer.ts: -------------------------------------------------------------------------------- 1 | import { BBox, CRS_EPSG4326, setAuthToken, MODISLayer, DATASET_MODIS } from '../../index'; 2 | import { 3 | constructFixtureFindTilesSearchIndex, 4 | constructFixtureFindTilesCatalog, 5 | } from './fixtures.MODISLayer'; 6 | 7 | import { 8 | AUTH_TOKEN, 9 | checkIfCorrectEndpointIsUsed, 10 | checkRequestFindTiles, 11 | checkResponseFindTiles, 12 | mockNetwork, 13 | } from './testUtils.findTiles'; 14 | 15 | import { 16 | checkIfCorrectEndpointIsUsedFindDatesUTC, 17 | checkRequestFindDatesUTC, 18 | checkResponseFindDatesUTC, 19 | } from './testUtils.findDatesUTC'; 20 | 21 | import { 22 | constructFixtureFindDatesUTCSearchIndex, 23 | constructFixtureFindDatesUTCCatalog, 24 | } from './fixtures.findDatesUTC'; 25 | 26 | const CATALOG_URL = 'https://services-uswest2.sentinel-hub.com/api/v1/catalog/1.0.0/search'; 27 | const SEARCH_INDEX_URL = 'https://services-uswest2.sentinel-hub.com/index/v3/collections/MODIS/searchIndex'; 28 | 29 | const fromTime: Date = new Date(Date.UTC(2020, 4 - 1, 1, 0, 0, 0, 0)); 30 | const toTime: Date = new Date(Date.UTC(2020, 5 - 1, 1, 23, 59, 59, 999)); 31 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 32 | 33 | const layerParamsArr: Record[] = [ 34 | { 35 | fromTime: fromTime, 36 | toTime: toTime, 37 | bbox: bbox, 38 | }, 39 | ]; 40 | 41 | describe('Test findTiles using searchIndex', () => { 42 | beforeEach(async () => { 43 | setAuthToken(null); 44 | mockNetwork.reset(); 45 | }); 46 | 47 | test('searchIndex is used if token is not set', async () => { 48 | await checkIfCorrectEndpointIsUsed(null, constructFixtureFindTilesSearchIndex({}), SEARCH_INDEX_URL); 49 | }); 50 | 51 | test.each(layerParamsArr)('check if correct request is constructed', async (layerParams) => { 52 | const fixtures = constructFixtureFindTilesSearchIndex(layerParams); 53 | await checkRequestFindTiles(fixtures); 54 | }); 55 | 56 | test('response from searchIndex', async () => { 57 | await checkResponseFindTiles(constructFixtureFindTilesSearchIndex({})); 58 | }); 59 | }); 60 | 61 | describe('Test findTiles using catalog', () => { 62 | beforeEach(async () => { 63 | setAuthToken(AUTH_TOKEN); 64 | mockNetwork.reset(); 65 | }); 66 | 67 | test('Catalog is used if token is set', async () => { 68 | await checkIfCorrectEndpointIsUsed(AUTH_TOKEN, constructFixtureFindTilesCatalog({}), CATALOG_URL); 69 | }); 70 | 71 | test.each(layerParamsArr)('check if correct request is constructed', async (layerParams) => { 72 | const fixtures = constructFixtureFindTilesCatalog(layerParams); 73 | await checkRequestFindTiles(fixtures); 74 | }); 75 | 76 | test('response from catalog', async () => { 77 | await checkResponseFindTiles(constructFixtureFindTilesCatalog({})); 78 | }); 79 | }); 80 | 81 | describe('Test findDatesUTC using searchIndex', () => { 82 | beforeEach(async () => { 83 | setAuthToken(null); 84 | mockNetwork.reset(); 85 | }); 86 | 87 | test('findAvailableData is used if token is not set', async () => { 88 | const layer = new MODISLayer({ 89 | instanceId: 'INSTANCE_ID', 90 | layerId: 'LAYER_ID', 91 | }); 92 | await checkIfCorrectEndpointIsUsedFindDatesUTC( 93 | null, 94 | constructFixtureFindDatesUTCSearchIndex(layer, {}), 95 | DATASET_MODIS.findDatesUTCUrl, 96 | ); 97 | }); 98 | 99 | test.each(layerParamsArr)('check if correct request is constructed', async (layerParams) => { 100 | let constructorParams: Record = {}; 101 | if (layerParams.maxCloudCoverPercent !== null && layerParams.maxCloudCoverPercent !== undefined) { 102 | constructorParams.maxCloudCoverPercent = layerParams.maxCloudCoverPercent; 103 | } 104 | 105 | const layer = new MODISLayer({ 106 | instanceId: 'INSTANCE_ID', 107 | layerId: 'LAYER_ID', 108 | ...constructorParams, 109 | }); 110 | const fixtures = constructFixtureFindDatesUTCSearchIndex(layer, layerParams); 111 | await checkRequestFindDatesUTC(fixtures); 112 | }); 113 | 114 | test('response from service', async () => { 115 | const layer = new MODISLayer({ 116 | instanceId: 'INSTANCE_ID', 117 | layerId: 'LAYER_ID', 118 | }); 119 | await checkResponseFindDatesUTC(constructFixtureFindDatesUTCSearchIndex(layer, {})); 120 | }); 121 | }); 122 | describe('Test findDatesUTC using catalog', () => { 123 | beforeEach(async () => { 124 | setAuthToken(AUTH_TOKEN); 125 | mockNetwork.reset(); 126 | }); 127 | 128 | test('catalog is used if token is set', async () => { 129 | const layer = new MODISLayer({ 130 | instanceId: 'INSTANCE_ID', 131 | layerId: 'LAYER_ID', 132 | }); 133 | await checkIfCorrectEndpointIsUsedFindDatesUTC( 134 | AUTH_TOKEN, 135 | constructFixtureFindDatesUTCCatalog(layer, {}), 136 | CATALOG_URL, 137 | ); 138 | }); 139 | 140 | test.each(layerParamsArr)('check if correct request is constructed', async (layerParams) => { 141 | let constructorParams: Record = {}; 142 | 143 | const layer = new MODISLayer({ 144 | instanceId: 'INSTANCE_ID', 145 | layerId: 'LAYER_ID', 146 | ...constructorParams, 147 | }); 148 | const fixtures = constructFixtureFindDatesUTCCatalog(layer, layerParams); 149 | await checkRequestFindDatesUTC(fixtures); 150 | }); 151 | 152 | test('response from service', async () => { 153 | const layer = new MODISLayer({ 154 | instanceId: 'INSTANCE_ID', 155 | layerId: 'LAYER_ID', 156 | }); 157 | await checkResponseFindDatesUTC(constructFixtureFindDatesUTCCatalog(layer, {})); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /src/layer/__tests__/S3OLCILayer.ts: -------------------------------------------------------------------------------- 1 | import { BBox, CRS_EPSG4326, S3OLCILayer, setAuthToken, DATASET_S3OLCI } from '../../index'; 2 | import { 3 | constructFixtureFindTilesSearchIndex, 4 | constructFixtureFindTilesCatalog, 5 | } from './fixtures.S3OLCILayer'; 6 | 7 | import { 8 | AUTH_TOKEN, 9 | checkIfCorrectEndpointIsUsed, 10 | checkRequestFindTiles, 11 | checkResponseFindTiles, 12 | mockNetwork, 13 | } from './testUtils.findTiles'; 14 | 15 | import { 16 | checkIfCorrectEndpointIsUsedFindDatesUTC, 17 | checkRequestFindDatesUTC, 18 | checkResponseFindDatesUTC, 19 | } from './testUtils.findDatesUTC'; 20 | 21 | import { 22 | constructFixtureFindDatesUTCSearchIndex, 23 | constructFixtureFindDatesUTCCatalog, 24 | } from './fixtures.findDatesUTC'; 25 | import { checkLayersParamsEndpoint } from './testUtils.layers'; 26 | 27 | const CATALOG_URL = 'https://creodias.sentinel-hub.com/api/v1/catalog/1.0.0/search'; 28 | const SEARCH_INDEX_URL = 'https://creodias.sentinel-hub.com/index/v3/collections/S3OLCI/searchIndex'; 29 | 30 | const fromTime: Date = new Date(Date.UTC(2020, 4 - 1, 1, 0, 0, 0, 0)); 31 | const toTime: Date = new Date(Date.UTC(2020, 5 - 1, 1, 23, 59, 59, 999)); 32 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 33 | 34 | const layerParamsArr: Record[] = [ 35 | { 36 | fromTime: fromTime, 37 | toTime: toTime, 38 | bbox: bbox, 39 | }, 40 | ]; 41 | 42 | describe('Test findTiles using searchIndex', () => { 43 | beforeEach(async () => { 44 | setAuthToken(null); 45 | mockNetwork.reset(); 46 | }); 47 | 48 | test('searchIndex is used if token is not set', async () => { 49 | await checkIfCorrectEndpointIsUsed( 50 | null, 51 | constructFixtureFindTilesSearchIndex(S3OLCILayer, {}), 52 | SEARCH_INDEX_URL, 53 | ); 54 | }); 55 | 56 | test.each(layerParamsArr)('check if correct request is constructed', async (layerParams) => { 57 | const fixtures = constructFixtureFindTilesSearchIndex(S3OLCILayer, layerParams); 58 | await checkRequestFindTiles(fixtures); 59 | }); 60 | 61 | test('response from searchIndex', async () => { 62 | await checkResponseFindTiles(constructFixtureFindTilesSearchIndex(S3OLCILayer, {})); 63 | }); 64 | }); 65 | 66 | describe('Test findTiles using catalog', () => { 67 | beforeEach(async () => { 68 | setAuthToken(AUTH_TOKEN); 69 | mockNetwork.reset(); 70 | }); 71 | 72 | test('Catalog is used if token is set', async () => { 73 | await checkIfCorrectEndpointIsUsed( 74 | AUTH_TOKEN, 75 | constructFixtureFindTilesCatalog(S3OLCILayer, {}), 76 | CATALOG_URL, 77 | ); 78 | }); 79 | 80 | test.each(layerParamsArr)('check if correct request is constructed', async (layerParams) => { 81 | const fixtures = constructFixtureFindTilesCatalog(S3OLCILayer, layerParams); 82 | await checkRequestFindTiles(fixtures); 83 | }); 84 | 85 | test('response from catalog', async () => { 86 | await checkResponseFindTiles(constructFixtureFindTilesCatalog(S3OLCILayer, {})); 87 | }); 88 | }); 89 | 90 | describe('Test findDatesUTC using searchIndex', () => { 91 | beforeEach(async () => { 92 | setAuthToken(null); 93 | mockNetwork.reset(); 94 | }); 95 | 96 | test('findAvailableData is used if token is not set', async () => { 97 | const layer = new S3OLCILayer({ 98 | instanceId: 'INSTANCE_ID', 99 | layerId: 'LAYER_ID', 100 | }); 101 | await checkIfCorrectEndpointIsUsedFindDatesUTC( 102 | null, 103 | constructFixtureFindDatesUTCSearchIndex(layer, {}), 104 | DATASET_S3OLCI.findDatesUTCUrl, 105 | ); 106 | }); 107 | 108 | test.each(layerParamsArr)('check if correct request is constructed', async (layerParams) => { 109 | let constructorParams: Record = {}; 110 | 111 | const layer = new S3OLCILayer({ 112 | instanceId: 'INSTANCE_ID', 113 | layerId: 'LAYER_ID', 114 | ...constructorParams, 115 | }); 116 | const fixtures = constructFixtureFindDatesUTCSearchIndex(layer, layerParams); 117 | await checkRequestFindDatesUTC(fixtures); 118 | }); 119 | 120 | test('response from service', async () => { 121 | const layer = new S3OLCILayer({ 122 | instanceId: 'INSTANCE_ID', 123 | layerId: 'LAYER_ID', 124 | }); 125 | await checkResponseFindDatesUTC(constructFixtureFindDatesUTCSearchIndex(layer, {})); 126 | }); 127 | }); 128 | describe('Test findDatesUTC using catalog', () => { 129 | beforeEach(async () => { 130 | setAuthToken(AUTH_TOKEN); 131 | mockNetwork.reset(); 132 | }); 133 | 134 | test('catalog is used if token is set', async () => { 135 | const layer = new S3OLCILayer({ 136 | instanceId: 'INSTANCE_ID', 137 | layerId: 'LAYER_ID', 138 | }); 139 | await checkIfCorrectEndpointIsUsedFindDatesUTC( 140 | AUTH_TOKEN, 141 | constructFixtureFindDatesUTCCatalog(layer, {}), 142 | CATALOG_URL, 143 | ); 144 | }); 145 | 146 | test.each(layerParamsArr)('check if correct request is constructed', async (layerParams) => { 147 | let constructorParams: Record = {}; 148 | 149 | const layer = new S3OLCILayer({ 150 | instanceId: 'INSTANCE_ID', 151 | layerId: 'LAYER_ID', 152 | ...constructorParams, 153 | }); 154 | const fixtures = constructFixtureFindDatesUTCCatalog(layer, layerParams); 155 | await checkRequestFindDatesUTC(fixtures); 156 | }); 157 | 158 | test('response from service', async () => { 159 | const layer = new S3OLCILayer({ 160 | instanceId: 'INSTANCE_ID', 161 | layerId: 'LAYER_ID', 162 | }); 163 | await checkResponseFindDatesUTC(constructFixtureFindDatesUTCCatalog(layer, {})); 164 | }); 165 | }); 166 | 167 | describe('correct endpoint is used for layer params', () => { 168 | beforeEach(async () => { 169 | setAuthToken(AUTH_TOKEN); 170 | mockNetwork.reset(); 171 | }); 172 | 173 | test('updateLayerFromServiceIfNeeded', async () => { 174 | await checkLayersParamsEndpoint(mockNetwork, S3OLCILayer, 'https://services.sentinel-hub.com'); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /src/layer/__tests__/WmsLayer.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | 4 | import { 5 | BBox, 6 | CRS_EPSG4326, 7 | ApiType, 8 | MimeTypes, 9 | WmsLayer, 10 | setAuthToken, 11 | invalidateCaches, 12 | } from '../../index'; 13 | 14 | import '../../../jest-setup'; 15 | 16 | const mockNetwork = new MockAdapter(axios); 17 | 18 | beforeEach(async () => { 19 | await invalidateCaches(); 20 | }); 21 | 22 | test('WmsLayer.getMapUrl returns an URL', () => { 23 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 24 | const layerId = 'PROBAV_S1_TOA_333M'; 25 | const layer = new WmsLayer({ 26 | baseUrl: 'https://proba-v-mep.esa.int/applications/geo-viewer/app/geoserver/ows', 27 | layerId, 28 | }); 29 | 30 | const getMapParams = { 31 | bbox: bbox, 32 | fromTime: new Date(Date.UTC(2020, 1 - 1, 10, 0, 0, 0)), // 2020-01-10/2020-01-10 33 | toTime: new Date(Date.UTC(2020, 1 - 1, 10, 23, 59, 59)), 34 | width: 512, 35 | height: 512, 36 | format: MimeTypes.JPEG, 37 | }; 38 | const imageUrl = layer.getMapUrl(getMapParams, ApiType.WMS); 39 | 40 | expect(imageUrl).toHaveOrigin('https://proba-v-mep.esa.int'); 41 | expect(imageUrl).toHaveQueryParamsValues({ 42 | service: 'WMS', 43 | version: '1.1.1', 44 | request: 'GetMap', 45 | format: 'image/jpeg', 46 | layers: layerId, 47 | srs: 'EPSG:4326', 48 | bbox: '19,20,20,21', 49 | time: '2020-01-10T00:00:00Z/2020-01-10T23:59:59Z', 50 | width: '512', 51 | height: '512', 52 | }); 53 | expect(imageUrl).not.toHaveQueryParams(['showlogo']); 54 | expect(imageUrl).not.toHaveQueryParams(['transparent']); 55 | }); 56 | 57 | test('WmsLayer.getMap makes an appropriate request', async () => { 58 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 59 | const layerId = 'PROBAV_S1_TOA_333M'; 60 | const layer = new WmsLayer({ 61 | baseUrl: 'https://proba-v-mep.esa.int/applications/geo-viewer/app/geoserver/ows', 62 | layerId, 63 | }); 64 | 65 | mockNetwork.reset(); 66 | mockNetwork.onAny().replyOnce(200, ''); // we don't care about the response, we will just inspect the request params 67 | 68 | const getMapParams = { 69 | bbox: bbox, 70 | fromTime: new Date(Date.UTC(2020, 1 - 1, 10, 0, 0, 0)), // 2020-01-10/2020-01-10 71 | toTime: new Date(Date.UTC(2020, 1 - 1, 10, 23, 59, 59)), 72 | width: 512, 73 | height: 512, 74 | format: MimeTypes.JPEG, 75 | }; 76 | await layer.getMap(getMapParams, ApiType.WMS); 77 | 78 | expect(mockNetwork.history.get.length).toBe(1); 79 | 80 | const { url } = mockNetwork.history.get[0]; 81 | expect(url).toHaveOrigin('https://proba-v-mep.esa.int'); 82 | expect(url).toHaveQueryParamsValues({ 83 | service: 'WMS', 84 | version: '1.1.1', 85 | request: 'GetMap', 86 | format: 'image/jpeg', 87 | layers: layerId, 88 | srs: 'EPSG:4326', 89 | bbox: '19,20,20,21', 90 | time: '2020-01-10T00:00:00Z/2020-01-10T23:59:59Z', 91 | width: '512', 92 | height: '512', 93 | }); 94 | }); 95 | 96 | test('WmsLayer.findDates should not include auth token in GetCapabilities request', async () => { 97 | const layerId = 'PROBAV_S1_TOA_333M'; 98 | const layer = new WmsLayer({ 99 | baseUrl: 'https://proba-v-mep.esa.int/applications/geo-viewer/app/geoserver/ows', 100 | layerId, 101 | }); 102 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 103 | const fromTime = new Date(Date.UTC(2020, 1 - 1, 10, 0, 0, 0)); 104 | const toTime = new Date(Date.UTC(2020, 1 - 1, 10, 23, 59, 59)); 105 | 106 | setAuthToken('asdf1234'); // this should not have any effect 107 | 108 | mockNetwork.reset(); 109 | // we just test that the request is correct, we don't mock the response data: 110 | mockNetwork.onAny().replyOnce(200, ''); 111 | 112 | try { 113 | await layer.findDatesUTC(bbox, fromTime, toTime); 114 | } catch (ex) { 115 | // we don't care about success, we will just inspect the request 116 | } 117 | 118 | expect(mockNetwork.history.get.length).toBe(1); 119 | 120 | const { url } = mockNetwork.history.get[0]; 121 | expect(url).toHaveOrigin('https://proba-v-mep.esa.int'); 122 | expect(url).toHaveQueryParamsValues({ 123 | service: 'wms', 124 | request: 'GetCapabilities', 125 | format: 'text/xml', 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /src/layer/__tests__/WmsWmtMsLayer.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | 4 | import { 5 | BBox, 6 | CRS_EPSG4326, 7 | ApiType, 8 | MimeTypes, 9 | setAuthToken, 10 | invalidateCaches, 11 | GetMapParams, 12 | } from '../../index'; 13 | 14 | import '../../../jest-setup'; 15 | import { WmsWmtMsLayer } from '../WmsWmtMsLayer'; 16 | 17 | const mockNetwork = new MockAdapter(axios); 18 | 19 | beforeEach(async () => { 20 | await invalidateCaches(); 21 | }); 22 | 23 | test('WmsWmtMsLayer.getMapUrl returns an URL', () => { 24 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 25 | const layerId = 'PROBAV_S1_TOA_333M'; 26 | const layer = new WmsWmtMsLayer({ 27 | baseUrl: 'https://proba-v-mep.esa.int/applications/geo-viewer/app/geoserver/ows', 28 | layerId, 29 | }); 30 | 31 | const getMapParams: GetMapParams = { 32 | bbox: bbox, 33 | fromTime: null, 34 | toTime: new Date(Date.UTC(2020, 1 - 1, 10, 23, 59, 59)), 35 | width: 511.675, 36 | height: 512.424, 37 | format: MimeTypes.JPEG, 38 | }; 39 | const imageUrl = layer.getMapUrl(getMapParams, ApiType.WMS); 40 | 41 | expect(imageUrl).toHaveOrigin('https://proba-v-mep.esa.int'); 42 | expect(imageUrl).toHaveQueryParamsValues({ 43 | service: 'WMS', 44 | version: '1.1.1', 45 | request: 'GetMap', 46 | format: 'image/jpeg', 47 | layers: layerId, 48 | srs: 'EPSG:3857', 49 | bbox: '2115070.325072198,2273030.926987689,2226389.8158654715,2391878.587944315', 50 | time: '2020-01-10', 51 | width: '512', 52 | height: '512', 53 | }); 54 | expect(imageUrl).not.toHaveQueryParams(['showlogo']); 55 | expect(imageUrl).not.toHaveQueryParams(['transparent']); 56 | }); 57 | 58 | test('WmsWmtMsLayer.getMap makes an appropriate request', async () => { 59 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 60 | const layerId = 'PROBAV_S1_TOA_333M'; 61 | const layer = new WmsWmtMsLayer({ 62 | baseUrl: 'https://proba-v-mep.esa.int/applications/geo-viewer/app/geoserver/ows', 63 | layerId, 64 | }); 65 | 66 | mockNetwork.reset(); 67 | mockNetwork.onAny().replyOnce(200, ''); // we don't care about the response, we will just inspect the request params 68 | 69 | const getMapParams: GetMapParams = { 70 | bbox: bbox, 71 | fromTime: null, 72 | toTime: new Date(Date.UTC(2020, 1 - 1, 10, 23, 59, 59)), 73 | width: 511.675, 74 | height: 512.424, 75 | format: MimeTypes.JPEG, 76 | }; 77 | await layer.getMap(getMapParams, ApiType.WMS); 78 | 79 | expect(mockNetwork.history.get.length).toBe(1); 80 | 81 | const { url } = mockNetwork.history.get[0]; 82 | expect(url).toHaveOrigin('https://proba-v-mep.esa.int'); 83 | expect(url).toHaveQueryParamsValues({ 84 | service: 'WMS', 85 | version: '1.1.1', 86 | request: 'GetMap', 87 | format: 'image/jpeg', 88 | layers: layerId, 89 | srs: 'EPSG:3857', 90 | bbox: '2115070.325072198,2273030.926987689,2226389.8158654715,2391878.587944315', 91 | time: '2020-01-10', 92 | width: '512', 93 | height: '512', 94 | }); 95 | }); 96 | 97 | test('WmsWmtMsLayer.findDates should not include auth token in GetCapabilities request', async () => { 98 | const layerId = 'PROBAV_S1_TOA_333M'; 99 | const layer = new WmsWmtMsLayer({ 100 | baseUrl: 'https://proba-v-mep.esa.int/applications/geo-viewer/app/geoserver/ows', 101 | layerId, 102 | }); 103 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 104 | const fromTime = new Date(Date.UTC(2020, 1 - 1, 10, 0, 0, 0)); 105 | const toTime = new Date(Date.UTC(2020, 1 - 1, 10, 23, 59, 59)); 106 | 107 | setAuthToken('asdf1234'); // this should not have any effect 108 | 109 | mockNetwork.reset(); 110 | // we just test that the request is correct, we don't mock the response data: 111 | mockNetwork.onAny().replyOnce(200, ''); 112 | 113 | try { 114 | await layer.findDatesUTC(bbox, fromTime, toTime); 115 | } catch (ex) { 116 | // we don't care about success, we will just inspect the request 117 | } 118 | 119 | expect(mockNetwork.history.get.length).toBe(1); 120 | 121 | const { url } = mockNetwork.history.get[0]; 122 | expect(url).toHaveOrigin('https://proba-v-mep.esa.int'); 123 | expect(url).toHaveQueryParamsValues({ 124 | service: 'wms', 125 | request: 'GetCapabilities', 126 | format: 'text/xml', 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /src/layer/__tests__/WmtsLayer.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | 4 | import { BBox, CRS_EPSG4326, ApiType, MimeTypes, WmtsLayer, invalidateCaches } from '../../index'; 5 | import '../../../jest-setup'; 6 | import { CRS_EPSG3857 } from '../../crs'; 7 | import { getCapabilitiesWmtsXMLResponse } from './fixtures.getCapabilitiesWMTS'; 8 | 9 | const mockNetwork = new MockAdapter(axios); 10 | 11 | beforeEach(async () => { 12 | await invalidateCaches(); 13 | }); 14 | 15 | test('WmtsLayer.getMap uses tileCoord instead of bbox', async () => { 16 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 17 | const layerId = 'planet_medres_normalized_analytic_2019-06_2019-11_mosaic'; 18 | const baseTemplateUrl = `https://tiles.planet.com/basemaps/v1/planet-tiles/planet_medres_normalized_analytic_2019-06_2019-11_mosaic/gmap/{TileMatrix}/{TileCol}/{TileRow}.png`; 19 | const layer = new WmtsLayer({ 20 | baseUrl: 'https://getCapabilities.com', 21 | layerId, 22 | resourceUrl: baseTemplateUrl + `?api_key=${process.env.PLANET_API_KEY}`, 23 | matrixSet: 'GoogleMapsCompatible15', 24 | }); 25 | mockNetwork.reset(); 26 | const getMapParams = { 27 | bbox: bbox, 28 | fromTime: new Date(), 29 | toTime: new Date(), 30 | width: 256, 31 | height: 256, 32 | tileCoord: { 33 | x: 8852, 34 | y: 9247, 35 | z: 14, 36 | }, 37 | 38 | format: MimeTypes.PNG, 39 | }; 40 | mockNetwork.onAny().replyOnce(200, getCapabilitiesWmtsXMLResponse); // updateLayerFromServiceIfNeeded response 41 | mockNetwork.onAny().replyOnce(200, ''); 42 | await layer.getMap(getMapParams, ApiType.WMTS); 43 | expect(mockNetwork.history.get.length).toBe(2); 44 | 45 | const { url } = mockNetwork.history.get[1]; 46 | expect(url).toHaveOrigin('https://tiles.planet.com'); 47 | expect(url).toHaveBaseUrl( 48 | `https://tiles.planet.com/basemaps/v1/planet-tiles/planet_medres_normalized_analytic_2019-06_2019-11_mosaic/gmap/${getMapParams.tileCoord.z}/${getMapParams.tileCoord.x}/${getMapParams.tileCoord.y}.png`, 49 | ); 50 | }); 51 | 52 | test('WmtsLayer.getMap uses bbox', async () => { 53 | const bbox = new BBox( 54 | CRS_EPSG3857, 55 | 1289034.0450012125, 56 | 1188748.6638910607, 57 | 1291480.029906338, 58 | 1191194.6487961877, 59 | ); 60 | const tileSize = 256; 61 | 62 | const layerId = 'planet_medres_normalized_analytic_2019-06_2019-11_mosaic'; 63 | const baseTemplateUrl = `https://tiles.planet.com/basemaps/v1/planet-tiles/planet_medres_normalized_analytic_2019-06_2019-11_mosaic/gmap/{TileMatrix}/{TileCol}/{TileRow}.png`; 64 | const layer = new WmtsLayer({ 65 | baseUrl: 'https://getCapabilities.com', 66 | layerId, 67 | resourceUrl: baseTemplateUrl + `?api_key=${process.env.PLANET_API_KEY}`, 68 | matrixSet: 'GoogleMapsCompatible15', 69 | }); 70 | 71 | mockNetwork.reset(); 72 | const getMapParams = { 73 | bbox: bbox, 74 | fromTime: new Date(), 75 | toTime: new Date(), 76 | width: tileSize, 77 | height: tileSize, 78 | format: MimeTypes.PNG, 79 | }; 80 | mockNetwork.onAny().replyOnce(200, getCapabilitiesWmtsXMLResponse); // updateLayerFromServiceIfNeeded response 81 | mockNetwork.onAny().replyOnce(200, ''); // we don't care about the response, we will just inspect the request params 82 | 83 | await layer.getMap(getMapParams, ApiType.WMTS); 84 | 85 | expect(mockNetwork.history.get.length).toBe(2); 86 | 87 | const { url } = mockNetwork.history.get[1]; 88 | expect(url).toHaveOrigin('https://tiles.planet.com'); 89 | expect(url).toHaveBaseUrl( 90 | `https://tiles.planet.com/basemaps/v1/planet-tiles/planet_medres_normalized_analytic_2019-06_2019-11_mosaic/gmap/14/8719/7705.png`, 91 | ); 92 | }); 93 | -------------------------------------------------------------------------------- /src/layer/__tests__/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | 4 | import { ApiType, setAuthToken, requestAuthToken, invalidateCaches } from '../../index'; 5 | 6 | import '../../../jest-setup'; 7 | import { constructFixtureGetMap } from './fixtures.auth'; 8 | 9 | const mockNetwork = new MockAdapter(axios); 10 | 11 | const EXAMPLE_TOKEN1 = 'TOKEN111'; 12 | const EXAMPLE_TOKEN2 = 'TOKEN222'; 13 | 14 | beforeEach(async () => { 15 | await invalidateCaches(); 16 | }); 17 | 18 | test('getMap + Processing throws an exception if authToken is not set', async () => { 19 | const { layer, getMapParams } = constructFixtureGetMap(); 20 | setAuthToken(null); 21 | await expect(layer.getMap(getMapParams, ApiType.PROCESSING)).rejects.toThrow(); 22 | }); 23 | 24 | test('setAuthToken sets the Authorization header', async () => { 25 | const { layer, getMapParams } = constructFixtureGetMap(); 26 | setAuthToken(null); 27 | 28 | mockNetwork.reset(); 29 | mockNetwork.onPost().replyOnce(200, ''); // we don't care about response, we just inspect the request 30 | 31 | setAuthToken(EXAMPLE_TOKEN1); 32 | await layer.getMap(getMapParams, ApiType.PROCESSING); 33 | 34 | expect(mockNetwork.history.post.length).toBe(1); 35 | const req = mockNetwork.history.post[0]; 36 | expect(req.headers.Authorization).toBe(`Bearer ${EXAMPLE_TOKEN1}`); 37 | }); 38 | 39 | test('reqConfig sets the Authorization header', async () => { 40 | const { layer, getMapParams } = constructFixtureGetMap(); 41 | setAuthToken(null); 42 | 43 | mockNetwork.reset(); 44 | mockNetwork.onPost().replyOnce(200, ''); // we don't care about response, we just inspect the request 45 | 46 | await layer.getMap(getMapParams, ApiType.PROCESSING, { authToken: EXAMPLE_TOKEN2 }); 47 | 48 | expect(mockNetwork.history.post.length).toBe(1); 49 | const req = mockNetwork.history.post[0]; 50 | expect(req.headers.Authorization).toBe(`Bearer ${EXAMPLE_TOKEN2}`); 51 | }); 52 | 53 | test('reqConfig overrides setAuthToken', async () => { 54 | const { layer, getMapParams } = constructFixtureGetMap(); 55 | setAuthToken(null); 56 | 57 | mockNetwork.reset(); 58 | mockNetwork.onPost().replyOnce(200, ''); // we don't care about response, we just inspect the request 59 | 60 | setAuthToken(EXAMPLE_TOKEN1); 61 | await layer.getMap(getMapParams, ApiType.PROCESSING, { authToken: EXAMPLE_TOKEN2 }); 62 | 63 | expect(mockNetwork.history.post.length).toBe(1); 64 | const req = mockNetwork.history.post[0]; 65 | expect(req.headers.Authorization).toBe(`Bearer ${EXAMPLE_TOKEN2}`); 66 | }); 67 | 68 | test('requestAuthToken correctly encodes URI parameters', async () => { 69 | mockNetwork.reset(); 70 | mockNetwork.onPost().replyOnce(200, ''); // we only check the request 71 | 72 | await requestAuthToken('asd,321', './*&'); 73 | 74 | expect(mockNetwork.history.post.length).toBe(1); 75 | const req = mockNetwork.history.post[0]; 76 | expect(req.data).toBe('grant_type=client_credentials&client_id=asd%2C321&client_secret=.%2F*%26'); 77 | }); 78 | 79 | test('getMap with different authToken following an identical failed getMap makes a request', async () => { 80 | const { layer, getMapParams } = constructFixtureGetMap(); 81 | 82 | mockNetwork.reset(); 83 | mockNetwork.onPost().replyOnce(429, ''); 84 | mockNetwork.onPost().replyOnce(429, ''); 85 | mockNetwork.onPost().replyOnce(429, ''); 86 | mockNetwork.onPost().replyOnce(200, ''); 87 | 88 | const reqConfig = { authToken: EXAMPLE_TOKEN1, retries: 2 }; 89 | try { 90 | await layer.getMap(getMapParams, ApiType.PROCESSING, reqConfig); 91 | } catch (err) { 92 | expect(err.response.status).toBe(429); 93 | } 94 | 95 | reqConfig.authToken = EXAMPLE_TOKEN2; 96 | await layer.getMap(getMapParams, ApiType.PROCESSING, reqConfig); 97 | 98 | expect(mockNetwork.history.post.length).toBe(4); 99 | expect(mockNetwork.history.post[2].headers.Authorization).toBe(`Bearer ${EXAMPLE_TOKEN1}`); 100 | expect(mockNetwork.history.post[3].headers.Authorization).toBe(`Bearer ${EXAMPLE_TOKEN2}`); 101 | }, 30000); 102 | -------------------------------------------------------------------------------- /src/layer/__tests__/cancelToken.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | import makeServiceWorkerEnv from 'service-worker-mock'; 4 | import fetch from 'node-fetch'; 5 | 6 | import { setAuthToken, invalidateCaches, CacheTarget, CancelToken, isCancelled, ApiType } from '../../index'; 7 | import { cacheableRequestsInProgress } from '../../utils/cacheHandlers'; 8 | 9 | import '../../../jest-setup'; 10 | import { constructFixtureFindTiles } from './fixtures.findTiles'; 11 | import { RequestConfiguration } from '../../utils/cancelRequests'; 12 | import { constructFixtureGetMap } from './fixtures.getMap'; 13 | 14 | const createRequestPromise = (useCache = true, setRequestError: (err: any) => void): any => { 15 | const { fromTime, toTime, bbox, layer, mockedResponse } = constructFixtureFindTiles({}); 16 | let cancelToken = new CancelToken(); 17 | const requestsConfig: RequestConfiguration = { 18 | cancelToken: cancelToken, 19 | }; 20 | 21 | if (useCache) { 22 | requestsConfig.cache = { 23 | expiresIn: 60, 24 | targets: [CacheTarget.MEMORY], 25 | }; 26 | } 27 | 28 | const thenFn = jest.fn(); 29 | const catchFn = jest.fn((err) => { 30 | setRequestError(err); 31 | }); 32 | 33 | const requestPromise = layer 34 | .findTiles(bbox, fromTime, toTime, null, null, requestsConfig) 35 | .then(thenFn) 36 | .catch(catchFn); 37 | 38 | return { 39 | requestPromise: requestPromise, 40 | thenFn: thenFn, 41 | catchFn: catchFn, 42 | cancelToken: cancelToken, 43 | mockedResponse: mockedResponse, 44 | }; 45 | }; 46 | const mockNetwork = new MockAdapter(axios, { delayResponse: 10 }); 47 | 48 | describe('Handling cancelled requests', () => { 49 | beforeEach(async () => { 50 | Object.assign(global, makeServiceWorkerEnv(), fetch); // adds these functions to the global object 51 | await invalidateCaches(); 52 | setAuthToken(undefined); 53 | mockNetwork.reset(); 54 | cacheableRequestsInProgress.clear(); 55 | }); 56 | 57 | it('doesnt cancel a request if cancel() is not called', async () => { 58 | let requestError = null; 59 | const { requestPromise, thenFn, catchFn, mockedResponse } = createRequestPromise( 60 | true, 61 | (err) => (requestError = err), 62 | ); 63 | 64 | mockNetwork.onPost().replyOnce(200, mockedResponse); 65 | 66 | await Promise.all([requestPromise]); 67 | expect(thenFn).toHaveBeenCalled(); 68 | expect(catchFn).not.toHaveBeenCalled(); 69 | expect(requestError).toBeNull(); 70 | expect(isCancelled(requestError)).toBeFalsy(); 71 | expect(cacheableRequestsInProgress.size).toBe(0); 72 | }); 73 | 74 | it.each([[true], [false]])('cancels a request', async (useCache) => { 75 | let requestError = null; 76 | const { requestPromise, thenFn, catchFn, cancelToken } = createRequestPromise( 77 | useCache, 78 | (err) => (requestError = err), 79 | ); 80 | 81 | mockNetwork.onPost().replyOnce(200); 82 | 83 | await Promise.all([requestPromise, setTimeout(() => cancelToken.cancel(), 1)]); 84 | expect(thenFn).not.toHaveBeenCalled(); 85 | expect(catchFn).toHaveBeenCalled(); 86 | expect(isCancelled(requestError)).toBeTruthy(); 87 | //check if request was removed from requestsInProgress 88 | expect(cacheableRequestsInProgress.size).toBe(0); 89 | }); 90 | 91 | it('makes a second request after first is cancelled', async () => { 92 | let requestError = null; 93 | const { requestPromise, thenFn, catchFn, cancelToken, mockedResponse } = createRequestPromise( 94 | true, 95 | (err) => (requestError = err), 96 | ); 97 | 98 | mockNetwork.onPost().replyOnce(200, mockedResponse); 99 | mockNetwork.onPost().replyOnce(200, mockedResponse); 100 | 101 | //first request is cancelled 102 | await Promise.all([requestPromise, setTimeout(() => cancelToken.cancel(), 1)]); 103 | expect(thenFn).not.toHaveBeenCalled(); 104 | expect(catchFn).toHaveBeenCalled(); 105 | expect(isCancelled(requestError)).toBeTruthy(); 106 | expect(cacheableRequestsInProgress.size).toBe(0); 107 | 108 | //repeat request without cancelling 109 | requestError = null; 110 | 111 | const { 112 | requestPromise: requestPromise2, 113 | thenFn: thenFn2, 114 | catchFn: catchFn2, 115 | } = createRequestPromise(true, (err) => (requestError = err)); 116 | 117 | await Promise.all([requestPromise2]); 118 | expect(thenFn2).toHaveBeenCalled(); 119 | expect(catchFn2).not.toHaveBeenCalled(); 120 | expect(requestError).toBeNull(); 121 | expect(isCancelled(requestError)).toBeFalsy(); 122 | expect(cacheableRequestsInProgress.size).toBe(0); 123 | }); 124 | 125 | it('handles multiple requests with the same cancel token', async () => { 126 | const { requestPromise, cancelToken, mockedResponse } = createRequestPromise(true, () => {}); 127 | 128 | const { layer, getMapParams, mockedResponse: mockedResponse2 } = constructFixtureGetMap(); 129 | 130 | mockNetwork.onPost().replyOnce(200, mockedResponse); 131 | mockNetwork.onPost().replyOnce(200, mockedResponse2); 132 | 133 | setAuthToken('EXAMPLE_TOKEN'); 134 | 135 | await Promise.all([ 136 | requestPromise, 137 | layer 138 | .getMap(getMapParams, ApiType.PROCESSING, { 139 | cancelToken: cancelToken, 140 | cache: { 141 | expiresIn: 60, 142 | targets: [CacheTarget.MEMORY], 143 | }, 144 | }) 145 | .catch((err: any) => console.log(err)), 146 | setTimeout(() => cancelToken.cancel(), 10), 147 | ]); 148 | 149 | expect(cacheableRequestsInProgress.size).toBe(0); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /src/layer/__tests__/fixtures.ProcessingDataFusionLayer.ts: -------------------------------------------------------------------------------- 1 | import { BBox, CRS_EPSG4326, MimeTypes } from '../../index'; 2 | 3 | export function constructFixtureGetMapRequest({ 4 | bbox = new BBox(CRS_EPSG4326, 18, 20, 20, 22), 5 | width = 512, 6 | height = 512, 7 | format = MimeTypes.JPEG, 8 | evalscript = '', 9 | data = [] as any[], 10 | }): Record { 11 | const expectedRequest = { 12 | evalscript: evalscript, 13 | input: { 14 | bounds: { 15 | bbox: [bbox.minX, bbox.minY, bbox.maxX, bbox.maxY], 16 | properties: { crs: bbox.crs.opengisUrl }, 17 | }, 18 | data: data, 19 | }, 20 | output: { 21 | width: width, 22 | height: height, 23 | responses: [ 24 | { 25 | format: { 26 | type: format, 27 | }, 28 | identifier: 'default', 29 | }, 30 | ], 31 | }, 32 | }; 33 | 34 | return { 35 | expectedRequest: expectedRequest, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/layer/__tests__/fixtures.auth.ts: -------------------------------------------------------------------------------- 1 | import { S2L2ALayer, BBox, CRS_EPSG4326, MimeTypes } from '../../index'; 2 | 3 | export function constructFixtureGetMap(): Record { 4 | const layer = new S2L2ALayer({ 5 | evalscript: '//VERSION=3\nreturn [B02, B02, B02];', 6 | maxCloudCoverPercent: 100, 7 | }); 8 | const getMapParams = { 9 | bbox: new BBox(CRS_EPSG4326, 18, 20, 20, 22), 10 | fromTime: new Date(Date.UTC(2018, 11 - 1, 22, 0, 0, 0)), 11 | toTime: new Date(Date.UTC(2018, 12 - 1, 22, 23, 59, 59)), 12 | width: 512, 13 | height: 512, 14 | format: MimeTypes.JPEG, 15 | }; 16 | 17 | return { 18 | layer: layer, 19 | getMapParams: getMapParams, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/layer/__tests__/fixtures.findTiles.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { 4 | LinkType, 5 | OrbitDirection, 6 | Resolution, 7 | Polarization, 8 | AcquisitionMode, 9 | S1GRDAWSEULayer, 10 | BBox, 11 | CRS_EPSG4326, 12 | } from '../../index'; 13 | 14 | export function constructFixtureFindTiles({ 15 | sensingTime = '2018-11-28T11:12:13Z', 16 | hasMore = false, 17 | }): Record { 18 | const fromTime = new Date(Date.UTC(2018, 11 - 1, 22, 0, 0, 0)); 19 | const toTime = new Date(Date.UTC(2018, 12 - 1, 22, 23, 59, 59)); 20 | const bbox = new BBox(CRS_EPSG4326, 19, 20, 20, 21); 21 | const layer = new S1GRDAWSEULayer({ 22 | instanceId: 'INSTANCE_ID', 23 | layerId: 'LAYER_ID', 24 | acquisitionMode: AcquisitionMode.IW, 25 | polarization: Polarization.DV, 26 | resolution: Resolution.HIGH, 27 | }); 28 | 29 | const mockedResponse = { 30 | tiles: [ 31 | { 32 | type: 'S1', 33 | id: 1293846, 34 | originalId: 'S1A_EW_GRDM_1SDH_20200202T180532_20200202T180632_031077_03921C_E6C8', 35 | dataUri: 36 | 's3://sentinel-s1-l1c/GRD/2020/2/2/EW/DH/S1A_EW_GRDM_1SDH_20200202T180532_20200202T180632_031077_03921C_E6C8', 37 | dataGeometry: { 38 | type: 'MultiPolygon', 39 | crs: { 40 | type: 'name', 41 | properties: { 42 | name: 'urn:ogc:def:crs:EPSG::4326', 43 | }, 44 | }, 45 | coordinates: [ 46 | [ 47 | [ 48 | [-28.958387727765576, 77.22089053106154], 49 | [-28.454271377131395, 77.28385150034897], 50 | [-27.718918346651687, 77.37243188785827], 51 | [-26.974008583323926, 77.45890918854761], 52 | [-26.217031402559755, 77.54352656462356], 53 | [-25.447186512415197, 77.62630504330521], 54 | [-24.667542862300945, 77.7068623880844], 55 | [-28.958387727765576, 77.22089053106154], 56 | ], 57 | ], 58 | ], 59 | }, 60 | sensingTime: sensingTime, 61 | rasterWidth: 10459, 62 | rasterHeight: 9992, 63 | polarization: 'DV', 64 | resolution: 'HIGH', 65 | orbitDirection: 'ASCENDING', 66 | acquisitionMode: 'IW', 67 | timeliness: 'NRT3h', 68 | additionalData: {}, 69 | missionDatatakeId: 234012, 70 | sliceNumber: 5, 71 | }, 72 | ], 73 | hasMore: hasMore, 74 | maxOrderKey: '2020-02-02T08:17:57Z;1295159', 75 | }; 76 | const expectedResultTiles = [ 77 | { 78 | geometry: { 79 | type: 'MultiPolygon', 80 | crs: { 81 | type: 'name', 82 | properties: { 83 | name: 'urn:ogc:def:crs:EPSG::4326', 84 | }, 85 | }, 86 | coordinates: [ 87 | [ 88 | [ 89 | [-28.958387727765576, 77.22089053106154], 90 | [-28.454271377131395, 77.28385150034897], 91 | [-27.718918346651687, 77.37243188785827], 92 | [-26.974008583323926, 77.45890918854761], 93 | [-26.217031402559755, 77.54352656462356], 94 | [-25.447186512415197, 77.62630504330521], 95 | [-24.667542862300945, 77.7068623880844], 96 | [-28.958387727765576, 77.22089053106154], 97 | ], 98 | ], 99 | ], 100 | }, 101 | sensingTime: moment.utc(sensingTime).toDate(), 102 | meta: { 103 | acquisitionMode: AcquisitionMode.IW, 104 | polarization: Polarization.DV, 105 | resolution: Resolution.HIGH, 106 | orbitDirection: OrbitDirection.ASCENDING, 107 | tileId: 1293846, 108 | }, 109 | links: [ 110 | { 111 | target: 112 | 's3://sentinel-s1-l1c/GRD/2020/2/2/EW/DH/S1A_EW_GRDM_1SDH_20200202T180532_20200202T180632_031077_03921C_E6C8', 113 | type: LinkType.AWS, 114 | }, 115 | ], 116 | }, 117 | ]; 118 | return { 119 | fromTime: fromTime, 120 | toTime: toTime, 121 | bbox: bbox, 122 | layer: layer, 123 | mockedResponse: mockedResponse, 124 | expectedResultTiles: expectedResultTiles, 125 | expectedResultHasMore: hasMore, 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /src/layer/__tests__/fixtures.getHugeMap.ts: -------------------------------------------------------------------------------- 1 | import { S2L2ALayer, BBox, CRS_EPSG4326 } from '../../index'; 2 | 3 | export function constructFixtureGetMapTiff(): Record { 4 | const layer = new S2L2ALayer({ 5 | evalscript: '//VERSION=3\nreturn [B02, B02, B02];', 6 | maxCloudCoverPercent: 100, 7 | }); 8 | const getMapParams = { 9 | bbox: new BBox(CRS_EPSG4326, 18, 20, 20, 22), 10 | fromTime: new Date(Date.UTC(2019, 11 - 1, 22, 0, 0, 0)), 11 | toTime: new Date(Date.UTC(2019, 12 - 1, 22, 23, 59, 59)), 12 | width: 2501, 13 | height: 2501, 14 | format: 'image/tiff', 15 | }; 16 | 17 | return { 18 | layer: layer, 19 | getMapParams: getMapParams, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/layer/__tests__/fixtures.getMap.ts: -------------------------------------------------------------------------------- 1 | import { S2L2ALayer, BBox, CRS_EPSG4326, MimeTypes } from '../../index'; 2 | 3 | export function constructFixtureGetMap(): Record { 4 | const layer = new S2L2ALayer({ 5 | evalscript: '//VERSION=3\nreturn [B02, B02, B02];', 6 | maxCloudCoverPercent: 100, 7 | }); 8 | const getMapParams = { 9 | bbox: new BBox(CRS_EPSG4326, 18, 20, 20, 22), 10 | fromTime: new Date(Date.UTC(2019, 11 - 1, 22, 0, 0, 0)), 11 | toTime: new Date(Date.UTC(2019, 12 - 1, 22, 23, 59, 59)), 12 | width: 512, 13 | height: 512, 14 | format: MimeTypes.JPEG, 15 | }; 16 | 17 | const buffer = new ArrayBuffer(8); 18 | const mockedResponse = (config: any): any => { 19 | if (config.responseType === 'arraybuffer') { 20 | return [200, buffer]; 21 | } 22 | }; 23 | 24 | return { 25 | layer: layer, 26 | getMapParams: getMapParams, 27 | mockedResponse: mockedResponse, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/layer/__tests__/fixtures.mosaickingOrder.ts: -------------------------------------------------------------------------------- 1 | import { BBox, CRS_EPSG4326, GetMapParams, MimeTypes } from '../../index'; 2 | 3 | const bbox4326 = new BBox(CRS_EPSG4326, 11.9, 42.05, 12.95, 43.09); 4 | 5 | const getMapParams: GetMapParams = { 6 | bbox: bbox4326, 7 | fromTime: new Date(Date.UTC(2018, 11 - 1, 22, 0, 0, 0)), 8 | toTime: new Date(Date.UTC(2018, 12 - 1, 22, 23, 59, 59)), 9 | width: 256, 10 | height: 256, 11 | format: MimeTypes.JPEG, 12 | }; 13 | 14 | const mockedLayersResponse = [ 15 | { 16 | '@id': 'https://services.sentinel-hub.com/api/v2/configuration/instances/INSTANCE_ID/layers/LAYER_ID', 17 | id: 'LAYER_ID', 18 | title: 'Title', 19 | description: 'Description', 20 | styles: [ 21 | { 22 | name: 'default', 23 | description: 'Default layer style', 24 | evalScript: 25 | '//VERSION=3\nlet minVal = 0.0;\nlet maxVal = 0.4;\n\nlet viz = new HighlightCompressVisualizer(minVal, maxVal);\n\nfunction setup() {\n return {\n input: ["B04", "B03", "B02","dataMask"],\n output: { bands: 4 }\n };\n}\n\nfunction evaluatePixel(samples) {\n let val = [samples.B04, samples.B03, samples.B02,samples.dataMask];\n return viz.processList(val);\n}', 26 | }, 27 | ], 28 | orderHint: 0, 29 | instance: { 30 | '@id': 'https://services.sentinel-hub.com/api/v2/configuration/instances/INSTANCE_ID', 31 | }, 32 | dataset: { '@id': 'https://services.sentinel-hub.com/configuration/v1/datasets/S2L2A' }, 33 | datasetSource: { '@id': 'https://services.sentinel-hub.com/configuration/v1/datasets/S2L2A/sources/2' }, 34 | defaultStyleName: 'default', 35 | datasourceDefaults: { 36 | upsampling: 'BICUBIC', 37 | mosaickingOrder: 'mostRecent', 38 | temporal: false, 39 | maxCloudCoverage: 100.0, 40 | previewMode: 'EXTENDED_PREVIEW', 41 | type: 'S2L2A', 42 | }, 43 | }, 44 | ]; 45 | 46 | export function constructFixtureMosaickingOrder(): Record { 47 | return { 48 | getMapParams: getMapParams, 49 | mockedLayersResponse: mockedLayersResponse, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/layer/__tests__/getHugeMap.ts: -------------------------------------------------------------------------------- 1 | import { ApiType, setAuthToken, invalidateCaches } from '../../index'; 2 | 3 | import '../../../jest-setup'; 4 | import { constructFixtureGetMapTiff } from './fixtures.getHugeMap'; 5 | 6 | const EXAMPLE_TOKEN = 'TOKEN111'; 7 | 8 | beforeEach(async () => { 9 | await invalidateCaches(); 10 | }); 11 | 12 | test('getHugeMap should throw an error when format is not supported', async () => { 13 | const { layer, getMapParams } = constructFixtureGetMapTiff(); 14 | setAuthToken(EXAMPLE_TOKEN); 15 | try { 16 | await layer.getHugeMap(getMapParams, ApiType.PROCESSING); 17 | } catch (e) { 18 | expect(e.message).toBe('Format image/tiff not supported, only image/png and image/jpeg are allowed'); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /src/layer/__tests__/legacy.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import '../../../jest-setup'; 3 | 4 | import { parseLegacyWmsGetMapParams, BBox, CRS_EPSG3857, GetMapParams, PreviewMode } from '../../index'; 5 | 6 | test('parseLegacyWmsGetMapParams with evalscripturl', () => { 7 | const evalscriptUrlOriginal = 8 | 'https://gist.githubusercontent.com/sinergise-anze/33fe78d9b1fd24d656882d7916a83d4d/raw/295b9d9f033c7e3f1e533363322d84846808564c/data-fusion-evalscript.js'; 9 | const wmsParams = { 10 | service: 'WMS', 11 | request: 'GetMap', 12 | showlogo: 'false', 13 | maxcc: '70', 14 | time: '2019-12-22/2019-12-22', 15 | crs: 'EPSG:3857', 16 | format: 'image/jpeg', 17 | bbox: [1282655, 5053636, 1500575, 5238596], 18 | evalscripturl: evalscriptUrlOriginal, 19 | evalsource: 'S2', 20 | layers: '1_TRUE_COLOR', 21 | width: 1700, 22 | height: 605, 23 | nicename: 'Sentinel-2+L1C+from+2019-12-22.jpg', 24 | transparent: '1', 25 | bgcolor: '00000000', 26 | preview: 3, 27 | gain: 0.7, 28 | gamma: 0.9, 29 | }; 30 | const { layers, evalscript, evalscriptUrl, evalsource, getMapParams, otherLayerParams } = 31 | parseLegacyWmsGetMapParams(wmsParams); 32 | 33 | expect(evalscript).toEqual(null); 34 | expect(evalscriptUrl).toEqual(evalscriptUrlOriginal); 35 | expect(evalsource).toEqual('S2'); 36 | expect(layers).toEqual('1_TRUE_COLOR'); 37 | 38 | const expectedGetMapParams: GetMapParams = { 39 | bbox: new BBox(CRS_EPSG3857, 1282655, 5053636, 1500575, 5238596), 40 | fromTime: moment.utc('2019-12-22').startOf('day').toDate(), 41 | toTime: moment.utc('2019-12-22').endOf('day').toDate(), 42 | format: 'image/jpeg', 43 | width: 1700, 44 | height: 605, 45 | preview: PreviewMode.EXTENDED_PREVIEW, 46 | nicename: 'Sentinel-2+L1C+from+2019-12-22.jpg', 47 | showlogo: false, 48 | bgcolor: '00000000', 49 | transparent: true, 50 | effects: { 51 | gain: 0.7, 52 | gamma: 0.9, 53 | }, 54 | // we are not testing unknown params field: 55 | unknown: getMapParams.unknown, 56 | }; 57 | expect(getMapParams).toStrictEqual(expectedGetMapParams); 58 | expect(otherLayerParams).toStrictEqual({ 59 | maxCloudCoverPercent: 70, 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/layer/__tests__/mosaickingOrder.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | 4 | import { ApiType, MosaickingOrder, S2L2ALayer, setAuthToken } from '../../index'; 5 | import { ProcessingPayload, ProcessingPayloadDatasource } from '../processing'; 6 | 7 | import '../../../jest-setup'; 8 | import { constructFixtureMosaickingOrder } from './fixtures.mosaickingOrder'; 9 | 10 | const extractDataFilterFromPayload = (payload: ProcessingPayload): any => { 11 | const data: ProcessingPayloadDatasource[] = payload.input.data; 12 | const processingPayloadDatasource: ProcessingPayloadDatasource = data.find((ppd) => ppd.dataFilter); 13 | return processingPayloadDatasource.dataFilter; 14 | }; 15 | 16 | test('Mosaicking order is not set in constructor', async () => { 17 | const layerS2L2A = new S2L2ALayer({ 18 | instanceId: 'INSTANCE_ID', 19 | layerId: 'LAYER_ID', 20 | }); 21 | 22 | expect(layerS2L2A.mosaickingOrder).toBeNull(); 23 | }); 24 | 25 | test('Mosaicking order is set in constructor', async () => { 26 | const layerS2L2A = new S2L2ALayer({ 27 | instanceId: 'INSTANCE_ID', 28 | layerId: 'LAYER_ID', 29 | mosaickingOrder: MosaickingOrder.LEAST_CC, 30 | }); 31 | 32 | expect(layerS2L2A.mosaickingOrder).toBe(MosaickingOrder.LEAST_CC); 33 | }); 34 | 35 | test('Mosaicking order can be changed', async () => { 36 | const layerS2L2A = new S2L2ALayer({ 37 | instanceId: 'INSTANCE_ID', 38 | layerId: 'LAYER_ID', 39 | mosaickingOrder: MosaickingOrder.LEAST_CC, 40 | }); 41 | expect(layerS2L2A.mosaickingOrder).toBe(MosaickingOrder.LEAST_CC); 42 | layerS2L2A.mosaickingOrder = MosaickingOrder.MOST_RECENT; 43 | expect(layerS2L2A.mosaickingOrder).toBe(MosaickingOrder.MOST_RECENT); 44 | }); 45 | 46 | test('Mosaicking order is set on instance/layer in dashboard WMS', async () => { 47 | const layerS2L2A = new S2L2ALayer({ 48 | instanceId: 'INSTANCE_ID', 49 | layerId: 'LAYER_ID', 50 | }); 51 | 52 | const { getMapParams } = constructFixtureMosaickingOrder(); 53 | 54 | const mockNetwork = new MockAdapter(axios); 55 | mockNetwork.reset(); 56 | mockNetwork.onGet().reply(200); 57 | 58 | expect(layerS2L2A.mosaickingOrder).toBeNull(); 59 | await layerS2L2A.getMap(getMapParams, ApiType.WMS); 60 | expect(mockNetwork.history.get.length).toBe(1); 61 | expect(mockNetwork.history.get[0].url).not.toHaveQueryParams(['priority']); 62 | 63 | layerS2L2A.mosaickingOrder = MosaickingOrder.LEAST_RECENT; 64 | await layerS2L2A.getMap(getMapParams, ApiType.WMS); 65 | expect(mockNetwork.history.get.length).toBe(2); 66 | const { url } = mockNetwork.history.get[1]; 67 | expect(url).toHaveQueryParams(['priority']); 68 | expect(url).toHaveQueryParamsValues({ priority: MosaickingOrder.LEAST_RECENT }); 69 | }); 70 | 71 | test('Mosaicking order is set from instance/layer in dashboard processing', async () => { 72 | const layerS2L2A = new S2L2ALayer({ 73 | instanceId: 'INSTANCE_ID', 74 | layerId: 'LAYER_ID', 75 | }); 76 | 77 | const { getMapParams, mockedLayersResponse } = constructFixtureMosaickingOrder(); 78 | 79 | const mockNetwork = new MockAdapter(axios); 80 | mockNetwork.reset(); 81 | mockNetwork.onGet().reply(200, mockedLayersResponse); 82 | mockNetwork.onPost().reply(200); 83 | setAuthToken('Token'); 84 | expect(layerS2L2A.mosaickingOrder).toBeNull(); 85 | await layerS2L2A.getMap(getMapParams, ApiType.PROCESSING); 86 | expect(mockNetwork.history.post.length).toBe(1); 87 | let dataFilter = extractDataFilterFromPayload(JSON.parse(mockNetwork.history.post[0].data)); 88 | expect(dataFilter.mosaickingOrder).toBe(MosaickingOrder.MOST_RECENT); 89 | expect(layerS2L2A.mosaickingOrder).toBe(MosaickingOrder.MOST_RECENT); 90 | 91 | dataFilter = null; 92 | layerS2L2A.mosaickingOrder = MosaickingOrder.LEAST_RECENT; 93 | await layerS2L2A.getMap(getMapParams, ApiType.PROCESSING); 94 | dataFilter = extractDataFilterFromPayload(JSON.parse(mockNetwork.history.post[1].data)); 95 | expect(mockNetwork.history.post.length).toBe(2); 96 | expect(dataFilter.mosaickingOrder).toBe(MosaickingOrder.LEAST_RECENT); 97 | expect(layerS2L2A.mosaickingOrder).toBe(MosaickingOrder.LEAST_RECENT); 98 | }); 99 | -------------------------------------------------------------------------------- /src/layer/__tests__/outputResponses.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | 4 | import { 5 | ApiType, 6 | setAuthToken, 7 | invalidateCaches, 8 | S2L2ALayer, 9 | BBox, 10 | CRS_EPSG4326, 11 | MimeTypes, 12 | } from '../../index'; 13 | import '../../../jest-setup'; 14 | 15 | const mockNetwork = new MockAdapter(axios); 16 | const EXAMPLE_TOKEN = 'TOKEN111'; 17 | 18 | const layerWMS = new S2L2ALayer({ instanceId: 'INSTANCE_ID', layerId: 'LAYER_ID' }); 19 | const layerProcessing = new S2L2ALayer({ 20 | evalscript: `//VERSION=3 21 | function setup() { 22 | return { 23 | input: ["B02", "B03", "B04"], 24 | output: [{ id: "default", bands: 4 }, { id: "index", bands: 2 }] 25 | }; 26 | } 27 | 28 | function evaluatePixel(sample) { 29 | return { 30 | default: [4 * sample.B04, 4 * sample.B03, 4 * sample.B02, sample.dataMask], 31 | index: [sample.B04, sample.dataMask] 32 | }; 33 | }`, 34 | maxCloudCoverPercent: 100, 35 | }); 36 | 37 | const getMapParams = { 38 | bbox: new BBox(CRS_EPSG4326, 18, 20, 20, 22), 39 | fromTime: new Date(Date.UTC(2019, 11 - 1, 22, 0, 0, 0)), 40 | toTime: new Date(Date.UTC(2019, 12 - 1, 22, 23, 59, 59)), 41 | width: 512, 42 | height: 512, 43 | format: MimeTypes.JPEG, 44 | }; 45 | const getMapParamsEmptyOutputResponseId = { ...getMapParams, outputResponseId: '' }; 46 | const getMapParamsDefaultResponseId = { ...getMapParams, outputResponseId: 'default' }; 47 | const getMapParamsIndexResponseId = { ...getMapParams, format: MimeTypes.PNG, outputResponseId: 'index' }; 48 | 49 | beforeEach(async () => { 50 | await invalidateCaches(); 51 | }); 52 | 53 | test('Error if outputResponseId is used with WMS', async () => { 54 | setAuthToken(EXAMPLE_TOKEN); 55 | try { 56 | await layerWMS.getMap(getMapParamsDefaultResponseId, ApiType.WMS); 57 | } catch (e) { 58 | expect(e.message).toBe('outputResponseId is only available with Processing API'); 59 | } 60 | }); 61 | 62 | test('Error if outputResponseId is empty and used with WMS', async () => { 63 | setAuthToken(EXAMPLE_TOKEN); 64 | try { 65 | await layerWMS.getMap(getMapParamsEmptyOutputResponseId, ApiType.WMS); 66 | } catch (e) { 67 | expect(e.message).toBe('outputResponseId is only available with Processing API'); 68 | } 69 | }); 70 | 71 | it('NO output response id, JPEG format', async () => { 72 | // arrayBuffer needs to be used, and removing this will cause getMap to fetch a blob, as window.Blob was created with jsdom 73 | window.Blob = undefined; 74 | 75 | setAuthToken(EXAMPLE_TOKEN); 76 | mockNetwork.reset(); 77 | mockNetwork.onPost().replyOnce(200, ''); 78 | 79 | await layerProcessing.getMap(getMapParams, ApiType.PROCESSING); 80 | 81 | const request = mockNetwork.history.post[0]; 82 | const requestData = JSON.parse(request.data); 83 | expect(requestData.output.responses).toEqual([{ identifier: 'default', format: { type: 'image/jpeg' } }]); 84 | }); 85 | 86 | it('EMPTY output response id, JPEG format', async () => { 87 | // arrayBuffer needs to be used, and removing this will cause getMap to fetch a blob, as window.Blob was created with jsdom 88 | window.Blob = undefined; 89 | setAuthToken(EXAMPLE_TOKEN); 90 | mockNetwork.reset(); 91 | mockNetwork.onPost().replyOnce(200, ''); 92 | 93 | await layerProcessing.getMap(getMapParamsEmptyOutputResponseId, ApiType.PROCESSING); 94 | 95 | const request = mockNetwork.history.post[0]; 96 | const requestData = JSON.parse(request.data); 97 | expect(requestData.output.responses).toEqual([{ identifier: 'default', format: { type: 'image/jpeg' } }]); 98 | }); 99 | 100 | it('DEFAULT output response, JPEG format', async () => { 101 | // arrayBuffer needs to be used, and removing this will cause getMap to fetch a blob, as window.Blob was created with jsdom 102 | window.Blob = undefined; 103 | setAuthToken(EXAMPLE_TOKEN); 104 | mockNetwork.reset(); 105 | mockNetwork.onPost().replyOnce(200, ''); 106 | 107 | await layerProcessing.getMap(getMapParamsDefaultResponseId, ApiType.PROCESSING); 108 | 109 | const request = mockNetwork.history.post[0]; 110 | const requestData = JSON.parse(request.data); 111 | expect(requestData.output.responses).toEqual([{ identifier: 'default', format: { type: 'image/jpeg' } }]); 112 | }); 113 | 114 | it('INDEX output response, PNG format', async () => { 115 | // arrayBuffer needs to be used, and removing this will cause getMap to fetch a blob, as window.Blob was created with jsdom 116 | window.Blob = undefined; 117 | setAuthToken(EXAMPLE_TOKEN); 118 | mockNetwork.reset(); 119 | mockNetwork.onPost().replyOnce(200, ''); 120 | 121 | await layerProcessing.getMap(getMapParamsIndexResponseId, ApiType.PROCESSING); 122 | 123 | const request = mockNetwork.history.post[0]; 124 | const requestData = JSON.parse(request.data); 125 | expect(requestData.output.responses).toEqual([{ identifier: 'index', format: { type: 'image/png' } }]); 126 | }); 127 | -------------------------------------------------------------------------------- /src/layer/__tests__/testUtils.findDatesUTC.ts: -------------------------------------------------------------------------------- 1 | import { setAuthToken, isAuthTokenSet } from '../../index'; 2 | import { mockNetwork } from './testUtils.findTiles'; 3 | 4 | export const AUTH_TOKEN = 'AUTH_TOKEN'; 5 | export const CATALOG_URL = 'https://services.sentinel-hub.com/api/v1/catalog/1.0.0/search'; 6 | 7 | export async function checkIfCorrectEndpointIsUsedFindDatesUTC( 8 | token: string, 9 | fixture: Record, 10 | endpoint: string, 11 | ): Promise { 12 | setAuthToken(token); 13 | expect(isAuthTokenSet()).toBe(!!token); 14 | 15 | const { fromTime, toTime, bbox, layer } = fixture; 16 | mockNetwork.onAny().reply(200); 17 | try { 18 | await layer.findDatesUTC(bbox, fromTime, toTime, { cache: { expiresIn: 0 } }); 19 | } catch (err) { 20 | //we don't care about response 21 | } 22 | expect(mockNetwork.history.post.length).toBe(1); 23 | const request = mockNetwork.history.post[0]; 24 | expect(request.url).toEqual(endpoint); 25 | } 26 | 27 | export async function checkRequestFindDatesUTC(fixtures: Record): Promise { 28 | const { bbox, expectedRequest, fromTime, layer, toTime } = fixtures; 29 | mockNetwork.onAny().reply(200); 30 | try { 31 | await layer.findDatesUTC(bbox, fromTime, toTime, { cache: { expiresIn: 0 } }); 32 | } catch (err) { 33 | //we don't care about response here 34 | } 35 | const request = mockNetwork.history.post[0]; 36 | expect(request.data).not.toBeNull(); 37 | expect(JSON.parse(request.data)).toStrictEqual(expectedRequest); 38 | } 39 | 40 | export async function checkResponseFindDatesUTC(fixtures: Record): Promise { 41 | const { fromTime, toTime, bbox, layer, mockedResponse, expectedRequest, expectedResult } = fixtures; 42 | 43 | mockNetwork.onPost().replyOnce(200, mockedResponse); 44 | 45 | const response = await layer.findDatesUTC(bbox, fromTime, toTime, { cache: { expiresIn: 0 } }); 46 | expect(mockNetwork.history.post.length).toBe(1); 47 | const request = mockNetwork.history.post[0]; 48 | expect(request.data).not.toBeNull(); 49 | expect(JSON.parse(request.data)).toStrictEqual(expectedRequest); 50 | expect(response).toStrictEqual(expectedResult); 51 | } 52 | -------------------------------------------------------------------------------- /src/layer/__tests__/testUtils.findTiles.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | import { setAuthToken, isAuthTokenSet } from '../../index'; 4 | 5 | export const mockNetwork = new MockAdapter(axios); 6 | 7 | export const AUTH_TOKEN = 'AUTH_TOKEN'; 8 | export const CATALOG_URL = 'https://services.sentinel-hub.com/api/v1/catalog/1.0.0/search'; 9 | 10 | export async function checkRequestFindTiles(fixtures: Record): Promise { 11 | const { bbox, expectedRequest, fromTime, layer, toTime } = fixtures; 12 | mockNetwork.onAny().reply(200); 13 | try { 14 | await layer.findTiles(bbox, fromTime, toTime, 5, 0, { cache: { expiresIn: 0 } }); 15 | } catch (err) { 16 | //we don't care about response here 17 | } 18 | const request = mockNetwork.history.post[0]; 19 | expect(request.data).not.toBeNull(); 20 | expect(JSON.parse(request.data)).toStrictEqual(expectedRequest); 21 | } 22 | export async function checkResponseFindTiles(fixtures: Record): Promise { 23 | const { 24 | fromTime, 25 | toTime, 26 | bbox, 27 | layer, 28 | mockedResponse, 29 | expectedRequest, 30 | expectedResultTiles, 31 | expectedResultHasMore, 32 | } = fixtures; 33 | 34 | mockNetwork.onPost().replyOnce(200, mockedResponse); 35 | 36 | const { tiles, hasMore } = await layer.findTiles(bbox, fromTime, toTime, 5, 0, { cache: { expiresIn: 0 } }); 37 | 38 | expect(mockNetwork.history.post.length).toBe(1); 39 | const request = mockNetwork.history.post[0]; 40 | expect(request.data).not.toBeNull(); 41 | expect(JSON.parse(request.data)).toStrictEqual(expectedRequest); 42 | expect(tiles).toStrictEqual(expectedResultTiles); 43 | expect(hasMore).toEqual(expectedResultHasMore); 44 | } 45 | export async function checkIfCorrectEndpointIsUsed( 46 | token: string, 47 | fixture: Record, 48 | endpoint: string, 49 | ): Promise { 50 | setAuthToken(token); 51 | expect(isAuthTokenSet()).toBe(!!token); 52 | 53 | const { fromTime, toTime, bbox, layer } = fixture; 54 | mockNetwork.onAny().reply(200); 55 | try { 56 | await layer.findTiles(bbox, fromTime, toTime, 5, 0, { cache: { expiresIn: 0 } }); 57 | } catch (err) { 58 | //we don't care about response 59 | } 60 | expect(mockNetwork.history.post.length).toBe(1); 61 | const request = mockNetwork.history.post[0]; 62 | expect(request.url).toEqual(endpoint); 63 | } 64 | -------------------------------------------------------------------------------- /src/layer/__tests__/testUtils.layers.ts: -------------------------------------------------------------------------------- 1 | import MockAdapter from 'axios-mock-adapter'; 2 | import { AbstractSentinelHubV3Layer } from '../AbstractSentinelHubV3Layer'; 3 | 4 | export const checkLayersParamsEndpoint = async ( 5 | mockNetwork: MockAdapter, 6 | layerClass: typeof AbstractSentinelHubV3Layer, 7 | expectedEndpoint: string, 8 | ): Promise => { 9 | const instanceId = 'INSTANCE_ID'; 10 | const layerId = 'LAYER_ID'; 11 | 12 | mockNetwork.onGet().reply(200, [ 13 | { 14 | id: layerId, 15 | styles: [ 16 | { 17 | evalScript: '//', 18 | }, 19 | ], 20 | }, 21 | ]); 22 | 23 | const layer = new layerClass({ 24 | instanceId, 25 | layerId, 26 | }); 27 | await layer.updateLayerFromServiceIfNeeded({}); 28 | expect(mockNetwork.history.get[0].url).toBe( 29 | `${expectedEndpoint}/api/v2/configuration/instances/${instanceId}/layers`, 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/layer/__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import { BBox } from '../../bbox'; 2 | import { CRS_EPSG3857, CRS_EPSG4326 } from '../../crs'; 3 | import { OgcServiceTypes, SH_SERVICE_ROOT_URL } from '../const'; 4 | import { 5 | createGetCapabilitiesXmlUrl, 6 | ensureMercatorBBox, 7 | getSHServiceRootUrlFromBaseUrl, 8 | metersPerPixel, 9 | } from '../utils'; 10 | 11 | const cases = [ 12 | { 13 | input: 'https://some-place/sub/wmts?api_key=notAKey', 14 | ogcServiceType: OgcServiceTypes.WMTS, 15 | expected: 16 | 'https://some-place/sub/wmts?service=wmts&request=GetCapabilities&format=text%2Fxml&api_key=notAKey', 17 | }, 18 | { 19 | input: 'https://some-place/sub/wmts', 20 | ogcServiceType: OgcServiceTypes.WMTS, 21 | expected: 'https://some-place/sub/wmts?service=wmts&request=GetCapabilities&format=text%2Fxml', 22 | }, 23 | { 24 | input: 'https://some-place/sub/someKey', 25 | ogcServiceType: OgcServiceTypes.WMS, 26 | expected: 'https://some-place/sub/someKey?service=wms&request=GetCapabilities&format=text%2Fxml', 27 | }, 28 | ]; 29 | 30 | describe("'add' utility", () => { 31 | test.each(cases)('build getCapabilities url', ({ input, ogcServiceType, expected }) => { 32 | const url = createGetCapabilitiesXmlUrl(input, ogcServiceType); 33 | expect(url).toStrictEqual(expected); 34 | }); 35 | }); 36 | 37 | describe('getSHServiceRootUrlFromBaseUrl', () => { 38 | test.each([ 39 | ['https://services.sentinel-hub.com/ogc/wms/instanceId', SH_SERVICE_ROOT_URL.default], 40 | ['https://services-uswest2.sentinel-hub.com/ogc/wms/instanceId', SH_SERVICE_ROOT_URL.default], 41 | ['https://creodias.sentinel-hub.com/ogc/wms/instanceId', SH_SERVICE_ROOT_URL.default], 42 | ['https://shservices.mundiwebservices.com/ogc/1wms/instanceId', SH_SERVICE_ROOT_URL.default], 43 | ['https://sh.dataspace.copernicus.eu/wms/instance', SH_SERVICE_ROOT_URL.cdse], 44 | ['', SH_SERVICE_ROOT_URL.default], 45 | [null, SH_SERVICE_ROOT_URL.default], 46 | [undefined, SH_SERVICE_ROOT_URL.default], 47 | ['not url', SH_SERVICE_ROOT_URL.default], 48 | ])('getSHServiceRootUrlFromBaseUrl %p', async (baseUrl, expected) => { 49 | expect(getSHServiceRootUrlFromBaseUrl(baseUrl)).toBe(expected); 50 | }); 51 | }); 52 | 53 | describe('ensureMercatorBBox', () => { 54 | test.each([ 55 | { 56 | bbox: new BBox( 57 | CRS_EPSG3857, 58 | 112.81332057952881, 59 | 63.97041521013803, 60 | 119.85694837570192, 61 | 65.98227733565385, 62 | ), 63 | expected: new BBox( 64 | CRS_EPSG3857, 65 | 112.81332057952881, 66 | 63.97041521013803, 67 | 119.85694837570192, 68 | 65.98227733565385, 69 | ), 70 | }, 71 | { 72 | bbox: new BBox( 73 | CRS_EPSG4326, 74 | -114.27429199218751, 75 | 45.85176048817254, 76 | -112.17864990234376, 77 | 48.21003212234042, 78 | ), 79 | expected: new BBox( 80 | CRS_EPSG3857, 81 | -12720955.995332174, 82 | 5756625.474213193, 83 | -12487670.185005816, 84 | 6141868.096770483, 85 | ), 86 | }, 87 | ])('ensureMercatorBBox %p', ({ bbox, expected }) => { 88 | expect(ensureMercatorBBox(bbox)).toMatchObject(expected); 89 | }); 90 | }); 91 | 92 | describe('metersPerPixel', () => { 93 | test.each([ 94 | { 95 | bbox: new BBox( 96 | CRS_EPSG3857, 97 | 112.81332057952881, 98 | 63.97041521013803, 99 | 119.85694837570192, 100 | 65.98227733565385, 101 | ), 102 | width: 512, 103 | expected: 0.01375708553868674, 104 | }, 105 | { 106 | bbox: new BBox( 107 | CRS_EPSG3857, 108 | -15028131.257091936, 109 | 2504688.542848655, 110 | -12523442.714243278, 111 | 5009377.085697314, 112 | ), 113 | width: 512, 114 | expected: 4150.788658570558, 115 | }, 116 | { 117 | bbox: new BBox( 118 | CRS_EPSG3857, 119 | -12601714.2312073, 120 | 5870363.772301538, 121 | -12523442.714243278, 122 | 5948635.289265559, 123 | ), 124 | width: 512, 125 | expected: 104.64937737265413, 126 | }, 127 | { 128 | bbox: new BBox( 129 | CRS_EPSG4326, 130 | -114.27429199218751, 131 | 45.85176048817254, 132 | -112.17864990234376, 133 | 48.21003212234042, 134 | ), 135 | width: 512, 136 | expected: 310.4876808881625, 137 | }, 138 | ])('metersPerPixel %p', ({ bbox, width, expected }) => { 139 | expect(metersPerPixel(bbox, width)).toEqual(expected); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /src/layer/__tests__/wmts.utils.ts: -------------------------------------------------------------------------------- 1 | import { BBox, CRS_EPSG3857, CRS_EPSG4326 } from '../..'; 2 | import { bboxToXyzGrid } from '../wmts.utils'; 3 | 4 | const tileMatrices256 = [...Array(20).keys()].map((zoom) => ({ 5 | zoom: zoom, 6 | tileWidth: 256, 7 | tileHeight: 256, 8 | matrixWidth: Math.pow(2, zoom), 9 | matrixHeight: Math.pow(2, zoom), 10 | })); 11 | 12 | const tileMatrices512 = [...Array(19).keys()].map((zoom) => ({ 13 | zoom: zoom + 1, 14 | tileWidth: 512, 15 | tileHeight: 512, 16 | matrixWidth: Math.pow(2, zoom), 17 | matrixHeight: Math.pow(2, zoom), 18 | })); 19 | 20 | const singleTile512Bbox = new BBox( 21 | CRS_EPSG3857, 22 | 1487158.8223163893, 23 | 5009377.085697314, 24 | 1565430.3392804097, 25 | 5087648.602661333, 26 | ); 27 | 28 | const rectangularBbox = new BBox( 29 | CRS_EPSG4326, 30 | 11.7938232421875, 31 | 41.21998578493921, 32 | 13.150634765625002, 33 | 42.431565872579185, 34 | ); 35 | 36 | const singleTile512Fixture = { 37 | description: 'Test extact 512 tile bbox, should return single xyz tile', 38 | bbox: singleTile512Bbox, 39 | tileMatrices: tileMatrices512, 40 | imageWidth: 512, 41 | imageHeight: 512, 42 | expectedResult: { 43 | nativeHeight: 512, 44 | nativeWidth: 512, 45 | tiles: [ 46 | { 47 | imageOffsetX: 0, 48 | imageOffsetY: 0, 49 | x: 275, 50 | y: 191, 51 | z: 10, 52 | }, 53 | ], 54 | }, 55 | }; 56 | 57 | const singleTile512Fixture256Tiles = { 58 | description: 'Test extact 512 tile bbox, stitched with multpile 256 tiles', 59 | bbox: singleTile512Bbox, 60 | tileMatrices: tileMatrices256, 61 | imageWidth: 512, 62 | imageHeight: 512, 63 | expectedResult: { 64 | nativeHeight: 512, 65 | nativeWidth: 512, 66 | tiles: [ 67 | { 68 | imageOffsetX: 0, 69 | imageOffsetY: 0, 70 | x: 550, 71 | y: 382, 72 | z: 10, 73 | }, 74 | { 75 | imageOffsetX: 0, 76 | imageOffsetY: 256, 77 | x: 550, 78 | y: 383, 79 | z: 10, 80 | }, 81 | { 82 | imageOffsetX: 256, 83 | imageOffsetY: 0, 84 | x: 551, 85 | y: 382, 86 | z: 10, 87 | }, 88 | { 89 | imageOffsetX: 256, 90 | imageOffsetY: 256, 91 | x: 551, 92 | y: 383, 93 | z: 10, 94 | }, 95 | ], 96 | }, 97 | }; 98 | 99 | const rectangularBboxFixture = { 100 | description: 'Test rectangular bbox, should return multiple xyz tiles', 101 | bbox: rectangularBbox, 102 | tileMatrices: tileMatrices256, 103 | imageWidth: 244, 104 | imageHeight: 293, 105 | expectedResult: { 106 | nativeWidth: 247, 107 | nativeHeight: 296.6024590163934, 108 | tiles: [ 109 | { x: 136, y: 94, z: 8, imageOffsetX: -99, imageOffsetY: -158 }, 110 | { x: 136, y: 95, z: 8, imageOffsetX: -99, imageOffsetY: 98 }, 111 | { x: 137, y: 94, z: 8, imageOffsetX: 157, imageOffsetY: -158 }, 112 | { x: 137, y: 95, z: 8, imageOffsetX: 157, imageOffsetY: 98 }, 113 | ], 114 | }, 115 | }; 116 | 117 | const cases = [ 118 | [singleTile512Fixture.description, singleTile512Fixture], 119 | [singleTile512Fixture256Tiles.description, singleTile512Fixture256Tiles], 120 | [rectangularBboxFixture.description, rectangularBboxFixture], 121 | ]; 122 | 123 | describe('test bboxToXyzGrid', () => { 124 | test.each(cases)( 125 | '%s', 126 | (testDescription: string, { bbox, tileMatrices, imageWidth, imageHeight, expectedResult }: any) => { 127 | const result = bboxToXyzGrid(bbox, imageWidth, imageHeight, tileMatrices); 128 | expect(result).toEqual(expectedResult); 129 | }, 130 | ); 131 | }); 132 | -------------------------------------------------------------------------------- /src/mapDataManipulation/__tests__/effectFunctions.ts: -------------------------------------------------------------------------------- 1 | import { runColorEffectFunction, runCustomEffectFunction } from '../effectFunctions'; 2 | 3 | test('apply color effect - linear interpolation 0 (border values)', () => { 4 | const originalPixel = [0.2, 0.8, 0.8, 1]; 5 | const expectedPixel = [0, 1, 1, 1]; 6 | const effects = { 7 | redRange: { from: 0.2, to: 0.8 }, 8 | greenRange: { from: 0.2, to: 0.8 }, 9 | blueRange: { from: 0.2, to: 0.8 }, 10 | }; 11 | const actualPixel = runColorEffectFunction(originalPixel, effects); 12 | expect(actualPixel).toEqual(expectedPixel); 13 | }); 14 | 15 | test('apply color effect - linear interpolation 1 (outside values)', () => { 16 | const originalPixel = [0.05, 0.61, 0.7, 1]; 17 | const expectedPixel = [0, 1, 1, 1]; 18 | const effects = { 19 | redRange: { from: 0.1, to: 0.6 }, 20 | greenRange: { from: 0.1, to: 0.6 }, 21 | blueRange: { from: 0.1, to: 0.6 }, 22 | }; 23 | const actualPixel = runColorEffectFunction(originalPixel, effects); 24 | expect(actualPixel).toEqual(expectedPixel); 25 | }); 26 | 27 | test('apply color effect - linear interpolation 2 (inside values)', () => { 28 | const originalPixel = [0.2, 0.4, 0.5, 1]; 29 | const expectedPixel = [0.2, 0.6, 0.8, 1]; 30 | const effects = { 31 | redRange: { from: 0.1, to: 0.6 }, 32 | greenRange: { from: 0.1, to: 0.6 }, 33 | blueRange: { from: 0.1, to: 0.6 }, 34 | }; 35 | const actualPixel = runColorEffectFunction(originalPixel, effects); 36 | expect(actualPixel).toHaveLength(4); 37 | expect(actualPixel[0]).toBeCloseTo(expectedPixel[0]); 38 | expect(actualPixel[1]).toBeCloseTo(expectedPixel[1]); 39 | expect(actualPixel[2]).toBeCloseTo(expectedPixel[2]); 40 | expect(actualPixel[3]).toEqual(expectedPixel[3]); 41 | }); 42 | 43 | test('custom effect - sum lower than 0.6, all are 0 ', () => { 44 | const originalPixel = [0.3, 0.1, 0.0, 1]; 45 | const expectedPixel = [0, 0, 0, 1]; 46 | const effects = { 47 | customEffect: ({ r, g, b, a }: { r: number; g: number; b: number; a: number }) => ({ 48 | r: r + g + b < 0.6 ? 0 : 1, 49 | g: r + g + b < 0.6 ? 0 : 1, 50 | b: r + g + b < 0.6 ? 0 : 1, 51 | a: a, 52 | }), 53 | }; 54 | const actualPixel = runCustomEffectFunction(originalPixel, effects); 55 | expect(actualPixel).toEqual(expectedPixel); 56 | }); 57 | 58 | test('custom effect - sum higher than 0.6, all are 1', () => { 59 | const originalPixel = [0.3, 0.1, 0.8, 1]; 60 | const expectedPixel = [1, 1, 1, 1]; 61 | const effects = { 62 | customEffect: ({ r, g, b, a }: { r: number; g: number; b: number; a: number }) => ({ 63 | r: r + g + b < 0.6 ? 0 : 1, 64 | g: r + g + b < 0.6 ? 0 : 1, 65 | b: r + g + b < 0.6 ? 0 : 1, 66 | a: a, 67 | }), 68 | }; 69 | const actualPixel = runCustomEffectFunction(originalPixel, effects); 70 | expect(actualPixel).toEqual(expectedPixel); 71 | }); 72 | 73 | test('custom effect - missing argument', () => { 74 | const originalPixel = [1, 1, 1, 1]; 75 | const effects = { 76 | customEffect: ({ r, g, b }: { r: number; g: number; b: number }) => ({ 77 | r: r, 78 | g: g, 79 | b: b, 80 | }), 81 | }; 82 | 83 | expect(() => runCustomEffectFunction(originalPixel, effects)).toThrow( 84 | 'Custom effect function must return an object with properties r, g, b, a.', 85 | ); 86 | }); 87 | -------------------------------------------------------------------------------- /src/mapDataManipulation/const.ts: -------------------------------------------------------------------------------- 1 | export type ColorRange = { 2 | from: number; 3 | to: number; 4 | }; 5 | 6 | export type Effects = { 7 | gain?: number; 8 | gamma?: number; 9 | redRange?: ColorRange; 10 | greenRange?: ColorRange; 11 | blueRange?: ColorRange; 12 | customEffect?: Function; 13 | }; 14 | -------------------------------------------------------------------------------- /src/mapDataManipulation/effectFunctions.ts: -------------------------------------------------------------------------------- 1 | import { Effects } from './const'; 2 | import { isEffectSet, transformValueToRange } from './mapDataManipulationUtils'; 3 | 4 | export function runGainEffectFunction(rgbaArray: number[], effects: Effects): number[] { 5 | // change the values according to the algorithm (gain) 6 | const minValue = 0.0; 7 | const maxValue = 1.0; 8 | const gain = isEffectSet(effects.gain) ? effects.gain : 1.0; 9 | const factor = gain / (maxValue - minValue); 10 | let offset = 0.0; 11 | offset = offset - factor * minValue; 12 | 13 | if (gain === 1.0) { 14 | return rgbaArray; 15 | } 16 | 17 | const transformValueWithGain = (x: number): number => Math.max(0.0, x * factor + offset); 18 | for (let i = 0; i < rgbaArray.length; i += 4) { 19 | rgbaArray[i] = transformValueWithGain(rgbaArray[i]); 20 | rgbaArray[i + 1] = transformValueWithGain(rgbaArray[i + 1]); 21 | rgbaArray[i + 2] = transformValueWithGain(rgbaArray[i + 2]); 22 | } 23 | 24 | return rgbaArray; 25 | } 26 | 27 | export function runGammaEffectFunction(rgbaArray: number[], effects: Effects): number[] { 28 | // change the values according to the algorithm (gamma) 29 | const gamma = isEffectSet(effects.gamma) ? effects.gamma : 1.0; 30 | 31 | if (gamma === 1.0) { 32 | return rgbaArray; 33 | } 34 | 35 | const transformValueWithGamma = (x: number): number => Math.pow(x, gamma); 36 | for (let i = 0; i < rgbaArray.length; i += 4) { 37 | rgbaArray[i] = transformValueWithGamma(rgbaArray[i]); 38 | rgbaArray[i + 1] = transformValueWithGamma(rgbaArray[i + 1]); 39 | rgbaArray[i + 2] = transformValueWithGamma(rgbaArray[i + 2]); 40 | } 41 | 42 | return rgbaArray; 43 | } 44 | 45 | export function runColorEffectFunction(rgbaArray: number[], effects: Effects): number[] { 46 | for (let i = 0; i < rgbaArray.length; i += 4) { 47 | const red = rgbaArray[i]; 48 | const green = rgbaArray[i + 1]; 49 | const blue = rgbaArray[i + 2]; 50 | 51 | if (isEffectSet(effects.redRange)) { 52 | rgbaArray[i] = transformValueToRange(red, effects.redRange.from, effects.redRange.to, 0, 1); 53 | } 54 | 55 | if (isEffectSet(effects.greenRange)) { 56 | rgbaArray[i + 1] = transformValueToRange(green, effects.greenRange.from, effects.greenRange.to, 0, 1); 57 | } 58 | 59 | if (isEffectSet(effects.blueRange)) { 60 | rgbaArray[i + 2] = transformValueToRange(blue, effects.blueRange.from, effects.blueRange.to, 0, 1); 61 | } 62 | } 63 | 64 | return rgbaArray; 65 | } 66 | 67 | export function runCustomEffectFunction(rgbaArray: number[], effects: Effects): number[] { 68 | if (!isEffectSet(effects.customEffect)) { 69 | return rgbaArray; 70 | } 71 | 72 | for (let i = 0; i < rgbaArray.length; i += 4) { 73 | const red = rgbaArray[i]; 74 | const green = rgbaArray[i + 1]; 75 | const blue = rgbaArray[i + 2]; 76 | const alpha = rgbaArray[i + 3]; 77 | 78 | const { r, g, b, a } = effects.customEffect({ r: red, g: green, b: blue, a: alpha }); 79 | 80 | if (r === undefined || g === undefined || b === undefined || a === undefined) { 81 | throw new Error('Custom effect function must return an object with properties r, g, b, a.'); 82 | } 83 | 84 | rgbaArray[i] = r; 85 | rgbaArray[i + 1] = g; 86 | rgbaArray[i + 2] = b; 87 | rgbaArray[i + 3] = a; 88 | } 89 | 90 | return rgbaArray; 91 | } 92 | -------------------------------------------------------------------------------- /src/mapDataManipulation/mapDataManipulationUtils.ts: -------------------------------------------------------------------------------- 1 | import { Effects, ColorRange } from './const'; 2 | 3 | // from one range to another 4 | // f(x) = c + ((d - c) / (b - a)) * (x - a) 5 | // a = oldMin, b = oldMax; c = newMin, d = newMax 6 | // [0,255] to [0,1]: a = 0, b = 255; c = 0, d = 1 7 | // [0,1] to [0,255]: a = 0, b = 1; c = 0, d = 255 8 | 9 | export function transformValueToRange( 10 | x: number, 11 | oldMin: number, 12 | oldMax: number, 13 | newMin: number, 14 | newMax: number, 15 | ): number { 16 | let newX = newMin + ((newMax - newMin) / (oldMax - oldMin)) * (x - oldMin); 17 | newX = Math.max(newX, newMin); 18 | newX = Math.min(newX, newMax); 19 | return newX; 20 | } 21 | 22 | export function isEffectSet(effect: number | ColorRange | Function): boolean { 23 | return effect !== undefined && effect !== null; 24 | } 25 | 26 | export function isAnyEffectSet(effects: Effects): boolean { 27 | return Object.values(effects).some((e) => isEffectSet(e)); 28 | } 29 | -------------------------------------------------------------------------------- /src/mapDataManipulation/runEffectFunctions.ts: -------------------------------------------------------------------------------- 1 | import { getImageProperties, getBlob } from '../utils/canvas'; 2 | import { Effects } from './const'; 3 | import { isAnyEffectSet, transformValueToRange } from './mapDataManipulationUtils'; 4 | import { 5 | runGainEffectFunction, 6 | runGammaEffectFunction, 7 | runColorEffectFunction, 8 | runCustomEffectFunction, 9 | } from './effectFunctions'; 10 | 11 | // The algorithm works with numbers between 0 and 1, so we must: 12 | // - change the range of the values from [0, 255] to [0, 1] 13 | // - change the values according to the algorithms (gain; gamma; r,g,b effects; custom effect) 14 | // - change the range of the values from [0, 1] back to [0, 255] 15 | 16 | export async function runEffectFunctions(originalBlob: Blob, effects: Effects): Promise { 17 | if (!isAnyEffectSet(effects)) { 18 | return originalBlob; 19 | } 20 | 21 | const { rgba, width, height, format } = await getImageProperties(originalBlob); 22 | 23 | // change the range of the values from [0, 255] to [0, 1] 24 | let rgbaArray = new Array(rgba.length); 25 | for (let i = 0; i < rgba.length; i++) { 26 | rgbaArray[i] = transformValueToRange(rgba[i], 0, 255, 0, 1); 27 | } 28 | 29 | // change the values according to the algorithm (gain) 30 | rgbaArray = runGainEffectFunction(rgbaArray, effects); 31 | 32 | // change the values according to the algorithm (gamma) 33 | rgbaArray = runGammaEffectFunction(rgbaArray, effects); 34 | 35 | // change the values according to the algorithm (r,g,b effects) 36 | rgbaArray = runColorEffectFunction(rgbaArray, effects); 37 | 38 | // run custom effect function (with custom range of values) 39 | rgbaArray = runCustomEffectFunction(rgbaArray, effects); 40 | 41 | // change the range of the values from [0, 1] back to [0, 255] 42 | const newImgData = new Uint8ClampedArray(rgbaArray.length); 43 | for (let i = 0; i < rgbaArray.length; i++) { 44 | newImgData[i] = transformValueToRange(rgbaArray[i], 0, 1, 0, 255); 45 | } 46 | 47 | const newBlob = await getBlob({ rgba: newImgData, width, height, format }); 48 | return newBlob; 49 | } 50 | -------------------------------------------------------------------------------- /src/statistics/Fis.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from 'axios'; 2 | import moment from 'moment'; 3 | import WKT from 'terraformer-wkt-parser'; 4 | import { CRS_EPSG4326, CRS_WGS84, findCrsFromUrn } from '../crs'; 5 | import type { AbstractLayer } from '../layer/AbstractLayer'; 6 | import { AbstractSentinelHubV1OrV2Layer } from '../layer/AbstractSentinelHubV1OrV2Layer'; 7 | import type { AbstractSentinelHubV3Layer } from '../layer/AbstractSentinelHubV3Layer'; 8 | import { FisPayload, FisResponse, GetStatsParams, HistogramType } from '../layer/const'; 9 | import { CACHE_CONFIG_NOCACHE } from '../utils/cacheHandlers'; 10 | import { getAxiosReqParams, RequestConfiguration } from '../utils/cancelRequests'; 11 | import type { StatisticsProvider } from './StatisticsProvider'; 12 | 13 | export class Fis implements StatisticsProvider { 14 | private createFISPayload( 15 | layer: AbstractSentinelHubV3Layer | AbstractSentinelHubV1OrV2Layer, 16 | params: GetStatsParams, 17 | ): FisPayload { 18 | if (!params.geometry) { 19 | throw new Error('Parameter "geometry" needs to be provided'); 20 | } 21 | 22 | if (!params.resolution) { 23 | throw new Error('Parameter "resolution" needs to be provided'); 24 | } 25 | 26 | if (!params.fromTime || !params.toTime) { 27 | throw new Error('Parameters "fromTime" and "toTime" need to be provided'); 28 | } 29 | 30 | const payload: FisPayload = { 31 | layer: layer.getLayerId(), 32 | crs: params.crs ? params.crs.authId : CRS_EPSG4326.authId, 33 | geometry: WKT.convert(params.geometry), 34 | time: `${moment.utc(params.fromTime).format('YYYY-MM-DDTHH:mm:ss') + 'Z'}/${ 35 | moment.utc(params.toTime).format('YYYY-MM-DDTHH:mm:ss') + 'Z' 36 | }`, 37 | resolution: undefined, 38 | bins: params.bins || 5, 39 | type: HistogramType.EQUALFREQUENCY, 40 | ...layer.getStatsAdditionalParameters(), 41 | }; 42 | 43 | if (params.geometry.crs) { 44 | const selectedCrs = findCrsFromUrn(params.geometry.crs.properties.name); 45 | payload.crs = selectedCrs.authId; 46 | } 47 | 48 | // When using CRS=EPSG:4326 or CRS_WGS84 one has to add the "m" suffix to enforce resolution in meters per pixel 49 | if (payload.crs === CRS_EPSG4326.authId || payload.crs === CRS_WGS84.authId) { 50 | payload.resolution = params.resolution + 'm'; 51 | } else { 52 | payload.resolution = params.resolution; 53 | } 54 | if (layer.getEvalscript()) { 55 | if (typeof window !== 'undefined' && window.btoa) { 56 | payload.evalscript = btoa(layer.getEvalscript()); 57 | } else { 58 | payload.evalscript = Buffer.from(layer.getEvalscript(), 'utf8').toString('base64'); 59 | } 60 | if ('getEvalsource' in layer && layer.getEvalsource instanceof Function) { 61 | payload.evalsource = layer.getEvalsource(); 62 | } 63 | } 64 | return payload; 65 | } 66 | 67 | private convertFISResponse(data: any): FisResponse { 68 | // convert date strings to Date objects 69 | for (let channel in data) { 70 | data[channel] = data[channel].map((dailyStats: any) => ({ 71 | ...dailyStats, 72 | date: new Date(dailyStats.date), 73 | })); 74 | } 75 | return data; 76 | } 77 | 78 | public async getStats( 79 | layer: AbstractLayer, 80 | _params: GetStatsParams, 81 | reqConfig?: RequestConfiguration, 82 | ): Promise { 83 | return layer.getStats(layer, reqConfig); 84 | } 85 | 86 | public async handleV3( 87 | layer: AbstractSentinelHubV3Layer, 88 | params: GetStatsParams, 89 | reqConfig?: RequestConfiguration, 90 | ): Promise { 91 | const payload: FisPayload = this.createFISPayload(layer, params); 92 | 93 | const axiosReqConfig: AxiosRequestConfig = { 94 | ...getAxiosReqParams(reqConfig, CACHE_CONFIG_NOCACHE), 95 | }; 96 | const shServiceHostname = layer.getShServiceHostname(); 97 | const { data } = await axios.post( 98 | shServiceHostname + 'ogc/fis/' + layer.getInstanceId(), 99 | payload, 100 | axiosReqConfig, 101 | ); 102 | 103 | return this.convertFISResponse(data); 104 | } 105 | 106 | public async handleV1orV2( 107 | layer: AbstractSentinelHubV1OrV2Layer, 108 | params: GetStatsParams, 109 | reqConfig?: RequestConfiguration, 110 | ): Promise { 111 | const payload: FisPayload = this.createFISPayload(layer, params); 112 | 113 | const { data } = await axios.get(layer.dataset.shServiceHostname + 'v1/fis/' + layer.getInstanceId(), { 114 | params: payload, 115 | ...getAxiosReqParams(reqConfig, CACHE_CONFIG_NOCACHE), 116 | }); 117 | 118 | return this.convertFISResponse(data); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/statistics/StatisticalApi.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from 'axios'; 2 | import { RequestConfiguration } from '..'; 3 | import { getAuthToken } from '../auth'; 4 | import { AbstractSentinelHubV3Layer } from '../layer/AbstractSentinelHubV3Layer'; 5 | import { GetStatsParams } from '../layer/const'; 6 | import { CACHE_CONFIG_30MIN } from '../utils/cacheHandlers'; 7 | import { getAxiosReqParams } from '../utils/cancelRequests'; 8 | import { StatisticsUtils } from './statistics.utils'; 9 | import { StatisticsProvider } from './StatisticsProvider'; 10 | import { StatisticalApiPayload, StatisticalApiResponse } from './const'; 11 | 12 | const STATS_DEFAULT_OUTPUT = 'default'; 13 | 14 | export class StatisticalApi implements StatisticsProvider { 15 | public async getStats( 16 | layer: AbstractSentinelHubV3Layer, 17 | params: GetStatsParams, 18 | reqConfig?: RequestConfiguration, 19 | ): Promise { 20 | const authToken = reqConfig && reqConfig.authToken ? reqConfig.authToken : getAuthToken(); 21 | if (!authToken) { 22 | throw new Error('Must be authenticated to use Statistical API'); 23 | } 24 | 25 | await layer.updateLayerFromServiceIfNeeded(reqConfig); 26 | 27 | const input = await StatisticsUtils.createInputPayload(layer, params, reqConfig); 28 | const aggregation = StatisticsUtils.createAggregationPayload(layer, { 29 | ...params, 30 | aggregationInterval: 'P1D', 31 | }); 32 | const calculations = StatisticsUtils.createCalculationsPayload( 33 | layer, 34 | params, 35 | params?.output || STATS_DEFAULT_OUTPUT, 36 | ); 37 | 38 | const payload: StatisticalApiPayload = { 39 | input: input, 40 | aggregation: aggregation, 41 | calculations: calculations, 42 | }; 43 | 44 | const data = this.getStatistics(`${layer.getShServiceHostname()}api/v1/statistics`, payload, reqConfig); 45 | 46 | return data; 47 | } 48 | 49 | public async getStatistics( 50 | shServiceHostname: string, 51 | payload: StatisticalApiPayload, 52 | reqConfig?: RequestConfiguration, 53 | ): Promise { 54 | const authToken = reqConfig && reqConfig.authToken ? reqConfig.authToken : getAuthToken(); 55 | if (!authToken) { 56 | throw new Error('Must be authenticated to use Statistical API'); 57 | } 58 | 59 | const requestConfig: AxiosRequestConfig = { 60 | headers: { 61 | Authorization: 'Bearer ' + authToken, 62 | 'Content-Type': 'application/json', 63 | Accept: '*/*', 64 | }, 65 | ...getAxiosReqParams(reqConfig, CACHE_CONFIG_30MIN), 66 | }; 67 | const { data } = await axios.post(shServiceHostname, payload, requestConfig); 68 | return data; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/statistics/StatisticsProvider.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLayer } from '../layer/AbstractLayer'; 2 | import { GetStatsParams, Stats } from '../layer/const'; 3 | import { RequestConfiguration } from '../utils/cancelRequests'; 4 | import { StatisticsProviderType } from './const'; 5 | import { Fis } from './Fis'; 6 | import { StatisticalApi } from './StatisticalApi'; 7 | 8 | export interface StatisticsProvider { 9 | getStats(layer: AbstractLayer, params: GetStatsParams, reqConfig?: RequestConfiguration): Promise; 10 | } 11 | 12 | export function getStatisticsProvider(statsProvider: StatisticsProviderType): StatisticsProvider { 13 | switch (statsProvider) { 14 | case StatisticsProviderType.STAPI: 15 | return new StatisticalApi(); 16 | case StatisticsProviderType.FIS: 17 | return new Fis(); 18 | default: 19 | throw new Error(`Unknows statistics provider ${statsProvider}`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/statistics/const.ts: -------------------------------------------------------------------------------- 1 | import { Polygon, BBox as BBoxTurf, MultiPolygon } from '@turf/helpers'; 2 | 3 | import { ProcessingPayloadDatasource } from '../layer/processing'; 4 | 5 | export type StatisticalApiInputPayload = { 6 | bounds: { 7 | bbox?: BBoxTurf; 8 | geometry?: Polygon | MultiPolygon; 9 | properties: { 10 | crs: string; 11 | }; 12 | }; 13 | data: ProcessingPayloadDatasource[]; 14 | }; 15 | 16 | export type StatisticalApiAggregationPayload = { 17 | timeRange: { 18 | from: string; 19 | to: string; 20 | }; 21 | aggregationInterval: { 22 | of: string; 23 | }; 24 | width?: number; 25 | height?: number; 26 | resx?: number; 27 | resy?: number; 28 | evalscript: string; 29 | }; 30 | 31 | export type StatisticalApiOutput = { 32 | histograms?: any; 33 | statistics?: any; 34 | }; 35 | 36 | export type StatisticalApiCalculationsPayload = { 37 | [output: string]: StatisticalApiOutput; 38 | }; 39 | 40 | export type StatisticalApiPayload = { 41 | input: StatisticalApiInputPayload; 42 | aggregation: StatisticalApiAggregationPayload; 43 | calculations: StatisticalApiCalculationsPayload; 44 | }; 45 | 46 | type StatisticalApiResponseError = 'BAD_REQUEST' | 'EXECUTION_ERROR' | 'TIMEOUT'; 47 | 48 | type StatisticalApiResponseInterval = { 49 | from: string; 50 | to: string; 51 | }; 52 | 53 | export type BandHistogram = { 54 | overflow: number; 55 | underflow: number; 56 | bins: { 57 | lowEdge: number; 58 | mean: number; 59 | count: number; 60 | }[]; 61 | }; 62 | 63 | export type BandStats = { 64 | min: number; 65 | max: number; 66 | mean: number; 67 | stDev: number; 68 | sampleCount: number; 69 | noDataCount: number; 70 | percentiles?: { 71 | [percentile: string]: number; 72 | }; 73 | }; 74 | 75 | type BandsType = { 76 | [band: string]: { 77 | stats: BandStats; 78 | histogram?: BandHistogram; 79 | }; 80 | }; 81 | 82 | export type StatisticalApiResponse = { 83 | interval: StatisticalApiResponseInterval; 84 | outputs: { 85 | [output: string]: { 86 | bands: BandsType; 87 | }; 88 | }; 89 | 90 | error?: StatisticalApiResponseError; 91 | }[]; 92 | 93 | export enum StatisticsProviderType { 94 | FIS = 'FIS', 95 | STAPI = 'STAPI', 96 | } 97 | -------------------------------------------------------------------------------- /src/statistics/index.ts: -------------------------------------------------------------------------------- 1 | export { getStatisticsProvider } from './StatisticsProvider'; 2 | export { 3 | BandHistogram, 4 | BandStats, 5 | StatisticalApiAggregationPayload, 6 | StatisticalApiCalculationsPayload, 7 | StatisticalApiInputPayload, 8 | StatisticalApiOutput, 9 | StatisticalApiPayload, 10 | StatisticalApiResponse, 11 | StatisticsProviderType, 12 | } from './const'; 13 | export { StatisticsUtils } from './statistics.utils'; 14 | -------------------------------------------------------------------------------- /src/statistics/statistics.utils.ts: -------------------------------------------------------------------------------- 1 | import { DailyChannelStats, Stats } from '../layer/const'; 2 | import { 3 | StatisticalApiInputPayload, 4 | StatisticalApiAggregationPayload, 5 | StatisticalApiCalculationsPayload, 6 | StatisticalApiOutput, 7 | StatisticalApiResponse, 8 | } from './const'; 9 | 10 | import { AbstractSentinelHubV3Layer } from '../layer/AbstractSentinelHubV3Layer'; 11 | import { RequestConfiguration } from '../utils/cancelRequests'; 12 | import { createProcessingPayload } from '../layer/processing'; 13 | 14 | function convertToFISResponse(data: StatisticalApiResponse, defaultOutput: string = 'default'): Stats { 15 | //array of stats objects (interval+outputs) 16 | const statisticsPerBand = new Map(); 17 | 18 | for (let statObject of data) { 19 | const date = new Date(statObject.interval.from); 20 | const { outputs } = statObject; 21 | const outputId = 22 | Object.keys(outputs).find((output) => output === defaultOutput) || Object.keys(outputs)[0]; 23 | const outputData = outputs[outputId]; 24 | const { bands } = outputData; 25 | 26 | Object.keys(bands).forEach((band) => { 27 | const { stats } = bands[band]; 28 | 29 | const dailyStats: DailyChannelStats = { 30 | date: date, 31 | basicStats: stats, 32 | }; 33 | // statistical api doesn't support equal frequency histograms so we try to 34 | // create them from percentiles 35 | if (!!stats.percentiles) { 36 | const lowEdges = Object.keys(stats.percentiles).sort((a, b) => parseFloat(a) - parseFloat(b)); 37 | const bins = [stats.min, ...lowEdges.map((lowEdge: any) => stats.percentiles[lowEdge])].map( 38 | (value) => ({ 39 | lowEdge: value, 40 | mean: null, 41 | count: null, 42 | }), 43 | ); 44 | 45 | dailyStats.histogram = { 46 | bins: bins, 47 | }; 48 | } 49 | 50 | //remove percentiles from basic stats 51 | delete stats.percentiles; 52 | 53 | let dcs: DailyChannelStats[] = []; 54 | if (statisticsPerBand.has(band)) { 55 | dcs = statisticsPerBand.get(band); 56 | } 57 | dcs.push(dailyStats); 58 | statisticsPerBand.set(band, dcs); 59 | }); 60 | } 61 | 62 | const result: Stats = {}; 63 | 64 | for (let band of statisticsPerBand.keys()) { 65 | const bandStats = statisticsPerBand.get(band); 66 | //bands in FIS response are 67 | // - prefixed with C 68 | // - sorted descending 69 | result[band.replace('B', 'C')] = bandStats.sort((a, b) => b.date.valueOf() - a.date.valueOf()); 70 | } 71 | 72 | return result; 73 | } 74 | 75 | async function createInputPayload( 76 | layer: AbstractSentinelHubV3Layer, 77 | params: any, 78 | reqConfig: RequestConfiguration, 79 | ): Promise { 80 | const processingPayload = createProcessingPayload( 81 | layer.dataset, 82 | { ...params }, 83 | layer.getEvalscript(), 84 | layer.getDataProduct(), 85 | layer.mosaickingOrder, 86 | layer.upsampling, 87 | layer.downsampling, 88 | ); 89 | const updatedProcessingPayload = await layer._updateProcessingGetMapPayload( 90 | processingPayload, 91 | 0, 92 | reqConfig, 93 | ); 94 | const { input } = updatedProcessingPayload; 95 | return input; 96 | } 97 | 98 | function createAggregationPayload( 99 | layer: AbstractSentinelHubV3Layer, 100 | params: any, 101 | ): StatisticalApiAggregationPayload { 102 | if (!params.fromTime) { 103 | throw new Error('fromTime must be defined'); 104 | } 105 | 106 | if (!params.toTime) { 107 | throw new Error('toTime must be defined'); 108 | } 109 | 110 | if (!params.aggregationInterval) { 111 | throw new Error('aggregationInterval must be defined'); 112 | } 113 | const resX = params.resolution; 114 | const resY = params.resolution; 115 | 116 | const payload: StatisticalApiAggregationPayload = { 117 | timeRange: { 118 | from: params.fromTime.toISOString(), 119 | to: params.toTime.toISOString(), 120 | }, 121 | aggregationInterval: { 122 | of: params.aggregationInterval, 123 | }, 124 | resx: resX, 125 | resy: resY, 126 | evalscript: layer.getEvalscript(), 127 | }; 128 | 129 | return payload; 130 | } 131 | 132 | function createCalculationsPayload( 133 | layer: AbstractSentinelHubV3Layer, 134 | params: any, 135 | output: string = 'default', 136 | ): StatisticalApiCalculationsPayload { 137 | //calculate percentiles for selected output 138 | 139 | const statisticsForAllBands: StatisticalApiOutput = { 140 | statistics: { 141 | //If it is "default", the statistics specified below will be calculated for all bands of this output 142 | default: { 143 | percentiles: { 144 | k: Array.from({ length: params.bins - 1 }, (_, i) => ((i + 1) * 100) / params.bins), 145 | }, 146 | }, 147 | }, 148 | }; 149 | 150 | return { 151 | [output]: statisticsForAllBands, 152 | }; 153 | } 154 | 155 | export const StatisticsUtils = { 156 | convertToFISResponse: convertToFISResponse, 157 | createInputPayload: createInputPayload, 158 | createAggregationPayload: createAggregationPayload, 159 | createCalculationsPayload: createCalculationsPayload, 160 | }; 161 | -------------------------------------------------------------------------------- /src/utils/cancelRequests.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | CancelTokenSource, 3 | AxiosRequestConfig, 4 | CancelToken as CancelTokenAxios, 5 | ResponseType, 6 | } from 'axios'; 7 | import { CacheConfig, removeCacheableRequestsInProgress } from './cacheHandlers'; 8 | import { getDefaultRequestsConfig } from './defaultReqsConfig'; 9 | 10 | export type RequestConfiguration = { 11 | authToken?: string | null; 12 | retries?: number; 13 | timeout?: number | null; 14 | cancelToken?: CancelToken; 15 | cache?: CacheConfig; 16 | responseType?: ResponseType; 17 | rewriteUrlFunc?: (url: string) => string; 18 | }; 19 | 20 | export class CancelToken { 21 | protected token: CancelTokenAxios | null = null; 22 | protected source: CancelTokenSource | null = null; 23 | //list of all request that can be cancelled by token instance 24 | protected cacheKeys: Set = new Set(); 25 | 26 | public constructor() { 27 | this.source = axios.CancelToken.source(); 28 | this.token = this.source.token; 29 | } 30 | 31 | public setCancelTokenCacheKey(cacheKey: string): void { 32 | this.cacheKeys.add(cacheKey); 33 | } 34 | 35 | public cancel(): void { 36 | if (this.cacheKeys.size > 0) { 37 | for (let cacheKey of this.cacheKeys) { 38 | removeCacheableRequestsInProgress(cacheKey); 39 | } 40 | this.cacheKeys.clear(); 41 | } 42 | this.source.cancel(); 43 | } 44 | 45 | public getToken(): CancelTokenAxios { 46 | return this.token; 47 | } 48 | } 49 | 50 | export const isCancelled = (err: Error): boolean => { 51 | return axios.isCancel(err); 52 | }; 53 | 54 | export const getAxiosReqParams = ( 55 | reqConfig: RequestConfiguration, 56 | defaultCache: CacheConfig, 57 | ): AxiosRequestConfig => { 58 | let axiosReqConfig: AxiosRequestConfig = { 59 | cache: defaultCache, 60 | }; 61 | 62 | const reqConfigWithDefault = { 63 | ...getDefaultRequestsConfig(), 64 | ...reqConfig, 65 | }; 66 | 67 | if (reqConfigWithDefault.cancelToken) { 68 | axiosReqConfig.setCancelTokenCacheKey = (cacheKey: string): void => 69 | reqConfigWithDefault.cancelToken.setCancelTokenCacheKey(cacheKey); 70 | axiosReqConfig.cancelToken = reqConfigWithDefault.cancelToken.getToken(); 71 | } 72 | if (reqConfigWithDefault.retries !== null && reqConfigWithDefault.retries !== undefined) { 73 | axiosReqConfig.retries = reqConfigWithDefault.retries; 74 | } 75 | if (reqConfigWithDefault.cache) { 76 | axiosReqConfig.cache = reqConfigWithDefault.cache; 77 | } 78 | if (reqConfigWithDefault.rewriteUrlFunc) { 79 | axiosReqConfig.rewriteUrlFunc = reqConfigWithDefault.rewriteUrlFunc; 80 | } 81 | if (reqConfigWithDefault.responseType) { 82 | axiosReqConfig.responseType = reqConfigWithDefault.responseType; 83 | } 84 | return axiosReqConfig; 85 | }; 86 | -------------------------------------------------------------------------------- /src/utils/canvas.ts: -------------------------------------------------------------------------------- 1 | import { MimeType, ImageProperties } from '../layer/const'; 2 | 3 | export async function drawBlobOnCanvas( 4 | ctx: CanvasRenderingContext2D, 5 | blob: Blob, 6 | dx: number = 0, 7 | dy: number = 0, 8 | dWidth?: number, 9 | dHeight?: number, 10 | ): Promise { 11 | const objectURL = URL.createObjectURL(blob); 12 | try { 13 | // wait until objectUrl is drawn on the image, so you can safely draw img on canvas: 14 | const imgDrawn: HTMLImageElement = await new Promise((resolve, reject) => { 15 | const img = new Image(); 16 | img.onload = () => resolve(img); 17 | img.onerror = reject; 18 | img.src = objectURL; 19 | }); 20 | 21 | const width = dWidth ?? imgDrawn.naturalWidth; 22 | const height = dHeight ?? imgDrawn.naturalHeight; 23 | 24 | ctx.drawImage(imgDrawn, dx, dy, width, height); 25 | } finally { 26 | URL.revokeObjectURL(objectURL); 27 | } 28 | } 29 | 30 | export async function canvasToBlob(canvas: HTMLCanvasElement, mimeFormat: MimeType | string): Promise { 31 | return await new Promise((resolve) => canvas.toBlob(resolve, mimeFormat)); 32 | } 33 | 34 | export async function getImageProperties(originalBlob: Blob): Promise { 35 | let imgObjectUrl: any; 36 | const imgCanvas = document.createElement('canvas'); 37 | try { 38 | const imgCtx = imgCanvas.getContext('2d'); 39 | imgObjectUrl = URL.createObjectURL(originalBlob); 40 | const img: any = await new Promise((resolve, reject) => { 41 | const img = new Image(); 42 | img.onload = () => resolve(img); 43 | img.onerror = reject; 44 | img.src = imgObjectUrl; 45 | }); 46 | imgCanvas.width = img.width; 47 | imgCanvas.height = img.height; 48 | imgCtx.drawImage(img, 0, 0); 49 | const imgData = imgCtx.getImageData(0, 0, img.width, img.height).data; 50 | 51 | const stringToMimeType = (str: any): MimeType => str; 52 | const format = stringToMimeType(originalBlob.type); 53 | 54 | return { rgba: imgData, width: img.width, height: img.height, format: format }; 55 | } catch (e) { 56 | console.error(e); 57 | throw new Error(e); 58 | } finally { 59 | imgCanvas.remove(); 60 | if (imgObjectUrl) { 61 | URL.revokeObjectURL(imgObjectUrl); 62 | } 63 | } 64 | } 65 | 66 | export async function getBlob(imageProperties: ImageProperties): Promise { 67 | const { rgba, width, height, format } = imageProperties; 68 | const imgCanvas = document.createElement('canvas'); 69 | try { 70 | imgCanvas.width = width; 71 | imgCanvas.height = height; 72 | const imgCtx = imgCanvas.getContext('2d'); 73 | const newImg = new ImageData(rgba, width, height); 74 | imgCtx.putImageData(newImg, 0, 0); 75 | const blob: Blob = await canvasToBlob(imgCanvas, format); 76 | return blob; 77 | } catch (e) { 78 | console.error(e); 79 | throw new Error(e); 80 | } finally { 81 | imgCanvas.remove(); 82 | } 83 | } 84 | 85 | export async function validateCanvasDimensions(canvas: HTMLCanvasElement): Promise { 86 | // If the canvas exceeds the size limit for the browser, canvas.toBlob returns null. 87 | const blob = await new Promise((resolve) => canvas.toBlob(resolve)); 88 | if (blob === null) { 89 | return false; 90 | } 91 | return true; 92 | } 93 | 94 | export async function scaleCanvasImage( 95 | canvas: HTMLCanvasElement, 96 | width: number, 97 | height: number, 98 | ): Promise { 99 | const newSizeCanvas = document.createElement('canvas'); 100 | newSizeCanvas.width = width; 101 | newSizeCanvas.height = height; 102 | const newSizeCtx = newSizeCanvas.getContext('2d'); 103 | newSizeCtx.imageSmoothingEnabled = false; 104 | newSizeCtx.drawImage(canvas, 0, 0, width, height); 105 | return newSizeCanvas; 106 | } 107 | -------------------------------------------------------------------------------- /src/utils/debug.ts: -------------------------------------------------------------------------------- 1 | let debugEnabled = false; 2 | 3 | export function setDebugEnabled(value: boolean): void { 4 | debugEnabled = value; 5 | } 6 | 7 | export function isDebugEnabled(): boolean { 8 | return debugEnabled; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/defaultReqsConfig.ts: -------------------------------------------------------------------------------- 1 | import { RequestConfiguration } from './cancelRequests'; 2 | 3 | let defaultRequestsConfig = {}; 4 | 5 | export const setDefaultRequestsConfig = (reqConfig: RequestConfiguration): void => { 6 | defaultRequestsConfig = reqConfig; 7 | }; 8 | 9 | export const getDefaultRequestsConfig = (): RequestConfiguration => { 10 | return defaultRequestsConfig; 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/ensureTimeout.ts: -------------------------------------------------------------------------------- 1 | import { CancelToken, RequestConfiguration } from './cancelRequests'; 2 | 3 | // a wrapper function that ensures network requests triggered by the inner function get cancelled after the specified timeout 4 | // the wrapper function will receive a single argument - a RequestConfiguration object that should be used to trigger all axios network requests 5 | export const ensureTimeout = async ( 6 | innerFunction: (requestConfig: RequestConfiguration) => Promise, 7 | reqConfig?: RequestConfiguration, 8 | ): Promise => { 9 | if (!reqConfig || !reqConfig.timeout) { 10 | // if timeout was not specified, call the passed function with the original config 11 | return await innerFunction(reqConfig); 12 | } 13 | 14 | const innerReqConfig: RequestConfiguration = { 15 | ...reqConfig, 16 | cancelToken: reqConfig.cancelToken ? reqConfig.cancelToken : new CancelToken(), 17 | // delete the timeout in case innerFunction has a nested ensureTimeout in order to prevent unnecessary setTimeout calls 18 | timeout: undefined, 19 | }; 20 | 21 | const timer = setTimeout(() => { 22 | innerReqConfig.cancelToken.cancel(); 23 | }, reqConfig.timeout); 24 | 25 | try { 26 | const resolvedValue = await innerFunction(innerReqConfig); 27 | clearTimeout(timer); 28 | return resolvedValue; 29 | } catch (e) { 30 | clearTimeout(timer); 31 | throw e; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/replaceHostnames.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const replaceHostnames: Record = {}; 4 | 5 | export function registerHostnameReplacing(fromHostname: string, toHostname: string): void { 6 | if (Object.keys(replaceHostnames).length === 0) { 7 | // the first time we are called we must also register an axios interceptor: 8 | axios.interceptors.request.use(replaceHostnamesInterceptor, (error) => Promise.reject(error)); 9 | } 10 | 11 | replaceHostnames[fromHostname] = toHostname; 12 | } 13 | 14 | const replaceHostnamesInterceptor = async (config: any): Promise => { 15 | const originalUrl = new URL(config.url); 16 | if (replaceHostnames[originalUrl.hostname] === undefined) { 17 | return config; 18 | } 19 | 20 | originalUrl.hostname = replaceHostnames[originalUrl.hostname]; 21 | config.url = originalUrl.toString(); 22 | return config; 23 | }; 24 | -------------------------------------------------------------------------------- /stories/legacyGetMapWmsUrlFromParams.stories.js: -------------------------------------------------------------------------------- 1 | import { legacyGetMapWmsUrlFromParams } from '../dist/sentinelHub.esm'; 2 | 3 | if (!process.env.INSTANCE_ID) { 4 | throw new Error('INSTANCE_ID environment variable is not defined!'); 5 | } 6 | 7 | if (!process.env.S2L2A_LAYER_ID) { 8 | throw new Error('S2L2A_LAYER_ID environment variable is not defined!'); 9 | } 10 | 11 | const instanceId = process.env.INSTANCE_ID; 12 | const s2l2aLayerId = process.env.S2L2A_LAYER_ID; 13 | const baseUrl = `https://services.sentinel-hub.com/ogc/wms/${instanceId}`; 14 | 15 | export default { 16 | title: 'legacyGetMapWmsUrlFromParams', 17 | }; 18 | 19 | const madridBboxAsArrayEPSG3857 = [ 20 | -430493.3433021127, 21 | 4931105.568733289, 22 | -410925.4640611076, 23 | 4950673.447974297, 24 | ]; 25 | const maxCC = 0; 26 | const gain = 2; 27 | const gamma = 2; 28 | const timeString = '2019-10-01/2020-04-23'; 29 | 30 | const basicParamsObject = { 31 | maxcc: maxCC, 32 | layers: s2l2aLayerId, 33 | time: timeString, 34 | showlogo: false, 35 | width: 512, 36 | height: 512, 37 | bbox: madridBboxAsArrayEPSG3857, 38 | format: 'image/jpeg', 39 | crs: 'EPSG:3857', 40 | preview: 2, 41 | bgcolor: '000000', 42 | }; 43 | // EOBrowser example: 44 | // https://apps.sentinel-hub.com/eo-browser/?lat=40.5486&lng=-3.7824&zoom=12&time=2020-02-23&preset=1_TRUE_COLOR&gainOverride=1&gammaOverride=1&redRangeOverride=[0,1]&greenRangeOverride=[0,1]&blueRangeOverride=[0,1]&datasource=Sentinel-2%20L2A 45 | 46 | const paramsObjectWithGain = { ...basicParamsObject, gain: gain }; 47 | // EOBrowser example for GAIN: 48 | // https://apps.sentinel-hub.com/eo-browser/?lat=40.5486&lng=-3.7824&zoom=12&time=2020-02-23&preset=1_TRUE_COLOR&gainOverride=2.0&gammaOverride=1&redRangeOverride=[0,1]&greenRangeOverride=[0,1]&blueRangeOverride=[0,1]&datasource=Sentinel-2%20L2A 49 | 50 | const paramsObjectWithGamma = { ...basicParamsObject, gamma: gamma }; 51 | // EOBrowser example for GAMMA: 52 | // https://apps.sentinel-hub.com/eo-browser/?lat=40.5486&lng=-3.7824&zoom=12&time=2020-02-23&preset=1_TRUE_COLOR&gainOverride=1&gammaOverride=2&redRangeOverride=[0,1]&greenRangeOverride=[0,1]&blueRangeOverride=[0,1]&datasource=Sentinel-2%20L2A 53 | 54 | const paramsObjectWithGainAndGamma = { ...basicParamsObject, gain: gain, gamma: gamma }; 55 | // EOBrowser example for GAIN AND GAMMA: 56 | // https://apps.sentinel-hub.com/eo-browser/?lat=40.5486&lng=-3.7824&zoom=12&time=2020-02-23&preset=1_TRUE_COLOR&gainOverride=2.0&gammaOverride=2&redRangeOverride=[0,1]&greenRangeOverride=[0,1]&blueRangeOverride=[0,1]&datasource=Sentinel-2%20L2A 57 | 58 | export const legacyGetMapWmsUrlFromBasicParams = () => { 59 | const img = document.createElement('img'); 60 | img.width = '512'; 61 | img.height = '512'; 62 | 63 | const wrapperEl = document.createElement('div'); 64 | wrapperEl.innerHTML = '

legacyGetMapWmsUrlFromParams

'; 65 | wrapperEl.innerHTML += 66 | '

Equivalent in EOBrowser

'; 67 | wrapperEl.insertAdjacentElement('beforeend', img); 68 | 69 | const perform = async () => { 70 | const imageUrl = legacyGetMapWmsUrlFromParams(baseUrl, basicParamsObject); 71 | img.src = imageUrl; 72 | }; 73 | perform().then(() => {}); 74 | 75 | return wrapperEl; 76 | }; 77 | 78 | export const legacyGetMapWmsUrlFromParamsGainGamma = () => { 79 | const imgNoGainGamma = document.createElement('img'); 80 | imgNoGainGamma.width = '256'; 81 | imgNoGainGamma.height = '256'; 82 | 83 | const imgGainIs2 = document.createElement('img'); 84 | imgGainIs2.width = '256'; 85 | imgGainIs2.height = '256'; 86 | 87 | const imgGammaIs2 = document.createElement('img'); 88 | imgGammaIs2.width = '256'; 89 | imgGammaIs2.height = '256'; 90 | 91 | const imgGainGammaAre2 = document.createElement('img'); 92 | imgGainGammaAre2.width = '256'; 93 | imgGainGammaAre2.height = '256'; 94 | 95 | const wrapperEl = document.createElement('div'); 96 | wrapperEl.innerHTML = '

WMS LegacyGetMapFromUrl

'; 97 | wrapperEl.innerHTML += "

[DOESN'T SUPPORT GAIN/GAMMA so an error should be shown]

"; 98 | wrapperEl.innerHTML += '

no gain/gamma | gain | gamma | gain and gamma

'; 99 | wrapperEl.insertAdjacentElement('beforeend', imgNoGainGamma); 100 | wrapperEl.insertAdjacentElement('beforeend', imgGainIs2); 101 | wrapperEl.insertAdjacentElement('beforeend', imgGammaIs2); 102 | wrapperEl.insertAdjacentElement('beforeend', imgGainGammaAre2); 103 | 104 | const perform = async () => { 105 | try { 106 | const imageUrlNoGainGamma = legacyGetMapWmsUrlFromParams(baseUrl, basicParamsObject); 107 | imgNoGainGamma.src = imageUrlNoGainGamma; 108 | 109 | const imageUrlGainIs2 = legacyGetMapWmsUrlFromParams(baseUrl, paramsObjectWithGain); 110 | imgGainIs2.src = imageUrlGainIs2; 111 | 112 | const imageUrlGammaIs2 = legacyGetMapWmsUrlFromParams(baseUrl, paramsObjectWithGamma); 113 | imgGammaIs2.src = imageUrlGammaIs2; 114 | 115 | const imageUrlGainGamaAre2 = legacyGetMapWmsUrlFromParams(baseUrl, paramsObjectWithGainAndGamma); 116 | imgGainGammaAre2.src = imageUrlGainGamaAre2; 117 | } catch (err) { 118 | wrapperEl.innerHTML += '
ERROR OCCURED: ' + err + '
'; 119 | } 120 | }; 121 | perform().then(() => {}); 122 | 123 | return wrapperEl; 124 | }; 125 | -------------------------------------------------------------------------------- /stories/storiesUtils.js: -------------------------------------------------------------------------------- 1 | import { 2 | setAuthToken, 3 | isAuthTokenSet, 4 | requestAuthToken, 5 | setDebugEnabled, 6 | MimeTypes, 7 | ApiType, 8 | } from '../dist/sentinelHub.esm'; 9 | 10 | // in storybooks, always display curl commands in console: 11 | setDebugEnabled(true); 12 | 13 | export function renderTilesList(containerEl, list) { 14 | list.forEach(tile => { 15 | const ul = document.createElement('ul'); 16 | containerEl.appendChild(ul); 17 | for (let key in tile) { 18 | const li = document.createElement('li'); 19 | ul.appendChild(li); 20 | let text; 21 | if (tile[key] instanceof Object) { 22 | text = JSON.stringify(tile[key]); 23 | } else { 24 | text = tile[key]; 25 | } 26 | li.innerHTML = `${key} : ${text}`; 27 | } 28 | }); 29 | } 30 | 31 | export async function setAuthTokenWithOAuthCredentials() { 32 | if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) { 33 | throw new Error( 34 | "Please set OAuth Client's id and secret for Processing API(CLIENT_ID, CLIENT_SECRET env vars)", 35 | ); 36 | } 37 | 38 | if (isAuthTokenSet()) { 39 | console.log('Auth token is already set.'); 40 | return; 41 | } 42 | const clientId = process.env.CLIENT_ID; 43 | const clientSecret = process.env.CLIENT_SECRET; 44 | const authToken = await requestAuthToken(clientId, clientSecret); 45 | setAuthToken(authToken); 46 | console.log('Auth token retrieved and set successfully'); 47 | } 48 | 49 | export const createFindDatesUTCStory = (layer, bbox4326, fromTime, toTime, useAuth) => { 50 | const wrapperEl = document.createElement('div'); 51 | wrapperEl.innerHTML = 52 | `

findDatesUTC using ${useAuth ? 'catalog' : 'search index'}

` + 53 | 'from: ' + 54 | fromTime + 55 | '
' + 56 | 'to: ' + 57 | toTime; 58 | 59 | const containerEl = document.createElement('pre'); 60 | wrapperEl.insertAdjacentElement('beforeend', containerEl); 61 | 62 | const img = document.createElement('img'); 63 | img.width = '512'; 64 | img.height = '512'; 65 | wrapperEl.insertAdjacentElement('beforeend', img); 66 | 67 | const perform = async () => { 68 | if (useAuth) { 69 | await setAuthTokenWithOAuthCredentials(); 70 | } else { 71 | setAuthToken(null); 72 | } 73 | const dates = await layer.findDatesUTC(bbox4326, fromTime, toTime); 74 | 75 | containerEl.innerHTML = JSON.stringify(dates, null, true); 76 | 77 | const resDateStartOfDay = new Date(new Date(dates[0]).setUTCHours(0, 0, 0, 0)); 78 | const resDateEndOfDay = new Date(new Date(dates[0]).setUTCHours(23, 59, 59, 999)); 79 | 80 | // prepare an image to show that the number makes sense: 81 | const getMapParams = { 82 | bbox: bbox4326, 83 | fromTime: resDateStartOfDay, 84 | toTime: resDateEndOfDay, 85 | width: 512, 86 | height: 512, 87 | format: MimeTypes.JPEG, 88 | }; 89 | const imageBlob = await layer.getMap(getMapParams, ApiType.WMS); 90 | img.src = URL.createObjectURL(imageBlob); 91 | }; 92 | perform().then(() => {}); 93 | 94 | return wrapperEl; 95 | }; 96 | -------------------------------------------------------------------------------- /stories/wmts.planet.stories.js: -------------------------------------------------------------------------------- 1 | import { 2 | CRS_EPSG3857, 3 | BBox, 4 | MimeTypes, 5 | ApiType, 6 | LayersFactory, 7 | CRS_EPSG4326, 8 | S2L1CLayer, 9 | PlanetNicfiLayer, 10 | } from '../dist/sentinelHub.esm'; 11 | 12 | if (!process.env.PLANET_API_KEY) { 13 | throw new Error('Please set the API Key for PLANET (PLANET_API_KEY env vars)'); 14 | } 15 | const instanceId = process.env.INSTANCE_ID; 16 | const s2LayerId = process.env.S2L1C_LAYER_ID; 17 | 18 | const baseUrl = `https://api.planet.com/basemaps/v1/mosaics/wmts?api_key=${process.env.PLANET_API_KEY}`; 19 | const layerId = 'planet_medres_normalized_analytic_2017-06_2017-11_mosaic'; 20 | 21 | const bbox = new BBox( 22 | CRS_EPSG3857, 23 | 1289034.0450012125, 24 | 1188748.6638910607, 25 | 1291480.029906338, 26 | 1191194.6487961877, 27 | ); 28 | 29 | const notExactTileBbox = new BBox( 30 | CRS_EPSG4326, 31 | 9.546533077955246, 32 | -2.7491153895984772, 33 | 9.940667599439623, 34 | -2.3553726144954044, 35 | ); 36 | 37 | export default { 38 | title: 'WMTS - Planet', 39 | }; 40 | 41 | export const getMapBbox = () => { 42 | const img = document.createElement('img'); 43 | img.width = '256'; 44 | img.height = '256'; 45 | 46 | const wrapperEl = document.createElement('div'); 47 | wrapperEl.innerHTML = '

GetMap with bbox(WMTS)

'; 48 | wrapperEl.insertAdjacentElement('beforeend', img); 49 | const perform = async () => { 50 | const layer = new PlanetNicfiLayer({ baseUrl, layerId }); 51 | 52 | const getMapParams = { 53 | bbox: bbox, 54 | fromTime: new Date(Date.UTC(2018, 11 - 1, 22, 0, 0, 0)), 55 | toTime: new Date(Date.UTC(2018, 12 - 1, 22, 23, 59, 59)), 56 | width: 256, 57 | height: 256, 58 | format: MimeTypes.JPEG, 59 | }; 60 | const imageBlob = await layer.getMap(getMapParams, ApiType.WMTS); 61 | img.src = URL.createObjectURL(imageBlob); 62 | }; 63 | perform().then(() => {}); 64 | return wrapperEl; 65 | }; 66 | 67 | export const getMapBboxStitched = () => { 68 | const img = document.createElement('img'); 69 | img.width = '512'; 70 | img.height = '512'; 71 | 72 | const s2Img = document.createElement('img'); 73 | s2Img.width = '512'; 74 | s2Img.height = '512'; 75 | 76 | const wrapperEl = document.createElement('div'); 77 | wrapperEl.innerHTML = '

GetMap with bbox(WMTS)

'; 78 | wrapperEl.insertAdjacentElement('beforeend', img); 79 | wrapperEl.insertAdjacentElement('beforeend', s2Img); 80 | const perform = async () => { 81 | const layer = new PlanetNicfiLayer({ baseUrl, layerId }); 82 | const layerS2L1C = new S2L1CLayer({ instanceId, layerId: s2LayerId }); 83 | 84 | const getMapParams = { 85 | bbox: notExactTileBbox, 86 | fromTime: new Date(Date.UTC(2018, 11 - 1, 22, 0, 0, 0)), 87 | toTime: new Date(Date.UTC(2018, 12 - 1, 22, 23, 59, 59)), 88 | width: 512, 89 | height: 512, 90 | format: MimeTypes.JPEG, 91 | }; 92 | const imageBlob = await layer.getMap(getMapParams, ApiType.WMTS); 93 | img.src = URL.createObjectURL(imageBlob); 94 | 95 | const getMapParamsS2 = { 96 | bbox: notExactTileBbox, 97 | fromTime: new Date(Date.UTC(2019, 4 - 1, 14, 0, 0, 0)), 98 | toTime: new Date(Date.UTC(2019, 4 - 1, 17, 23, 59, 59)), 99 | width: 512, 100 | height: 512, 101 | format: MimeTypes.JPEG, 102 | showlogo: false, 103 | }; 104 | const s2ImageBlob = await layerS2L1C.getMap(getMapParamsS2, ApiType.WMS); 105 | s2Img.src = URL.createObjectURL(s2ImageBlob); 106 | }; 107 | perform().then(() => {}); 108 | return wrapperEl; 109 | }; 110 | 111 | export const getMap = () => { 112 | const img = document.createElement('img'); 113 | img.width = '256'; 114 | img.height = '256'; 115 | 116 | const wrapperEl = document.createElement('div'); 117 | wrapperEl.innerHTML = '

GetMap (WMTS)

'; 118 | wrapperEl.insertAdjacentElement('beforeend', img); 119 | const perform = async () => { 120 | const layer = new PlanetNicfiLayer({ baseUrl, layerId }); 121 | 122 | const getMapParams = { 123 | tileCoord: { 124 | x: 8852, 125 | y: 9247, 126 | z: 14, 127 | }, 128 | fromTime: new Date(Date.UTC(2018, 11 - 1, 22, 0, 0, 0)), 129 | toTime: new Date(Date.UTC(2018, 12 - 1, 22, 23, 59, 59)), 130 | width: 256, 131 | height: 256, 132 | format: MimeTypes.JPEG, 133 | matrixSet: 'GoogleMapsCompatible15', 134 | }; 135 | const imageBlob = await layer.getMap(getMapParams, ApiType.WMTS); 136 | img.src = URL.createObjectURL(imageBlob); 137 | }; 138 | perform().then(() => {}); 139 | return wrapperEl; 140 | }; 141 | 142 | export const getMapWmtsLayersFactory = () => { 143 | const img = document.createElement('img'); 144 | img.width = '256'; 145 | img.height = '256'; 146 | 147 | const wrapperEl = document.createElement('div'); 148 | wrapperEl.innerHTML = '

GetMap with WMTS

'; 149 | wrapperEl.insertAdjacentElement('beforeend', img); 150 | 151 | const perform = async () => { 152 | const layer = (await LayersFactory.makeLayers(baseUrl, lId => layerId === lId))[0]; 153 | 154 | const getMapParams = { 155 | bbox: bbox, 156 | fromTime: new Date(Date.UTC(2018, 11 - 1, 22, 0, 0, 0)), 157 | toTime: new Date(Date.UTC(2018, 12 - 1, 22, 23, 59, 59)), 158 | width: 256, 159 | height: 256, 160 | format: MimeTypes.JPEG, 161 | }; 162 | const imageBlob = await layer.getMap(getMapParams, ApiType.WMTS); 163 | img.src = URL.createObjectURL(imageBlob); 164 | }; 165 | perform().then(() => {}); 166 | 167 | return wrapperEl; 168 | }; 169 | -------------------------------------------------------------------------------- /stories/wmts.stories.js: -------------------------------------------------------------------------------- 1 | import { WmtsLayer, BBox, MimeTypes, ApiType, CRS_EPSG4326, S2L1CLayer } from '../dist/sentinelHub.esm'; 2 | 3 | const notExactTileBbox = new BBox( 4 | CRS_EPSG4326, 5 | 9.546533077955246, 6 | -2.7491153895984772, 7 | 9.940667599439623, 8 | -2.3553726144954044, 9 | ); 10 | 11 | const rectangularBbox = new BBox( 12 | CRS_EPSG4326, 13 | 21.649502366781235, 14 | -33.649031907049206, 15 | 21.735161393880848, 16 | -33.5284837550155, 17 | ); 18 | 19 | const instanceId = process.env.INSTANCE_ID; 20 | const s2LayerId = process.env.S2L1C_LAYER_ID; 21 | 22 | export default { 23 | title: 'WMTS', 24 | }; 25 | 26 | export const getMapBbox = () => { 27 | const img = document.createElement('img'); 28 | img.width = '512'; 29 | img.height = '512'; 30 | 31 | const s2Img = document.createElement('img'); 32 | s2Img.width = '512'; 33 | s2Img.height = '512'; 34 | 35 | const wrapperEl = document.createElement('div'); 36 | wrapperEl.innerHTML = '

GetMap with bbox(WMTS)

'; 37 | wrapperEl.insertAdjacentElement('beforeend', img); 38 | wrapperEl.insertAdjacentElement('beforeend', s2Img); 39 | const perform = async () => { 40 | const layer = new WmtsLayer({ 41 | baseUrl: `https://services.sentinel-hub.com/ogc/wmts/${instanceId}`, 42 | layerId: s2LayerId, 43 | resourceUrl: `https://services.sentinel-hub.com/ogc/wmts/${instanceId}?REQUEST=GetTile&&showlogo=false&TILEMATRIXSET=PopularWebMercator256&LAYER=${s2LayerId}&TILEMATRIX={TileMatrix}&TILEROW={TileRow}&TILECOL={TileCol}&TIME=2019-04-17/2019-04-17&&format=image%2Fpng`, 44 | matrixSet: 'PopularWebMercator256', 45 | }); 46 | const layerS2L1C = new S2L1CLayer({ instanceId, layerId: s2LayerId }); 47 | 48 | const getMapParams = { 49 | bbox: notExactTileBbox, 50 | fromTime: new Date(Date.UTC(2018, 11 - 1, 22, 0, 0, 0)), 51 | toTime: new Date(Date.UTC(2018, 12 - 1, 22, 23, 59, 59)), 52 | width: 512, 53 | height: 512, 54 | format: MimeTypes.PNG, 55 | }; 56 | const imageBlob = await layer.getMap(getMapParams, ApiType.WMTS); 57 | img.src = URL.createObjectURL(imageBlob); 58 | 59 | const getMapParamsS2 = { 60 | bbox: notExactTileBbox, 61 | fromTime: new Date(Date.UTC(2019, 4 - 1, 17, 0, 0, 0)), 62 | toTime: new Date(Date.UTC(2019, 4 - 1, 17, 23, 59, 59)), 63 | width: 512, 64 | height: 512, 65 | format: MimeTypes.PNG, 66 | showlogo: false, 67 | preview: 2, 68 | }; 69 | const s2ImageBlob = await layerS2L1C.getMap(getMapParamsS2, ApiType.WMS); 70 | s2Img.src = URL.createObjectURL(s2ImageBlob); 71 | }; 72 | perform().then(() => {}); 73 | return wrapperEl; 74 | }; 75 | 76 | export const getMapBboxRectangular = () => { 77 | const width = 474; 78 | const height = 802; 79 | const img = document.createElement('img'); 80 | img.width = width; 81 | img.height = height; 82 | 83 | const s2Img = document.createElement('img'); 84 | s2Img.width = width; 85 | s2Img.height = height; 86 | 87 | const wrapperEl = document.createElement('div'); 88 | wrapperEl.innerHTML = '

GetMap bbox(WMTS) with different width and height

'; 89 | wrapperEl.insertAdjacentElement('beforeend', img); 90 | wrapperEl.insertAdjacentElement('beforeend', s2Img); 91 | const perform = async () => { 92 | const layer = new WmtsLayer({ 93 | baseUrl: `https://services.sentinel-hub.com/ogc/wmts/${instanceId}`, 94 | layerId: s2LayerId, 95 | resourceUrl: `https://services.sentinel-hub.com/ogc/wmts/${instanceId}?REQUEST=GetTile&&showlogo=false&TILEMATRIXSET=PopularWebMercator512&LAYER=${s2LayerId}&TILEMATRIX={TileMatrix}&TILEROW={TileRow}&TILECOL={TileCol}&TIME=2019-04-18/2019-04-18&&format=image%2Fpng`, 96 | matrixSet: 'PopularWebMercator512', 97 | }); 98 | const layerS2L1C = new S2L1CLayer({ instanceId, layerId: s2LayerId }); 99 | 100 | const getMapParams = { 101 | bbox: rectangularBbox, 102 | fromTime: new Date(Date.UTC(2018, 11 - 1, 22, 0, 0, 0)), 103 | toTime: new Date(Date.UTC(2018, 12 - 1, 22, 23, 59, 59)), 104 | width: width, 105 | height: height, 106 | format: MimeTypes.PNG, 107 | }; 108 | const imageBlob = await layer.getMap(getMapParams, ApiType.WMTS); 109 | img.src = URL.createObjectURL(imageBlob); 110 | 111 | const getMapParamsS2 = { 112 | bbox: rectangularBbox, 113 | fromTime: new Date(Date.UTC(2019, 4 - 1, 18, 0, 0, 0)), 114 | toTime: new Date(Date.UTC(2019, 4 - 1, 18, 23, 59, 59)), 115 | width: width, 116 | height: height, 117 | format: MimeTypes.PNG, 118 | showlogo: false, 119 | }; 120 | const s2ImageBlob = await layerS2L1C.getMap(getMapParamsS2, ApiType.WMS); 121 | 122 | s2Img.src = URL.createObjectURL(s2ImageBlob); 123 | }; 124 | perform().then(() => {}); 125 | return wrapperEl; 126 | }; 127 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "./dist", 5 | "module": "es6", 6 | "noImplicitAny": true, 7 | "outDir": "./dist", 8 | "target": "es2016", 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": ["src/**/*"], 14 | } 15 | --------------------------------------------------------------------------------