├── .babelrc
├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── debug
├── index.html
├── src
│ ├── App.vue
│ └── main.js
└── webpack.config.js
├── docs
├── DEPLOYING.md
├── PROVIDERS.md
├── SYMBOLOGY.md
└── ndwi.png
├── landsat-serverless.yaml
├── package.json
├── rollup.config.js
├── serverless.yaml
├── src
├── cog.js
├── index.js
├── providers
│ ├── DeaProvider.js
│ ├── LandsatPdsProvider.js
│ └── index.js
├── routes
│ ├── calculate.js
│ ├── index.js
│ ├── metadata.js
│ └── rgb.js
├── symbology
│ └── index.js
├── tiler.js
└── utils.js
├── test.html
├── test
└── getHarnessFiles.js
└── validTiles.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false
7 | }
8 | ]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "rules": {
4 | "padded-blocks": 0,
5 | "no-redeclare": 0
6 | }
7 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | package-lock.json*
3 | node_modules/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | .serverless/
9 | test/harness/
10 | server.js
11 | test.html
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Rowan Winsemius
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 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## geotiff-server
2 | A node.js server that generate tiles from cloud-optimised geotiff's designed for use with a serverless function provider like AWS Lambda.
3 |
4 | ## The general idea
5 | - A simple slippy-map tile server based on NodeJS and [express.js](https://expressjs.com/)
6 | - Configurable 'providers' to handle interactions with datastores such as the [landsat on AWS](https://landsatonaws.com/)
7 | - An endpoint for rgb tiles allowing different band combinations
8 | - An endpoint for calculated indices such as NDVI
9 | - An endpoint for getting metadata
10 |
11 | ## RGB endpoint
12 |
13 | | Param | Description | Mandatory |
14 | | ------------- |:-------------:| ----------:|
15 | | sceneID | A unique id for a geotiff in a datastore | true |
16 | | provider | A name of a datastore to search | false (defaults to landsat-pds on AWS) |
17 | | rgbBands | Which bands to use as the RGB | false |
18 | | pMin | The lower percentile value to clip values to (can be retrieved via the metadata endpoint) | false |
19 | | pMax | The upper percentile value to clip values to (can be retrieved via the metadata endpoint) | false |
20 |
21 |
22 | #### Example
23 | ````
24 | http://localhost:5000/tiles/1829/1100/11.jpeg?
25 | sceneId=LC80990692014143LGN00
26 |
27 |
28 | // A more complicated example
29 | http://localhost:5000/tiles/{x}/{y}/{z}.jpeg?
30 | sceneId=S2A_OPER_MSI_ARD_TL_EPAE_20190828T022125_A021835_T54KWC_N02.08
31 | &provider=DEA
32 | &pMin=630
33 | &pMax=2646
34 | ````
35 |
36 | ## Calculated endpoint
37 |
38 | | Param | Description | Mandatory |
39 | | ------------- |:-------------:| ----------:|
40 | | sceneID | A unique id for a geotiff in a datastore | true |
41 | | provider | A name of a datastore to search | false (defaults to landsat-pds on AWS) |
42 | | ratio | A band calculation to apply (eg `(b3-b5)/(b3+b5)`) | true |
43 | | style | A style to use for colouring the image (valid options currently are 'NDWI', 'NDVI') | false |
44 |
45 | #### Example
46 | ````
47 | http://localhost:5000/tiles/calculate/1829/1100/11.jpeg?
48 | sceneId=LC80990692014143LGN00
49 | &ratio=(b3-b5)/(b3+b5) <---- although ensure the ratio is urlEncoded
50 | &style=NDWI
51 | ````
52 |
53 |
54 | ## Metadata endpoint
55 |
56 | | Param | Description | Mandatory |
57 | | ------------- |:-------------:| ----------:|
58 | | sceneID | A unique id for a geotiff in a datastore | true |
59 | | provider | A name of a datastore to search | false (defaults to landsat-pds on AWS) |
60 | | bands | A comma seperated lists of bands to retrieve information for | false |
61 |
62 | #### Example
63 | ````
64 | http://localhost:5000/metadata?
65 | sceneId=S2A_OPER_MSI_ARD_TL_EPAE_20190828T022125_A021835_T54KWC_N02.08
66 | &provider=DEA
67 | &bands=b1,b2
68 |
69 | => [
70 | {
71 | "bandName": "b1",
72 | "stats": {
73 | "percentiles": [584,1058],
74 | "min": 327,
75 | "max": 1517,
76 | "stdDeviation": 115.71190214142455
77 | }
78 | },
79 | {
80 | "bandName": "b2",
81 | "stats": {
82 | "percentiles": [630, 1320],
83 | "min": 265,
84 | "max": 3348,
85 | "stdDeviation": 163.80461720104134
86 | }
87 | }
88 | ]
89 |
90 | ````
91 |
92 | ## Why?
93 | - Cloud Optimised GeoTIFFs are awesome and I wanted to learn more about them.
94 |
95 | ## Haven't you heard of rio-tiler/landsat-tiler?
96 | - Yep it's very awesome, it's what inspired this project
97 | - The downsides I ran into were
98 | - it requires Docker (which doesn't work very nicely on Windows)
99 | - It relies on rasterio, which relies on GDAL, and consequently the build was very large (hence why it's using the docker setup to try and streamline some of the build processes)
100 |
101 | ## Roadmap
102 |
103 | - More colour ramps and investigate sending styles to chromajs from the client side
104 | - Investigate symbolising by classes for the `calculate` endpoint (eg -1 to -0.5, -0.5 to 0, 0 to 0.5, 0.5 to 1)
105 | - Implement error handling
106 | - Switch to png's instead of jpgs to allow transparency
107 |
--------------------------------------------------------------------------------
/debug/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | debug
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/debug/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Scene ID
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
66 |
67 |
77 |
--------------------------------------------------------------------------------
/debug/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import iView from 'iview'
4 | import 'iview/dist/styles/iview.css'
5 |
6 | Vue.use(iView)
7 |
8 | new Vue({
9 | el: '#app',
10 | render: h => h(App)
11 | })
12 |
--------------------------------------------------------------------------------
/debug/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const VueLoaderPlugin = require('vue-loader/lib/plugin')
3 |
4 | module.exports = {
5 | entry: './debug/src/main.js',
6 | output: {
7 | path: __dirname,
8 | publicPath: '/debug/',
9 | filename: 'build.js'
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.geojson$/,
15 | loader: 'json-loader'
16 | },
17 | {
18 | test: /\.(png|jpg|gif|woff(2)?|ttf|eot|svg)$/,
19 | use: [
20 | {
21 | loader: 'file-loader',
22 | options: {}
23 | }
24 | ]
25 | },
26 | {
27 | test: /\.css$/,
28 | use: [
29 | 'vue-style-loader',
30 | 'css-loader'
31 | ]
32 | }, {
33 | test: /\.vue$/,
34 | loader: 'vue-loader'
35 | }
36 | ]
37 | },
38 | plugins: [
39 | new VueLoaderPlugin()
40 | ],
41 | devServer: {
42 | historyApiFallback: true,
43 | noInfo: true,
44 | overlay: true,
45 | openPage: 'debug/index.html'
46 | },
47 | performance: {
48 | hints: false
49 | },
50 | devtool: '#eval-source-map'
51 | }
52 |
53 | if (process.env.NODE_ENV === 'production') {
54 | module.exports.devtool = '#source-map'
55 | module.exports.plugins = (module.exports.plugins || []).concat([
56 | new webpack.DefinePlugin({
57 | 'process.env': {
58 | NODE_ENV: '"production"'
59 | }
60 | })
61 | ])
62 | }
63 |
--------------------------------------------------------------------------------
/docs/DEPLOYING.md:
--------------------------------------------------------------------------------
1 | ## How to deploy
2 |
3 | 1. Clone this repo
4 | 2. Configure your providers in `src/providers`
5 | - See PROVIDERS.md
6 | 3. Configure your colour ramps in `src/symbology`
7 | 4. Run `npm run build`
8 | 5. Run `sls deploy` to deploy your tile server using serverless
9 | - this is currently configured to run in AWS Lamdba in US-West-2 as that's next to the landsat public dataset, however if your setting up your own provider and COGs are stored somewhere else in the world you may want to adjust the location of your lambda.
10 | 6. Consume you're tiles
11 | ````
12 | // A simple example as a leaflet tile layer
13 | L.tileLayer('https://abcdsdfds.execute-api.us-west-2.amazonaws.com/production/tiles/calculate/{x}/{y}/{z}.jpg?{params}', {
14 | params: 'sceneId=LC80990692014143LGN00',
15 | maxZoom: 16
16 | }).addTo(mymap)
17 | ````
18 |
--------------------------------------------------------------------------------
/docs/PROVIDERS.md:
--------------------------------------------------------------------------------
1 | ## Providers
2 | A provider is a datastore where COG's are found, for example the landsat public dataset on AWS.
3 |
4 | Geotiff-Server is designed to be flexible to allow you to add additional providers.
5 |
6 | A provider is a basic object that provides a few details about the datasource such as what bands it has, how to find a scene, whether it requires reprojecting etc
7 |
8 | ### Adding a new provider
9 | - To create a new provider copy and paste the `providers/LandsatPDSProvider.js` file and modify it accordingly.
10 | - You'll then also need to add it to the `providerList` in `providers/index.js`
--------------------------------------------------------------------------------
/docs/SYMBOLOGY.md:
--------------------------------------------------------------------------------
1 | ## Symbology
2 | We aim to provide a flexible framework for symbology although we're still in our early days of implementation.
3 |
4 | We're using [chromajs](http://gka.github.io/chroma.js) for
5 |
6 | ### RGB
7 | - The rgb tiles endpoint does not accept a style parameter
8 |
9 | ### Calculate endpoint
10 | Below are details on a number of parameters related to styling.
11 |
12 | #### Style
13 | The `style` param accepts either a string of a prefined style, such as 'NDWI' or 'NDVI'. Alternatively you can set the style parameter to 'custom'. If you do this read the custom section below.
14 |
15 | #### Classes
16 | The `classes` param can be used to specify the number of classes to break your ramp into.
17 | - If you pass a number the scale will broken into equi-distant classes (eg `classes=5`)
18 | - Alternatively you can also define custom class breaks by passing them as array (eg `classes=0,0.3,0.55,0.85,1`)
19 |
20 | ### Custom Styles
21 | - `customColors` an array of colors or a valid chromajs color scale name. Defaults to `spectral`
22 | - `customDomain` an array of values to use as a [domain](http://gka.github.io/chroma.js/#scale-domain) for the chromajs color scale. Defaults to `0,1`
23 | `customClasses` see documenation above re classes
24 |
--------------------------------------------------------------------------------
/docs/ndwi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rowanwins/geotiff-server/fa5744c7d2f73aee99878e953534a1365a189e18/docs/ndwi.png
--------------------------------------------------------------------------------
/landsat-serverless.yaml:
--------------------------------------------------------------------------------
1 | service: geotiff-server
2 |
3 | provider:
4 | name: aws
5 | runtime: nodejs8.10
6 | stage: production
7 | region: us-west-2
8 |
9 | iamRoleStatements:
10 | - Effect: "Allow"
11 | Action:
12 | - "s3:GetObject"
13 | Resource:
14 | - "arn:aws:s3:::landsat-pds/*"
15 |
16 | custom:
17 | apigwBinary:
18 | types:
19 | - '*/*'
20 |
21 | plugins:
22 | - serverless-apigw-binary
23 |
24 | functions:
25 | app:
26 | handler: server.handler
27 | memorySize: 1536
28 | timeout: 20
29 | events:
30 | - http:
31 | path: /{proxy+}
32 | method: get
33 | cors: true
34 |
35 | package:
36 | exclude:
37 | - ".*/**"
38 | include:
39 | - server.js
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "geotiff-server",
3 | "version": "0.0.1",
4 | "description": "A Cloud Optimised Geotiff tile server based on geotiff.js",
5 | "main": "index.js",
6 | "scripts": {
7 | "build:dev": "rollup -c -w",
8 | "build": "cross-env NODE_ENV=production rollup -c",
9 | "debug": "cross-env webpack-dev-server --config debug/webpack.config.js --mode development --open --hot",
10 | "lambda:update": "sls deploy function -f app",
11 | "dev": "run-p build:dev serve",
12 | "getHarness": "node test/getHarnessFiles.js",
13 | "serve": "nodemon server.js",
14 | "start": "node server.js",
15 | "test:serve": "cross-env NODE_ENV=testLocal npm run dev"
16 | },
17 | "author": "",
18 | "license": "ISC",
19 | "dependencies": {
20 | "aws-serverless-express": "^3.2.0",
21 | "babel-polyfill": "^6.26.0",
22 | "chroma-js": "^1.3.7",
23 | "cors": "^2.8.5",
24 | "expr-eval": "^1.2.2",
25 | "express": "^4.16.3",
26 | "express-async-errors": "^3.0.0",
27 | "fast-png": "^4.0.1",
28 | "filepath": "^1.1.0",
29 | "geotiff": "1.0.0-beta.5",
30 | "jpeg-js": "^0.3.4",
31 | "js-yaml": "^3.12.0",
32 | "node-fetch": "^2.1.2",
33 | "serverless-http": "^1.6.0",
34 | "simple-statistics": "^6.1.0",
35 | "tilebelt": "^1.0.1",
36 | "xmlhttprequest": "^1.8.0"
37 | },
38 | "devDependencies": {
39 | "@babel/cli": "^7.5.5",
40 | "@babel/core": "^7.5.5",
41 | "@babel/polyfill": "^7.0.0-beta.54",
42 | "@babel/preset-env": "^7.5.5",
43 | "apidoc": "^0.17.6",
44 | "axios": "^0.19.0",
45 | "babel-loader": "^8.0.6",
46 | "babel-runtime": "^6.26.0",
47 | "body-parser": "^1.19.0",
48 | "cross-env": "^5.2.0",
49 | "css-loader": "^3.2.0",
50 | "eslint": "^5.1.0",
51 | "eslint-config-standard": "^11.0.0",
52 | "eslint-plugin-import": "^2.13.0",
53 | "eslint-plugin-node": "^7.0.1",
54 | "eslint-plugin-promise": "^3.8.0",
55 | "eslint-plugin-standard": "^3.1.0",
56 | "file-loader": "^4.2.0",
57 | "iview": "^3.5.0-rc.1",
58 | "leaflet": "^1.5.1",
59 | "node-wget": "^0.4.2",
60 | "nodemon": "^1.18.3",
61 | "npm-run-all": "^4.1.3",
62 | "rollup": "^0.63.5",
63 | "rollup-plugin-babel": "^4.3.3",
64 | "rollup-plugin-commonjs": "^9.3.4",
65 | "rollup-plugin-json": "^3.0.0",
66 | "rollup-plugin-node-resolve": "^3.4.0",
67 | "serverless-apigw-binary": "^0.4.4",
68 | "style-loader": "^1.0.0",
69 | "ttf-loader": "^1.0.2",
70 | "vue": "^2.6.10",
71 | "vue-loader": "^15.7.1",
72 | "vue-style-loader": "^4.1.2",
73 | "vue-template-compiler": "^2.6.10",
74 | "webpack": "^4.39.3",
75 | "webpack-cli": "^3.3.7",
76 | "webpack-dev-server": "^3.8.0"
77 | },
78 | "engines": {
79 | "node": "8.5.0"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve'
2 | import babel from 'rollup-plugin-babel'
3 | import commonjs from 'rollup-plugin-commonjs'
4 | import json from 'rollup-plugin-json'
5 |
6 | export default {
7 | input: 'src/index.js',
8 | output: {
9 | file: 'server.js',
10 | format: 'cjs',
11 | sourcemap: 'inline'
12 | },
13 | external: ['http', 'fs', 'util', 'querystring', 'string_decoder', 'https', 'url', 'stream', 'events', 'path', 'net', 'buffer', 'tty', 'zlib', 'crypto'],
14 | watch: {
15 | include: 'src/**',
16 | exclude: ['node_modules/**', 'geotiff/**']
17 | },
18 | plugins: [
19 | json(),
20 | resolve({
21 | main: true,
22 | preferBuiltins: true
23 | }),
24 | commonjs({
25 | include: 'node_modules/**'
26 | }),
27 | babel({
28 | exclude: 'node_modules/**',
29 | externalHelpers: false
30 | })
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/serverless.yaml:
--------------------------------------------------------------------------------
1 | service: geotiff-server
2 |
3 | provider:
4 | name: aws
5 | runtime: nodejs8.10
6 | stage: production
7 | region: ap-southeast-2
8 |
9 | iamRoleStatements:
10 | - Effect: "Allow"
11 | Action:
12 | - "s3:GetObject"
13 | Resource:
14 | - "arn:aws:s3:::dea-public-data/*"
15 |
16 | custom:
17 | apigwBinary:
18 | types:
19 | - '*/*'
20 |
21 | plugins:
22 | - serverless-apigw-binary
23 |
24 | package:
25 | individually: true
26 | exclude:
27 | - ./**
28 |
29 | functions:
30 | app:
31 | handler: server.handler
32 | memorySize: 1536
33 | timeout: 20
34 | events:
35 | - http:
36 | path: /{any+}
37 | method: get
38 | cors: true
39 | environment:
40 | NODE_ENV: production
41 | package:
42 | include:
43 | - server.js
--------------------------------------------------------------------------------
/src/cog.js:
--------------------------------------------------------------------------------
1 | import geotiff from 'geotiff'
2 | let path = require('path')
3 |
4 | export async function openTiffImage (band, bbox, provider) {
5 | try {
6 | if (process.env.NODE_ENV === 'testLocal') {
7 | band.tiff = await geotiff.fromFile(path.join(__dirname, 'test', 'harness', path.basename(band.urlPath)))
8 | } else {
9 | band.tiff = await geotiff.fromUrl(band.urlPath)
10 | return band
11 | }
12 | } catch (err) {
13 | throw err
14 | }
15 | }
16 |
17 | export async function getScene (band, bbox) {
18 | try {
19 | const data = await band.tiff.readRasters({
20 | bbox: bbox,
21 | width: 256,
22 | height: 256
23 | })
24 | band.data = data[0]
25 | return band
26 | } catch (err) {
27 | throw err
28 | }
29 | }
30 |
31 | export async function getSceneOverview (band, bbox, provider) {
32 | try {
33 | let tiff = null
34 | if (process.env.NODE_ENV === 'testLocal') {
35 | tiff = await geotiff.fromFile(path.join(__dirname, 'test', 'harness', path.basename(band.urlPath)))
36 | } else {
37 | tiff = await geotiff.fromUrl(band.urlPath)
38 | }
39 | const image = await tiff.getImage()
40 | band.meta = image.getGDALMetadata()
41 | const data = await tiff.readRasters({
42 | bbox: bbox,
43 | width: 1024,
44 | height: 1024
45 | })
46 | band.data = Object.values(data[0])
47 | return band
48 | } catch (err) {
49 | throw err
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 |
3 | import { routes } from './routes'
4 |
5 | // Setup the expressjs server
6 | import express from 'express'
7 | import serverless from 'serverless-http'
8 | import expressAsync from 'express-async-errors' //eslint-disable-line
9 | import cors from 'cors'
10 |
11 | var app = express()
12 | app.use(cors())
13 | app.use('/', routes)
14 |
15 | module.exports.handler = serverless(app, {
16 | binary: ['image/png', 'image/jpeg']
17 | })
18 |
19 | if (process.env.NODE_ENV !== 'production') {
20 | const port = process.env.PORT || 5000
21 | app.listen(port, () => {
22 | console.log(`Listening on ${port}`)
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/providers/DeaProvider.js:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch'
2 | import yaml from 'js-yaml'
3 | import { latLonToUtm } from '../utils'
4 |
5 | export class DeaProvider {
6 | constructor () {
7 | this.name = 'DEA'
8 | this.baseUrl = 'https://data.dea.ga.gov.au/L2/sentinel-2-nrt/'
9 | // this.baseUrl = 'http://dea-public-data.s3-ap-southeast-2.amazonaws.com/L2/sentinel-2-nrt/'
10 | this.naturalColorBands = ['b4', 'b3', 'b2']
11 | this.bands = [
12 | {
13 | shortName: 'b1',
14 | bandNumber: 1,
15 | commonName: 'Ultra Blue (coastal/aerosol)',
16 | resolution: 30,
17 | filePath: 'B01'
18 | },
19 | {
20 | shortName: 'b2',
21 | bandNumber: 2,
22 | commonName: 'Blue',
23 | resolution: 30,
24 | filePath: 'B02'
25 | },
26 | {
27 | shortName: 'b3',
28 | bandNumber: 3,
29 | commonName: 'Green',
30 | resolution: 30,
31 | filePath: 'B03'
32 | },
33 | {
34 | shortName: 'b4',
35 | bandNumber: 4,
36 | commonName: 'Red',
37 | resolution: 30,
38 | filePath: 'B04'
39 | },
40 | {
41 | shortName: 'b5',
42 | bandNumber: 5,
43 | commonName: 'NIR',
44 | resolution: 30,
45 | filePath: 'B05'
46 | },
47 | {
48 | shortName: 'b6',
49 | bandNumber: 6,
50 | commonName: 'SWIR-1',
51 | resolution: 30,
52 | filePath: 'B06'
53 | },
54 | {
55 | shortName: 'b7',
56 | bandNumber: 7,
57 | commonName: 'SWIR-1',
58 | resolution: 30,
59 | filePath: 'B07'
60 | },
61 | {
62 | shortName: 'b8',
63 | bandNumber: 8,
64 | commonName: 'SWIR-1',
65 | resolution: 30,
66 | filePath: 'B08'
67 | },
68 | // {
69 | // shortName: 'b9',
70 | // bandNumber: 9,
71 | // commonName: 'SWIR-1',
72 | // resolution: 30,
73 | // filePath: 'B09'
74 | // },
75 | // {
76 | // shortName: 'b10',
77 | // bandNumber: 10,
78 | // commonName: 'SWIR-1',
79 | // resolution: 30,
80 | // filePath: 'B10'
81 | // },
82 | {
83 | shortName: 'b11',
84 | bandNumber: 11,
85 | commonName: 'SWIR-1',
86 | resolution: 30,
87 | filePath: 'B11'
88 | }
89 | ]
90 | this.requiresReprojecting = true
91 | this.requiresToaCorrection = false
92 | }
93 |
94 | constructImageUrl (sceneId, band) {
95 | // var basepath = 'http://dea-public-data.s3-ap-southeast-2.amazonaws.com/L2/sentinel-2-nrt/S2MSIARD/2018-08-21/S2A_OPER_MSI_ARD_TL_SGS__20180821T013813_A016515_T56KKB_N02.06/LAMBERTIAN/LAMBERTIAN_'
96 | const part = sceneId.split('EPAE_')[1]
97 | const date = part.substring(0, 8)
98 | const year = date.substring(0, 4)
99 | const month = date.substring(4, 6)
100 | const day = date.substring(6, 8)
101 | return `${this.baseUrl}S2MSIARD/${year}-${month}-${day}/${sceneId}/LAMBERTIAN/LAMBERTIAN_${band.filePath}.TIF`
102 | }
103 |
104 | getBandByShortName (shortName) {
105 | for (var i = 0; i < this.bands.length; i++) {
106 | if (this.bands[i].shortName === shortName) return this.bands[i]
107 | }
108 | }
109 |
110 | getRequiredBandsByShortNames (bands, sceneId) {
111 | const requiredBands = []
112 | for (var i = 0; i < bands.length; i++) {
113 | const matchingBand = this.getBandByShortName(bands[i])
114 | matchingBand.urlPath = this.constructImageUrl(sceneId, matchingBand)
115 | requiredBands.push(matchingBand)
116 | }
117 | return requiredBands
118 | }
119 |
120 | getBandUrls (sceneId, bands) {
121 | const urls = []
122 | for (var i = 0; i < bands.length; i++) {
123 | urls.push({
124 | band: bands[i],
125 | urlPath: this.constructImageUrl(sceneId, bands[i])
126 | })
127 | }
128 | return urls
129 | }
130 |
131 | async getMetadata (sceneId) {
132 | const part = sceneId.split('EPAE_')[1]
133 | const date = part.substring(0, 8)
134 | const year = date.substring(0, 4)
135 | const month = date.substring(4, 6)
136 | const day = date.substring(6, 8)
137 | const metaUrl = `${this.baseUrl}S2MSIARD/${year}-${month}-${day}/${sceneId}/ARD-METADATA.yaml`
138 | const res = await fetch(metaUrl, {
139 | timeout: 5000
140 | })
141 | let meta = await res.text()
142 | meta = yaml.safeLoad(meta)
143 | return meta
144 | }
145 |
146 | getBBox () {
147 | const coords = this.metadata.grid_spatial.projection.geo_ref_points
148 | return [coords.ll.x, coords.ll.y, coords.ur.x, coords.ur.y]
149 | }
150 |
151 | getWgsBBox () {
152 | const coords = this.metadata.extent.coord
153 | return [coords.ll.lon, coords.ll.lat, coords.ur.lon, coords.ur.lat]
154 | }
155 |
156 | reprojectBbbox (requestBbox, nativeSR) {
157 | const zoneHemi = nativeSR.split('UTM zone ')[1]
158 | const sceneUtmZone = zoneHemi.substring(0, 2)
159 | const bboxMinUtm = latLonToUtm([requestBbox[0], requestBbox[1]], sceneUtmZone, true)
160 | const bboxMaxUtm = latLonToUtm([requestBbox[2], requestBbox[3]], sceneUtmZone, true)
161 | return [bboxMinUtm[0], bboxMinUtm[1], bboxMaxUtm[0], bboxMaxUtm[1]]
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/src/providers/LandsatPdsProvider.js:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch'
2 | import { latLonToUtm, degreesToRadians } from '../utils'
3 |
4 | export class LandsatPdsProvider {
5 | constructor () {
6 | this.name = 'landsat-pds'
7 | this.baseUrl = 'http://landsat-pds.s3.amazonaws.com/L8'
8 | this.naturalColorBands = ['b4', 'b3', 'b2']
9 | this.bands = [
10 | {
11 | shortName: 'b1',
12 | bandNumber: 1,
13 | commonName: 'Ultra Blue (coastal/aerosol)',
14 | resolution: 30,
15 | filePath: 'B1'
16 | },
17 | {
18 | shortName: 'b2',
19 | bandNumber: 2,
20 | commonName: 'Blue',
21 | resolution: 30,
22 | filePath: 'B2'
23 | },
24 | {
25 | shortName: 'b3',
26 | bandNumber: 3,
27 | commonName: 'Green',
28 | resolution: 30,
29 | filePath: 'B3'
30 | },
31 | {
32 | shortName: 'b4',
33 | bandNumber: 4,
34 | commonName: 'Red',
35 | resolution: 30,
36 | filePath: 'B4'
37 | },
38 | {
39 | shortName: 'b5',
40 | bandNumber: 5,
41 | commonName: 'NIR',
42 | resolution: 30,
43 | filePath: 'B5'
44 | },
45 | {
46 | shortName: 'b6',
47 | bandNumber: 6,
48 | commonName: 'SWIR-1',
49 | resolution: 30,
50 | filePath: 'B6'
51 | }
52 | ]
53 | this.requiresReprojecting = true
54 | this.requiresToaCorrection = true
55 | }
56 |
57 | constructImageUrl (sceneId, band) {
58 | const path = sceneId.substring(3, 6)
59 | const row = sceneId.substring(6, 9)
60 | return `${this.baseUrl}/${path}/${row}/${sceneId}/${sceneId}_${band.filePath}.TIF`
61 | }
62 |
63 | getBandByShortName (shortName) {
64 | for (var i = 0; i < this.bands.length; i++) {
65 | if (this.bands[i].shortName === shortName) return this.bands[i]
66 | }
67 | }
68 |
69 | getRequiredBandsByShortNames (bands, sceneId) {
70 | const requiredBands = []
71 | for (var i = 0; i < bands.length; i++) {
72 | const matchingBand = this.getBandByShortName(bands[i])
73 | matchingBand.urlPath = this.constructImageUrl(sceneId, matchingBand)
74 | requiredBands.push(matchingBand)
75 | }
76 | return requiredBands
77 | }
78 |
79 | getBandUrls (sceneId, bands) {
80 | const urls = []
81 | for (var i = 0; i < bands.length; i++) {
82 | urls.push({
83 | band: bands[i],
84 | url: this.constructImageUrl(sceneId, bands[i])
85 | })
86 | }
87 | return urls
88 | }
89 |
90 | async getMetadata (sceneId) {
91 | const path = sceneId.substring(3, 6)
92 | const row = sceneId.substring(6, 9)
93 | var metaUrl = `${this.baseUrl}/${path}/${row}/${sceneId}/${sceneId}_MTL.json`
94 |
95 | const res = await fetch(metaUrl, {
96 | timeout: 5000
97 | })
98 | const meta = await res.json()
99 | return meta
100 | }
101 |
102 | reprojectBbbox (requestBbox, nativeSR) {
103 | const zoneHemi = nativeSR.split('UTM zone ')[1]
104 | const sceneUtmZone = zoneHemi.substring(0, 2)
105 | const bboxMinUtm = latLonToUtm([requestBbox[0], requestBbox[1]], sceneUtmZone)
106 | const bboxMaxUtm = latLonToUtm([requestBbox[2], requestBbox[3]], sceneUtmZone)
107 | return [bboxMinUtm[0], bboxMinUtm[1], bboxMaxUtm[0], bboxMaxUtm[1]]
108 | }
109 |
110 | performToa (band) {
111 | const sunElevation = this.metadata.L1_METADATA_FILE.IMAGE_ATTRIBUTES.SUN_ELEVATION
112 | const se = Math.sin(degreesToRadians(sunElevation))
113 | const reflectanceRescalingFactor = this.metadata.L1_METADATA_FILE.RADIOMETRIC_RESCALING[`REFLECTANCE_MULT_BAND_${band.bandNumber}`]
114 | const reflectanceAddition = this.metadata.L1_METADATA_FILE.RADIOMETRIC_RESCALING[`REFLECTANCE_ADD_BAND_${band.bandNumber}`]
115 |
116 | const tmp = new Float32Array(band.data.length)
117 | for (var i = 0; i < band.data.length; i++) {
118 | band.data[i] = (((reflectanceRescalingFactor * band.data[i]) + reflectanceAddition) / se) * 100000
119 | tmp[i] = band.data[i] / 65535
120 | band.data[i] = Math.round(tmp[i] * 255)
121 | }
122 |
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/src/providers/index.js:
--------------------------------------------------------------------------------
1 | import { LandsatPdsProvider } from './LandsatPdsProvider'
2 | import { DeaProvider } from './DeaProvider'
3 |
4 | const providerList = [new LandsatPdsProvider(), new DeaProvider()]
5 | export default providerList
6 |
7 | export function getProviderByName (providerName) {
8 | for (var i = 0; i < providerList.length; i++) {
9 | if (providerList[i].name === providerName) {
10 | return providerList[i]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/routes/calculate.js:
--------------------------------------------------------------------------------
1 | import { getProviderByName } from '../providers'
2 | import { createBbox, createSingleBandTile } from '../tiler'
3 | import { findUniqueBandShortNamesInString } from '../utils'
4 | import { getScene, openTiffImage } from '../cog'
5 | import { getRampByName, createCustomRamp } from '../symbology'
6 |
7 | export default async (req, res, next) => {
8 | const sceneId = req.query.sceneId ? req.query.sceneId : null
9 | const ratio = req.query.ratio ? req.query.ratio : null
10 | if (sceneId === null || ratio === null) throw new Error('GeoTiff-Server: You must pass in sceneId and ratio parameters to your query')
11 |
12 | const providerSrc = req.query.provider ? req.query.provider : 'landsat-pds'
13 | const provider = getProviderByName(providerSrc)
14 |
15 | var requestBbox = createBbox(Number(req.params.x), Number(req.params.y), Number(req.params.z))
16 |
17 | const style = req.query.style ? req.query.style : 'NDWI'
18 |
19 | let colorRamp = null
20 | if (style === 'custom') {
21 | colorRamp = createCustomRamp(req.query)
22 | } else {
23 | const classes = req.query.classes ? req.query.classes : null
24 | colorRamp = getRampByName(style, classes)
25 | }
26 | const requiredBandsShortNames = findUniqueBandShortNamesInString(ratio)
27 |
28 | const bandsToUse = provider.getRequiredBandsByShortNames(requiredBandsShortNames, sceneId)
29 |
30 | const getImageCalls = []
31 |
32 | for (var i = 0; i < bandsToUse.length; i++) {
33 | getImageCalls.push(openTiffImage(bandsToUse[i], provider))
34 | }
35 | await Promise.all(getImageCalls)
36 |
37 | const firstImage = await bandsToUse[0].tiff.getImage()
38 | const geotiffProps = firstImage.getGeoKeys()
39 | let imgBbox = null
40 |
41 | if (provider.requiresReprojecting) imgBbox = provider.reprojectBbbox(requestBbox, geotiffProps.GTCitationGeoKey)
42 |
43 | const getDataCalls = []
44 | for (var i = 0; i < bandsToUse.length; i++) {
45 | getDataCalls.push(getScene(bandsToUse[i], imgBbox))
46 | }
47 | await Promise.all(getDataCalls)
48 | const png = createSingleBandTile(bandsToUse, ratio, colorRamp)
49 | var img = Buffer.from(png.data, 'binary')
50 |
51 | res.contentType('image/jpeg')
52 | res.send(img)
53 | }
54 |
--------------------------------------------------------------------------------
/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import rgbTiles from './rgb'
2 | import calculate from './calculate'
3 | import metadata from './metadata'
4 | import express from 'express'
5 |
6 | export const routes = express.Router()
7 |
8 | routes.get('/', (req, res) => { res.status(200).json({ message: 'Connected!' }) })
9 | routes.get('/metadata', metadata)
10 | routes.get('/tiles/:x/:y/:z.jpeg', rgbTiles)
11 | routes.get('/tiles/calculate/:x/:y/:z.jpeg', calculate)
12 |
--------------------------------------------------------------------------------
/src/routes/metadata.js:
--------------------------------------------------------------------------------
1 | import { getProviderByName } from '../providers'
2 | import { getSceneOverview } from '../cog'
3 |
4 | import {min, max, quantile, standardDeviation} from 'simple-statistics'
5 |
6 | export default async (req, res) => {
7 | const sceneId = req.query.sceneId ? req.query.sceneId : null
8 | if (sceneId === null) throw new Error('GeoTiff-Server: You must pass in a sceneId to your query')
9 | const providerSrc = req.query.provider ? req.query.provider : 'landsat-pds'
10 | const provider = getProviderByName(providerSrc)
11 | provider.metadata = await provider.getMetadata(sceneId)
12 |
13 | let imgBbox = provider.getBBox()
14 |
15 | const requiredBandsShortNames = req.query.bands ? req.query.bands.split(',') : provider.bands
16 |
17 | const bands = provider.getRequiredBandsByShortNames(requiredBandsShortNames, sceneId)
18 | const getDataCalls = []
19 |
20 | for (var i = 0; i < bands.length; i++) {
21 | getDataCalls.push(getSceneOverview(bands[i], imgBbox, provider))
22 | }
23 |
24 | const bandSummaries = await Promise.all(getDataCalls)
25 |
26 | const out = {
27 | bbox: imgBbox,
28 | wgsBbox: provider.getWgsBBox(),
29 | bandInformation: []
30 | }
31 |
32 | for (var i = 0; i < bandSummaries.length; i++) {
33 | out.bandInformation.push({
34 | bandName: requiredBandsShortNames[i].shortName,
35 | stats: getBasicStats(bandSummaries[i].data)
36 | })
37 | }
38 | res.json(out)
39 | }
40 |
41 | function getBasicStats (arr) {
42 | var filtered = arr.filter(function (element) {
43 | return element !== -999
44 | })
45 | return {
46 | percentiles: [quantile(filtered, 0.02), quantile(filtered, 0.98)],
47 | min: min(filtered),
48 | max: max(filtered),
49 | stdDeviation: standardDeviation(filtered)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/routes/rgb.js:
--------------------------------------------------------------------------------
1 | import { getProviderByName } from '../providers'
2 | import { createBbox, createRgbTile } from '../tiler'
3 | import { openTiffImage, getScene } from '../cog'
4 |
5 | export default async (req, res) => {
6 | const sceneId = req.query.sceneId ? req.query.sceneId : null
7 | if (sceneId === null) throw new Error('GeoTiff-Server: You must pass in a sceneId to your query')
8 |
9 | const providerSrc = req.query.provider ? req.query.provider : 'landsat-pds'
10 | const pMin = req.query.pMin ? req.query.pMin : 0
11 | const pMax = req.query.pMax ? req.query.pMax : 10000
12 |
13 | const provider = getProviderByName(providerSrc)
14 |
15 | var requestBbox = createBbox(Number(req.params.x), Number(req.params.y), Number(req.params.z))
16 |
17 | const requiredBandsShortNames = req.query.rgbBands ? req.query.rgbBands.split(',') : provider.naturalColorBands
18 |
19 | const bandsToUse = provider.getRequiredBandsByShortNames(requiredBandsShortNames, sceneId)
20 |
21 | const getImageCalls = []
22 |
23 | for (var i = 0; i < bandsToUse.length; i++) {
24 | getImageCalls.push(openTiffImage(bandsToUse[i], provider))
25 | }
26 | await Promise.all(getImageCalls)
27 |
28 | const firstImage = await bandsToUse[0].tiff.getImage()
29 | const geotiffProps = firstImage.getGeoKeys()
30 | let imgBbox = null
31 |
32 | if (provider.requiresReprojecting) imgBbox = provider.reprojectBbbox(requestBbox, geotiffProps.GTCitationGeoKey)
33 |
34 | const getDataCalls = []
35 | for (var i = 0; i < bandsToUse.length; i++) {
36 | getDataCalls.push(getScene(bandsToUse[i], imgBbox))
37 | }
38 | await Promise.all(getDataCalls)
39 |
40 | const png = createRgbTile(bandsToUse[0].data, bandsToUse[1].data, bandsToUse[2].data, [pMin, pMax])
41 | var img = Buffer.from(png.data, 'binary')
42 |
43 | res.contentType('image/jpeg')
44 | res.send(img)
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/symbology/index.js:
--------------------------------------------------------------------------------
1 | import chroma from 'chroma-js'
2 |
3 | export function getRampByName (name, numClasses) {
4 | if (name === 'NDWI') return createNdwi(numClasses)
5 | if (name === 'NDVI') return createNdvi(numClasses)
6 | }
7 |
8 | export function getRgbFromRamp (colorRamp, value) {
9 | if (value < colorRamp.minValue) return colorRamp.minRgb
10 | var rgb = colorRamp.scale(value)
11 | return [rgb._rgb[0], rgb._rgb[1], rgb._rgb[2], 0]
12 | }
13 |
14 | export function createCustomRamp (params) {
15 | const customClasses = params.customClasses ? params.customClasses : null
16 | const customDomain = params.customDomain ? params.customDomain : [0, 1]
17 | const customColors = params.customColors ? params.customColors : 'Spectral'
18 |
19 | if (customClasses === null) return createRegularRamp(0, null, customColors, customDomain)
20 | return createClassRamp(-1, null, customColors, customDomain, customClasses) //eslint-disable-line
21 | }
22 |
23 | function createRegularRamp (minValue, minRgb, colorsArr, domainArr) {
24 | if (minRgb === null) minRgb = [191, 191, 191, 0]
25 | return {
26 | minValue: minValue,
27 | minRgb: minRgb,
28 | scale: chroma.scale(colorsArr).mode('lab').domain(domainArr)
29 | }
30 | }
31 |
32 | function createClassRamp (minValue, minRgb, colorsArr, domainArr, classes) {
33 | classes = classes ? classes : 5 //eslint-disable-line
34 | if (classes.indexOf(',') !== -1) {
35 | classes.split(',')
36 | classes = classes.map(num => {
37 | return parseFloat(num)
38 | })
39 | }
40 | if (minRgb === null) minRgb = [191, 191, 191, 0]
41 | return {
42 | minValue: minValue,
43 | minRgb: minRgb,
44 | scale: chroma.scale(colorsArr).domain(domainArr).classes(classes)
45 | }
46 | }
47 |
48 | function createNdwi (numClasses) {
49 | if (numClasses === null) return createRegularRamp(0, null, ['green', 'white', 'blue'], [-1, 1])
50 | return createClassRamp(-1, null, ['green', 'white', 'blue'], [-1, 1], numClasses)
51 | }
52 |
53 | function createNdvi (numClasses) {
54 | if (numClasses === null) return createRegularRamp(0, null, ['lightyellow', 'darkgreen'], [0, 1])
55 | return createClassRamp(0, null, ['green', 'white', 'blue'], [-1, 1], numClasses)
56 | }
57 |
--------------------------------------------------------------------------------
/src/tiler.js:
--------------------------------------------------------------------------------
1 | import { getRgbFromRamp } from './symbology'
2 | import { rescaleValueTo256 } from './utils'
3 |
4 | import tilebelt from 'tilebelt'
5 | import jpeg from 'jpeg-js'
6 | import expr from 'expr-eval'
7 | const Parser = expr.Parser
8 |
9 | const tileHeight = 256
10 | const tileWidth = 256
11 |
12 | const frameData = Buffer.alloc(tileWidth * tileHeight * 4)
13 |
14 | export function createBbox (x, y, z) {
15 | return tilebelt.tileToBBOX([x, y, z])
16 | }
17 |
18 | export function createRgbTile (rData, gData, bData, percentiles) {
19 | for (let i = 0; i < frameData.length / 4; i++) {
20 | frameData[i * 4] = rescaleValueTo256(rData[i], percentiles)
21 | frameData[(i * 4) + 1] = rescaleValueTo256(gData[i], percentiles)
22 | frameData[(i * 4) + 2] = rescaleValueTo256(bData[i], percentiles)
23 | frameData[(i * 4) + 3] = 0
24 | }
25 | return encodeImageData(frameData)
26 | }
27 |
28 | export function createSingleBandTile (bands, expression, colorRamp) {
29 | var parser = new Parser()
30 | var expr = parser.parse(expression)
31 | for (let i = 0; i < frameData.length / 4; i++) {
32 | const args = {}
33 | for (var ii = 0; ii < bands.length; ii++) {
34 | args[bands[ii].shortName] = bands[ii].data[i]
35 | }
36 |
37 | var calculatedVal = expr.evaluate(args)
38 | const rgb = getRgbFromRamp(colorRamp, calculatedVal)
39 |
40 | frameData[i * 4] = rgb[0]
41 | frameData[(i * 4) + 1] = rgb[1]
42 | frameData[(i * 4) + 2] = rgb[2]
43 | frameData[(i * 4) + 3] = rgb[3]
44 | }
45 |
46 | return encodeImageData(frameData)
47 | }
48 |
49 | function encodeImageData (data) {
50 | return jpeg.encode({
51 | data: data,
52 | width: tileWidth,
53 | height: tileHeight
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | // Taken from https://stackoverflow.com/a/14438954
2 | export function getUniqueValues (arr) {
3 | function onlyUnique (value, index, self) {
4 | return self.indexOf(value) === index
5 | }
6 | return arr.filter(onlyUnique)
7 | }
8 |
9 | export function findUniqueBandShortNamesInString (string) {
10 | var regExpressionTester = /(b)\d+/g
11 | return getUniqueValues(string.match(regExpressionTester))
12 | }
13 |
14 | export function latLonToUtm (coords, zone, requiresSouthernHemiAdjustment) {
15 | const lat = coords[1]
16 | const lon = coords[0]
17 | if (!(-80 <= lat && lat <= 84)) throw new Error('Outside UTM limits') //eslint-disable-line
18 |
19 | const falseEasting = 500e3
20 | const falseNorthing = 10000e3
21 |
22 | var λ0 = degreesToRadians(((zone - 1) * 6 - 180 + 3)) // longitude of central meridian
23 |
24 | // grid zones are 8° tall; 0°N is offset 10 into latitude bands array
25 | var mgrsLatBands = 'CDEFGHJKLMNPQRSTUVWXX' // X is repeated for 80-84°N
26 | var latBand = mgrsLatBands.charAt(Math.floor(lat / 8 + 10))
27 | // adjust zone & central meridian for Norway
28 | if (zone === 31 && latBand === 'V' && lon >= 3) { zone++; degreesToRadians(λ0 += (6)) }
29 | // adjust zone & central meridian for Svalbard
30 | if (zone === 32 && latBand === 'X' && lon < 9) { zone--; degreesToRadians(λ0 -= (6)) }
31 | if (zone === 32 && latBand === 'X' && lon >= 9) { zone++; degreesToRadians(λ0 += (6)) }
32 | if (zone === 34 && latBand === 'X' && lon < 21) { zone--; degreesToRadians(λ0 -= (6)) }
33 | if (zone === 34 && latBand === 'X' && lon >= 21) { zone++; degreesToRadians(λ0 += (6)) }
34 | if (zone === 36 && latBand === 'X' && lon < 33) { zone--; degreesToRadians(λ0 -= (6)) }
35 | if (zone === 36 && latBand === 'X' && lon >= 33) { zone++; degreesToRadians(λ0 += (6)) }
36 |
37 | var φ = degreesToRadians(lat)
38 | var λ = degreesToRadians(lon) - λ0
39 |
40 | const a = 6378137
41 | const f = 1 / 298.257223563
42 | // WGS 84: a = 6378137, b = 6356752.314245, f = 1/298.257223563;
43 |
44 | var k0 = 0.9996
45 |
46 | var e = Math.sqrt(f * (2 - f))
47 | var n = f / (2 - f)
48 | var n2 = n * n
49 | const n3 = n * n2
50 | const n4 = n * n3
51 | const n5 = n * n4
52 | const n6 = n * n5
53 |
54 | const cosλ = Math.cos(λ)
55 | const sinλ = Math.sin(λ)
56 |
57 | var τ = Math.tan(φ)
58 | var σ = Math.sinh(e * Math.atanh(e * τ / Math.sqrt(1 + τ * τ)))
59 |
60 | var τʹ = τ * Math.sqrt(1 + σ * σ) - σ * Math.sqrt(1 + τ * τ)
61 |
62 | var ξʹ = Math.atan2(τʹ, cosλ)
63 | var ηʹ = Math.asinh(sinλ / Math.sqrt(τʹ * τʹ + cosλ * cosλ))
64 |
65 | var A = a / (1 + n) * (1 + 1 / 4 * n2 + 1 / 64 * n4 + 1 / 256 * n6)
66 |
67 | var α = [ null, // note α is one-based array (6th order Krüger expressions)
68 | 1 / 2 * n - 2 / 3 * n2 + 5 / 16 * n3 + 41 / 180 * n4 - 127 / 288 * n5 + 7891 / 37800 * n6,
69 | 13 / 48 * n2 - 3 / 5 * n3 + 557 / 1440 * n4 + 281 / 630 * n5 - 1983433 / 1935360 * n6,
70 | 61 / 240 * n3 - 103 / 140 * n4 + 15061 / 26880 * n5 + 167603 / 181440 * n6,
71 | 9561 / 161280 * n4 - 179 / 168 * n5 + 6601661 / 7257600 * n6,
72 | 34729 / 80640 * n5 - 3418889 / 1995840 * n6,
73 | 212378941 / 319334400 * n6 ]
74 |
75 | var ξ = ξʹ
76 | var j = 1
77 | for (j; j <= 6; j++) ξ += α[j] * Math.sin(2 * j * ξʹ) * Math.cosh(2 * j * ηʹ)
78 |
79 | var η = ηʹ
80 | for (j = 1; j <= 6; j++) η += α[j] * Math.cos(2 * j * ξʹ) * Math.sinh(2 * j * ηʹ)
81 |
82 | var x = k0 * A * η
83 | var y = k0 * A * ξ
84 |
85 | x = x + falseEasting
86 |
87 | if (requiresSouthernHemiAdjustment && y < 0) y = y + falseNorthing
88 |
89 | return [x, y]
90 | };
91 |
92 | export function degreesToRadians (degrees) {
93 | return degrees * Math.PI / 180
94 | }
95 |
96 | export function scaleValueBetweenRange (value, max, min) {
97 | return (value - min) / (max - min)
98 | }
99 |
100 | export function rescaleValueTo256 (oldVal, percentiles) {
101 | if (oldVal < percentiles[0]) return 0
102 | if (oldVal > percentiles[1]) return 255
103 | return (255 - 0) * (oldVal - percentiles[0]) / (percentiles[1] - percentiles[0]) + 0
104 | }
105 |
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Quick Start - Leaflet
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/test/getHarnessFiles.js:
--------------------------------------------------------------------------------
1 | const wget = require('node-wget')
2 |
3 | const bands = ['B2', 'B3', 'B4', 'B5', 'MTL']
4 |
5 | const baseURL = 'http://landsat-pds.s3.amazonaws.com/L8/099/069/LC80990692014143LGN00/LC80990692014143LGN00_'
6 | const file = 'LC80990692014143LGN00_'
7 |
8 | console.log(`Starting image retrieval - this'll take a few mins`)
9 |
10 | bands.map((band) => {
11 | const ext = band[0] === 'B' ? '.TIF' : '.json'
12 | wget({
13 | url: `${baseURL}${band}${ext}`,
14 | dest: __dirname + '/harness/' //eslint-disable-line
15 | },
16 | function (error, response, body) {
17 | if (error) {
18 | console.log(error)
19 | } else {
20 | console.log(`Retrieved ${file}${band}${ext}`)
21 | }
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/validTiles.md:
--------------------------------------------------------------------------------
1 | http://localhost:5000/tiles/58575/35087/16.jpg?sceneId=LC80990692014143LGN00
2 |
3 | // DEA Tile
4 | http://localhost:5000/tiles/457/285/9.jpeg?sceneId=S2A_OPER_MSI_ARD_TL_EPAE_20190828T022125_A021835_T54KWC_N02.08&provider=DEA&percentiles={"b2":[630,1320], "b3":[911,1850], "b4":[1198,2646]}
5 |
6 | http://localhost:5000/tiles/457/285/9.jpeg?sceneId=S2A_OPER_MSI_ARD_TL_EPAE_20190828T022125_A021835_T54KWC_N02.08&provider=DEA&pMin=630&pMax=2646
7 |
8 | http://localhost:5000/tiles/calculate/457/285/9.jpeg?sceneId=S2A_OPER_MSI_ARD_TL_EPAE_20190828T022125_A021835_T54KWC_N02.08&provider=DEA&ratio=%28b3-b5%29%2F%28b3%2Bb5%29&style=NDWI
--------------------------------------------------------------------------------