├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .markdownlintrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── docs ├── README.md ├── developer-guide │ ├── authentication.md │ ├── get-started.md │ └── pydeck-integration.md ├── table-of-contents.json └── whats-new.md ├── examples ├── image-collection │ ├── README.md │ ├── app.js │ ├── index.html │ ├── package.json │ └── webpack.config.js ├── image │ ├── README.md │ ├── app.js │ ├── index.html │ ├── package.json │ └── webpack.config.js ├── intl-boundary │ ├── README.md │ ├── app.js │ ├── index.html │ ├── package.json │ └── webpack.config.js ├── noaa-hurricanes │ ├── README.md │ ├── app.js │ ├── index.html │ ├── package.json │ └── webpack.config.js ├── power-plants │ ├── README.md │ ├── app.js │ ├── index.html │ ├── package.json │ └── webpack.config.js ├── shared │ ├── google-login │ │ ├── google-api.js │ │ └── google-login-provider.js │ ├── google-maps │ │ └── deck-with-google-maps.js │ ├── index.js │ ├── libs │ │ └── ee_api_js.js │ └── react-components │ │ ├── earthengine-icon.js │ │ ├── google-login-pane.js │ │ ├── icon-wrapper.js │ │ └── info-box.js └── terrain │ ├── README.md │ ├── app.js │ ├── index.html │ ├── package.json │ └── webpack.config.js ├── lerna.json ├── modules └── earthengine-layers │ ├── README.md │ ├── docs │ └── api-reference │ │ ├── earthengine-layer.md │ │ └── earthengine-terrain-layer.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── bundle.js │ ├── earth-engine-layer.js │ ├── earth-engine-terrain-layer.js │ ├── ee-api.js │ ├── index.js │ └── utils.js │ └── test │ ├── index.js │ ├── tile-layer │ └── tile-layer.spec.js │ └── tileset-2d │ ├── tileset-2d.spec.js │ └── viewport-util.spec.js ├── ocular-dev-tools.config.js ├── package.json ├── py ├── .gitignore ├── CHANGELOG.md ├── MANIFEST.in ├── README.md ├── docs │ ├── pydeck-earthengine-layer.md │ └── pydeck-earthengine-terrain-layer.md ├── examples │ ├── Introduction.ipynb │ ├── README.md │ ├── international_boundaries.ipynb │ ├── noaa_hurricanes.ipynb │ ├── power_plants.ipynb │ ├── temperature.ipynb │ └── terrain.ipynb ├── pydeck_earthengine_layers │ ├── __init__.py │ └── pydeck_earthengine_layers.py ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg └── setup.py ├── test ├── browser.js ├── compare-image.js ├── modules.js ├── node-examples.js ├── node.js ├── render │ ├── constants.js │ ├── index.js │ ├── jupyter-widget-custom-layer-bundle.js │ ├── jupyter-widget-test.html │ └── jupyter-widget.js ├── size │ └── import-nothing.js └── utils │ └── utils.js ├── webpack.config.js ├── website ├── .eslintignore ├── .gitignore ├── gatsby-config.js ├── gatsby-node.js ├── package.json ├── src │ └── components │ │ └── README.md ├── static │ ├── CNAME │ └── images │ │ ├── icon-high-precision.svg │ │ ├── image-animation-example-screenshot.gif │ │ ├── image-animation-example-still.png │ │ ├── image-animation-wide_less-bright.gif │ │ ├── image-example-screenshot.jpg │ │ ├── intl-boundaries.jpg │ │ ├── noaa_hurricanes.jpg │ │ ├── power-plants.jpg │ │ ├── terrain.jpg │ │ └── unfolded.png └── templates │ └── index.jsx └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | libs/ 2 | dist/ 3 | node_modules/ 4 | workers/ 5 | dist.js 6 | *.min.js 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | module.exports = { 3 | parserOptions: { 4 | ecmaVersion: 2018 5 | }, 6 | plugins: ['react', 'import'], 7 | extends: ['uber-jsx', 'uber-es2015', 'prettier', 'prettier/react', 'plugin:import/errors'], 8 | overrides: [{ 9 | files: ['*.spec.js', 'webpack.config.js', '**/bundle/*.js'], 10 | rules: { 11 | 'import/no-extraneous-dependencies': 0 12 | } 13 | }], 14 | settings: { 15 | 'import/core-modules': [ 16 | '@luma.gl/core', 17 | '@luma.gl/constants', 18 | 'math.gl', 19 | '@math.gl/web-mercator' 20 | ], 21 | react: { 22 | version: 'detect' 23 | } 24 | }, 25 | rules: { 26 | 'guard-for-in': 0, 27 | 'no-inline-comments': 0, 28 | camelcase: 0, 29 | 'react/forbid-prop-types': 0, 30 | 'react/no-deprecated': 0, 31 | 'import/no-unresolved': ['error', {ignore: ['test']}], 32 | 'import/no-extraneous-dependencies': 0 // ['error', {devDependencies: false, peerDependencies: true}] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | **/*.min.js 4 | node_modules/ 5 | coverage/ 6 | test/**/*-failed.png 7 | .nyc_output/ 8 | .reify-cache/ 9 | 10 | */**/yarn.lock 11 | yarn-error.log 12 | package-lock.json 13 | 14 | .vscode/ 15 | .project 16 | .idea 17 | .DS_Store 18 | *.zip 19 | *.rar 20 | *.log 21 | .exit_code 22 | 23 | # Jupyter Notebook 24 | .ipynb_checkpoints 25 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "colors": true, 4 | "header-increment": false, 5 | "line-length": false, 6 | "ul-style": {"style": "sublist"}, 7 | "no-trailing-punctuation": {"punctuation": ".,;:"}, 8 | "no-duplicate-header": false, 9 | "no-inline-html": false, 10 | "no-hard-tabs": false, 11 | "whitespace": false 12 | } 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | workers/ 2 | **/dist*/**/*.js 3 | dist.js 4 | *.min.js 5 | *.md 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 100 2 | semi: true 3 | singleQuote: true 4 | trailingComma: none 5 | bracketSpacing: false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Unfolded 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 | # earthengine-layers 2 | 3 | This repository contains [deck.gl](https://deck.gl) layers to visualize [Google Earth Engine](https://github.com/google/earthengine-api) objects in JavaScript and Python. 4 | 5 | For documentation please visit the [website](https://earthengine-layers.com). 6 | 7 | ## License 8 | 9 | MIT License 10 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # earthengine-layers 2 | 3 | [deck.gl](https://deck.gl) layers for Google Earth Engine for JavaScript and 4 | Python. 5 | 6 | The primary export is the `EarthEngineLayer` layer, which accepts [Google Earth 7 | Engine API](https://github.com/google/earthengine-api) objects (`ee.Image`, 8 | `ee.ImageCollection`, `ee.FeatureCollection`) through its `eeObject` prop, and 9 | renders the desired datasets via a customized deck.gl `TileLayer`. 10 | 11 | ## License 12 | 13 | MIT License 14 | -------------------------------------------------------------------------------- /docs/developer-guide/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | Authenticating with Earth Engine services is likely to be the biggest 4 | complication for developers who are not already working with the EE API. 5 | 6 | While the Earth Engine API documentation and forums are the official source of 7 | information on how to authenticate, this is a quick overview. 8 | 9 | ## Pydeck 10 | 11 | If using the `pydeck-earthengine-layers` package, any extra authentication steps 12 | should be handled for you automatically. You'll need only the standard 13 | authentication required by the `earthengine-api` library to work with Earth 14 | Engine Python objects, i.e.: 15 | 16 | ```py 17 | import ee 18 | 19 | # Necessary only on the first install; opens a Google sign-in prompt 20 | ee.Authenticate() 21 | 22 | # Necessary in every Python session 23 | ee.Initialize() 24 | ``` 25 | 26 | ## JavaScript 27 | 28 | The `EarthEngineLayer` provides an `initializeEEApi` helper to authenticate and 29 | initialize the JavaScript Earth Engine library. This calls 30 | `ee.data.authenticateViaOauth()` or `ee.setToken()`, and then `ee.initialize()`, 31 | and returns a `Promise` that resolves when authentication and initialization is 32 | completed and the EE API is ready to use. 33 | 34 | ### Authenticating via Login (OAuth2) 35 | 36 | The easiest way to authenticate from a JavaScript application is to use 37 | `initializeEEApi` with a `clientId`. 38 | 39 | Note that this requires: 40 | 41 | - registering a Google client id 42 | - requesting Earth Engine access for that client 43 | - adding the URLs from which the application will be served to the whitelisted origins for that client id. 44 | - Visitors to your website must have a Google Account that is approved for use with Earth Engine. 45 | 46 | ## Authenticating via Access Token 47 | 48 | If you have an existing OAuth2 authentication workflow, you can use that to 49 | generate access tokens, which you then pass to 50 | 51 | ```js 52 | initializeEEApi({token}) 53 | ``` 54 | 55 | An access token is valid for a short period of time, usually one hour, and any 56 | requests to the EarthEngine backend will fail after that time until a new access 57 | token is provided. 58 | -------------------------------------------------------------------------------- /docs/developer-guide/get-started.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | ## Installing 4 | 5 | ```sh 6 | $ yarn add @google/earthengine 7 | $ yarn add @unfolded/earthengine-layers 8 | $ yarn add deck.gl 9 | ``` 10 | 11 | ## Using in Python 12 | 13 | The `EarthEngineLayer` can be used as a plugin layer to 14 | [`pydeck`](https://pydeck.gl). For more information see [pydeck 15 | integration](/docs/developer-guide/pydeck-integration.md). 16 | 17 | ## Using in JavaScript 18 | 19 | To use the `EarthEngineLayer` in your JavaScript application to visualize Earth 20 | Engine API objects (such as `ee.Image` objects): 21 | 22 | ```js 23 | import {Deck} from '@deck.gl/core'; 24 | import {EarthEngineLayer} from '@unfolded/earthengine-layers'; 25 | import ee from '@google/earthengine'; 26 | 27 | const eeObject = ee.Image('CGIAR/SRTM90_V4'); 28 | const visParams = { 29 | min: 0, 30 | max: 4000, 31 | palette: ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'] 32 | }; 33 | 34 | new Deck({ 35 | ..., 36 | layers: new EarthEngineLayer({eeObject, visParams}) 37 | }); 38 | ``` 39 | 40 | ## Cloning the Repo 41 | 42 | ```sh 43 | git clone https://github.com/UnfoldedInc/earthengine-layers 44 | cd earthengine-layers 45 | ``` 46 | 47 | ## Running Examples 48 | 49 | You will need a Google client id which has been approved for use with Earth 50 | Engine. You also need to make sure you log in with a Google user account which 51 | has been approved for use with earth engine. [Go 52 | here](https://console.cloud.google.com/apis/credentials/oauthclient) to create a 53 | client ID, and add appropriate urls (such as `http://localhost:8080` for 54 | examples) in the "Authorized JavaScript origins" section. 55 | 56 | ```sh 57 | cd examples/image 58 | EE_CLIENT_ID=.apps.googleusercontent.com yarn start 59 | ``` 60 | 61 | ## Contributing 62 | 63 | ### Building and Testing Code 64 | 65 | ```sh 66 | git clone https://github.com/UnfoldedInc/earthengine-layers 67 | cd earthengine-layers 68 | yarn bootstrap 69 | ``` 70 | 71 | ```sh 72 | yarn lint 73 | yarn lint fix # Autoformats code 74 | yarn test 75 | ``` 76 | 77 | ## Building the Website 78 | 79 | To build the website locally (for instance if you are making contributions) 80 | 81 | ```sh 82 | cd website 83 | yarn 84 | yarn develop 85 | ``` 86 | 87 | To build the website for production 88 | 89 | ```sh 90 | cd website 91 | export EE_CLIENT_ID=... 92 | export GoogleMapsAPIKey=... 93 | yarn build 94 | yarn deploy 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/developer-guide/pydeck-integration.md: -------------------------------------------------------------------------------- 1 | # pydeck Integration 2 | 3 | The `EarthEngineLayer` can be imported into [`pydeck`](https://pydeck.gl) as a 4 | custom layer module using the `pydeck-earthengine-layers` package, making it 5 | possible to visualize planetary-scale geospatial datasets from Python. 6 | 7 | To use this layer, you'll need to authenticate with an EarthEngine-enabled 8 | Google Account. [Visit here][gee-signup] to sign up. 9 | 10 | [gee-signup]: https://signup.earthengine.google.com/#!/ 11 | 12 | ## Examples 13 | 14 | There are no interactive pydeck examples hosted on this website because the 15 | pydeck `EarthEngineLayer` authentication process requires the user to sign in 16 | with their own credentials in a Python session. 17 | 18 | Static Jupyter Notebook examples are available [on nbviewer][nbviewer-examples]. 19 | Each notebook corresponds to a JavaScript example viewable on the 20 | [`earthengine-layers` website][ee-layers-js-examples] 21 | 22 | [nbviewer-examples]: https://nbviewer.jupyter.org/github/UnfoldedInc/earthengine-layers/tree/master/py/examples/ 23 | [ee-layers-js-examples]: https://earthengine-layers.com/examples 24 | 25 | ## Usage 26 | 27 | ```py 28 | from pydeck_earthengine_layers import EarthEngineLayer 29 | import pydeck as pdk 30 | import ee 31 | 32 | # Initialize Earth Engine library 33 | ee.Initialize() 34 | 35 | # Create an Earth Engine object 36 | image = ee.Image('CGIAR/SRTM90_V4') 37 | 38 | # Define Earth Engine visualization parameters 39 | vis_params = { 40 | "min": 0, 41 | "max": 4000, 42 | 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'] 43 | } 44 | 45 | # Create a pydeck EarthEngineLayer object, using the Earth Engine object and 46 | # desired visualization parameters 47 | ee_layer = EarthEngineLayer(image, vis_params) 48 | 49 | # Define the initial viewport for the map 50 | view_state = pdk.ViewState(latitude=37.7749295, longitude=-122.4194155, zoom=10, bearing=0, pitch=45) 51 | 52 | # Create a Deck instance, and display in Jupyter 53 | r = pdk.Deck(layers=[ee_layer], initial_view_state=view_state) 54 | r.show() 55 | ``` 56 | 57 | ## Installation 58 | 59 | To install dependencies from the Python Package Index (PyPI): 60 | 61 | ```bash 62 | pip install earthengine-api pydeck pydeck-earthengine-layers 63 | ``` 64 | 65 | `pydeck-earthengine-layers` is also available on [Conda](https://anaconda.org/conda-forge/pydeck-earthengine-layers). If you use Conda for package 66 | management, you may install `earthengine-api`, `pydeck` and `pydeck-earthengine-layers` from `conda-forge`: 67 | 68 | ```bash 69 | conda install -c conda-forge earthengine-api pydeck pydeck-earthengine-layers 70 | ``` 71 | 72 | ### Enable with Jupyter 73 | 74 | After installing `pydeck`, you **must also enable it for use with Jupyter**. The 75 | following is a short overview; for more information, see the [pydeck 76 | documentation][pydeck-enable-jupyter]. 77 | 78 | [pydeck-enable-jupyter]: https://pydeck.gl/installation.html#enabling-pydeck-for-jupyter 79 | 80 | **Jupyter Notebook** 81 | 82 | To use with Jupyter Notebook, first make sure Jupyter Notebook is installed: 83 | 84 | ```bash 85 | conda install -c conda-forge jupyter notebook 86 | ``` 87 | 88 | Then to enable pydeck with Jupyter Notebook, run 89 | 90 | ```bash 91 | jupyter nbextension install --sys-prefix --symlink --overwrite --py pydeck 92 | jupyter nbextension enable --sys-prefix --py pydeck 93 | jupyter nbextension install @deck.gl/jupyter-widget@$DECKGL_SEMVER 94 | ``` 95 | 96 | **Jupyter Lab** 97 | 98 | To use with Jupyter Lab, first make sure that Jupyter Lab is installed: 99 | (`nodejs` is necessary to install Jupyter Lab extensions): 100 | 101 | ```bash 102 | conda install -c conda-forge jupyter jupyterlab nodejs 103 | ``` 104 | 105 | Then to enable pydeck with Jupyter Lab, run 106 | 107 | ```bash 108 | jupyter labextension install @jupyter-widgets/jupyterlab-manager 109 | DECKGL_SEMVER=`python -c "import pydeck; print(pydeck.frontend_semver.DECKGL_SEMVER)"` 110 | jupyter labextension install @deck.gl/jupyter-widget@$DECKGL_SEMVER 111 | ``` 112 | 113 | Note that only Jupyter Lab version 1.x is currently supported. Jupyter Lab 114 | version 2.0 and greater are expected to be supported in the upcoming pydeck 115 | 0.4.0 release. 116 | -------------------------------------------------------------------------------- /docs/table-of-contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "table-of-contents", 3 | "chapters": [ 4 | { 5 | "title": "Developer Guide", 6 | "chapters": [ 7 | { 8 | "title": "Overview", 9 | "entries": [ 10 | {"entry": "docs"}, 11 | {"entry": "docs/whats-new"} 12 | ] 13 | }, 14 | { 15 | "title": "Guides", 16 | "entries": [ 17 | {"entry": "docs/developer-guide/get-started"}, 18 | {"entry": "docs/developer-guide/authentication"}, 19 | {"entry": "docs/developer-guide/pydeck-integration"} 20 | ] 21 | } 22 | ] 23 | }, 24 | { 25 | "title": "API Reference", 26 | "entries": [ 27 | {"entry": "modules/earthengine-layers/docs/api-reference/earthengine-layer"}, 28 | {"entry": "modules/earthengine-layers/docs/api-reference/earthengine-terrain-layer"}, 29 | {"entry": "py/docs/pydeck-earthengine-layer"}, 30 | {"entry": "py/docs/pydeck-earthengine-terrain-layer"} 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /docs/whats-new.md: -------------------------------------------------------------------------------- 1 | # What's New 2 | 3 | ## v1.2.1 - (2020-09-10) 4 | 5 | - Bump `earthengine-api` dependency to `^0.1.230`. 6 | - Bump `loaders.gl` to `2.3.0-alpha.12`. (An alpha release is used to fix a bundling issue for pydeck). 7 | 8 | ## v1.2.0 - (2020-08-19) 9 | 10 | - `EarthEngineTerrainLayer` available in pydeck 11 | - Improved tile request strategies to prevent EarthEngine tile load failures 12 | - Bump `earthengine-api` dependency to `^0.1.230`. 13 | 14 | ## v1.1.1 - (2020-08-13) 15 | 16 | - Update `loaders.gl` to a 2.3.0 alpha release to fix a bundling issue for pydeck 17 | 18 | ## v1.1.0 - (2020-08-05) 19 | 20 | - New `EarthEngineTerrainLayer` 21 | - Updated python installation docs to reflect Conda-forge package 22 | - Use Google Maps as a basemap for website examples 23 | - Update to deck.gl 8.2 24 | 25 | ## v1.0.0 - (2020-06-08) 26 | 27 | First major version release to NPM, PyPI, and Conda-forge. 28 | 29 | The `EarthEngineLayer` in JavaScript and Python allows for rendering Earth 30 | Engine API objects in deck.gl. The `animate` and `asVector` props allow for rich 31 | rendering of `ee.ImageCollection` objects as animations and of 32 | `ee.FeatureCollection` objects with rich client-side data-driven vector styling. 33 | -------------------------------------------------------------------------------- /examples/image-collection/README.md: -------------------------------------------------------------------------------- 1 | ## Example: Use deck.gl with Google Earth Engine 2 | 3 | Uses [Webpack](https://github.com/webpack/webpack) to bundle files and serves it 4 | with [webpack-dev-server](https://webpack.js.org/guides/development/#webpack-dev-server). 5 | 6 | ## Usage 7 | 8 | To install dependencies: 9 | 10 | ```bash 11 | npm install 12 | # or 13 | yarn 14 | ``` 15 | 16 | Commands: 17 | * `npm start` is the development target, to serves the app and hot reload. 18 | * `npm run build` is the production target, to create the final bundle and write to disk. 19 | -------------------------------------------------------------------------------- /examples/image-collection/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | 4 | import DeckGL from '@deck.gl/react'; 5 | import {EarthEngineLayer} from '@unfolded.gl/earthengine-layers'; 6 | 7 | import ee from '@google/earthengine'; 8 | 9 | import {GoogleLoginProvider, GoogleLoginPane, InfoBox} from '../shared'; 10 | 11 | // Add a EE-enabled Google Client id here (or inject it with e.g. a webpack environment plugin) 12 | const EE_CLIENT_ID = process.env.EE_CLIENT_ID; // eslint-disable-line 13 | 14 | const INITIAL_VIEW_STATE = { 15 | longitude: -80.41669, 16 | latitude: 37.7853, 17 | zoom: 2, 18 | pitch: 0, 19 | bearing: 0 20 | }; 21 | 22 | export default class App extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = {eeObject: null}; 26 | 27 | this.loginProvider = new GoogleLoginProvider({ 28 | scopes: ['https://www.googleapis.com/auth/earthengine'], 29 | clientId: EE_CLIENT_ID, 30 | onLoginChange: this._onLoginSuccess.bind(this) 31 | }); 32 | } 33 | 34 | async _onLoginSuccess(user, loginProvider) { 35 | await EarthEngineLayer.initializeEEApi({clientId: EE_CLIENT_ID}); 36 | 37 | const eeObject = ee 38 | .ImageCollection('NOAA/GFS0P25') 39 | .filterDate('2018-12-22', '2018-12-23') 40 | .limit(24) 41 | .select('temperature_2m_above_ground'); 42 | 43 | this.setState({eeObject}); 44 | } 45 | 46 | render() { 47 | const {eeObject} = this.state; 48 | 49 | const visParams = { 50 | min: -40.0, 51 | max: 35.0, 52 | palette: ['blue', 'purple', 'cyan', 'green', 'yellow', 'red'] 53 | }; 54 | 55 | const layers = [new EarthEngineLayer({eeObject, visParams, animate: true})]; 56 | 57 | return ( 58 |
59 | 60 | 61 | 62 |

63 | 64 | Hourly temperature 65 | {' '} 66 | animated using an ee.ImageCollection object. 67 | Note that this currently does not work on Safari or iOS devices. 68 |

69 |
70 |
71 |
72 | ); 73 | } 74 | } 75 | 76 | export function renderToDOM(container) { 77 | return render(, container); 78 | } 79 | -------------------------------------------------------------------------------- /examples/image-collection/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | earthengine-layer-example 6 | 21 | 22 | 23 |
24 | 25 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /examples/image-collection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deck.gl-earthengine-imagecollection-demo", 3 | "version": "1.2.1", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --hot --open", 8 | "start-local": "webpack-dev-server --env.local --progress --hot --open", 9 | "build": "webpack -p" 10 | }, 11 | "dependencies": { 12 | "@babel/plugin-proposal-class-properties": "^7.5.5", 13 | "@deck.gl/core": "^8.2.5", 14 | "@deck.gl/geo-layers": "^8.2.5", 15 | "@deck.gl/layers": "^8.2.5", 16 | "@deck.gl/mesh-layers": "^8.2.5", 17 | "@deck.gl/react": "^8.2.5", 18 | "@google/earthengine": "^0.1.234", 19 | "@unfolded.gl/earthengine-layers": "1.2.1", 20 | "react": "^16.3.0", 21 | "react-dom": "^16.3.0", 22 | "styled-components": "^5.0.1" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.0.0", 26 | "@babel/preset-react": "^7.0.0", 27 | "babel-loader": "^8.0.5", 28 | "webpack": "^4.20.2", 29 | "webpack-cli": "^3.1.2", 30 | "webpack-dev-server": "^3.1.1" 31 | }, 32 | "gitHead": "c57dbd5b98fb743617ed6770ed5bf031c4f09410" 33 | } 34 | -------------------------------------------------------------------------------- /examples/image-collection/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const MODULE_ALIASES = {}; // require('../aliases'); 3 | const {resolve} = require('path'); 4 | 5 | // eslint-disable-next-line 6 | if (!process.env.EE_CLIENT_ID) { 7 | throw new Error('Environment variable EE_CLIENT_ID not set'); 8 | } 9 | 10 | const CONFIG = { 11 | mode: 'development', 12 | 13 | entry: { 14 | app: './app.js' 15 | }, 16 | 17 | output: { 18 | library: 'App' 19 | }, 20 | 21 | resolve: { 22 | // Make src files outside of this dir resolve modules in our node_modules folder 23 | modules: [resolve(__dirname, '.'), resolve(__dirname, 'node_modules'), 'node_modules'] 24 | }, 25 | 26 | module: { 27 | rules: [ 28 | { 29 | // Transpile ES6 to ES5 with babel 30 | // Remove if your app does not use JSX or you don't need to support old browsers 31 | test: /\.js$/, 32 | loader: 'babel-loader', 33 | exclude: [/node_modules/], 34 | include: [ 35 | resolve(__dirname, '.'), 36 | resolve(__dirname, '../shared'), 37 | /modules\/.*\/src/, 38 | ...Object.values(MODULE_ALIASES) 39 | ], 40 | options: { 41 | presets: ['@babel/preset-react'], 42 | ignore: [/ee_api/] 43 | } 44 | } 45 | ] 46 | }, 47 | 48 | plugins: [new webpack.EnvironmentPlugin(['EE_CLIENT_ID'])] 49 | }; 50 | 51 | // This line enables bundling against src in this repo rather than installed module 52 | module.exports = CONFIG; // env => (env ? require('../webpack.config.local')(CONFIG)(env) : CONFIG); 53 | -------------------------------------------------------------------------------- /examples/image/README.md: -------------------------------------------------------------------------------- 1 | ## Example: Use deck.gl with Google Earth Engine 2 | 3 | Uses [Webpack](https://github.com/webpack/webpack) to bundle files and serves it 4 | with [webpack-dev-server](https://webpack.js.org/guides/development/#webpack-dev-server). 5 | 6 | ## Usage 7 | 8 | To install dependencies: 9 | 10 | ```bash 11 | npm install 12 | # or 13 | yarn 14 | ``` 15 | 16 | Commands: 17 | * `npm start` is the development target, to serves the app and hot reload. 18 | * `npm run build` is the production target, to create the final bundle and write to disk. 19 | -------------------------------------------------------------------------------- /examples/image/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | 4 | import DeckGL from '@deck.gl/react'; 5 | import {EarthEngineLayer} from '@unfolded.gl/earthengine-layers'; 6 | 7 | import ee from '@google/earthengine'; 8 | 9 | import {GoogleLoginProvider, GoogleLoginPane, InfoBox} from '../shared'; 10 | 11 | // Add a EE-enabled Google Client id here (or inject it with e.g. a webpack environment plugin) 12 | const EE_CLIENT_ID = process.env.EE_CLIENT_ID; // eslint-disable-line 13 | 14 | const INITIAL_VIEW_STATE = { 15 | longitude: -85, 16 | latitude: 25, 17 | zoom: 3, 18 | pitch: 0, 19 | bearing: 0 20 | }; 21 | 22 | export default class App extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = {eeObject: null}; 26 | 27 | this.loginProvider = new GoogleLoginProvider({ 28 | scopes: ['https://www.googleapis.com/auth/earthengine'], 29 | clientId: EE_CLIENT_ID, 30 | onLoginChange: this._onLoginSuccess.bind(this) 31 | }); 32 | } 33 | 34 | async _onLoginSuccess(user, loginProvider) { 35 | await EarthEngineLayer.initializeEEApi({clientId: EE_CLIENT_ID}); 36 | this.setState({eeObject: ee.Image('CGIAR/SRTM90_V4')}); 37 | } 38 | 39 | render() { 40 | const {eeObject} = this.state; 41 | 42 | const visParams = { 43 | min: 0, 44 | max: 4000, 45 | palette: ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'] 46 | }; 47 | 48 | const layers = [new EarthEngineLayer({eeObject, visParams, opacity: 0.5})]; 49 | 50 | return ( 51 |
52 | 53 | 54 | 55 | The{' '} 56 | 57 | SRTM elevation dataset 58 | {' '} 59 | displayed using an ee.Image object. 60 | 61 | 62 |
63 | ); 64 | } 65 | } 66 | 67 | export function renderToDOM(container) { 68 | return render(, container); 69 | } 70 | -------------------------------------------------------------------------------- /examples/image/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | earthengine-layer-example 6 | 21 | 22 | 23 |
24 | 25 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /examples/image/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deck.gl-earthengine-image-demo", 3 | "version": "1.2.1", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --hot --open", 8 | "start-local": "webpack-dev-server --env.local --progress --hot --open", 9 | "build": "webpack -p" 10 | }, 11 | "dependencies": { 12 | "@babel/plugin-proposal-class-properties": "^7.5.5", 13 | "@deck.gl/core": "^8.2.5", 14 | "@deck.gl/geo-layers": "^8.2.5", 15 | "@deck.gl/layers": "^8.2.5", 16 | "@deck.gl/mesh-layers": "^8.2.5", 17 | "@deck.gl/react": "^8.2.5", 18 | "@google/earthengine": "^0.1.234", 19 | "@unfolded.gl/earthengine-layers": "1.2.1", 20 | "react": "^16.3.0", 21 | "react-dom": "^16.3.0", 22 | "react-map-gl": "^5.2.3", 23 | "styled-components": "^5.0.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.0.0", 27 | "@babel/preset-react": "^7.0.0", 28 | "babel-loader": "^8.0.5", 29 | "webpack": "^4.20.2", 30 | "webpack-cli": "^3.1.2", 31 | "webpack-dev-server": "^3.1.1" 32 | }, 33 | "gitHead": "c57dbd5b98fb743617ed6770ed5bf031c4f09410" 34 | } 35 | -------------------------------------------------------------------------------- /examples/image/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const MODULE_ALIASES = {}; // require('../aliases'); 3 | const {resolve} = require('path'); 4 | 5 | // eslint-disable-next-line 6 | if (!process.env.EE_CLIENT_ID) { 7 | throw new Error('Environment variable EE_CLIENT_ID not set'); 8 | } 9 | 10 | const CONFIG = { 11 | mode: 'development', 12 | 13 | entry: { 14 | app: './app.js' 15 | }, 16 | 17 | output: { 18 | library: 'App' 19 | }, 20 | 21 | resolve: { 22 | // Make src files outside of this dir resolve modules in our node_modules folder 23 | modules: [resolve(__dirname, '.'), resolve(__dirname, 'node_modules'), 'node_modules'] 24 | }, 25 | 26 | module: { 27 | rules: [ 28 | { 29 | // Transpile ES6 to ES5 with babel 30 | // Remove if your app does not use JSX or you don't need to support old browsers 31 | test: /\.js$/, 32 | loader: 'babel-loader', 33 | exclude: [/node_modules/], 34 | include: [ 35 | resolve(__dirname, '.'), 36 | resolve(__dirname, '../shared'), 37 | /modules\/.*\/src/, 38 | ...Object.values(MODULE_ALIASES) 39 | ], 40 | options: { 41 | presets: ['@babel/preset-react'], 42 | ignore: [/ee_api/] 43 | } 44 | } 45 | ] 46 | }, 47 | 48 | plugins: [new webpack.EnvironmentPlugin(['EE_CLIENT_ID'])] 49 | }; 50 | 51 | // This line enables bundling against src in this repo rather than installed module 52 | module.exports = CONFIG; // env => (env ? require('../webpack.config.local')(CONFIG)(env) : CONFIG); 53 | -------------------------------------------------------------------------------- /examples/intl-boundary/README.md: -------------------------------------------------------------------------------- 1 | ## Example: Use deck.gl with Google Earth Engine 2 | 3 | Uses [Webpack](https://github.com/webpack/webpack) to bundle files and serves it 4 | with [webpack-dev-server](https://webpack.js.org/guides/development/#webpack-dev-server). 5 | 6 | ## Usage 7 | 8 | To install dependencies: 9 | 10 | ```bash 11 | npm install 12 | # or 13 | yarn 14 | ``` 15 | 16 | Commands: 17 | * `npm start` is the development target, to serves the app and hot reload. 18 | * `npm run build` is the production target, to create the final bundle and write to disk. 19 | -------------------------------------------------------------------------------- /examples/intl-boundary/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | 4 | import DeckGL from '@deck.gl/react'; 5 | import {EarthEngineLayer} from '@unfolded.gl/earthengine-layers'; 6 | 7 | import ee from '@google/earthengine'; 8 | 9 | import {GoogleLoginProvider, GoogleLoginPane, InfoBox} from '../shared'; 10 | 11 | // Add a EE-enabled Google Client id here (or inject it with e.g. a webpack environment plugin) 12 | const EE_CLIENT_ID = process.env.EE_CLIENT_ID; // eslint-disable-line 13 | 14 | const INITIAL_VIEW_STATE = { 15 | longitude: -80.41669, 16 | latitude: 37.7853, 17 | zoom: 2, 18 | pitch: 0, 19 | bearing: 0 20 | }; 21 | 22 | export default class App extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = {eeObject: null}; 26 | 27 | this.loginProvider = new GoogleLoginProvider({ 28 | scopes: ['https://www.googleapis.com/auth/earthengine'], 29 | clientId: EE_CLIENT_ID, 30 | onLoginChange: this._onLoginSuccess.bind(this) 31 | }); 32 | } 33 | 34 | async _onLoginSuccess(user, loginProvider) { 35 | await EarthEngineLayer.initializeEEApi({clientId: EE_CLIENT_ID}); 36 | const dataset = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017'); 37 | const styleParams = {fillColor: 'b5ffb4', color: '00909F', width: 3.0}; 38 | const eeObject = dataset.style(styleParams); 39 | this.setState({eeObject}); 40 | } 41 | 42 | render() { 43 | const {eeObject} = this.state; 44 | const {asVector = false} = this.props; 45 | 46 | const visParams = {}; 47 | const layers = asVector 48 | ? [ 49 | new EarthEngineLayer({ 50 | eeObject, 51 | asVector: true, 52 | getLineColor: [255, 0, 0], 53 | getLineWidth: 1000, 54 | lineWidthMinPixels: 3, 55 | opacity: 0.5 56 | }) 57 | ] 58 | : [new EarthEngineLayer({eeObject, visParams, opacity: 0.5})]; 59 | 60 | return ( 61 |
62 | 63 | 64 | 65 | The{' '} 66 | 67 | Large Scale International Boundary Polygons 68 | {' '} 69 | dataset displayed using an ee.FeatureCollection object. 70 | 71 | 72 |
73 | ); 74 | } 75 | } 76 | 77 | export function renderToDOM(container) { 78 | return render(, container); 79 | } 80 | -------------------------------------------------------------------------------- /examples/intl-boundary/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | earthengine-layer-example 6 | 21 | 22 | 23 |
24 | 25 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /examples/intl-boundary/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deck.gl-earthengine-feature-collection-tile-demo", 3 | "version": "1.2.1", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --hot --open", 8 | "start-local": "webpack-dev-server --env.local --progress --hot --open", 9 | "build": "webpack -p" 10 | }, 11 | "dependencies": { 12 | "@babel/plugin-proposal-class-properties": "^7.5.5", 13 | "@deck.gl/core": "^8.2.5", 14 | "@deck.gl/geo-layers": "^8.2.5", 15 | "@deck.gl/layers": "^8.2.5", 16 | "@deck.gl/mesh-layers": "^8.2.5", 17 | "@deck.gl/react": "^8.2.5", 18 | "@google/earthengine": "^0.1.234", 19 | "@unfolded.gl/earthengine-layers": "1.2.1", 20 | "react": "^16.3.0", 21 | "react-dom": "^16.3.0", 22 | "styled-components": "^5.0.1" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.0.0", 26 | "@babel/preset-react": "^7.0.0", 27 | "babel-loader": "^8.0.5", 28 | "webpack": "^4.20.2", 29 | "webpack-cli": "^3.1.2", 30 | "webpack-dev-server": "^3.1.1" 31 | }, 32 | "gitHead": "c57dbd5b98fb743617ed6770ed5bf031c4f09410" 33 | } 34 | -------------------------------------------------------------------------------- /examples/intl-boundary/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const MODULE_ALIASES = {}; // require('../aliases'); 3 | const {resolve} = require('path'); 4 | 5 | // eslint-disable-next-line 6 | if (!process.env.EE_CLIENT_ID) { 7 | throw new Error('Environment variable EE_CLIENT_ID not set'); 8 | } 9 | 10 | const CONFIG = { 11 | mode: 'development', 12 | 13 | entry: { 14 | app: './app.js' 15 | }, 16 | 17 | output: { 18 | library: 'App' 19 | }, 20 | 21 | resolve: { 22 | // Make src files outside of this dir resolve modules in our node_modules folder 23 | modules: [resolve(__dirname, '.'), resolve(__dirname, 'node_modules'), 'node_modules'] 24 | }, 25 | 26 | module: { 27 | rules: [ 28 | { 29 | // Transpile ES6 to ES5 with babel 30 | // Remove if your app does not use JSX or you don't need to support old browsers 31 | test: /\.js$/, 32 | loader: 'babel-loader', 33 | exclude: [/node_modules/], 34 | include: [ 35 | resolve(__dirname, '.'), 36 | resolve(__dirname, '../shared'), 37 | /modules\/.*\/src/, 38 | ...Object.values(MODULE_ALIASES) 39 | ], 40 | options: { 41 | presets: ['@babel/preset-react'], 42 | ignore: [/ee_api/] 43 | } 44 | } 45 | ] 46 | }, 47 | 48 | plugins: [new webpack.EnvironmentPlugin(['EE_CLIENT_ID'])] 49 | }; 50 | 51 | // This line enables bundling against src in this repo rather than installed module 52 | module.exports = CONFIG; // env => (env ? require('../webpack.config.local')(CONFIG)(env) : CONFIG); 53 | -------------------------------------------------------------------------------- /examples/noaa-hurricanes/README.md: -------------------------------------------------------------------------------- 1 | ## Example: Use deck.gl with Google Earth Engine 2 | 3 | Uses [Webpack](https://github.com/webpack/webpack) to bundle files and serves it 4 | with [webpack-dev-server](https://webpack.js.org/guides/development/#webpack-dev-server). 5 | 6 | ## Usage 7 | 8 | To install dependencies: 9 | 10 | ```bash 11 | npm install 12 | # or 13 | yarn 14 | ``` 15 | 16 | Commands: 17 | * `npm start` is the development target, to serves the app and hot reload. 18 | * `npm run build` is the production target, to create the final bundle and write to disk. 19 | -------------------------------------------------------------------------------- /examples/noaa-hurricanes/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | 4 | import {EarthEngineLayer} from '@unfolded.gl/earthengine-layers'; 5 | import ee from '@google/earthengine'; 6 | 7 | import {DeckWithGoogleMaps, GoogleLoginProvider, GoogleLoginPane, InfoBox} from '../shared'; 8 | 9 | // Add a EE-enabled Google Client id here (or inject it with e.g. a webpack environment plugin) 10 | const EE_CLIENT_ID = process.env.EE_CLIENT_ID; // eslint-disable-line 11 | const GOOGLE_MAPS_TOKEN = process.env.GoogleMapsAPIKey; // eslint-disable-line 12 | 13 | const INITIAL_VIEW_STATE = { 14 | longitude: -53, 15 | latitude: 36, 16 | zoom: 3, 17 | pitch: 0, 18 | bearing: 0 19 | }; 20 | 21 | export default class App extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | this.state = {eeObject: null, asVector: true}; 25 | 26 | this.loginProvider = new GoogleLoginProvider({ 27 | scopes: ['https://www.googleapis.com/auth/earthengine'], 28 | clientId: EE_CLIENT_ID, 29 | onLoginChange: this._onLoginSuccess.bind(this) 30 | }); 31 | } 32 | 33 | async _onLoginSuccess(user, loginProvider) { 34 | await EarthEngineLayer.initializeEEApi({clientId: EE_CLIENT_ID}); 35 | const {year = '2017'} = this.props; 36 | 37 | // Show hurricane tracks and points for 2017. 38 | const hurricanes = ee.FeatureCollection('NOAA/NHC/HURDAT2/atlantic'); 39 | 40 | const points = hurricanes.filter(ee.Filter.date(ee.Date(year).getRange('year'))); 41 | 42 | // Find all of the hurricane ids. 43 | const storm_ids = points 44 | .toList(1000) 45 | .map(point => ee.Feature(point).get('id')) 46 | .distinct(); 47 | 48 | // Create a line for each hurricane. 49 | const lines = ee.FeatureCollection( 50 | storm_ids.map(storm_id => { 51 | const pts = points 52 | .filter(ee.Filter.eq('id', ee.String(storm_id))) 53 | .sort('system:time_start'); 54 | const line = ee.Geometry.LineString(pts.geometry().coordinates()); 55 | const feature = ee.Feature(line); 56 | return feature.set('id', storm_id); 57 | }) 58 | ); 59 | 60 | this.setState({points, lines}); 61 | } 62 | 63 | render() { 64 | const {points, lines, asVector} = this.state; 65 | 66 | const layers = asVector 67 | ? [ 68 | new EarthEngineLayer({ 69 | id: 'lines-vector', 70 | eeObject: lines, 71 | asVector, 72 | getLineColor: [255, 0, 0], 73 | getLineWidth: 1000, 74 | lineWidthMinPixels: 3 75 | }), 76 | new EarthEngineLayer({ 77 | id: 'points-vector', 78 | eeObject: points, 79 | asVector, 80 | getFillColor: [0, 0, 0], 81 | pointRadiusMinPixels: 3, 82 | getRadius: 100, 83 | getLineColor: [255, 255, 255], 84 | lineWidthMinPixels: 0.5, 85 | stroked: true 86 | }) 87 | ] 88 | : [ 89 | new EarthEngineLayer({ 90 | id: 'lines-raster', 91 | eeObject: lines, 92 | visParams: {color: 'red'} 93 | }), 94 | new EarthEngineLayer({ 95 | id: 'points-raster', 96 | eeObject: points, 97 | visParams: {color: 'black'} 98 | }) 99 | ]; 100 | 101 | return ( 102 |
103 | 109 | 110 | 111 | The{' '} 112 | 113 | Atlantic hurricane catalog 114 | {' '} 115 | displayed using an ee.FeatureCollection object. 116 |

117 | 121 | this.setState(prevState => { 122 | return {asVector: !prevState.asVector}; 123 | }) 124 | } 125 | /> 126 | Render as vector data 127 |

