├── .firebaserc
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── firebase.json
├── iceland-vegetation.jpg
├── package.json
├── postcss.config.js
├── scripts
├── filter-iceland.js
└── make-vectors.sh
├── src
├── @types
│ └── geotiff
│ │ └── index.d.ts
├── assets
│ ├── atlas
│ │ ├── ndvi-anomaly.atlas
│ │ ├── ndvi-anomaly.atlas.json
│ │ ├── ndvi.atlas
│ │ └── ndvi.atlas.json
│ └── img
│ │ ├── fa-bars.svg
│ │ ├── fa-search-minus.svg
│ │ ├── fa-search-plus.svg
│ │ ├── fa-times.svg
│ │ ├── favicon.png
│ │ ├── iceland_ndvi.png
│ │ ├── ndvi-high.svg
│ │ ├── ndvi-low.svg
│ │ ├── ndvi-medium.svg
│ │ ├── select-arrow.svg
│ │ ├── vegetation.svg
│ │ └── vplogo.svg
├── js
│ ├── components
│ │ ├── App.tsx
│ │ ├── Container.tsx
│ │ ├── Footer.tsx
│ │ ├── Header.tsx
│ │ ├── Heading.tsx
│ │ ├── Incompatible.tsx
│ │ ├── InfoOverlay.tsx
│ │ ├── Loading.tsx
│ │ ├── Logo.tsx
│ │ ├── ModeSelect.tsx
│ │ ├── MouseElement.tsx
│ │ ├── RightNav.tsx
│ │ ├── SingleView.tsx
│ │ ├── SingleViewContainer.tsx
│ │ ├── SizedElement.tsx
│ │ ├── TouchElement.tsx
│ │ ├── Zoom.tsx
│ │ ├── icons
│ │ │ ├── Bars.tsx
│ │ │ ├── Comment.tsx
│ │ │ ├── FileText.tsx
│ │ │ ├── GitHub.tsx
│ │ │ └── Times.tsx
│ │ └── timeSeries
│ │ │ ├── Brush.tsx
│ │ │ ├── Chart.tsx
│ │ │ ├── Legend.tsx
│ │ │ ├── NDVI.tsx
│ │ │ ├── NDVIAnomaly.tsx
│ │ │ ├── NDVIAnomalySorted.tsx
│ │ │ ├── NDVISorted.tsx
│ │ │ ├── Series.tsx
│ │ │ ├── SeriesSorted.tsx
│ │ │ ├── XAxis.tsx
│ │ │ ├── XAxisSorted.tsx
│ │ │ ├── YAxisNDVI.tsx
│ │ │ └── YAxisNDVIAnomaly.tsx
│ ├── constants.ts
│ ├── gl
│ │ ├── BoxSelectView.ts
│ │ ├── GLManager.ts
│ │ ├── GLTest.ts
│ │ ├── OutlineView.ts
│ │ ├── RasterHeightGather.ts
│ │ ├── RasterMask.ts
│ │ ├── RasterView.ts
│ │ ├── RasterWidthGather.ts
│ │ ├── VectorView.ts
│ │ └── shaders
│ │ │ ├── functions
│ │ │ ├── atlasSample.glsl.ts
│ │ │ ├── atlasUV.glsl.ts
│ │ │ ├── colorScale.glsl.ts
│ │ │ ├── geoByteScale.glsl.ts
│ │ │ ├── isPointInBBox.glsl.ts
│ │ │ ├── lngLatToMercator.glsl.ts
│ │ │ ├── lngLatToSinusoidal.glsl.ts
│ │ │ ├── luma.glsl.ts
│ │ │ ├── mercatorToLngLat.glsl.ts
│ │ │ ├── ndviScale.glsl.ts
│ │ │ ├── pointInBBox.glsl.ts
│ │ │ └── sinusoidalToLngLat.glsl.ts
│ │ │ ├── gatherFragHeight.glsl.ts
│ │ │ ├── gatherFragWidth.glsl.ts
│ │ │ ├── gatherVert.glsl.ts
│ │ │ ├── landFrag.glsl.ts
│ │ │ ├── landVert.glsl.ts
│ │ │ ├── maskFrag.glsl.ts
│ │ │ ├── maskVert.glsl.ts
│ │ │ ├── outlineFrag.glsl.ts
│ │ │ ├── outlineVert.glsl.ts
│ │ │ ├── viewFrag.glsl.ts
│ │ │ └── viewVert.glsl.ts
│ ├── index.tsx
│ ├── models
│ │ ├── Atlas.ts
│ │ ├── BoundingBox.ts
│ │ ├── Camera.ts
│ │ ├── Point.ts
│ │ ├── RootStore.ts
│ │ └── VectorLayer.ts
│ ├── scales.ts
│ └── utils.ts
└── scss
│ └── styles.scss
├── tsconfig.json
├── tslint.json
├── webpack.common.js
├── webpack.dev.js
├── webpack.prod.js
├── webpack.stage.js
└── yarn.lock
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "stage": "iceland-stage"
4 | }
5 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.psd filter=lfs diff=lfs merge=lfs -text
2 | *.tif filter=lfs diff=lfs merge=lfs -text
3 | *.tiff filter=lfs diff=lfs merge=lfs -text
4 | *.atlas filter=lfs diff=lfs merge=lfs -text
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Numerous always-ignore extensions
2 | *.diff
3 | *.err
4 | *.orig
5 | *.log
6 | *.rej
7 | *.swo
8 | *.swp
9 | *.vi
10 | *~
11 | *.sass-cache
12 |
13 | # OS or Editor folders
14 | .DS_Store
15 | .cache
16 | .project
17 | .settings
18 | .tmproj
19 | nbproject
20 | Thumbs.db
21 | .exrc
22 |
23 | # NPM packages folder
24 | node_modules/
25 |
26 | # Brunch output folder
27 | public/
28 |
29 | # Temporary data folder
30 | tmpdata/
31 |
32 | # Generated files
33 | src/assets/geojson/
34 | dist/
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 VisualPerspective, LLC
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 |
2 |
3 |
4 |
5 | # NDVI Viewer
6 |
7 | Allows the analysis of NDVI over time for a region using regl, WebGL, and React. ([Live Demo](http://iceland.visualperspective.io/))
8 |
9 | *__Note:__ this is alpha software and requires more documentation and tests before being generally useful.*
10 |
11 | ### Running the project
12 | To run the project, you'll need:
13 | * Node 9+
14 | * a recent version of Yarn
15 | * git-lfs
16 | * ogr2ogr
17 |
18 | Then:
19 | * Clone this repo, and run `yarn` to install dependencies
20 | * Run `bash scripts/make-vectors.sh` to download and process geojson for Iceland
21 | * Run `yarn start` to start a development server
22 | * The app should be available at `http://localhost:8080` (initial page load will take a while for compilation)
23 |
24 | ### Data format
25 | The main data file, `src/assets/atlas/ndvi.atlas`, was produced using [geotiff-atlas](https://github.com/VisualPerspective/geotiff-atlas). Refer to the README of that repository for more information.
26 |
27 | ### Focus on Iceland data set
28 | For now, this viewer is focused on Iceland, to make it work with another region some changes and enhancements would need to be made:
29 | * Change various constants in `constants.ts`
30 | * Download a set of geotiffs for an area, and process them using https://github.com/VisualPerspective/geotiff-atlas
31 | * Make other tweaks/changes to make the code more general
32 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "dist",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/iceland-vegetation.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VisualPerspective/ndvi-viewer/aad996d893f21ca00c3012bc0515af1e5b3fab54/iceland-vegetation.jpg
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "iceland-ndvi",
3 | "version": "0.0.1",
4 | "main": "index.js",
5 | "license": "Commercial",
6 | "private": true,
7 | "dependencies": {
8 | "@types/earcut": "^2.1.0",
9 | "@types/lodash": "^4.14.110",
10 | "@types/node": "^12.7.11",
11 | "@types/react": "^16.4.4",
12 | "@types/react-dom": "^16.0.6",
13 | "@types/react-helmet": "^5.0.6",
14 | "autoprefixer": "^8.6.4",
15 | "axios": "^0.18.0",
16 | "babel-runtime": "^6.26.0",
17 | "copy-webpack-plugin": "^4.5.2",
18 | "css-loader": "^0.28.11",
19 | "d3": "^5.5.0",
20 | "d3-scale-chromatic": "^1.3.0",
21 | "earcut": "^2.1.3",
22 | "es6-promise": "^4.2.4",
23 | "file-loader": "^1.1.11",
24 | "geotiff": "1.0.0-beta.3",
25 | "gl-matrix": "^2.6.1",
26 | "glsl-colormap": "^1.0.1",
27 | "html-webpack-plugin": "^3.2.0",
28 | "lodash": "^4.17.10",
29 | "mini-css-extract-plugin": "^0.4.0",
30 | "mobx": "^4.3.1",
31 | "mobx-react": "^5.2.3",
32 | "mobx-react-devtools": "^5.0.1",
33 | "ndjson-cli": "^0.3.1",
34 | "node-sass": "^4.9.0",
35 | "postcss-loader": "^2.1.5",
36 | "range-slider-sass": "^2.0.0",
37 | "react": "^16.4.1",
38 | "react-dom": "^16.4.1",
39 | "react-helmet": "5.2.0",
40 | "reflect-metadata": "^0.1.12",
41 | "regl": "^1.3.7",
42 | "sass-loader": "^7.0.3",
43 | "shader-loader": "^1.3.1",
44 | "ts-loader": "^6.2.0",
45 | "tslint": "^5.10.0",
46 | "tslint-loader": "^3.6.0",
47 | "typescript": "^3.6.3",
48 | "viewport-mercator-project": "5.1.0",
49 | "webpack": "^4.12.2",
50 | "webpack-cli": "^3.0.8",
51 | "webpack-merge": "^4.1.3",
52 | "webpack-serve": "^1.0.4",
53 | "worker-loader": "^2.0.0"
54 | },
55 | "scripts": {
56 | "start": "webpack-serve --config webpack.dev.js --host 0.0.0.0 --static ./src/assets/",
57 | "build": "webpack --config webpack.prod.js",
58 | "build-stage": "webpack --config webpack.stage.js",
59 | "deploy": "yarn build && firebase deploy",
60 | "deploy-stage": "yarn build-stage && firebase deploy"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/scripts/filter-iceland.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash')
2 | var countries = require('../tmpdata/vectors.json')
3 | var iceland = _.find(countries.features, (feature) => (
4 | feature.properties.ADM0_A3 === 'ISL'
5 | ))
6 |
7 | console.log(JSON.stringify(iceland))
8 |
--------------------------------------------------------------------------------
/scripts/make-vectors.sh:
--------------------------------------------------------------------------------
1 | mkdir -p tmpdata
2 | cd tmpdata
3 |
4 | # countries
5 | if [ ! -f countries.zip ]; then
6 | curl "http://naciscdn.org/naturalearth/10m/cultural/ne_10m_admin_0_countries.zip" -o countries.zip
7 | fi
8 | unzip -o countries.zip
9 |
10 | mkdir -p ../src/assets/geojson/
11 |
12 | ogr2ogr -f GeoJSON -t_srs crs:84 vectors.json ne_10m_admin_0_countries.shp
13 |
14 | node ../scripts/filter-iceland.js > ../src/assets/geojson/vectors.json
15 |
--------------------------------------------------------------------------------
/src/@types/geotiff/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'geotiff' {
2 | export const fromArrayBuffer: (response: ArrayBuffer) => object
3 | export const Pool: any
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/atlas/ndvi-anomaly.atlas:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:76826b07a94864e2225621a360efea72ed46f33bc7a9d1bce09bf466dc489fd8
3 | size 53015040
4 |
--------------------------------------------------------------------------------
/src/assets/atlas/ndvi-anomaly.atlas.json:
--------------------------------------------------------------------------------
1 | {
2 | "approximateDownloadSize": 20800000,
3 | "format": "float32",
4 | "rasterWidth": 590,
5 | "rasterHeight": 416,
6 | "numRasters": 204,
7 | "rastersWide": 6,
8 | "rastersHigh": 9,
9 | "channels": 4,
10 | "boundingBox": [
11 | -1153751,
12 | 7029082,
13 | -607255,
14 | 7414791
15 | ]
16 | }
--------------------------------------------------------------------------------
/src/assets/atlas/ndvi.atlas:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:771d5893a396719741ec4430a9c8ddc129982602de05c21679954410cc060d8b
3 | size 53015040
4 |
--------------------------------------------------------------------------------
/src/assets/atlas/ndvi.atlas.json:
--------------------------------------------------------------------------------
1 | {
2 | "approximateDownloadSize": 20800000,
3 | "format": "float32",
4 | "rasterWidth": 590,
5 | "rasterHeight": 416,
6 | "numRasters": 204,
7 | "rastersWide": 6,
8 | "rastersHigh": 9,
9 | "channels": 4,
10 | "boundingBox": [
11 | -1153751,
12 | 7029082,
13 | -607255,
14 | 7414791
15 | ]
16 | }
--------------------------------------------------------------------------------
/src/assets/img/fa-bars.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/img/fa-search-minus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/img/fa-search-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/img/fa-times.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VisualPerspective/ndvi-viewer/aad996d893f21ca00c3012bc0515af1e5b3fab54/src/assets/img/favicon.png
--------------------------------------------------------------------------------
/src/assets/img/iceland_ndvi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VisualPerspective/ndvi-viewer/aad996d893f21ca00c3012bc0515af1e5b3fab54/src/assets/img/iceland_ndvi.png
--------------------------------------------------------------------------------
/src/assets/img/ndvi-high.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/img/ndvi-low.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/img/ndvi-medium.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/img/select-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/img/vegetation.svg:
--------------------------------------------------------------------------------
1 | Asset 1
--------------------------------------------------------------------------------
/src/assets/img/vplogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/js/components/App.tsx:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata'
2 | import * as React from 'react'
3 | import Container from '@app/components/Container'
4 | import RootStore from '@app/models/RootStore'
5 | import GLTest from '@app/gl/GLTest'
6 | import { Provider } from 'mobx-react'
7 |
8 | const rootStore: RootStore = new RootStore()
9 |
10 | // Check for needed JS and GL features
11 | try {
12 | // tslint:disable-next-line
13 | new GLTest()
14 |
15 | if (Array.prototype.find === undefined) {
16 | rootStore.compatible = false
17 | }
18 | } catch (e) {
19 | rootStore.compatible = false
20 | }
21 |
22 | if (rootStore.compatible) {
23 | rootStore.initialize()
24 | }
25 |
26 | const App = () => (
27 |
28 |
29 |
30 | )
31 |
32 | export default App
33 |
--------------------------------------------------------------------------------
/src/js/components/Container.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Helmet } from 'react-helmet'
3 | import { inject, observer } from 'mobx-react'
4 | import { strings } from '@app/constants'
5 | import InfoOverlay from '@app/components/InfoOverlay'
6 | import Header from '@app/components/Header'
7 | import Footer from '@app/components/Footer'
8 | import SingleViewContainer from '@app/components/SingleViewContainer'
9 | import Loading from '@app/components/Loading'
10 | import Incompatible from '@app/components/Incompatible'
11 | import RootStore from '@app/models/RootStore'
12 |
13 | const Container: React.SFC<{ rootStore?: RootStore }> = (props) => (
14 | <>
15 |
16 |
17 |
18 |
19 |
20 |
27 |
28 | {
29 | props.rootStore.compatible ? (
30 | props.rootStore.initialized ? (
31 | <>
32 |
33 |
34 |
35 | { props.rootStore.moreInfoOpen && }
36 | >
37 | ) : (
38 |
39 | )
40 | ) : (
41 |
42 | )
43 | }
44 | >
45 | )
46 |
47 | export default inject('rootStore')(observer(Container))
48 |
--------------------------------------------------------------------------------
/src/js/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { observer } from 'mobx-react'
3 | import SizedElement from '@app/components/SizedElement'
4 | import MouseElement from '@app/components/MouseElement'
5 | import TouchElement from '@app/components/TouchElement'
6 | import Chart from '@app/components/timeSeries/Chart'
7 |
8 | const Footer = () => (
9 |
10 | (
11 | (
16 | (
20 |
28 | )} />
29 | )} />
30 | )} />
31 |
32 | )
33 |
34 | export default observer(Footer)
35 |
--------------------------------------------------------------------------------
/src/js/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import RootStore from '@app/models/RootStore'
4 | import { strings } from '@app/constants'
5 | import ModeSelect from '@app/components/ModeSelect'
6 | import Heading from '@app/components/Heading'
7 | import RightNav from '@app/components/RightNav'
8 |
9 | const Header = ({ rootStore }: { rootStore?: RootStore }) => (
10 |
19 | )
20 |
21 | export default inject('rootStore')(observer(Header))
22 |
--------------------------------------------------------------------------------
/src/js/components/Heading.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { strings } from '@app/constants'
3 |
4 | const Heading = () => (
5 |
6 |
7 | {strings.HEADING}
8 |
9 | )
10 |
11 | export default Heading
12 |
--------------------------------------------------------------------------------
/src/js/components/Incompatible.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import constants, { strings } from '@app/constants'
4 | import RootStore from '@app/models/RootStore'
5 | import Heading from '@app/components/Heading'
6 |
7 | const Incompatible = ({ rootStore }: { rootStore?: RootStore }) => (
8 |
9 |
10 |
11 |
12 | {strings.APP_NAME} does not support certain
13 | browsers or mobile devices yet.
14 | Try Chrome, Firefox, or Edge.
15 | Also, you can
16 | read about this project on our blog.
17 |
18 |
19 |
20 | )
21 |
22 | export default inject('rootStore')(observer(Incompatible))
23 |
--------------------------------------------------------------------------------
/src/js/components/InfoOverlay.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import constants from '@app/constants'
4 | import RootStore from '@app/models/RootStore'
5 | import RightNav from '@app/components/RightNav'
6 | import Comment from '@app/components/icons/Comment'
7 | import FileText from '@app/components/icons/FileText'
8 | import GitHub from '@app/components/icons/GitHub'
9 | import ModeSelect from '@app/components/ModeSelect'
10 | import Logo from '@app/components/Logo'
11 |
12 | const InfoOverlay = ({ rootStore }: { rootStore?: RootStore }) => (
13 |
14 |
{
15 | rootStore.moreInfoOpen = false
16 | }} />
17 |
36 |
37 | )
38 |
39 | export default inject('rootStore')(observer(InfoOverlay))
40 |
--------------------------------------------------------------------------------
/src/js/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import RootStore from '@app/models/RootStore'
4 |
5 | const Loading: React.SFC<{ rootStore?: RootStore }> = ({ rootStore }) => (
6 |
7 |
8 | Loading...
9 |
11 |
13 |
14 |
15 | )
16 |
17 | export default inject('rootStore')(observer(Loading))
18 |
--------------------------------------------------------------------------------
/src/js/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | const Logo = () => (
4 |
9 | )
10 |
11 | export default Logo
12 |
--------------------------------------------------------------------------------
/src/js/components/ModeSelect.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import RootStore from '@app/models/RootStore'
4 | import { Modes } from '@app/constants'
5 |
6 | const ModeSelect: React.SFC<{ rootStore?: RootStore }> = ({ rootStore }) => (
7 |
{
9 | rootStore.mode = e.target.value
10 | }}>
11 | {
12 | Object.values(Modes).map(mode => (
13 | {mode}
14 | ))
15 | }
16 |
17 | )
18 |
19 | export default inject('rootStore')(observer(ModeSelect))
20 |
--------------------------------------------------------------------------------
/src/js/components/MouseElement.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { observer } from 'mobx-react'
3 | import { action, observable } from 'mobx'
4 | import Point from '@app/models/Point'
5 |
6 | class MouseElement extends React.Component<{
7 | render: ({}: {
8 | dragging: boolean,
9 | previousMousePosition?: Point,
10 | mousePosition?: Point,
11 | mouseTarget?: EventTarget,
12 | startDragging: (e: MouseEvent) => void,
13 | }) => React.ReactElement
,
14 | }, any> {
15 | @observable previousMousePosition: Point
16 | @observable mousePosition: Point
17 | @observable mouseTarget: EventTarget
18 | @observable dragging: boolean
19 |
20 | mouseUpListener = () => {
21 | this.dragging = false
22 | }
23 |
24 | @action mouseMoveListener = (e: MouseEvent) => {
25 | this.moveHandler(e.target, e.pageX, e.pageY)
26 | }
27 |
28 | moveHandler = (target: EventTarget, x: number, y: number) => {
29 | this.mouseTarget = target
30 | if (this.mousePosition !== undefined) {
31 | this.previousMousePosition.set(
32 | this.mousePosition.x,
33 | this.mousePosition.y
34 | )
35 | this.mousePosition.set(x, y)
36 | } else {
37 | this.previousMousePosition = new Point(x, y)
38 | this.mousePosition = new Point(x, y)
39 | }
40 | }
41 |
42 | @action startDragging (e: MouseEvent) {
43 | this.dragging = true
44 | }
45 |
46 | componentDidMount () {
47 | document.addEventListener('mouseup', this.mouseUpListener)
48 | document.addEventListener('mousemove', this.mouseMoveListener)
49 | }
50 |
51 | componentWillUnmount () {
52 | document.removeEventListener('mouseup', this.mouseUpListener)
53 | document.removeEventListener('mousemove', this.mouseMoveListener)
54 | }
55 |
56 | render () {
57 | return this.props.render({
58 | dragging: this.dragging,
59 | previousMousePosition: this.previousMousePosition,
60 | mousePosition: this.mousePosition,
61 | mouseTarget: this.mouseTarget,
62 | startDragging: this.startDragging.bind(this),
63 | })
64 | }
65 | }
66 |
67 | export default observer(MouseElement)
68 |
--------------------------------------------------------------------------------
/src/js/components/RightNav.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import RootStore from '@app/models/RootStore'
4 | import Heading from '@app/components/Heading'
5 | import Logo from '@app/components/Logo'
6 | import Times from '@app/components/icons/Times'
7 | import Bars from '@app/components/icons/Bars'
8 |
9 | const RightNav = ({ rootStore }: { rootStore?: RootStore }) => (
10 | { rootStore.moreInfoOpen = !rootStore.moreInfoOpen }}>
12 |
13 |
14 | {
15 | rootStore.moreInfoOpen
16 | ?
17 | :
18 | }
19 | More Info
20 |
21 |
22 |
23 | )
24 |
25 | export default inject('rootStore')(observer(RightNav))
26 |
--------------------------------------------------------------------------------
/src/js/components/SingleView.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { reaction, IReactionDisposer } from 'mobx'
3 | import { inject, observer } from 'mobx-react'
4 | import * as _ from 'lodash'
5 | import Point from '@app/models/Point'
6 | import RootStore from '@app/models/RootStore'
7 | import GLManager from '@app/gl/GLManager'
8 |
9 | class SingleView extends React.Component<{
10 | width: number,
11 | height: number,
12 | previousMousePosition: Point,
13 | mousePosition: Point,
14 | dragging: boolean,
15 | startDragging: (e: MouseEvent | TouchEvent) => void,
16 | pinching: boolean
17 | pinchScale: number
18 | panning: boolean
19 | previousPanPosition: Point
20 | panPosition: Point
21 | rootStore?: RootStore
22 | }, any> {
23 | dragReactionDisposer: IReactionDisposer
24 | glManager: GLManager
25 | canvasElement: React.RefObject = React.createRef()
26 |
27 | componentDidMount () {
28 | this.glManager = new GLManager({
29 | canvas: (this.canvasElement.current as HTMLCanvasElement),
30 | rootStore: this.props.rootStore,
31 | })
32 |
33 | this.dragReactionDisposer = reaction(() => ({
34 | mousePositionX: _.get(this.props.mousePosition, 'x'),
35 | mousePositionY: _.get(this.props.mousePosition, 'y'),
36 | }), () => {
37 | if (this.props.dragging) {
38 | this.handleMove(
39 | this.props.mousePosition,
40 | this.props.previousMousePosition,
41 | )
42 | }
43 | })
44 |
45 | this.dragReactionDisposer = reaction(() => ({
46 | panPositionX: _.get(this.props.panPosition, 'x'),
47 | panPositionY: _.get(this.props.panPosition, 'y'),
48 | }), () => {
49 | if (this.props.panning) {
50 | this.handleMove(
51 | this.props.panPosition,
52 | this.props.previousPanPosition,
53 | )
54 | }
55 | })
56 |
57 | window.addEventListener('wheel', this.handleWheel, { passive: false })
58 | }
59 |
60 | relativePosition (point: Point) {
61 | const canvasRect = this.canvasElement.current.getBoundingClientRect()
62 | return new Point(point.x - canvasRect.left, point.y - canvasRect.top)
63 | }
64 |
65 | handleMove = (position: Point, previousPosition: Point) => {
66 | if (
67 | position !== undefined &&
68 | previousPosition !== undefined
69 | ) {
70 | const fromPixel = this.relativePosition(previousPosition)
71 | const toPixel = this.relativePosition(position)
72 | const delta = this.props.rootStore.camera.lngLatDelta(fromPixel, toPixel)
73 |
74 | this.props.rootStore.camera.position.set(
75 | this.props.rootStore.camera.position.x - delta.x,
76 | this.props.rootStore.camera.position.y - delta.y
77 | )
78 | }
79 | }
80 |
81 | handleWheel = (e: WheelEvent) => {
82 | let amount = -e.deltaY * 0.001
83 |
84 | // Deal with Firefox mousewheel speed being slower
85 | if ((e as any).mozInputSource === 1 && e.deltaMode === 1) {
86 | amount *= 50
87 | }
88 |
89 | if (e.target === this.canvasElement.current) {
90 | this.props.rootStore.camera.zoom += amount
91 | e.preventDefault()
92 | }
93 | }
94 |
95 | componentWillUnmount () {
96 | window.removeEventListener('wheel', this.handleWheel)
97 | this.dragReactionDisposer()
98 | }
99 |
100 | componentDidUpdate () {
101 | this.glManager.render()
102 | }
103 |
104 | render () {
105 | return (
106 | {
110 | this.props.startDragging(e.nativeEvent)
111 | }}
112 | />
113 | )
114 | }
115 | }
116 |
117 | export default inject('rootStore')(observer(SingleView))
118 |
--------------------------------------------------------------------------------
/src/js/components/SingleViewContainer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { observer, inject } from 'mobx-react'
3 | import MouseElement from '@app/components/MouseElement'
4 | import TouchElement from '@app/components/TouchElement'
5 | import SizedElement from '@app/components/SizedElement'
6 | import SingleView from '@app/components/SingleView'
7 | import Zoom from '@app/components/Zoom'
8 | import RootStore from '@app/models/RootStore'
9 |
10 | const SingleViewContainer = ({ rootStore }: { rootStore?: RootStore }) => (
11 | (
12 | <>
13 | (
19 | {
20 | rootStore.camera.zoom += (ratio - 1) / 10
21 | }}
22 | render={({
23 | pinching,
24 | pinchScale,
25 | panning,
26 | previousPanPosition,
27 | panPosition,
28 | }) => (
29 |
41 | )} />
42 | )} />
43 |
44 | >
45 | )} />
46 | )
47 |
48 | export default inject('rootStore')(observer(SingleViewContainer))
49 |
--------------------------------------------------------------------------------
/src/js/components/SizedElement.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { observer } from 'mobx-react'
3 | import { action, observable } from 'mobx'
4 |
5 | class SizedElement extends React.Component<{
6 | className: string,
7 | render: ({}: {
8 | width: number,
9 | height: number,
10 | }) => React.ReactElement
11 | }, any> {
12 | container: HTMLDivElement
13 | @observable width: number
14 | @observable height: number
15 |
16 | componentDidMount () {
17 | this.handleSizeChange()
18 | window.addEventListener('resize', this.handleSizeChange)
19 | }
20 |
21 | componentWillUnmount () {
22 | window.removeEventListener('resize', this.handleSizeChange)
23 | }
24 |
25 | @action handleSizeChange = () => {
26 | this.width = this.container.offsetWidth
27 | this.height = this.container.offsetHeight
28 | }
29 |
30 | render () {
31 | return (
32 | this.container = ref
34 | }>
35 | {
36 | this.props.render({
37 | width: this.width,
38 | height: this.height,
39 | })
40 | }
41 |
42 | )
43 | }
44 | }
45 |
46 | export default observer(SizedElement)
47 |
--------------------------------------------------------------------------------
/src/js/components/TouchElement.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { observer } from 'mobx-react'
3 | import { action, observable } from 'mobx'
4 | import Point from '@app/models/Point'
5 |
6 | class TouchElement extends React.Component<{
7 | pinchHandler?: (ratio: number) => void,
8 | render: ({}: {
9 | pinching?: boolean,
10 | pinchScale?: number,
11 | panning: boolean,
12 | panPosition: Point,
13 | previousPanPosition: Point,
14 | touchTarget: EventTarget,
15 | }) => React.ReactElement,
16 | }, any> {
17 | @observable panning: boolean
18 | @observable pinching: boolean
19 | @observable pinchScale: number
20 | @observable previousDistance: number
21 | @observable touchTarget: EventTarget
22 | panPosition: Point
23 | previousPanPosition: Point
24 | pinchElement: React.RefObject = React.createRef()
25 |
26 | pinchDistance (e: TouchEvent) {
27 | const from = new Point(e.touches[0].pageX, e.touches[0].pageY)
28 | const to = new Point(e.touches[1].pageX, e.touches[1].pageY)
29 | return from.distanceTo(to)
30 | }
31 |
32 | @action touchesChangeListener = (e: TouchEvent) => {
33 | e.preventDefault()
34 | this.touchTarget = e.target
35 |
36 | const wasPinching = this.pinching
37 | this.pinching = (e.touches.length === 2)
38 |
39 | if (!wasPinching && this.pinching) {
40 | this.previousDistance = this.pinchDistance(e)
41 | }
42 |
43 | const wasPanning = this.panning
44 | this.panning = (e.touches.length === 1)
45 |
46 | if (!wasPanning && this.panning) {
47 | const x = e.touches[0].pageX
48 | const y = e.touches[0].pageY
49 | this.previousPanPosition = new Point(x, y)
50 | this.panPosition = new Point(x, y)
51 | }
52 | }
53 |
54 | @action touchMoveListener = (e: TouchEvent) => {
55 | e.preventDefault()
56 | if (this.pinching) {
57 | const newDistance = this.pinchDistance(e)
58 | if (this.props.pinchHandler !== undefined) {
59 | this.props.pinchHandler(newDistance / this.previousDistance)
60 | }
61 | this.previousDistance = newDistance
62 | } else if (this.panning) {
63 | const target = document.elementFromPoint(
64 | e.touches[0].clientX,
65 | e.touches[0].clientY
66 | )
67 |
68 | this.moveHandler(target, e.touches[0].pageX, e.touches[0].pageY)
69 | }
70 | }
71 |
72 | moveHandler = (target: EventTarget, x: number, y: number) => {
73 | this.touchTarget = target
74 | if (this.panPosition !== undefined) {
75 | this.previousPanPosition.set(
76 | this.panPosition.x,
77 | this.panPosition.y
78 | )
79 | this.panPosition.set(x, y)
80 | } else {
81 | this.previousPanPosition = new Point(x, y)
82 | this.panPosition = new Point(x, y)
83 | }
84 | }
85 |
86 | componentDidMount () {
87 | const el = this.pinchElement.current
88 | el.addEventListener('touchstart', this.touchesChangeListener)
89 | el.addEventListener('touchend', this.touchesChangeListener)
90 | el.addEventListener('touchmove', this.touchMoveListener)
91 | }
92 |
93 | componentWillUnmount () {
94 | const el = this.pinchElement.current
95 | el.removeEventListener('touchstart', this.touchesChangeListener)
96 | el.removeEventListener('touchend', this.touchesChangeListener)
97 | el.addEventListener('touchmove', this.touchMoveListener)
98 | }
99 |
100 | render () {
101 | return (
102 |
103 | {
104 | this.props.render({
105 | pinching: this.pinching,
106 | pinchScale: this.pinchScale,
107 | panning: this.panning,
108 | panPosition: this.panPosition,
109 | previousPanPosition: this.previousPanPosition,
110 | touchTarget: this.touchTarget,
111 | })
112 | }
113 |
114 | )
115 | }
116 | }
117 |
118 | export default observer(TouchElement)
119 |
--------------------------------------------------------------------------------
/src/js/components/Zoom.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import RootStore from '@app/models/RootStore'
4 |
5 | const Zoom: React.SFC<{ rootStore?: RootStore }> = ({ rootStore }) => (
6 |
21 | )
22 |
23 | export default inject('rootStore')(observer(Zoom))
24 |
--------------------------------------------------------------------------------
/src/js/components/icons/Bars.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | // tslint:disable
4 |
5 | export default () => (
6 |
7 |
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/src/js/components/icons/Comment.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | // tslint:disable
4 |
5 | export default () => (
6 |
7 |
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/src/js/components/icons/FileText.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | // tslint:disable
4 |
5 | export default () => (
6 |
7 |
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/src/js/components/icons/GitHub.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | // tslint:disable
4 |
5 | export default () => (
6 |
7 |
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/src/js/components/icons/Times.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | // tslint:disable
4 |
5 | export default () => (
6 |
7 |
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/Brush.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import constants from '@app/constants'
4 | import RootStore from '@app/models/RootStore'
5 | import { translate } from '@app/utils'
6 |
7 | const Brush = ({ xScale, yScale, rootStore }: {
8 | xScale: any,
9 | yScale: any,
10 | rootStore?: RootStore,
11 | }) => {
12 | const label = (
13 | constants.MONTHS[rootStore.timePeriod % 12] + ' ' +
14 | (Number(constants.START_YEAR) + Math.floor(rootStore.timePeriod / 12))
15 | )
16 |
17 | return (
18 |
20 |
21 |
22 |
23 |
24 |
25 | {label}
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default inject('rootStore')(observer(Brush))
33 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/Chart.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import { reaction, IReactionDisposer } from 'mobx'
4 | import { Modes } from '@app/constants'
5 | import RootStore from '@app/models/RootStore'
6 | import NDVI from '@app/components/timeSeries/NDVI'
7 | import NDVISorted from '@app/components/timeSeries/NDVISorted'
8 | import NDVIAnomaly from '@app/components/timeSeries/NDVIAnomaly'
9 | import NDVIAnomalySorted from '@app/components/timeSeries/NDVIAnomalySorted'
10 |
11 | class Chart extends React.Component<{
12 | width: number,
13 | height: number,
14 | mouseTarget: EventTarget,
15 | touchTarget: EventTarget,
16 | dragging: boolean,
17 | panning: boolean,
18 | startDragging: (e: MouseEvent) => void,
19 | rootStore?: RootStore,
20 | }> {
21 | dragReactionDisposer: IReactionDisposer
22 | panReactionDisposer: IReactionDisposer
23 | chart: SVGElement
24 |
25 | margin = {
26 | top: 25,
27 | bottom: 40,
28 | left: 60,
29 | right: 10,
30 | }
31 |
32 | componentDidMount () {
33 | this.dragReactionDisposer = reaction(() => ({
34 | target: this.props.mouseTarget,
35 | }), () => {
36 | if (this.props.dragging) {
37 | this.handleBrushMove(this.props.mouseTarget)
38 | }
39 | })
40 |
41 | this.panReactionDisposer = reaction(() => ({
42 | target: this.props.touchTarget,
43 | }), () => {
44 | if (this.props.panning) {
45 | this.handleBrushMove(this.props.touchTarget)
46 | }
47 | })
48 | }
49 |
50 | handleBrushMove (target: any) {
51 | if (
52 | target !== undefined &&
53 | target.getAttribute !== undefined
54 | ) {
55 | const timePeriod = parseInt(target.getAttribute('data-time-period'), 10)
56 |
57 | if (!isNaN(timePeriod)) {
58 | this.props.rootStore.timePeriod = timePeriod
59 | }
60 | }
61 | }
62 |
63 | render () {
64 | const {
65 | width,
66 | height,
67 | rootStore,
68 | } = this.props
69 |
70 | const onTimePeriodSelect = (i: number, isClick: boolean) => {
71 | if (this.props.dragging === true || isClick) {
72 | this.props.rootStore.timePeriod = i
73 | }
74 | }
75 |
76 | let ContainerComponent
77 | switch (rootStore.mode) {
78 | case Modes.NDVI:
79 | ContainerComponent = NDVI
80 | break
81 | case Modes.NDVI_GROUPED:
82 | ContainerComponent = NDVISorted
83 | break
84 | case Modes.NDVI_ANOMALY:
85 | ContainerComponent = NDVIAnomaly
86 | break
87 | case Modes.NDVI_ANOMALY_GROUPED:
88 | ContainerComponent = NDVIAnomalySorted
89 | break
90 | }
91 |
92 | return (width && height) ? (
93 | this.chart = ref}
94 | onMouseDown={(e) => {
95 | this.props.startDragging(e.nativeEvent)
96 | }}>
97 |
102 |
103 | ) : null
104 | }
105 | }
106 |
107 | export default inject('rootStore')(observer(Chart))
108 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/Legend.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import { format } from 'd3'
4 | import constants from '@app/constants'
5 | import RootStore from '@app/models/RootStore'
6 | import { translate } from '@app/utils'
7 |
8 | const Legend = ({ xScale, yScale, colorScale, rootStore }: {
9 | xScale: any,
10 | yScale: any,
11 | colorScale: any,
12 | rootStore?: RootStore,
13 | }) => {
14 | const label = (
15 | constants.MONTHS[rootStore.timePeriod % 12] + ' ' +
16 | (Number(constants.START_YEAR) + Math.floor(rootStore.timePeriod / 12))
17 | )
18 |
19 | const formatter = format('.2f')
20 | const mean = rootStore.timePeriodAverages[rootStore.timePeriod]
21 |
22 | return (
23 |
27 |
28 |
29 |
30 |
31 |
32 | Average {rootStore.modeConfig.DATA_LABEL} in
33 | selected region for {label}:
34 | {formatter(mean)}
35 |
36 |
37 | )
38 | }
39 |
40 | export default inject('rootStore')(observer(Legend))
41 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/NDVI.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import RootStore from '@app/models/RootStore'
3 | import { inject, observer } from 'mobx-react'
4 | import Legend from '@app/components/timeSeries/Legend'
5 | import XAxis from '@app/components/timeSeries/XAxis'
6 | import YAxisNDVI from '@app/components/timeSeries/YAxisNDVI'
7 | import Brush from '@app/components/timeSeries/Brush'
8 | import Series from '@app/components/timeSeries/Series'
9 | import {
10 | makeXScale,
11 | makeYScaleNDVI,
12 | makeColorScaleNDVI,
13 | } from '@app/scales'
14 |
15 | const NDVI = ({
16 | onTimePeriodSelect,
17 | width,
18 | height,
19 | margin,
20 | rootStore,
21 | }: {
22 | onTimePeriodSelect: any,
23 | width: number,
24 | height: number,
25 | margin: any
26 | rootStore?: RootStore
27 | }) => {
28 | const xScale = makeXScale({
29 | numTimePeriods: rootStore.numTimePeriods,
30 | width,
31 | margin,
32 | })
33 |
34 | const yScale = makeYScaleNDVI({
35 | height,
36 | margin,
37 | })
38 |
39 | const colorScale = makeColorScaleNDVI()
40 |
41 | return (
42 | <>
43 |
47 |
51 |
54 |
57 |
63 | >
64 | )
65 | }
66 |
67 | export default inject('rootStore')(observer(NDVI))
68 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/NDVIAnomaly.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import RootStore from '@app/models/RootStore'
3 | import { inject, observer } from 'mobx-react'
4 | import Legend from '@app/components/timeSeries/Legend'
5 | import XAxis from '@app/components/timeSeries/XAxis'
6 | import YAxisNDVIAnomaly from '@app/components/timeSeries/YAxisNDVIAnomaly'
7 | import Brush from '@app/components/timeSeries/Brush'
8 | import Series from '@app/components/timeSeries/Series'
9 | import {
10 | makeXScale,
11 | makeYScaleNDVIAnomaly,
12 | makeColorScaleNDVIAnomaly,
13 | } from '@app/scales'
14 |
15 | const NDVIAnomaly = ({
16 | onTimePeriodSelect,
17 | width,
18 | height,
19 | margin,
20 | rootStore,
21 | }: {
22 | onTimePeriodSelect: any,
23 | width: number,
24 | height: number,
25 | margin: any
26 | rootStore?: RootStore
27 | }) => {
28 | const xScale = makeXScale({
29 | numTimePeriods: rootStore.numTimePeriods,
30 | width,
31 | margin,
32 | })
33 |
34 | const yScale = makeYScaleNDVIAnomaly({
35 | height,
36 | margin,
37 | })
38 |
39 | const colorScale = makeColorScaleNDVIAnomaly()
40 |
41 | return (
42 | <>
43 |
47 |
51 |
54 |
57 |
63 | >
64 | )
65 | }
66 |
67 | export default inject('rootStore')(observer(NDVIAnomaly))
68 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/NDVIAnomalySorted.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import RootStore from '@app/models/RootStore'
3 | import { inject, observer } from 'mobx-react'
4 | import Legend from '@app/components/timeSeries/Legend'
5 | import XAxisSorted from '@app/components/timeSeries/XAxisSorted'
6 | import YAxisNDVIAnomaly from '@app/components/timeSeries/YAxisNDVIAnomaly'
7 | import Brush from '@app/components/timeSeries/Brush'
8 | import SeriesSorted from '@app/components/timeSeries/SeriesSorted'
9 | import {
10 | makeXScaleSorted,
11 | makeXScaleSortedBands,
12 | makeYScaleNDVIAnomaly,
13 | makeColorScaleNDVIAnomaly,
14 | } from '@app/scales'
15 |
16 | const NDVIAnomalySorted = ({
17 | onTimePeriodSelect,
18 | width,
19 | height,
20 | margin,
21 | rootStore,
22 | }: {
23 | onTimePeriodSelect: any
24 | width: number
25 | height: number
26 | margin: any
27 | rootStore?: RootStore
28 | }) => {
29 | const xScaleSortedBands = makeXScaleSortedBands({
30 | width,
31 | margin,
32 | })
33 |
34 | const xScale = makeXScaleSorted({
35 | numTimePeriods: rootStore.numTimePeriods,
36 | width,
37 | margin,
38 | })
39 |
40 | const yScale = makeYScaleNDVIAnomaly({
41 | height,
42 | margin,
43 | })
44 |
45 | const colorScale = makeColorScaleNDVIAnomaly()
46 |
47 | return (
48 | <>
49 |
53 |
57 |
60 |
63 |
69 | >
70 | )
71 | }
72 |
73 | export default inject('rootStore')(observer(NDVIAnomalySorted))
74 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/NDVISorted.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import RootStore from '@app/models/RootStore'
3 | import { inject, observer } from 'mobx-react'
4 | import Legend from '@app/components/timeSeries/Legend'
5 | import XAxisSorted from '@app/components/timeSeries/XAxisSorted'
6 | import YAxisNDVI from '@app/components/timeSeries/YAxisNDVI'
7 | import Brush from '@app/components/timeSeries/Brush'
8 | import SeriesSorted from '@app/components/timeSeries/SeriesSorted'
9 | import {
10 | makeXScaleSorted,
11 | makeXScaleSortedBands,
12 | makeYScaleNDVI,
13 | makeColorScaleNDVI,
14 | } from '@app/scales'
15 |
16 | const NDVISorted = ({
17 | onTimePeriodSelect,
18 | width,
19 | height,
20 | margin,
21 | rootStore,
22 | }: {
23 | onTimePeriodSelect: any
24 | width: number
25 | height: number
26 | margin: any
27 | rootStore?: RootStore
28 | }) => {
29 | const xScaleSortedBands = makeXScaleSortedBands({
30 | width,
31 | margin,
32 | })
33 |
34 | const xScale = makeXScaleSorted({
35 | numTimePeriods: rootStore.numTimePeriods,
36 | width,
37 | margin,
38 | })
39 |
40 | const yScale = makeYScaleNDVI({
41 | height,
42 | margin,
43 | })
44 |
45 | const colorScale = makeColorScaleNDVI()
46 |
47 | return (
48 | <>
49 |
53 |
57 |
60 |
63 |
69 | >
70 | )
71 | }
72 |
73 | export default inject('rootStore')(observer(NDVISorted))
74 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/Series.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import RootStore from '@app/models/RootStore'
4 | import { translate } from '@app/utils'
5 | import * as _ from 'lodash'
6 |
7 | const Series = ({
8 | xScale,
9 | yScale,
10 | colorScale,
11 | onTimePeriodSelect,
12 | marginBottom,
13 | rootStore,
14 | }: {
15 | xScale: any,
16 | yScale: any,
17 | colorScale: any,
18 | onTimePeriodSelect: any,
19 | marginBottom: number,
20 | rootStore?: RootStore,
21 | }) => (
22 |
23 | {
24 | _.times(rootStore.numTimePeriods, i => (
25 | rootStore.timePeriodAverages[i] !== undefined && (
26 |
27 |
32 |
33 |
34 |
35 | )
36 | ))
37 | }
38 | {
39 | _.times(rootStore.numTimePeriods, i => (
40 | rootStore.timePeriodAverages[i] !== undefined && (
41 |
42 |
47 |
49 |
50 |
57 |
58 | )
59 | ))
60 | }
61 |
62 | )
63 |
64 | export default inject('rootStore')(observer(Series))
65 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/SeriesSorted.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import RootStore from '@app/models/RootStore'
4 | import { translate } from '@app/utils'
5 | import * as _ from 'lodash'
6 |
7 | const SeriesSorted = ({
8 | xScale,
9 | yScale,
10 | colorScale,
11 | onTimePeriodSelect,
12 | marginBottom,
13 | rootStore,
14 | }: {
15 | xScale: any,
16 | yScale: any,
17 | colorScale: any,
18 | onTimePeriodSelect: any,
19 | marginBottom: number,
20 | rootStore?: RootStore,
21 | }) => (
22 |
23 | {
24 | _.times(rootStore.numTimePeriods, i => {
25 | const period = rootStore.timePeriodsByMonth[i]
26 |
27 | return (
28 |
29 | {
30 | period.average !== undefined && (
31 |
36 |
37 |
38 | )
39 | }
40 |
41 | )
42 | })
43 | }
44 | {
45 | _.times(rootStore.numTimePeriods, i => {
46 | const period = rootStore.timePeriodsByMonth[i]
47 |
48 | return (
49 |
50 | {
51 | period.average !== undefined && (
52 |
57 |
59 |
60 | )
61 | }
62 |
69 |
70 | )
71 | })
72 | }
73 |
74 | )
75 |
76 | export default inject('rootStore')(observer(SeriesSorted))
77 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/XAxis.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import * as _ from 'lodash'
4 | import RootStore from '@app/models/RootStore'
5 | import { translate } from '@app/utils'
6 | import constants from '@app/constants'
7 |
8 | const XAxis = ({ xScale, yScale, rootStore }: {
9 | xScale: any,
10 | yScale: any,
11 | rootStore?: RootStore,
12 | }) => (
13 |
14 | {
15 | _.times(rootStore.numTimePeriods / 12, i => (
16 |
18 |
22 | {
23 | constants.START_YEAR + i
24 | }
25 |
26 |
28 |
29 | ))
30 | }
31 |
32 | )
33 |
34 | export default inject('rootStore')(observer(XAxis))
35 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/XAxisSorted.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import * as _ from 'lodash'
4 | import RootStore from '@app/models/RootStore'
5 | import { translate } from '@app/utils'
6 | import constants from '@app/constants'
7 |
8 | const XAxisSorted = ({ xScale, yScale, rootStore }: {
9 | xScale: any,
10 | yScale: any,
11 | rootStore?: RootStore,
12 | }) => (
13 |
14 | {
15 | _.map(constants.MONTHS, (month, i) => (
16 |
21 |
25 | {month}
26 |
27 |
31 |
32 | ))
33 | }
34 |
39 |
43 |
44 |
45 | )
46 |
47 | export default inject('rootStore')(observer(XAxisSorted))
48 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/YAxisNDVI.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import { format, interpolateNumber } from 'd3'
4 | import * as _ from 'lodash'
5 | import { translate } from '@app/utils'
6 | import RootStore from '@app/models/RootStore'
7 |
8 | const YAxisNDVI = ({ xScale, yScale, colorScale, rootStore }: {
9 | xScale: any,
10 | yScale: any,
11 | colorScale: any,
12 | rootStore?: RootStore
13 | }) => {
14 | const leftScaleWidth = 7
15 | const numStops = 10
16 | const stops = _.times(numStops, i => ({
17 | color: colorScale(interpolateNumber(
18 | ...rootStore.modeConfig.CHART_RANGE,
19 | )(i / (numStops - 1))),
20 | percent: i / (numStops - 1) * 100,
21 | }))
22 |
23 | const formatter = format('.1f')
24 |
25 | return (
26 |
27 |
28 |
29 | {
30 | stops.map(stop => (
31 |
37 | ))
38 | }
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
63 |
64 |
65 |
66 |
67 |
72 | {
73 | rootStore.modeConfig.Y_TICKS.map((tick, i) => (
74 |
81 |
84 | 0 && i < rootStore.modeConfig.Y_TICKS.length - 1 ? 'white' : ''
86 | }
87 | x2={leftScaleWidth}
88 | y2='0.5' />
89 | {formatter(tick)}
90 |
91 | ))
92 | }
93 |
94 | )
95 | }
96 |
97 | export default inject('rootStore')(observer(YAxisNDVI))
98 |
--------------------------------------------------------------------------------
/src/js/components/timeSeries/YAxisNDVIAnomaly.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { inject, observer } from 'mobx-react'
3 | import RootStore from '@app/models/RootStore'
4 | import { format, interpolateNumber } from 'd3'
5 | import * as _ from 'lodash'
6 | import { translate } from '@app/utils'
7 |
8 | const YAxisNDVIAnomaly = ({ xScale, yScale, colorScale, rootStore }: {
9 | xScale: any,
10 | yScale: any,
11 | colorScale: any,
12 | rootStore?: RootStore,
13 | }) => {
14 | const leftScaleWidth = 7
15 | const numStops = 10
16 | const stops = _.times(numStops, i => ({
17 | color: colorScale(interpolateNumber(
18 | ...rootStore.modeConfig.CHART_RANGE,
19 | )(i / (numStops - 1))),
20 | percent: i / (numStops - 1) * 100,
21 | }))
22 |
23 | const formatter = format('.1f')
24 |
25 | return (
26 |
27 |
28 |
29 | {
30 | stops.map(stop => (
31 |
37 | ))
38 | }
39 |
40 |
41 |
46 | {
47 | rootStore.modeConfig.Y_TICKS.map((tick, i) => (
48 |
55 |
58 | 0 && i < rootStore.modeConfig.Y_TICKS.length - 1 ? 'white' : ''
60 | }
61 | x2={leftScaleWidth}
62 | y2='0.5' />
63 | {formatter(tick)}
64 |
65 | ))
66 | }
67 |
68 | )
69 | }
70 |
71 | export default inject('rootStore')(observer(YAxisNDVIAnomaly))
72 |
--------------------------------------------------------------------------------
/src/js/constants.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 |
3 | export const strings = {
4 | HEADING: 'Iceland Vegetation',
5 | APP_NAME: 'Iceland Vegetation viewer',
6 | MODE_SELECT_LABEL: 'View:',
7 | }
8 |
9 | export enum Modes {
10 | NDVI = 'NDVI',
11 | NDVI_GROUPED = 'NDVI By Month',
12 | NDVI_ANOMALY = 'NDVI Anomaly',
13 | NDVI_ANOMALY_GROUPED = 'NDVI Anomaly By Month',
14 | }
15 |
16 | const NDVI_CONFIG = {
17 | ATLAS: process.env.NDVI_ATLAS,
18 | ATLAS_CONFIG: 'atlas/ndvi.atlas.json',
19 | Y_TICKS: [ -0.2, 0, 0.2, 0.4, 0.6, 0.8, 1.0 ],
20 | RANGE: [-0.2, 1.0],
21 | CHART_RANGE: [-0.2, 1.0],
22 | NO_DATA_COLOR: [0.2, 0.2, 0.2, 1.0],
23 | SELECTED_COLOR: [1, 1, 1, 1],
24 | UNSELECTED_COLOR: [0.6, 0.6, 0.6, 1],
25 | DATA_LABEL: 'NDVI',
26 | LEGEND_OFFSET: 155,
27 | }
28 |
29 | const NDVI_ANOMALY_CONFIG = {
30 | ATLAS: process.env.NDVI_ANOMALY_ATLAS,
31 | ATLAS_CONFIG: 'atlas/ndvi-anomaly.atlas.json',
32 | Y_TICKS: [ -0.6, -0.4, -0.2, 0, 0.2, 0.4, 0.6 ],
33 | RANGE: [-1.2, 1.2],
34 | CHART_RANGE: [-0.6, 0.6],
35 | NO_DATA_COLOR: [1, 1, 1, 1],
36 | SELECTED_COLOR: [0.4, 0.4, 0.4, 1],
37 | UNSELECTED_COLOR: [0.6, 0.6, 0.6, 1],
38 | DATA_LABEL: 'NDVI anomaly',
39 | LEGEND_OFFSET: 195,
40 | }
41 |
42 | export default {
43 | BLOG_URL: process.env.SITE_URL + '/blog/iceland-ndvi-viewer',
44 | BLOG_ANOMALY_URL: process.env.SITE_URL + '/blog/iceland-ndvi-anomaly/',
45 | GITHUB_URL: 'https://github.com/VisualPerspective/ndvi-viewer/',
46 | CONTACT_US_URL: process.env.SITE_URL + '/contact',
47 | VECTOR_URL: '/geojson/vectors.json',
48 | DATA_TEXTURE_SIZE: 4096,
49 | NO_DATA_THRESHOLD: 0.001,
50 | NO_DATA_VALUE: 0,
51 | TILE_SIZE: 512,
52 | MONTHS: [
53 | 'Jan',
54 | 'Feb',
55 | 'Mar',
56 | 'Apr',
57 | 'May',
58 | 'June',
59 | 'July',
60 | 'Aug',
61 | 'Sept',
62 | 'Oct',
63 | 'Nov',
64 | 'Dec',
65 | ],
66 | START_YEAR: 2001,
67 | START_TIME_PERIOD: 6,
68 | SELECTED_BOX_PADDING: 20,
69 | PROFILE: JSON.parse(process.env.PROFILE),
70 | MODE_CONFIGS: {
71 | [Modes.NDVI]: {
72 | ...NDVI_CONFIG,
73 | },
74 | [Modes.NDVI_GROUPED]: {
75 | ...NDVI_CONFIG,
76 | },
77 | [Modes.NDVI_ANOMALY]: {
78 | ...NDVI_ANOMALY_CONFIG,
79 | },
80 | [Modes.NDVI_ANOMALY_GROUPED]: {
81 | ...NDVI_ANOMALY_CONFIG,
82 | },
83 | },
84 | DATA_TEXTURE_OPTIONS: ({
85 | type: 'float',
86 | format: 'rgba',
87 | min: 'nearest',
88 | mag: 'nearest',
89 | mipmap: false,
90 | wrapS: 'clamp',
91 | wrapT: 'clamp',
92 | flipY: false,
93 | } as REGL.Texture2DOptions),
94 | DATA_SQUARE_POSITIONS: [
95 | [-1, -1],
96 | [1, -1],
97 | [1, 1],
98 | [-1, -1],
99 | [1, 1],
100 | [-1, 1],
101 | ],
102 | GL_EXTENSIONS: [
103 | 'OES_texture_float',
104 | 'OES_element_index_uint',
105 | ],
106 | }
107 |
--------------------------------------------------------------------------------
/src/js/gl/BoxSelectView.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import { mat4 } from 'gl-matrix'
3 | import RootStore from '@app/models/RootStore'
4 |
5 | import vert from '@app/gl/shaders/outlineVert'
6 | import frag from '@app/gl/shaders/outlineFrag'
7 |
8 | interface IUniforms {
9 | selectedColor: number[]
10 | unselectedColor: number[]
11 | model: REGL.Mat4
12 | view: REGL.Mat4
13 | projection: REGL.Mat4
14 | scale: number
15 | selectedBBoxLngLat: number[]
16 | }
17 |
18 | interface IAttributes {
19 | position: number[]
20 | }
21 |
22 | interface IProps {
23 | selectedColor: number[]
24 | unselectedColor: number[]
25 | view: REGL.Mat4
26 | projection: REGL.Mat4
27 | scale: number
28 | vertices: number[][]
29 | linesLength: number
30 | selectedBBoxLngLat: number[]
31 | }
32 |
33 | class BoxSelectView {
34 | renderer: any
35 | ctx: REGL.Regl
36 | rootStore: RootStore
37 |
38 | constructor ({
39 | ctx,
40 | rootStore,
41 | }: {
42 | ctx: REGL.Regl,
43 | rootStore?: RootStore,
44 | }) {
45 | this.ctx = ctx
46 | this.rootStore = rootStore
47 |
48 | this.renderer = ctx({
49 | frag: frag(),
50 | vert: vert(),
51 | attributes: {
52 | position: ctx.prop('vertices'),
53 | },
54 | uniforms: {
55 | selectedColor: ctx.prop('selectedColor'),
56 | unselectedColor: ctx.prop('unselectedColor'),
57 | model: mat4.fromTranslation([], [0, 0, 0]),
58 | view: ctx.prop('view'),
59 | projection: ctx.prop('projection'),
60 | scale: ctx.prop('scale'),
61 | selectedBBoxLngLat: ctx.prop('selectedBBoxLngLat'),
62 | },
63 | depth: {
64 | enable: false,
65 | },
66 | count: ctx.prop('linesLength'),
67 | primitive: 'lines',
68 | })
69 | }
70 |
71 | render () {
72 | this.renderer({
73 | ...(this.rootStore.camera.renderInfo),
74 | vertices: this.rootStore.selectedBox.outline,
75 | linesLength: this.rootStore.selectedBox.outline.length,
76 | selectedBBoxLngLat: this.rootStore.selectedBox.array,
77 | selectedColor: this.rootStore.modeConfig.SELECTED_COLOR,
78 | unselectedColor: this.rootStore.modeConfig.SELECTED_COLOR,
79 | })
80 | }
81 | }
82 |
83 | export default BoxSelectView
84 |
--------------------------------------------------------------------------------
/src/js/gl/GLManager.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import * as _ from 'lodash'
3 | import { reaction } from 'mobx'
4 | import RasterView from '@app/gl/RasterView'
5 | import RasterMask from '@app/gl/RasterMask'
6 | import RasterWidthGather from '@app/gl/RasterWidthGather'
7 | import RasterHeightGather from '@app/gl/RasterHeightGather'
8 | import VectorView from '@app/gl/VectorView'
9 | import OutlineView from '@app/gl/OutlineView'
10 | import BoxSelectView from '@app/gl/BoxSelectView'
11 | import RootStore from '@app/models/RootStore'
12 | import constants from '@app/constants'
13 |
14 | class GLManager {
15 | canvas: HTMLCanvasElement
16 | ctx: any
17 | rasterView: RasterView
18 | rasterMask: RasterMask
19 | rasterWidthGather: RasterWidthGather
20 | rasterHeightGather: RasterHeightGather
21 | vectorView: VectorView
22 | outlineView: OutlineView
23 | boxSelectView: BoxSelectView
24 | rootStore: RootStore
25 | rasterTexture: REGL.Texture2D
26 | rasterMaskTexture: REGL.Texture2D
27 | widthGatherTexture: REGL.Texture2D
28 | heightGatherTexture: REGL.Texture2D
29 | pendingRender: boolean = false
30 | debouncedCompute: () => void
31 |
32 | constructor ({
33 | canvas,
34 | rootStore,
35 | }: {
36 | canvas: HTMLCanvasElement,
37 | rootStore?: RootStore
38 | }) {
39 | this.canvas = canvas
40 | this.rootStore = rootStore
41 |
42 | this.debouncedCompute = _.debounce(() => { this.compute() })
43 |
44 | this.ctx = REGL({
45 | profile: constants.PROFILE,
46 | canvas: this.canvas,
47 | extensions: constants.GL_EXTENSIONS,
48 | attributes: { alpha: false },
49 | })
50 |
51 | this.rasterTexture = this.ctx.texture({
52 | ...(constants.DATA_TEXTURE_OPTIONS),
53 | type: 'uint8',
54 | })
55 |
56 | this.updateDataTexture()
57 |
58 | this.vectorView = new VectorView({
59 | rootStore,
60 | ctx: this.ctx,
61 | })
62 |
63 | this.outlineView = new OutlineView({
64 | rootStore,
65 | ctx: this.ctx,
66 | })
67 |
68 | this.boxSelectView = new BoxSelectView({
69 | rootStore,
70 | ctx: this.ctx,
71 | })
72 |
73 | this.rasterMaskTexture = this.ctx.texture({
74 | ...(constants.DATA_TEXTURE_OPTIONS),
75 | type: 'uint8',
76 | width: rootStore.rasterWidth,
77 | height: rootStore.rasterHeight,
78 | })
79 |
80 | this.rasterMask = new RasterMask({
81 | rootStore,
82 | ctx: this.ctx,
83 | rasterMaskTexture: this.rasterMaskTexture,
84 | })
85 |
86 | this.rasterView = new RasterView({
87 | rootStore,
88 | ctx: this.ctx,
89 | rasterTexture: this.rasterTexture,
90 | rasterMaskTexture: this.rasterMaskTexture,
91 | })
92 |
93 | this.widthGatherTexture = this.ctx.texture({
94 | ...(constants.DATA_TEXTURE_OPTIONS),
95 | width: rootStore.samplesWide,
96 | height: constants.DATA_TEXTURE_SIZE,
97 | })
98 |
99 | this.rasterWidthGather = new RasterWidthGather({
100 | rootStore,
101 | ctx: this.ctx,
102 | rasterTexture: this.rasterTexture,
103 | rasterMaskTexture: this.rasterMaskTexture,
104 | widthGatherTexture: this.widthGatherTexture,
105 | })
106 |
107 | this.heightGatherTexture = this.ctx.texture({
108 | ...(constants.DATA_TEXTURE_OPTIONS),
109 | width: rootStore.samplesWide,
110 | height: rootStore.textureRastersHigh,
111 | })
112 |
113 | this.rasterHeightGather = new RasterHeightGather({
114 | rootStore,
115 | ctx: this.ctx,
116 | widthGatherTexture: this.widthGatherTexture,
117 | heightGatherTexture: this.heightGatherTexture,
118 | })
119 |
120 | this.rasterWidthGather.compute()
121 | rootStore.timePeriodAverages.replace(
122 | this.rasterHeightGather.compute()
123 | )
124 |
125 | reaction(() => ({
126 | timePeriod: this.rootStore.timePeriod,
127 | zoom: this.rootStore.camera.zoom,
128 | cameraPosition: this.rootStore.camera.position.array,
129 | selectedBox: this.rootStore.selectedBox.array,
130 | }), this.render.bind(this))
131 |
132 | reaction(() => ({
133 | mode: this.rootStore.mode,
134 | }), () => {
135 | this.updateDataTexture()
136 | this.render()
137 | })
138 | }
139 |
140 | updateDataTexture () {
141 | const atlasConfig = this.rootStore.atlas.config
142 | this.rasterTexture({
143 | width: atlasConfig.rasterWidth * atlasConfig.rastersWide,
144 | height: atlasConfig.rasterHeight * atlasConfig.rastersHigh,
145 | data: this.rootStore.atlas.data,
146 | })
147 | }
148 |
149 | render () {
150 | const devicePixelRatio = window.devicePixelRatio || 1
151 | const newWidth = this.canvas.offsetWidth * devicePixelRatio
152 | const newHeight = this.canvas.offsetHeight * devicePixelRatio
153 |
154 | if (this.canvas.width !== newWidth ||
155 | this.canvas.height !== newHeight) {
156 | this.canvas.width = newWidth
157 | this.canvas.height = newHeight
158 | this.rootStore.camera.size.set(
159 | Math.max(this.canvas.offsetWidth, constants.SELECTED_BOX_PADDING * 3),
160 | Math.max(this.canvas.offsetHeight, constants.SELECTED_BOX_PADDING * 3)
161 | )
162 | }
163 |
164 | if (!this.pendingRender) {
165 | this.pendingRender = true
166 | const tick = this.ctx.frame(() => {
167 | tick.cancel()
168 | this.pendingRender = false
169 | this.ctx.clear({ color: this.rootStore.modeConfig.NO_DATA_COLOR })
170 | this.rasterMask.render()
171 | this.vectorView.render()
172 | this.rasterView.render()
173 | this.outlineView.render()
174 | this.boxSelectView.render()
175 | this.debouncedCompute()
176 | })
177 | }
178 | }
179 |
180 | compute () {
181 | this.rasterWidthGather.compute()
182 | this.rootStore.timePeriodAverages.replace(
183 | this.rasterHeightGather.compute()
184 | )
185 | }
186 | }
187 |
188 | export default GLManager
189 |
--------------------------------------------------------------------------------
/src/js/gl/GLTest.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import constants from '@app/constants'
3 |
4 | const TEST_SIZE = 16
5 |
6 | class GLTest {
7 | canvas: HTMLCanvasElement
8 | ctx: any
9 | texture: REGL.Texture2D
10 | fbo: REGL.Framebuffer
11 |
12 | constructor () {
13 | this.canvas = document.createElement('canvas')
14 |
15 | this.ctx = REGL({
16 | canvas: this.canvas,
17 | extensions: constants.GL_EXTENSIONS,
18 | attributes: { alpha: false },
19 | })
20 |
21 | const data = new Float32Array(TEST_SIZE * TEST_SIZE * 4)
22 |
23 | this.texture = this.ctx.texture({
24 | ...(constants.DATA_TEXTURE_OPTIONS),
25 | radius: 16,
26 | data,
27 | })
28 |
29 | this.fbo = this.ctx.framebuffer({ color: this.texture })
30 |
31 | this.ctx({ framebuffer: this.fbo })(() => {
32 | this.ctx.clear({ color: [0.5, 0.5, 0.5, 0.5] })
33 | this.ctx.read()
34 | })
35 | }
36 | }
37 |
38 | export default GLTest
39 |
--------------------------------------------------------------------------------
/src/js/gl/OutlineView.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import { mat4 } from 'gl-matrix'
3 | import RootStore from '@app/models/RootStore'
4 |
5 | import vert from '@app/gl/shaders/outlineVert'
6 | import frag from '@app/gl/shaders/outlineFrag'
7 |
8 | interface IUniforms {
9 | selectedColor: number[]
10 | unselectedColor: number[]
11 | model: REGL.Mat4
12 | view: REGL.Mat4
13 | projection: REGL.Mat4
14 | scale: number
15 | selectedBBoxLngLat: number[]
16 | }
17 |
18 | interface IAttributes {
19 | position: number[]
20 | }
21 |
22 | interface IProps {
23 | selectedColor: number[]
24 | unselectedColor: number[]
25 | view: REGL.Mat4
26 | projection: REGL.Mat4
27 | scale: number
28 | vertices: number[][]
29 | linesLength: number
30 | selectedBBoxLngLat: number[]
31 | }
32 |
33 | class OutlineView {
34 | renderer: any
35 | ctx: REGL.Regl
36 | rootStore: RootStore
37 |
38 | constructor ({
39 | ctx,
40 | rootStore,
41 | }: {
42 | ctx: REGL.Regl,
43 | rootStore?: RootStore,
44 | }) {
45 | this.ctx = ctx
46 | this.rootStore = rootStore
47 |
48 | this.renderer = ctx({
49 | frag: frag(),
50 | vert: vert(),
51 | attributes: {
52 | position: ctx.prop('vertices'),
53 | },
54 | uniforms: {
55 | selectedColor: ctx.prop('selectedColor'),
56 | unselectedColor: ctx.prop('unselectedColor'),
57 | model: mat4.fromTranslation([], [0, 0, 0]),
58 | view: ctx.prop('view'),
59 | projection: ctx.prop('projection'),
60 | scale: ctx.prop('scale'),
61 | selectedBBoxLngLat: ctx.prop('selectedBBoxLngLat'),
62 | },
63 | depth: {
64 | enable: false,
65 | },
66 | count: ctx.prop('linesLength'),
67 | primitive: 'lines',
68 | })
69 | }
70 |
71 | render () {
72 | this.renderer({
73 | ...(this.rootStore.camera.renderInfo),
74 | vertices: this.rootStore.vectorLayer.outline.peek(),
75 | linesLength: this.rootStore.vectorLayer.outline.length / 2,
76 | selectedBBoxLngLat: this.rootStore.selectedBox.array,
77 | selectedColor: this.rootStore.modeConfig.SELECTED_COLOR,
78 | unselectedColor: this.rootStore.modeConfig.UNSELECTED_COLOR,
79 | })
80 | }
81 | }
82 |
83 | export default OutlineView
84 |
--------------------------------------------------------------------------------
/src/js/gl/RasterHeightGather.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import RootStore from '@app/models/RootStore'
3 | import constants from '@app/constants'
4 | import {
5 | compensatedSquareUVs
6 | } from '@app/utils'
7 |
8 | import vert from '@app/gl/shaders/gatherVert'
9 | import frag from '@app/gl/shaders/gatherFragHeight'
10 |
11 | interface IUniforms {
12 | widthGather: REGL.Texture2D
13 | imageSize: number[]
14 | imagesWide: number
15 | imagesHigh: number
16 | }
17 |
18 | interface IAttributes {
19 | position: number[][]
20 | uv: number[][]
21 | }
22 |
23 | interface IProps {
24 | framebufferWidth: number
25 | framebufferHeight: number
26 | }
27 |
28 | class RasterHeightGather {
29 | renderer: any
30 | ctx: REGL.Regl
31 | widthGatherTexture: REGL.Texture2D
32 | heightGatherTexture: REGL.Texture2D
33 | heightGatherFBO: REGL.Framebuffer
34 | rootStore: RootStore
35 |
36 | constructor ({
37 | ctx,
38 | widthGatherTexture,
39 | heightGatherTexture,
40 | rootStore,
41 | }: {
42 | ctx: REGL.Regl
43 | widthGatherTexture: REGL.Texture2D
44 | heightGatherTexture: REGL.Texture2D
45 | rootStore?: RootStore
46 | }) {
47 | this.ctx = ctx
48 | this.rootStore = rootStore
49 |
50 | this.widthGatherTexture = widthGatherTexture
51 | this.heightGatherTexture = heightGatherTexture
52 |
53 | this.heightGatherFBO = ctx.framebuffer({
54 | color: this.heightGatherTexture,
55 | })
56 |
57 | this.renderer = ctx({
58 | framebuffer: this.heightGatherFBO,
59 | context: {
60 | framebufferWidth: ctx.prop('framebufferWidth'),
61 | framebufferHeight: ctx.prop('framebufferHeight'),
62 | },
63 | frag: frag({
64 | rasterHeight: rootStore.rasterHeight,
65 | noDataThreshold: constants.NO_DATA_THRESHOLD,
66 | }),
67 | vert: vert(),
68 | depth: {
69 | enable: false,
70 | },
71 | attributes: {
72 | position: constants.DATA_SQUARE_POSITIONS,
73 | uv: compensatedSquareUVs({
74 | width: rootStore.samplesWide,
75 | height: rootStore.textureRastersHigh,
76 | }),
77 | },
78 | uniforms: {
79 | widthGather: this.widthGatherTexture,
80 | imageSize: this.rootStore.rasterSizePercent.array,
81 | imagesWide: this.rootStore.samplesWide,
82 | imagesHigh: this.rootStore.textureRastersHigh,
83 | },
84 | count: constants.DATA_SQUARE_POSITIONS.length,
85 | })
86 | }
87 |
88 | compute (): number[] {
89 | let pixels: Float32Array
90 | this.ctx({ framebuffer: this.heightGatherFBO })(() => {
91 | this.renderer({
92 | framebufferWidth: this.rootStore.samplesWide,
93 | framebufferHeight: this.rootStore.textureRastersHigh,
94 | })
95 |
96 | pixels = this.ctx.read({
97 | x: 0,
98 | y: 0,
99 | width: this.rootStore.samplesWide,
100 | height: this.rootStore.textureRastersHigh,
101 | })
102 | })
103 |
104 | return this.valuesFromGatheredPixels(pixels)
105 | }
106 |
107 | valuesFromGatheredPixels (pixels: Float32Array) {
108 | const totals: number[] = []
109 | const pixelCounts: number[] = []
110 | const values: number[] = []
111 | for (let i = 0; i < this.rootStore.numTimePeriods; i++) {
112 | const total = pixels[i * 4]
113 | const pixelCount = pixels[i * 4 + 1]
114 | totals.push(total)
115 | pixelCounts.push(pixelCount)
116 |
117 | values.push(pixelCount === 0 ? undefined : total / pixelCount)
118 | }
119 |
120 | return values
121 | }
122 | }
123 |
124 | export default RasterHeightGather
125 |
--------------------------------------------------------------------------------
/src/js/gl/RasterMask.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import RootStore from '@app/models/RootStore'
3 | import constants from '@app/constants'
4 | import {
5 | compensatedSquareUVs
6 | } from '@app/utils'
7 |
8 | import vert from '@app/gl/shaders/maskVert'
9 | import frag from '@app/gl/shaders/maskFrag'
10 |
11 | interface IUniforms {
12 | mask: REGL.Texture2D
13 | rasterBBoxMeters: number[]
14 | selectedBBoxLngLat: number[]
15 | rasterWidth: number
16 | rasterHeight: number
17 | }
18 |
19 | interface IAttributes {
20 | position: number[][]
21 | uv: number[][]
22 | }
23 |
24 | interface IProps {
25 | framebufferWidth: number
26 | framebufferHeight: number
27 | rasterBBoxMeters: number[]
28 | selectedBBoxLngLat: number[]
29 | }
30 |
31 | class RasterMask {
32 | renderer: any
33 | ctx: REGL.Regl
34 | rasterMaskTexture: REGL.Texture2D
35 | rootStore: RootStore
36 | maskFBO: REGL.Framebuffer
37 |
38 | constructor ({
39 | ctx,
40 | rasterMaskTexture,
41 | rootStore,
42 | }: {
43 | ctx: REGL.Regl
44 | rasterMaskTexture: REGL.Texture2D
45 | rootStore?: RootStore
46 | }) {
47 | this.ctx = ctx
48 | this.rasterMaskTexture = rasterMaskTexture
49 | this.rootStore = rootStore
50 |
51 | this.maskFBO = ctx.framebuffer({
52 | color: this.rasterMaskTexture,
53 | })
54 |
55 | this.renderer = ctx({
56 | framebuffer: this.maskFBO,
57 | context: {
58 | framebufferWidth: ctx.prop('framebufferWidth'),
59 | framebufferHeight: ctx.prop('framebufferHeight'),
60 | },
61 | frag: frag(),
62 | vert: vert(),
63 | depth: {
64 | enable: false,
65 | },
66 | attributes: {
67 | position: constants.DATA_SQUARE_POSITIONS,
68 | uv: compensatedSquareUVs({
69 | width: rootStore.rasterWidth,
70 | height: rootStore.rasterHeight,
71 | }),
72 | },
73 | uniforms: {
74 | mask: this.rasterMaskTexture,
75 | rasterWidth: rootStore.rasterWidth,
76 | rasterHeight: rootStore.rasterHeight,
77 | rasterBBoxMeters: ctx.prop('rasterBBoxMeters'),
78 | selectedBBoxLngLat: ctx.prop('selectedBBoxLngLat'),
79 | },
80 | count: constants.DATA_SQUARE_POSITIONS.length,
81 | })
82 | }
83 |
84 | render () {
85 | this.ctx({ framebuffer: this.maskFBO })(() => {
86 | this.renderer({
87 | framebufferWidth: this.rootStore.rasterWidth,
88 | framebufferHeight: this.rootStore.rasterHeight,
89 | rasterBBoxMeters: this.rootStore.boundingBox.array,
90 | selectedBBoxLngLat: this.rootStore.selectedBox.array,
91 | })
92 | })
93 | }
94 | }
95 |
96 | export default RasterMask
97 |
--------------------------------------------------------------------------------
/src/js/gl/RasterView.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import { mat4 } from 'gl-matrix'
3 | import RootStore from '@app/models/RootStore'
4 | import constants, { Modes } from '@app/constants'
5 | import { uniformArrayAsObject } from '@app/utils'
6 | import { GL_COLORS_NDVI, GL_COLORS_NDVI_ANOMALY } from '@app/scales'
7 | import vert from '@app/gl/shaders/viewVert'
8 | import frag from '@app/gl/shaders/viewFrag'
9 |
10 | interface IUniforms {
11 | 'model': REGL.Mat4
12 | 'view': REGL.Mat4
13 | 'projection': REGL.Mat4
14 | 'raster': REGL.Texture2D
15 | 'mask': REGL.Texture2D
16 | 'imagesWide': number
17 | 'imageSize': number[]
18 | 'rasterBBoxMeters': number[]
19 | 'selectedBBoxLngLat': number[]
20 | 'atlasSize': number
21 | 'timePeriod': number
22 | 'scale': number
23 | 'colors': number[][]
24 | 'colors[0]': number[]
25 | 'colors[1]': number[]
26 | 'colors[2]': number[]
27 | 'colors[3]': number[]
28 | 'colors[4]': number[]
29 | 'colors[5]': number[]
30 | 'colors[6]': number[]
31 | 'colors[7]': number[]
32 | 'colors[8]': number[]
33 | 'noDataColor': number[]
34 | }
35 |
36 | interface IAttributes {
37 | position: number[][]
38 | }
39 |
40 | interface IProps {
41 | view: REGL.Mat4
42 | projection: REGL.Mat4
43 | rasterBBoxMeters: number[]
44 | selectedBBoxLngLat: number[]
45 | scale: number
46 | triangles: number[][]
47 | trianglesLength: number
48 | timePeriod: number
49 | 'colors': number[][],
50 | 'colors[0]': number[],
51 | 'colors[1]': number[],
52 | 'colors[2]': number[],
53 | 'colors[3]': number[],
54 | 'colors[4]': number[],
55 | 'colors[5]': number[],
56 | 'colors[6]': number[],
57 | 'colors[7]': number[],
58 | 'colors[8]': number[],
59 | 'noDataColor': number[]
60 | }
61 |
62 | class RasterView {
63 | renderer: any
64 | ctx: REGL.Regl
65 | rasterTexture: REGL.Texture2D
66 | rasterMaskTexture: REGL.Texture2D
67 | rootStore: RootStore
68 |
69 | constructor ({
70 | ctx,
71 | rasterTexture,
72 | rasterMaskTexture,
73 | rootStore,
74 | }: {
75 | ctx: REGL.Regl,
76 | rasterTexture: REGL.Texture2D,
77 | rasterMaskTexture: REGL.Texture2D,
78 | rootStore?: RootStore,
79 | }) {
80 | this.ctx = ctx
81 | this.rasterTexture = rasterTexture
82 | this.rasterMaskTexture = rasterMaskTexture
83 | this.rootStore = rootStore
84 |
85 | this.renderer = ctx({
86 | frag: frag({
87 | noDataThreshold: constants.NO_DATA_THRESHOLD,
88 | }),
89 | vert: vert(),
90 | attributes: {
91 | position: ctx.prop('triangles'),
92 | },
93 | uniforms: {
94 | 'model': mat4.fromTranslation([], [0, 0, 0]),
95 | 'view': ctx.prop('view'),
96 | 'projection': ctx.prop('projection'),
97 | 'imagesWide': this.rootStore.textureRastersWide,
98 | 'imageSize': this.rootStore.rasterSizePercent.array,
99 | 'raster': this.rasterTexture,
100 | 'mask': this.rasterMaskTexture,
101 | 'rasterBBoxMeters': ctx.prop('rasterBBoxMeters'),
102 | 'selectedBBoxLngLat': ctx.prop('selectedBBoxLngLat'),
103 | 'atlasSize': constants.DATA_TEXTURE_SIZE,
104 | 'timePeriod': ctx.prop('timePeriod'),
105 | 'scale': ctx.prop('scale'),
106 | 'colors': ctx.prop('colors'),
107 | 'colors[0]': ctx.prop('colors[0]'),
108 | 'colors[1]': ctx.prop('colors[1]'),
109 | 'colors[2]': ctx.prop('colors[2]'),
110 | 'colors[3]': ctx.prop('colors[3]'),
111 | 'colors[4]': ctx.prop('colors[4]'),
112 | 'colors[5]': ctx.prop('colors[5]'),
113 | 'colors[6]': ctx.prop('colors[6]'),
114 | 'colors[7]': ctx.prop('colors[7]'),
115 | 'colors[8]': ctx.prop('colors[8]'),
116 | 'noDataColor': ctx.prop('noDataColor'),
117 | },
118 | count: ctx.prop('trianglesLength'),
119 | blend: {
120 | enable: true,
121 | func: {
122 | src: 'src alpha',
123 | dst: 'one minus src alpha',
124 | },
125 | },
126 | })
127 | }
128 |
129 | render () {
130 | const triangles = this.rootStore.boundingBox.lngLatFromSinusoidal.triangles
131 |
132 | let modeUniforms
133 | switch (this.rootStore.mode) {
134 | case Modes.NDVI:
135 | case Modes.NDVI_GROUPED:
136 | modeUniforms = {
137 | colors: GL_COLORS_NDVI,
138 | ...uniformArrayAsObject('colors', GL_COLORS_NDVI),
139 | }
140 | break
141 | case Modes.NDVI_ANOMALY:
142 | case Modes.NDVI_ANOMALY_GROUPED:
143 | modeUniforms = {
144 | colors: GL_COLORS_NDVI_ANOMALY,
145 | ...uniformArrayAsObject('colors', GL_COLORS_NDVI_ANOMALY),
146 | }
147 | }
148 |
149 | this.renderer({
150 | ...(this.rootStore.camera.renderInfo),
151 | timePeriod: this.rootStore.timePeriod,
152 | triangles,
153 | trianglesLength: triangles.length,
154 | rasterBBoxMeters: this.rootStore.boundingBox.array,
155 | selectedBBoxLngLat: this.rootStore.selectedBox.array,
156 | noDataColor: this.rootStore.modeConfig.NO_DATA_COLOR,
157 | ...modeUniforms,
158 | })
159 | }
160 | }
161 |
162 | export default RasterView
163 |
--------------------------------------------------------------------------------
/src/js/gl/RasterWidthGather.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import RootStore from '@app/models/RootStore'
3 | import constants from '@app/constants'
4 | import {
5 | compensatedSquareUVs
6 | } from '@app/utils'
7 |
8 | import vert from '@app/gl/shaders/gatherVert'
9 | import frag from '@app/gl/shaders/gatherFragWidth'
10 |
11 | interface IUniforms {
12 | raster: REGL.Texture2D
13 | mask: REGL.Texture2D
14 | imageSize: number[]
15 | imagesWide: number
16 | targetHeight: number
17 | minValue: number
18 | maxValue: number
19 | }
20 |
21 | interface IAttributes {
22 | position: number[][]
23 | uv: number[][]
24 | }
25 |
26 | interface IProps {
27 | framebufferWidth: number
28 | framebufferHeight: number
29 | minValue: number
30 | maxValue: number
31 | }
32 |
33 | class RasterWidthGather {
34 | renderer: any
35 | ctx: REGL.Regl
36 | rasterTexture: REGL.Texture2D
37 | rasterMaskTexture: REGL.Texture2D
38 | widthGatherTexture: REGL.Texture2D
39 | widthGatherFBO: REGL.Framebuffer
40 | rootStore: RootStore
41 |
42 | constructor ({
43 | ctx,
44 | rasterTexture,
45 | rasterMaskTexture,
46 | widthGatherTexture,
47 | rootStore,
48 | }: {
49 | ctx: REGL.Regl
50 | rasterTexture: REGL.Texture2D
51 | rasterMaskTexture: REGL.Texture2D
52 | widthGatherTexture: REGL.Texture2D
53 | rootStore?: RootStore
54 | }) {
55 | this.ctx = ctx
56 | this.rasterTexture = rasterTexture
57 | this.rasterMaskTexture = rasterMaskTexture
58 | this.rootStore = rootStore
59 |
60 | this.widthGatherTexture = widthGatherTexture
61 |
62 | this.widthGatherFBO = ctx.framebuffer({
63 | color: this.widthGatherTexture,
64 | })
65 |
66 | this.renderer = ctx({
67 | framebuffer: this.widthGatherFBO,
68 | context: {
69 | framebufferWidth: ctx.prop('framebufferWidth'),
70 | framebufferHeight: ctx.prop('framebufferHeight'),
71 | },
72 | frag: frag({
73 | rasterWidth: rootStore.rasterWidth,
74 | noDataThreshold: constants.NO_DATA_THRESHOLD,
75 | }),
76 | vert: vert(),
77 | depth: {
78 | enable: false,
79 | },
80 | attributes: {
81 | position: constants.DATA_SQUARE_POSITIONS,
82 | uv: compensatedSquareUVs({
83 | width: rootStore.samplesWide,
84 | height: constants.DATA_TEXTURE_SIZE,
85 | }),
86 | },
87 | uniforms: {
88 | raster: this.rasterTexture,
89 | mask: this.rasterMaskTexture,
90 | imageSize: this.rootStore.rasterSizePercent.array,
91 | imagesWide: this.rootStore.textureRastersWide,
92 | targetHeight: constants.DATA_TEXTURE_SIZE,
93 | minValue: ctx.prop('minValue'),
94 | maxValue: ctx.prop('maxValue'),
95 | },
96 | count: constants.DATA_SQUARE_POSITIONS.length,
97 | })
98 | }
99 |
100 | compute () {
101 | this.ctx({ framebuffer: this.widthGatherFBO })(() => {
102 | this.renderer({
103 | framebufferWidth: this.rootStore.samplesWide,
104 | framebufferHeight: constants.DATA_TEXTURE_SIZE,
105 | minValue: this.rootStore.modeConfig.RANGE[0],
106 | maxValue: this.rootStore.modeConfig.RANGE[1],
107 | })
108 | })
109 | }
110 | }
111 |
112 | export default RasterWidthGather
113 |
--------------------------------------------------------------------------------
/src/js/gl/VectorView.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import { mat4 } from 'gl-matrix'
3 | import RootStore from '@app/models/RootStore'
4 |
5 | import vert from '@app/gl/shaders/landVert'
6 | import frag from '@app/gl/shaders/landFrag'
7 |
8 | interface IUniforms {
9 | model: REGL.Mat4
10 | view: REGL.Mat4
11 | projection: REGL.Mat4
12 | scale: number
13 | }
14 |
15 | interface IAttributes {
16 | position: number[]
17 | }
18 |
19 | interface IProps {
20 | view: REGL.Mat4
21 | projection: REGL.Mat4
22 | scale: number
23 | vertices: number[][]
24 | elements: number[]
25 | }
26 |
27 | class VectorView {
28 | renderer: any
29 | ctx: REGL.Regl
30 | rootStore: RootStore
31 |
32 | constructor ({
33 | ctx,
34 | rootStore,
35 | }: {
36 | ctx: REGL.Regl,
37 | rootStore?: RootStore,
38 | }) {
39 | this.ctx = ctx
40 | this.rootStore = rootStore
41 |
42 | this.renderer = ctx({
43 | frag: frag(),
44 | vert: vert(),
45 | attributes: {
46 | position: ctx.prop('vertices'),
47 | },
48 | uniforms: {
49 | model: mat4.fromTranslation([], [0, 0, 0]),
50 | view: ctx.prop('view'),
51 | projection: ctx.prop('projection'),
52 | scale: ctx.prop('scale'),
53 | },
54 | elements: ctx.prop('elements'),
55 | depth: {
56 | enable: false,
57 | },
58 | })
59 | }
60 |
61 | render () {
62 | this.renderer({
63 | ...(this.rootStore.camera.renderInfo),
64 | vertices: this.rootStore.vectorLayer.vertices.peek(),
65 | elements: this.ctx.elements({
66 | data: this.rootStore.vectorLayer.indices.peek(),
67 | type: 'uint32',
68 | }),
69 | })
70 | }
71 | }
72 |
73 | export default VectorView
74 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/atlasSample.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | // Returns a sample value from a data atlas where data
3 | // elements are packed sequentially across RGBA channels
4 | float atlasSample(float index, vec4 sample) {
5 | return
6 | (1.0 - step(0.1, abs(0.0 - index))) * sample.r +
7 | (1.0 - step(0.1, abs(1.0 - index))) * sample.g +
8 | (1.0 - step(0.1, abs(2.0 - index))) * sample.b +
9 | (1.0 - step(0.1, abs(3.0 - index))) * sample.a;
10 | }
11 | `
12 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/atlasUV.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | // Transforms a standard UV to a UV within the appropriate
3 | // sub-image of an image atlas. The image within the atlas
4 | // is specified by 'index'
5 | vec2 atlasUV(
6 | vec2 uv,
7 | int index,
8 | int imagesWide,
9 | vec2 imageSize
10 | ) {
11 | const float numChannels = 4.0;
12 | float imagesPerRow = numChannels * float(imagesWide);
13 | vec2 start = vec2(
14 | floor(mod(float(index) / numChannels, float(imagesWide))),
15 | floor(float(index) / imagesPerRow)
16 | );
17 | return vec2(
18 | (clamp(uv.x, 0.0, 0.999) + start.x) * imageSize.x,
19 | (clamp(uv.y, 0.0, 0.999) + start.y) * imageSize.y
20 | );
21 | }
22 | `
23 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/colorScale.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | vec4 colorScale (float x, vec4[9] colors) {
3 | const float e0 = 0.0;
4 | const float e1 = 0.13;
5 | const float e2 = 0.25;
6 | const float e3 = 0.38;
7 | const float e4 = 0.5;
8 | const float e5 = 0.63;
9 | const float e6 = 0.75;
10 | const float e7 = 0.88;
11 | const float e8 = 1.0;
12 | float a0 = smoothstep(e0,e1,x);
13 | float a1 = smoothstep(e1,e2,x);
14 | float a2 = smoothstep(e2,e3,x);
15 | float a3 = smoothstep(e3,e4,x);
16 | float a4 = smoothstep(e4,e5,x);
17 | float a5 = smoothstep(e5,e6,x);
18 | float a6 = smoothstep(e6,e7,x);
19 | float a7 = smoothstep(e7,e8,x);
20 | return max(mix(colors[0],colors[1],a0)*step(e0,x)*step(x,e1),
21 | max(mix(colors[1],colors[2],a1)*step(e1,x)*step(x,e2),
22 | max(mix(colors[2],colors[3],a2)*step(e2,x)*step(x,e3),
23 | max(mix(colors[3],colors[4],a3)*step(e3,x)*step(x,e4),
24 | max(mix(colors[4],colors[5],a4)*step(e4,x)*step(x,e5),
25 | max(mix(colors[5],colors[6],a5)*step(e5,x)*step(x,e6),
26 | max(mix(colors[6],colors[7],a6)*step(e6,x)*step(x,e7),
27 | mix(colors[7],colors[8],a7)*step(e7,x)*step(x,e8)
28 | )))))));
29 | }
30 | `
31 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/geoByteScale.glsl.ts:
--------------------------------------------------------------------------------
1 | // Given a non-zero 8-bit value from a geoTIFF whose nodata value is 0,
2 | // returns the value scaled to the range 0-1
3 | export default () => `
4 | float geoByteScale(float byteValue) {
5 | return (byteValue - (1.0 / 255.0)) * (255.0 / 254.0);
6 | }
7 | `
8 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/isPointInBBox.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | // given a point and a bounding box, returns 1 if the point is
3 | // in the bbox, else 0
4 | float isPointInBBox(vec2 point, vec4 bbox) {
5 | if (
6 | point.x > bbox[0] && point.x < bbox[2] &&
7 | point.y > bbox[1] && point.y < bbox[3]
8 | ) {
9 | return 1.0;
10 | }
11 | else {
12 | return 0.0;
13 | }
14 | }
15 | `
16 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/lngLatToMercator.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | vec2 lngLatToMercator(vec2 position, float scale) {
3 | const float PI = 3.141592653589793;
4 | const float PI_4 = PI / 4.0;
5 | const float DEGREES_TO_RADIANS = PI / 180.0;
6 | float lambda2 = position.x * DEGREES_TO_RADIANS;
7 | float phi2 = position.y * DEGREES_TO_RADIANS;
8 | float x = scale * (lambda2 + PI) / (2.0 * PI);
9 | float y = scale * (PI - log(tan(PI_4 + phi2 * 0.5))) / (2.0 * PI);
10 | return vec2(x, y);
11 | }
12 | `
13 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/lngLatToSinusoidal.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | vec2 lngLatToSinusoidal(vec2 position) {
3 | const float PI = 3.141592653589793;
4 | const float EARTH_CIRCUMFERENCE = 6371007.0 * PI * 2.0;
5 |
6 | float latFactor = cos(position.y * PI / 180.0);
7 |
8 | float y = position.y / 360.0 * EARTH_CIRCUMFERENCE;
9 | float x = position.x / 360.0 * EARTH_CIRCUMFERENCE * latFactor;
10 | return vec2(x, y);
11 | }
12 | `
13 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/luma.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | float luma(vec3 color) {
3 | return dot(color, vec3(0.299, 0.587, 0.114));
4 | }
5 |
6 | float luma(vec4 color) {
7 | return dot(color.rgb, vec3(0.299, 0.587, 0.114));
8 | }
9 | `
10 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/mercatorToLngLat.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | vec2 mercatorToLngLat(vec2 position, float scale) {
3 | const float PI = 3.141592653589793;
4 | const float PI_4 = PI / 4.0;
5 | const float RADIANS_TO_DEGREES = 180.0 / PI;
6 |
7 | float lambda2 = (position.x / scale) * (2.0 * PI) - PI;
8 | float phi2 = 2.0 * (atan(exp(PI - (position.y / scale) * (2.0 * PI))) - PI_4);
9 | return vec2(lambda2 * RADIANS_TO_DEGREES, phi2 * RADIANS_TO_DEGREES);
10 | }
11 | `
12 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/ndviScale.glsl.ts:
--------------------------------------------------------------------------------
1 | // Converts a value from 0-1 to the NDVI -0.2 to 1.0 scale
2 | export default () => `
3 | float ndviScale(float value) {
4 | return value * 1.2 - 0.2;
5 | }
6 | `
7 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/pointInBBox.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | // given a point and a bounding box, returns the point's position
3 | // within the box normalized from 0 to 1
4 | vec2 pointInBBox(vec2 point, vec4 bbox) {
5 | return vec2(
6 | (point.x - bbox[0]) / (bbox[2] - bbox[0]),
7 | (point.y - bbox[1]) / (bbox[3] - bbox[1])
8 | );
9 | }
10 | `
11 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/functions/sinusoidalToLngLat.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | vec2 sinusoidalToLngLat(vec2 position) {
3 | const float PI = 3.141592653589793;
4 | const float EARTH_CIRCUMFERENCE = 6371007.0 * PI * 2.0;
5 |
6 | float normalizedY = position.y / EARTH_CIRCUMFERENCE * 2.0 + 0.5;
7 | float scaledX = position.x / sin(PI * normalizedY);
8 | float normalizedX = scaledX / EARTH_CIRCUMFERENCE * 2.0 + 0.5;
9 |
10 | return vec2(
11 | (normalizedX - 0.5) * 180.0,
12 | (normalizedY - 0.5) * 180.0
13 | );
14 | }
15 | `
16 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/gatherFragHeight.glsl.ts:
--------------------------------------------------------------------------------
1 | import atlasUV from '@app/gl/shaders/functions/atlasUV'
2 | import atlasSample from '@app/gl/shaders/functions/atlasSample'
3 |
4 | export default ({
5 | rasterHeight,
6 | noDataThreshold,
7 | }: {
8 | rasterHeight: number
9 | noDataThreshold: number
10 | }) => `
11 | precision highp float;
12 | uniform sampler2D widthGather;
13 | uniform int imagesWide;
14 | uniform int imagesHigh;
15 | uniform vec2 imageSize;
16 | varying vec2 vUv;
17 |
18 | ${atlasUV()}
19 | ${atlasSample()}
20 |
21 | void main () {
22 | float total = 0.0, pixels = 0.0;
23 |
24 | for (int i = 0; i < ${rasterHeight}; i++) {
25 | vec2 uv = vec2(
26 | (vUv.x + 0.5) / float(imagesWide),
27 | (vUv.y + (float(i) / ${rasterHeight}.0)) * imageSize.y
28 | );
29 |
30 | vec4 sample = texture2D(widthGather, uv);
31 |
32 | total += sample.r;
33 | pixels += sample.g;
34 | }
35 |
36 | gl_FragColor = vec4(
37 | total,
38 | pixels,
39 | 0.0,
40 | 1.0
41 | );
42 | }
43 | `
44 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/gatherFragWidth.glsl.ts:
--------------------------------------------------------------------------------
1 | import atlasUV from '@app/gl/shaders/functions/atlasUV'
2 | import atlasSample from '@app/gl/shaders/functions/atlasSample'
3 | import geoByteScale from '@app/gl/shaders/functions/geoByteScale'
4 |
5 | export default ({
6 | rasterWidth,
7 | noDataThreshold,
8 | }: {
9 | rasterWidth: number,
10 | noDataThreshold: number,
11 | }) => `
12 | precision highp float;
13 | uniform sampler2D raster;
14 | uniform sampler2D mask;
15 | uniform vec2 imageSize;
16 | uniform int imagesWide;
17 | uniform float targetHeight;
18 | uniform float minValue;
19 | uniform float maxValue;
20 | varying vec2 vUv;
21 |
22 | ${atlasUV()}
23 | ${atlasSample()}
24 | ${geoByteScale()}
25 |
26 | void main () {
27 | float total = 0.0, pixels = 0.0;
28 | float imageRow = floor((vUv.y / targetHeight) / imageSize.y);
29 | float index = floor(imageRow * float(imagesWide) * 4.0) + vUv.x;
30 |
31 | for (int i = 0; i < ${rasterWidth}; i++) {
32 | vec2 relativeUV = vec2(
33 | (float(i) + 0.5) / ${rasterWidth}.0,
34 | mod(vUv.y / targetHeight, imageSize.y) / imageSize.y
35 | );
36 |
37 | vec2 uv = atlasUV(
38 | relativeUV,
39 | int(index),
40 | imagesWide,
41 | imageSize
42 | );
43 |
44 | vec4 sample = texture2D(raster, uv);
45 | float maskSample = texture2D(mask, relativeUV).r;
46 | float value = atlasSample(mod(index, 4.0), sample);
47 | float scaled = mix(minValue, maxValue, geoByteScale(value));
48 |
49 | total += step(0.5, maskSample) * step(${noDataThreshold}, value) * scaled;
50 | pixels += step(0.5, maskSample) * step(${noDataThreshold}, value);
51 | }
52 |
53 | gl_FragColor = vec4(
54 | total,
55 | pixels,
56 | 0.0,
57 | 1.0
58 | );
59 | }
60 | `
61 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/gatherVert.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | precision highp float;
3 | attribute vec2 position;
4 | attribute vec2 uv;
5 | varying vec2 vUv;
6 | void main () {
7 | gl_Position = vec4(position, 0, 1);
8 | vUv = uv;
9 | }
10 | `
11 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/landFrag.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | precision highp float;
3 |
4 | void main() {
5 | gl_FragColor = vec4(0.1, 0.1, 0.1, 1.0);
6 | }
7 | `
8 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/landVert.glsl.ts:
--------------------------------------------------------------------------------
1 | import lngLatToMercator from '@app/gl/shaders/functions/lngLatToMercator'
2 |
3 | export default () => `
4 | precision highp float;
5 | attribute vec2 position;
6 | uniform mat4 model, view, projection;
7 | uniform float scale;
8 |
9 | ${lngLatToMercator()}
10 |
11 | void main() {
12 | vec4 mercator = vec4(lngLatToMercator(position, scale), 0.0, 1.0);
13 | gl_Position = projection * view * mercator;
14 | }
15 | `
16 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/maskFrag.glsl.ts:
--------------------------------------------------------------------------------
1 | import sinusoidalToLngLat from '@app/gl/shaders/functions/sinusoidalToLngLat'
2 | import isPointInBBox from '@app/gl/shaders/functions/isPointInBBox'
3 |
4 | export default () => `
5 | precision highp float;
6 |
7 | ${sinusoidalToLngLat()}
8 | ${isPointInBBox()}
9 |
10 | uniform vec4 rasterBBoxMeters;
11 | uniform vec4 selectedBBoxLngLat;
12 | uniform float rasterWidth;
13 | uniform float rasterHeight;
14 | varying vec2 vUv;
15 |
16 | void main() {
17 | vec2 uv = vec2(vUv.x / rasterWidth, vUv.y / rasterHeight);
18 |
19 | vec2 meters = vec2(
20 | rasterBBoxMeters[0] + uv.x * (rasterBBoxMeters[2] - rasterBBoxMeters[0]),
21 | rasterBBoxMeters[1] + uv.y * (rasterBBoxMeters[3] - rasterBBoxMeters[1])
22 | );
23 |
24 | vec2 lngLat = sinusoidalToLngLat(meters);
25 |
26 | gl_FragColor = vec4(vec3(isPointInBBox(lngLat, selectedBBoxLngLat)), 1.0);
27 | // gl_FragColor = vec4(meters.x + 880503.0, meters.y - 7221936.5, 0.0, 1.0);
28 | // gl_FragColor = vec4(lngLat.x + 18.7, lngLat.y - 64.94, 0.0, 1.0);
29 | }
30 | `
31 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/maskVert.glsl.ts:
--------------------------------------------------------------------------------
1 | export default () => `
2 | precision highp float;
3 | attribute vec2 position;
4 | attribute vec2 uv;
5 | varying vec2 vUv;
6 | void main () {
7 | gl_Position = vec4(position, 0, 1);
8 | vUv = uv;
9 | }
10 | `
11 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/outlineFrag.glsl.ts:
--------------------------------------------------------------------------------
1 | import mercatorToLngLat from '@app/gl/shaders/functions/mercatorToLngLat'
2 | import isPointInBBox from '@app/gl/shaders/functions/isPointInBBox'
3 |
4 | export default () => `
5 | precision highp float;
6 | uniform vec4 selectedColor;
7 | uniform vec4 unselectedColor;
8 | uniform float scale;
9 | uniform vec4 selectedBBoxLngLat;
10 | varying vec2 mercator;
11 |
12 | ${mercatorToLngLat()}
13 | ${isPointInBBox()}
14 |
15 | void main() {
16 | vec2 lngLat = mercatorToLngLat(mercator, scale);
17 | float selected = isPointInBBox(lngLat, selectedBBoxLngLat);
18 |
19 | gl_FragColor = mix(unselectedColor, selectedColor, selected);
20 | }
21 | `
22 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/outlineVert.glsl.ts:
--------------------------------------------------------------------------------
1 | import lngLatToMercator from '@app/gl/shaders/functions/lngLatToMercator'
2 |
3 | export default () => `
4 | precision highp float;
5 | attribute vec2 position;
6 | uniform mat4 model, view, projection;
7 | uniform float scale;
8 | varying vec2 mercator;
9 |
10 | ${lngLatToMercator()}
11 |
12 | void main() {
13 | mercator = lngLatToMercator(position, scale);
14 | gl_Position = projection * view * vec4(mercator, 0.0, 1.0);
15 | }
16 | `
17 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/viewFrag.glsl.ts:
--------------------------------------------------------------------------------
1 | import luma from '@app/gl/shaders/functions/luma'
2 | import colorScale from '@app/gl/shaders/functions/colorScale'
3 | import lngLatToSinusoidal from '@app/gl/shaders/functions/lngLatToSinusoidal'
4 | import mercatorToLngLat from '@app/gl/shaders/functions/mercatorToLngLat'
5 | import pointInBBox from '@app/gl/shaders/functions/pointInBBox'
6 | import atlasUV from '@app/gl/shaders/functions/atlasUV'
7 | import atlasSample from '@app/gl/shaders/functions/atlasSample'
8 | import geoByteScale from '@app/gl/shaders/functions/geoByteScale'
9 |
10 | export default ({
11 | noDataThreshold,
12 | }: {
13 | noDataThreshold: number,
14 | }) => `
15 | precision highp float;
16 |
17 | ${luma()}
18 | ${colorScale()}
19 | ${lngLatToSinusoidal()}
20 | ${mercatorToLngLat()}
21 | ${pointInBBox()}
22 | ${atlasUV()}
23 | ${atlasSample()}
24 | ${geoByteScale()}
25 |
26 | uniform highp int timePeriod;
27 | uniform float scale;
28 | uniform sampler2D raster;
29 | uniform sampler2D mask;
30 | uniform vec4 rasterBBoxMeters;
31 | uniform vec4 selectedBBoxLngLat;
32 | uniform vec2 imageSize;
33 | uniform int imagesWide;
34 | uniform vec4 colors[9];
35 | uniform vec4 noDataColor;
36 |
37 | varying vec2 mercator;
38 |
39 | void main() {
40 | float timeComponent = mod(float(timePeriod), 4.0);
41 |
42 | vec2 lngLat = mercatorToLngLat(mercator, scale);
43 | vec2 meters = lngLatToSinusoidal(lngLat);
44 | vec2 projectedUV = pointInBBox(meters, rasterBBoxMeters);
45 |
46 | float maskSample = texture2D(mask, projectedUV).r;
47 |
48 | vec4 sample = texture2D(raster, atlasUV(
49 | projectedUV,
50 | timePeriod,
51 | imagesWide,
52 | imageSize
53 | ));
54 |
55 | float unscaled = atlasSample(timeComponent, sample);
56 | float scaled = geoByteScale(unscaled);
57 |
58 | float hasdata = step(${noDataThreshold}, unscaled);
59 |
60 | vec4 color = mix(
61 | noDataColor,
62 | colorScale(scaled, colors),
63 | hasdata
64 | );
65 |
66 | vec4 grayscale = mix(
67 | noDataColor,
68 | mix(vec4(vec3(luma(color)), 1.0), noDataColor, 0.3),
69 | hasdata
70 | );
71 |
72 | gl_FragColor = mix(grayscale, color, maskSample);
73 | }
74 | `
75 |
--------------------------------------------------------------------------------
/src/js/gl/shaders/viewVert.glsl.ts:
--------------------------------------------------------------------------------
1 | import lngLatToMercator from '@app/gl/shaders/functions/lngLatToMercator'
2 |
3 | export default () => `
4 | precision highp float;
5 | attribute vec2 position;
6 | attribute vec2 uvs;
7 | uniform mat4 model, view, projection;
8 | uniform highp int timePeriod;
9 | uniform float scale;
10 | varying vec2 mercator;
11 |
12 | ${lngLatToMercator()}
13 |
14 | void main() {
15 | mercator = lngLatToMercator(position, scale);
16 | gl_Position = projection * view * vec4(mercator, 0.0, 1.0);
17 | }
18 | `
19 |
--------------------------------------------------------------------------------
/src/js/index.tsx:
--------------------------------------------------------------------------------
1 | require('es6-promise/auto')
2 |
3 | import * as React from 'react'
4 | import * as ReactDOM from 'react-dom'
5 | import App from '@app/components/App'
6 |
7 | require('@scss/styles.scss')
8 |
9 | const div = document.createElement('div')
10 | div.className = 'container'
11 | document.body.appendChild(div)
12 |
13 | ReactDOM.render(
14 | ,
15 | div
16 | )
17 |
--------------------------------------------------------------------------------
/src/js/models/Atlas.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { observable } from 'mobx'
3 |
4 | class Atlas {
5 | data: Uint8Array
6 | config: any
7 | @observable loadProgress: number = 0
8 |
9 | async initialize ({
10 | url,
11 | configUrl,
12 | }: {
13 | url: string
14 | configUrl: string
15 | }) {
16 | const configResponse = await axios.get(configUrl)
17 |
18 | this.config = configResponse.data
19 |
20 | const atlasResponse = await axios.get(url, {
21 | responseType: 'arraybuffer',
22 | onDownloadProgress: (e: any) => {
23 | this.loadProgress = Math.min(
24 | e.loaded /
25 | this.config.approximateDownloadSize *
26 | 100,
27 | 100
28 | )
29 | },
30 | })
31 |
32 | this.data = new Uint8Array(atlasResponse.data)
33 | }
34 | }
35 |
36 | export default Atlas
37 |
--------------------------------------------------------------------------------
/src/js/models/BoundingBox.ts:
--------------------------------------------------------------------------------
1 | import { observable, computed } from 'mobx'
2 | import * as _ from 'lodash'
3 | import { sinusoidalToLngLat } from '@app/utils'
4 | import Point from '@app/models/Point'
5 |
6 | class BoundingBox {
7 | @observable min: Point
8 | @observable max: Point
9 |
10 | constructor ({ min, max }: { min: Point, max: Point } = {
11 | min: new Point(0, 0),
12 | max: new Point(0, 0),
13 | }) {
14 | this.min = min
15 | this.max = max
16 | }
17 |
18 | @computed get triangles (): any {
19 | return [
20 | [this.min.x, this.min.y],
21 | [this.max.x, this.min.y],
22 | [this.max.x, this.max.y],
23 | [this.min.x, this.min.y],
24 | [this.max.x, this.max.y],
25 | [this.min.x, this.max.y],
26 | ]
27 | }
28 |
29 | @computed get outline (): any {
30 | return [
31 | [this.min.x, this.min.y],
32 | [this.max.x, this.min.y],
33 | [this.max.x, this.min.y],
34 | [this.max.x, this.max.y],
35 | [this.max.x, this.max.y],
36 | [this.min.x, this.max.y],
37 | [this.min.x, this.max.y],
38 | [this.min.x, this.min.y],
39 | ]
40 | }
41 |
42 | @computed get array (): number[] {
43 | return [
44 | this.min.x,
45 | this.min.y,
46 | this.max.x,
47 | this.max.y,
48 | ]
49 | }
50 |
51 | set array (bbox: number[]) {
52 | this.min = new Point(bbox[0], bbox[1])
53 | this.max = new Point(bbox[2], bbox[3])
54 | }
55 |
56 | static fromArray (bbox: number[]): BoundingBox {
57 | const result = new BoundingBox()
58 | result.array = bbox
59 | return result
60 | }
61 |
62 | get lngLatFromSinusoidal (): BoundingBox {
63 | const points = [
64 | sinusoidalToLngLat({ x: this.min.x, y: this.min.y }),
65 | sinusoidalToLngLat({ x: this.max.x, y: this.min.y }),
66 | sinusoidalToLngLat({ x: this.min.x, y: this.max.y }),
67 | sinusoidalToLngLat({ x: this.max.x, y: this.max.y }),
68 | ]
69 |
70 | return new BoundingBox({
71 | min: new Point(_.minBy(points, 'x').x, _.minBy(points, 'y').y),
72 | max: new Point(_.maxBy(points, 'x').x, _.maxBy(points, 'y').y),
73 | })
74 | }
75 |
76 | @computed get center (): Point {
77 | return new Point(
78 | (this.max.x + this.min.x) / 2,
79 | (this.max.y + this.min.y) / 2
80 | )
81 | }
82 |
83 | set center (newCenter: Point) {
84 | const xChange = newCenter.x - this.center.x
85 | const yChange = newCenter.y - this.center.y
86 | this.min.x += xChange
87 | this.max.x += xChange
88 | this.min.y += yChange
89 | this.max.y += yChange
90 | }
91 |
92 | scaled (scale: number): BoundingBox {
93 | return new BoundingBox({
94 | min: new Point(
95 | (this.min.x - this.center.x) * scale + this.center.x,
96 | (this.min.y - this.center.y) * scale + this.center.y,
97 | ),
98 | max: new Point(
99 | (this.max.x - this.center.x) * scale + this.center.x,
100 | (this.max.y - this.center.y) * scale + this.center.y,
101 | ),
102 | })
103 | }
104 |
105 | @computed get square (): BoundingBox {
106 | const size = Math.max(this.max.x - this.min.x, this.max.y - this.min.y) / 2
107 | return new BoundingBox({
108 | min: new Point(
109 | this.center.x - size,
110 | this.center.y - size
111 | ),
112 | max: new Point(
113 | this.center.x + size,
114 | this.center.y + size
115 | ),
116 | })
117 | }
118 |
119 | contains (point: Point): boolean {
120 | return (
121 | _.inRange(point.x, this.min.x, this.max.x) &&
122 | _.inRange(point.y, this.min.y, this.max.y)
123 | )
124 | }
125 | }
126 |
127 | export default BoundingBox
128 |
--------------------------------------------------------------------------------
/src/js/models/Camera.ts:
--------------------------------------------------------------------------------
1 | import * as REGL from 'regl'
2 | import * as _ from 'lodash'
3 | import { interpolateNumber } from 'd3'
4 | import { observable, computed } from 'mobx'
5 | import constants from '@app/constants'
6 | import Point from '@app/models/Point'
7 | import BoundingBox from '@app/models/BoundingBox'
8 | import * as Viewport from 'viewport-mercator-project'
9 |
10 | class Camera {
11 | @observable size: Point
12 | @observable position: Point
13 | @observable boundingBox: BoundingBox
14 | @observable pitch = 0
15 | @observable bearing = 0
16 | @observable altitude = 1.5
17 | @observable _zoom: number
18 |
19 | constructor ({ size, boundingBox }: {
20 | size: Point,
21 | boundingBox: BoundingBox
22 | }) {
23 | this.size = size
24 | this.boundingBox = boundingBox
25 | this.reset()
26 | }
27 |
28 | @computed get zoom () { return this._zoom }
29 | set zoom (value: number) {
30 | this._zoom = _.clamp(value, 0, 1)
31 | }
32 |
33 | reset () {
34 | this.zoom = 0
35 | this.position = this.boundingBox.center
36 | this.position.boundingBoxConstraint = this.boundingBox
37 | }
38 |
39 | pixelToLngLat (pixel: Point) {
40 | return Point.fromArray(this.viewport.unproject(pixel.array))
41 | }
42 |
43 | lngLatDelta (fromPixel: Point, toPixel: Point) {
44 | const fromLngLat = this.pixelToLngLat(fromPixel)
45 | const toLngLat = this.pixelToLngLat(toPixel)
46 |
47 | return new Point(toLngLat.x - fromLngLat.x, toLngLat.y - fromLngLat.y)
48 | }
49 |
50 | @computed get viewport (): Viewport.WebMercatorViewport {
51 | const lngLatZoom = Viewport.fitBounds({
52 | width: this.size.x,
53 | height: this.size.y,
54 | padding: 25,
55 | bounds: [
56 | this.boundingBox.min.array,
57 | this.boundingBox.max.array,
58 | ],
59 | })
60 |
61 | // Attempt to set a max zoom which contains a reasonable number
62 | // of data pixels, regardless of window size
63 | const maxZoom = Math.log2(Math.min(this.size.x, this.size.y) * 2)
64 |
65 | return new Viewport.WebMercatorViewport({
66 | pitch: this.pitch,
67 | bearing: this.bearing,
68 | altitude: this.altitude,
69 | longitude: this.position.x,
70 | latitude: this.position.y,
71 | zoom: interpolateNumber(lngLatZoom.zoom, maxZoom)(this.zoom),
72 | width: this.size.x,
73 | height: this.size.y,
74 | })
75 | }
76 |
77 | @computed get renderInfo (): {
78 | scale: number
79 | view: REGL.Mat4
80 | projection: REGL.Mat4
81 | } {
82 | return {
83 | scale: this.viewport.scale * constants.TILE_SIZE,
84 | view: this.viewport.viewMatrix,
85 | projection: this.viewport.projectionMatrix,
86 | }
87 | }
88 | }
89 |
90 | export default Camera
91 |
--------------------------------------------------------------------------------
/src/js/models/Point.ts:
--------------------------------------------------------------------------------
1 | import { observable, computed, action } from 'mobx'
2 | import * as _ from 'lodash'
3 | import BoundingBox from '@app/models/BoundingBox'
4 |
5 | class Point {
6 | @observable _x: number
7 | @observable _y: number
8 | @observable boundingBoxConstraint: BoundingBox
9 |
10 | constructor (x: number, y: number) {
11 | this.set(x, y)
12 | }
13 |
14 | @computed get x () { return this._x }
15 | set x (value: number) {
16 | if (this.boundingBoxConstraint !== undefined) {
17 | this._x = _.clamp(
18 | value,
19 | this.boundingBoxConstraint.min.x,
20 | this.boundingBoxConstraint.max.x
21 | )
22 | } else {
23 | this._x = value
24 | }
25 | }
26 |
27 | @computed get y () { return this._y }
28 | set y (value: number) {
29 | if (this.boundingBoxConstraint !== undefined) {
30 | this._y = _.clamp(
31 | value,
32 | this.boundingBoxConstraint.min.y,
33 | this.boundingBoxConstraint.max.y
34 | )
35 | } else {
36 | this._y = value
37 | }
38 | }
39 |
40 | @action set (x: number, y: number) {
41 | this.x = x
42 | this.y = y
43 | }
44 |
45 | @computed get array () {
46 | return [this.x, this.y]
47 | }
48 |
49 | distanceTo (other: Point) {
50 | return Math.sqrt(
51 | Math.pow(this.x - other.x, 2) +
52 | Math.pow(this.y - other.y, 2)
53 | )
54 | }
55 |
56 | static fromArray (array: number[]): Point {
57 | return new Point(array[0], array[1])
58 | }
59 | }
60 |
61 | export default Point
62 |
--------------------------------------------------------------------------------
/src/js/models/RootStore.ts:
--------------------------------------------------------------------------------
1 | import { observable, computed } from 'mobx'
2 | import * as _ from 'lodash'
3 | import constants, { Modes } from '@app/constants'
4 | import Point from '@app/models/Point'
5 | import BoundingBox from '@app/models/BoundingBox'
6 | import Camera from '@app/models/Camera'
7 | import VectorLayer from '@app/models/VectorLayer'
8 | import Atlas from '@app/models/Atlas'
9 |
10 | class RootStore {
11 | @observable initialized: boolean = false
12 | @observable compatible: boolean = true
13 | @observable menuOpen: boolean = false
14 | @observable moreInfoOpen: boolean = false
15 |
16 | @observable camera: Camera
17 | @observable vectorLayer: VectorLayer
18 | @observable ndviAtlas: Atlas
19 | @observable ndviAnomalyAtlas: Atlas
20 | @observable boundingBox = observable(new BoundingBox())
21 | @observable mode: Modes = Modes.NDVI
22 |
23 | @observable timePeriod: number = constants.START_TIME_PERIOD
24 |
25 | @computed get atlas () {
26 | switch (this.mode) {
27 | case Modes.NDVI:
28 | case Modes.NDVI_GROUPED:
29 | return this.ndviAtlas
30 | break
31 | case Modes.NDVI_ANOMALY:
32 | case Modes.NDVI_ANOMALY_GROUPED:
33 | return this.ndviAnomalyAtlas
34 | break
35 | }
36 | }
37 |
38 | @computed get modeConfig () {
39 | return constants.MODE_CONFIGS[this.mode]
40 | }
41 |
42 | @computed get numTimePeriods () {
43 | return this.atlas.config.numRasters
44 | }
45 |
46 | readonly timePeriodAverages = observable([])
47 |
48 | @computed get timePeriodsByMonth () {
49 | return _.times(this.numTimePeriods, (i) => {
50 | const n = this.numTimePeriods
51 | const id = ((i * 12) % n) + Math.floor((i * 12) / n)
52 |
53 | return {
54 | id,
55 | average: this.timePeriodAverages[id],
56 | }
57 | })
58 | }
59 |
60 | @computed get percentLoaded () {
61 | return (
62 | (this.ndviAtlas ? this.ndviAtlas.loadProgress : 0) +
63 | (this.ndviAnomalyAtlas ? this.ndviAnomalyAtlas.loadProgress : 0)
64 | ) / 2
65 | }
66 |
67 | @computed get rasterWidth () {
68 | return this.atlas.config.rasterWidth
69 | }
70 |
71 | @computed get rasterHeight () {
72 | return this.atlas.config.rasterHeight
73 | }
74 |
75 | @computed get rasterSizePercent () {
76 | return new Point(
77 | 1 / this.atlas.config.rastersWide,
78 | 1 / this.atlas.config.rastersHigh
79 | )
80 | }
81 |
82 | @computed get textureRastersWide () {
83 | return this.atlas.config.rastersWide
84 | }
85 |
86 | @computed get textureRastersHigh () {
87 | return this.atlas.config.rastersHigh
88 | }
89 |
90 | @computed get samplesWide () {
91 | return this.textureRastersWide * 4
92 | }
93 |
94 | async initialize () {
95 | this.vectorLayer = new VectorLayer()
96 | await this.vectorLayer.initialize(constants.VECTOR_URL)
97 |
98 | this.ndviAtlas = new Atlas()
99 | this.ndviAnomalyAtlas = new Atlas()
100 |
101 | await Promise.all([
102 | this.ndviAtlas.initialize({
103 | url: constants.MODE_CONFIGS[Modes.NDVI].ATLAS,
104 | configUrl: constants.MODE_CONFIGS[Modes.NDVI].ATLAS_CONFIG,
105 | }),
106 | this.ndviAnomalyAtlas.initialize({
107 | url: constants.MODE_CONFIGS[Modes.NDVI_ANOMALY].ATLAS,
108 | configUrl: constants.MODE_CONFIGS[Modes.NDVI_ANOMALY].ATLAS_CONFIG,
109 | }),
110 | ])
111 |
112 | this.boundingBox = observable(BoundingBox.fromArray(
113 | this.atlas.config.boundingBox
114 | ))
115 |
116 | const startingBox = this.boundingBox.square.lngLatFromSinusoidal.scaled(1.1)
117 |
118 | this.camera = new Camera({
119 | size: new Point(100, 100),
120 | boundingBox: BoundingBox.fromArray(startingBox.array),
121 | })
122 |
123 | this.initialized = true
124 | }
125 |
126 | @computed get selectedBox (): BoundingBox {
127 | const size = this.camera.size
128 | const center = new Point(size.x / 2, size.y / 2)
129 | const extent = Math.min(size.x, size.y) / 2 -
130 | constants.SELECTED_BOX_PADDING
131 |
132 | const minPixel = new Point(center.x - extent, center.y + extent)
133 | const maxPixel = new Point(center.x + extent, center.y - extent)
134 |
135 | return new BoundingBox({
136 | min: this.camera.pixelToLngLat(minPixel),
137 | max: this.camera.pixelToLngLat(maxPixel),
138 | })
139 | }
140 | }
141 |
142 | export default RootStore
143 |
--------------------------------------------------------------------------------
/src/js/models/VectorLayer.ts:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx'
2 | import earcut from 'earcut'
3 | import axios from 'axios'
4 |
5 | class VectorLayer {
6 | readonly outline = observable([])
7 | readonly vertices = observable([])
8 | readonly indices = observable([])
9 | readonly holes = observable([])
10 | @observable dimensions: number
11 |
12 | async initialize (url: string) {
13 | const response = await axios.get(url)
14 | const vectors = response.data
15 |
16 | vectors.geometry.coordinates.forEach((polygon: any) => {
17 | const data = earcut.flatten(polygon)
18 | const polygonOutline: number[] = []
19 | for (let i = 0; i < (data.vertices.length - 3); i += 2) {
20 | polygonOutline.push(data.vertices[i])
21 | polygonOutline.push(data.vertices[i + 1])
22 | polygonOutline.push(data.vertices[i + 2])
23 | polygonOutline.push(data.vertices[i + 3])
24 | }
25 |
26 | this.outline.replace(this.outline.concat(polygonOutline))
27 | this.vertices.replace(this.vertices.concat(data.vertices))
28 | this.holes.replace(this.vertices.concat(data.holes))
29 | this.dimensions = data.dimensions
30 | this.indices.replace(this.indices.concat(
31 | earcut(data.vertices, data.holes, data.dimensions)
32 | ))
33 | })
34 | }
35 | }
36 |
37 | export default VectorLayer
38 |
--------------------------------------------------------------------------------
/src/js/scales.ts:
--------------------------------------------------------------------------------
1 | import {
2 | scaleLinear,
3 | scalePoint,
4 | scaleSequential,
5 | scaleBand,
6 | range,
7 | color,
8 | interpolateBrBG,
9 | easeQuadInOut,
10 | } from 'd3'
11 |
12 | import * as _ from 'lodash'
13 |
14 | import {
15 | interpolateViridis,
16 | } from 'd3-scale-chromatic'
17 |
18 | import constants, { Modes } from '@app/constants'
19 |
20 | export const makeYScaleNDVI = ({ height, margin }: {
21 | height: number,
22 | margin: any,
23 | }) => (
24 | scaleLinear()
25 | .domain(constants.MODE_CONFIGS[Modes.NDVI].CHART_RANGE)
26 | .range([height - margin.bottom, margin.top])
27 | )
28 |
29 | export const makeYScaleNDVIAnomaly = ({ height, margin }: {
30 | height: number,
31 | margin: any,
32 | }) => (
33 | scaleLinear()
34 | .domain(constants.MODE_CONFIGS[Modes.NDVI_ANOMALY].CHART_RANGE)
35 | .range([height - margin.bottom, margin.top])
36 | )
37 |
38 | export const makeXScale = ({ numTimePeriods, width, margin }: {
39 | numTimePeriods: number,
40 | width: number,
41 | margin: any,
42 | }) => (
43 | scalePoint()
44 | .domain(range(-2, numTimePeriods))
45 | .range([
46 | margin.left,
47 | width - margin.right,
48 | ])
49 | )
50 |
51 | export const makeXScaleSortedBands = ({ width, margin }: {
52 | width: number,
53 | margin: any,
54 | }) => (
55 | scaleBand()
56 | .domain(constants.MONTHS)
57 | .range([margin.left, width - margin.right])
58 | .padding(0.15)
59 | )
60 |
61 | export const makeXScaleSorted = ({ numTimePeriods, width, margin }: {
62 | numTimePeriods: number,
63 | width: number,
64 | margin: any,
65 | }) => {
66 | const bands = makeXScaleSortedBands({ width, margin })
67 |
68 | const scale: any = (i: number) => {
69 | const n = numTimePeriods
70 | const month = i % 12
71 | const year = Math.floor(i / 12)
72 | const years = Math.floor(n / 12)
73 |
74 | return bands(constants.MONTHS[month]) +
75 | (bands.bandwidth() * (year) / (years - 1))
76 | }
77 |
78 | scale.step = () => (
79 | bands.bandwidth() /
80 | (numTimePeriods / 12)
81 | )
82 |
83 | return scale
84 | }
85 |
86 | // Creates an array of 9 interpolated vec4 colors for a d3
87 | // color scale interpolator. These can be passed into the
88 | // colorScale shader.
89 | export const glColors = (interpolate: any) => (
90 | _.times(9, (i: number) => {
91 | const interpolated = color(interpolate(i / 8))
92 |
93 | return [
94 | interpolated.r / 255,
95 | interpolated.g / 255,
96 | interpolated.b / 255,
97 | 1.0,
98 | ]
99 | })
100 | )
101 |
102 | export const makeColorScaleNDVI = () =>
103 | scaleSequential(interpolateViridis).domain(
104 | constants.MODE_CONFIGS[Modes.NDVI].RANGE
105 | )
106 |
107 | export const GL_COLORS_NDVI = glColors(interpolateViridis)
108 |
109 | const interpolateNDVIAnomaly = (i: number) => (
110 | interpolateBrBG(easeQuadInOut(i))
111 | )
112 |
113 | export const makeColorScaleNDVIAnomaly = () =>
114 | scaleSequential(interpolateNDVIAnomaly).domain(
115 | constants.MODE_CONFIGS[Modes.NDVI_ANOMALY].RANGE
116 | )
117 |
118 | export const GL_COLORS_NDVI_ANOMALY = glColors(interpolateNDVIAnomaly)
119 |
--------------------------------------------------------------------------------
/src/js/utils.ts:
--------------------------------------------------------------------------------
1 | const EARTH_CIRCUMFERENCE: number = 6371007 * Math.PI * 2
2 |
3 | export const sinusoidalToLngLat = (
4 | { x, y }: { x: number, y: number }
5 | ) => {
6 | const normalizedY: number = y / EARTH_CIRCUMFERENCE * 2 + 0.5
7 | const scaledX: number = x / Math.sin(Math.PI * normalizedY)
8 | const normalizedX: number = scaledX / EARTH_CIRCUMFERENCE * 2 + 0.5
9 | return {
10 | x: (normalizedX - 0.5) * 180,
11 | y: (normalizedY - 0.5) * 180,
12 | }
13 | }
14 |
15 | export const translate = (x: number, y: number) => (
16 | `translate(${x} ${y})`
17 | )
18 |
19 | export const debugImageFromArray = (
20 | { data, width, height }: { data: any, width: number, height: number }
21 | ) => {
22 | const rgbaArray = data.map((x: number, i: number) => (
23 | [
24 | x,
25 | x,
26 | x,
27 | 255,
28 | ][i % 4]
29 | ))
30 |
31 | let canvas = (document.querySelector('.debug-image') as HTMLCanvasElement)
32 | if (canvas === null) {
33 | canvas = document.createElement('canvas')
34 | canvas.className = 'debug-image'
35 | document.body.appendChild(canvas)
36 | }
37 |
38 | const ctx = canvas.getContext('2d')
39 |
40 | canvas.width = width
41 | canvas.height = height
42 |
43 | const imageData = ctx.createImageData(width, height)
44 | imageData.data.set(rgbaArray)
45 | ctx.putImageData(imageData, 0, 0)
46 | }
47 |
48 | export const compensatedSquareUVs = ({ width, height }: {
49 | width: number,
50 | height: number
51 | }) => {
52 | return [
53 | [-0.5, -0.5],
54 | [width - 0.5, -0.5],
55 | [width - 0.5, height - 0.5],
56 | [-0.5, -0.5],
57 | [width - 0.5, height - 0.5],
58 | [-0.5, height - 0.5],
59 | ]
60 | }
61 |
62 | export const uniformArrayAsObject = (name: string, array: any[]): any => {
63 | const object: any = {}
64 | array.forEach((entry, i) => {
65 | object[`${name}[${i}]`] = entry
66 | })
67 |
68 | return object
69 | }
70 |
--------------------------------------------------------------------------------
/src/scss/styles.scss:
--------------------------------------------------------------------------------
1 | @import 'range-slider-sass';
2 | $thumb-color: #fff;
3 | $thumb-radius: 100%;
4 | $thumb-height: 1rem;
5 | $thumb-width: 1rem;
6 | $track-color: #888;
7 | $track-height: 0.5rem;
8 | $track-border-width: 0;
9 |
10 | $text-color: #333;
11 | $link-color: #3384dd;
12 |
13 | input[type=range] {
14 | @include input-type-range();
15 | }
16 |
17 | * { margin: 0; padding: 0; list-style: none; }
18 |
19 | html {
20 | color: $text-color;
21 | font-size: 100%;
22 | }
23 |
24 | html, body, .container {
25 | height: 100%;
26 | }
27 |
28 | .container {
29 | display: flex;
30 | flex-direction: column;
31 | }
32 |
33 | body, select, input, button {
34 | font-family: 'Heebo', sans-serif;
35 | font-weight: 300;
36 | }
37 |
38 | select, input, button {
39 | font-size: 1em;
40 | background: white;
41 | border: 1px solid #33333360;
42 | border-radius: 0.25rem;
43 | appearance: none;
44 | }
45 |
46 | select {
47 | padding: 0 2.25rem 0 0.75rem;
48 | background: url('/img/select-arrow.svg') right no-repeat;
49 | }
50 |
51 | a, a:visited {
52 | text-decoration: none;
53 | color: $link-color;
54 | font-weight: 400;
55 | }
56 |
57 | p:not(:first-child) { margin-top: 0.75rem; }
58 |
59 | p.form-line span:not(:first-child) {
60 | padding: 0 0.25rem;
61 | }
62 |
63 | h1 {
64 | text-align: left;
65 | font-weight: 300;
66 | font-size: 1.5rem;
67 | >img {
68 | height: 1.8rem;
69 | position: relative;
70 | padding-right: 0.5rem;
71 | top: 0.45rem;
72 | margin-top: -0.5rem;
73 | }
74 | }
75 |
76 | header {
77 | display: flex;
78 | align-items: center;
79 | justify-content: space-between;
80 | flex: 0 0 auto;
81 | border-bottom: 1px solid #bbb;
82 |
83 | >* {
84 | flex-basis: 22rem;
85 | text-align: center;
86 | &.nav { flex-shrink: 0 }
87 | &.nav>img { display: none; }
88 | &.center { flex-grow: 1; }
89 | }
90 |
91 | .info {
92 | font-size: 0.9rem;
93 | margin-right: 1rem;
94 | white-space: nowrap;
95 | i {
96 | font-size: 0.8rem;
97 | font-family: times;
98 | font-weight: bold;
99 | padding: 0 0.4rem;
100 | background: $link-color;
101 | border-radius: 1rem;
102 | color: white;
103 | position: relative;
104 | bottom: 0.05rem;
105 | }
106 | }
107 |
108 | padding: 0.75rem 1rem;
109 | box-shadow: 0 0 4px #0008;
110 | }
111 |
112 | .single-view {
113 | flex-grow: 1;
114 | display: flex;
115 | position: relative;
116 | min-height: 0;
117 |
118 | canvas { min-height: 50px; min-width: 50px; }
119 |
120 | .zoom {
121 | position: absolute;
122 | bottom: 0;
123 | right: 0;
124 |
125 | >label {
126 | width: 3rem;
127 | height: 9.56rem;
128 | display: flex;
129 | flex-direction: column;
130 | background: #222c;
131 |
132 | img, input { display: block; }
133 | img {
134 | color: white;
135 | height: 1rem;
136 | padding: 0.6rem 1rem;
137 | cursor: pointer;
138 | opacity: 0.8;
139 | position: relative;
140 | bottom: -0.1rem;
141 | &:hover { opacity: 1; }
142 |
143 | &.bottom {
144 | position: absolute;
145 | bottom: 0.25rem;
146 | }
147 | }
148 |
149 | input {
150 | transform: rotate(-90deg) translate(-2.1rem, -0.83rem);
151 | width: 4.5rem;
152 | }
153 | }
154 | }
155 | }
156 |
157 | .pinch-area {
158 | touch-action: none;
159 | user-select: none;
160 | }
161 |
162 | .full-width { width: 100%; }
163 | .full-size { width: 100%; height: 100%; }
164 |
165 | footer {
166 | padding: 1.5rem;
167 | border-top: 1px solid #bbb;
168 | input[type=range] { width: 100% }
169 | }
170 |
171 | .info-overlay {
172 | display: flex;
173 | position: absolute;
174 | width: 100%;
175 | height: 100%;
176 | background: #0006;
177 |
178 | .overlay { flex-grow: 1; }
179 |
180 | .info {
181 | width: 24rem;
182 | padding: 0.75rem 1rem;
183 | box-sizing: border-box;
184 | background: white;
185 | display: flex;
186 | flex-direction: column;
187 | }
188 | }
189 |
190 | .right-nav {
191 | cursor: pointer;
192 | color: $text-color;
193 | transition: color 0.1s;
194 | display: flex;
195 | justify-content: space-between;
196 | align-self: flex-start;
197 | flex-shrink: 0;
198 | width: 100%;
199 | position: relative;
200 | top: 0.15rem;
201 |
202 | &:hover {
203 | color: $link-color;
204 | }
205 |
206 | .logo {
207 | text-align: right;
208 | padding: 0;
209 | img {
210 | height: 2rem;
211 | vertical-align: middle;
212 | }
213 | }
214 |
215 | h1 { display: none; }
216 | }
217 |
218 | .right-nav-items {
219 | display: flex;
220 | flex-direction: column;
221 | margin: 2rem 0;
222 | >* { padding: 0.75rem 0; }
223 | a:not(:hover) { color: $text-color; }
224 | .mobile { display: none; }
225 | }
226 |
227 |
228 | .right-nav, .right-nav-items {
229 | svg {
230 | display: inline;
231 | height: 1.25rem;
232 | position: relative;
233 | padding-right: 0.25rem;
234 | top: 0.25rem;
235 | }
236 | }
237 |
238 | .horizontal-chart {
239 | height: 15rem;
240 | display: flex;
241 | }
242 |
243 | .time-series {
244 | flex: 0 0 100%;
245 | overflow: visible;
246 | line { stroke: #333; stroke-width: 1px; }
247 | .tick {
248 | * { pointer-events: none; user-select: none }
249 | text { fill: #777; }
250 | line { stroke: #aaa; }
251 | line.white { stroke: #fff; }
252 |
253 | &.y-tick {
254 | text { text-anchor: end; }
255 | }
256 |
257 | &.x-tick {
258 | line { stroke: #ddd; }
259 | text { text-anchor: start; transform: rotate(45deg); }
260 | rect.divider { fill: #fff; }
261 | }
262 | }
263 |
264 | .brush polygon {
265 | transition: transform 0.15s; transform: scale(0.8);
266 | }
267 |
268 | &:hover .brush polygon { transform: scale(1); }
269 |
270 | .brush {
271 | text-anchor: middle;
272 | pointer-events: none;
273 | user-select: none;
274 | }
275 |
276 | .mouse-target { fill: none; pointer-events: all; }
277 |
278 | .legend {
279 | fill: #333;
280 | pointer-events: none;
281 | user-select: none;
282 | }
283 |
284 | .average-background circle { stroke: #888; }
285 | }
286 |
287 | .centered-content {
288 | width: 100%;
289 | height: 100%;
290 | display: flex;
291 | align-items: center;
292 | justify-content: center;
293 |
294 | .incompatible {
295 | width: 25rem;
296 | padding: 1.5rem;
297 | }
298 |
299 | .progress-bar {
300 | width: 300px;
301 | height: 20px;
302 | overflow: visible;
303 |
304 | .progress-bar-background {
305 | fill: #eee;
306 | stroke: #eee;
307 | stroke-width: 5px;
308 | }
309 |
310 | .progress-bar-bar { fill: #56c; }
311 |
312 | text { text-anchor: middle; }
313 | }
314 | }
315 |
316 | .debug-image {
317 | float: left;
318 | width: 512px;
319 | height: 512px;
320 | margin-right: 10px;
321 | image-rendering: pixelated;
322 | }
323 |
324 | @media (max-width: 1000px) {
325 | html { font-size: 14px; }
326 |
327 | header {
328 | .center { display: none; }
329 | .right-nav {
330 | justify-content: flex-end;
331 | flex-shrink: 1;
332 | }
333 | }
334 |
335 | .right-nav, .right-nav-items {
336 | &.open h1 { display: block; }
337 | .button-label { display: none; }
338 | }
339 |
340 | .logo { display: none; }
341 | .right-nav-items {
342 | padding-left: 0.2rem;
343 | .mobile { display: block; }
344 | .logo {
345 | display: block;
346 | img { height: 2rem; }
347 | }
348 | }
349 |
350 | footer {
351 | padding: 1.5rem 1rem 0.5rem 0;
352 | margin-left: -12px;
353 | .y-tick text { display: none; }
354 | .brush {
355 | text { display: none; }
356 | rect { opacity: 0.75; }
357 | }
358 | }
359 |
360 | .info-overlay {
361 | .overlay { display: none; }
362 | .info { flex-basis: 100%; }
363 | }
364 |
365 | .time-series .by-year .x-tick:nth-child(even) text {
366 | display: none;
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "noImplicitAny": true,
6 | "target": "es5",
7 | "jsx": "react",
8 | "allowJs": true,
9 | "lib": ["es2018", "dom"],
10 | "experimentalDecorators": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "jsRules": {},
7 | "rules": {
8 | "no-empty": false,
9 | "no-empty-interface": false,
10 | "semicolon": [true, "never"],
11 | "object-literal-sort-keys": false,
12 | "quotemark": [true, "single"],
13 | "ordered-imports": false,
14 | "member-access": false,
15 | "member-ordering": false,
16 | "space-before-function-paren": [true, "always"],
17 | "no-var-requires": false,
18 | "array-type": false,
19 | "arrow-parens": false,
20 | "no-unused-variable": true,
21 | "trailing-comma": [
22 | true,
23 | {
24 | "multiline": {
25 | "objects": "always",
26 | "arrays": "always",
27 | "functions": "ignore",
28 | "typeLiterals": "ignore"
29 | },
30 | "esSpecCompliant": true
31 | }
32 | ],
33 | "variable-name": [
34 | true,
35 | "ban-keywords",
36 | "check-format",
37 | "allow-pascal-case",
38 | "allow-leading-underscore"
39 | ]
40 | },
41 | "rulesDirectory": []
42 | }
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const CopyWebpackPlugin = require('copy-webpack-plugin')
3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin")
4 | const HtmlWebpackPlugin = require('html-webpack-plugin')
5 |
6 | module.exports = {
7 | entry: './src/js/index.tsx',
8 | module: {
9 | rules: [
10 | {
11 | test: /\.worker\.js$/,
12 | use: { loader: 'worker-loader' }
13 | },
14 | {
15 | test: /\.(ts|tsx|glsl)$/,
16 | enforce: 'pre',
17 | use: [
18 | {
19 | loader: 'tslint-loader',
20 | options: {
21 | emitErrors: true,
22 | tsConfigFile: 'tsconfig.json',
23 | typeCheck: true
24 | }
25 | }
26 | ]
27 | },
28 | {
29 | test: /\.tsx?$/,
30 | use: 'ts-loader',
31 | exclude: /node_modules/
32 | },
33 | {
34 | test: /\.scss$/,
35 | use: [
36 | MiniCssExtractPlugin.loader,
37 | "css-loader",
38 | "sass-loader",
39 | "postcss-loader"
40 | ]
41 | },
42 | {
43 | test: /\.(tiff?)$/,
44 | exclude: /node_modules/,
45 | loader: 'file-loader'
46 | }
47 | ]
48 | },
49 | plugins: [
50 | new CopyWebpackPlugin([{ from: 'src/assets', to: '.' }]),
51 | new MiniCssExtractPlugin('styles.css'),
52 | new HtmlWebpackPlugin({
53 | title: 'Iceland Vegetation',
54 | meta: {
55 | 'og:url': 'https://iceland.visualperspective.io/',
56 | 'og:title': 'Iceland Vegetation',
57 | 'og:description': 'Visualize Iceland\'s vegetation over time.',
58 | 'og:site_name': 'Iceland Vegetation',
59 | 'og:image': 'https://iceland.visualperspective.io/img/iceland_ndvi.png'
60 | }
61 | })
62 | ],
63 | output: {
64 | filename: '[name].js',
65 | path: path.resolve(__dirname, 'dist')
66 | },
67 | resolve: {
68 | modules: ['node_modules'],
69 | extensions: ['.ts', '.tsx', '.js', '.json', '.glsl.ts'],
70 | alias: {
71 | '@app': path.resolve(__dirname, 'src/js/'),
72 | '@scss': path.resolve(__dirname, 'src/scss/'),
73 | '@assets': path.resolve(__dirname, 'src/assets/'),
74 | }
75 | },
76 | node: {
77 | fs: 'empty'
78 | }
79 | };
80 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const merge = require('webpack-merge');
3 | const common = require('./webpack.common.js');
4 |
5 | module.exports = merge(common, {
6 | mode: 'development',
7 | devtool: 'inline-source-map',
8 | plugins: common.plugins.concat([
9 | new webpack.DefinePlugin({
10 | 'process.env.NDVI_ATLAS': JSON.stringify('atlas/ndvi.atlas'),
11 | 'process.env.NDVI_ANOMALY_ATLAS': JSON.stringify('atlas/ndvi-anomaly.atlas'),
12 | 'process.env.PROFILE': true,
13 | 'process.env.SITE_URL': JSON.stringify('https://vp-stage.firebaseapp.com'),
14 | })
15 | ])
16 | });
17 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const merge = require('webpack-merge');
3 | const common = require('./webpack.common.js');
4 |
5 | module.exports = merge(common, {
6 | mode: 'production',
7 | devtool: 'source-map',
8 | plugins: common.plugins.concat([
9 | new webpack.DefinePlugin({
10 | 'process.env.NDVI_ATLAS': JSON.stringify('https://storage.googleapis.com/iceland-ndvi/static/ndvi.atlas'),
11 | 'process.env.NDVI_ANOMALY_ATLAS': JSON.stringify('https://storage.googleapis.com/iceland-ndvi/static/ndvi-anomaly.atlas'),
12 | 'process.env.PROFILE': false,
13 | 'process.env.SITE_URL': JSON.stringify('https://visualperspective.io'),
14 | })
15 | ])
16 | });
17 |
--------------------------------------------------------------------------------
/webpack.stage.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const merge = require('webpack-merge');
3 | const common = require('./webpack.common.js');
4 |
5 | module.exports = merge(common, {
6 | mode: 'production',
7 | devtool: 'source-map',
8 | plugins: common.plugins.concat([
9 | new webpack.DefinePlugin({
10 | 'process.env.NDVI_ATLAS': JSON.stringify('https://storage.googleapis.com/iceland-ndvi/static/ndvi.atlas'),
11 | 'process.env.NDVI_ANOMALY_ATLAS': JSON.stringify('https://storage.googleapis.com/iceland-ndvi/static/ndvi-anomaly.atlas'),
12 | 'process.env.PROFILE': false,
13 | 'process.env.SITE_URL': JSON.stringify('https://vp-stage.firebaseapp.com'),
14 | })
15 | ])
16 | });
17 |
--------------------------------------------------------------------------------