├── .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 | 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 --------------------------------------------------------------------------------