128 |
129 |
130 | ); 131 | } 132 | } 133 | 134 | export function renderToDOM(container) { 135 | return render(, container); 136 | } 137 | -------------------------------------------------------------------------------- /examples/noaa-hurricanes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | earthengine-layer-example 6 | 21 | 22 | 23 |
24 | 25 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /examples/noaa-hurricanes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deck.gl-earthengine-hurricanes-demo", 3 | "version": "1.2.1", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --hot --open", 8 | "start-local": "webpack-dev-server --env.local --progress --hot --open", 9 | "build": "webpack -p" 10 | }, 11 | "dependencies": { 12 | "@babel/plugin-proposal-class-properties": "^7.5.5", 13 | "@deck.gl/core": "^8.2.5", 14 | "@deck.gl/geo-layers": "^8.2.5", 15 | "@deck.gl/google-maps": "^8.2.5", 16 | "@deck.gl/layers": "^8.2.5", 17 | "@deck.gl/mesh-layers": "^8.2.5", 18 | "@deck.gl/react": "^8.2.5", 19 | "@google/earthengine": "^0.1.234", 20 | "@unfolded.gl/earthengine-layers": "1.2.1", 21 | "react": "^16.3.0", 22 | "react-dom": "^16.3.0", 23 | "styled-components": "^5.0.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.0.0", 27 | "@babel/preset-react": "^7.0.0", 28 | "babel-loader": "^8.0.5", 29 | "webpack": "^4.20.2", 30 | "webpack-cli": "^3.1.2", 31 | "webpack-dev-server": "^3.1.1" 32 | }, 33 | "gitHead": "c57dbd5b98fb743617ed6770ed5bf031c4f09410" 34 | } 35 | -------------------------------------------------------------------------------- /examples/noaa-hurricanes/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const MODULE_ALIASES = {}; // require('../aliases'); 3 | const {resolve} = require('path'); 4 | 5 | // eslint-disable-next-line 6 | if (!process.env.EE_CLIENT_ID) { 7 | throw new Error('Environment variable EE_CLIENT_ID not set'); 8 | } 9 | 10 | const CONFIG = { 11 | mode: 'development', 12 | 13 | entry: { 14 | app: './app.js' 15 | }, 16 | 17 | output: { 18 | library: 'App' 19 | }, 20 | 21 | resolve: { 22 | // Make src files outside of this dir resolve modules in our node_modules folder 23 | modules: [resolve(__dirname, '.'), resolve(__dirname, 'node_modules'), 'node_modules'] 24 | }, 25 | 26 | module: { 27 | rules: [ 28 | { 29 | // Transpile ES6 to ES5 with babel 30 | // Remove if your app does not use JSX or you don't need to support old browsers 31 | test: /\.js$/, 32 | loader: 'babel-loader', 33 | exclude: [/node_modules/], 34 | include: [ 35 | resolve(__dirname, '.'), 36 | resolve(__dirname, '../shared'), 37 | /modules\/.*\/src/, 38 | ...Object.values(MODULE_ALIASES) 39 | ], 40 | options: { 41 | presets: ['@babel/preset-react'], 42 | ignore: [/ee_api/] 43 | } 44 | } 45 | ] 46 | }, 47 | 48 | plugins: [new webpack.EnvironmentPlugin(['EE_CLIENT_ID', 'GoogleMapsAPIKey'])] 49 | }; 50 | 51 | // This line enables bundling against src in this repo rather than installed module 52 | module.exports = CONFIG; // env => (env ? require('../webpack.config.local')(CONFIG)(env) : CONFIG); 53 | -------------------------------------------------------------------------------- /examples/power-plants/README.md: -------------------------------------------------------------------------------- 1 | ## Example: Use deck.gl with Google Earth Engine 2 | 3 | Uses [Webpack](https://github.com/webpack/webpack) to bundle files and serves it 4 | with [webpack-dev-server](https://webpack.js.org/guides/development/#webpack-dev-server). 5 | 6 | ## Usage 7 | 8 | To install dependencies: 9 | 10 | ```bash 11 | npm install 12 | # or 13 | yarn 14 | ``` 15 | 16 | Commands: 17 | * `npm start` is the development target, to serves the app and hot reload. 18 | * `npm run build` is the production target, to create the final bundle and write to disk. 19 | -------------------------------------------------------------------------------- /examples/power-plants/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | 4 | import {EarthEngineLayer} from '@unfolded.gl/earthengine-layers'; 5 | import ee from '@google/earthengine'; 6 | 7 | import {DeckWithGoogleMaps, GoogleLoginProvider, GoogleLoginPane, InfoBox} from '../shared'; 8 | 9 | // Add a EE-enabled Google Client id here (or inject it with e.g. a webpack environment plugin) 10 | const EE_CLIENT_ID = process.env.EE_CLIENT_ID; // eslint-disable-line 11 | const GOOGLE_MAPS_TOKEN = process.env.GoogleMapsAPIKey; // eslint-disable-line 12 | 13 | const INITIAL_VIEW_STATE = { 14 | longitude: -53, 15 | latitude: 36, 16 | zoom: 3, 17 | pitch: 0, 18 | bearing: 0 19 | }; 20 | 21 | const FUEL_COLOR_MAPPING_VECTOR = { 22 | Coal: [0, 0, 0], 23 | Oil: [89, 55, 4], 24 | Gas: [188, 128, 189], 25 | Hydro: [5, 101, 166], 26 | Nuclear: [227, 26, 28], 27 | Solar: [255, 127, 0], 28 | Waste: [106, 61, 154], 29 | Wind: [92, 162, 209], 30 | Geothermal: [253, 191, 111], 31 | Biomass: [34, 154, 0] 32 | }; 33 | 34 | class Tooltip extends React.Component { 35 | render() { 36 | const {hoveredObject, x, y} = this.props; 37 | return ( 38 |
52 |
53 | Name 54 |
55 |
56 |
{hoveredObject.properties.name}
57 |
58 |
59 | Fuel Type 60 |
61 |
62 |
{hoveredObject.properties.fuel1}
63 |
64 |
65 | Capacity (MW) 66 |
67 |
{Math.round(hoveredObject.properties.capacitymw)}
68 |
69 | ); 70 | } 71 | } 72 | 73 | export default class App extends React.Component { 74 | constructor(props) { 75 | super(props); 76 | this.state = {eeObject: null, hoveredObject: null}; 77 | 78 | this._onHover = this._onHover.bind(this); 79 | 80 | this.loginProvider = new GoogleLoginProvider({ 81 | scopes: ['https://www.googleapis.com/auth/earthengine'], 82 | clientId: EE_CLIENT_ID, 83 | onLoginChange: this._onLoginSuccess.bind(this) 84 | }); 85 | } 86 | 87 | async _onLoginSuccess(user, loginProvider) { 88 | await EarthEngineLayer.initializeEEApi({clientId: EE_CLIENT_ID}); 89 | 90 | const eeObject = ee.FeatureCollection('WRI/GPPD/power_plants'); 91 | this.setState({eeObject}); 92 | } 93 | 94 | _onHover({x, y, object}) { 95 | this.setState({x, y, hoveredObject: object}); 96 | } 97 | 98 | render() { 99 | const {eeObject, x, y, hoveredObject} = this.state; 100 | 101 | const layers = 102 | eeObject && 103 | new EarthEngineLayer({ 104 | eeObject, 105 | getRadius: f => Math.pow(f.properties.capacitymw, 1.35), 106 | getFillColor: f => FUEL_COLOR_MAPPING_VECTOR[f.properties.fuel1], 107 | selectors: ['fuel1', 'capacitymw', 'name'], 108 | asVector: true, 109 | lineWidthMinPixels: 0.5, 110 | pointRadiusMinPixels: 2, 111 | opacity: 0.4, 112 | id: 'fuel', 113 | pickable: true, 114 | onHover: this._onHover, 115 | // Prevent z-fighting when pitched 116 | parameters: {depthTest: false} 117 | }); 118 | 119 | return ( 120 |
121 | 128 | {hoveredObject && } 129 | 130 | 131 | 132 | The{' '} 133 | 134 | Global Power Plant Database 135 | {' '} 136 | displayed using an ee.FeatureCollection object. 137 | 138 |
139 | ); 140 | } 141 | } 142 | 143 | export function renderToDOM(container) { 144 | return render(, container); 145 | } 146 | -------------------------------------------------------------------------------- /examples/power-plants/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | earthengine-layer-example 6 | 21 | 22 | 23 |
24 | 25 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /examples/power-plants/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deck.gl-earthengine-power-plants-demo", 3 | "version": "1.2.1", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --hot --open", 8 | "start-local": "webpack-dev-server --env.local --progress --hot --open", 9 | "build": "webpack -p" 10 | }, 11 | "dependencies": { 12 | "@babel/plugin-proposal-class-properties": "^7.5.5", 13 | "@deck.gl/core": "^8.2.5", 14 | "@deck.gl/geo-layers": "^8.2.5", 15 | "@deck.gl/google-maps": "^8.2.5", 16 | "@deck.gl/layers": "^8.2.5", 17 | "@deck.gl/mesh-layers": "^8.2.5", 18 | "@deck.gl/react": "^8.2.5", 19 | "@google/earthengine": "^0.1.234", 20 | "@unfolded.gl/earthengine-layers": "1.2.1", 21 | "react": "^16.3.0", 22 | "react-dom": "^16.3.0", 23 | "styled-components": "^5.0.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.0.0", 27 | "@babel/preset-react": "^7.0.0", 28 | "babel-loader": "^8.0.5", 29 | "webpack": "^4.20.2", 30 | "webpack-cli": "^3.1.2", 31 | "webpack-dev-server": "^3.1.1" 32 | }, 33 | "gitHead": "c57dbd5b98fb743617ed6770ed5bf031c4f09410" 34 | } 35 | -------------------------------------------------------------------------------- /examples/power-plants/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const MODULE_ALIASES = {}; // require('../aliases'); 3 | const {resolve} = require('path'); 4 | 5 | // eslint-disable-next-line 6 | if (!process.env.EE_CLIENT_ID) { 7 | throw new Error('Environment variable EE_CLIENT_ID not set'); 8 | } 9 | 10 | const CONFIG = { 11 | mode: 'development', 12 | 13 | entry: { 14 | app: './app.js' 15 | }, 16 | 17 | output: { 18 | library: 'App' 19 | }, 20 | 21 | resolve: { 22 | // Make src files outside of this dir resolve modules in our node_modules folder 23 | modules: [resolve(__dirname, '.'), resolve(__dirname, 'node_modules'), 'node_modules'] 24 | }, 25 | 26 | module: { 27 | rules: [ 28 | { 29 | // Transpile ES6 to ES5 with babel 30 | // Remove if your app does not use JSX or you don't need to support old browsers 31 | test: /\.js$/, 32 | loader: 'babel-loader', 33 | exclude: [/node_modules/], 34 | include: [ 35 | resolve(__dirname, '.'), 36 | resolve(__dirname, '../shared'), 37 | /modules\/.*\/src/, 38 | ...Object.values(MODULE_ALIASES) 39 | ], 40 | options: { 41 | presets: ['@babel/preset-react'], 42 | ignore: [/ee_api/] 43 | } 44 | } 45 | ] 46 | }, 47 | 48 | plugins: [new webpack.EnvironmentPlugin(['EE_CLIENT_ID', 'GoogleMapsAPIKey'])] 49 | }; 50 | 51 | // This line enables bundling against src in this repo rather than installed module 52 | module.exports = CONFIG; // env => (env ? require('../webpack.config.local')(CONFIG)(env) : CONFIG); 53 | -------------------------------------------------------------------------------- /examples/shared/google-login/google-api.js: -------------------------------------------------------------------------------- 1 | // Helper functions for gapi, Google's JavaScript API 2 | // - Ensures async functions return clean, "re-entrant" promises 3 | // - Functions for some additional REST endpoints not currently covered by JS API. 4 | 5 | /* eslint-disable camelcase */ 6 | // @ts-nocheck TODO 7 | 8 | /* global gapi */ 9 | /* global document */ 10 | /* global fetch */ 11 | 12 | // We load `gapi` at run-time from this URL 13 | const GOOGLE_API_URL = 'https://apis.google.com/js/api.js'; 14 | 15 | let gapiPromise = null; 16 | let gapiClientLoadPromise = null; 17 | let gapiClientInitPromise = null; 18 | let auth2Promise = null; 19 | 20 | // GAPI 21 | 22 | // Load the GAPI script re-entrantly 23 | // Alternatively, add to your html 24 | export async function loadGapi() { 25 | gapiPromise = gapiPromise || (await loadScript(GOOGLE_API_URL)); 26 | return await gapiPromise; 27 | } 28 | 29 | // GAPI.CLIENT 30 | 31 | // Load the GAPI JS client library re-entrantly 32 | export async function loadGapiClient() { 33 | // gapi.load is supposed return a thenable object, but await does not seem to work on it so wrap in Promise instead 34 | gapiClientLoadPromise = 35 | gapiClientLoadPromise || 36 | (await new Promise((resolve, reject) => 37 | gapi.load('client', {callback: resolve, onerror: reject}) 38 | )); 39 | return await gapiClientLoadPromise; 40 | } 41 | 42 | // Intialize the GAPI JS client library 43 | // Usually, the minimum argument is `discoveryDocs`: `gapi.client.init({discoveryDocs})` 44 | // If called multiple times, only the options from the first call are applied. 45 | export async function initGapiClient(options) { 46 | await loadGapi(); 47 | await loadGapiClient(); 48 | 49 | gapiClientInitPromise = gapiClientInitPromise || (await gapi.client.init(options)); 50 | return await gapiClientInitPromise; 51 | } 52 | 53 | // GAPI.AUTH2 54 | 55 | // Initialize gapi.auth2 56 | // Note: Not always used. Can't use `gapi.auth2.init()` and `gapi.auth2.authorize()` at the same time! 57 | export async function auth2Initialize(options) { 58 | auth2Promise = auth2Promise || gapi.auth2.init(options); 59 | return await auth2Promise; 60 | } 61 | 62 | // NOTE: We are not using async/await syntax here because: 63 | // - babel's "heavy-handed" transpilation of `async/await` moves this code into a callback 64 | // - but it needs to run directly in the `onClick` handler since it opens a popup 65 | // - and popups that are not a direct result of the user's actions are typically blocked by browser settings. 66 | export function auth2Signin(options) { 67 | return gapi.auth2.getAuthInstance().signIn(options); 68 | } 69 | 70 | export function auth2Authorize(authOptions) { 71 | return new Promise((resolve, reject) => 72 | gapi.auth2.authorize(authOptions, authResponse => { 73 | const {error} = authResponse; 74 | if (error) { 75 | // TODO - more info... 76 | reject(new Error(error)); 77 | console.error('GOOGLE API LOGIN ERROR', error); // eslint-disable-line 78 | return; 79 | } 80 | resolve(authResponse); 81 | }) 82 | ); 83 | } 84 | 85 | // When using `gapi.auth2.authorize`, the user profile needs to be manually fetched 86 | export async function auth2GetUserInfo({access_token}) { 87 | const response = await fetch('https://www.googleapis.com/oauth2/v1/userinfo', { 88 | headers: { 89 | Authorization: `Bearer ${access_token}` 90 | } 91 | }); 92 | 93 | const userInfo = await response.json(); 94 | return userInfo; 95 | } 96 | 97 | // PRIVATE UTILITIES 98 | 99 | function loadScript(src) { 100 | return new Promise((resolve, reject) => { 101 | const script = document.createElement('script'); 102 | script.src = src; 103 | script.onload = resolve; 104 | script.onerror = reject; 105 | document.head.appendChild(script); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /examples/shared/google-login/google-login-provider.js: -------------------------------------------------------------------------------- 1 | // This is a helper class for Google Login, intended to be replaced by application code 2 | 3 | /* eslint-disable camelcase */ 4 | /* global gapi */ 5 | import {loadGapi, loadGapiClient, initGapiClient, auth2Initialize} from './google-api'; 6 | 7 | const isBrowser = typeof document !== 'undefined'; 8 | 9 | const defaultProps = { 10 | // gapi.client 11 | // discoveryDocs, 12 | // gapi.auth2 13 | // clientId, 14 | // scope: '', 15 | // scopes: [], 16 | accessType: 'online', 17 | prompt: '', 18 | cookiePolicy: 'single_host_origin', 19 | fetchBasicProfile: true, 20 | uxMode: 'popup', 21 | 22 | // eslint-disable-next-line 23 | onLoginChange: user => {} 24 | }; 25 | 26 | export default class GoogleLoginProvider { 27 | constructor(props) { 28 | this.props = { 29 | ...defaultProps, 30 | ...props 31 | }; 32 | this.props.scope = this.props.scope || this.props.scopes.join(' '); 33 | delete this.props.scopes; 34 | 35 | this.signedIn = false; 36 | this.user = null; 37 | 38 | if (!isBrowser) { 39 | return; 40 | } 41 | 42 | // "Start loading" the google API 43 | this._initialize(); 44 | 45 | this._setUser(null); 46 | this._checkIfLoggedIn(); 47 | } 48 | 49 | /** 50 | * This method will handle the oauth flow by performing the following steps: 51 | * - Opening a popup window and letting the user select google account 52 | * - If already signed in to the browser, the window will disappear immediately 53 | * @returns {Promise} 54 | */ 55 | async login(options) { 56 | await this._initialize(); 57 | 58 | // Check if auto sign in happened 59 | if (this.signedIn) { 60 | return this.user; 61 | } 62 | 63 | return await this._loginViaSignIn(options); 64 | } 65 | 66 | async logout() { 67 | await this._logoutViaSignOut(); 68 | } 69 | 70 | setAccessToken(access_token, id_token) { 71 | // access_token is used to authenticate google services 72 | gapi.auth2.setToken(access_token); 73 | // id_token is used to authenticate application services 74 | this.idToken = id_token; 75 | // TODO - call callback 76 | } 77 | 78 | // Provides the current auth token. 79 | getAccessToken() { 80 | const token = this.accessToken; 81 | return (token || '') !== '' ? token : null; 82 | } 83 | 84 | // Normalize a google user object 85 | static normalizeUser(googleUser, authResponse = null) { 86 | if (googleUser && googleUser.getBasicProfile) { 87 | authResponse = authResponse || googleUser.getAuthResponse(true); 88 | return normalizeUserProfile(googleUser, authResponse); 89 | } 90 | if (googleUser && googleUser.picture) { 91 | return normalizeUserInfo(googleUser, authResponse); 92 | } 93 | return googleUser; 94 | } 95 | 96 | // PRIVATE 97 | 98 | _checkIfLoggedIn() { 99 | // Initialize will init auth... 100 | this._initialize(); 101 | } 102 | 103 | // Initialize the Google API (gapi) by loading it dynamically and initializing with appropriate scope 104 | // TODO - https://stackoverflow.com/questions/15657983/popup-blocking-the-gdrive-authorization-in-chrome 105 | // Ensure we call async initialization code only once 106 | async _initialize() { 107 | await loadGapi(); 108 | await loadGapiClient(); 109 | // Initialize the JavaScript client library. 110 | const {discoveryDocs} = this.props; 111 | await initGapiClient({discoveryDocs}); 112 | 113 | await this._initializeAuth2(); 114 | } 115 | 116 | async _initializeAuth2() { 117 | let user = null; 118 | 119 | const authOptions = getAuthOptionsFromProps(this.props); 120 | const authResponse = await auth2Initialize(authOptions); // Only options from first call take effect..Initialize. 121 | if (authResponse.isSignedIn.get()) { 122 | const googleUser = authResponse.currentUser.get(); 123 | user = GoogleLoginProvider.normalizeUser(googleUser); 124 | } 125 | 126 | this._listenToLoginStatus(); 127 | 128 | if (user) { 129 | this._setUser(user); 130 | } 131 | } 132 | 133 | async _loginViaSignIn(options, responseType) { 134 | await this._initialize(); 135 | 136 | // Offline access 137 | if (responseType === 'code') { 138 | return await gapi.auth2.grantOfflineAccess(options); 139 | } 140 | 141 | // NOTE: We are not using async/await syntax here because: 142 | // - babel's "heavy-handed" transpilation of `async/await` moves this code into a callback 143 | // - but it needs to run directly in the `onClick` handler since it opens a popup 144 | // - and popups that are not a direct result of the user's actions are typically blocked by browser settings. 145 | return await gapi.auth2 146 | .getAuthInstance() 147 | .signIn(options) 148 | .then(googleUser => { 149 | const user = GoogleLoginProvider.normalizeUser(googleUser); 150 | this._setUser(user); 151 | return user; 152 | }) 153 | .catch(error => { 154 | console.error('GOOGLE ACCOUNT LOGIN ERROR', error); // eslint-disable-line 155 | }); 156 | } 157 | 158 | async _logoutViaSignOut() { 159 | const authInstance = gapi && gapi.auth2 && gapi.auth2.getAuthInstance(); 160 | if (authInstance) { 161 | await authInstance.signOut(); 162 | await authInstance.disconnect(); 163 | } 164 | } 165 | 166 | // Set (or clear) user data 167 | _setUser(user) { 168 | const oldUser = this.user; 169 | this.signedIn = Boolean(user); 170 | this.user = user; 171 | 172 | const userChanged = Boolean(user) !== Boolean(oldUser); 173 | if (userChanged) { 174 | this.props.onLoginChange(user, this); 175 | } 176 | } 177 | 178 | // Set up a listener for logout 179 | _listenToLoginStatus() { 180 | // For APIKEY, no authinstance is provided 181 | const authInstance = gapi.auth2.getAuthInstance(); 182 | if (!authInstance) { 183 | return; 184 | } 185 | 186 | // LISTEN FOR LOGIN 187 | authInstance.isSignedIn.listen(isSignedIn => { 188 | if (isSignedIn) { 189 | // NOTE: This is only triggered on fresh login. 190 | // I.e. signIn() does not trigger this if browser is already logged in 191 | console.log('GOOGLE API LOGIN DETECTED'); // eslint-disable-line 192 | } else { 193 | console.log('GOOGLE API LOGOUT DETECTED'); // eslint-disable-line 194 | this._setUser(null); 195 | // Call callback 196 | this.props.onLoginChange(null); 197 | } 198 | }); 199 | } 200 | } 201 | 202 | // LOCAL HELPERS 203 | 204 | // Normalizes "manually" queries user data (when using gapi.auth2.authorize) 205 | function normalizeUserInfo(userInfo, authResponse) { 206 | return { 207 | email: userInfo.email, 208 | domain: userInfo.hd, 209 | imageUrl: userInfo.picture, // Picture seems to be of different size in this path 210 | 211 | name: userInfo.name, 212 | givenName: userInfo.given_name, 213 | familyName: userInfo.family_name, 214 | 215 | // TODO - granted scopes? 216 | scopes: [], 217 | 218 | accessToken: authResponse.access_token, 219 | idToken: authResponse.id_token 220 | }; 221 | } 222 | 223 | // Normalizes user data from the GoogleUser object (when using gapi.auth2.signin) 224 | function normalizeUserProfile(googleUser, authResponse) { 225 | const userProfile = googleUser.getBasicProfile(); 226 | 227 | // Normalize user data (idea is common format across identity providers) 228 | return { 229 | email: userProfile && userProfile.getEmail(), 230 | domain: googleUser.getHostedDomain(), 231 | imageUrl: userProfile && userProfile.getImageUrl(), 232 | 233 | name: userProfile && userProfile.getName(), 234 | givenName: userProfile && userProfile.getGivenName(), 235 | familyName: userProfile && userProfile.getFamilyName(), 236 | 237 | scopes: googleUser && googleUser.getGrantedScopes().split(' '), 238 | 239 | accessToken: authResponse.access_token, 240 | idToken: authResponse.id_token 241 | }; 242 | } 243 | 244 | function getAuthOptionsFromProps(props) { 245 | return { 246 | access_type: props.accessType, 247 | client_id: props.clientId, 248 | cookie_policy: props.cookiePolicy, 249 | login_hint: props.loginHint, 250 | scope: props.scope, 251 | hosted_domain: props.hostedDomain, 252 | fetch_basic_profile: props.fetchBasicProfile, 253 | ux_mode: props.uxMode, 254 | redirect_uri: props.redirectUri 255 | }; 256 | } 257 | -------------------------------------------------------------------------------- /examples/shared/google-maps/deck-with-google-maps.js: -------------------------------------------------------------------------------- 1 | /* global console, document, window */ 2 | import React, {Component} from 'react'; 3 | import {GoogleMapsOverlay} from '@deck.gl/google-maps'; 4 | 5 | const HOST = 'https://maps.googleapis.com/maps/api/js'; 6 | const LOADING_GIF = 'https://upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif'; 7 | 8 | const style = { 9 | height: '100%', 10 | width: '100%' 11 | }; 12 | 13 | function injectScript(src) { 14 | return new Promise((resolve, reject) => { 15 | const script = document.createElement('script'); 16 | script.src = src; 17 | script.addEventListener('load', resolve); 18 | script.addEventListener('error', e => reject(e.error)); 19 | document.head.appendChild(script); 20 | }); 21 | } 22 | 23 | function loadGoogleMapApi(apiKey, onComplete) { 24 | const url = `${HOST}?key=${apiKey}&libraries=places`; 25 | injectScript(url) 26 | .then(() => onComplete()) 27 | // eslint-disable-next-line no-console 28 | .catch(e => console.error(e)); 29 | } 30 | 31 | export default class DeckWithGoogleMaps extends Component { 32 | constructor(props) { 33 | super(props); 34 | 35 | this.state = { 36 | googleMapsLoaded: typeof window !== 'undefined' && window.google && window.google.maps 37 | }; 38 | } 39 | 40 | componentDidMount() { 41 | const {googleMapsToken} = this.props; 42 | if (!window.google || (window.google && !window.google.maps)) { 43 | loadGoogleMapApi(googleMapsToken, () => { 44 | this.setState({googleMapsLoaded: true}); 45 | }); 46 | } 47 | } 48 | 49 | render() { 50 | const {googleMapsLoaded} = this.state; 51 | if (!googleMapsLoaded) { 52 | return Loading Google Maps overlay...; 53 | } 54 | 55 | return ; 56 | } 57 | } 58 | 59 | class DeckOverlayWrapper extends Component { 60 | constructor(props) { 61 | super(props); 62 | this.state = { 63 | isOverlayConfigured: false 64 | }; 65 | this.DeckOverlay = new GoogleMapsOverlay({layers: []}); 66 | this.containerRef = React.createRef(); 67 | } 68 | 69 | componentDidMount() { 70 | const {initialViewState} = this.props; 71 | const view = { 72 | center: {lat: initialViewState.latitude, lng: initialViewState.longitude}, 73 | mapTypeId: this.props.mapTypeId || 'satellite', 74 | zoom: initialViewState.zoom 75 | }; 76 | 77 | const map = new window.google.maps.Map(this.containerRef.current, view); 78 | this.DeckOverlay.setMap(map); 79 | this.DeckOverlay.setProps({layers: this.props.layers}); 80 | // eslint-disable-next-line react/no-did-mount-set-state 81 | this.setState({isOverlayConfigured: true}); 82 | } 83 | 84 | componentDidUpdate(prevProps, prevState, snapshot) { 85 | this.DeckOverlay.setProps({layers: this.props.layers}); 86 | } 87 | 88 | componentWillUnmount() { 89 | delete this.DeckOverlay; 90 | } 91 | 92 | render() { 93 | return
; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /examples/shared/index.js: -------------------------------------------------------------------------------- 1 | // Helper class for Google Login 2 | export {default as GoogleLoginProvider} from './google-login/google-login-provider'; 3 | 4 | // Helper class for using Google Maps in React 5 | export {default as DeckWithGoogleMaps} from './google-maps/deck-with-google-maps'; 6 | 7 | // React SVG Icons 8 | export {default as GoogleEarthEngineIcon} from './react-components/earthengine-icon'; 9 | 10 | // React Components 11 | export {default as GoogleLoginPane} from './react-components/google-login-pane'; 12 | export {default as InfoBox} from './react-components/info-box'; 13 | -------------------------------------------------------------------------------- /examples/shared/react-components/google-login-pane.js: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useState} from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import EarthEngineIcon from './earthengine-icon'; 5 | 6 | const StyledPanel = styled.div` 7 | position: absolute; 8 | top: 40px; 9 | left: 0; 10 | z-index: 1000; 11 | padding: 16px; 12 | `; 13 | 14 | const Button = styled.div.attrs({ 15 | className: 'button' 16 | })` 17 | align-items: center; 18 | background-color: white; 19 | border-radius: 6px; 20 | color: #000000; 21 | font-family: ff-clan-web-pro, 'Helvetica Neue', Helvetica, sans-serif; 22 | cursor: pointer; 23 | display: inline-flex; 24 | font-size: 11px; 25 | justify-content: center; 26 | letter-spacing: 0.3px; 27 | line-height: 12px; 28 | outline: 0; 29 | text-align: center; 30 | transition: all 0.4s ease; 31 | vertical-align: middle; 32 | width: ${props => props.width || 'auto'}; 33 | padding: 0; 34 | border-radius: ${props => `${props.height / 2}px` || 'auto'}; 35 | 36 | :hover, 37 | :focus, 38 | :active, 39 | &.active { 40 | background-color: #e2e2e2; 41 | } 42 | `; 43 | 44 | const StyledEELogo = styled.div` 45 | display: flex; 46 | flex-direction: column; 47 | align-items: flex-start; 48 | margin-top: -6px; 49 | 50 | .login { 51 | margin-left: 9px; 52 | font-size: 16px; 53 | } 54 | `; 55 | 56 | const StyledLoggedInUser = styled.div.attrs({ 57 | className: 'logged-in-user' 58 | })` 59 | display: flex; 60 | align-items: center; 61 | 62 | .email { 63 | transition: width 0.8s ease, margin 0.8s ease; 64 | width: 0; 65 | margin: 0; 66 | overflow: hidden; 67 | } 68 | 69 | :hover { 70 | .email { 71 | width: auto; 72 | margin: 0 12px; 73 | } 74 | } 75 | `; 76 | 77 | const StyledError = styled.div` 78 | color: red; 79 | margin-top: 10px; 80 | `; 81 | 82 | const UserIcon = ({user, email, height = 36}) => { 83 | if (!user) { 84 | return null; 85 | } 86 | 87 | return ( 88 | 89 | {user.email} 96 | {email ?
{user.email}
: null} 97 |
98 | ); 99 | }; 100 | 101 | const LoggedInUser = ({user, href, target, iconHeight}) => ( 102 | 105 | ); 106 | 107 | // TODO - move this insane function into the UserAccount class? 108 | function getError(err) { 109 | if (!err) { 110 | return 'Something went wrong'; 111 | } 112 | 113 | if (typeof err === 'string') { 114 | return err; 115 | } else if (err instanceof Error) { 116 | return err.message; 117 | } else if (typeof err === 'object') { 118 | return err.error 119 | ? getError(err.error) 120 | : err.err 121 | ? getError(err.err) 122 | : err.message 123 | ? getError(err.message) 124 | : JSON.stringify(err); 125 | } 126 | 127 | return null; 128 | } 129 | 130 | function getErrorDescription(err = {}) { 131 | return typeof err.error_description === 'string' ? err.error_description : ''; 132 | } 133 | 134 | const ErrorDisplay = ({error}) => ( 135 | 136 | {getError(error)} : {getErrorDescription(error)} 137 | 138 | ); 139 | 140 | const LoginButton = ({Icon, onLogin, loginButtonColor = 'white'}) => { 141 | return ( 142 | 153 | ); 154 | }; 155 | 156 | const LoginPane = ({ 157 | user, 158 | Icon, 159 | iconHeight, 160 | loginButtonColor, 161 | onLogin, 162 | href, 163 | target = '_blank', 164 | error 165 | }) => { 166 | return ( 167 |
168 | {user ? ( 169 | 170 | ) : ( 171 | 172 | )} 173 | {error ? : null} 174 |
175 | ); 176 | }; 177 | 178 | /** 179 | * TODO - this is a module, replace with typescript defs 180 | * @param {object} props 181 | * @param {any} loginProvider 182 | * @param {number} props.iconHeight 183 | * @param {string} [props.loginButtonColor] 184 | */ 185 | const GoogleLoginPane = ({loginProvider, iconHeight, loginButtonColor}) => { 186 | const [error, setError] = useState(null); 187 | const onLogin = useCallback( 188 | () => { 189 | try { 190 | loginProvider.login({prompt: 'login'}); 191 | setError(null); 192 | } catch (err) { 193 | setError(err); 194 | } 195 | }, 196 | [loginProvider] 197 | ); 198 | 199 | if (!loginProvider) { 200 | return
; 201 | } 202 | 203 | return ( 204 | 205 | 215 | 216 | ); 217 | }; 218 | 219 | export default GoogleLoginPane; 220 | -------------------------------------------------------------------------------- /examples/shared/react-components/icon-wrapper.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | import React, {Component} from 'react'; 22 | import PropTypes from 'prop-types'; 23 | 24 | const getStyleClassFromColor = (totalColor, colors) => 25 | new Array(totalColor) 26 | .fill(1) 27 | .reduce((accu, c, i) => `${accu}.cr${i + 1} {fill:${colors[i % colors.length]};}`, ''); 28 | 29 | export default class IconWrapper extends Component { 30 | static displayName() { 31 | return 'Base Icon'; 32 | } 33 | 34 | static propTypes() { 35 | return { 36 | /** Set the height of the icon, ex. '16px' */ 37 | height: PropTypes.string, 38 | /** Set the width of the icon, ex. '16px' */ 39 | width: PropTypes.string, 40 | /** Set the viewbox of the svg */ 41 | viewBox: PropTypes.string, 42 | /** Path element */ 43 | children: PropTypes.node, 44 | 45 | predefinedClassName: PropTypes.string, 46 | className: PropTypes.string 47 | }; 48 | } 49 | 50 | static defaultProps() { 51 | return { 52 | height: null, 53 | width: null, 54 | viewBox: '0 0 64 64', 55 | predefinedClassName: '', 56 | className: '' 57 | }; 58 | } 59 | 60 | render() { 61 | const { 62 | height, 63 | width, 64 | viewBox, 65 | style = {}, 66 | children, 67 | predefinedClassName, 68 | className, 69 | colors, 70 | totalColor, 71 | ...props 72 | } = this.props; 73 | const svgHeight = height; 74 | const svgWidth = width || svgHeight; 75 | style.fill = 'currentColor'; 76 | 77 | const fillStyle = 78 | Array.isArray(colors) && totalColor && getStyleClassFromColor(totalColor, colors); 79 | 80 | return ( 81 | 89 | {fillStyle ? : null} 90 | {children} 91 | 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /examples/shared/react-components/info-box.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | // import EarthEngineIcon from './earthengine-icon'; 5 | 6 | const StyledPanel = styled.div` 7 | position: absolute; 8 | top: 40px; 9 | right: 0px; 10 | width: 344px; 11 | background: #fff; 12 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.15); 13 | margin: 24px; 14 | font-family: ff-clan-web-pro, 'Helvetica Neue', Helvetica, sans-serif; 15 | font-size: 15px; 16 | z-index: 1000; 17 | padding: 12px 24px; 18 | padding-top: 2px; 19 | `; 20 | 21 | const StyledEELogo = styled.div` 22 | display: flex; 23 | flex-direction: column; 24 | align-items: flex-start; 25 | margin-top: -6px; 26 | 27 | .login { 28 | margin-left: 9px; 29 | font-size: 16px; 30 | } 31 | `; 32 | const InfoBox = ({title = 'Example', children}) => { 33 | return ( 34 | 35 |

36 | {title} 37 |

38 | {children} 39 |

40 | 41 | To run this demo, you need to sign in with an{' '} 42 | Earth Engine-enabled Google Account. 43 | Loading EE data may take some time. 44 | 45 |

46 |
47 | ); 48 | }; 49 | 50 | export default InfoBox; 51 | -------------------------------------------------------------------------------- /examples/terrain/README.md: -------------------------------------------------------------------------------- 1 | ## Example: Use deck.gl with Google Earth Engine 2 | 3 | Uses [Webpack](https://github.com/webpack/webpack) to bundle files and serves it 4 | with [webpack-dev-server](https://webpack.js.org/guides/development/#webpack-dev-server). 5 | 6 | ## Usage 7 | 8 | To install dependencies: 9 | 10 | ```bash 11 | npm install 12 | # or 13 | yarn 14 | ``` 15 | 16 | Commands: 17 | * `npm start` is the development target, to serves the app and hot reload. 18 | * `npm run build` is the production target, to create the final bundle and write to disk. 19 | -------------------------------------------------------------------------------- /examples/terrain/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | 4 | import DeckGL from '@deck.gl/react'; 5 | import {EarthEngineTerrainLayer} from '@unfolded.gl/earthengine-layers'; 6 | 7 | import ee from '@google/earthengine'; 8 | 9 | import {GoogleLoginProvider, GoogleLoginPane, InfoBox} from '../shared'; 10 | 11 | // Add a EE-enabled Google Client id here (or inject it with e.g. a webpack environment plugin) 12 | const EE_CLIENT_ID = process.env.EE_CLIENT_ID; // eslint-disable-line 13 | 14 | const INITIAL_VIEW_STATE = { 15 | longitude: -111.96, 16 | latitude: 36.15, 17 | zoom: 10.5, 18 | pitch: 65, 19 | bearing: -66.16, 20 | maxPitch: 80 21 | }; 22 | 23 | export default class App extends React.Component { 24 | constructor(props) { 25 | super(props); 26 | this.state = {eeObject: null, eeTerrainObject: null}; 27 | 28 | this.loginProvider = new GoogleLoginProvider({ 29 | scopes: ['https://www.googleapis.com/auth/earthengine'], 30 | clientId: EE_CLIENT_ID, 31 | onLoginChange: this._onLoginSuccess.bind(this) 32 | }); 33 | } 34 | 35 | async _onLoginSuccess(user, loginProvider) { 36 | await EarthEngineTerrainLayer.initializeEEApi({clientId: EE_CLIENT_ID}); 37 | this.setState({ 38 | eeObject: ee.Image('CGIAR/SRTM90_V4'), 39 | eeTerrainObject: ee.Image('USGS/NED').select('elevation') 40 | }); 41 | } 42 | 43 | render() { 44 | const {eeObject, eeTerrainObject} = this.state; 45 | 46 | const visParams = { 47 | min: 0, 48 | max: 4000, 49 | palette: ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'] 50 | }; 51 | 52 | const layers = [ 53 | new EarthEngineTerrainLayer({eeObject, visParams, eeTerrainObject, opacity: 1}) 54 | ]; 55 | 56 | return ( 57 |
58 | 59 | 60 | 61 | The{' '} 62 | 63 | SRTM elevation dataset 64 | {' '} 65 | rendered with a hypsometric tint and overlaid over a terrain mesh generated from the{' '} 66 | 67 | USGS National Elevation Dataset 68 | {' '} 69 | . 70 | 71 | 72 |
73 | ); 74 | } 75 | } 76 | 77 | export function renderToDOM(container) { 78 | return render(, container); 79 | } 80 | -------------------------------------------------------------------------------- /examples/terrain/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | earthengine-layer-example 6 | 21 | 22 | 23 |
24 | 25 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /examples/terrain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deck.gl-earthengine-terrain-demo", 3 | "version": "1.2.1", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --hot --open", 8 | "start-local": "webpack-dev-server --env.local --progress --hot --open", 9 | "build": "webpack -p" 10 | }, 11 | "dependencies": { 12 | "@babel/plugin-proposal-class-properties": "^7.5.5", 13 | "@deck.gl/core": "^8.2.5", 14 | "@deck.gl/geo-layers": "^8.2.5", 15 | "@deck.gl/layers": "^8.2.5", 16 | "@deck.gl/mesh-layers": "^8.2.5", 17 | "@deck.gl/react": "^8.2.5", 18 | "@google/earthengine": "^0.1.234", 19 | "@unfolded.gl/earthengine-layers": "1.2.1", 20 | "react": "^16.3.0", 21 | "react-dom": "^16.3.0", 22 | "styled-components": "^5.0.1" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.0.0", 26 | "@babel/preset-react": "^7.0.0", 27 | "babel-loader": "^8.0.5", 28 | "webpack": "^4.20.2", 29 | "webpack-cli": "^3.1.2", 30 | "webpack-dev-server": "^3.1.1" 31 | }, 32 | "gitHead": "c57dbd5b98fb743617ed6770ed5bf031c4f09410" 33 | } 34 | -------------------------------------------------------------------------------- /examples/terrain/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const MODULE_ALIASES = {}; // require('../aliases'); 3 | const {resolve} = require('path'); 4 | 5 | // eslint-disable-next-line 6 | if (!process.env.EE_CLIENT_ID) { 7 | throw new Error('Environment variable EE_CLIENT_ID not set'); 8 | } 9 | 10 | const CONFIG = { 11 | mode: 'development', 12 | 13 | entry: { 14 | app: './app.js' 15 | }, 16 | 17 | output: { 18 | library: 'App' 19 | }, 20 | 21 | resolve: { 22 | // Make src files outside of this dir resolve modules in our node_modules folder 23 | modules: [resolve(__dirname, '.'), resolve(__dirname, 'node_modules'), 'node_modules'] 24 | }, 25 | 26 | module: { 27 | rules: [ 28 | { 29 | // Transpile ES6 to ES5 with babel 30 | // Remove if your app does not use JSX or you don't need to support old browsers 31 | test: /\.js$/, 32 | loader: 'babel-loader', 33 | exclude: [/node_modules/], 34 | include: [ 35 | resolve(__dirname, '.'), 36 | resolve(__dirname, '../shared'), 37 | /modules\/.*\/src/, 38 | ...Object.values(MODULE_ALIASES) 39 | ], 40 | options: { 41 | presets: ['@babel/preset-react'], 42 | ignore: [/ee_api/] 43 | } 44 | } 45 | ] 46 | }, 47 | 48 | plugins: [new webpack.EnvironmentPlugin(['EE_CLIENT_ID'])] 49 | }; 50 | 51 | // This line enables bundling against src in this repo rather than installed module 52 | module.exports = CONFIG; // env => (env ? require('../webpack.config.local')(CONFIG)(env) : CONFIG); 53 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0", 3 | "version": "1.2.1", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true, 6 | "packages": [ 7 | "modules/*" 8 | ], 9 | "command": { 10 | "bootstrap": {} 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/earthengine-layers/README.md: -------------------------------------------------------------------------------- 1 | # @unfolded.gl/earthengine-layers 2 | 3 | This module contains a [deck.gl](https://deck.gl) layer for [Google Earth Engine API objects](https://github.com/google/earthengine-api). 4 | 5 | The primary export is the `EarthEngineLayer` layer, which accepts Google Earth Engine API objects (`ee.Image`, `ee.ImageCollection`, `ee.FeatureCollection`) through its `eeObject` prop, and renders the desired datasets via a customized deck.gl `TileLayer`. 6 | 7 | For documentation please visit the [website](https://earthengine-layers.com). 8 | -------------------------------------------------------------------------------- /modules/earthengine-layers/docs/api-reference/earthengine-layer.md: -------------------------------------------------------------------------------- 1 | # EarthEngineLayer 2 | 3 |

4 | @unfolded.gl/earthengine-layers 5 |

6 | 7 | The `EarthEngineLayer` connects [Google Earth Engine][gee] to 8 | [deck.gl](https://deck.gl), making it possible to visualize planetary-scale 9 | geospatial datasets in a JavaScript application. 10 | 11 | [gee]: https://earthengine.google.com/ 12 | 13 | To use this layer, you need to sign in with an EarthEngine-enabled Google 14 | Account. [Visit here][gee-signup] to sign up. 15 | 16 | [gee-signup]: https://signup.earthengine.google.com/#!/ 17 | 18 | This particular example uses the deck.gl React bindings but the 19 | `EarthEngineLayer` can of course also be used with the pure JavaScript and 20 | scripting APIs: 21 | 22 | ```js 23 | import React from 'react'; 24 | import DeckGL from '@deck.gl/react'; 25 | import {EarthEngineLayer} from '@unfolded.gl/earthengine-layers'; 26 | import ee from '@google/earthengine'; 27 | 28 | export default class App extends React.Component { 29 | constructor(props) { 30 | super(props); 31 | this.state = {eeObject: null}; 32 | } 33 | 34 | async _onLoginSuccess(user, loginProvider) { 35 | const token = 'Google OAuth2 access token' 36 | await EarthEngineLayer.initializeEEApi({clientId: EE_CLIENT_ID, token}); 37 | this.setState({eeObject: ee.Image('CGIAR/SRTM90_V4')}); 38 | } 39 | 40 | render() { 41 | const {viewport} = this.props; 42 | const {eeObject} = this.state; 43 | const visParams = { 44 | min: 0, 45 | max: 4000, 46 | palette: ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'] 47 | }; 48 | const layers = [new EarthEngineLayer({eeObject, visParams})]; 49 | return ( 50 | 51 | ); 52 | } 53 | } 54 | ``` 55 | 56 | 57 | ## Installation 58 | 59 | To install the dependencies from NPM: 60 | 61 | ```bash 62 | npm install deck.gl @google/earthengine @unfolded.gl/earthengine-layers 63 | # or 64 | npm install @deck.gl/core @deck.gl/layers @deck.gl/geo-layers @google/earthengine @unfolded.gl/earthengine-layers 65 | ``` 66 | 67 | ```js 68 | import {EarthEngineLayer} from '@unfolded.gl/earthengine-layers'; 69 | new EarthEngineLayer({}); 70 | ``` 71 | 72 | To use pre-bundled scripts: 73 | 74 | ```html 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ``` 85 | 86 | ```js 87 | new deck.EarthEngineLayer({}); 88 | ``` 89 | 90 | ## Static Methods 91 | 92 | ### async initializeEEApi({clientId?: string, token?: string}) 93 | 94 | Can be called to initialize the earth engine API. Calls 95 | `ee.data.authenticateViaOauth()`, `ee.initialize()` or `ee.setToken()`, and 96 | returns a `Promise` that resolves when authentication and initialization is 97 | completed and the EE API is ready to use. 98 | 99 | This method is just a convenience, it can be replaced with direct calls to the 100 | EE API. 101 | 102 | Parameters: 103 | - `clientId` A valid Google clientId that has been authenticated with the earthengine scope and set up to whitelist the 'origin' URL that the app will be served on. 104 | - `token` Alternatively, a pre-generated OAuth2 access token. 105 | 106 | 107 | ## Properties 108 | 109 | Inherits all properties from base [`Layer`][base-layer] and from the [`TileLayer`][tile-layer]. If rendering images, inherits all properties from the [`BitmapLayer`][bitmap-layer]. If rendering vector data, inherits all properties from the [`GeoJsonLayer`][geojson-layer]. 110 | 111 | [base-layer]: https://deck.gl/#/documentation/deckgl-api-reference/layers/layer 112 | [tile-layer]: https://deck.gl/#/documentation/deckgl-api-reference/layers/tile-layer 113 | [bitmap-layer]: https://deck.gl/#/documentation/deckgl-api-reference/layers/bitmap-layer 114 | [geojson-layer]: https://deck.gl/#/documentation/deckgl-api-reference/layers/geojson-layer 115 | 116 | ### Authentication Options 117 | 118 | ##### `token` (String, optional) 119 | 120 | - Default: `null` 121 | 122 | A valid Google OAuth2 access token. Unnecessary from `pydeck` or if using 123 | `initializeEEApi` described above. 124 | 125 | ### Data Options 126 | 127 | ##### `eeObject` (EarthEngine Object|String) 128 | 129 | - Default: `null` 130 | 131 | Either an EarthEngine JavaScript API object, or a serialized string representing 132 | an object (created with, e.g. `ee.Image.serialize()`). 133 | 134 | By default, `getMap` is called on the object, and image tiles are displayed 135 | representing the object. You can pass `asVector` or `animate` for alternative 136 | rendering. 137 | 138 | ##### `visParams` (Object, optional) 139 | 140 | - Default: `null` 141 | 142 | An object representing the visualization parameters passed to Earth Engine. See 143 | [Earth Engine documentation][visparams-docs] for more information on supported 144 | parameters. Alternatively, you can style objects by using the 145 | [`.style()`][style-fn] function for `FeatureCollection` or `ImageCollection` 146 | objects. 147 | 148 | [visparams-docs]: https://developers.google.com/earth-engine/image_visualization 149 | [style-fn]: https://developers.google.com/earth-engine/api_docs#ee.featurecollection.style 150 | 151 | Unused when `asVector` is `true`. 152 | 153 | ##### `selectors` (Array of String, optional) 154 | 155 | - Default: `[]` 156 | 157 | Names of additional properties to download when `asVector` is `true`. 158 | 159 | By default, only the geometries of the `FeatureCollection` are downloaded. In 160 | order to apply data-driven styling using GeoJsonLayer styling properties, you 161 | need to specify those property names here. 162 | 163 | ### Render Options 164 | 165 | ##### `asVector` (Boolean, optional) 166 | 167 | - Default: `false` 168 | 169 | If `true`, render vector data using the deck.gl `GeoJsonLayer`. 170 | 171 | Rendering as vector is only possible for `ee.FeatureCollection` objects; an 172 | error will be produced if `asVector` is set to `true` when another object type 173 | is passed. 174 | 175 | When `asVector` is set, the GeoJSON representation of the `FeatureCollection` is 176 | downloaded, which can include very precise geometries. As such, beware of large 177 | downloads: the Earth Engine backend may return no data if the output would be 178 | larger than 100MB. 179 | 180 | To reduce the dataset size, use `filter` expressions on the `FeatureCollection` 181 | object before passing it to `EarthEngineLayer`. 182 | 183 | ##### `animate` (Boolean, optional) 184 | 185 | - Default: `false` 186 | 187 | If `true`, render an animated ImageCollection. 188 | 189 | Rendering an animation is only possible for `ee.ImageCollection` objects; an 190 | error will be produced if `animate` is set to `true` when another object type is 191 | passed. 192 | 193 | The `ImageCollection` should be filtered and sorted in the order desired for the 194 | animation. If an `ImageCollection` contains 20 images, the animation will 195 | contain those images as individual frames of the animation, in the same order. 196 | Earth Engine has an upper limit of 100 animation frames. 197 | 198 | ##### `animationSpeed` (Number, optional) 199 | 200 | - Default: `12` 201 | 202 | If `animate` is `true`, `animationSpeed` represents the number of frames per 203 | second. Keeping this constant implies that animations will play at the same 204 | speed; an `ImageCollection` with more frames will have a longer loop than one 205 | with fewer frames. 206 | 207 | 208 | ## Source 209 | 210 | [modules/earthengine-layers/src/earth-engine-layer](https://github.com/UnfoldedInc/earthengine-layers/tree/master/modules/earthengine-layers/src) 211 | -------------------------------------------------------------------------------- /modules/earthengine-layers/docs/api-reference/earthengine-terrain-layer.md: -------------------------------------------------------------------------------- 1 | # EarthEngineTerrainLayer 2 | 3 |

4 | @unfolded.gl/earthengine-layers 5 |

6 | 7 | The `EarthEngineTerrainLayer` connects [Google Earth Engine][gee] to 8 | [deck.gl](https://deck.gl), making it possible to visualize planetary-scale 9 | geospatial datasets _over 3D terrain_ in a JavaScript application. The 10 | difference with the `EarthEngineLayer` is that you must provide _two_ 11 | EarthEngine data sources, one to be used as the imagery source, another as the 12 | terrain source. 13 | 14 | [gee]: https://earthengine.google.com/ 15 | 16 | To use this layer, you need to sign in with an EarthEngine-enabled Google 17 | Account. [Visit here][gee-signup] to sign up. 18 | 19 | [gee-signup]: https://signup.earthengine.google.com/#!/ 20 | 21 | This particular example uses the deck.gl React bindings but the 22 | `EarthEngineTerrainLayer` can of course also be used with the pure JavaScript 23 | and scripting APIs: 24 | 25 | ```js 26 | import React from 'react'; 27 | import DeckGL from '@deck.gl/react'; 28 | import {EarthEngineTerrainLayer} from '@unfolded.gl/earthengine-layers'; 29 | import ee from '@google/earthengine'; 30 | 31 | export default class App extends React.Component { 32 | constructor(props) { 33 | super(props); 34 | this.state = {eeObject: null, eeTerrainObject: null}; 35 | } 36 | 37 | async _onLoginSuccess(user, loginProvider) { 38 | const token = 'Google OAuth2 access token' 39 | await EarthEngineTerrainLayer.initializeEEApi({clientId: EE_CLIENT_ID, token}); 40 | this.setState({ 41 | eeObject: ee.Image('CGIAR/SRTM90_V4'), 42 | eeTerrainObject: ee.Image('USGS/NED').select('elevation') 43 | }); 44 | } 45 | 46 | render() { 47 | const {viewport} = this.props; 48 | const {eeObject, eeTerrainObject} = this.state; 49 | const visParams = { 50 | min: 0, 51 | max: 4000, 52 | palette: ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'] 53 | }; 54 | const layers = [new EarthEngineTerrainLayer({eeObject, visParams, eeTerrainObject, opacity: 1})]; 55 | return ( 56 | 57 | ); 58 | } 59 | } 60 | ``` 61 | 62 | ## Installation 63 | 64 | To install the dependencies from NPM: 65 | 66 | ```bash 67 | npm install deck.gl @google/earthengine @unfolded.gl/earthengine-layers 68 | # or 69 | npm install @deck.gl/core @deck.gl/layers @deck.gl/geo-layers @google/earthengine @unfolded.gl/earthengine-layers 70 | ``` 71 | 72 | ```js 73 | import {EarthEngineTerrainLayer} from '@unfolded.gl/earthengine-layers'; 74 | new EarthEngineTerrainLayer({}); 75 | ``` 76 | 77 | To use pre-bundled scripts: 78 | 79 | ```html 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | ``` 91 | 92 | ```js 93 | new deck.EarthEngineTerrainLayer({}); 94 | ``` 95 | 96 | ## Static Methods 97 | 98 | ### async initializeEEApi({clientId?: string, token?: string}) 99 | 100 | Can be called to initialize the earth engine API. Calls 101 | `ee.data.authenticateViaOauth()`, `ee.initialize()` or `ee.setToken()`, and 102 | returns a `Promise` that resolves when authentication and initialization is 103 | completed and the EE API is ready to use. 104 | 105 | This method is just a convenience, it can be replaced with direct calls to the 106 | EE API. 107 | 108 | Parameters: 109 | - `clientId` A valid Google clientId that has been authenticated with the earthengine scope and set up to whitelist the 'origin' URL that the app will be served on. 110 | - `token` Alternatively, a pre-generated OAuth2 access token. 111 | 112 | ## Properties 113 | 114 | Inherits all properties from base [`Layer`][base-layer] and from the [`TileLayer`][tile-layer]. 115 | 116 | [base-layer]: https://deck.gl/#/documentation/deckgl-api-reference/layers/layer 117 | [tile-layer]: https://deck.gl/#/documentation/deckgl-api-reference/layers/tile-layer 118 | 119 | ### Authentication Options 120 | 121 | ##### `token` (String, optional) 122 | 123 | - Default: `null` 124 | 125 | A valid Google OAuth2 access token. Unnecessary from `pydeck` or if using 126 | `initializeEEApi` described above. 127 | 128 | ### Data Options 129 | 130 | ##### `eeObject` (EarthEngine Object|String) 131 | 132 | - Default: `null` 133 | 134 | The EarthEngine source used for rendering imagery on top of terrain. 135 | 136 | Either an EarthEngine JavaScript API object, or a serialized string representing 137 | an object (created with, e.g. `ee.Image.serialize()`). 138 | 139 | This source is expected to be a raster image whose data value represents 140 | elevations in meters. 141 | 142 | ##### `eeTerrainObject` (EarthEngine Object|String) 143 | 144 | - Default: `null` 145 | 146 | The EarthEngine source to be used as the terrain. 147 | 148 | Either an EarthEngine JavaScript API object, or a serialized string representing 149 | an object (created with, e.g. `ee.Image.serialize()`). 150 | 151 | By default, `getMap` is called on the object, and image tiles are displayed 152 | representing the object. 153 | 154 | ##### `visParams` (Object, optional) 155 | 156 | - Default: `null` 157 | 158 | An object representing the visualization parameters passed to Earth Engine. See 159 | [Earth Engine documentation][visparams-docs] for more information on supported 160 | parameters. Alternatively, you can style objects by using the 161 | [`.style()`][style-fn] function for `FeatureCollection` or `ImageCollection` 162 | objects. 163 | 164 | [visparams-docs]: https://developers.google.com/earth-engine/image_visualization 165 | [style-fn]: https://developers.google.com/earth-engine/api_docs#ee.featurecollection.style 166 | 167 | ## Source 168 | 169 | [modules/earthengine-layers/src/earth-engine-terrain-layer](https://github.com/UnfoldedInc/earthengine-layers/tree/master/modules/earthengine-layers/src) 170 | -------------------------------------------------------------------------------- /modules/earthengine-layers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unfolded.gl/earthengine-layers", 3 | "description": "deck.gl layers for Google Earth Engine", 4 | "version": "1.2.1", 5 | "license": "MIT", 6 | "author": "Unfolded, Inc.", 7 | "publishConfig": { 8 | "access": "public" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/UnfoldedInc/platform.git" 13 | }, 14 | "main": "src/index.js", 15 | "scripts": { 16 | "build": "rollup --config" 17 | }, 18 | "dependencies": { 19 | "@loaders.gl/core": "2.3.0-alpha.10", 20 | "@loaders.gl/images": "2.3.0-alpha.10", 21 | "@loaders.gl/json": "2.3.0-alpha.10", 22 | "@math.gl/web-mercator": "3.1.2" 23 | }, 24 | "devDependencies": { 25 | "@rollup/plugin-commonjs": "^15.0.0", 26 | "@rollup/plugin-json": "^4.0.2", 27 | "@rollup/plugin-node-resolve": "^7.1.1", 28 | "rollup": "^2.4.0", 29 | "rollup-plugin-terser": "^5.3.0" 30 | }, 31 | "peerDependencies": { 32 | "@deck.gl/core": "^8.2.0", 33 | "@deck.gl/geo-layers": "^8.2.0", 34 | "@deck.gl/layers": "^8.2.0", 35 | "@deck.gl/mesh-layers": "^8.2.0", 36 | "@google/earthengine": "^0.1.234" 37 | }, 38 | "gitHead": "73094f68451e557a8b32909cf8376f45942de021" 39 | } 40 | -------------------------------------------------------------------------------- /modules/earthengine-layers/rollup.config.js: -------------------------------------------------------------------------------- 1 | import {terser} from 'rollup-plugin-terser'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import json from '@rollup/plugin-json'; 5 | 6 | const config = ({file, plugins = [], globals = {}, external = []}) => ({ 7 | input: 'src/bundle.js', 8 | output: { 9 | file, 10 | format: 'iife', 11 | name: 'EarthEngineLayerLibrary', 12 | globals: { 13 | ...globals, 14 | '@deck.gl/core': 'deck', 15 | '@loaders.gl/core': 'loaders' 16 | } 17 | }, 18 | external: [...external, '@deck.gl/core', '@loaders.gl/core'], 19 | plugins: [ 20 | ...plugins, 21 | resolve({ 22 | browser: true, 23 | preferBuiltins: true 24 | }), 25 | commonjs(), 26 | json() 27 | ] 28 | }); 29 | 30 | const eeGlobals = { 31 | '@google/earthengine': 'ee', 32 | '@deck.gl/core': 'deck', 33 | '@deck.gl/geo-layers': 'deck', 34 | '@deck.gl/layers': 'deck', 35 | '@deck.gl/mesh-layers': 'deck' 36 | }; 37 | 38 | // deck.gl globals provided by pydeck. See: 39 | // https://github.com/visgl/deck.gl/blob/c8702ae134e0598b2fdca03642744f25e9de593b/modules/jupyter-widget/src/deck-bundle.js 40 | const pydeckGlobals = { 41 | '@deck.gl/aggregation-layers': 'deck', 42 | '@deck.gl/core': 'deck', 43 | '@deck.gl/geo-layers': 'deck', 44 | '@deck.gl/google-maps': 'deck', 45 | '@deck.gl/json': 'deck', 46 | '@deck.gl/layers': 'deck', 47 | '@deck.gl/mesh-layers': 'deck' 48 | }; 49 | 50 | export default [ 51 | config({ 52 | file: 'dist/dist.js', 53 | globals: eeGlobals, 54 | external: Object.keys(eeGlobals) 55 | }), 56 | config({ 57 | file: 'dist/dist.min.js', 58 | plugins: [terser()], 59 | globals: eeGlobals, 60 | external: Object.keys(eeGlobals) 61 | }), 62 | config({ 63 | file: 'dist/pydeck_layer_module.js', 64 | globals: pydeckGlobals, 65 | external: Object.keys(pydeckGlobals) 66 | }), 67 | config({ 68 | file: 'dist/pydeck_layer_module.min.js', 69 | globals: pydeckGlobals, 70 | external: Object.keys(pydeckGlobals), 71 | plugins: [terser()] 72 | }) 73 | ]; 74 | -------------------------------------------------------------------------------- /modules/earthengine-layers/src/bundle.js: -------------------------------------------------------------------------------- 1 | /* global window, global */ 2 | import EarthEngineLayer from './earth-engine-layer'; 3 | import EarthEngineTerrainLayer from './earth-engine-terrain-layer'; 4 | const EarthEngineLayerLibrary = { 5 | EarthEngineLayer, 6 | EarthEngineTerrainLayer 7 | }; 8 | 9 | const _global = typeof window === 'undefined' ? global : window; 10 | _global.EarthEngineLayerLibrary = EarthEngineLayerLibrary; 11 | 12 | export default EarthEngineLayerLibrary; 13 | -------------------------------------------------------------------------------- /modules/earthengine-layers/src/earth-engine-terrain-layer.js: -------------------------------------------------------------------------------- 1 | import {CompositeLayer} from '@deck.gl/core'; 2 | import {TerrainLayer} from '@deck.gl/geo-layers'; 3 | import ee from '@google/earthengine'; 4 | import {initializeEEApi} from './ee-api'; // Promisify ee apis 5 | import {deepEqual, promisifyEEMethod} from './utils'; 6 | 7 | /** 8 | * Decoder for Terrarium encoding 9 | */ 10 | const ELEVATION_DECODER = { 11 | rScaler: 256, 12 | gScaler: 1, 13 | bScaler: 1 / 256, 14 | offset: -32768 15 | }; 16 | 17 | // Global access token, to allow single EE API initialization if using multiple 18 | // layers 19 | let accessToken; 20 | 21 | const defaultProps = { 22 | ...TerrainLayer.defaultProps, 23 | // data prop is unused 24 | data: {type: 'object', value: null}, 25 | token: {type: 'string', value: null}, 26 | eeObject: {type: 'object', value: null}, 27 | eeTerrainObject: {type: 'object', value: null}, 28 | visParams: {type: 'object', value: null, equal: deepEqual}, 29 | 30 | // TileLayer props with custom defaults 31 | maxRequests: 6, 32 | refinementStrategy: 'no-overlap', 33 | tileSize: 256 34 | }; 35 | 36 | export default class EarthEngineTerrainLayer extends CompositeLayer { 37 | // helper function to initialize EE API 38 | static async initializeEEApi({clientId, token}) { 39 | await initializeEEApi({clientId, token}); 40 | } 41 | 42 | initializeState() { 43 | this.state = {}; 44 | } 45 | 46 | // Note - Layer.updateState is not async. But it lets us `await` the initialization below 47 | async updateState({props, oldProps, changeFlags}) { 48 | await this._updateToken(props, oldProps, changeFlags); 49 | this._updateEEObject(props, oldProps, changeFlags); 50 | await this._updateEEVisParams(props, oldProps, changeFlags); 51 | } 52 | 53 | async _updateToken(props, oldProps, changeFlags) { 54 | if (!props.token || props.token === accessToken) { 55 | return; 56 | } 57 | 58 | const {token} = props; 59 | await initializeEEApi({token}); 60 | accessToken = token; 61 | } 62 | 63 | _updateEEObject(props, oldProps, changeFlags) { 64 | if ( 65 | props.eeObject === oldProps.eeObject && 66 | props.eeTerrainObject === oldProps.eeTerrainObject 67 | ) { 68 | return; 69 | } 70 | 71 | let eeObject; 72 | let eeTerrainObject; 73 | // If a string, assume a JSON-serialized EE object. 74 | if (typeof props.eeObject === 'string') { 75 | eeObject = ee.Deserializer.fromJSON(props.eeObject); 76 | } else { 77 | eeObject = props.eeObject; 78 | } 79 | if (typeof props.eeTerrainObject === 'string') { 80 | eeTerrainObject = ee.Deserializer.fromJSON(props.eeTerrainObject); 81 | } else { 82 | eeTerrainObject = props.eeTerrainObject; 83 | } 84 | 85 | if (eeTerrainObject) { 86 | // Quantize eeTerrainObject 87 | const added = eeTerrainObject.add(32768); 88 | const red = added.divide(256).floor(); 89 | const green = added.mod(256).floor(); 90 | const blue = added 91 | .subtract(added.floor()) 92 | .multiply(255) 93 | .floor(); 94 | eeTerrainObject = ee.Image.rgb(red, green, blue); 95 | } 96 | 97 | // TODO - what case is this handling 98 | if (Array.isArray(props.eeObject) && props.eeObject.length === 0) { 99 | eeObject = null; 100 | } 101 | 102 | this.setState({eeObject, eeTerrainObject}); 103 | } 104 | 105 | async _updateEEVisParams(props, oldProps, changeFlags) { 106 | if ( 107 | props.visParams === oldProps.visParams && 108 | props.eeObject === oldProps.eeObject && 109 | props.eeTerrainObject === oldProps.eeTerrainObject 110 | ) { 111 | return; 112 | } 113 | 114 | const {eeObject, eeTerrainObject} = this.state; 115 | if (!eeObject) { 116 | return; 117 | } 118 | 119 | if (!eeObject.getMap) { 120 | throw new Error('eeObject must have a getMap() method'); 121 | } 122 | 123 | // Evaluate map 124 | // Done for all eeObjects, including ImageCollection, to get a stable 125 | // identifier 126 | const {mapid, urlFormat} = await promisifyEEMethod(eeObject, 'getMap', props.visParams); 127 | 128 | const {mapid: meshMapid, urlFormat: meshUrlFormat} = await promisifyEEMethod( 129 | eeTerrainObject, 130 | 'getMap', 131 | { 132 | min: 0, 133 | max: 255, 134 | format: 'png' 135 | } 136 | ); 137 | 138 | this.setState({mapid, urlFormat, meshMapid, meshUrlFormat}); 139 | } 140 | 141 | renderLayers() { 142 | const {mapid, urlFormat, meshMapid, meshUrlFormat} = this.state; 143 | const {extent, maxRequests, maxZoom, minZoom, tileSize} = this.props; 144 | 145 | return ( 146 | mapid && 147 | meshMapid && 148 | new TerrainLayer( 149 | this.getSubLayerProps({ 150 | id: mapid 151 | }), 152 | { 153 | elevationData: meshUrlFormat, 154 | texture: urlFormat, 155 | elevationDecoder: ELEVATION_DECODER, 156 | meshMaxError: 10, 157 | extent, 158 | maxRequests, 159 | maxZoom, 160 | minZoom, 161 | tileSize 162 | } 163 | ) 164 | ); 165 | } 166 | } 167 | 168 | EarthEngineTerrainLayer.layerName = 'EarthEngineTerrainLayer'; 169 | EarthEngineTerrainLayer.defaultProps = defaultProps; 170 | -------------------------------------------------------------------------------- /modules/earthengine-layers/src/ee-api.js: -------------------------------------------------------------------------------- 1 | import ee from '@google/earthengine'; 2 | 3 | export async function initializeEEApi({clientId, token}) { 4 | if (token) { 5 | setToken(token); 6 | } else { 7 | await authenticateViaOAuth(clientId); 8 | } 9 | await _initialize(); 10 | } 11 | 12 | // Authenticate using an OAuth pop-up. 13 | async function authenticateViaOAuth(clientId, extraScopes = null) { 14 | return await new Promise((resolve, reject) => { 15 | ee.data.authenticateViaOauth( 16 | clientId, 17 | value => resolve(value), 18 | error => reject(error), 19 | extraScopes, 20 | value => { 21 | // called if automatic behind-the-scenes authentication fails 22 | // console.debug('EE authentication opening popup', value); 23 | ee.data.authenticateViaPopup(resolve, reject); 24 | } 25 | ); 26 | }); 27 | } 28 | 29 | // Set short-lived Access Token directly 30 | function setToken(token) { 31 | if (token) { 32 | ee.apiclient.setAuthToken('', 'Bearer', token, 3600, [], undefined, false); 33 | } 34 | } 35 | 36 | // Initialize client library 37 | async function _initialize(baseurl = null, tileurl = null) { 38 | // TODO initialize seems to need ee to be set on global window object 39 | // We may be importing in non-standard way...? 40 | /* global window */ 41 | window.ee = ee; 42 | 43 | return await new Promise((resolve, reject) => { 44 | ee.initialize(baseurl, tileurl, value => resolve(value), error => reject(error)); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /modules/earthengine-layers/src/index.js: -------------------------------------------------------------------------------- 1 | // Layers 2 | export {default as EarthEngineLayer} from './earth-engine-layer'; 3 | export {default as EarthEngineTerrainLayer} from './earth-engine-terrain-layer'; 4 | -------------------------------------------------------------------------------- /modules/earthengine-layers/src/utils.js: -------------------------------------------------------------------------------- 1 | // From https://github.com/visgl/deck.gl/blob/c66e3e5bca63b6e214c27259025947dcfa7e359a/modules/core/src/utils/deep-equal.js 2 | // Partial deep equal (only recursive on arrays) 3 | export function deepEqual(a, b) { 4 | if (a === b) { 5 | return true; 6 | } 7 | if (!a || !b) { 8 | return false; 9 | } 10 | for (const key in a) { 11 | const aValue = a[key]; 12 | const bValue = b[key]; 13 | const equals = 14 | aValue === bValue || 15 | (Array.isArray(aValue) && Array.isArray(bValue) && deepEqual(aValue, bValue)); 16 | if (!equals) { 17 | return false; 18 | } 19 | } 20 | return true; 21 | } 22 | 23 | // Promisify eeObject methods 24 | export function promisifyEEMethod(eeObject, method, ...args) { 25 | return new Promise((resolve, reject) => 26 | eeObject[method](...args, (value, error) => { 27 | if (error) { 28 | reject(error); 29 | return; 30 | } 31 | resolve(value); 32 | }) 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /modules/earthengine-layers/test/index.js: -------------------------------------------------------------------------------- 1 | // import './tile-cache.spec'; 2 | // import './tile.spec'; 3 | // import './tile-layer.spec'; 4 | // import './viewport-util.spec'; 5 | -------------------------------------------------------------------------------- /modules/earthengine-layers/test/tile-layer/tile-layer.spec.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 - 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | import test from 'tape-catch'; 22 | import {generateLayerTests, testLayer} from '@deck.gl/test-utils'; 23 | import {TileLayer} from '@deck.gl/geo-layers'; 24 | 25 | test('TileLayer', t => { 26 | const testCases = generateLayerTests({ 27 | Layer: TileLayer, 28 | assert: t.ok, 29 | onBeforeUpdate: ({testCase}) => t.comment(testCase.title) 30 | }); 31 | testLayer({Layer: TileLayer, testCases, onError: t.notOk}); 32 | t.end(); 33 | }); 34 | 35 | test('TileLayer#updateTriggers', t => { 36 | const testCases = [ 37 | { 38 | props: { 39 | getTileData: 0 40 | }, 41 | onAfterUpdate({layer}) { 42 | t.equal(layer.state.tileCache._getTileData, 0, 'Should create a tileCache.'); 43 | } 44 | }, 45 | { 46 | updateProps: { 47 | getTileData: 1 48 | }, 49 | onAfterUpdate({layer}) { 50 | t.equal( 51 | layer.state.tileCache._getTileData, 52 | 0, 53 | 'Should not create a tileCache when updateTriggers not changed.' 54 | ); 55 | } 56 | }, 57 | { 58 | updateProps: { 59 | getTileData: 2, 60 | updateTriggers: { 61 | getTileData: 2 62 | } 63 | }, 64 | onAfterUpdate({layer}) { 65 | t.equal( 66 | layer.state.tileCache._getTileData, 67 | 2, 68 | 'Should create a new tileCache with updated getTileData.' 69 | ); 70 | } 71 | } 72 | ]; 73 | 74 | testLayer({Layer: TileLayer, testCases, onError: t.notOk}); 75 | 76 | t.end(); 77 | }); 78 | -------------------------------------------------------------------------------- /modules/earthengine-layers/test/tileset-2d/tileset-2d.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import test from 'tape-catch'; 4 | // import TileCache from '@deck.gl/geo-layers/tile-layer/utils/tile-cache'; 5 | // import Tile from '@deck.gl/geo-layers/tile-layer/utils/tile'; 6 | import {WebMercatorViewport} from '@deck.gl/core'; 7 | 8 | const testViewState = { 9 | bearing: 0, 10 | pitch: 0, 11 | longitude: -77.06753216318891, 12 | latitude: 38.94628276371387, 13 | zoom: 12, 14 | minZoom: 2, 15 | maxZoom: 14, 16 | height: 1, 17 | width: 1 18 | }; 19 | 20 | // testViewState should load tile 12-1171-1566 21 | const testTile = new Tile({x: 1171, y: 1566, z: 12}); 22 | 23 | const testViewport = new WebMercatorViewport(testViewState); 24 | 25 | const cacheMaxSize = 1; 26 | const maxZoom = 13; 27 | const minZoom = 11; 28 | 29 | const getTileData = () => Promise.resolve(null); 30 | const testTileCacheProps = { 31 | getTileData, 32 | maxSize: cacheMaxSize, 33 | minZoom, 34 | maxZoom, 35 | onTileLoad: () => {} 36 | }; 37 | 38 | test('TileCache#TileCache#should clear the cache when finalize is called', t => { 39 | const tileCache = new TileCache(testTileCacheProps); 40 | tileCache.update(testViewport); 41 | t.equal(tileCache._cache.size, 1); 42 | tileCache.finalize(); 43 | t.equal(tileCache._cache.size, 0); 44 | t.end(); 45 | }); 46 | 47 | test('TileCache#should call onUpdate with the expected tiles', t => { 48 | const tileCache = new TileCache(testTileCacheProps); 49 | tileCache.update(testViewport); 50 | 51 | t.equal(tileCache.tiles[0].x, testTile.x); 52 | t.equal(tileCache.tiles[0].y, testTile.y); 53 | t.equal(tileCache.tiles[0].z, testTile.z); 54 | 55 | tileCache.finalize(); 56 | t.end(); 57 | }); 58 | 59 | test('TileCache#should clear not visible tiles when cache is full', t => { 60 | const tileCache = new TileCache(testTileCacheProps); 61 | // load a viewport to fill the cache 62 | tileCache.update(testViewport); 63 | // load another viewport. The previous cached tiles shouldn't be visible 64 | tileCache.update( 65 | new WebMercatorViewport( 66 | Object.assign({}, testViewState, { 67 | longitude: -100, 68 | latitude: 80 69 | }) 70 | ) 71 | ); 72 | 73 | t.equal(tileCache._cache.size, 1); 74 | t.ok(tileCache._cache.get('12-910-459'), 'expected tile is in cache'); 75 | 76 | tileCache.finalize(); 77 | t.end(); 78 | }); 79 | 80 | test('TileCache#should load the cached parent tiles while we are loading the current tiles', t => { 81 | const tileCache = new TileCache(testTileCacheProps); 82 | tileCache.update(testViewport); 83 | 84 | const zoomedInViewport = new WebMercatorViewport( 85 | Object.assign({}, testViewState, { 86 | zoom: maxZoom 87 | }) 88 | ); 89 | tileCache.update(zoomedInViewport); 90 | t.ok( 91 | tileCache.tiles.some( 92 | tile => tile.x === testTile.x && tile.y === testTile.y && tile.z === testTile.z 93 | ), 94 | 'loads cached parent tiles' 95 | ); 96 | 97 | tileCache.finalize(); 98 | t.end(); 99 | }); 100 | 101 | test('TileCache#should try to load the existing zoom levels if we zoom in too far', t => { 102 | const tileCache = new TileCache(testTileCacheProps); 103 | const zoomedInViewport = new WebMercatorViewport( 104 | Object.assign({}, testViewState, { 105 | zoom: 20 106 | }) 107 | ); 108 | 109 | tileCache.update(zoomedInViewport); 110 | tileCache.tiles.forEach(tile => { 111 | t.equal(tile.z, maxZoom); 112 | }); 113 | 114 | tileCache.finalize(); 115 | t.end(); 116 | }); 117 | 118 | test('TileCache#should not display anything if we zoom out too far', t => { 119 | const tileCache = new TileCache(testTileCacheProps); 120 | const zoomedOutViewport = new WebMercatorViewport( 121 | Object.assign({}, testViewState, { 122 | zoom: 1 123 | }) 124 | ); 125 | 126 | tileCache.update(zoomedOutViewport); 127 | t.equal(tileCache.tiles.length, 0); 128 | tileCache.finalize(); 129 | t.end(); 130 | }); 131 | 132 | test('TileCache#should set isLoaded to true even when loading the tile throws an error', t => { 133 | const errorTileCache = new TileCache({ 134 | getTileData: () => Promise.reject(null), 135 | onTileError: () => { 136 | t.equal(errorTileCache.tiles[0].isLoaded, true); 137 | errorTileCache.finalize(); 138 | t.end(); 139 | }, 140 | maxSize: cacheMaxSize, 141 | minZoom, 142 | maxZoom 143 | }); 144 | 145 | errorTileCache.update(testViewport); 146 | }); 147 | -------------------------------------------------------------------------------- /modules/earthengine-layers/test/tileset-2d/viewport-util.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import test from 'tape-catch'; 4 | import {getTileIndices} from '@deck.gl/geo-layers/tile-layer/utils/viewport-util'; 5 | import {WebMercatorViewport} from '@deck.gl/core'; 6 | 7 | const TEST_CASES = [ 8 | { 9 | title: 'flat viewport (fractional)', 10 | viewport: new WebMercatorViewport({ 11 | width: 800, 12 | height: 400, 13 | longitude: -90, 14 | latitude: 45, 15 | zoom: 3.5 16 | }), 17 | minZoom: undefined, 18 | maxZoom: undefined, 19 | output: ['1,2,3', '1,3,3', '2,2,3', '2,3,3'] 20 | }, 21 | { 22 | title: 'tilted viewport', 23 | viewport: new WebMercatorViewport({ 24 | width: 800, 25 | height: 400, 26 | pitch: 30, 27 | bearing: -25, 28 | longitude: -90, 29 | latitude: 45, 30 | zoom: 3.5 31 | }), 32 | minZoom: undefined, 33 | maxZoom: undefined, 34 | output: ['0,1,3', '0,2,3', '0,3,3', '1,1,3', '1,2,3', '1,3,3', '2,1,3', '2,2,3', '2,3,3'] 35 | }, 36 | { 37 | title: 'flat viewport (exact)', 38 | viewport: new WebMercatorViewport({ 39 | width: 1024, 40 | height: 1024, 41 | longitude: 0, 42 | latitude: 0, 43 | orthographic: true, 44 | zoom: 2 45 | }), 46 | minZoom: undefined, 47 | maxZoom: undefined, 48 | output: ['1,1,2', '1,2,2', '2,1,2', '2,2,2'] 49 | }, 50 | { 51 | title: 'under zoom', 52 | viewport: new WebMercatorViewport({longitude: -90, latitude: 45, zoom: 3}), 53 | minZoom: 4, 54 | maxZoom: undefined, 55 | output: [] 56 | }, 57 | { 58 | title: 'over zoom', 59 | viewport: new WebMercatorViewport({ 60 | width: 800, 61 | height: 400, 62 | longitude: -90, 63 | latitude: 45, 64 | zoom: 5 65 | }), 66 | minZoom: 0, 67 | maxZoom: 3, 68 | output: ['1,2,3', '2,2,3'] 69 | }, 70 | { 71 | title: 'z0 repeat', 72 | viewport: new WebMercatorViewport({ 73 | width: 800, 74 | height: 400, 75 | longitude: -90, 76 | latitude: 0, 77 | zoom: 0 78 | }), 79 | minZoom: undefined, 80 | maxZoom: undefined, 81 | output: ['0,0,0'] 82 | } 83 | ]; 84 | 85 | function getTileIds(tiles) { 86 | const set = new Set(); 87 | for (const tile of tiles) { 88 | set.add(`${tile.x},${tile.y},${tile.z}`); 89 | } 90 | return Array.from(set).sort(); 91 | } 92 | 93 | test('getTileIndices', t => { 94 | for (const testCase of TEST_CASES) { 95 | const result = getTileIndices(testCase.viewport, testCase.maxZoom, testCase.minZoom); 96 | t.deepEqual(getTileIds(result), testCase.output, testCase.title); 97 | } 98 | 99 | t.end(); 100 | }); 101 | -------------------------------------------------------------------------------- /ocular-dev-tools.config.js: -------------------------------------------------------------------------------- 1 | const {resolve} = require('path'); 2 | 3 | const config = { 4 | lint: { 5 | paths: ['modules', 'examples', 'test'], 6 | extensions: ['js'] 7 | }, 8 | 9 | aliases: { 10 | // 'deck.gl-test': resolve(__dirname, './test') 11 | }, 12 | 13 | browserTest: { 14 | server: {wait: 5000} 15 | }, 16 | 17 | entry: { 18 | test: 'test/node.js', 19 | 'test-browser': 'test/browser.js', 20 | bench: 'test/bench/node.js', 21 | 'bench-browser': 'test/bench/browser.js', 22 | size: 'test/size/import-nothing.js' 23 | } 24 | }; 25 | 26 | module.exports = config; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "earthengine-layers-monorepo", 3 | "description": "deck.gl layers for Google's Earth Engine API", 4 | "license": "MIT", 5 | "private": true, 6 | "keywords": [ 7 | "deck.gl", 8 | "earthengine", 9 | "earth engine", 10 | "ee", 11 | "visualization", 12 | "layer" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/UnfoldedInc/earthengine-layers.git" 17 | }, 18 | "workspaces": [ 19 | "examples/*", 20 | "modules/*" 21 | ], 22 | "scripts": { 23 | "bootstrap": "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true yarn && ocular-bootstrap", 24 | "clean": "ocular-clean", 25 | "build": "ocular-clean && ocular-build && lerna run build", 26 | "version": "ocular-build core", 27 | "lint": "ocular-lint", 28 | "cover": "ocular-test cover", 29 | "publish": "ocular-publish", 30 | "test": "ocular-test", 31 | "test-fast": "ocular-test fast", 32 | "test-node": "ocular-test node", 33 | "test-browser": "ocular-test browser", 34 | "bench": "ocular-test bench", 35 | "bench-browser": "ocular-test bench-browser", 36 | "metrics": "ocular-metrics" 37 | }, 38 | "peerDependencies": { 39 | "@deck.gl/core": "^8.2.0", 40 | "@deck.gl/layers": "^8.2.0", 41 | "@deck.gl/geo-layers": "^8.2.0" 42 | }, 43 | "devDependencies": { 44 | "@deck.gl/core": "^8.2.0", 45 | "@deck.gl/layers": "^8.2.0", 46 | "@deck.gl/geo-layers": "^8.2.0", 47 | "@deck.gl/mesh-layers": "^8.2.0", 48 | "@deck.gl/test-utils": "^8.2.0", 49 | "@luma.gl/engine": "^8.2.0", 50 | "@luma.gl/test-utils": "^8.2.0", 51 | "@probe.gl/bench": "^3.2.1", 52 | "@probe.gl/test-utils": "^3.2.1", 53 | "babel-loader": "^8.0.0", 54 | "babel-plugin-inline-webgl-constants": "^1.0.2", 55 | "babel-plugin-remove-glsl-comments": "^0.1.0", 56 | "babel-preset-minify": "^0.5.0", 57 | "coveralls": "^3.0.0", 58 | "eslint-config-prettier": "^4.1.0", 59 | "eslint-config-uber-jsx": "^3.3.3", 60 | "eslint-plugin-import": "^2.16.0", 61 | "eslint-plugin-react": "^7.10", 62 | "gl": "^4.3.3", 63 | "glsl-transpiler": "^1.8.3", 64 | "ocular-dev-tools": "^0.1.8", 65 | "png.js": "^0.1.1", 66 | "pre-commit": "^1.2.2", 67 | "pre-push": "^0.1.1", 68 | "react": "^16.2.0", 69 | "react-dom": "^16.2.0", 70 | "react-map-gl": "^5.1.0", 71 | "reify": "^0.18.1" 72 | }, 73 | "resolutions": { 74 | "@loaders.gl/core": "2.3.0-alpha.12", 75 | "@loaders.gl/images": "2.3.0-alpha.12", 76 | "@loaders.gl/json": "2.3.0-alpha.12", 77 | "@loaders.gl/loader-utils": "2.3.0-alpha.12" 78 | }, 79 | "pre-commit": [ 80 | "test-fast" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /py/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /py/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2.0 (2020-08-19) 4 | 5 | - New `EarthEngineTerrainLayer` exposed through Pydeck. Allows for visualizing EarthEngine `Image` objects over 3D terrain 6 | - Reduced positional arguments. For the `EarthEngineLayer`, only `ee_object` and `vis_params` may be positional arguments; all others must be keyword arguments. For the `EarthEngineTerrainLayer`, only `ee_object`, `ee_terrain_object`, and `vis_params` may be positional arguments; all others must be keyword arguments. 7 | - Various JavaScript-side improvements, which trickle down to Pydeck. See [What's New](/docs/whats-new) for more information. 8 | 9 | ## 1.1.0 10 | 11 | Skipped to maintain identical versions as JS bundle 12 | 13 | ## 1.0.1 (2020-08-06) 14 | 15 | - Pin JS bundle used by pydeck to `1.0.0`, since `1.1.0` upgrades the TileLayer to deck.gl 8.2 and is currently broken. 16 | 17 | ## 1.0.0 (2020-06-05) 18 | 19 | - Release in conjunction with NPM 1.0 release. 20 | 21 | ## 0.2.2 (2020-06-05) 22 | 23 | - Update Python module to point to _beta_ NPM module via unpkg for now 24 | 25 | ## 0.2.1 (2020-06-04) 26 | 27 | - Update Python module to point to released NPM module via unpkg, instead of serving directly from Github. #77 28 | 29 | ## 0.2.0 (2020-05-26) 30 | 31 | - Set `vis_params` as the second positional argument #56 32 | - Use a minified bundle #51 33 | - Option to set a custom EE Layer bundle url #31 34 | 35 | ## 0.1.1 (2020-04-27) 36 | 37 | - Include `requirements.txt` and `requirements_dev.txt` in `MANIFEST.in` to fix install. 38 | 39 | ## 0.1.0 (2020-04-23) 40 | 41 | - First release on PyPI. 42 | -------------------------------------------------------------------------------- /py/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include README.md 3 | include requirements.txt 4 | include requirements_dev.txt 5 | 6 | recursive-include tests * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | 10 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 11 | -------------------------------------------------------------------------------- /py/README.md: -------------------------------------------------------------------------------- 1 | # pydeck-earthengine-layers 2 | 3 | Pydeck wrapper for use with Google Earth Engine 4 | 5 | ## Install 6 | 7 | ```bash 8 | pip install pydeck-earthengine-layers 9 | ``` 10 | 11 | This also ensures [pydeck](https://pydeck.gl/) is installed. If you haven't 12 | previously enabled pydeck to be used with Jupyter, follow [its 13 | instructions](https://pydeck.gl/installation.html) to install. 14 | 15 | ## Using 16 | 17 | For an example, see the 18 | [`examples/Introduction.ipynb`](https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/master/py/examples/Introduction.ipynb) Jupyter Notebook. 19 | 20 | ## Release notes 21 | 22 | To release, bump the version by running: 23 | 24 | ```bash 25 | bumpversion patch 26 | // or 27 | bumpversion minor 28 | ``` 29 | 30 | Then create a dist bundle with: 31 | 32 | ```bash 33 | python setup.py sdist 34 | ``` 35 | 36 | Then upload to PyPI with 37 | 38 | ```bash 39 | twine upload dist/pydeck-earthengine-layers-0.1.0.tar.gz 40 | ``` 41 | 42 | replacing with the current version number. 43 | -------------------------------------------------------------------------------- /py/docs/pydeck-earthengine-layer.md: -------------------------------------------------------------------------------- 1 | # Pydeck EarthEngineLayer 2 | 3 |

4 | @unfolded.gl/earthengine-layers 5 |

6 | 7 | The pydeck `EarthEngineLayer` connects [Google Earth Engine][gee] to 8 | [pydeck](https://pydeck.gl), making it possible to visualize planetary-scale 9 | geospatial datasets from Python. 10 | 11 | [gee]: https://earthengine.google.com/ 12 | 13 | To use this layer, you'll need to authenticate with an EarthEngine-enabled 14 | Google Account. [Visit here][gee-signup] to sign up. 15 | 16 | [gee-signup]: https://signup.earthengine.google.com/#!/ 17 | 18 | ```py 19 | from pydeck_earthengine_layers import EarthEngineLayer 20 | import pydeck as pdk 21 | import ee 22 | 23 | # Initialize Earth Engine library 24 | ee.Initialize() 25 | 26 | # Create an Earth Engine object 27 | image = ee.Image('CGIAR/SRTM90_V4') 28 | 29 | # Define Earth Engine visualization parameters 30 | vis_params = { 31 | "min": 0, 32 | "max": 4000, 33 | 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'] 34 | } 35 | 36 | # Create a pydeck EarthEngineLayer object, using the Earth Engine object and 37 | # desired visualization parameters 38 | ee_layer = EarthEngineLayer(image, vis_params) 39 | 40 | # Define the initial viewport for the map 41 | view_state = pdk.ViewState(latitude=37.7749295, longitude=-122.4194155, zoom=10, bearing=0, pitch=45) 42 | 43 | # Create a Deck instance, and display in Jupyter 44 | r = pdk.Deck(layers=[ee_layer], initial_view_state=view_state) 45 | r.show() 46 | ``` 47 | 48 | ## Installation 49 | 50 | See the [pydeck integration guide](/docs/developer-guide/pydeck-integration.md) 51 | for installation instructions. 52 | 53 | ## `EarthEngineLayer` 54 | 55 | The `pydeck-earthengine-layer` package exports the `EarthEngineLayer` class. 56 | This makes interfacing with EarthEngine easy: behind the scenes it handles 57 | authentication, loading the EarthEngine JavaScript library, and correctly 58 | passing the EarthEngine objects to JavaScript. 59 | 60 | ### Arguments 61 | 62 | - `ee_object`: (_required_), an instance of a Python EarthEngine object, such as 63 | an `ee.Image`, `ee.ImageCollection`, `ee.FeatureCollection`, etc. 64 | - `vis_params`: (`dict`, _optional_), parameters for how to visualize the 65 | EarthEngine object. See [EarthEngine 66 | documentation](https://developers.google.com/earth-engine/image_visualization) 67 | for expected `dict` layout. Default: `None`. 68 | - `credentials`: (_optional_), Google OAuth2 credentials object. Default: saved 69 | credentials are loaded automatically. 70 | - `library_url`: (`str`, _optional_), Force loading of the JavaScript 71 | `EarthEngineLayer` bundle for a specific URL. By default, loads from the most 72 | recent `EarthEngineLayer` release. 73 | 74 | **All properties accepted by the [JavaScript 75 | `EarthEngineLayer`](/modules/earthengine-layers/docs/api-reference/earthengine-layer.md#properties) 76 | are accepted as keyword arguments**. So, for example, to animate an 77 | `ee.ImageCollection` from Python, you can pass 78 | 79 | ```py 80 | EarthEngineLayer(ee.ImageCollection(...), animate=True) 81 | ``` 82 | 83 | ## Source 84 | 85 | [`py/pydeck_earthengine_layers/`](https://github.com/UnfoldedInc/earthengine-layers/tree/master/py/pydeck_earthengine_layers) 86 | -------------------------------------------------------------------------------- /py/docs/pydeck-earthengine-terrain-layer.md: -------------------------------------------------------------------------------- 1 | # Pydeck EarthEngineTerrainLayer 2 | 3 |

4 | @unfolded.gl/earthengine-layers 5 |

6 | 7 | The pydeck `EarthEngineTerrainLayer` connects [Google Earth Engine][gee] to 8 | [pydeck](https://pydeck.gl), making it possible to visualize planetary-scale 9 | geospatial datasets over 3D terrain from Python. 10 | 11 | [gee]: https://earthengine.google.com/ 12 | 13 | To use this layer, you'll need to authenticate with an EarthEngine-enabled 14 | Google Account. [Visit here][gee-signup] to sign up. 15 | 16 | [gee-signup]: https://signup.earthengine.google.com/#!/ 17 | 18 | ```py 19 | from pydeck_earthengine_layers import EarthEngineTerrainLayer 20 | import pydeck as pdk 21 | import ee 22 | 23 | # Initialize Earth Engine library 24 | ee.Initialize() 25 | 26 | # Create Earth Engine objects for visualization and terrain heights 27 | image = ee.Image('USGS/SRTMGL1_003') 28 | terrain = ee.Image('USGS/NED').select('elevation') 29 | 30 | # Define Earth Engine visualization parameters 31 | vis_params = { 32 | "min": 0, 33 | "max": 4000, 34 | 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'] 35 | } 36 | 37 | # Create a pydeck EarthEngineTerrainLayer object, using the Earth Engine objects and 38 | # desired visualization parameters 39 | ee_layer = EarthEngineTerrainLayer(image, terrain, vis_params) 40 | 41 | # Define the initial viewport for the map 42 | view_state = pdk.ViewState( 43 | latitude=36.15, 44 | longitude=-111.96, 45 | zoom=10.5, 46 | bearing=-66.16, 47 | pitch=60) 48 | 49 | # Create a Deck instance, and display in Jupyter 50 | r = pdk.Deck(layers=[ee_layer], initial_view_state=view_state) 51 | r.show() 52 | ``` 53 | 54 | ## Installation 55 | 56 | See the [pydeck integration guide](/docs/developer-guide/pydeck-integration.md) 57 | for installation instructions. 58 | 59 | ## `EarthEngineTerrainLayer` 60 | 61 | The `pydeck-earthengine-layer` package exports the `EarthEngineTerrainLayer` 62 | class. This makes interfacing with EarthEngine easy: behind the scenes it 63 | handles authentication, loading the EarthEngine JavaScript library, and 64 | correctly passing the EarthEngine objects to JavaScript. 65 | 66 | ### Arguments 67 | 68 | - `ee_object`: (_required_), an instance of a Python EarthEngine `Image` object used for visualization 69 | - `ee_terrain_object`: (_required_), an instance of a Python EarthEngine `Image` object used for terrain heights 70 | - `vis_params`: (`dict`, _optional_), parameters for how to visualize the 71 | EarthEngine object. See [EarthEngine 72 | documentation](https://developers.google.com/earth-engine/image_visualization) 73 | for expected `dict` layout. Default: `None`. 74 | - `credentials`: (_optional_), Google OAuth2 credentials object. Default: saved 75 | credentials are loaded automatically. 76 | - `library_url`: (`str`, _optional_), Force loading of the JavaScript 77 | `EarthEngineLayer` bundle for a specific URL. By default, loads from the most 78 | recent `EarthEngineLayer` release. 79 | 80 | **All properties accepted by the [JavaScript 81 | `EarthEngineTerrainLayer`](/modules/earthengine-layers/docs/api-reference/earthengine-terrain-layer.md#properties) 82 | are accepted as keyword arguments**. 83 | 84 | ## Source 85 | 86 | [`py/pydeck_earthengine_layers/`](https://github.com/UnfoldedInc/earthengine-layers/tree/master/py/pydeck_earthengine_layers) 87 | -------------------------------------------------------------------------------- /py/examples/Introduction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Pydeck Earth Engine Introduction\n", 8 | "\n", 9 | "This is an introduction to using [Pydeck](https://pydeck.gl) and [Deck.gl](https://deck.gl) with [Google Earth Engine](https://earthengine.google.com/) in Jupyter Notebooks.\n", 10 | "\n", 11 | "To see this example online, view the [JavaScript version][js-example].\n", 12 | "\n", 13 | "[js-example]: https://earthengine-layers.com/examples/image" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "To run this notebook locally, you'll need to install some dependencies. Installing into a new Conda environment is recommended. To create and enter the environment, make sure you have [Conda installed](https://www.anaconda.com/products/individual), then run:\n", 21 | "\n", 22 | "```bash\n", 23 | "conda create -n pydeck-ee -c conda-forge python jupyter jupyterlab notebook pydeck earthengine-api nodejs -y\n", 24 | "conda activate pydeck-ee\n", 25 | "\n", 26 | "# Install the pydeck-earthengine-layers package from pip\n", 27 | "pip install pydeck-earthengine-layers\n", 28 | "\n", 29 | "# If using Jupyter Notebook:\n", 30 | "jupyter nbextension install --sys-prefix --symlink --overwrite --py pydeck\n", 31 | "jupyter nbextension enable --sys-prefix --py pydeck\n", 32 | "\n", 33 | "# If using Jupyter Lab\n", 34 | "jupyter labextension install @jupyter-widgets/jupyterlab-manager\n", 35 | "DECKGL_SEMVER=`python -c \"import pydeck; print(pydeck.frontend_semver.DECKGL_SEMVER)\"`\n", 36 | "jupyter labextension install @deck.gl/jupyter-widget@$DECKGL_SEMVER\n", 37 | "```\n", 38 | "\n", 39 | "then open Jupyter Notebook with `jupyter notebook` or Jupyter Lab with `jupyter lab`." 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "Now in a Python Jupyter Notebook, let's first import required packages:" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "from pydeck_earthengine_layers import EarthEngineLayer\n", 56 | "import pydeck as pdk\n", 57 | "import ee" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "### Authenticate with Earth Engine\n", 65 | "\n", 66 | "Using Earth Engine requires authentication. If you don't have a Google account approved for use with Earth Engine, you'll need to request access. For more information and to sign up, go to https://signup.earthengine.google.com/." 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "If you haven't used Earth Engine in Python before, the next block will create a prompt which waits for user input. If you don't see a prompt, you may need to authenticate on the command line with `earthengine authenticate` and then restart the notebook." 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": { 80 | "scrolled": true 81 | }, 82 | "outputs": [], 83 | "source": [ 84 | "try:\n", 85 | " ee.Initialize()\n", 86 | "except Exception as e:\n", 87 | " ee.Authenticate()\n", 88 | " ee.Initialize()" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "metadata": {}, 94 | "source": [ 95 | "### Elevation data example\n", 96 | "\n", 97 | "Now let's make a simple example using [Shuttle Radar Topography Mission (SRTM)][srtm] elevation data. Here we create an `ee.Image` object referencing that dataset.\n", 98 | "\n", 99 | "[srtm]: https://developers.google.com/earth-engine/datasets/catalog/CGIAR_SRTM90_V4" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "image = ee.Image('CGIAR/SRTM90_V4')" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "Here `vis_params` consists of parameters that will be passed to the Earth Engine [`visParams` argument][visparams]. Any parameters that you could pass directly to Earth Engine in the code editor, you can also pass here to the `EarthEngineLayer`.\n", 116 | "\n", 117 | "[visparams]: https://developers.google.com/earth-engine/image_visualization" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "vis_params={\n", 127 | " \"min\": 0, \n", 128 | " \"max\": 4000,\n", 129 | " \"palette\": ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5']\n", 130 | "}" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": {}, 136 | "source": [ 137 | "Now we're ready to create the Pydeck layer. The `EarthEngineLayer` makes this simple. Just pass the Earth Engine object to the class." 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "Including the `id` argument isn't necessary when you only have one pydeck layer, but it is necessary to distinguish multiple layers, so it's good to get into the habit of including an `id` parameter." 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": null, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "ee_layer = EarthEngineLayer(\n", 154 | " image,\n", 155 | " vis_params,\n", 156 | " id=\"SRTM_layer\"\n", 157 | ")" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "Then just pass this layer to a `pydeck.Deck` instance, and call `.show()` to create a map:" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "view_state = pdk.ViewState(latitude=37.7749295, longitude=-122.4194155, zoom=10, bearing=0, pitch=45)\n", 174 | "r = pdk.Deck(\n", 175 | " layers=[ee_layer], \n", 176 | " initial_view_state=view_state\n", 177 | ")\n", 178 | "r.show()" 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "metadata": {}, 184 | "source": [ 185 | "## Hillshade Example\n", 186 | "\n", 187 | "As a slightly more in-depth example, let's use the SRTM dataset to calculate hillshading. This example comes from [giswqs/earthengine-py-notebooks][giswqs/earthengine-py-notebooks]:\n", 188 | "\n", 189 | "[giswqs/earthengine-py-notebooks]: https://github.com/giswqs/earthengine-py-notebooks/blob/master/Visualization/hillshade.ipynb" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": null, 195 | "metadata": {}, 196 | "outputs": [], 197 | "source": [ 198 | "# Add Earth Engine dataset\n", 199 | "import math\n", 200 | "\n", 201 | "def Radians(img):\n", 202 | " return img.toFloat().multiply(math.pi).divide(180)\n", 203 | "\n", 204 | "def Hillshade(az, ze, slope, aspect):\n", 205 | " \"\"\"Compute hillshade for the given illumination az, el.\"\"\"\n", 206 | " azimuth = Radians(ee.Image(az))\n", 207 | " zenith = Radians(ee.Image(ze))\n", 208 | " # Hillshade = cos(Azimuth - Aspect) * sin(Slope) * sin(Zenith) +\n", 209 | " # cos(Zenith) * cos(Slope)\n", 210 | " return (azimuth.subtract(aspect).cos()\n", 211 | " .multiply(slope.sin())\n", 212 | " .multiply(zenith.sin())\n", 213 | " .add(\n", 214 | " zenith.cos().multiply(slope.cos())))\n", 215 | "\n", 216 | "terrain = ee.Algorithms.Terrain(ee.Image('srtm90_v4'))\n", 217 | "slope_img = Radians(terrain.select('slope'))\n", 218 | "aspect_img = Radians(terrain.select('aspect'))" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "metadata": { 225 | "scrolled": true 226 | }, 227 | "outputs": [], 228 | "source": [ 229 | "ee_object = Hillshade(0, 60, slope_img, aspect_img)\n", 230 | "ee_layer = EarthEngineLayer(ee_object)\n", 231 | "view_state = pdk.ViewState(latitude=36.0756, longitude=-111.9987, zoom=7, bearing=0, pitch=30)\n", 232 | "r = pdk.Deck(\n", 233 | " layers=[ee_layer], \n", 234 | " initial_view_state=view_state\n", 235 | ")\n", 236 | "r.show()" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": null, 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [] 245 | } 246 | ], 247 | "metadata": { 248 | "kernelspec": { 249 | "display_name": "Python 3", 250 | "language": "python", 251 | "name": "python3" 252 | }, 253 | "language_info": { 254 | "codemirror_mode": { 255 | "name": "ipython", 256 | "version": 3 257 | }, 258 | "file_extension": ".py", 259 | "mimetype": "text/x-python", 260 | "name": "python", 261 | "nbconvert_exporter": "python", 262 | "pygments_lexer": "ipython3", 263 | "version": "3.7.7" 264 | } 265 | }, 266 | "nbformat": 4, 267 | "nbformat_minor": 4 268 | } 269 | -------------------------------------------------------------------------------- /py/examples/README.md: -------------------------------------------------------------------------------- 1 | ## Earth Engine Pydeck examples 2 | 3 | This folder contains examples for using Pydeck with Google Earth Engine. 4 | 5 | ### Install 6 | 7 | To run the example notebooks locally, you need to install a few Python 8 | dependencies. A custom Conda environment is recommended. To create and enter the 9 | environment, and enable Pydeck with Jupyter Notebook run: 10 | 11 | ```bash 12 | conda create -n pydeck-ee -c conda-forge python jupyter jupyterlab notebook pydeck earthengine-api nodejs -y 13 | conda activate pydeck-ee 14 | 15 | # Install the pydeck-earthengine-layers package from pip 16 | pip install pydeck-earthengine-layers 17 | 18 | # If using Jupyter Notebook: 19 | jupyter nbextension install --sys-prefix --symlink --overwrite --py pydeck 20 | jupyter nbextension enable --sys-prefix --py pydeck 21 | 22 | # If using Jupyter Lab 23 | jupyter labextension install @jupyter-widgets/jupyterlab-manager 24 | DECKGL_SEMVER=`python -c "import pydeck; print(pydeck.frontend_semver.DECKGL_SEMVER)"` 25 | jupyter labextension install @deck.gl/jupyter-widget@$DECKGL_SEMVER 26 | ``` 27 | 28 | ### Start a notebook 29 | 30 | Then to start a notebook with Jupyter Notebook, run: 31 | 32 | ```bash 33 | jupyter notebook Introduction.ipynb 34 | ``` 35 | 36 | To start a notebook with Jupyter Lab, run: 37 | 38 | ```bash 39 | jupyter lab Introduction.ipynb 40 | ``` -------------------------------------------------------------------------------- /py/examples/international_boundaries.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Pydeck + Earth Engine: Polygon FeatureCollection\n", 8 | "\n", 9 | "This is an example of using [pydeck](https://pydeck.gl) to visualize a Google Earth Engine `FeatureCollection` of polygons.\n", 10 | "To install and run this notebook locally, refer to the [Pydeck Earth Engine documentation](https://earthengine-layers.com/docs/developer-guide/pydeck-integration).\n", 11 | "\n", 12 | "To see this example online, view the [JavaScript version][js-example].\n", 13 | "\n", 14 | "[js-example]: https://earthengine-layers.com/examples/intl-boundary" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "Import required packages:" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "from pydeck_earthengine_layers import EarthEngineLayer\n", 31 | "import pydeck as pdk\n", 32 | "import ee" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "### Authenticate with Earth Engine\n", 40 | "\n", 41 | "Using Earth Engine requires authentication. If you don't have a Google account approved for use with Earth Engine, you'll need to request access. For more information and to sign up, go to https://signup.earthengine.google.com/." 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": { 48 | "scrolled": true 49 | }, 50 | "outputs": [], 51 | "source": [ 52 | "try:\n", 53 | " ee.Initialize()\n", 54 | "except Exception as e:\n", 55 | " ee.Authenticate()\n", 56 | " ee.Initialize()" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "### International Boundaries dataset\n", 64 | "\n", 65 | "This example uses the [Large Scale International Boundary Polygons][lsib] dataset, which contains simplified boundaries of countries.\n", 66 | "\n", 67 | "[lsib]: https://developers.google.com/earth-engine/datasets/catalog/USDOS_LSIB_SIMPLE_2017" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "Import the dataset by creating an Earth Engine object that references it." 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "dataset = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017')" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "Apply Earth Engine styling on this dataset. Here we render the fill of the polygon with a shade of green, and the border color with a shade of blue." 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "countries = dataset.style(\n", 100 | " fillColor='b5ffb4',\n", 101 | " color='00909F',\n", 102 | " width=3\n", 103 | ")" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "Create a new `EarthEngineLayer` with this dataset that can then be passed to Pydeck." 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "layer = EarthEngineLayer(countries, id=\"international_boundaries\")" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "Then just pass this layer to a `pydeck.Deck` instance, and call `.show()` to create a map:" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "view_state = pdk.ViewState(latitude=36, longitude=10, zoom=3)\n", 136 | "r = pdk.Deck(\n", 137 | " layers=[layer], \n", 138 | " initial_view_state=view_state\n", 139 | ")\n", 140 | "r.show()" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [] 149 | } 150 | ], 151 | "metadata": { 152 | "kernelspec": { 153 | "display_name": "Python 3", 154 | "language": "python", 155 | "name": "python3" 156 | }, 157 | "language_info": { 158 | "codemirror_mode": { 159 | "name": "ipython", 160 | "version": 3 161 | }, 162 | "file_extension": ".py", 163 | "mimetype": "text/x-python", 164 | "name": "python", 165 | "nbconvert_exporter": "python", 166 | "pygments_lexer": "ipython3", 167 | "version": "3.7.7" 168 | } 169 | }, 170 | "nbformat": 4, 171 | "nbformat_minor": 4 172 | } 173 | -------------------------------------------------------------------------------- /py/examples/power_plants.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Pydeck + Earth Engine: Points FeatureCollection\n", 8 | "\n", 9 | "This is an example of using [pydeck](https://pydeck.gl) to visualize a Google Earth Engine `FeatureCollection` of points.\n", 10 | "To install and run this notebook locally, refer to the [Pydeck Earth Engine documentation](https://earthengine-layers.com/docs/developer-guide/pydeck-integration).\n", 11 | "\n", 12 | "To see this example online, view the [JavaScript version][js-example].\n", 13 | "\n", 14 | "[js-example]: https://earthengine-layers.com/examples/power-plants" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "Import required packages:" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "from pydeck_earthengine_layers import EarthEngineLayer\n", 31 | "import pydeck as pdk\n", 32 | "import ee" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "### Authenticate with Earth Engine\n", 40 | "\n", 41 | "Using Earth Engine requires authentication. If you don't have a Google account approved for use with Earth Engine, you'll need to request access. For more information and to sign up, go to https://signup.earthengine.google.com/." 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": { 48 | "scrolled": true 49 | }, 50 | "outputs": [], 51 | "source": [ 52 | "try:\n", 53 | " ee.Initialize()\n", 54 | "except Exception as e:\n", 55 | " ee.Authenticate()\n", 56 | " ee.Initialize()" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "### Power plants dataset\n", 64 | "\n", 65 | "This example uses the [Global Power Plant Database][dataset], which contains location of power plants around the world, as well as metadata of which fuel source is used, and how much energy is created.\n", 66 | "\n", 67 | "[dataset]: https://developers.google.com/earth-engine/datasets/catalog/WRI_GPPD_power_plants\n", 68 | "\n", 69 | "First we'll render using EarthEngine's native capabilities, then we'll render using pydeck's rich data-driven styling capabilities." 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "# Load the FeatureCollection\n", 79 | "table = ee.FeatureCollection(\"WRI/GPPD/power_plants\")" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "# Create color palette\n", 89 | "fuel_color = ee.Dictionary({\n", 90 | " 'Coal': '000000',\n", 91 | " 'Oil': '593704',\n", 92 | " 'Gas': 'BC80BD',\n", 93 | " 'Hydro': '0565A6',\n", 94 | " 'Nuclear': 'E31A1C',\n", 95 | " 'Solar': 'FF7F00',\n", 96 | " 'Waste': '6A3D9A',\n", 97 | " 'Wind': '5CA2D1',\n", 98 | " 'Geothermal': 'FDBF6F',\n", 99 | " 'Biomass': '229A00'\n", 100 | "})" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "metadata": {}, 107 | "outputs": [], 108 | "source": [ 109 | "# List of fuels to add to the map\n", 110 | "fuels = ['Coal', 'Oil', 'Gas', 'Hydro', 'Nuclear', 'Solar', 'Waste',\n", 111 | " 'Wind', 'Geothermal', 'Biomass']" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "def add_style(point):\n", 121 | " \"\"\"Computes size from capacity and color from fuel type.\n", 122 | " \n", 123 | " Args:\n", 124 | " - point: (ee.Geometry.Point) A Point\n", 125 | " \n", 126 | " Returns:\n", 127 | " (ee.Geometry.Point): Input point with added style dictionary\n", 128 | " \"\"\"\n", 129 | " size = ee.Number(point.get('capacitymw')).sqrt().divide(10).add(2)\n", 130 | " color = fuel_color.get(point.get('fuel1'))\n", 131 | " return point.set('styleProperty', ee.Dictionary({'pointSize': size, 'color': color}))" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "# Make a FeatureCollection out of the power plant data table\n", 141 | "pp = ee.FeatureCollection(table).map(add_style)" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "# Create a layer for each fuel type\n", 151 | "layers = []\n", 152 | "for fuel in fuels:\n", 153 | " layer = EarthEngineLayer(\n", 154 | " pp.filter(ee.Filter.eq('fuel1', fuel)).style(styleProperty='styleProperty', neighborhood=50),\n", 155 | " id=fuel,\n", 156 | " opacity=0.65,\n", 157 | " )\n", 158 | " layers.append(layer)" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "Then just pass this layer to a `pydeck.Deck` instance, and call `.show()` to create a map:" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "view_state = pdk.ViewState(latitude=36, longitude=-53, zoom=3)\n", 175 | "r = pdk.Deck(\n", 176 | " layers=layers, \n", 177 | " initial_view_state=view_state\n", 178 | ")\n", 179 | "r.show()" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "metadata": {}, 185 | "source": [ 186 | "### Vector rendering\n", 187 | "\n", 188 | "To experience the full potential of pydeck, we need to explore rendering the data with vector output. Rendering the data as a vector allows for client-side data driven styling and picking objects to get information on each." 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "fuel_color = {\n", 198 | " 'Coal': [0, 0, 0],\n", 199 | " 'Oil': [89, 55, 4],\n", 200 | " 'Gas': [188, 128, 189],\n", 201 | " 'Hydro': [5, 101, 166],\n", 202 | " 'Nuclear': [227, 26, 28],\n", 203 | " 'Solar': [255, 127, 0],\n", 204 | " 'Waste': [106, 61, 154],\n", 205 | " 'Wind': [92, 162, 209],\n", 206 | " 'Geothermal': [253, 191, 111],\n", 207 | " 'Biomass': [34, 154, 0],\n", 208 | "}" 209 | ] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "execution_count": null, 214 | "metadata": {}, 215 | "outputs": [], 216 | "source": [ 217 | "layers = []\n", 218 | "for fuel in fuels:\n", 219 | " layer = EarthEngineLayer(\n", 220 | " # Create each layer as consisting of a single fuel type\n", 221 | " table.filter(ee.Filter.eq('fuel1', fuel)),\n", 222 | " vis_params={},\n", 223 | " # Define a custom radius\n", 224 | " getRadius='.properties.capacitymw ** 1.35',\n", 225 | " lineWidthMinPixels=0.5,\n", 226 | " pointRadiusMinPixels=2,\n", 227 | " # Use dictionary for color mapping\n", 228 | " get_fill_color=fuel_color[fuel],\n", 229 | " # Provide a list of extra dataset properties that should be downloaded for rendering\n", 230 | " # Without this, only the geometry will be downloaded\n", 231 | " selectors=['capacitymw'],\n", 232 | " # Render as a vector, instead of as a raster generated on EE's servers\n", 233 | " as_vector=True,\n", 234 | " id=fuel,\n", 235 | " opacity=0.65,\n", 236 | " )\n", 237 | " layers.append(layer)" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "Then just as before, we define a `ViewState` and pass these layers to the `Deck` object." 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": null, 250 | "metadata": {}, 251 | "outputs": [], 252 | "source": [ 253 | "view_state = pdk.ViewState(latitude=36, longitude=-53, zoom=3)\n", 254 | "r = pdk.Deck(\n", 255 | " layers=layers, \n", 256 | " initial_view_state=view_state\n", 257 | ")\n", 258 | "r.show()" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": null, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [] 267 | } 268 | ], 269 | "metadata": { 270 | "kernelspec": { 271 | "display_name": "Python 3", 272 | "language": "python", 273 | "name": "python3" 274 | }, 275 | "language_info": { 276 | "codemirror_mode": { 277 | "name": "ipython", 278 | "version": 3 279 | }, 280 | "file_extension": ".py", 281 | "mimetype": "text/x-python", 282 | "name": "python", 283 | "nbconvert_exporter": "python", 284 | "pygments_lexer": "ipython3", 285 | "version": "3.7.7" 286 | } 287 | }, 288 | "nbformat": 4, 289 | "nbformat_minor": 4 290 | } 291 | -------------------------------------------------------------------------------- /py/examples/temperature.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Pydeck + Earth Engine: ImageCollection Animation\n", 8 | "\n", 9 | "This is an example of using [pydeck](https://pydeck.gl) to animate a Google Earth Engine `ImageCollection` object. To install and run this notebook locally, refer to the [Pydeck Earth Engine documentation](https://earthengine-layers.com/docs/developer-guide/pydeck-integration).\n", 10 | "\n", 11 | "To see this example online, view the [JavaScript version][js-example].\n", 12 | "\n", 13 | "[js-example]: https://earthengine-layers.com/examples/image-collection" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "Import required packages:" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "from pydeck_earthengine_layers import EarthEngineLayer\n", 30 | "import pydeck as pdk\n", 31 | "import ee" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "### Authenticate with Earth Engine\n", 39 | "\n", 40 | "Using Earth Engine requires authentication. If you don't have a Google account approved for use with Earth Engine, you'll need to request access. For more information and to sign up, go to https://signup.earthengine.google.com/." 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "metadata": { 47 | "scrolled": true 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "try:\n", 52 | " ee.Initialize()\n", 53 | "except Exception as e:\n", 54 | " ee.Authenticate()\n", 55 | " ee.Initialize()" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "### GFS Temperature Forecast Dataset\n", 63 | "\n", 64 | "This example uses the [Global Forecast System 384-Hour Predicted Atmosphere Data][gfs] dataset, which contains a weather forecast model produced by the National Centers for Environmental Prediction (NCEP)\n", 65 | "\n", 66 | "[gfs]: https://developers.google.com/earth-engine/datasets/catalog/NOAA_GFS0P25" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "Import the dataset by creating an Earth Engine object that references it." 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 3, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "# Initialize an ee.ImageColllection object referencing the Global Forecast System dataset\n", 83 | "image_collection = ee.ImageCollection('NOAA/GFS0P25')\n", 84 | "\n", 85 | "# Select images from December 22, 2018\n", 86 | "image_collection = image_collection.filterDate('2018-12-22', '2018-12-23')\n", 87 | "\n", 88 | "# Choose the first 24 images in the ImageCollection\n", 89 | "image_collection = image_collection.limit(24)\n", 90 | "\n", 91 | "# Select a single band to visualize\n", 92 | "image_collection = image_collection.select('temperature_2m_above_ground')" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "Create a `vis_params` object that defines how EarthEngine should style the `ImageCollection`" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 4, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "# Style temperature values between -40C and 35C, \n", 109 | "# with lower values shades of blue, purple, and cyan, \n", 110 | "# and higher values shades of green, yellow, and red\n", 111 | "vis_params = {\n", 112 | " 'min': -40.0,\n", 113 | " 'max': 35.0,\n", 114 | " 'palette': ['blue', 'purple', 'cyan', 'green', 'yellow', 'red']\n", 115 | "};" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "Create a new `EarthEngineLayer` with this dataset that can then be passed to Pydeck." 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 5, 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "layer = EarthEngineLayer(\n", 132 | " image_collection,\n", 133 | " vis_params,\n", 134 | " animate=True,\n", 135 | " id=\"global_weather\")" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "Then just pass this layer to a `pydeck.Deck` instance, and call `.show()` to create a map:" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 6, 148 | "metadata": {}, 149 | "outputs": [ 150 | { 151 | "data": { 152 | "application/vnd.jupyter.widget-view+json": { 153 | "model_id": "fc9ad416126540ee90b4a549a794b8d6", 154 | "version_major": 2, 155 | "version_minor": 0 156 | }, 157 | "text/plain": [ 158 | "DeckGLWidget(custom_libraries=[{'libraryName': 'EarthEngineLayerLibrary', 'resourceUri': 'https://unpkg.com/@u…" 159 | ] 160 | }, 161 | "metadata": {}, 162 | "output_type": "display_data" 163 | } 164 | ], 165 | "source": [ 166 | "view_state = pdk.ViewState(latitude=36, longitude=10, zoom=1)\n", 167 | "r = pdk.Deck(\n", 168 | " layers=[layer], \n", 169 | " initial_view_state=view_state\n", 170 | ")\n", 171 | "r.show()" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": null, 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [] 180 | } 181 | ], 182 | "metadata": { 183 | "kernelspec": { 184 | "display_name": "Python 3", 185 | "language": "python", 186 | "name": "python3" 187 | }, 188 | "language_info": { 189 | "codemirror_mode": { 190 | "name": "ipython", 191 | "version": 3 192 | }, 193 | "file_extension": ".py", 194 | "mimetype": "text/x-python", 195 | "name": "python", 196 | "nbconvert_exporter": "python", 197 | "pygments_lexer": "ipython3", 198 | "version": "3.7.7" 199 | } 200 | }, 201 | "nbformat": 4, 202 | "nbformat_minor": 4 203 | } 204 | -------------------------------------------------------------------------------- /py/examples/terrain.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Pydeck + Earth Engine: Terrain Visualization\n", 8 | "\n", 9 | "This is an example of using [pydeck](https://pydeck.gl) to visualize a Google Earth Engine `Image` object _over 3D terrain_. To install and run this notebook locally, refer to the [Pydeck Earth Engine documentation](https://earthengine-layers.com/docs/developer-guide/pydeck-integration).\n", 10 | "\n", 11 | "To see this example online, view the [JavaScript version][js-example].\n", 12 | "\n", 13 | "[js-example]: https://earthengine-layers.com/examples/terrain" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "First, import required packages. Note that here we import the `EarthEngineTerrainLayer` instead of the `EarthEngineLayer`." 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "from pydeck_earthengine_layers import EarthEngineTerrainLayer\n", 30 | "import pydeck as pdk\n", 31 | "import ee" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "### Authenticate with Earth Engine\n", 39 | "\n", 40 | "Using Earth Engine requires authentication. If you don't have a Google account approved for use with Earth Engine, you'll need to request access. For more information and to sign up, go to https://signup.earthengine.google.com/." 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": null, 46 | "metadata": { 47 | "scrolled": true 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "try:\n", 52 | " ee.Initialize()\n", 53 | "except Exception as e:\n", 54 | " ee.Authenticate()\n", 55 | " ee.Initialize()" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "### Terrain Example\n", 63 | "\n", 64 | "In contrast to the `EarthEngineLayer`, where you need to supply _one_ Earth Engine object to render, with the `EarthEngineTerrainLayer` you must supply **two** Earth Engine objects. The first is used to render the image in the same way as the `EarthEngineLayer`; the second supplies elevation values used to extrude terrain in 3D. Hence the former can be any `Image` object; the latter must be an `Image` object whose values represents terrain heights.\n", 65 | "\n", 66 | "It's important for the terrain source to have relatively high spatial resolution. In previous examples, we used [SRTM90][srtm90] as an elevation source, but that only has a resolution of 90 meters. When used as an elevation source, it looks very blocky/pixelated at high zoom levels. In this example we'll use [SRTM30][srtm30] (30-meter resolution) as the `Image` source and the [USGS's National Elevation Dataset][ned] (10-meter resolution, U.S. only) as the terrain source. SRTM30 is generally the best-resolution worldwide data source available.\n", 67 | "\n", 68 | "[srtm90]: https://developers.google.com/earth-engine/datasets/catalog/CGIAR_SRTM90_V4\n", 69 | "[srtm30]: https://developers.google.com/earth-engine/datasets/catalog/USGS_SRTMGL1_003\n", 70 | "[ned]: https://developers.google.com/earth-engine/datasets/catalog/USGS_NED" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "image = ee.Image('USGS/SRTMGL1_003')\n", 80 | "terrain = ee.Image('USGS/NED').select('elevation')" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "Here `vis_params` consists of parameters that will be passed to the Earth Engine [`visParams` argument][visparams]. Any parameters that you could pass directly to Earth Engine in the code editor, you can also pass here to the `EarthEngineLayer`.\n", 88 | "\n", 89 | "[visparams]: https://developers.google.com/earth-engine/image_visualization" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "vis_params={\n", 99 | " \"min\": 0, \n", 100 | " \"max\": 4000,\n", 101 | " \"palette\": ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5']\n", 102 | "}" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "Now we're ready to create the Pydeck layer. The `EarthEngineLayer` makes this simple. Just pass the Earth Engine object to the class." 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "Including the `id` argument isn't necessary when you only have one pydeck layer, but it is necessary to distinguish multiple layers, so it's good to get into the habit of including an `id` parameter." 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "ee_layer = EarthEngineTerrainLayer(\n", 126 | " image,\n", 127 | " terrain,\n", 128 | " vis_params,\n", 129 | " id=\"EETerrainLayer\"\n", 130 | ")" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": {}, 136 | "source": [ 137 | "Then just pass this layer to a `pydeck.Deck` instance, and call `.show()` to create a map:" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "view_state = pdk.ViewState(\n", 147 | " latitude=36.15,\n", 148 | " longitude=-111.96,\n", 149 | " zoom=10.5, \n", 150 | " bearing=-66.16,\n", 151 | " pitch=60)\n", 152 | "r = pdk.Deck(\n", 153 | " layers=[ee_layer], \n", 154 | " initial_view_state=view_state\n", 155 | ")\n", 156 | "r.show()" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": null, 162 | "metadata": {}, 163 | "outputs": [], 164 | "source": [] 165 | } 166 | ], 167 | "metadata": { 168 | "kernelspec": { 169 | "display_name": "Python 3", 170 | "language": "python", 171 | "name": "python3" 172 | }, 173 | "language_info": { 174 | "codemirror_mode": { 175 | "name": "ipython", 176 | "version": 3 177 | }, 178 | "file_extension": ".py", 179 | "mimetype": "text/x-python", 180 | "name": "python", 181 | "nbconvert_exporter": "python", 182 | "pygments_lexer": "ipython3", 183 | "version": "3.7.7" 184 | } 185 | }, 186 | "nbformat": 4, 187 | "nbformat_minor": 4 188 | } 189 | -------------------------------------------------------------------------------- /py/pydeck_earthengine_layers/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for pydeck-earthengine-layers.""" 2 | 3 | __author__ = """Kyle Barron""" 4 | __email__ = 'kyle@unfolded.ai' 5 | __version__ = '1.2.0' 6 | 7 | from .pydeck_earthengine_layers import ( 8 | EarthEngineLayer, EarthEngineTerrainLayer) 9 | -------------------------------------------------------------------------------- /py/pydeck_earthengine_layers/pydeck_earthengine_layers.py: -------------------------------------------------------------------------------- 1 | """Main module.""" 2 | 3 | from datetime import datetime, timedelta 4 | 5 | import ee 6 | import pydeck as pdk 7 | import requests 8 | 9 | EARTHENGINE_LAYER_BUNDLE_URL = 'https://unpkg.com/@unfolded.gl/earthengine-layers@1.2.0/dist/pydeck_layer_module.min.js' 10 | 11 | 12 | class EarthEngineLayer(pdk.Layer): 13 | """Wrapper class for using the EarthEngineLayer with Pydeck 14 | """ 15 | layer_name = 'EarthEngineLayer' 16 | 17 | def __init__( 18 | self, 19 | ee_object, 20 | vis_params=None, 21 | *, 22 | credentials=None, 23 | library_url=EARTHENGINE_LAYER_BUNDLE_URL, 24 | **kwargs): 25 | """EarthEngineLayer constructor 26 | 27 | Args: 28 | - ee_object: Earth Engine object 29 | - vis_params: Dict of vis_params to pass to the Earth Engine backend 30 | - credentials: Google OAuth2 credentials object. Saved credentials 31 | will be loaded if not passed. 32 | - library_url: URL from which to load EarthEngineLayer JavaScript 33 | bundle 34 | """ 35 | # Workaround for keyword arguments that Pydeck 0.4.0 can't handle 36 | kwargs = self._handle_kwargs(remove_keys=('selectors', ), **kwargs) 37 | super(EarthEngineLayer, self).__init__( 38 | self.layer_name, None, vis_params=vis_params, **kwargs) 39 | 40 | # Should we assume ee has already been initialized? 41 | ee.Initialize() 42 | 43 | self._set_custom_library(url=library_url) 44 | 45 | self.credentials = credentials or ee.data.get_persistent_credentials() 46 | 47 | # Initialize token expiration to the past 48 | self.access_token = None 49 | self.token_expiration = datetime.now() - timedelta(seconds=1) 50 | 51 | # Define _token as an attribute, so `token` shows up in `vars(class)` 52 | self._token = self.token 53 | 54 | self.ee_object = ee_object.serialize() if not isinstance( 55 | ee_object, str) else ee_object 56 | 57 | # This keeps pydeck from serializing these attributes to JS 58 | # Note: this might be global, but other layers shouldn't depend on these 59 | # keys 60 | pdk.bindings.json_tools.IGNORE_KEYS.extend([ 61 | 'credentials', 'access_token', 'token_expiration']) 62 | 63 | def _handle_kwargs(self, remove_keys=('selectors', ), **kwargs): 64 | """Remove selected keys from kwargs and bind to object directly 65 | 66 | As of Pydeck v0.4.0, Pydeck does "magic" on inputs. String input is 67 | almost always considered a function, and a list of strings is always 68 | considered a function. This means it's currently impossible in pure 69 | Pydeck to pass the `selectors` prop, which represents a list of 70 | properties that should be downloaded as a vector GeoJSON 71 | 72 | For more info/discussion, see https://github.com/visgl/deck.gl/pull/4897 73 | 74 | Args: 75 | - remove_keys: iterable of keys to remove from kwargs and bind to 76 | self 77 | - kwargs: kwargs to handle 78 | 79 | Returns: 80 | - `dict` with input kwargs modified and remove_keys bound to self 81 | """ 82 | for key, value in kwargs.copy().items(): 83 | if key in remove_keys: 84 | setattr(self, key, value) 85 | del kwargs[key] 86 | 87 | return kwargs 88 | 89 | def _set_custom_library( 90 | self, 91 | library_name='EarthEngineLayerLibrary', 92 | url=EARTHENGINE_LAYER_BUNDLE_URL): 93 | """Add EarthEngineLayer JS bundle to pydeck's custom libraries 94 | """ 95 | custom_library = {'libraryName': library_name, 'resourceUri': url} 96 | 97 | if pdk.settings.custom_libraries is None: 98 | pdk.settings.custom_libraries = [custom_library] 99 | return 100 | 101 | exists = any([ 102 | x.get('libraryName') == library_name 103 | for x in pdk.settings.custom_libraries]) 104 | 105 | if not exists: 106 | pdk.settings.custom_libraries.append(custom_library) 107 | 108 | def _refresh_token(self): 109 | """Retrieve OAuth2 access token using persistent credentials 110 | """ 111 | data = { 112 | 'client_id': self.credentials.client_id, 113 | 'client_secret': self.credentials.client_secret, 114 | 'refresh_token': self.credentials.refresh_token, 115 | 'grant_type': 'refresh_token'} 116 | r = requests.post(self.credentials.token_uri, data=data) 117 | 118 | if r.status_code != 200: 119 | raise ValueError('Unable to fetch access token') 120 | 121 | token_dict = r.json() 122 | 123 | # Set expiration time: expires_in * .9 124 | # See reference EE API code here: 125 | # https://github.com/google/earthengine-api/blob/5909d9a19a9b829d49b69f38ac4205b4924b21c9/javascript/src/apiclient.js#L1107 126 | self.access_token = token_dict['access_token'] 127 | self.token_expiration = datetime.now() + timedelta( 128 | seconds=token_dict['expires_in'] * .9) 129 | 130 | @property 131 | def token(self): 132 | """Load existing access_token, or fetch a new one if expired 133 | """ 134 | if datetime.now() < self.token_expiration: 135 | return self.access_token 136 | 137 | self._refresh_token() 138 | return self.access_token 139 | 140 | 141 | class EarthEngineTerrainLayer(EarthEngineLayer): 142 | """Wrapper class for using the EarthEngineTerrainLayer with Pydeck 143 | """ 144 | layer_name = 'EarthEngineTerrainLayer' 145 | 146 | def __init__( 147 | self, 148 | ee_object, 149 | ee_terrain_object, 150 | vis_params=None, 151 | *, 152 | credentials=None, 153 | library_url=EARTHENGINE_LAYER_BUNDLE_URL, 154 | **kwargs): 155 | """EarthEngineTerrainLayer constructor 156 | 157 | Args: 158 | - ee_object: Earth Engine object used for image visualization 159 | - ee_terrain_object: Earth Engine object used for terrain heights 160 | - vis_params: Dict of vis_params to pass to the Earth Engine backend 161 | - credentials: Google OAuth2 credentials object. Saved credentials 162 | will be loaded if not passed. 163 | - library_url: URL from which to load EarthEngineLayer JavaScript 164 | bundle 165 | """ 166 | super(EarthEngineTerrainLayer, self).__init__( 167 | ee_object=ee_object, 168 | ee_terrain_object=ee_terrain_object, 169 | vis_params=vis_params, 170 | credentials=credentials, 171 | library_url=library_url, 172 | **kwargs) 173 | 174 | self.ee_terrain_object = ee_terrain_object.serialize( 175 | ) if not isinstance(ee_terrain_object, str) else ee_terrain_object 176 | -------------------------------------------------------------------------------- /py/requirements.txt: -------------------------------------------------------------------------------- 1 | earthengine-api>=0.1.215 2 | pydeck>=0.3.0 3 | requests 4 | -------------------------------------------------------------------------------- /py/requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip==21.1 2 | bump2version==0.5.11 3 | wheel==0.33.6 4 | watchdog==0.9.0 5 | flake8==3.7.8 6 | tox==3.14.0 7 | coverage==4.5.4 8 | Sphinx==1.8.5 9 | twine==1.14.0 10 | 11 | pytest==4.6.5 12 | pytest-runner==5.1 -------------------------------------------------------------------------------- /py/setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.2.0 3 | 4 | [bumpversion:file:setup.py] 5 | search = version='{current_version}' 6 | replace = version='{new_version}' 7 | 8 | [bumpversion:file:pydeck_earthengine_layers/__init__.py] 9 | search = __version__ = '{current_version}' 10 | replace = __version__ = '{new_version}' 11 | 12 | [bdist_wheel] 13 | universal = 1 14 | 15 | [flake8] 16 | exclude = docs 17 | 18 | [isort] 19 | multi_line_output=4 20 | 21 | [aliases] 22 | test = pytest 23 | 24 | [tool:pytest] 25 | collect_ignore = ['setup.py'] 26 | 27 | -------------------------------------------------------------------------------- /py/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import find_packages, setup 6 | 7 | with open('README.md') as readme_file: 8 | readme = readme_file.read() 9 | 10 | with open('CHANGELOG.md') as history_file: 11 | history = history_file.read() 12 | 13 | with open('requirements.txt') as requirements_file: 14 | requirements = [l.strip() for l in requirements_file.readlines()] 15 | 16 | with open('requirements_dev.txt') as test_requirements_file: 17 | test_requirements = [l.strip() for l in test_requirements_file.readlines()] 18 | 19 | setup_requirements = ['setuptools >= 38.6.0', 'twine >= 1.11.0'] 20 | 21 | setup( 22 | author="Kyle Barron", 23 | author_email='kyle@unfolded.ai', 24 | python_requires='>=3.5', 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Natural Language :: English', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.5', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | ], 36 | description="Pydeck wrapper for use with Google Earth Engine", 37 | install_requires=requirements, 38 | license="MIT license", 39 | long_description=readme + '\n\n' + history, 40 | long_description_content_type='text/markdown', 41 | include_package_data=True, 42 | keywords='pydeck_earthengine_layers', 43 | name='pydeck-earthengine-layers', 44 | packages=find_packages(include=['pydeck_earthengine_layers', 'pydeck_earthengine_layers.*']), 45 | setup_requires=setup_requirements, 46 | test_suite='tests', 47 | tests_require=test_requirements, 48 | url='https://github.com/UnfoldedInc/earthengine-layers/blob/master/py', 49 | version='1.2.0', 50 | zip_safe=False, 51 | ) 52 | -------------------------------------------------------------------------------- /test/browser.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 - 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | /* global window */ 22 | const test = require('tape'); 23 | const {_enableDOMLogging: enableDOMLogging} = require('@probe.gl/test-utils'); 24 | 25 | // require('@luma.gl/debug'); 26 | 27 | let failed = false; 28 | test.onFinish(window.browserTestDriver_finish); 29 | test.onFailure(() => { 30 | failed = true; 31 | window.browserTestDriver_fail(); 32 | }); 33 | 34 | // tap-browser-color alternative 35 | enableDOMLogging({ 36 | getStyle: message => ({ 37 | background: failed ? '#F28E82' : '#8ECA6C', 38 | position: 'absolute', 39 | top: '500px', 40 | width: '100%' 41 | }) 42 | }); 43 | 44 | test('deck.gl', t => { 45 | // require('./modules'); 46 | 47 | // Tests currently only work in browser 48 | // require('./render'); 49 | // require('./interaction'); 50 | 51 | t.end(); 52 | }); 53 | -------------------------------------------------------------------------------- /test/compare-image.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 - 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Enables ES2015 import/export in Node.js 22 | 23 | // TODO: move to test-utils lib 24 | 25 | const console = require('console'); 26 | const fs = require('fs'); 27 | const PNG = require('pngjs').PNG; 28 | const pixelmatch = require('pixelmatch'); 29 | 30 | function printResult(diffRatio, threshold) { 31 | return diffRatio <= threshold 32 | ? console.log('\x1b[32m%s\x1b[0m', 'Rendering test Passed!') 33 | : console.log('\x1b[31m%s\x1b[0m', 'Rendering test failed!'); 34 | } 35 | 36 | function compareImage(newImageName, goldenImageName, threshold) { 37 | const newImageData = fs.readFileSync(newImageName); 38 | const goldenImageData = fs.readFileSync(goldenImageName); 39 | const newImage = PNG.sync.read(newImageData); 40 | const goldenImage = PNG.sync.read(goldenImageData); 41 | const diffImage = new PNG({width: goldenImage.width, height: goldenImage.height}); 42 | const pixelDiffSize = pixelmatch( 43 | goldenImage.data, 44 | newImage.data, 45 | diffImage.data, 46 | goldenImage.width, 47 | goldenImage.height, 48 | {threshold: 0.105, includeAA: true} 49 | ); 50 | 51 | const pixelDiffRatio = pixelDiffSize / (goldenImage.width * goldenImage.height); 52 | console.log(`Testing ${newImageName}`); 53 | console.log(`Mismatched pixel number: ${pixelDiffSize}`); 54 | console.log(`Mismatched pixel ratio: ${pixelDiffRatio}`); 55 | 56 | const diffImageData = PNG.sync.write(diffImage, {colorType: 6}); 57 | const diffImageName = `${newImageName.split('.')[0]}_diff.png`; 58 | fs.writeFileSync(diffImageName, diffImageData); 59 | 60 | fs.unlinkSync(newImageName); 61 | fs.unlinkSync(diffImageName); 62 | printResult(pixelDiffRatio, threshold); 63 | return pixelDiffRatio <= threshold; 64 | } 65 | 66 | module.exports = compareImage; 67 | -------------------------------------------------------------------------------- /test/modules.js: -------------------------------------------------------------------------------- 1 | import '../modules/earthengine-layers/test'; 2 | -------------------------------------------------------------------------------- /test/node-examples.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 - 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Enables ES2015 import/export in Node.js 22 | 23 | const {execFile, execFileSync} = require('child_process'); 24 | const puppeteer = require('puppeteer'); 25 | const console = require('console'); 26 | const process = require('process'); 27 | 28 | const path = require('path'); 29 | 30 | const compareImage = require('./compare-image'); 31 | 32 | const LIB_DIR = path.resolve(__dirname, '..'); 33 | const EXAMPLES_DIR = path.resolve(LIB_DIR, 'examples'); 34 | 35 | let exampleDir; 36 | 37 | async function validateWithWaitingTime(child, folder, waitingTime, threshold) { 38 | const browser = await puppeteer.launch({ 39 | headless: false, 40 | args: [`--window-size=${1000},${800}`] 41 | }); 42 | const page = await browser.newPage(); 43 | await page.waitFor(2000); 44 | await page.goto('http://localhost:8080', {timeout: 50000}); 45 | await page.setViewport({width: 1000, height: 800}); 46 | await page.waitFor(waitingTime); 47 | await page.screenshot({path: 'new.png'}); 48 | 49 | const golderImageName = `${LIB_DIR}/test/render/golden-images/examples/${folder.replace( 50 | /\//g, 51 | '_' 52 | )}.png`; 53 | const result = compareImage('new.png', golderImageName, threshold); 54 | 55 | child.kill(); 56 | await page.waitFor(1000); 57 | await browser.close(); 58 | 59 | return result; 60 | } 61 | 62 | async function yarnAndLaunchWebpack() { 63 | const output = execFileSync('yarn'); //eslint-disable-line 64 | // console.log(output.toString('utf8')); 65 | const child = execFile( 66 | './node_modules/.bin/webpack-dev-server', 67 | ['--env.local', '--config', 'webpack.config.js', '--progress', '--hot'], 68 | { 69 | maxBuffer: 5000 * 1024 70 | }, 71 | err => { 72 | if (err) { 73 | console.error(err); 74 | return; 75 | } 76 | } 77 | ); 78 | return child; 79 | } 80 | 81 | function checkMapboxToken() { 82 | // eslint-disable-next-line 83 | if (process.env.GoogleMapsAPIKey === undefined) { 84 | console.log('\x1b[31m%s\x1b[0m', 'Need set GoogleMapsAPIKey!'); 85 | process.exit(1); //eslint-disable-line 86 | } 87 | } 88 | 89 | function changeFolder(folder) { 90 | console.log('--------------------------'); 91 | console.log(`Begin to test ${folder}`); 92 | process.chdir(path.resolve(exampleDir, folder)); 93 | } 94 | 95 | async function runTestExample(folder) { 96 | changeFolder(folder); 97 | const child = await yarnAndLaunchWebpack(); 98 | const valid = await validateWithWaitingTime(child, folder, 5000, 0.01); 99 | if (!valid) { 100 | process.exit(1); //eslint-disable-line 101 | } 102 | } 103 | 104 | (async () => { 105 | checkMapboxToken(); 106 | 107 | exampleDir = EXAMPLES_DIR; 108 | await runTestExample('experimental/bezier'); 109 | await runTestExample('experimental/json-pure-js'); 110 | 111 | await runTestExample('get-started/pure-js'); 112 | await runTestExample('get-started/pure-js-without-map'); 113 | await runTestExample('get-started/react-webpack-2'); 114 | await runTestExample('get-started/react-without-map'); 115 | 116 | await runTestExample('layer-browser'); 117 | 118 | await runTestExample('website/3d-heatmap'); 119 | await runTestExample('website/arc'); 120 | await runTestExample('website/brushing'); 121 | await runTestExample('website/geojson'); 122 | await runTestExample('website/highway'); 123 | await runTestExample('website/icon'); 124 | await runTestExample('website/line'); 125 | await runTestExample('website/plot'); 126 | await runTestExample('website/scatterplot'); 127 | await runTestExample('website/screen-grid'); 128 | await runTestExample('website/tagmap'); 129 | })(); 130 | -------------------------------------------------------------------------------- /test/node.js: -------------------------------------------------------------------------------- 1 | /* global global */ 2 | 3 | require('reify'); 4 | 5 | // Polyfill for loaders 6 | // TODO - @loaders.gl/polyfills seems to freeze the tests 7 | global.fetch = () => Promise.reject('fetch not available in node'); 8 | 9 | // Polyfill with JSDOM 10 | // const {JSDOM} = require('jsdom'); 11 | // const dom = new JSDOM(``); 12 | // // These globals are required by @jupyter-widgets/base 13 | // global.window = dom.window; 14 | // global.navigator = dom.window.navigator; 15 | // global.document = dom.window.document; 16 | // global.Element = dom.window.Element; 17 | // global.__JSDOM__ = true; 18 | // global.Image = dom.window.Image; 19 | // global.HTMLCanvasElement = dom.window.HTMLCanvasElement; 20 | // global.HTMLVideoElement = dom.window.HTMLVideoElement; 21 | 22 | // const moduleAlias = require('module-alias'); 23 | 24 | // moduleAlias.addAlias('@jupyter-widgets/base', (fromPath, request, alias) => { 25 | // return `${__dirname}/modules/jupyter-widget/mock-widget-base.js`; 26 | // }); 27 | 28 | // const {gl} = require('@deck.gl/test-utils'); 29 | // Create a dummy canvas for the headless gl context 30 | // const canvas = global.document.createElement('canvas'); 31 | // canvas.width = gl.drawingBufferWidth; 32 | // canvas.height = gl.drawingBufferHeight; 33 | // gl.canvas = canvas; 34 | 35 | require('./modules'); 36 | -------------------------------------------------------------------------------- /test/render/constants.js: -------------------------------------------------------------------------------- 1 | export const WIDTH = 800; 2 | export const HEIGHT = 450; 3 | 4 | // Different Platforms render text differently. The golden images 5 | // are currently generated on Mac OS X 6 | /* global navigator */ 7 | let OS; 8 | if (navigator.platform.startsWith('Mac')) { 9 | OS = 'Mac'; 10 | } else if (navigator.platform.startsWith('Win')) { 11 | OS = 'Windows'; 12 | } else { 13 | OS = 'Other'; 14 | } 15 | 16 | export {OS}; 17 | -------------------------------------------------------------------------------- /test/render/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 - 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | import test from 'tape'; 21 | import TEST_CASES from './test-cases'; 22 | import {WIDTH, HEIGHT} from './constants'; 23 | import {SnapshotTestRunner} from '@deck.gl/test-utils'; 24 | 25 | import './jupyter-widget'; 26 | 27 | test('Render Test', t => { 28 | // tape's default timeout is 500ms 29 | t.timeoutAfter(TEST_CASES.length * 2000 + 10000); 30 | 31 | new SnapshotTestRunner({width: WIDTH, height: HEIGHT}) 32 | .add(TEST_CASES) 33 | .run({ 34 | onTestStart: testCase => t.comment(testCase.name), 35 | onTestPass: (testCase, result) => t.pass(`match: ${result.matchPercentage}`), 36 | onTestFail: (testCase, result) => t.fail(result.error || `match: ${result.matchPercentage}`), 37 | 38 | imageDiffOptions: { 39 | threshold: 0.99, 40 | includeEmpty: false 41 | // uncomment to save screenshot to disk 42 | // , saveOnFail: true 43 | // uncomment `saveAs` to overwrite current golden images 44 | // if left commented will be saved as `[name]-fail.png.` enabling comparison 45 | // , saveAs: '[name].png' 46 | } 47 | }) 48 | .then(() => t.end()); 49 | }); 50 | -------------------------------------------------------------------------------- /test/render/jupyter-widget-custom-layer-bundle.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | /* minified bundle of a custom @deck.gl/jupyter-widget layer, for use in a library like pydeck */ 4 | !function(e,r){"object"==typeof exports&&"object"==typeof module?module.exports=r(require("deck"),require("deck")):"function"==typeof define&&define.amd?define([,],r):"object"==typeof exports?exports.CustomLayerLibrary=r(require("deck"),require("deck")):e.CustomLayerLibrary=r(e.deck,e.deck)}(window,(function(e,r){return function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}return t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:n})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(t.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)t.d(n,o,function(r){return e[r]}.bind(null,o));return n},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=2)}([function(r,t){r.exports=e},function(e,t){e.exports=r},function(e,r,t){"use strict";t.r(r);var n=t(0),o=t(1);class u extends n.CompositeLayer{renderLayers(){return new o.ScatterplotLayer(this.props)}}t.d(r,"DemoCompositeLayer",(function(){return u}))}])})); 5 | -------------------------------------------------------------------------------- /test/render/jupyter-widget-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 | 21 |
22 | 23 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/size/import-nothing.js: -------------------------------------------------------------------------------- 1 | // This currently slows down tests too much 2 | // import {} from '@unfolded.gl/earthengine-layers'; 3 | -------------------------------------------------------------------------------- /test/utils/utils.js: -------------------------------------------------------------------------------- 1 | const EPSILON = 0.0001; 2 | 3 | export function floatEquals(x, y) { 4 | return Math.abs(x - y) < EPSILON; 5 | } 6 | 7 | export function vecEquals(v1, v2) { 8 | for (let i = 0; i < v1.length; ++i) { 9 | if (!floatEquals(v1[i], v2[i])) { 10 | return false; 11 | } 12 | } 13 | 14 | return v1.length === v2.length; 15 | } 16 | 17 | export function vecNormalized(v) { 18 | for (let i = 0; i < v.length; ++i) { 19 | if (!Number.isFinite(v[i])) { 20 | return false; 21 | } 22 | } 23 | if (!floatEquals(v.len(), 1)) { 24 | return false; 25 | } 26 | 27 | return true; 28 | } 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const getWebpackConfig = require('ocular-dev-tools/config/webpack.config'); 2 | 3 | module.exports = env => { 4 | const config = getWebpackConfig(env); 5 | // Unfortunately, ocular-dev-tool swallows logs... 6 | require('fs').writeFileSync('/tmp/ocular.log', JSON.stringify(config, null, 2)); 7 | 8 | config.node = { 9 | child_process: 'empty', 10 | fs: 'empty', 11 | crypto: 'empty', 12 | net: 'empty', 13 | tls: 'empty' 14 | }; 15 | 16 | return config; 17 | }; 18 | -------------------------------------------------------------------------------- /website/.eslintignore: -------------------------------------------------------------------------------- 1 | ../modules -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | public 2 | .cache 3 | report*.json 4 | examples 5 | -------------------------------------------------------------------------------- /website/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('path').resolve; 2 | 3 | const DOC_TABLE_OF_CONTENTS = require('../docs/table-of-contents.json'); 4 | 5 | module.exports = { 6 | plugins: [ 7 | { 8 | resolve: `gatsby-theme-ocular`, 9 | options: { 10 | logLevel: 1, // Adjusts amount of debug information from ocular-gatsby 11 | 12 | // Folders 13 | DIR_NAME: __dirname, 14 | ROOT_FOLDER: `${__dirname}/../`, 15 | 16 | DOCS: DOC_TABLE_OF_CONTENTS, 17 | DOC_FOLDERS: [`${__dirname}/../docs/`, `${__dirname}/../modules/`, `${__dirname}/../py/`], 18 | SOURCE: [`${__dirname}/static`, `${__dirname}/src`], 19 | 20 | PROJECT_TYPE: 'github', 21 | 22 | PROJECT_NAME: 'earthengine-layers', 23 | PROJECT_ORG: 'UnfoldedInc', 24 | PROJECT_ORG_LOGO: 'images/unfolded.png', // TODO - earthengine logo? 25 | PROJECT_URL: 'https://github.com/UnfoldedInc/earthengine-layers', 26 | PROJECT_DESC: 'deck.gl layers for Google Earth Engine for JavaScript and Python', 27 | PATH_PREFIX: '/', 28 | 29 | GA_TRACKING: null, 30 | 31 | // For showing star counts and contributors. 32 | // Should be like btoa('YourUsername:YourKey') and should be readonly. 33 | GITHUB_KEY: null, 34 | 35 | HOME_PATH: '/', 36 | 37 | PROJECTS: [ 38 | { 39 | name: 'deck.gl', 40 | url: 'https://deck.gl' 41 | }, 42 | { 43 | name: 'pydeck', 44 | url: 'https://pydeck.gl' 45 | }, 46 | { 47 | name: 'earthengine-api', 48 | url: 'https://github.com/google/earthengine-api' 49 | } 50 | ], 51 | 52 | LINK_TO_GET_STARTED: '/docs/developer-guide/get-started', 53 | 54 | ADDITIONAL_LINKS: [{name: 'Blog', href: 'http://medium.com/vis-gl', index: 1}], 55 | 56 | INDEX_PAGE_URL: resolve(__dirname, './templates/index.jsx'), 57 | 58 | EXAMPLES: [ 59 | { 60 | title: 'EE Image', 61 | image: 'images/image-example-screenshot.jpg', 62 | componentUrl: resolve(__dirname, '../examples/image/app.js'), 63 | path: 'examples/image' 64 | }, 65 | { 66 | title: 'EE ImageCollection', 67 | image: 'images/image-animation-example-still.png', 68 | componentUrl: resolve(__dirname, '../examples/image-collection/app.js'), 69 | path: 'examples/image-collection' 70 | }, 71 | { 72 | title: 'EE FeatureCollection (points)', 73 | image: 'images/power-plants.jpg', 74 | componentUrl: resolve(__dirname, '../examples/power-plants/app.js'), 75 | path: 'examples/power-plants' 76 | }, 77 | { 78 | title: 'EE FeatureCollection (lines)', 79 | image: 'images/noaa_hurricanes.jpg', 80 | componentUrl: resolve(__dirname, '../examples/noaa-hurricanes/app.js'), 81 | path: 'examples/noaa-hurricanes' 82 | }, 83 | { 84 | title: 'EE FeatureCollection (polygons)', 85 | image: 'images/intl-boundaries.jpg', 86 | componentUrl: resolve(__dirname, '../examples/intl-boundary/app.js'), 87 | path: 'examples/intl-boundary' 88 | }, 89 | { 90 | title: 'Terrain', 91 | image: 'images/terrain.jpg', 92 | componentUrl: resolve(__dirname, '../examples/terrain/app.js'), 93 | path: 'examples/terrain' 94 | } 95 | ], 96 | 97 | STYLESHEETS: ['https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.0/mapbox-gl.css'] 98 | } 99 | }, 100 | {resolve: 'gatsby-plugin-no-sourcemaps'}, 101 | { 102 | resolve: 'gatsby-plugin-env-variables', 103 | options: { 104 | whitelist: ['GoogleMapsAPIKey', 'EE_CLIENT_ID'] 105 | } 106 | } 107 | ] 108 | }; 109 | -------------------------------------------------------------------------------- /website/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const resolve = require('path').resolve; 2 | const getOcularConfig = require('ocular-dev-tools/config/ocular.config'); 3 | const DEPENDENCIES = require('./package.json').dependencies; 4 | 5 | module.exports.onCreateWebpackConfig = function onCreateWebpackConfigOverride(opts) { 6 | const { 7 | stage, // build stage: ‘develop’, ‘develop-html’, ‘build-javascript’, or ‘build-html’ 8 | // rules, // Object (map): set of preconfigured webpack config rules 9 | // plugins, // Object (map): A set of preconfigured webpack config plugins 10 | getConfig, // Function that returns the current webpack config 11 | // loaders, // Object (map): set of preconfigured webpack config loaders 12 | actions 13 | } = opts; 14 | 15 | console.log(`App rewriting gatsby webpack config ${stage}`); // eslint-disable-line 16 | 17 | const config = getConfig(); 18 | config.resolve = config.resolve || {}; 19 | config.resolve.alias = config.resolve.alias || {}; 20 | 21 | const ALIASES = getOcularConfig({root: resolve(__dirname, '..')}).aliases; 22 | 23 | // When duplicating example dependencies in website, autogenerate 24 | // aliases to ensure the website version is picked up 25 | // NOTE: module dependencies are automatically injected 26 | // TODO - should this be automatically done by ocular-gatsby? 27 | const dependencyAliases = {}; 28 | for (const dependency in DEPENDENCIES) { 29 | dependencyAliases[dependency] = `${__dirname}/node_modules/${dependency}`; 30 | } 31 | 32 | Object.assign(config.resolve.alias, ALIASES, dependencyAliases); 33 | 34 | // Completely replace the webpack config for the current stage. 35 | // This can be dangerous and break Gatsby if certain configuration options are changed. 36 | // Generally only useful for cases where you need to handle config merging logic yourself, 37 | // in which case consider using webpack-merge. 38 | actions.replaceWebpackConfig(config); 39 | }; 40 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "earthenginelayers-website", 3 | "version": "0.0.0", 4 | "description": "Website for earthenginelayers module", 5 | "license": "MIT", 6 | "keywords": [ 7 | "ocular" 8 | ], 9 | "main": "gatsby-config.js", 10 | "scripts": { 11 | "check-env-var": "if [[ -z $GoogleMapsAPIKey ]]; then echo 'Missing GoogleMapsAPIKey environment variable' && exit 1; fi; if [[ -z $EE_CLIENT_ID ]]; then echo 'Missing EE_CLIENT_ID environment variable' && exit 1; fi;", 12 | "start": "yarn clean && yarn develop", 13 | "build": "yarn check-env-var && yarn clean-examples && yarn clean && gatsby build --no-uglify", 14 | "clean": "rm -rf ./.cache ./public", 15 | "clean-examples": "find ../examples -name node_modules -exec rm -rf {} \\; || true", 16 | "develop": "yarn clean-examples && gatsby develop --port 8080", 17 | "serve": "gatsby serve --port 8080", 18 | "deploy": "NODE_DEBUG=gh-pages gh-pages -d public" 19 | }, 20 | "dependencies": { 21 | "@deck.gl/core": "^8.2.5", 22 | "@deck.gl/geo-layers": "^8.2.5", 23 | "@deck.gl/layers": "^8.2.5", 24 | "@deck.gl/mesh-layers": "^8.2.5", 25 | "@deck.gl/react": "^8.2.5", 26 | "@loaders.gl/core": "^2.3.0-alpha.10", 27 | "@luma.gl/constants": "^8.2.0", 28 | "@luma.gl/core": "^8.2.0", 29 | "@luma.gl/experimental": "^8.2.0", 30 | "@luma.gl/webgl": "^8.2.0", 31 | "@math.gl/culling": "^3.1.3", 32 | "@math.gl/geospatial": "^3.1.3", 33 | "@probe.gl/bench": "^3.3.0-alpha.8", 34 | "@probe.gl/react-bench": "^3.3.0-alpha.8", 35 | "@probe.gl/stats-widget": "^3.3.0-alpha.8", 36 | "babel-plugin-version-inline": "^1.0.0", 37 | "gatsby-plugin-env-variables": "^1.0.1", 38 | "marked": "^0.7.0", 39 | "math.gl": "^3.1.3", 40 | "react": "^16.12.0", 41 | "react-dom": "^16.12.0", 42 | "styled-components": "^4.4.1" 43 | }, 44 | "devDependencies": { 45 | "gatsby": "^2.21.16", 46 | "gatsby-plugin-no-sourcemaps": "^2.1.2", 47 | "gatsby-theme-ocular": "1.2.0-beta.8", 48 | "gh-pages": "^2.2.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /website/src/components/README.md: -------------------------------------------------------------------------------- 1 | Add any custom React components here. -------------------------------------------------------------------------------- /website/static/CNAME: -------------------------------------------------------------------------------- 1 | earthengine-layers.com -------------------------------------------------------------------------------- /website/static/images/icon-high-precision.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | high-precision 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /website/static/images/image-animation-example-screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/619e9da47e85f24d8f8fe231913040787a1301e3/website/static/images/image-animation-example-screenshot.gif -------------------------------------------------------------------------------- /website/static/images/image-animation-example-still.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/619e9da47e85f24d8f8fe231913040787a1301e3/website/static/images/image-animation-example-still.png -------------------------------------------------------------------------------- /website/static/images/image-animation-wide_less-bright.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/619e9da47e85f24d8f8fe231913040787a1301e3/website/static/images/image-animation-wide_less-bright.gif -------------------------------------------------------------------------------- /website/static/images/image-example-screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/619e9da47e85f24d8f8fe231913040787a1301e3/website/static/images/image-example-screenshot.jpg -------------------------------------------------------------------------------- /website/static/images/intl-boundaries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/619e9da47e85f24d8f8fe231913040787a1301e3/website/static/images/intl-boundaries.jpg -------------------------------------------------------------------------------- /website/static/images/noaa_hurricanes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/619e9da47e85f24d8f8fe231913040787a1301e3/website/static/images/noaa_hurricanes.jpg -------------------------------------------------------------------------------- /website/static/images/power-plants.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/619e9da47e85f24d8f8fe231913040787a1301e3/website/static/images/power-plants.jpg -------------------------------------------------------------------------------- /website/static/images/terrain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/619e9da47e85f24d8f8fe231913040787a1301e3/website/static/images/terrain.jpg -------------------------------------------------------------------------------- /website/static/images/unfolded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnfoldedInc/earthengine-layers/619e9da47e85f24d8f8fe231913040787a1301e3/website/static/images/unfolded.png -------------------------------------------------------------------------------- /website/templates/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Home} from 'gatsby-theme-ocular/components'; 3 | // import GLTFExample from './example-gltf'; TODO Add EEDemo 4 | import styled from 'styled-components'; 5 | 6 | if (typeof window !== 'undefined') { 7 | window.website = true; 8 | } 9 | 10 | const Bullet = styled.li` 11 | background: url(images/icon-high-precision.svg) no-repeat left top; 12 | list-style: none; 13 | max-width: 540px; 14 | padding: 8px 0 12px 42px; 15 | font: ${(props) => props.theme.typography.font300}; 16 | `; 17 | 18 | const HeroExample = () => ( 19 |
27 | ); 28 | 29 | export default class IndexPage extends React.Component { 30 | render() { 31 | return ( 32 | 33 |
    34 | deck.gl layers for the Google Earth Engine API. 35 | Effortlessly visualize planetary scale satellite data. 36 | Python and Jupyter Notebook supported via pydeck. 37 |
38 |
39 | ); 40 | } 41 | } 42 | --------------------------------------------------------------------------------