├── .babelrc ├── .eslintrc.js ├── .github └── workflows │ └── build-deploy.yaml ├── .gitignore ├── LICENSE ├── README.md ├── images ├── EOX_Logo.svg └── EOX_Logo_white.svg ├── index.html ├── package-lock.json ├── package.json ├── src ├── actions │ ├── main.js │ └── scenes │ │ └── index.js ├── app.jsx ├── components │ ├── comps.jsx │ ├── mapview.jsx │ ├── scenes │ │ ├── add.jsx │ │ ├── details.jsx │ │ ├── list.jsx │ │ └── step.jsx │ └── test.jsx ├── index.jsx ├── maputil.js ├── reducers │ ├── main.js │ └── scenes │ │ └── index.js ├── renderutils.js ├── store.js ├── styles.css ├── types.js └── webglrenderer.js └── webpack.conf.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | 6 | "es2015", 7 | // "stage-0" 8 | ], 9 | "plugins": [ 10 | "transform-object-rest-spread", 11 | "react-hot-loader/babel" 12 | ] 13 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': 'airbnb', 3 | 'parser': 'babel-eslint', 4 | 'parserOptions': { 5 | 'sourceType': 'module', 6 | 'allowImportExportEverywhere': false 7 | }, 8 | 'env': { 9 | 'browser': true, 10 | 'worker': true, 11 | }, 12 | 'rules': { 13 | 'no-underscore-dangle': 0, 14 | 'class-methods-use-this': 0, 15 | 'no-plusplus': 0, 16 | 'no-loop-func': 0, 17 | 'no-mixed-operators': [ 18 | 'error', { 19 | 'allowSamePrecedence': true 20 | } 21 | ], 22 | 'no-param-reassign': [ 23 | 'error', { 24 | 'props': false 25 | } 26 | ], 27 | 'no-prototype-builtins': 0, 28 | 'no-restricted-syntax': [ 29 | 'error', 30 | 'LabeledStatement', 31 | 'WithStatement', 32 | ], 33 | 'no-bitwise': 0, 34 | 'import/prefer-default-export': 0, 35 | 'prefer-default-export': 0, 36 | 'func-names': 0, 37 | 'arrow-body-style': 0, 38 | 'function-paren-newline': 0, 39 | 'object-curly-newline': 0, 40 | 'no-await-in-loop': 0, 41 | 'prefer-destructuring': ['error', { 'object': true, 'array': false }], 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /.github/workflows/build-deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Setup node 14 | uses: actions/setup-node@v1 15 | - run: npm install 16 | - run: npm run build 17 | - name: Deploy pages 18 | uses: JamesIves/github-pages-deploy-action@4.0.0 19 | with: 20 | branch: gh-pages 21 | folder: dist/ 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 EOX IT Services GmbH 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COG-Explorer 2 | 3 | This is the repository for the COG-Explorer app. 4 | 5 | ## Setup 6 | 7 | To set everything up run: 8 | 9 | npm install 10 | 11 | ## Building 12 | 13 | To build the application bundle run: 14 | 15 | npm run build 16 | 17 | The bundle is now available in the `dist` directory. 18 | 19 | ## Deployment 20 | 21 | To deploy the app on github pages first commit the built bundle: 22 | 23 | git commit dist/ -m "Updating bundle." 24 | 25 | Now run this command to deploy on github pages: 26 | 27 | npm run deploy 28 | 29 | Note: the `gh-pages` branch must be present and you have to have write access to your remote. -------------------------------------------------------------------------------- /images/EOX_Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /images/EOX_Logo_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | COG-Explorer 10 | 11 | 12 | 77 | 78 | 81 | 82 |
83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "l8app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack-dev-server -d --hot --config webpack.conf.js", 8 | "build": "rm -rf dist/* ; webpack --config webpack.conf.js; cp -R images index.html dist/", 9 | "deploy": "git subtree push --prefix dist/ origin gh-pages" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "geotiff": "^1.0.4", 15 | "ol": "^4.6.5", 16 | "proj4": "^2.4.4", 17 | "react": "^16.3.1", 18 | "react-dom": "^16.3.1", 19 | "react-redux": "^5.0.7", 20 | "redux": "^3.7.2", 21 | "redux-thunk": "^2.2.0", 22 | "url-join": "^4.0.0" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "^6.26.0", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^7.1.4", 28 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 29 | "babel-plugin-transform-react-jsx": "^6.24.1", 30 | "babel-preset-env": "^1.6.1", 31 | "babel-preset-es2015": "^6.24.1", 32 | "babel-preset-react": "^6.24.1", 33 | "css-loader": "^0.28.11", 34 | "escope": "^3.6.0", 35 | "eslint": "^4.18.2", 36 | "eslint-config-airbnb": "^16.1.0", 37 | "eslint-plugin-import": "^2.8.0", 38 | "eslint-plugin-jsx-a11y": "^6.0.3", 39 | "eslint-plugin-react": "^7.7.0", 40 | "react-hot-loader": "^4.1.0", 41 | "style-loader": "^0.20.3", 42 | "webpack": "^4.5.0", 43 | "webpack-cli": "^3.3.4", 44 | "webpack-dev-server": "^3.1.3", 45 | "worker-loader": "^1.1.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/actions/main.js: -------------------------------------------------------------------------------- 1 | import types from '../types'; 2 | 3 | const { START_LOADING, STOP_LOADING, TILE_START_LOADING, TILE_STOP_LOADING, SET_POSITION } = types; 4 | 5 | export function startLoading() { 6 | return { type: START_LOADING }; 7 | } 8 | 9 | export function stopLoading() { 10 | return { type: STOP_LOADING }; 11 | } 12 | 13 | export function tileStartLoading() { 14 | return { type: TILE_START_LOADING }; 15 | } 16 | 17 | export function tileStopLoading() { 18 | return { type: TILE_STOP_LOADING }; 19 | } 20 | 21 | export function setPosition(longitude, latitude, zoom) { 22 | return { type: SET_POSITION, longitude, latitude, zoom }; 23 | } 24 | -------------------------------------------------------------------------------- /src/actions/scenes/index.js: -------------------------------------------------------------------------------- 1 | import urlJoin from 'url-join'; 2 | import { fromUrl } from 'geotiff'; 3 | 4 | import types from '../../types'; 5 | import { startLoading, stopLoading } from '../main'; 6 | 7 | const { 8 | SCENE_ADD, SCENE_REMOVE, SCENE_CHANGE_BANDS, 9 | SCENE_PIPELINE_ADD_STEP, SCENE_PIPELINE_REMOVE_STEP, SCENE_PIPELINE_INDEX_STEP, 10 | SCENE_PIPELINE_EDIT_STEP, SET_ERROR, 11 | } = types; 12 | 13 | 14 | const urlToAttribution = { 15 | 'https://s3-us-west-2.amazonaws.com/planet-disaster-data/hurricane-harvey/SkySat_Freeport_s03_20170831T162740Z3.tif': 'cc-by-sa, downloaded from https://www.planet.com/disaster/hurricane-harvey-2017-08-28/', 16 | }; 17 | 18 | 19 | export function addScene(url, bands, redBand, greenBand, blueBand, isSingle, hasOvr, isRGB, attribution, pipeline = []) { 20 | return { 21 | type: SCENE_ADD, 22 | sceneId: url, 23 | bands, 24 | redBand, 25 | greenBand, 26 | blueBand, 27 | isSingle, 28 | hasOvr, 29 | isRGB, 30 | attribution: attribution || urlToAttribution[url], 31 | pipeline, 32 | }; 33 | } 34 | 35 | export function sceneChangeBands(url, newBands) { 36 | return { 37 | type: SCENE_CHANGE_BANDS, 38 | sceneId: url, 39 | newBands, 40 | }; 41 | } 42 | 43 | export function removeScene(url) { 44 | return { 45 | type: SCENE_REMOVE, 46 | sceneId: url, 47 | }; 48 | } 49 | 50 | export function setError(message = null) { 51 | return { 52 | type: SET_ERROR, 53 | message, 54 | }; 55 | } 56 | 57 | const landsat8Pipeline = [ 58 | { 59 | operation: 'sigmoidal-contrast', 60 | contrast: 50, 61 | bias: 0.16, 62 | }, { 63 | operation: 'gamma', 64 | bands: 'red', 65 | value: 1.03, 66 | }, { 67 | operation: 'gamma', 68 | bands: 'blue', 69 | value: 0.925, 70 | }, 71 | ]; 72 | 73 | export function addSceneFromIndex(url, attribution, pipeline) { 74 | return async (dispatch, getState) => { 75 | const { scenes } = getState(); 76 | 77 | // remove old scenes 78 | for (const scene of scenes) { 79 | dispatch(removeScene(scene.id)); 80 | } 81 | 82 | // clear previous error 83 | dispatch(setError()); 84 | 85 | dispatch(startLoading()); 86 | try { 87 | // find out type of data the URL is pointing to 88 | const headerResponse = await fetch(url, { method: 'HEAD' }); 89 | 90 | if (!headerResponse.ok) { 91 | throw new Error(`Failed to fetch ${url}`); 92 | } 93 | 94 | const contentType = headerResponse.headers.get('content-type'); 95 | 96 | if (contentType.includes('text/html')) { 97 | const relUrl = url.endsWith('/') ? url : url.substring(0, url.lastIndexOf('/')); 98 | const response = await fetch(url, {}); 99 | const content = await response.text(); 100 | const doc = (new DOMParser()).parseFromString(content, 'text/html'); 101 | const files = Array.from(doc.querySelectorAll('a[href]')) 102 | .map(a => a.getAttribute('href')) 103 | .map(file => urlJoin(relUrl, file)); 104 | 105 | let usedPipeline = pipeline; 106 | let red = 0; 107 | let green = 0; 108 | let blue = 0; 109 | let bands; 110 | 111 | if (files.find(file => /LC0?8.*B[0-9]+.TIF$/.test(file))) { 112 | // landsat case 113 | red = 4; 114 | green = 3; 115 | blue = 2; 116 | bands = new Map( 117 | files 118 | .filter(file => /LC0?8.*B[0-9]+.TIF$/.test(file)) 119 | .map(file => [parseInt(/.*B([0-9]+).TIF/.exec(file)[1], 10), file]), 120 | ); 121 | 122 | usedPipeline = usedPipeline || landsat8Pipeline; 123 | } else { 124 | bands = new Map( 125 | files 126 | .filter(file => /.TIFF?$/gi.test(file)) 127 | .map((file, i) => [i, file]), 128 | ); 129 | } 130 | 131 | const hasOvr = typeof files.find(file => /.TIFF?.OVR$/i.test(file)) !== 'undefined'; 132 | dispatch( 133 | addScene(url, bands, red, green, blue, false, hasOvr, false, attribution, usedPipeline) 134 | ); 135 | } else if (contentType.includes('image/tiff')) { 136 | const tiff = await fromUrl(url); 137 | const image = await tiff.getImage(); 138 | 139 | const samples = image.getSamplesPerPixel(); 140 | const bands = new Map(); 141 | for (let i = 0; i < samples; ++i) { 142 | bands.set(i, url); 143 | } 144 | 145 | let [red, green, blue] = []; 146 | if (samples === 3 || typeof image.fileDirectory.PhotometricInterpretation !== 'undefined') { 147 | red = 0; 148 | green = 1; 149 | blue = 2; 150 | } else { 151 | red = 0; 152 | green = 0; 153 | blue = 0; 154 | } 155 | 156 | const isRGB = ( 157 | typeof image.fileDirectory.PhotometricInterpretation !== 'undefined' 158 | && image.getSampleByteSize(0) === 1 159 | ); 160 | 161 | dispatch(addScene(url, bands, red, green, blue, true, false, isRGB, attribution, pipeline)); 162 | } 163 | } catch (error) { 164 | // TODO: set error somewhere to present to user 165 | dispatch(setError(error.toString())); 166 | } finally { 167 | dispatch(stopLoading()); 168 | } 169 | }; 170 | } 171 | 172 | export function addStep(url, operation) { 173 | let values; 174 | switch (operation) { 175 | case 'sigmoidal-contrast': 176 | values = { 177 | contrast: 50, 178 | bias: 0.15, 179 | }; 180 | break; 181 | case 'gamma': 182 | values = { value: 1.0 }; 183 | break; 184 | default: 185 | values = {}; 186 | break; 187 | } 188 | return { 189 | type: SCENE_PIPELINE_ADD_STEP, 190 | sceneId: url, 191 | payload: { 192 | operation, 193 | ...values, 194 | }, 195 | }; 196 | } 197 | 198 | export function editStep(url, index, payload) { 199 | return { 200 | type: SCENE_PIPELINE_EDIT_STEP, 201 | sceneId: url, 202 | index, 203 | payload, 204 | }; 205 | } 206 | 207 | export function indexStep(url, index, newIndex) { 208 | return { 209 | type: SCENE_PIPELINE_INDEX_STEP, 210 | sceneId: url, 211 | index, 212 | newIndex, 213 | }; 214 | } 215 | 216 | export function removeStep(url, index) { 217 | return { 218 | type: SCENE_PIPELINE_REMOVE_STEP, 219 | sceneId: url, 220 | index, 221 | }; 222 | } 223 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { hot } from 'react-hot-loader' 4 | 5 | import AddSceneForm from './components/scenes/add'; 6 | import ListScenes from './components/scenes/list'; 7 | import SceneDetails from './components/scenes/details'; 8 | import MapView from './components/mapview'; 9 | 10 | import { setError } from './actions/scenes'; 11 | 12 | const mapStateToProps = ({ scenes, main }) => ({ scenes, ...main }); 13 | const mapDispatchToProps = { setError }; 14 | 15 | class ConnectedApp extends Component { 16 | constructor() { 17 | super(); 18 | this.state = { 19 | currentSceneId: null, 20 | showList: false, 21 | }; 22 | 23 | this.handleSceneShowClicked = this.handleSceneShowClicked.bind(this); 24 | } 25 | 26 | handleSceneShowClicked(id = null) { 27 | this.setState({ currentSceneId: id }); 28 | } 29 | 30 | render() { 31 | const { currentSceneId, showList } = this.state; 32 | const { scenes, isLoading, tilesLoading, longitude, latitude, zoom, errorMessage } = this.props; 33 | const scene = scenes[0]; 34 | const pipelineStr = scene ? scene.pipeline.map((step) => { 35 | switch (step.operation) { 36 | case 'sigmoidal-contrast': 37 | return `sigmoidal(${step.bands || 'all'},${step.contrast},${step.bias})`; 38 | case 'gamma': 39 | return `gamma(${step.bands || 'all'},${step.value})`; 40 | default: 41 | return ''; 42 | } 43 | }).join(';') : ''; 44 | 45 | const bands = scene && !scene.isRGB ? [scene.redBand, scene.greenBand, scene.blueBand].join(',') : ''; 46 | 47 | window.location.hash = `#long=${longitude.toFixed(3)}&lat=${latitude.toFixed(3)}&zoom=${Math.round(zoom)}&scene=${scene ? scene.id : ''}&bands=${bands}&pipeline=${pipelineStr}`; 48 | 49 | return ( 50 |
51 | 96 | 97 |
106 |
107 | 108 | {/* 109 | */} 110 | 111 |
112 | 113 |
114 | 115 |
116 |
117 | 129 | { 130 | showList && scenes.length > 0 && 131 |
143 | {/* { } */} 144 | { scenes.length > 0 && 145 | 146 | } 147 |
148 | } 149 |
150 |
151 | ); 152 | } 153 | } 154 | 155 | const App = hot(module)(connect(mapStateToProps, mapDispatchToProps)((ConnectedApp))); 156 | 157 | export default App; 158 | -------------------------------------------------------------------------------- /src/components/comps.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | const mapDispatchToProps = dispatch => { 5 | return { 6 | addArticle: article => dispatch(addArticle(article)) 7 | }; 8 | }; 9 | 10 | class ConnectedSigmoidalContrastEditComponent extends Component { 11 | constructor() { 12 | super(); 13 | this.x = ''; 14 | 15 | this.handleChangeContrast = this.handleChangeContrast.bind(this); 16 | this.handleChangeBias = this.handleChangeBias.bind(this); 17 | } 18 | 19 | handleChangeContrast(event) { 20 | console.log(event.target.value); 21 | } 22 | 23 | handleChangeBias(event) { 24 | console.log(event.target.value); 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 | Contrast:
31 |
32 | Bias:
33 |
34 | ); 35 | } 36 | } 37 | 38 | const SigmoidalContrastEditComponent = connect(null, mapDispatchToProps)(ConnectedSigmoidalContrastEditComponent); 39 | 40 | export default SigmoidalContrastEditComponent; 41 | -------------------------------------------------------------------------------- /src/components/mapview.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import proj4 from 'proj4'; 5 | 6 | import 'ol/ol.css'; 7 | import Map from 'ol/map'; 8 | import View from 'ol/view'; 9 | import TileLayer from 'ol/layer/tile'; 10 | import TileWMS from 'ol/source/tilewms'; 11 | import TileGrid from 'ol/tilegrid/tilegrid'; 12 | import proj from 'ol/proj'; 13 | import extent from 'ol/extent'; 14 | 15 | import { fromUrl, fromUrls, Pool } from 'geotiff'; 16 | 17 | import CanvasTileImageSource, { ProgressBar } from '../maputil'; 18 | import { renderData } from '../renderutils'; 19 | 20 | import { tileStartLoading, tileStopLoading, setPosition } from '../actions/main'; 21 | import { type } from 'os'; 22 | 23 | 24 | proj.setProj4(proj4); 25 | 26 | async function all(promises) { 27 | return await Promise.all(promises); 28 | } 29 | 30 | const mapStateToProps = ({ scenes, main }) => { 31 | return { scenes, longitude: main.longitude, latitude: main.latitude, zoom: main.zoom }; 32 | }; 33 | 34 | const mapDispatchToProps = { 35 | tileStartLoading, 36 | tileStopLoading, 37 | setPosition, 38 | }; 39 | 40 | class MapView extends Component { 41 | constructor() { 42 | super(); 43 | this.mapRef = createRef(); 44 | this.progressBarRef = createRef(); 45 | 46 | this.map = new Map({ 47 | layers: [ 48 | new TileLayer({ 49 | extent: [-180, -90, 180, 90], 50 | source: new TileWMS({ 51 | url: 'https://tiles.maps.eox.at/wms', 52 | params: { LAYERS: 's2cloudless-2023' }, 53 | projection: 'EPSG:4326', 54 | attributions: [ 55 | 'Sentinel-2 cloudless - https://s2maps.eu by EOX IT Services GmbH (Contains modified Copernicus Sentinel data 2023)', 56 | ], 57 | }), 58 | }), 59 | ], 60 | view: new View({ 61 | projection: 'EPSG:4326', 62 | center: [0, 0], 63 | zoom: 5, 64 | // maxZoom: 13, 65 | minZoom: 3, 66 | maxZoom: 23, 67 | }), 68 | }); 69 | this.sceneLayers = {}; 70 | this.sceneSources = {}; 71 | this.tileCache = {}; 72 | this.renderedTileCache = {}; 73 | 74 | this.progressBar = new ProgressBar(); 75 | 76 | this.map.on('moveend', () => { 77 | const view = this.map.getView(); 78 | this.props.setPosition(...view.getCenter(), view.getZoom()); 79 | }); 80 | 81 | this.pool = new Pool(); 82 | } 83 | 84 | componentDidMount() { 85 | this.map.setTarget(this.mapRef.current); 86 | this.progressBar.setElement(this.progressBarRef.current); 87 | } 88 | 89 | componentDidUpdate(prevProps, prevState) { 90 | const { scenes: prevScenes } = prevProps; 91 | const { scenes } = this.props; 92 | 93 | if (prevScenes.length > scenes.length) { 94 | // TODO find scene and remove layer 95 | const removedScene = prevScenes.find(scene => scenes.indexOf(scene) === -1); 96 | delete this.renderedTileCache[removedScene.id]; 97 | this.map.removeLayer(this.sceneLayers[removedScene.id]); 98 | } else if (prevScenes.length < scenes.length) { 99 | this.addSceneLayer(scenes[scenes.length - 1]); 100 | } else { 101 | const changedScene = scenes.find((scene, index) => scene !== prevScenes[index]); 102 | 103 | if (changedScene) { 104 | delete this.renderedTileCache[changedScene.id]; 105 | 106 | const layer = this.sceneLayers[changedScene.id]; 107 | if (layer) { 108 | const source = layer.getSource(); 109 | // refresh the source cache 110 | source.setTileUrlFunction( 111 | source.getTileUrlFunction(), 112 | (new Date()).getTime(), 113 | ); 114 | } 115 | } 116 | } 117 | } 118 | 119 | componentWillUnmount() { 120 | this.map.setTarget(null); 121 | } 122 | 123 | async getImage(sceneId, url, hasOvr = true) { 124 | if (!this.sceneSources[sceneId]) { 125 | this.sceneSources[sceneId] = {}; 126 | } 127 | if (!this.sceneSources[sceneId][url]) { 128 | if (hasOvr) { 129 | this.sceneSources[sceneId][url] = fromUrls(url, [`${url}.ovr`]); 130 | } else { 131 | this.sceneSources[sceneId][url] = fromUrl(url); 132 | } 133 | } 134 | return this.sceneSources[sceneId][url]; 135 | } 136 | 137 | async getRawTile(tiff, url, z, x, y, isRGB = false, samples) { 138 | const id = `${url}-${samples ? samples.join(',') : 'all'}-${z}-${x}-${y}`; 139 | 140 | if (!this.tileCache[id]) { 141 | const image = await tiff.getImage(await tiff.getImageCount() - z - 1); 142 | 143 | // const poolSize = image.fileDirectory.Compression === 5 ? 4 : null; 144 | // const poolSize = null; 145 | 146 | const wnd = [ 147 | x * image.getTileWidth(), 148 | image.getHeight() - ((y + 1) * image.getTileHeight()), 149 | (x + 1) * image.getTileWidth(), 150 | image.getHeight() - (y * image.getTileHeight()), 151 | ]; 152 | 153 | if (isRGB) { 154 | this.tileCache[id] = image.readRGB({ 155 | window: wnd, 156 | pool: image.fileDirectory.Compression === 5 ? this.pool : null, 157 | }); 158 | } else { 159 | this.tileCache[id] = image.readRasters({ 160 | window: wnd, 161 | samples, 162 | pool: image.fileDirectory.Compression === 5 ? this.pool : null, 163 | }); 164 | } 165 | } 166 | 167 | return this.tileCache[id]; 168 | } 169 | 170 | async addSceneLayer(scene) { 171 | this.sceneSources[scene.id] = { 172 | [scene.redBand]: this.getImage(scene.id, scene.bands.get(scene.redBand), scene.hasOvr), 173 | [scene.greenBand]: this.getImage(scene.id, scene.bands.get(scene.greenBand), scene.hasOvr), 174 | [scene.blueBand]: this.getImage(scene.id, scene.bands.get(scene.blueBand), scene.hasOvr), 175 | }; 176 | const tiff = await this.getImage(scene.id, scene.bands.get(scene.redBand), scene.hasOvr); 177 | 178 | // calculate tilegrid from the 'red' image 179 | const images = []; 180 | const count = await tiff.getImageCount(); 181 | for (let i = 0; i < count; ++i) { 182 | images.push(await tiff.getImage(i)); 183 | } 184 | 185 | const first = images[0]; 186 | const resolutions = images.map((image => image.getResolution(first)[0])); 187 | const tileSizes = images.map((image => [image.getTileWidth(), image.getTileHeight()])); 188 | 189 | const tileGrid = new TileGrid({ 190 | extent: first.getBoundingBox(), 191 | origin: [first.getOrigin()[0], first.getBoundingBox()[1]], 192 | resolutions: resolutions.reverse(), 193 | tileSizes: tileSizes.reverse(), 194 | }); 195 | 196 | const code = first.geoKeys.ProjectedCSTypeGeoKey || first.geoKeys.GeographicTypeGeoKey; 197 | if (typeof code === 'undefined') { 198 | throw new Error('No ProjectedCSTypeGeoKey or GeographicTypeGeoKey provided'); 199 | } 200 | 201 | const epsg = `EPSG:${code}`; 202 | if (!proj4.defs(epsg)) { 203 | const response = await fetch(`https://epsg.io/${code}.proj4`); 204 | proj4.defs(epsg, await response.text()); 205 | } 206 | 207 | const layer = new TileLayer({ 208 | source: new CanvasTileImageSource({ 209 | projection: epsg, 210 | tileGrid, 211 | tileRenderFunction: (...args) => this.renderTile(scene.id, ...args), 212 | attributions: scene.attribution, 213 | }), 214 | }); 215 | 216 | this.map.addLayer(layer); 217 | this.sceneLayers[scene.id] = layer; 218 | 219 | const view = this.map.getView(); 220 | const lonLatExtent = proj.transformExtent( 221 | first.getBoundingBox(), epsg, this.map.getView().getProjection(), 222 | ); 223 | 224 | // only animate to new bounds when center is not already inside image 225 | if (!extent.containsCoordinate(lonLatExtent, view.getCenter())) { 226 | view.fit( 227 | lonLatExtent, { 228 | duration: 1000, 229 | padding: [0, this.map.getSize()[0] / 2, 0, 0], 230 | }, 231 | ); 232 | } 233 | 234 | const source = layer.getSource(); 235 | this.progressBar.setSource(source); 236 | 237 | source.on('tileloadstart', this.props.tileStartLoading); 238 | source.on('tileloadend', this.props.tileStopLoading); 239 | source.on('tileloaderror', this.props.tileStopLoading); 240 | } 241 | 242 | async renderTile(sceneId, canvas, z, x, y) { 243 | const id = `${z}-${x}-${y}`; 244 | 245 | if (!this.renderedTileCache[sceneId]) { 246 | this.renderedTileCache[sceneId] = {}; 247 | } 248 | 249 | if (!this.renderedTileCache[sceneId][id]) { 250 | this.renderedTileCache[sceneId][id] = this.renderTileInternal(sceneId, canvas, z, x, y); 251 | } 252 | return this.renderedTileCache[sceneId][id]; 253 | } 254 | 255 | async renderTileInternal(sceneId, canvas, z, x, y) { 256 | const scene = this.props.scenes.find(s => s.id === sceneId); 257 | 258 | if (!scene) { 259 | return; 260 | } 261 | 262 | if (scene.isRGB) { // && scene.isSingle) { 263 | const tiff = await this.getImage(sceneId, scene.bands.get(scene.redBand), scene.hasOvr); 264 | tiff.baseUrl = sceneId; 265 | console.time(`parsing ${sceneId + z + x + y}`); 266 | const data = await this.getRawTile(tiff, tiff.baseUrl, z, x, y, true); 267 | console.timeEnd(`parsing ${sceneId + z + x + y}`); 268 | const { width, height } = data; 269 | canvas.width = width; 270 | canvas.height = height; 271 | 272 | console.time(`rendering ${sceneId + z + x + y}`); 273 | // const ctx = canvas.getContext('2d'); 274 | // const imageData = ctx.createImageData(width, height); 275 | // const out = imageData.data; 276 | // let o = 0; 277 | 278 | // let shift = 0; 279 | // if (data instanceof Uint16Array) { 280 | // shift = 8; 281 | // } 282 | 283 | // for (let i = 0; i < data.length; i += 3) { 284 | // out[o] = data[i] >> shift; 285 | // out[o + 1] = data[i + 1] >> shift; 286 | // out[o + 2] = data[i + 2] >> shift; 287 | // out[o + 3] = data[i] || data[i + 1] || data[i + 2] ? 255 : 0; 288 | // o += 4; 289 | // } 290 | // ctx.putImageData(imageData, 0, 0); 291 | 292 | renderData(canvas, scene.pipeline, width, height, data, null, null, true); 293 | console.timeEnd(`rendering ${sceneId + z + x + y}`); 294 | } else if (scene.isSingle) { 295 | const tiff = await this.getImage(sceneId, scene.bands.get(scene.redBand), scene.hasOvr); 296 | tiff.baseUrl = sceneId; 297 | console.time(`parsing ${sceneId + z + x + y}`); 298 | const data = await this.getRawTile(tiff, tiff.baseUrl, z, x, y, false, [ 299 | scene.redBand, scene.greenBand, scene.blueBand, 300 | ]); 301 | console.timeEnd(`parsing ${sceneId + z + x + y}`); 302 | 303 | const { width, height } = data; 304 | canvas.width = width; 305 | canvas.height = height; 306 | 307 | console.time(`rendering ${sceneId + z + x + y}`); 308 | const [red, green, blue] = data; 309 | // const [red, green, blue] = [redArr, greenArr, blueArr].map(arr => arr[0]); 310 | renderData(canvas, scene.pipeline, width, height, red, green, blue, false); 311 | console.timeEnd(`rendering ${sceneId + z + x + y}`); 312 | } else { 313 | const [redImage, greenImage, blueImage] = await all([ 314 | this.getImage(sceneId, scene.bands.get(scene.redBand), scene.hasOvr), 315 | this.getImage(sceneId, scene.bands.get(scene.greenBand), scene.hasOvr), 316 | this.getImage(sceneId, scene.bands.get(scene.blueBand), scene.hasOvr), 317 | ]); 318 | 319 | redImage.baseUrl = scene.bands.get(scene.redBand); 320 | greenImage.baseUrl = scene.bands.get(scene.greenBand); 321 | blueImage.baseUrl = scene.bands.get(scene.blueBand); 322 | 323 | const [redArr, greenArr, blueArr] = await all([redImage, greenImage, blueImage].map( 324 | tiff => this.getRawTile(tiff, tiff.baseUrl, z, x, y), 325 | )); 326 | 327 | const { width, height } = redArr; 328 | canvas.width = width; 329 | canvas.height = height; 330 | 331 | console.time(`rendering ${sceneId + z + x + y}`); 332 | const [red, green, blue] = [redArr, greenArr, blueArr].map(arr => arr[0]); 333 | renderData(canvas, scene.pipeline, width, height, red, green, blue, false); 334 | console.timeEnd(`rendering ${sceneId + z + x + y}`); 335 | } 336 | } 337 | 338 | render() { 339 | const { longitude, latitude, zoom } = this.props; 340 | this.map.getView().setCenter([longitude, latitude]); 341 | this.map.getView().setZoom(zoom); 342 | return ( 343 |
344 |
345 |
346 | ); 347 | } 348 | } 349 | 350 | const ConnectedMapView = connect(mapStateToProps, mapDispatchToProps)(MapView); 351 | export default ConnectedMapView; 352 | -------------------------------------------------------------------------------- /src/components/scenes/add.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { addSceneFromIndex } from '../../actions/scenes/index'; 5 | import '../../styles.css'; 6 | 7 | const urlPattern = new RegExp('^(https?:\\/\\/)?' + // protocol 8 | '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|' + // domain name 9 | '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address 10 | '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path 11 | '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string 12 | '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator 13 | 14 | const mapStateToProps = ({ scenes, main }) => ({ scenes, isLoading: main.isLoading }); 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | addSceneFromIndex: (...args) => dispatch(addSceneFromIndex(...args)), 19 | }; 20 | }; 21 | 22 | class ConnectedAddSceneForm extends Component { 23 | constructor() { 24 | super(); 25 | 26 | this.state = { 27 | url: '', 28 | }; 29 | 30 | this.handleUrlChange = this.handleUrlChange.bind(this); 31 | this.handleAddClick = this.handleAddClick.bind(this); 32 | } 33 | 34 | handleUrlChange(event) { 35 | this.setState({ url: event.target.value }); 36 | } 37 | 38 | handleAddClick() { 39 | this.props.addSceneFromIndex(this.state.url); 40 | } 41 | 42 | checkUrl(url) { 43 | return urlPattern.test(url) && !this.props.scenes.find(scene => scene.id === url); 44 | } 45 | 46 | isLoading() { 47 | return this.props.isLoading; 48 | } 49 | 50 | render() { 51 | const { url } = this.state; 52 | const example1Url = 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/TCI.tif'; 53 | const example2Url = 'https://oin-hotosm.s3.amazonaws.com/56f9b5a963ebf4bc00074e70/0/56f9c2d42b67227a79b4faec.tif'; 54 | const example3Url = 'https://oin-hotosm.s3.amazonaws.com/59c66c5223c8440011d7b1e4/0/7ad397c0-bba2-4f98-a08a-931ec3a6e943.tif'; 55 | return ( 56 | 57 |
58 | 64 |
65 | 73 | 81 |
82 | 89 | 96 | 103 |
104 |
105 |
106 |
107 | ); 108 | } 109 | } 110 | 111 | const AddSceneForm = connect(mapStateToProps, mapDispatchToProps)(ConnectedAddSceneForm); 112 | 113 | export default AddSceneForm; 114 | -------------------------------------------------------------------------------- /src/components/scenes/details.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { removeScene, sceneChangeBands, addStep } from '../../actions/scenes'; 5 | import Step from './step'; 6 | 7 | const mapStateToProps = ({ scenes }, { id, onSceneHide }) => { 8 | return { scene: scenes.find(scene => scene.id === id), onSceneHide }; 9 | }; 10 | 11 | const mapDispatchToProps = { 12 | removeScene, 13 | sceneChangeBands, 14 | addStep, 15 | }; 16 | 17 | class ConnectedSceneDetails extends Component { 18 | constructor() { 19 | super(); 20 | this.state = { 21 | newStepType: '', 22 | }; 23 | } 24 | 25 | onScenarioChange(sceneId, newBands) { 26 | const { scene } = this.props; 27 | if (newBands !== '') { 28 | const [redBand, greenBand, blueBand] = newBands.split(',').map(i => parseInt(i, 10)); 29 | this.props.sceneChangeBands(scene.id, { redBand, greenBand, blueBand }); 30 | } 31 | } 32 | 33 | render() { 34 | const { scene, onSceneHide, removeScene, sceneChangeBands, addStep } = this.props; 35 | const bandIds = Array.from(Uint8Array.from(scene.bands.keys()).sort()); 36 | 37 | const isLandsat = Array.from(scene.bands.values()).find(file => /LC0?8.*B[0-9]+.TIF$/.test(file)); 38 | 39 | return ( 40 |
41 |
Details for {scene.id}
42 |
43 | { 44 | scene.isRGB || 45 |
46 |
47 |
48 | 49 | 58 |
59 |
60 | 61 | 70 |
71 |
72 | 73 | 82 |
83 | 84 | { 85 | isLandsat && 86 |
87 | 88 | 108 |
109 | } 110 | 111 | {/* 112 | */} 113 |
114 |
115 | } 116 |
117 | {scene.pipeline.map( 118 | (step, index) => ( 119 | 126 | ), 127 | )} 128 |
129 |
Add step
130 |
131 |
132 |
133 | 141 |
142 | 148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | ); 157 | } 158 | } 159 | 160 | const SceneDetails = connect(mapStateToProps, mapDispatchToProps)(ConnectedSceneDetails); 161 | export default SceneDetails; 162 | -------------------------------------------------------------------------------- /src/components/scenes/list.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | const mapStateToProps = ({ scenes }, { onSceneClicked }) => { 5 | return { scenes, onSceneClicked }; 6 | }; 7 | 8 | 9 | // class ConnectedSceneList extends Component { 10 | // constructor() 11 | // render() { 12 | // const { order } = props; 13 | // return ( 14 | //
    15 | // {order.map(id =>
  • {id}
  • )} 16 | //
