├── .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 |
13 |
14 |
15 |
21 |
22 |
Howdie!
23 |
24 | Welcome to the COG-Explorer!
25 |
26 |
27 | This app allows you to visually inspect individual Cloud Optimized GeoTIFFs (COGs) like
28 | scenes from the Landsat-8 archive on Amazon S3.
29 | Either copy the URL to the COG or to the index.html
of the Landsat 8 scene you want to
30 | investigate in the "Custom URL" field and hit "Load URL or sample", or alternatively, show
31 | one of the preselected samples using the button. Note, while optimizing
32 | the data access by utilizing the COG features to only retrieve necessary parts of the data this
33 | app downloads the actual raster values, UInt16 for Landsat 8, which uses quite some download bandwidth.
34 | Additionally the data is download in its original projection and re-projected on the fly.
35 |
36 |
37 | The selected COG is displayed on the map, which you can zoom and pan as you
38 | like. It is visualized as natural color composite with some default processing
39 | steps applied. Using the
40 | button you can adjust the visualization by selecting the bands to use and processing
41 | steps to apply.
42 |
43 |
44 | For the band selection, either define red, green, and blue channels
45 | individually or use one of the predefined scenarios. As image processing steps
46 | currently sigmoidal contrast enhancement and gamma can be applied on all or
47 | individual channels.
48 |
49 |
50 | This app uses the geotiff.js
51 | library to read the actual raster values like UInt16 from the band files (or the overviews, depending on the maps
52 | zoomlevel). Taking advantage of the optimized structure of COGs, only the parts
53 | of the file that are actually required are requested, drastically reducing
54 | the overall amount of data transmitted from the archive.
55 |
56 |
57 | Please keep in mind that at the moment this app is a proof-of-concept demonstration
58 | with some room for improvement. Nevertheless we're looking forward to hear your feedback.
59 | Also let us know if you want to help in any way.
60 |
61 |
62 | If you want to know more about COGs please refer to their introduction page .
63 |
64 |
Proudly presented by:
65 |
66 |
67 |
68 |
69 |
70 |
71 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
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 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | COG-Explorer
60 |
61 | {
62 | errorMessage &&
63 |
{errorMessage}
73 | this.props.setError()}>
79 | ×
80 |
81 |
82 | }
83 |
0) ? 'visible' : 'hidden',
90 | zIndex: 99,
91 | }}
92 | />
93 |
94 |
95 |
96 |
97 |
112 |
113 |
114 |
115 |
116 |
117 |
this.setState({ showList: !showList })}
125 | disabled={scenes.length === 0}
126 | >
127 |
128 |
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 |
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 |
71 | Load URL or sample
72 |
73 |
79 | Toggle Dropdown
80 |
81 |
82 | this.props.addSceneFromIndex(example1Url)}
85 | disabled={!this.checkUrl(example1Url) || this.isLoading()}
86 | >
87 | Sentinel-2 RGB sample
88 |
89 | this.props.addSceneFromIndex(example2Url)}
92 | disabled={!this.checkUrl(example2Url) || this.isLoading()}
93 | >
94 | OpenAerialMap sample 1
95 |
96 | this.props.addSceneFromIndex(example3Url)}
99 | disabled={!this.checkUrl(example3Url) || this.isLoading()}
100 | >
101 | OpenAerialMap sample 2
102 |
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 |
115 | }
116 |
117 | {scene.pipeline.map(
118 | (step, index) => (
119 |
126 | ),
127 | )}
128 |
129 |
Add step
130 |
131 |
132 |
133 |
this.setState({ newStepType: event.target.value })}
136 | >
137 | ---
138 | sigmoidal-contrast
139 | gamma
140 |
141 |
142 | addStep(scene.id, this.state.newStepType)}
146 | disabled={this.state.newStepType === ''}
147 | >Add
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 |
Contrast:
17 |
18 |
editStep(sceneId, index, { contrast: e.target.value })}
26 | />
27 |
28 | {step.contrast}
29 |
30 |
31 |
32 |
33 |
Bias:
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 |
Value:
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 | indexStep(sceneId, index, index - 1)} disabled={index === 0}>
93 |
94 |
95 | indexStep(sceneId, index, index + 1)} disabled={isLast}>
96 |
97 |
98 | removeStep(sceneId, index)}>
99 |
100 |
101 |
102 |
103 |
104 |
105 | Channels:
106 | editStep(sceneId, index, { bands: e.target.value })}
110 | >
111 | all
112 | red
113 | green
114 | blue
115 |
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 |
--------------------------------------------------------------------------------