├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── config_vars.py ├── notebooks ├── bqplot.ipynb ├── geemap.ipynb ├── ipyleaflet.ipynb ├── leafmap.ipynb └── water_app.ipynb └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Qiusheng Wu 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: voila --port=$PORT --Voila.ip=0.0.0.0 --no-browser --strip_sources=True --enable_nbextensions=True --MappingKernelManager.cull_interval=60 --MappingKernelManager.cull_idle_timeout=120 --NotebookClient.iopub_timeout=30 notebooks/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # geemap-heroku 2 | 3 | Python scripts for deploying Earth Engine Apps to heroku, try it out: 4 | 5 | ## How to deploy your own Earth Engine Apps? 6 | 7 | - [Sign up](https://signup.heroku.com/) for a free heroku account. 8 | - Follow the [instructions](https://devcenter.heroku.com/articles/getting-started-with-python#set-up) to install [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) and Heroku Command Line Interface (CLI). 9 | - Authenticate heroku using the `heroku login` command. 10 | - Clone this repository: 11 | - Create your own Earth Engine notebook and put it under the `notebooks` directory. 12 | - Add Python dependencies in the `requirements.txt` file if needed. 13 | - Edit the `Procfile` file by replacing `notebooks/geemap.ipynb` with the path to your own notebook. 14 | - Commit changes to the repository by using `git add . && git commit -am "message"`. 15 | - Create a heroku app: `heroku create` 16 | - Run the `config_vars.py` script to extract Earth Engine token from your computer and set it as an environment variable on heroku: `python config_vars.py` 17 | - Deploy your code to heroku: `git push heroku master` 18 | - Open your heroku app: `heroku open` 19 | 20 | ## Optional steps 21 | 22 | - To specify a name for your app, use `heroku apps:create example` 23 | - To preview your app locally, use `heroku local web` 24 | - To hide code cells from your app, you can edit the `Procfile` file and set `--strip_sources=True` 25 | - To periodically check for idle kernels, you can edit the `Procfile` file and set `--MappingKernelManager.cull_interval=60 --MappingKernelManager.cull_idle_timeout=120` 26 | - To view information about your running app, use `heroku logs --tail` 27 | - To set an environment variable on heroku, use `heroku config:set NAME=VALUE` 28 | - To view environment variables for your app, use `heroku config` 29 | 30 | ## Credits 31 | 32 | The instructions above on how to deploy a voila application on heroku are adapted from [voila-dashboards/voila-heroku](https://github.com/voila-dashboards/voila-heroku). 33 | -------------------------------------------------------------------------------- /config_vars.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import json 4 | from subprocess import DEVNULL, STDOUT, check_call 5 | 6 | def set_heroku_vars(token_name='EARTHENGINE_TOKEN'): 7 | """Extracts Earth Engine token from the local computer and sets it as an environment variable on heroku. 8 | 9 | Args: 10 | token_name (str, optional): Name of the Earth Engine token. Defaults to 'EARTHENGINE_TOKEN'. 11 | """ 12 | try: 13 | 14 | ee_token_dir = os.path.expanduser("~/.config/earthengine/") 15 | ee_token_file = os.path.join(ee_token_dir, 'credentials') 16 | 17 | if not os.path.exists(ee_token_file): 18 | print('The credentials file does not exist.') 19 | else: 20 | 21 | with open(ee_token_file) as f: 22 | content = f.read() 23 | content_json = json.loads(content) 24 | token = content_json["refresh_token"] 25 | secret = '{}={}'.format(token_name, token) 26 | if platform.system() == 'Windows': 27 | check_call(['heroku', 'config:set', secret], stdout=DEVNULL, stderr=STDOUT, shell=True) 28 | else: 29 | check_call(['heroku', 'config:set', secret], stdout=DEVNULL, stderr=STDOUT) 30 | 31 | except Exception as e: 32 | print(e) 33 | return 34 | 35 | if __name__ == '__main__': 36 | 37 | set_heroku_vars(token_name='EARTHENGINE_TOKEN') -------------------------------------------------------------------------------- /notebooks/bqplot.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from bqplot import pyplot as plt\n", 10 | "import ipywidgets as widgets\n", 11 | "import numpy as np" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "# generate some fake \n", 21 | "n = 2000\n", 22 | "x = np.linspace(0.0, 10.0, n)\n", 23 | "np.random.seed(0)\n", 24 | "y = np.cumsum(np.random.randn(n)*10).astype(int)" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "fig_hist = plt.figure( title='Histogram')\n", 34 | "hist = plt.hist(y, bins=25)" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "hist.bins = 10;" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "slider = widgets.IntSlider(description='Bins number', min=1, max=100, v_model=30)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": { 59 | "scrolled": false 60 | }, 61 | "outputs": [], 62 | "source": [ 63 | "widgets.link((hist, 'bins'), (slider, 'value'))\n", 64 | "\n", 65 | "fig_lines = plt.figure( title='Line Chart')\n", 66 | "lines = plt.plot(x, y)\n", 67 | "\n", 68 | "fig_lines.layout.width = 'auto'\n", 69 | "fig_lines.layout.height = 'auto'\n", 70 | "fig_hist.layout.width = 'auto'\n", 71 | "fig_hist.layout.height = 'auto'\n", 72 | "\n", 73 | "grid_layout = widgets.GridspecLayout(5, 3)\n", 74 | "\n", 75 | "grid_layout[:2, :] = fig_lines\n", 76 | "grid_layout[2:4, :] = fig_hist\n", 77 | "grid_layout[4, 1] = slider\n", 78 | "\n", 79 | "grid_layout.layout.height = '1000px'\n", 80 | "\n", 81 | "grid_layout" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "selector = plt.brush_int_selector()\n", 91 | "def update_range(*ignore):\n", 92 | " if selector.selected is not None and len(selector.selected) == 2:\n", 93 | " xmin, xmax = selector.selected\n", 94 | " mask = (x > xmin) & (x < xmax)\n", 95 | " hist.sample = y[mask]\n", 96 | "selector.observe(update_range, 'selected') " 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [] 105 | } 106 | ], 107 | "metadata": { 108 | "kernelspec": { 109 | "display_name": "Python 3", 110 | "language": "python", 111 | "name": "python3" 112 | }, 113 | "language_info": { 114 | "codemirror_mode": { 115 | "name": "ipython", 116 | "version": 3 117 | }, 118 | "file_extension": ".py", 119 | "mimetype": "text/x-python", 120 | "name": "python", 121 | "nbconvert_exporter": "python", 122 | "pygments_lexer": "ipython3", 123 | "version": "3.7.3" 124 | }, 125 | "widgets": { 126 | "application/vnd.jupyter.widget-state+json": { 127 | "state": {}, 128 | "version_major": 2, 129 | "version_minor": 0 130 | } 131 | } 132 | }, 133 | "nbformat": 4, 134 | "nbformat_minor": 4 135 | } 136 | -------------------------------------------------------------------------------- /notebooks/geemap.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import ee\n", 10 | "import geemap" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "## Earth Engine App" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "Map = geemap.Map(center=(40, -100), zoom=4)" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "# Add Earth Engine dataset\n", 36 | "dem = ee.Image('USGS/SRTMGL1_003')\n", 37 | "landcover = ee.Image(\"ESA/GLOBCOVER_L4_200901_200912_V2_3\").select('landcover')\n", 38 | "landsat7 = ee.Image('LE7_TOA_5YEAR/1999_2003')\n", 39 | "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", 40 | "\n", 41 | "# Set visualization parameters.\n", 42 | "vis_params = {\n", 43 | " 'min': 0,\n", 44 | " 'max': 4000,\n", 45 | " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5']}\n", 46 | "\n", 47 | "# Add Earth Eninge layers to Map\n", 48 | "Map.addLayer(landsat7, {'bands': ['B4', 'B3', 'B2'], 'min': 20, 'max': 200}, 'Landsat 7')\n", 49 | "Map.addLayer(landcover, {}, 'Land cover')\n", 50 | "Map.addLayer(dem, vis_params, 'STRM DEM', True, 1)\n", 51 | "Map.addLayer(states, {}, \"US States\")\n", 52 | "\n", 53 | "Map" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "print('Change layer opacity:')\n", 63 | "dem_layer = Map.layers[-2]\n", 64 | "dem_layer.interact(opacity=(0, 1, 0.1))" 65 | ] 66 | } 67 | ], 68 | "metadata": { 69 | "kernelspec": { 70 | "display_name": "Python 3", 71 | "language": "python", 72 | "name": "python3" 73 | }, 74 | "language_info": { 75 | "codemirror_mode": { 76 | "name": "ipython", 77 | "version": 3 78 | }, 79 | "file_extension": ".py", 80 | "mimetype": "text/x-python", 81 | "name": "python", 82 | "nbconvert_exporter": "python", 83 | "pygments_lexer": "ipython3", 84 | "version": "3.9.10" 85 | } 86 | }, 87 | "nbformat": 4, 88 | "nbformat_minor": 4 89 | } 90 | -------------------------------------------------------------------------------- /notebooks/ipyleaflet.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "bcc1396c-3770-40f0-8cc5-22ed12469db0", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import ipyleaflet" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "bfdc2d9d-de54-41ae-a8b2-818571b2b7cf", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "m = ipyleaflet.Map(center=[40, -100], zoom=4)\n", 21 | "m" 22 | ] 23 | } 24 | ], 25 | "metadata": { 26 | "kernelspec": { 27 | "display_name": "Python 3", 28 | "language": "python", 29 | "name": "python3" 30 | }, 31 | "language_info": { 32 | "codemirror_mode": { 33 | "name": "ipython", 34 | "version": 3 35 | }, 36 | "file_extension": ".py", 37 | "mimetype": "text/x-python", 38 | "name": "python", 39 | "nbconvert_exporter": "python", 40 | "pygments_lexer": "ipython3", 41 | "version": "3.9.10" 42 | } 43 | }, 44 | "nbformat": 4, 45 | "nbformat_minor": 5 46 | } 47 | -------------------------------------------------------------------------------- /notebooks/leafmap.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 6, 6 | "id": "bc362c3b-4854-4da0-bdac-4866de84da3f", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import leafmap" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 7, 16 | "id": "b63a4fa5-0089-4128-a72e-27df26c09c42", 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "data": { 21 | "application/vnd.jupyter.widget-view+json": { 22 | "model_id": "9b677c790a784537b6fe4d35d84b3f91", 23 | "version_major": 2, 24 | "version_minor": 0 25 | }, 26 | "text/plain": [ 27 | "Map(center=[20, 0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_text…" 28 | ] 29 | }, 30 | "metadata": {}, 31 | "output_type": "display_data" 32 | } 33 | ], 34 | "source": [ 35 | "m = leafmap.Map()\n", 36 | "m.add_basemap('HYBRID')\n", 37 | "m" 38 | ] 39 | } 40 | ], 41 | "metadata": { 42 | "kernelspec": { 43 | "display_name": "Python 3", 44 | "language": "python", 45 | "name": "python3" 46 | }, 47 | "language_info": { 48 | "codemirror_mode": { 49 | "name": "ipython", 50 | "version": 3 51 | }, 52 | "file_extension": ".py", 53 | "mimetype": "text/x-python", 54 | "name": "python", 55 | "nbconvert_exporter": "python", 56 | "pygments_lexer": "ipython3", 57 | "version": "3.9.10" 58 | } 59 | }, 60 | "nbformat": 4, 61 | "nbformat_minor": 5 62 | } 63 | -------------------------------------------------------------------------------- /notebooks/water_app.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Using Earth Engine and geemap for mapping surface water dynamics\n", 8 | "\n", 9 | "**Steps to create Landsat timeseries:**\n", 10 | "\n", 11 | "1. Pan and zoom to your area of interest, and click on the map to select a polygon.\n", 12 | "2. Adjust the parameters (e.g., band combination, threshold, color) if needed.\n", 13 | "3. Click the `Submit` button to create timeseries of Landsat imagery and normalized difference indices.\n", 14 | "\n", 15 | "**Web Apps:** https://gishub.org/water-app, https://gishub.org/water-ngrok\n", 16 | "\n", 17 | "**Contact:** Dr. Qiusheng Wu ([Website](https://wetlands.io/), [LinkedIn](https://www.linkedin.com/in/qiushengwu), [Twitter](https://twitter.com/giswqs), [YouTube](https://www.youtube.com/c/QiushengWu))" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# Check geemap installation\n", 27 | "import subprocess\n", 28 | "\n", 29 | "try:\n", 30 | " import geemap\n", 31 | "except ImportError:\n", 32 | " print('geemap package is not installed. Installing ...')\n", 33 | " subprocess.check_call([\"python\", '-m', 'pip', 'install', 'geemap'])" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "# Import libraries\n", 43 | "import os\n", 44 | "import ee\n", 45 | "import geemap\n", 46 | "import ipywidgets as widgets\n", 47 | "from bqplot import pyplot as plt\n", 48 | "from ipyleaflet import WidgetControl" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": { 55 | "scrolled": false 56 | }, 57 | "outputs": [], 58 | "source": [ 59 | "# Create an interactive map\n", 60 | "Map = geemap.Map(center=[37.71, 105.47], zoom=4, add_google_map=False)\n", 61 | "Map.add_basemap('HYBRID')\n", 62 | "Map.add_basemap('ROADMAP')\n", 63 | "\n", 64 | "# Add Earth Engine data\n", 65 | "fc = ee.FeatureCollection('users/giswqs/public/chn_admin_level2')\n", 66 | "Map.addLayer(fc, {}, '二级行政区')\n", 67 | "Map" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "# Designe interactive widgets\n", 77 | "\n", 78 | "style = {'description_width': 'initial'}\n", 79 | "\n", 80 | "output_widget = widgets.Output(layout={'border': '1px solid black'})\n", 81 | "output_control = WidgetControl(widget=output_widget, position='bottomright')\n", 82 | "Map.add_control(output_control)\n", 83 | "\n", 84 | "admin1_widget = widgets.Text(\n", 85 | " description='一级行政区:',\n", 86 | " value='广东省',\n", 87 | " width=200,\n", 88 | " style=style\n", 89 | ")\n", 90 | "\n", 91 | "admin2_widget = widgets.Text(\n", 92 | " description='二级行政区:',\n", 93 | " value='广州市',\n", 94 | " width=300,\n", 95 | " style=style\n", 96 | ")\n", 97 | "\n", 98 | "band_combo = widgets.Dropdown(\n", 99 | " description='显示影像波段组合:',\n", 100 | " options=['Red/Green/Blue', 'NIR/Red/Green', 'SWIR2/SWIR1/NIR', 'NIR/SWIR1/Red','SWIR2/NIR/Red', \n", 101 | " 'SWIR2/SWIR1/Red', 'SWIR1/NIR/Blue', 'NIR/SWIR1/Blue', 'SWIR2/NIR/Green', 'SWIR1/NIR/Red'],\n", 102 | " value='NIR/Red/Green',\n", 103 | " style=style\n", 104 | ")\n", 105 | "\n", 106 | "year_widget = widgets.IntSlider(min=1984, max=2020, value=2010, description='显示指定年份数据:', width=400, style=style)\n", 107 | "\n", 108 | "fmask_widget = widgets.Checkbox(\n", 109 | " value=True,\n", 110 | " description='应用fmask(去除云, 阴影, 雪)',\n", 111 | " style=style\n", 112 | ")\n", 113 | "\n", 114 | "\n", 115 | "# Normalized Satellite Indices: https://www.usna.edu/Users/oceano/pguth/md_help/html/norm_sat.htm\n", 116 | "\n", 117 | "nd_options = ['Vegetation Index (NDVI)', \n", 118 | " 'Water Index (NDWI)',\n", 119 | " 'Modified Water Index (MNDWI)',\n", 120 | " 'Snow Index (NDSI)',\n", 121 | " 'Soil Index (NDSI)',\n", 122 | " 'Burn Ratio (NBR)',\n", 123 | " 'Customized']\n", 124 | "nd_indices = widgets.Dropdown(options=nd_options, value='Modified Water Index (MNDWI)', description='归一化指数:', style=style)\n", 125 | "\n", 126 | "first_band = widgets.Dropdown(\n", 127 | " description='波段1:',\n", 128 | " options=['Blue', 'Green','Red','NIR', 'SWIR1', 'SWIR2'],\n", 129 | " value='Green',\n", 130 | " style=style\n", 131 | ")\n", 132 | "\n", 133 | "second_band = widgets.Dropdown(\n", 134 | " description='波段2:',\n", 135 | " options=['Blue', 'Green','Red','NIR', 'SWIR1', 'SWIR2'],\n", 136 | " value='SWIR1',\n", 137 | " style=style\n", 138 | ")\n", 139 | "\n", 140 | "nd_threshold = widgets.FloatSlider(\n", 141 | " value=0,\n", 142 | " min=-1,\n", 143 | " max=1,\n", 144 | " step=0.01,\n", 145 | " description='阈值:',\n", 146 | " orientation='horizontal',\n", 147 | " style=style\n", 148 | ")\n", 149 | "\n", 150 | "nd_color = widgets.ColorPicker(\n", 151 | " concise=False,\n", 152 | " description='颜色:',\n", 153 | " value='blue',\n", 154 | " style=style\n", 155 | ")\n", 156 | "\n", 157 | "def nd_index_change(change):\n", 158 | " if nd_indices.value == 'Vegetation Index (NDVI)':\n", 159 | " first_band.value = 'NIR'\n", 160 | " second_band.value = 'Red'\n", 161 | " elif nd_indices.value == 'Water Index (NDWI)':\n", 162 | " first_band.value = 'NIR'\n", 163 | " second_band.value = 'SWIR1' \n", 164 | " elif nd_indices.value == 'Modified Water Index (MNDWI)':\n", 165 | " first_band.value = 'Green'\n", 166 | " second_band.value = 'SWIR1' \n", 167 | " elif nd_indices.value == 'Snow Index (NDSI)':\n", 168 | " first_band.value = 'Green'\n", 169 | " second_band.value = 'SWIR1'\n", 170 | " elif nd_indices.value == 'Soil Index (NDSI)':\n", 171 | " first_band.value = 'SWIR1'\n", 172 | " second_band.value = 'NIR' \n", 173 | " elif nd_indices.value == 'Burn Ratio (NBR)':\n", 174 | " first_band.value = 'NIR'\n", 175 | " second_band.value = 'SWIR2'\n", 176 | " elif nd_indices.value == 'Customized':\n", 177 | " first_band.value = None\n", 178 | " second_band.value = None\n", 179 | " \n", 180 | "nd_indices.observe(nd_index_change, names='value')\n", 181 | "\n", 182 | "submit = widgets.Button(\n", 183 | " description='Submit',\n", 184 | " button_style='primary',\n", 185 | " tooltip='Click me',\n", 186 | " style=style\n", 187 | ")\n", 188 | "\n", 189 | "full_widget = widgets.VBox([\n", 190 | " widgets.HBox([admin1_widget, admin2_widget]),\n", 191 | " widgets.HBox([band_combo, year_widget, fmask_widget]),\n", 192 | " widgets.HBox([nd_indices, first_band, second_band, nd_threshold, nd_color]),\n", 193 | " submit\n", 194 | "])\n", 195 | "\n", 196 | "full_widget" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "# Capture user interaction with the map\n", 206 | "\n", 207 | "def handle_interaction(**kwargs):\n", 208 | " latlon = kwargs.get('coordinates')\n", 209 | " if kwargs.get('type') == 'click':\n", 210 | " Map.default_style = {'cursor': 'wait'}\n", 211 | " xy = ee.Geometry.Point(latlon[::-1])\n", 212 | " selected_fc = fc.filterBounds(xy)\n", 213 | " \n", 214 | " with output_widget:\n", 215 | " output_widget.clear_output()\n", 216 | " \n", 217 | " try:\n", 218 | " admin1_id = selected_fc.first().get('ADM1_ZH').getInfo()\n", 219 | " admin2_id = selected_fc.first().get('ADM2_ZH').getInfo()\n", 220 | " admin1_widget.value = admin1_id\n", 221 | " admin2_widget.value = admin2_id\n", 222 | " Map.layers = Map.layers[:4] \n", 223 | " geom = selected_fc.geometry()\n", 224 | " layer_name = admin1_id + '-' + admin2_id\n", 225 | " Map.addLayer(ee.Image().paint(geom, 0, 2), {'palette': 'red'}, layer_name) \n", 226 | " print(layer_name)\n", 227 | " except:\n", 228 | " print('找不到相关行政区')\n", 229 | " Map.layers = Map.layers[:4]\n", 230 | " \n", 231 | " Map.default_style = {'cursor': 'pointer'}\n", 232 | "\n", 233 | "Map.on_interaction(handle_interaction)" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": null, 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "# Click event handler\n", 243 | "\n", 244 | "def submit_clicked(b):\n", 245 | " \n", 246 | " with output_widget:\n", 247 | " output_widget.clear_output()\n", 248 | " print('Computing...')\n", 249 | " Map.default_style = {'cursor': 'wait'}\n", 250 | "\n", 251 | " try:\n", 252 | " admin1_id = admin1_widget.value\n", 253 | " admin2_id = admin2_widget.value\n", 254 | " band1 = first_band.value\n", 255 | " band2 = second_band.value\n", 256 | " selected_year = year_widget.value\n", 257 | " threshold = nd_threshold.value\n", 258 | " bands = band_combo.value.split('/')\n", 259 | " apply_fmask = fmask_widget.value\n", 260 | " palette = nd_color.value\n", 261 | " \n", 262 | " roi = fc.filter(ee.Filter.And(ee.Filter.eq('ADM1_ZH', admin1_id), ee.Filter.eq('ADM2_ZH', admin2_id)))\n", 263 | " Map.layers = Map.layers[:4] \n", 264 | " geom = roi.geometry()\n", 265 | " layer_name = admin1_id + '-' + admin2_id\n", 266 | " Map.addLayer(ee.Image().paint(geom, 0, 2), {'palette': 'red'}, layer_name) \n", 267 | " \n", 268 | " images = geemap.landsat_timeseries(roi=roi, start_year=1984, end_year=2020, start_date='01-01', end_date='12-31', apply_fmask=apply_fmask)\n", 269 | " nd_images = images.map(lambda img: img.normalizedDifference([band1, band2]))\n", 270 | " result_images = nd_images.map(lambda img: img.gt(threshold))\n", 271 | "\n", 272 | " selected_image = ee.Image(images.toList(images.size()).get(selected_year - 1984))\n", 273 | " selected_result_image = ee.Image(result_images.toList(result_images.size()).get(selected_year - 1984)).selfMask()\n", 274 | " \n", 275 | " vis_params = {\n", 276 | " 'bands': bands,\n", 277 | " 'min': 0,\n", 278 | " 'max': 3000\n", 279 | " }\n", 280 | " \n", 281 | " Map.addLayer(selected_image, vis_params, 'Landsat ' + str(selected_year))\n", 282 | " Map.addLayer(selected_result_image, {'palette': palette}, 'Result ' + str(selected_year))\n", 283 | "\n", 284 | " \n", 285 | " def cal_area(img):\n", 286 | " pixel_area = img.multiply(ee.Image.pixelArea()).divide(1e6)\n", 287 | " img_area = pixel_area.reduceRegion(**{\n", 288 | " 'geometry': roi.geometry(),\n", 289 | " 'reducer': ee.Reducer.sum(),\n", 290 | " 'scale': 1000,\n", 291 | " 'maxPixels': 1e12,\n", 292 | " 'bestEffort': True\n", 293 | " })\n", 294 | " return img.set({'area': img_area})\n", 295 | " \n", 296 | " areas = result_images.map(cal_area)\n", 297 | " stats = areas.aggregate_array('area').getInfo()\n", 298 | " x = list(range(1984, 2021))\n", 299 | " y = [item.get('nd') for item in stats]\n", 300 | " \n", 301 | " fig = plt.figure(1)\n", 302 | " fig.layout.height = '270px'\n", 303 | " plt.clear()\n", 304 | " plt.plot(x, y)\n", 305 | " plt.title('Temporal trend (1984-2020)')\n", 306 | " plt.xlabel('Year')\n", 307 | " plt.ylabel('Area (km2)')\n", 308 | " \n", 309 | " output_widget.clear_output() \n", 310 | "\n", 311 | " plt.show()\n", 312 | " \n", 313 | " except Exception as e:\n", 314 | " print(e)\n", 315 | " print('An error occurred during computation.')\n", 316 | " \n", 317 | "\n", 318 | " Map.default_style = {'cursor': 'default'}\n", 319 | "\n", 320 | "submit.on_click(submit_clicked)" 321 | ] 322 | } 323 | ], 324 | "metadata": { 325 | "gist": { 326 | "data": { 327 | "description": "notebooks/wetland_mapping.ipynb", 328 | "public": true 329 | }, 330 | "id": "" 331 | }, 332 | "hide_input": true, 333 | "kernelspec": { 334 | "display_name": "Python 3", 335 | "language": "python", 336 | "name": "python3" 337 | }, 338 | "language_info": { 339 | "codemirror_mode": { 340 | "name": "ipython", 341 | "version": 3 342 | }, 343 | "file_extension": ".py", 344 | "mimetype": "text/x-python", 345 | "name": "python", 346 | "nbconvert_exporter": "python", 347 | "pygments_lexer": "ipython3", 348 | "version": "3.8.2" 349 | }, 350 | "toc": { 351 | "base_numbering": 1, 352 | "nav_menu": {}, 353 | "number_sections": true, 354 | "sideBar": true, 355 | "skip_h1_title": true, 356 | "title_cell": "Table of Contents", 357 | "title_sidebar": "Table of Contents", 358 | "toc_cell": false, 359 | "toc_position": {}, 360 | "toc_section_display": true, 361 | "toc_window_display": false 362 | }, 363 | "varInspector": { 364 | "cols": { 365 | "lenName": 16, 366 | "lenType": 16, 367 | "lenVar": 40 368 | }, 369 | "kernels_config": { 370 | "python": { 371 | "delete_cmd_postfix": "", 372 | "delete_cmd_prefix": "del ", 373 | "library": "var_list.py", 374 | "varRefreshCmd": "print(var_dic_list())" 375 | }, 376 | "r": { 377 | "delete_cmd_postfix": ") ", 378 | "delete_cmd_prefix": "rm(", 379 | "library": "var_list.r", 380 | "varRefreshCmd": "cat(var_dic_list()) " 381 | } 382 | }, 383 | "types_to_exclude": [ 384 | "module", 385 | "function", 386 | "builtin_function_or_method", 387 | "instance", 388 | "_Feature" 389 | ], 390 | "window_display": false 391 | } 392 | }, 393 | "nbformat": 4, 394 | "nbformat_minor": 4 395 | } 396 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | voila 2 | geemap 3 | leafmap 4 | Jinja2==3.0.3 5 | --------------------------------------------------------------------------------