17 | // ); 18 | // } 19 | // } 20 | 21 | export default connect(mapStateToProps)((props) => { 22 | const { scenes, onSceneClicked } = props; 23 | return ( 24 | 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/scenes/step.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { editStep, indexStep, removeStep } from '../../actions/scenes'; 5 | 6 | const mapDispatchToProps = { 7 | editStep, 8 | indexStep, 9 | removeStep, 10 | }; 11 | 12 | const Sigmoidal = connect(null, mapDispatchToProps)(({ step, sceneId, index, editStep }) => { 13 | return ( 14 | 15 |
16 | 17 |
18 | editStep(sceneId, index, { contrast: e.target.value })} 26 | /> 27 |
28 | {step.contrast} 29 |
30 |
31 |
32 |
33 | 34 |
35 | editStep(sceneId, index, { bias: e.target.value })} 43 | /> 44 |
45 | {step.bias} 46 |
47 |
48 |
49 |
50 | ); 51 | }); 52 | 53 | const Gamma = connect(null, mapDispatchToProps)(({ step, sceneId, index, editStep }) => { 54 | return ( 55 | 56 |
57 | 58 |
59 | editStep(sceneId, index, { value: e.target.value })} 67 | /> 68 |
69 | {step.value} 70 |
71 |
72 |
73 |
74 | ); 75 | }); 76 | 77 | 78 | export default connect(null, mapDispatchToProps)(({ sceneId, step, index, isLast, editStep, indexStep, removeStep }) => { 79 | let sub; 80 | 81 | if (step.operation === 'sigmoidal-contrast') { 82 | sub = ; 83 | } else if (step.operation === 'gamma') { 84 | sub = ; 85 | } 86 | 87 | return ( 88 |
89 |
90 | {step.operation} 91 | 92 | 95 | 98 | 101 | 102 |
103 |
104 |
105 | 106 | 116 |
117 | {sub} 118 |
119 |
120 | ); 121 | }); 122 | -------------------------------------------------------------------------------- /src/components/test.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Test extends Component { 4 | constructor() { 5 | super(); 6 | this.state = { 7 | title: '', 8 | }; 9 | } 10 | render() { 11 | return
Test {this.state.title}
; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import App from './app'; 6 | import store from './store'; 7 | 8 | render( 9 | 10 | 11 | , 12 | document.getElementById('app'), 13 | ); 14 | -------------------------------------------------------------------------------- /src/maputil.js: -------------------------------------------------------------------------------- 1 | import Tile from 'ol/tile'; 2 | import TileImageSource from 'ol/source/tileimage'; 3 | import TileState from 'ol/tilestate'; 4 | 5 | 6 | class CanvasTile extends Tile { 7 | constructor(tileCoord, state, src, crossOrigin, tileLoadFunction, options) { 8 | super(tileCoord, state, options); 9 | this.canvas = document.createElement('canvas'); 10 | this.tileLoadFunction = tileLoadFunction; 11 | this.src = src; 12 | } 13 | 14 | getCanvas() { 15 | return this.canvas; 16 | } 17 | 18 | getImage() { 19 | return this.getCanvas(); 20 | } 21 | 22 | load() { 23 | this.tileLoadFunction(this, this.src, 24 | () => this.setState(TileState.LOADED), 25 | () => this.setState(TileState.ERROR), 26 | ); 27 | } 28 | } 29 | 30 | export default class CanvasTileImageSource extends TileImageSource { 31 | constructor(opts) { 32 | super(Object.assign({}, opts, { 33 | tileUrlFunction: (...args) => this.customTileUrlFunction(...args), 34 | tileLoadFunction: (...args) => this.customTileLoadFunction(...args), 35 | tileClass: CanvasTile, 36 | })); 37 | 38 | this.tileRenderFunction = opts.tileRenderFunction; 39 | } 40 | 41 | customTileUrlFunction([z, x, y], pixelRatio) { 42 | return JSON.stringify({ z, x, y, pixelRatio }); 43 | } 44 | 45 | async customTileLoadFunction(tile, url, done, error) { 46 | const { z, x, y } = JSON.parse(url); 47 | try { 48 | this.dispatchEvent('tileloadstart'); 49 | await this.tileRenderFunction(tile.getCanvas(), z, x, y); 50 | done(); 51 | this.dispatchEvent('tileloadend'); 52 | } catch (err) { 53 | error(); 54 | this.dispatchEvent('tileloaderror'); 55 | } 56 | } 57 | } 58 | 59 | export class ProgressBar { 60 | constructor(el, source = null) { 61 | this.loaded = 0; 62 | this.loading = 0; 63 | this.setSource(source); 64 | 65 | this.loadingListener = () => { 66 | this.addLoading(); 67 | }; 68 | 69 | this.loadedListener = () => { 70 | this.addLoaded(); 71 | }; 72 | } 73 | 74 | setSource(source = null) { 75 | if (this.source) { 76 | this.source.un('tileloadstart', this.loadingListener); 77 | this.source.un('tileloadend', this.loadedListener); 78 | this.source.un('tileloaderror', this.loadedListener); 79 | } 80 | this.source = source; 81 | if (this.source) { 82 | this.source.on('tileloadstart', this.loadingListener); 83 | this.source.on('tileloadend', this.loadedListener); 84 | this.source.on('tileloaderror', this.loadedListener); 85 | } 86 | } 87 | 88 | setElement(el = null) { 89 | this.el = el; 90 | } 91 | 92 | show() { 93 | this.el.style.visibility = 'visible'; 94 | } 95 | 96 | hide() { 97 | if (this.loading === this.loaded) { 98 | this.el.style.visibility = 'hidden'; 99 | this.el.style.width = 0; 100 | } 101 | } 102 | 103 | addLoading() { 104 | if (this.loading === 0) { 105 | this.show(); 106 | } 107 | ++this.loading; 108 | this.update(); 109 | } 110 | 111 | addLoaded() { 112 | setTimeout(() => { 113 | ++this.loaded; 114 | this.update(); 115 | }, 100); 116 | } 117 | 118 | isLoading() { 119 | return this.loading > this.loaded; 120 | } 121 | 122 | update() { 123 | if (!this.el) { 124 | return; 125 | } 126 | const width = `${(this.loaded / this.loading * 100).toFixed(1)}%`; 127 | this.el.style.width = width; 128 | if (this.loading === this.loaded) { 129 | this.loading = 0; 130 | this.loaded = 0; 131 | setTimeout(() => { 132 | this.hide(); 133 | }, 500); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/reducers/main.js: -------------------------------------------------------------------------------- 1 | import types from '../types'; 2 | 3 | const { START_LOADING, STOP_LOADING, TILE_START_LOADING, TILE_STOP_LOADING, SET_POSITION, SET_ERROR } = types; 4 | 5 | const initialState = { 6 | isLoading: false, 7 | tilesLoading: 0, 8 | longitude: 0, 9 | latitude: 0, 10 | zoom: 5, 11 | errorMessage: null, 12 | }; 13 | 14 | export default function (state = initialState, action) { 15 | switch (action.type) { 16 | case START_LOADING: 17 | return { ...state, isLoading: true }; 18 | case STOP_LOADING: 19 | return { ...state, isLoading: false }; 20 | case TILE_START_LOADING: 21 | return { ...state, tilesLoading: state.tilesLoading + 1 }; 22 | case TILE_STOP_LOADING: 23 | return { ...state, tilesLoading: state.tilesLoading - 1 }; 24 | case SET_POSITION: 25 | return { 26 | ...state, 27 | longitude: action.longitude, 28 | latitude: action.latitude, 29 | zoom: action.zoom || state.zoom, 30 | }; 31 | 32 | case SET_ERROR: 33 | return { 34 | ...state, 35 | errorMessage: action.message, 36 | }; 37 | default: 38 | return state; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/reducers/scenes/index.js: -------------------------------------------------------------------------------- 1 | import types from '../../types'; 2 | 3 | const { 4 | SCENE_ADD, SCENE_REMOVE, SCENE_CHANGE_BANDS, 5 | SCENE_PIPELINE_ADD_STEP, SCENE_PIPELINE_REMOVE_STEP, SCENE_PIPELINE_INDEX_STEP, 6 | SCENE_PIPELINE_EDIT_STEP, 7 | } = types; 8 | 9 | const initialState = []; 10 | 11 | function scenePipeline(state, action) { 12 | switch (action.type) { 13 | case SCENE_PIPELINE_ADD_STEP: 14 | return typeof action.index !== 'undefined' ? [ 15 | ...state.slice(0, action.index), 16 | action.payload, 17 | ...state.slice(action.index), 18 | ] : [...state, action.payload]; 19 | case SCENE_PIPELINE_REMOVE_STEP: 20 | console.log(state.filter((current, index) => index !== action.index)); 21 | return state.filter((current, index) => index !== action.index); 22 | case SCENE_PIPELINE_INDEX_STEP: { 23 | const item = state[action.index]; 24 | return scenePipeline( 25 | scenePipeline(state, { type: SCENE_PIPELINE_REMOVE_STEP, index: action.index }), 26 | { type: SCENE_PIPELINE_ADD_STEP, index: action.newIndex, payload: item }, 27 | ); 28 | } 29 | case SCENE_PIPELINE_EDIT_STEP: { 30 | const item = { 31 | ...state[action.index], 32 | ...action.payload, 33 | }; 34 | return scenePipeline( 35 | scenePipeline(state, { type: SCENE_PIPELINE_REMOVE_STEP, index: action.index }), 36 | { type: SCENE_PIPELINE_ADD_STEP, index: action.index, payload: item }, 37 | ); 38 | } 39 | default: 40 | return state; 41 | } 42 | } 43 | 44 | 45 | export default function (state = initialState, action) { 46 | switch (action.type) { 47 | case SCENE_ADD: 48 | return [ 49 | ...state.filter(scene => scene.id !== action.sceneId), { 50 | id: action.sceneId, 51 | bands: action.bands, 52 | redBand: action.redBand, 53 | greenBand: action.greenBand, 54 | blueBand: action.blueBand, 55 | isSingle: action.isSingle, 56 | hasOvr: action.hasOvr, 57 | isRGB: action.isRGB, 58 | attribution: action.attribution, 59 | pipeline: action.pipeline, 60 | }, 61 | ]; 62 | 63 | // return [{ 64 | // id: action.sceneId, 65 | // bands: action.bands, 66 | // redBand: action.redBand, 67 | // greenBand: action.greenBand, 68 | // blueBand: action.blueBand, 69 | // pipeline: action.pipeline, 70 | // }]; 71 | case SCENE_REMOVE: 72 | return state.filter(scene => scene.id !== action.sceneId); 73 | case SCENE_CHANGE_BANDS: 74 | return state.map( 75 | scene => ( 76 | (scene.id === action.sceneId) ? { 77 | ...scene, 78 | ...action.newBands, 79 | } : scene 80 | ), 81 | ); 82 | case SCENE_PIPELINE_ADD_STEP: 83 | case SCENE_PIPELINE_REMOVE_STEP: 84 | case SCENE_PIPELINE_INDEX_STEP: 85 | case SCENE_PIPELINE_EDIT_STEP: 86 | return state.map( 87 | scene => ( 88 | (scene.id === action.sceneId) ? { 89 | ...scene, 90 | pipeline: scenePipeline(scene.pipeline, action), 91 | } : scene 92 | ), 93 | ); 94 | default: 95 | return state; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/renderutils.js: -------------------------------------------------------------------------------- 1 | import WebGLRenderer from './webglrenderer'; 2 | 3 | function toMathArray(input) { 4 | // console.log(input.__proto__); 5 | const min = 0; 6 | let max; 7 | if (input instanceof Uint8Array || input instanceof Uint8ClampedArray) { 8 | max = 255; 9 | } else if (input instanceof Uint16Array) { 10 | max = 65535; 11 | } else if (input instanceof Uint32Array) { 12 | max = 4294967295; 13 | } 14 | // TODO: more types 15 | 16 | const out = new Float32Array(input.length); 17 | for (let i = 0; i < out.length; ++i) { 18 | out[i] = input[i] / max; 19 | } 20 | return out; 21 | } 22 | 23 | function toOriginalArray(input, Type) { 24 | let max = 0; 25 | switch (Type) { 26 | case Uint8Array: 27 | case Uint8ClampedArray: 28 | max = 255; 29 | break; 30 | case Uint16Array: 31 | max = 65535; 32 | break; 33 | case Uint32Array: 34 | max = 4294967295; 35 | break; 36 | default: 37 | throw new Error(`Unsupported array type ${Type}`); 38 | } 39 | 40 | const out = new Type(input.length); 41 | for (let i = 0; i < out.length; ++i) { 42 | out[i] = Math.round(input[i] * max); 43 | } 44 | return out; 45 | } 46 | 47 | function sigmoidalContrast(data, contrast, bias) { 48 | const alpha = bias; 49 | const beta = contrast; 50 | 51 | if (beta > 0) { 52 | const denominator = 1 / (1 + Math.exp(beta * (alpha - 1))) - 1 / (1 + Math.exp(beta * alpha)); 53 | for (let i = 0; i < data.length; ++i) { 54 | const numerator = 1 / (1 + Math.exp(beta * (alpha - data[i]))) - 1 / (1 + Math.exp(beta * alpha)); 55 | data[i] = numerator / denominator; 56 | } 57 | } else { 58 | for (let i = 0; i < data.length; ++i) { 59 | data[i] = ( 60 | (beta * alpha) - Math.log( 61 | ( 62 | 1 / ( 63 | (data[i] / (1 + Math.exp((beta * alpha) - beta))) - 64 | (data[i] / (1 + Math.exp(beta * alpha))) + 65 | (1 / (1 + Math.exp(beta * alpha))) 66 | ) 67 | ) - 1) 68 | ) / beta; 69 | } 70 | } 71 | return data; 72 | } 73 | 74 | // const fragSigmoidalContrastSrc = ` 75 | // float sigmoidalContrast(float v, float contrast, float bias) { 76 | // float alpha = bias; 77 | // float beta = contrast; 78 | 79 | // if (beta > 0.0) { 80 | // float denominator = 1.0 / (1.0 + exp(beta * (alpha - 1.0))) - 1.0 / (1.0 + exp(beta * alpha)); 81 | // float numerator = 1.0 / (1.0 + exp(beta * (alpha - v))) - 1.0 / (1.0 + exp(beta * alpha)); 82 | // return numerator / denominator; 83 | // } else { 84 | // return ( 85 | // (beta * alpha) - log( 86 | // ( 87 | // 1.0 / ( 88 | // (v / (1.0 + exp((beta * alpha) - beta))) - 89 | // (v / (1.0 + exp(beta * alpha))) + 90 | // (1.0 / (1.0 + exp(beta * alpha))) 91 | // ) 92 | // ) - 1.0) 93 | // ) / beta; 94 | // } 95 | // } 96 | // `; 97 | 98 | function gamma(data, g) { 99 | for (let i = 0; i < data.length; ++i) { 100 | data[i] **= (1 / g); 101 | } 102 | return data; 103 | } 104 | 105 | // const fragGammaSrc = ` 106 | // float gamma(float v, float g) { 107 | // return pow(v, 1.0 / g); 108 | // } 109 | // `; 110 | 111 | function blitChannels(canvas, width, height, red, green, blue) { 112 | const ctx = canvas.getContext('2d'); 113 | const id = ctx.createImageData(width, height); 114 | const o = id.data; 115 | for (let i = 0; i < id.data.length / 4; ++i) { 116 | o[i * 4] = red[i]; 117 | o[(i * 4) + 1] = green[i]; 118 | o[(i * 4) + 2] = blue[i]; 119 | o[(i * 4) + 3] = (!red[i] && !green[i] && !blue[i]) ? 0 : 255; 120 | } 121 | ctx.putImageData(id, 0, 0); 122 | } 123 | 124 | function renderData2d(canvas, pipeline, width, height, redData, greenData, blueData) { 125 | let [red, green, blue] = [ 126 | toMathArray(redData), 127 | toMathArray(greenData), 128 | toMathArray(blueData), 129 | ]; 130 | 131 | let bands = [red, green, blue]; 132 | 133 | for (const step of pipeline) { 134 | let usedBands = [red, green, blue]; 135 | if (step.bands === 'red') { 136 | usedBands = [red]; 137 | } else if (step.bands === 'green') { 138 | usedBands = [green]; 139 | } else if (step.bands === 'blue') { 140 | usedBands = [blue]; 141 | } 142 | 143 | bands = bands.map((band) => { 144 | if (usedBands.indexOf(band) === -1) { 145 | return band; 146 | } 147 | if (step.operation === 'sigmoidal-contrast') { 148 | return sigmoidalContrast(band, step.contrast, step.bias); 149 | } else if (step.operation === 'gamma') { 150 | return gamma(band, step.value); 151 | } 152 | console.warning(`Unknown operation ${step.operation}`); 153 | return band; 154 | }); 155 | } 156 | 157 | [red, green, blue] = bands.map(band => toOriginalArray(band, Uint8Array)); 158 | blitChannels(canvas, width, height, red, green, blue); 159 | } 160 | 161 | // function create3DContext(canvas, optAttribs) { 162 | // const names = ['webgl', 'experimental-webgl']; 163 | // let context = null; 164 | // for (let ii = 0; ii < names.length; ++ii) { 165 | // try { 166 | // context = canvas.getContext(names[ii], optAttribs); 167 | // } catch (e) { } // eslint-disable-line 168 | // if (context) { 169 | // break; 170 | // } 171 | // } 172 | // if (!context || !context.getExtension('OES_texture_float')) { 173 | // return null; 174 | // } 175 | // return context; 176 | // } 177 | 178 | // function addLines(source) { 179 | // return source 180 | // .split('\n') 181 | // .map((line, i) => `${(i + 1).toString().padStart(3)}\t${line}`) 182 | // .join('\n'); 183 | // } 184 | 185 | // function createProgram(gl, vertexShaderSource, fragmentShaderSource) { 186 | // // create the shader program 187 | // const vertexShader = gl.createShader(gl.VERTEX_SHADER); 188 | // gl.shaderSource(vertexShader, vertexShaderSource); 189 | // gl.compileShader(vertexShader); 190 | // if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 191 | // throw new Error(gl.getShaderInfoLog(vertexShader) + addLines(vertexShaderSource)); 192 | // } 193 | 194 | // const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 195 | // gl.shaderSource(fragmentShader, fragmentShaderSource); 196 | // gl.compileShader(fragmentShader); 197 | // if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { 198 | // throw new Error(gl.getShaderInfoLog(fragmentShader) + addLines(fragmentShaderSource)); 199 | // } 200 | 201 | // const program = gl.createProgram(); 202 | // gl.attachShader(program, vertexShader); 203 | // gl.attachShader(program, fragmentShader); 204 | // gl.linkProgram(program); 205 | // return program; 206 | // } 207 | 208 | // function setRectangle(gl, x, y, width, height) { 209 | // const x1 = x; 210 | // const x2 = x + width; 211 | // const y1 = y; 212 | // const y2 = y + height; 213 | // gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 214 | // x1, y1, 215 | // x2, y1, 216 | // x1, y2, 217 | // x1, y2, 218 | // x2, y1, 219 | // x2, y2]), gl.STATIC_DRAW); 220 | // } 221 | 222 | // function createTexture(gl, data, width, height) { 223 | // gl.viewport(0, 0, width, height); 224 | // const texture = gl.createTexture(); 225 | // gl.bindTexture(gl.TEXTURE_2D, texture); 226 | 227 | // // Set the parameters so we can render any size image. 228 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 229 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 230 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 231 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 232 | 233 | // // Upload the image into the texture. 234 | // gl.texImage2D(gl.TEXTURE_2D, 0, 235 | // gl.LUMINANCE, 236 | // width, height, 0, 237 | // gl.LUMINANCE, gl.FLOAT, data ? new Float32Array(data) : null, 238 | // ); 239 | // return texture; 240 | // } 241 | 242 | // const vertexShaderSource = ` 243 | // attribute vec2 a_position; 244 | // attribute vec2 a_texCoord; 245 | // uniform mat3 u_matrix; 246 | // uniform vec2 u_resolution; 247 | // uniform float u_flipY; 248 | // varying vec2 v_texCoord; 249 | // void main() { 250 | // // apply transformation matrix 251 | // vec2 position = (u_matrix * vec3(a_position, 1)).xy; 252 | // // convert the rectangle from pixels to 0.0 to 1.0 253 | // vec2 zeroToOne = position / u_resolution; 254 | // // convert from 0->1 to 0->2 255 | // vec2 zeroToTwo = zeroToOne * 2.0; 256 | // // convert from 0->2 to -1->+1 (clipspace) 257 | // vec2 clipSpace = zeroToTwo - 1.0; 258 | // gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1); 259 | // // pass the texCoord to the fragment shader 260 | // // The GPU will interpolate this value between points. 261 | // v_texCoord = a_texCoord; 262 | // } 263 | // `; 264 | 265 | // function createFragmentShaderSource(pipeline) { 266 | 267 | // return ` 268 | // precision mediump float; 269 | 270 | // // our textures 271 | // uniform sampler2D u_textureRed; 272 | // uniform sampler2D u_textureGreen; 273 | // uniform sampler2D u_textureBlue; 274 | 275 | // // ${fragSigmoidalContrastSrc} 276 | // // ${fragGammaSrc} 277 | 278 | // // the texCoords passed in from the vertex shader. 279 | // varying vec2 v_texCoord; 280 | 281 | // void main() { 282 | // float red = texture2D(u_textureRed, v_texCoord)[0] / 65535.0; 283 | // float green = texture2D(u_textureGreen, v_texCoord)[0] / 65535.0; 284 | // float blue = texture2D(u_textureBlue, v_texCoord)[0] / 65535.0; 285 | 286 | // red = sigmoidalContrast(red, 50.0, 0.16); 287 | // green = sigmoidalContrast(green, 50.0, 0.16); 288 | // blue = sigmoidalContrast(blue, 50.0, 0.16); 289 | 290 | // red = gamma(red, 1.03); 291 | // blue = gamma(blue, 0.925); 292 | 293 | // if (red == 0.0 && green == 0.0 && blue == 0.0) { 294 | // discard; 295 | // } 296 | // gl_FragColor = vec4( 297 | // red, //red * 255.0, 298 | // green, //green * 255.0, 299 | // blue, //blue * 255.0, 300 | // 1.0 301 | // ); 302 | 303 | // // if (value == u_noDataValue) 304 | // // gl_FragColor = vec4(0.0, 0, 0, 0.0); 305 | // // else if ((!u_clampLow && value < u_domain[0]) || (!u_clampHigh && value > u_domain[1])) 306 | // // gl_FragColor = vec4(0, 0, 0, 0); 307 | // // else { 308 | // // float normalisedValue = (value - u_domain[0]) / (u_domain[1] - u_domain[0]); 309 | // // gl_FragColor = texture2D(u_textureScale, vec2(normalisedValue, 0)); 310 | // // } 311 | // }`; 312 | // } 313 | 314 | // class StepRenderer { 315 | // constructor(gl, vertexShaderSource, fragmentShaderSource, parameterMapping) { 316 | // const program = createProgram(gl, vertexShaderSource, fragmentShaderSource); 317 | // this.program = program; 318 | // gl.useProgram(program); 319 | 320 | // // look up where the vertex data needs to go. 321 | // const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord'); 322 | 323 | // // provide texture coordinates for the rectangle. 324 | // const texCoordBuffer = gl.createBuffer(); 325 | // gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); 326 | // gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 327 | // 0.0, 0.0, 328 | // 1.0, 0.0, 329 | // 0.0, 1.0, 330 | // 0.0, 1.0, 331 | // 1.0, 0.0, 332 | // 1.0, 1.0]), gl.STATIC_DRAW); 333 | // gl.enableVertexAttribArray(texCoordLocation); 334 | // gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); 335 | 336 | // // set the images 337 | // gl.uniform1i(gl.getUniformLocation(program, 'u_textureInput'), 0); 338 | 339 | // this.parameterMapping = parameterMapping; 340 | // } 341 | 342 | // render(gl, inputTexture, outputFramebuffer, unwrap, wrap, flipY, width, height, parameters) { 343 | // const { program } = this; 344 | // gl.useProgram(program); 345 | // for (const [paramName, shaderName] of Object.entries(this.parameterMapping)) { 346 | // gl.uniform1f(gl.getUniformLocation(program, shaderName), parameters[paramName]); 347 | // } 348 | 349 | // gl.uniform1i(gl.getUniformLocation(program, 'u_unwrap'), unwrap); 350 | // gl.uniform1i(gl.getUniformLocation(program, 'u_wrap'), wrap); 351 | // gl.uniform1f(gl.getUniformLocation(program, 'u_flipY'), flipY ? -1 : 1); 352 | 353 | // gl.activeTexture(gl.TEXTURE0); 354 | // gl.bindTexture(gl.TEXTURE_2D, inputTexture); 355 | 356 | // const positionLocation = gl.getAttribLocation(program, 'a_position'); 357 | // const resolutionLocation = gl.getUniformLocation(program, 'u_resolution'); 358 | // const matrixLocation = gl.getUniformLocation(program, 'u_matrix'); 359 | 360 | // gl.uniform2f(resolutionLocation, width, height); 361 | // const matrix = [ 362 | // 1, 0, 0, 363 | // 0, 1, 0, 364 | // 0, 0, 1, 365 | // ]; 366 | // gl.uniformMatrix3fv(matrixLocation, false, matrix); 367 | 368 | // const positionBuffer = gl.createBuffer(); 369 | // gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 370 | // gl.enableVertexAttribArray(positionLocation); 371 | // gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); 372 | 373 | // setRectangle(gl, 0, 0, width, height); 374 | 375 | // // define output framebuffer 376 | // gl.bindFramebuffer(gl.FRAMEBUFFER, outputFramebuffer); 377 | // gl.viewport(0, 0, width, height); 378 | 379 | // gl.drawArrays(gl.TRIANGLES, 0, 6); 380 | // } 381 | // } 382 | 383 | // function createAndSetupTexture(gl) { 384 | // const texture = gl.createTexture(); 385 | // gl.bindTexture(gl.TEXTURE_2D, texture); 386 | 387 | // // Set up texture so we can render any size image and so we are 388 | // // working with pixels. 389 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 390 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 391 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 392 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 393 | 394 | // return texture; 395 | // } 396 | 397 | // const common = ` 398 | // precision highp float; 399 | // uniform bool u_unwrap; 400 | // uniform bool u_wrap; 401 | // uniform sampler2D u_textureData; 402 | // // the texCoords passed in from the vertex shader. 403 | // varying vec2 v_texCoord; 404 | // `; 405 | 406 | // const lib = ` 407 | // ${common} 408 | // vec4 packFloat(float v) { 409 | // vec4 enc = vec4(1.0, 255.0, 65025.0, 16581375.0) * v; 410 | // enc = fract(enc); 411 | // enc -= enc.yzww * vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 0.0); 412 | // return enc; 413 | // } 414 | 415 | // float unpackFloat(vec4 rgba) { 416 | // return dot(rgba, vec4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 16581375.0)); 417 | // } 418 | 419 | // /* 420 | // const vec4 bitSh = vec4(256. * 256. * 256., 256. * 256., 256., 1.); 421 | // const vec4 bitMsk = vec4(0., vec3(1. / 256.0)); 422 | // const vec4 bitShifts = vec4(1.) / bitSh; 423 | 424 | // vec4 packFloatY(float value) { 425 | // vec4 comp = fract(value * bitSh); 426 | // comp -= comp.xxyz * bitMsk; 427 | // return comp; 428 | // } 429 | 430 | // float unpackFloatY(vec4 color) { 431 | // return dot(color, bitShifts); 432 | // } 433 | // */ 434 | 435 | // /* 436 | // vec4 packFloat(const float value) 437 | // { 438 | // const vec4 bitSh = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0); 439 | // const vec4 bitMsk = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0); 440 | // vec4 res = fract(value * bitSh); 441 | // res -= res.xxyz * bitMsk; 442 | // return res; 443 | // } 444 | 445 | // float unpackFloat(const vec4 value) 446 | // { 447 | // const vec4 bitSh = vec4(1.0/(256.0*256.0*256.0), 1.0/(256.0*256.0), 1.0/256.0, 1.0); 448 | // return(dot(value, bitSh)); 449 | // } 450 | // vec4 packFloat(const in float depth) 451 | // { 452 | // const vec4 bit_shift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0); 453 | // const vec4 bit_mask = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0); 454 | // vec4 res = fract(depth * bit_shift); 455 | // res -= res.xxyz * bit_mask; 456 | // return res; 457 | // } 458 | 459 | // float unpackFloat(const in vec4 rgba_depth) 460 | // { 461 | // const vec4 bit_shift = vec4(1.0/(256.0*256.0*256.0), 1.0/(256.0*256.0), 1.0/256.0, 1.0); 462 | // float depth = dot(rgba_depth, bit_shift); 463 | // return depth; 464 | // } 465 | // */ 466 | 467 | // /* 468 | // const float c_precision = 128.0; 469 | // const float c_precisionp1 = c_precision + 1.0; 470 | 471 | // vec4 packFloat(float value) { 472 | // vec3 color; 473 | // color.r = mod(value, c_precisionp1) / c_precision; 474 | // color.b = mod(floor(value / c_precisionp1), c_precisionp1) / c_precision; 475 | // color.g = floor(value / (c_precisionp1 * c_precisionp1)) / c_precision; 476 | // return vec4(color, 1); 477 | // } 478 | 479 | // float unpackFloat(vec4 color) { 480 | // color = clamp(color, 0.0, 1.0); 481 | // return floor(color.r * c_precision + 0.5) 482 | // + floor(color.b * c_precision + 0.5) * c_precisionp1 483 | // + floor(color.g * c_precision + 0.5) * c_precisionp1 * c_precisionp1; 484 | // } 485 | // */ 486 | // `; 487 | 488 | 489 | // const stretchShaderSource = ` 490 | // ${lib} 491 | // uniform float u_max; 492 | 493 | // void main() { 494 | // float value; 495 | // if (u_unwrap) { 496 | // value = unpackFloat(texture2D(u_textureData, v_texCoord)); 497 | // } else { 498 | // value = texture2D(u_textureData, v_texCoord)[0]; 499 | // } 500 | 501 | // gl_FragColor = packFloat(value / u_max); 502 | // } 503 | // `; 504 | 505 | // const sigmoidalContrastShaderSource = ` 506 | // ${lib} 507 | // uniform float u_contrast; 508 | // uniform float u_bias; 509 | 510 | // float sigmoidalContrast(float v, float contrast, float bias) { 511 | // float alpha = bias; 512 | // float beta = contrast; 513 | 514 | // if (beta > 0.0) { 515 | // float denominator = 1.0 / (1.0 + exp(beta * (alpha - 1.0))) - 1.0 / (1.0 + exp(beta * alpha)); 516 | // float numerator = 1.0 / (1.0 + exp(beta * (alpha - v))) - 1.0 / (1.0 + exp(beta * alpha)); 517 | // return numerator / denominator; 518 | // } else { 519 | // return ( 520 | // (beta * alpha) - log( 521 | // ( 522 | // 1.0 / ( 523 | // (v / (1.0 + exp((beta * alpha) - beta))) - 524 | // (v / (1.0 + exp(beta * alpha))) + 525 | // (1.0 / (1.0 + exp(beta * alpha))) 526 | // ) 527 | // ) - 1.0) 528 | // ) / beta; 529 | // } 530 | // } 531 | 532 | // void main() { 533 | // float value; 534 | // if (u_unwrap) { 535 | // value = unpackFloat(texture2D(u_textureData, v_texCoord)); 536 | // } else { 537 | // value = texture2D(u_textureData, v_texCoord)[0]; 538 | // } 539 | 540 | // gl_FragColor = packFloat(sigmoidalContrast(value, u_contrast, u_bias)); 541 | // } 542 | 543 | // `; 544 | 545 | // const gammaShaderSource = ` 546 | // ${lib} 547 | // uniform float u_gamma; 548 | 549 | // float gamma(float v, float g) { 550 | // return pow(v, 1.0 / g); 551 | // } 552 | 553 | // void main() { 554 | // float value; 555 | // if (u_unwrap) { 556 | // value = unpackFloat(texture2D(u_textureData, v_texCoord)); 557 | // } else { 558 | // value = texture2D(u_textureData, v_texCoord)[0]; 559 | // } 560 | 561 | // gl_FragColor = packFloat(gamma(value, u_gamma)); 562 | // } 563 | // `; 564 | 565 | // const combineShaderSource = ` 566 | // precision mediump float; 567 | // uniform bool u_unwrap; 568 | // uniform bool u_wrap; 569 | // uniform sampler2D u_textureRed; 570 | // uniform sampler2D u_textureGreen; 571 | // uniform sampler2D u_textureBlue; 572 | // // the texCoords passed in from the vertex shader. 573 | // varying vec2 v_texCoord; 574 | 575 | // float unpackFloat(vec4 rgba) { 576 | // return dot(rgba, vec4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 16581375.0)); 577 | // } 578 | 579 | // void main() { 580 | // float red = unpackFloat(texture2D(u_textureRed, v_texCoord)); 581 | // float green = unpackFloat(texture2D(u_textureGreen, v_texCoord)); 582 | // float blue = unpackFloat(texture2D(u_textureBlue, v_texCoord)); 583 | 584 | // if (red == 0.0 && green == 0.0 && blue == 0.0) { 585 | // discard; 586 | // } 587 | // gl_FragColor = vec4(red, green, blue, 1.0); 588 | // } 589 | // `; 590 | 591 | // const renderCanvas = document.createElement('canvas'); 592 | // const gl = create3DContext(renderCanvas); 593 | // let combineProgram; 594 | // if (gl) { 595 | // combineProgram = createProgram(gl, vertexShaderSource, combineShaderSource); 596 | // gl.useProgram(combineProgram); 597 | 598 | // // look up where the vertex data needs to go. 599 | // const texCoordLocation = gl.getAttribLocation(combineProgram, 'a_texCoord'); 600 | 601 | // // provide texture coordinates for the rectangle. 602 | // const texCoordBuffer = gl.createBuffer(); 603 | // gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); 604 | // gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 605 | // 0.0, 0.0, 606 | // 1.0, 0.0, 607 | // 0.0, 1.0, 608 | // 0.0, 1.0, 609 | // 1.0, 0.0, 610 | // 1.0, 1.0]), gl.STATIC_DRAW); 611 | // gl.enableVertexAttribArray(texCoordLocation); 612 | // gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); 613 | 614 | // // set the images 615 | // gl.uniform1i(gl.getUniformLocation(combineProgram, 'u_textureRed'), 0); 616 | // gl.uniform1i(gl.getUniformLocation(combineProgram, 'u_textureGreen'), 1); 617 | // gl.uniform1i(gl.getUniformLocation(combineProgram, 'u_textureBlue'), 2); 618 | // } 619 | 620 | // const stretchRenderer = new StepRenderer(gl, vertexShaderSource, stretchShaderSource, { max: 'u_max' }); 621 | // const stepRenderers = { 622 | // 'sigmoidal-contrast': new StepRenderer(gl, vertexShaderSource, sigmoidalContrastShaderSource, { contrast: 'u_contrast', bias: 'u_bias' }), 623 | // gamma: new StepRenderer(gl, vertexShaderSource, gammaShaderSource, { value: 'u_gamma' }), 624 | // }; 625 | 626 | 627 | // function checkFB(gl) { 628 | // var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); 629 | // switch (status) { 630 | // case gl.FRAMEBUFFER_COMPLETE: 631 | // break; 632 | // case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: 633 | // throw ("Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_ATTACHMENT"); 634 | // break; 635 | // case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: 636 | // throw ("Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"); 637 | // break; 638 | // case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: 639 | // throw ("Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_DIMENSIONS"); 640 | // break; 641 | // case gl.FRAMEBUFFER_UNSUPPORTED: 642 | // throw ("Incomplete framebuffer: FRAMEBUFFER_UNSUPPORTED"); 643 | // break; 644 | // default: 645 | // throw ("Incomplete framebuffer: " + status); 646 | // } 647 | // } 648 | 649 | // function renderDataWebGl(canvas, gl, pipeline, width, height, redData, greenData, blueData) { 650 | // try { 651 | // gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT); 652 | 653 | // renderCanvas.width = width; 654 | // renderCanvas.height = height; 655 | 656 | // const fboTextures = []; 657 | // const framebuffers = []; 658 | // for (let ii = 0; ii < 2; ++ii) { 659 | // const texture = createAndSetupTexture(gl); 660 | // fboTextures.push(texture); 661 | 662 | // // make the texture the same size as the image 663 | // gl.texImage2D( 664 | // gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, 665 | // gl.RGBA, gl.UNSIGNED_BYTE, null, 666 | // ); 667 | 668 | // // Create a framebuffer 669 | // const fbo = gl.createFramebuffer(); 670 | // framebuffers.push(fbo); 671 | // } 672 | 673 | // const [textureRed, textureGreen, textureBlue] = [['red', redData], ['green', greenData], ['blue', blueData]].map( 674 | // ([color, data]) => { 675 | // const usedSteps = pipeline.filter(step => (step.bands === color || step.bands === 'all' || !step.bands)); 676 | 677 | // const texture = createTexture(gl, data, width, height); 678 | // const outputTexture = createAndSetupTexture(gl); 679 | // // make the texture the same size as the image 680 | // gl.texImage2D( 681 | // gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, 682 | // gl.RGBA, gl.UNSIGNED_BYTE, null, 683 | // ); 684 | 685 | // if (usedSteps.length > 0) { 686 | // for (let i = 0; i < 2; ++i) { 687 | // gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[i]); 688 | // gl.framebufferTexture2D( 689 | // gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, fboTextures[i], 0); 690 | // checkFB(gl); 691 | // } 692 | // } else { 693 | // gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[0]); 694 | // gl.framebufferTexture2D( 695 | // gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0); 696 | // checkFB(gl); 697 | // } 698 | 699 | // // stretch to 0-1 700 | // stretchRenderer.render(gl, texture, framebuffers[0], false, true, false, width, height, { max: 65535 }); 701 | 702 | // for (let i = 0; i < usedSteps.length; ++i) { 703 | // const step = usedSteps[i]; 704 | // const renderer = stepRenderers[step.operation]; 705 | 706 | // // if we are in the last step, set the target for the framebuffer to the output texture 707 | // if (i === usedSteps.length - 1) { 708 | // gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[(i + 1) % 2]); 709 | // gl.framebufferTexture2D( 710 | // gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0); 711 | // checkFB(gl); 712 | // } 713 | 714 | // renderer.render( 715 | // gl, 716 | // fboTextures[i % 2], // input texture 717 | // framebuffers[(i + 1) % 2], // output framebuffer 718 | // i > 0, // whether float unwrapping is necessary 719 | // true, // whether to wrap float as rgba 720 | // false, // flip 721 | // width, height, 722 | // step, 723 | // ); 724 | // } 725 | // return outputTexture; 726 | // }); 727 | 728 | // gl.viewport(0, 0, width, height); 729 | 730 | // // const textureRed = createTexture(gl, redData, width, height); 731 | // // const textureGreen = createTexture(gl, greenData, width, height); 732 | // // const textureBlue = createTexture(gl, blueData, width, height); 733 | 734 | // gl.useProgram(combineProgram); 735 | 736 | // gl.activeTexture(gl.TEXTURE0); 737 | // gl.bindTexture(gl.TEXTURE_2D, textureRed); 738 | // gl.activeTexture(gl.TEXTURE1); 739 | // gl.bindTexture(gl.TEXTURE_2D, textureGreen); 740 | // gl.activeTexture(gl.TEXTURE2); 741 | // gl.bindTexture(gl.TEXTURE_2D, textureBlue); 742 | 743 | // const positionLocation = gl.getAttribLocation(combineProgram, 'a_position'); 744 | // const resolutionLocation = gl.getUniformLocation(combineProgram, 'u_resolution'); 745 | // const matrixLocation = gl.getUniformLocation(combineProgram, 'u_matrix'); 746 | 747 | // gl.uniform1f(gl.getUniformLocation(combineProgram, 'u_flipY'), -1); 748 | 749 | // gl.uniform2f(resolutionLocation, canvas.width, canvas.height); 750 | // const matrix = [ 751 | // 1, 0, 0, 752 | // 0, 1, 0, 753 | // 0, 0, 1, 754 | // ]; 755 | // gl.uniformMatrix3fv(matrixLocation, false, matrix); 756 | 757 | // const positionBuffer = gl.createBuffer(); 758 | // gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 759 | // gl.enableVertexAttribArray(positionLocation); 760 | // gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); 761 | 762 | // setRectangle(gl, 0, 0, canvas.width, canvas.height); 763 | 764 | // // Draw the rectangle. 765 | // gl.bindFramebuffer(gl.FRAMEBUFFER, null); 766 | // gl.viewport(0, 0, width, height); 767 | // gl.drawArrays(gl.TRIANGLES, 0, 6); 768 | 769 | // // cleanup 770 | // gl.deleteTexture(textureRed); 771 | // gl.deleteTexture(textureGreen); 772 | // gl.deleteTexture(textureBlue); 773 | 774 | // // blit the current canvas on the output canvas 775 | // canvas.width = width; 776 | // canvas.height = height; 777 | // const ctx = canvas.getContext('2d'); 778 | // ctx.drawImage(renderCanvas, 0, 0); 779 | // } catch (e) { 780 | // console.error(e); 781 | // return; 782 | // } 783 | // } 784 | 785 | let webGLRenderer = null; 786 | if (WebGLRenderer.isSupported()) { 787 | webGLRenderer = new WebGLRenderer(); 788 | } 789 | 790 | export function renderData(canvas, ...args) { 791 | // TODO: prefer rendering via webgl 792 | // const gl = create3DContext(canvas); 793 | if (webGLRenderer) { 794 | // return renderDataWebGl(canvas, gl, ...args); 795 | return webGLRenderer.render(canvas, ...args); 796 | } 797 | return renderData2d(canvas, ...args); 798 | } 799 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import sceneReducer from './reducers/scenes'; 4 | import mainReducer from './reducers/main'; 5 | import { addSceneFromIndex, sceneChangeBands } from './actions/scenes'; 6 | 7 | 8 | function parseQuery(query) { 9 | return new Map(query.split('&').map(item => item.split('='))); 10 | } 11 | 12 | const params = parseQuery(window.location.hash.slice(1)); 13 | 14 | const pipeline = params.has('pipeline') ? params.get('pipeline').split(';').map((item) => { 15 | const sigmoidalRe = /sigmoidal\(([a-z]+),([0-9.]+),([0-9.]+)\)$/i; 16 | const gammaRe = /gamma\(([a-z]+),([0-9.]+)\)$/i; 17 | if (sigmoidalRe.test(item)) { 18 | const match = item.match(sigmoidalRe); 19 | return { 20 | operation: 'sigmoidal-contrast', 21 | bands: match[1], 22 | contrast: parseFloat(match[2]), 23 | bias: parseFloat(match[3]), 24 | }; 25 | } else if (gammaRe.test(item)) { 26 | const match = item.match(gammaRe); 27 | return { 28 | operation: 'gamma', 29 | bands: match[1], 30 | value: parseFloat(match[2]), 31 | }; 32 | } 33 | return null; 34 | }).filter(item => item) : undefined; 35 | 36 | const bands = params.has('bands') ? params.get('bands') 37 | .split(',') 38 | .map(b => parseInt(b, 10)) 39 | .filter(b => !Number.isNaN(b)) 40 | : []; 41 | 42 | const store = createStore( 43 | combineReducers({ 44 | scenes: sceneReducer, 45 | main: mainReducer, 46 | }), { 47 | main: { 48 | longitude: params.has('long') ? parseFloat(params.get('long')) : 16.37, 49 | latitude: params.has('lat') ? parseFloat(params.get('lat')) : 48.21, 50 | zoom: params.has('zoom') ? parseFloat(params.get('zoom')) : 5, 51 | }, 52 | scenes: [], 53 | }, 54 | applyMiddleware(thunk), 55 | ); 56 | 57 | const scene = params.get('scene'); 58 | if (scene && scene !== '') { 59 | const request = store.dispatch(addSceneFromIndex(scene, undefined, pipeline)); 60 | if (bands.length === 3) { 61 | request.then(() => store.dispatch(sceneChangeBands(scene, { 62 | redBand: bands[0], 63 | greenBand: bands[1], 64 | blueBand: bands[2], 65 | }))); 66 | } 67 | } 68 | 69 | export default store; 70 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background: #373b50 !important; 3 | padding-bottom: 0 !important; 4 | padding-top: 0 !important; 5 | margin: 0 !important; 6 | border: 0 !important; 7 | height: 50px !important; 8 | /* display: block !important; */ 9 | } 10 | 11 | .navbar-brand>img { 12 | height: 20px !important; 13 | width: auto !important; 14 | } 15 | 16 | .map-progress-bar { 17 | position: absolute; 18 | bottom: 0; 19 | left: 0; 20 | height: 2px; 21 | background: white; 22 | width: 0; 23 | transition: width 250ms; 24 | z-index: 1000000; 25 | } 26 | 27 | 28 | .fade-in { 29 | animation: fadein 2s; 30 | } 31 | 32 | .fade-out { 33 | animation: fadeout 2s; 34 | } 35 | 36 | .hide { 37 | display: none; 38 | } 39 | 40 | @keyframes fadein { 41 | from { opacity: 0; } 42 | to { opacity: 1; } 43 | } 44 | 45 | @keyframes fadeout { 46 | from { opacity: 1; } 47 | to { opacity: 0; } 48 | } -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | const types = { 2 | START_LOADING: 'START_LOADING', 3 | STOP_LOADING: 'STOP_LOADING', 4 | TILE_START_LOADING: 'TILE_START_LOADING', 5 | TILE_STOP_LOADING: 'TILE_STOP_LOADING', 6 | SET_POSITION: 'SET_POSITION', 7 | SCENE_ADD: 'SCENE_ADD', 8 | SCENE_REMOVE: 'SCENE_REMOVE', 9 | SCENE_CHANGE_BANDS: 'SCENE_CHANGE_BANDS', 10 | SCENE_PIPELINE_ADD_STEP: 'SCENE_PIPELINE_ADD_STEP', 11 | SCENE_PIPELINE_REMOVE_STEP: 'SCENE_PIPELINE_REMOVE_STEP', 12 | SCENE_PIPELINE_INDEX_STEP: 'SCENE_PIPELINE_INDEX_STEP', 13 | SCENE_PIPELINE_EDIT_STEP: 'SCENE_PIPELINE_EDIT_STEP', 14 | SET_ERROR: 'SET_ERROR', 15 | }; 16 | 17 | export default types; 18 | -------------------------------------------------------------------------------- /src/webglrenderer.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const globalVertexShaderSource = ` 4 | attribute vec2 a_position; 5 | attribute vec2 a_texCoord; 6 | uniform mat3 u_matrix; 7 | uniform vec2 u_resolution; 8 | uniform float u_flipY; 9 | varying vec2 v_texCoord; 10 | void main() { 11 | // apply transformation matrix 12 | vec2 position = (u_matrix * vec3(a_position, 1)).xy; 13 | // convert the rectangle from pixels to 0.0 to 1.0 14 | vec2 zeroToOne = position / u_resolution; 15 | // convert from 0->1 to 0->2 16 | vec2 zeroToTwo = zeroToOne * 2.0; 17 | // convert from 0->2 to -1->+1 (clipspace) 18 | vec2 clipSpace = zeroToTwo - 1.0; 19 | gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1); 20 | // pass the texCoord to the fragment shader 21 | // The GPU will interpolate this value between points. 22 | v_texCoord = a_texCoord; 23 | } 24 | `; 25 | 26 | 27 | function getMaxValue(input) { 28 | if (input instanceof Uint8Array || input instanceof Uint8ClampedArray) { 29 | return 255; 30 | } else if (input instanceof Uint16Array) { 31 | return 65535; 32 | } else if (input instanceof Uint32Array) { 33 | return 4294967295; 34 | } 35 | return 0; 36 | } 37 | 38 | 39 | function create3DContext(canvas, optAttribs) { 40 | const names = ['webgl', 'experimental-webgl']; 41 | let context = null; 42 | for (let ii = 0; ii < names.length; ++ii) { 43 | try { 44 | context = canvas.getContext(names[ii], optAttribs); 45 | } catch (e) { } // eslint-disable-line 46 | if (context) { 47 | break; 48 | } 49 | } 50 | if (!context || !context.getExtension('OES_texture_float')) { 51 | return null; 52 | } 53 | return context; 54 | } 55 | 56 | 57 | function addLines(source) { 58 | return source 59 | .split('\n') 60 | .map((line, i) => `${(i + 1).toString().padStart(3)}\t${line}`) 61 | .join('\n'); 62 | } 63 | 64 | function createProgram(gl, vertexShaderSource, fragmentShaderSource) { 65 | // create the shader program 66 | const vertexShader = gl.createShader(gl.VERTEX_SHADER); 67 | gl.shaderSource(vertexShader, vertexShaderSource); 68 | gl.compileShader(vertexShader); 69 | if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 70 | throw new Error(gl.getShaderInfoLog(vertexShader) + addLines(vertexShaderSource)); 71 | } 72 | 73 | const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 74 | gl.shaderSource(fragmentShader, fragmentShaderSource); 75 | gl.compileShader(fragmentShader); 76 | if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { 77 | throw new Error(gl.getShaderInfoLog(fragmentShader) + addLines(fragmentShaderSource)); 78 | } 79 | 80 | const program = gl.createProgram(); 81 | gl.attachShader(program, vertexShader); 82 | gl.attachShader(program, fragmentShader); 83 | gl.linkProgram(program); 84 | return program; 85 | } 86 | 87 | class StepBase { 88 | static get name() { 89 | return null; 90 | } 91 | 92 | constructor(gl, prefix, bands = 'all') { 93 | this.gl = gl; 94 | this.prefix = prefix; 95 | this.bands = bands; 96 | } 97 | } 98 | 99 | 100 | class SigmoidalContrastStep extends StepBase { 101 | static get name() { 102 | return 'sigmoidal-contrast'; 103 | } 104 | 105 | static getFragmentSourceLib() { 106 | return ` 107 | float sigmoidalContrast(float v, float contrast, float bias) { 108 | float alpha = bias; 109 | float beta = contrast; 110 | 111 | if (beta > 0.0) { 112 | float denominator = 1.0 / (1.0 + exp(beta * (alpha - 1.0))) - 1.0 / (1.0 + exp(beta * alpha)); 113 | float numerator = 1.0 / (1.0 + exp(beta * (alpha - v))) - 1.0 / (1.0 + exp(beta * alpha)); 114 | return numerator / denominator; 115 | } else { 116 | return ( 117 | (beta * alpha) - log( 118 | ( 119 | 1.0 / ( 120 | (v / (1.0 + exp((beta * alpha) - beta))) - 121 | (v / (1.0 + exp(beta * alpha))) + 122 | (1.0 / (1.0 + exp(beta * alpha))) 123 | ) 124 | ) - 1.0) 125 | ) / beta; 126 | } 127 | } 128 | `; 129 | } 130 | 131 | getUniformIds() { 132 | return [`u_${this.prefix}_contrast`, `u_${this.prefix}_bias`]; 133 | } 134 | 135 | getCall(variableName) { 136 | return `sigmoidalContrast(${variableName}, u_${this.prefix}_contrast, u_${this.prefix}_bias)`; 137 | } 138 | 139 | bindUniforms(gl, program, values) { 140 | gl.uniform1f( 141 | gl.getUniformLocation(program, `u_${this.prefix}_contrast`), 142 | values.contrast, 143 | ); 144 | gl.uniform1f( 145 | gl.getUniformLocation(program, `u_${this.prefix}_bias`), 146 | values.bias, 147 | ); 148 | } 149 | } 150 | 151 | class GammaStep extends StepBase { 152 | static get name() { 153 | return 'gamma'; 154 | } 155 | 156 | static getFragmentSourceLib() { 157 | return ` 158 | float gamma(float v, float g) { 159 | return pow(v, 1.0 / g); 160 | } 161 | `; 162 | } 163 | 164 | getUniformIds() { 165 | return [`u_${this.prefix}_gamma`]; 166 | } 167 | 168 | getCall(variableName) { 169 | return `gamma(${variableName}, u_${this.prefix}_gamma)`; 170 | } 171 | 172 | bindUniforms(gl, program, values) { 173 | gl.uniform1f( 174 | gl.getUniformLocation(program, `u_${this.prefix}_gamma`), 175 | values.value, 176 | ); 177 | } 178 | } 179 | 180 | const stepClasses = { 181 | 'sigmoidal-contrast': SigmoidalContrastStep, 182 | gamma: GammaStep, 183 | }; 184 | 185 | 186 | function getPipelineId(pipeline) { 187 | return pipeline.map(stepDef => `${stepDef.operation}#${stepDef.bands}`).join('/'); 188 | } 189 | 190 | 191 | function setRectangle(gl, x, y, width, height) { 192 | const x1 = x; 193 | const x2 = x + width; 194 | const y1 = y; 195 | const y2 = y + height; 196 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 197 | x1, y1, 198 | x2, y1, 199 | x1, y2, 200 | x1, y2, 201 | x2, y1, 202 | x2, y2]), gl.STATIC_DRAW); 203 | } 204 | 205 | class PipelineProgramWrapper { 206 | constructor(gl, pipeline) { 207 | this.buildSteps = pipeline.map((step, i) => { 208 | return new stepClasses[step.operation](this.gl, i, step.bands); 209 | }); 210 | const program = this.buildPipelineProgram(gl); 211 | this.program = program; 212 | } 213 | 214 | render(gl, pipeline, width, height, redData, greenData, blueData, isRGB) { 215 | const { program } = this; 216 | gl.useProgram(program); 217 | gl.viewport(0, 0, width, height); 218 | 219 | // look up where the vertex data needs to go. 220 | const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord'); 221 | 222 | // provide texture coordinates for the rectangle. 223 | const texCoordBuffer = gl.createBuffer(); 224 | gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); 225 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 226 | 0.0, 0.0, 227 | 1.0, 0.0, 228 | 0.0, 1.0, 229 | 0.0, 1.0, 230 | 1.0, 0.0, 231 | 1.0, 1.0]), gl.STATIC_DRAW); 232 | gl.enableVertexAttribArray(texCoordLocation); 233 | gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); 234 | 235 | gl.uniform1f(gl.getUniformLocation(program, 'u_flipY'), true ? -1 : 1); 236 | 237 | 238 | const positionLocation = gl.getAttribLocation(program, 'a_position'); 239 | const resolutionLocation = gl.getUniformLocation(program, 'u_resolution'); 240 | const matrixLocation = gl.getUniformLocation(program, 'u_matrix'); 241 | 242 | gl.uniform2f(resolutionLocation, width, height); 243 | const matrix = [ 244 | 1, 0, 0, 245 | 0, 1, 0, 246 | 0, 0, 1, 247 | ]; 248 | gl.uniformMatrix3fv(matrixLocation, false, matrix); 249 | 250 | gl.uniform1i(gl.getUniformLocation(program, 'u_singleTexture'), isRGB ? 1 : 0); 251 | 252 | const positionBuffer = gl.createBuffer(); 253 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 254 | gl.enableVertexAttribArray(positionLocation); 255 | gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); 256 | 257 | setRectangle(gl, 0, 0, width, height); 258 | 259 | let textureRed; 260 | let textureGreen; 261 | let textureBlue; 262 | if (isRGB) { 263 | gl.uniform1f(gl.getUniformLocation(program, 'u_maxValue'), 1.0); 264 | textureRed = gl.createTexture(); 265 | gl.bindTexture(gl.TEXTURE_2D, textureRed); 266 | 267 | gl.uniform1i(gl.getUniformLocation(program, 'u_textureRed'), 0); 268 | 269 | // Set the parameters so we can render any size image. 270 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 271 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 272 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 273 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 274 | 275 | gl.texImage2D( 276 | gl.TEXTURE_2D, 277 | 0, 278 | gl.RGB, 279 | width, 280 | height, 281 | 0, 282 | gl.RGB, 283 | gl.UNSIGNED_BYTE, 284 | (redData instanceof Uint8Array) ? redData : new Uint8Array(redData), 285 | ); 286 | 287 | gl.activeTexture(gl.TEXTURE0); 288 | gl.bindTexture(gl.TEXTURE_2D, textureRed); 289 | } else { 290 | gl.uniform1f(gl.getUniformLocation(program, 'u_maxValue'), getMaxValue(redData)); 291 | 292 | [textureRed, textureGreen, textureBlue] = [redData, greenData, blueData].map((data) => { 293 | const texture = gl.createTexture(); 294 | gl.bindTexture(gl.TEXTURE_2D, texture); 295 | 296 | gl.uniform1i(gl.getUniformLocation(program, 'u_textureRed'), 0); 297 | gl.uniform1i(gl.getUniformLocation(program, 'u_textureGreen'), 1); 298 | gl.uniform1i(gl.getUniformLocation(program, 'u_textureBlue'), 2); 299 | 300 | // Set the parameters so we can render any size image. 301 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 302 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 303 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 304 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 305 | 306 | // Upload the image into the texture. 307 | gl.texImage2D( 308 | gl.TEXTURE_2D, 309 | 0, 310 | gl.LUMINANCE, 311 | width, 312 | height, 313 | 0, 314 | gl.LUMINANCE, 315 | gl.FLOAT, 316 | data ? new Float32Array(data) : null, 317 | ); 318 | return texture; 319 | }); 320 | gl.activeTexture(gl.TEXTURE0); 321 | gl.bindTexture(gl.TEXTURE_2D, textureRed); 322 | gl.activeTexture(gl.TEXTURE1); 323 | gl.bindTexture(gl.TEXTURE_2D, textureGreen); 324 | gl.activeTexture(gl.TEXTURE2); 325 | gl.bindTexture(gl.TEXTURE_2D, textureBlue); 326 | } 327 | 328 | this.buildSteps.forEach((step, i) => step.bindUniforms(gl, program, pipeline[i])); 329 | 330 | gl.viewport(0, 0, width, height); 331 | gl.drawArrays(gl.TRIANGLES, 0, 6); 332 | 333 | if (isRGB) { 334 | gl.deleteTexture(textureRed); 335 | } else { 336 | // cleanup 337 | gl.deleteTexture(textureRed); 338 | gl.deleteTexture(textureGreen); 339 | gl.deleteTexture(textureBlue); 340 | } 341 | } 342 | 343 | buildPipelineProgram(gl) { 344 | const fragmentShaderSource = ` 345 | precision mediump float; 346 | // our textures 347 | uniform bool u_singleTexture; 348 | uniform float u_minValue; 349 | uniform float u_maxValue; 350 | uniform sampler2D u_textureRed; 351 | uniform sampler2D u_textureGreen; 352 | uniform sampler2D u_textureBlue; 353 | ${this.buildSteps.map(step => 354 | step.getUniformIds().map(uid => ` uniform float ${uid};`).join('\n'), 355 | ).join('\n')} 356 | // the texCoords passed in from the vertex shader. 357 | varying vec2 v_texCoord; 358 | 359 | ${Object.values(stepClasses).map(cls => cls.getFragmentSourceLib()).join('\n')} 360 | 361 | void main() { 362 | float red; 363 | float green; 364 | float blue; 365 | if (u_singleTexture) { 366 | vec4 value = texture2D(u_textureRed, v_texCoord); 367 | red = value.r / u_maxValue; 368 | green = value.g / u_maxValue; 369 | blue = value.b / u_maxValue; 370 | } else { 371 | red = texture2D(u_textureRed, v_texCoord)[0] / u_maxValue; 372 | green = texture2D(u_textureGreen, v_texCoord)[0] / u_maxValue; 373 | blue = texture2D(u_textureBlue, v_texCoord)[0] / u_maxValue; 374 | } 375 | 376 | ${this.buildSteps.map((step) => { 377 | if (step.bands === 'all') { 378 | return ` 379 | red = ${step.getCall('red')}; 380 | green = ${step.getCall('green')}; 381 | blue = ${step.getCall('blue')}; 382 | `; 383 | } 384 | return `${step.bands} = ${step.getCall(step.bands)};`; 385 | }).join('\n')} 386 | 387 | if (red == 0.0 && green == 0.0 && blue == 0.0) { 388 | discard; 389 | } 390 | gl_FragColor = vec4( 391 | red, 392 | green, 393 | blue, 394 | 1.0 395 | ); 396 | }`; 397 | return createProgram(gl, globalVertexShaderSource, fragmentShaderSource); 398 | } 399 | } 400 | 401 | export default class WebGLRenderer { 402 | static isSupported() { 403 | return create3DContext(document.createElement('canvas')) !== null; 404 | } 405 | 406 | constructor() { 407 | this.renderCanvas = document.createElement('canvas'); 408 | this.gl = create3DContext(this.renderCanvas); 409 | this.wrappers = {}; 410 | } 411 | 412 | render(canvas, pipeline, width, height, redData, greenData, blueData, isRGB) { 413 | try { 414 | const pipelineId = getPipelineId(pipeline); 415 | if (!this.wrappers[pipelineId]) { 416 | this.wrappers[pipelineId] = new PipelineProgramWrapper(this.gl, pipeline); 417 | } 418 | this.renderCanvas.width = width; 419 | this.renderCanvas.height = height; 420 | this.gl.clear(this.gl.DEPTH_BUFFER_BIT | this.gl.COLOR_BUFFER_BIT); 421 | 422 | this.wrappers[pipelineId].render( 423 | this.gl, pipeline, width, height, redData, greenData, blueData, isRGB, 424 | ); 425 | 426 | canvas.width = width; 427 | canvas.height = height; 428 | const ctx = canvas.getContext('2d'); 429 | ctx.drawImage(this.renderCanvas, 0, 0); 430 | } catch (e) { 431 | console.error(e); 432 | } 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /webpack.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const isProduction = (process.env.NODE_ENV === 'production'); 4 | 5 | module.exports = { 6 | entry: ['babel-polyfill', './src/index.jsx'], 7 | 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'app.bundle.js', 11 | globalObject: 'this', 12 | }, 13 | 14 | module: { 15 | rules: [ 16 | // test: /\.worker\.js$/, 17 | // use: [{ 18 | // loader: 'babel-loader', 19 | // // options are in .babelrc 20 | // }, { 21 | // loader: 'worker-loader', 22 | // options: { 23 | // inline: true, 24 | // }, 25 | // }], 26 | { 27 | test: /\.worker\.js$/, 28 | use: { 29 | loader: 'worker-loader', 30 | options: { 31 | name: isProduction ? '[hash].worker.min.js' : '[hash].worker.js', 32 | inline: true, 33 | fallback: true, 34 | }, 35 | }, 36 | }, { 37 | test: /\.jsx?$/, 38 | exclude: /(node_modules|bower_components)/, 39 | use: { 40 | loader: 'babel-loader', 41 | // options are in .babelrc 42 | }, 43 | }, { 44 | test: /\.css$/, 45 | use: [ 46 | { loader: 'style-loader' }, 47 | { loader: 'css-loader' }, 48 | ], 49 | }, 50 | ], 51 | }, 52 | 53 | resolve: { 54 | extensions: ['.js', '.jsx'], 55 | }, 56 | 57 | node: { 58 | fs: 'empty', 59 | }, 60 | 61 | devServer: { 62 | host: '0.0.0.0', 63 | port: 8091, 64 | inline: true, 65 | disableHostCheck: true, 66 | watchContentBase: true, 67 | overlay: { 68 | warnings: true, 69 | errors: true, 70 | }, 71 | }, 72 | 73 | devtool: 'source-map', 74 | cache: true, 75 | }; 76 | --------------------------------------------------------------------------------