├── tests ├── data │ └── warning_list.txt ├── conftest.py ├── __init__.py ├── test_geeservermap.py └── check_warnings.py ├── app.py ├── geeservermap ├── calls.py ├── py.typed ├── elements │ ├── __init__.py │ └── layers.py ├── __init__.py ├── exceptions.py ├── map.py ├── main.py ├── templates │ ├── layout.html │ └── map.html ├── async_jobs.py └── helpers.py ├── docs ├── usage.rst ├── contribute.rst ├── _static │ └── custom.css ├── _template │ └── pypackage-credit.html ├── index.rst └── conf.py ├── .devcontainer └── devcontainer.json ├── .readthedocs.yaml ├── .copier-answers.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── PULL_REQUEST_TEMPLATE │ │ └── pr_template.md │ └── feature_request.md └── workflows │ ├── release.yaml │ ├── pypackage_check.yaml │ └── unit.yaml ├── AUTHORS.rst ├── LICENSE ├── .pre-commit-config.yaml ├── noxfile.py ├── .gitignore ├── README.rst ├── pyproject.toml ├── CONTRIBUTING.rst └── CODE_OF_CONDUCT.rst /tests/data/warning_list.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | """TODO Missing docstring.""" 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Pytest session configuration.""" 2 | -------------------------------------------------------------------------------- /geeservermap/calls.py: -------------------------------------------------------------------------------- 1 | """Custom calls for geeservermap.""" 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """make test folder a package for coverage.""" 2 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | **geeservermap** usage documentation. 5 | -------------------------------------------------------------------------------- /geeservermap/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. The mypy package uses inline types. -------------------------------------------------------------------------------- /docs/contribute.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | .. include:: ../CONTRIBUTING.rst 5 | :start-line: 3 6 | -------------------------------------------------------------------------------- /geeservermap/elements/__init__.py: -------------------------------------------------------------------------------- 1 | """GEE Server Map elements package.""" 2 | 3 | from . import layers as layers 4 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* add dollar sign in console code-block */ 2 | div.highlight-console pre span.go::before { 3 | content: "$"; 4 | margin-right: 10px; 5 | margin-left: 5px; 6 | } 7 | -------------------------------------------------------------------------------- /docs/_template/pypackage-credit.html: -------------------------------------------------------------------------------- 1 |

2 | From 3 | @12rambau/pypackage 4 | 0.1.5 Copier project. 5 |

6 | -------------------------------------------------------------------------------- /tests/test_geeservermap.py: -------------------------------------------------------------------------------- 1 | """Test the geeservermap package.""" 2 | 3 | import geeservermap 4 | 5 | 6 | def test_author(): 7 | """Default test to check author exist.""" 8 | assert geeservermap.__author__ == "LDC Research Repository" 9 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python 3", 3 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", 4 | "features": { 5 | "ghcr.io/devcontainers-contrib/features/nox:2": {}, 6 | "ghcr.io/devcontainers-contrib/features/pre-commit:2": {} 7 | }, 8 | "postCreateCommand": "pre-commit install" 9 | } 10 | -------------------------------------------------------------------------------- /geeservermap/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """GEE Server Map package.""" 3 | 4 | from . import helpers as helpers 5 | from .elements import layers as layers 6 | from .main import MESSAGES as MESSAGES 7 | from .map import Map as Map 8 | 9 | __version__ = "0.0.0rc2" 10 | __author__ = "LDC Research Repository" 11 | __email__ = "remote-sensing@ldc.com" 12 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.10" 9 | 10 | sphinx: 11 | configuration: docs/conf.py 12 | 13 | python: 14 | install: 15 | - method: pip 16 | path: . 17 | extra_requirements: 18 | - doc 19 | -------------------------------------------------------------------------------- /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier 2 | _commit: 0.1.5 3 | _src_path: gh:12rambau/pypackage 4 | author_email: remote-sensing@ldc.com 5 | author_first_name: LDC Research Repository 6 | author_last_name: "" 7 | author_orcid: "" 8 | github_repo_name: geeservermap 9 | github_user: Louis-Dreyfus-Comany 10 | project_name: geeservermap 11 | project_slug: geeservermap 12 | short_description: Interactive map for Google Earth Engine in python 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE/pr_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request template 3 | about: Create a pull request 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## reference the related issue 10 | 11 | PR should answer problem stated in the issue tracker. please open one before starting a PR 12 | 13 | ## description of the changes 14 | 15 | Describe the changes you propose 16 | 17 | ## mention 18 | 19 | @mentions of the person or team responsible for reviewing proposed changes 20 | 21 | ## comments 22 | 23 | any other comments we should pay attention to 24 | -------------------------------------------------------------------------------- /geeservermap/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom exceptions for geeservermap.""" 2 | 3 | 4 | class ServerNotRunning(Exception): 5 | """Exception raised when the server is not running.""" 6 | 7 | def __init__(self, port): 8 | """Initialize the exception with the port number.""" 9 | self.message = f"""The server is not running or it isn't running on port {port}. To run the 10 | server follow instructions on . If the server is running in a 11 | different port, you must specify it when creating the Map instance: 12 | Map = geeservermap.Map(port=xxxx)""" 13 | super().__init__(self.message) 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.10" 16 | - name: Install dependencies 17 | run: pip install twine build nox 18 | - name: Build and publish 19 | env: 20 | TWINE_USERNAME: __token__ 21 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 22 | run: | 23 | python -m build 24 | twine upload dist/* 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :html_theme.sidebar_secondary.remove: 2 | 3 | 4 | geeservermap 5 | ============ 6 | 7 | .. toctree:: 8 | :hidden: 9 | 10 | usage 11 | contribute 12 | 13 | Documentation contents 14 | ---------------------- 15 | 16 | The documentation contains 3 main sections: 17 | 18 | .. grid:: 1 2 3 3 19 | 20 | .. grid-item:: 21 | 22 | .. card:: Usage 23 | :link: usage.html 24 | 25 | Usage and installation 26 | 27 | .. grid-item:: 28 | 29 | .. card:: Contribute 30 | :link: contribute.html 31 | 32 | Help us improve the lib. 33 | 34 | .. grid-item:: 35 | 36 | .. card:: API 37 | :link: autoapi/index.html 38 | 39 | Discover the lib API. 40 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Thanks goes to these wonderful people (`emoji key `_): 2 | 3 | .. raw:: html 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 |
9 | 10 | Louis-Dreyfus-Comany
11 | LDC Research Repository 12 |
13 | 💻 14 |
18 | 19 | This project follows the `all-contributors `_ specification. 20 | 21 | Contributions of any kind are welcome! 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 LDC Research Repository 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 | -------------------------------------------------------------------------------- /geeservermap/map.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Map module for GEE Server Map.""" 3 | 4 | import ee 5 | import requests 6 | 7 | from .elements import layers 8 | from .exceptions import ServerNotRunning 9 | from .main import PORT 10 | 11 | 12 | class Map: 13 | """Map class for managing layers in GEE Server Map.""" 14 | 15 | def __init__(self, port=PORT, do_async=False): 16 | """Initialize the Map instance.""" 17 | self.port = port 18 | self.do_async = do_async 19 | 20 | def _addImage(self, image, visParams=None, name=None, shown=True, opacity=1): 21 | """Add Image Layer to map.""" 22 | vis = layers.VisParams.from_image(image, visParams) 23 | image = layers.Image(image, vis) 24 | layer = image.layer(opacity, shown) 25 | data = layer.info() 26 | data["name"] = name 27 | try: 28 | requests.get(f"http://localhost:{self.port}/add_layer", params=data) 29 | except ConnectionError: 30 | raise ServerNotRunning(self.port) 31 | 32 | def addLayer(self, layer, visParas=None, name=None, shown=True, opacity=1): 33 | """Add a layer to the Map.""" 34 | if isinstance(layer, ee.Image): 35 | self._addImage(layer, visParas, name, shown, opacity) 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: [pre-commit, commit-msg] 2 | 3 | repos: 4 | - repo: "https://github.com/psf/black" 5 | rev: "22.3.0" 6 | hooks: 7 | - id: black 8 | stages: [commit] 9 | 10 | - repo: "https://github.com/commitizen-tools/commitizen" 11 | rev: "v2.18.0" 12 | hooks: 13 | - id: commitizen 14 | stages: [commit-msg] 15 | 16 | - repo: "https://github.com/kynan/nbstripout" 17 | rev: "0.5.0" 18 | hooks: 19 | - id: nbstripout 20 | stages: [commit] 21 | 22 | - repo: "https://github.com/pre-commit/mirrors-prettier" 23 | rev: "v2.7.1" 24 | hooks: 25 | - id: prettier 26 | stages: [commit] 27 | exclude: tests\/test_.+\. 28 | 29 | - repo: https://github.com/charliermarsh/ruff-pre-commit 30 | rev: "v0.0.215" 31 | hooks: 32 | - id: ruff 33 | stages: [commit] 34 | 35 | - repo: https://github.com/PyCQA/doc8 36 | rev: "v1.1.1" 37 | hooks: 38 | - id: doc8 39 | stages: [commit] 40 | 41 | - repo: https://github.com/FHPythonUtils/LicenseCheck 42 | rev: "2023.5.1" 43 | hooks: 44 | - id: licensecheck 45 | stages: [commit] 46 | 47 | - repo: https://github.com/codespell-project/codespell 48 | rev: v2.2.4 49 | hooks: 50 | - id: codespell 51 | stages: [commit] 52 | additional_dependencies: 53 | - tomli 54 | 55 | # Prevent committing inline conflict markers 56 | - repo: https://github.com/pre-commit/pre-commit-hooks 57 | rev: v4.3.0 58 | hooks: 59 | - id: check-merge-conflict 60 | stages: [commit] 61 | args: [--assume-in-merge] 62 | -------------------------------------------------------------------------------- /tests/check_warnings.py: -------------------------------------------------------------------------------- 1 | """Check the warnings from doc builds.""" 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | 7 | def check_warnings(file: Path) -> int: 8 | """Check the list of warnings produced by the CI tests. 9 | 10 | Raises errors if there are unexpected ones and/or if some are missing. 11 | 12 | Args: 13 | file: the path to the generated warning.txt file from 14 | the CI build 15 | 16 | Returns: 17 | 0 if the warnings are all there 18 | 1 if some warning are not registered or unexpected 19 | """ 20 | # print some log 21 | print("\n=== Sphinx Warnings test ===\n") 22 | 23 | # find the file where all the known warnings are stored 24 | warning_file = Path(__file__).parent / "data" / "warning_list.txt" 25 | 26 | test_warnings = file.read_text().strip().split("\n") 27 | ref_warnings = warning_file.read_text().strip().split("\n") 28 | 29 | print( 30 | f'Checking build warnings in file: "{file}" and comparing to expected ' 31 | f'warnings defined in "{warning_file}"\n\n' 32 | ) 33 | 34 | # find all the missing warnings 35 | missing_warnings = [] 36 | for wa in ref_warnings: 37 | index = [i for i, twa in enumerate(test_warnings) if wa in twa] 38 | if len(index) == 0: 39 | missing_warnings += [wa] 40 | print(f"Warning was not raised: {wa}") 41 | else: 42 | test_warnings.pop(index[0]) 43 | 44 | # the remaining one are unexpected 45 | for twa in test_warnings: 46 | print(f"Unexpected warning: {twa}") 47 | 48 | # delete the tmp warnings file 49 | file.unlink() 50 | 51 | return len(missing_warnings) != 0 or len(test_warnings) != 0 52 | 53 | 54 | if __name__ == "__main__": 55 | 56 | # cast the file to path and resolve to an absolute one 57 | file = Path.cwd() / "warnings.txt" 58 | 59 | # execute the test 60 | sys.exit(check_warnings(file)) 61 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """All the process that can be run using nox. 2 | 3 | The nox run are build in isolated environment that will be stored in .nox. to force the venv update, remove the .nox/xxx folder. 4 | """ 5 | 6 | import nox 7 | 8 | nox.options.sessions = ["lint", "test", "docs", "mypy"] 9 | 10 | 11 | @nox.session(reuse_venv=True) 12 | def lint(session): 13 | """Apply the pre-commits.""" 14 | session.install("pre-commit") 15 | session.run("pre-commit", "run", "--all-files", *session.posargs) 16 | 17 | 18 | @nox.session(reuse_venv=True) 19 | def test(session): 20 | """Run all the test using the environment variable of the running machine.""" 21 | session.install(".[test]") 22 | test_files = session.posargs or ["tests"] 23 | session.run("pytest", "--color=yes", "--cov", "--cov-report=xml", *test_files) 24 | 25 | 26 | @nox.session(reuse_venv=True, name="dead-fixtures") 27 | def dead_fixtures(session): 28 | """Check for dead fixtures within the tests.""" 29 | session.install(".[test]") 30 | session.run("pytest", "--dead-fixtures") 31 | 32 | 33 | @nox.session(reuse_venv=True) 34 | def docs(session): 35 | """Build the documentation.""" 36 | build = session.posargs.pop() if session.posargs else "html" 37 | session.install(".[doc]") 38 | dst, warn = f"docs/_build/{build}", "warnings.txt" 39 | session.run("sphinx-build", "-v", "-b", build, "docs", dst, "-w", warn) 40 | session.run("python", "tests/check_warnings.py") 41 | 42 | 43 | @nox.session(name="mypy", reuse_venv=True) 44 | def mypy(session): 45 | """Run a mypy check of the lib.""" 46 | session.install("mypy") 47 | test_files = session.posargs or ["geeservermap"] 48 | session.run("mypy", *test_files) 49 | 50 | 51 | @nox.session(reuse_venv=True) 52 | def stubgen(session): 53 | """Generate stub files for the lib but requires human attention before merge.""" 54 | session.install("mypy") 55 | package = session.posargs or ["geeservermap"] 56 | session.run("stubgen", "-p", package[0], "-o", "stubs", "--include-private") 57 | -------------------------------------------------------------------------------- /.github/workflows/pypackage_check.yaml: -------------------------------------------------------------------------------- 1 | name: template update check 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 1 * *" # Run at 00:00 on the first day of each month 7 | 8 | jobs: 9 | check_version: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.10" 16 | - name: install dependencies 17 | run: pip install requests 18 | - name: get latest pypackage release 19 | id: get_latest_release 20 | run: | 21 | RELEASE=$(curl -s https://api.github.com/repos/12rambau/pypackage/releases | jq -r '.[0].tag_name') 22 | echo "latest=$RELEASE" >> $GITHUB_OUTPUT 23 | echo "latest release: $RELEASE" 24 | - name: get current pypackage version 25 | id: get_current_version 26 | run: | 27 | RELEASE=$(yq -r "._commit" .copier-answers.yml) 28 | echo "current=$RELEASE" >> $GITHUB_OUTPUT 29 | echo "current release: $RELEASE" 30 | - name: open issue 31 | if: steps.get_current_version.outputs.current != steps.get_latest_release.outputs.latest 32 | uses: rishabhgupta/git-action-issue@v2 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | title: "Update template to ${{ steps.get_latest_release.outputs.latest }}" 36 | body: | 37 | The package is based on the ${{ steps.get_current_version.outputs.current }} version of [@12rambau/pypackage](https://github.com/12rambau/pypackage). 38 | 39 | The latest version of the template is ${{ steps.get_latest_release.outputs.latest }}. 40 | 41 | Please consider updating the template to the latest version to include all the latest developments. 42 | 43 | Run the following code in your project directory to update the template: 44 | 45 | ``` 46 | copier update --trust --defaults --vcs-ref ${{ steps.get_latest_release.outputs.latest }} 47 | ``` 48 | 49 | > **Note** 50 | > You may need to reinstall ``copier`` and ``jinja2-time`` if they are not available in your environment. 51 | 52 | After solving the merging issues you can push back the changes to your main branch. 53 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Configuration file for the Sphinx documentation builder. 2 | 3 | This file only contains a selection of the most common options. For a full 4 | list see the documentation: 5 | https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | """ 7 | 8 | # -- Path setup ---------------------------------------------------------------- 9 | from datetime import datetime 10 | 11 | # -- Project information ------------------------------------------------------- 12 | project = "geeservermap" 13 | author = "LDC Research Repository " 14 | copyright = f"2020-{datetime.now().year}, {author}" 15 | release = "0.0.0rc2" 16 | 17 | # -- General configuration ----------------------------------------------------- 18 | extensions = [ 19 | "sphinx_copybutton", 20 | "sphinx.ext.napoleon", 21 | "sphinx.ext.viewcode", 22 | "sphinx.ext.intersphinx", 23 | "sphinx_design", 24 | "autoapi.extension", 25 | ] 26 | exclude_patterns = ["**.ipynb_checkpoints"] 27 | templates_path = ["_template"] 28 | 29 | # -- Options for HTML output --------------------------------------------------- 30 | html_theme = "pydata_sphinx_theme" 31 | html_static_path = ["_static"] 32 | html_theme_options = { 33 | "logo": { 34 | "text": project, 35 | }, 36 | "use_edit_page_button": True, 37 | "footer_end": ["theme-version", "pypackage-credit"], 38 | "icon_links": [ 39 | { 40 | "name": "GitHub", 41 | "url": "https://github.com/Louis-Dreyfus-Comany/geeservermap", 42 | "icon": "fa-brands fa-github", 43 | }, 44 | { 45 | "name": "Pypi", 46 | "url": "https://pypi.org/project/geeservermap/", 47 | "icon": "fa-brands fa-python", 48 | }, 49 | ], 50 | } 51 | html_context = { 52 | "github_user": "Louis-Dreyfus-Comany", 53 | "github_repo": "geeservermap", 54 | "github_version": "", 55 | "doc_path": "docs", 56 | } 57 | html_css_files = ["custom.css"] 58 | 59 | # -- Options for autosummary/autodoc output ------------------------------------ 60 | autodoc_typehints = "description" 61 | autoapi_dirs = ["../geeservermap"] 62 | autoapi_python_class_content = "init" 63 | autoapi_member_order = "groupwise" 64 | 65 | # -- Options for intersphinx output -------------------------------------------- 66 | intersphinx_mapping = {} 67 | -------------------------------------------------------------------------------- /.github/workflows/unit.yaml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.10" 17 | - uses: pre-commit/action@v3.0.0 18 | 19 | mypy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-python@v4 24 | with: 25 | python-version: "3.10" 26 | - name: Install nox 27 | run: pip install nox 28 | - name: run mypy checks 29 | run: nox -s mypy 30 | 31 | docs: 32 | needs: [lint, mypy] 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: actions/setup-python@v4 37 | with: 38 | python-version: "3.10" 39 | - name: Install nox 40 | run: pip install nox 41 | - name: build static docs 42 | run: nox -s docs 43 | 44 | build: 45 | needs: [lint, mypy] 46 | strategy: 47 | fail-fast: true 48 | matrix: 49 | os: [ubuntu-latest] 50 | python-version: ["3.8", "3.9", "3.10", "3.11"] 51 | include: 52 | - os: macos-latest # macos test 53 | python-version: "3.11" 54 | - os: windows-latest # windows test 55 | python-version: "3.11" 56 | runs-on: ${{ matrix.os }} 57 | steps: 58 | - uses: actions/checkout@v3 59 | - name: Set up Python ${{ matrix.python-version }} 60 | uses: actions/setup-python@v4 61 | with: 62 | python-version: ${{ matrix.python-version }} 63 | - name: Install nox 64 | run: pip install nox 65 | - name: test with pytest 66 | run: nox -s test 67 | 68 | coverage: 69 | needs: [build] 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v3 73 | - uses: actions/setup-python@v4 74 | with: 75 | python-version: "3.10" 76 | - name: Install deps 77 | run: pip install nox 78 | - name: test with pytest 79 | run: nox -s test 80 | - name: assess dead fixtures 81 | run: nox -s dead-fixtures 82 | - name: codecov 83 | uses: codecov/codecov-action@v3 84 | with: 85 | token: ${{ secrets.CODECOV_TOKEN }} 86 | verbose: true 87 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | .ruff_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | docs/api/ 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 | 133 | # system IDE 134 | .vscode/ 135 | 136 | # image tmp file 137 | *Zone.Identifier -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | geeservermap 3 | ============ 4 | 5 | .. image:: https://img.shields.io/badge/License-MIT-yellow.svg?logo=opensourceinitiative&logoColor=white 6 | :target: LICENSE 7 | :alt: License: MIT 8 | 9 | .. image:: https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?logo=git&logoColor=white 10 | :target: https://conventionalcommits.org 11 | :alt: conventional commit 12 | 13 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 14 | :target: https://github.com/psf/black 15 | :alt: Black badge 16 | 17 | .. image:: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?logo=prettier&logoColor=white 18 | :target: https://github.com/prettier/prettier 19 | :alt: prettier badge 20 | 21 | .. image:: https://img.shields.io/badge/pre--commit-active-yellow?logo=pre-commit&logoColor=white 22 | :target: https://pre-commit.com/ 23 | :alt: pre-commit 24 | 25 | .. image:: https://img.shields.io/pypi/v/geeservermap?color=blue&logo=pypi&logoColor=white 26 | :target: https://pypi.org/project/geeservermap/ 27 | :alt: PyPI version 28 | 29 | .. image:: https://img.shields.io/github/actions/workflow/status/Louis-Dreyfus-Company/geeservermap/unit.yaml?logo=github&logoColor=white 30 | :target: https://github.com/Louis-Dreyfus-Company/geeservermap/actions/workflows/unit.yaml 31 | :alt: build 32 | 33 | .. image:: https://img.shields.io/codecov/c/github/Louis-Dreyfus-Company/geeservermap?logo=codecov&logoColor=white 34 | :target: https://codecov.io/gh/Louis-Dreyfus-Company/geeservermap 35 | :alt: Test Coverage 36 | 37 | .. image:: https://img.shields.io/readthedocs/geeservermap?logo=readthedocs&logoColor=white 38 | :target: https://geeservermap.readthedocs.io/en/latest/ 39 | :alt: Documentation Status 40 | 41 | Google Earth Engine Map Server 42 | ------------------------------ 43 | 44 | An interface that can be run as an independent server and interact with local 45 | code. 46 | 47 | You can create as many interfaces as you want. 48 | 49 | Each interface contains: 50 | 51 | Map 52 | ^^^ 53 | 54 | The map where each layer will be added 55 | 56 | NOT IMPLEMENTED YET 57 | ------------------- 58 | 59 | Layers 60 | ^^^^^^ 61 | A list of layers where you can modify some layer's parameters 62 | 63 | Inspector 64 | ^^^^^^^^^ 65 | 66 | Toggle Button 67 | ############# 68 | 69 | When toggled if you click on the map it will show the information of the 70 | active layers on Layers widget. 71 | 72 | Clear Button 73 | ############ 74 | 75 | Button to clear inpector's data 76 | 77 | 78 | Credits 79 | ------- 80 | 81 | This package was created with `Copier `__ and the `@12rambau/pypackage `__ 0.1.5 project template . 82 | -------------------------------------------------------------------------------- /geeservermap/main.py: -------------------------------------------------------------------------------- 1 | """This module contains the main function to run the geeservermap Flask app.""" 2 | 3 | # from dotenv import load_dotenv 4 | import argparse 5 | import uuid 6 | 7 | from flask import Flask, jsonify, render_template, request 8 | from flask_socketio import SocketIO, emit 9 | 10 | MESSAGES = {} 11 | 12 | # load_dotenv() # Load environment variable from .env 13 | PORT = 8018 14 | WIDTH = 800 15 | HEIGHT = 600 16 | 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument( 19 | "--port", default=PORT, help=f"Port in which the app will run. Defaults to {PORT}" 20 | ) 21 | parser.add_argument( 22 | "--width", default=WIDTH, help=f"Width of the map's pane. Defaults to {WIDTH} px" 23 | ) 24 | parser.add_argument( 25 | "--height", 26 | default=HEIGHT, 27 | help=f"Height of the map's pane. Defaults to {HEIGHT} px", 28 | ) 29 | 30 | app = Flask(__name__) 31 | socketio = SocketIO(app, cors_allowed_origins='*') # uses eventlet/gevent if installed 32 | 33 | 34 | @socketio.on('connect') 35 | def _on_connect(): 36 | """Handle a new client connection.""" 37 | print('Client connected:', request.sid) 38 | 39 | 40 | def register_map(width, height, port=PORT): 41 | """Register the index endpoint, allowing the user to pass a height and width.""" 42 | 43 | @app.route("/") 44 | def map(): 45 | return render_template("map.html", width=width, height=height, port=port) 46 | 47 | 48 | @app.route("/add_layer", methods=["GET"]) 49 | def add_layer(): 50 | """Endpoint to add a layer to the map.""" 51 | url = request.args.get("url", type=str) 52 | name = request.args.get("name", type=str) 53 | visible = request.args.get("visible", type=bool) 54 | opacity = request.args.get("opacity", type=float) 55 | layer = {"url": url, "name": name, "visible": visible, "opacity": opacity} 56 | job_id = uuid.uuid4().hex 57 | MESSAGES[job_id] = layer 58 | # broadcast full state to all connected websocket clients 59 | try: 60 | socketio.emit("messages", MESSAGES) 61 | except Exception as exc: 62 | print("socketio emit error:", exc) 63 | 64 | return jsonify({"job_id": job_id}) 65 | 66 | 67 | @app.route("/get_message", methods=["GET"]) 68 | def get_message(): 69 | """Endpoint to retrieve a message by its job ID.""" 70 | job_id = request.args.get("id", type=str) 71 | return jsonify(MESSAGES.get(job_id)) 72 | 73 | 74 | @app.route("/messages") 75 | def messages(): 76 | """Endpoint to retrieve all messages.""" 77 | return jsonify(MESSAGES) 78 | 79 | 80 | def run(): 81 | """Run the Flask app.""" 82 | args = parser.parse_args() 83 | port = args.port 84 | register_map(width=args.width, height=args.height, port=port) 85 | socketio.run(app, debug=True, port=port) 86 | -------------------------------------------------------------------------------- /geeservermap/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 18 | 19 | 24 | 25 | 31 | 32 | 33 | 38 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 56 | 57 | 58 | 59 | 76 | 77 | 78 | 79 | Google Earth Engine Server Map (URL) 80 | 81 | 82 | {% block map %}{% endblock %} 83 | 84 | 85 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "geeservermap" 7 | version = "0.0.0rc2" 8 | description = "Interactive map for Google Earth Engine in python" 9 | keywords = [ 10 | "google earth engine", 11 | "raster", 12 | "image processing", 13 | "gis", 14 | "flask", 15 | ] 16 | classifiers = [ 17 | "Development Status :: 3 - Alpha", 18 | "Intended Audience :: Developers", 19 | "License :: OSI Approved :: MIT License", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | ] 25 | requires-python = ">=3.8" 26 | dependencies = [ 27 | "flask", 28 | "requests", 29 | "brotli", 30 | "earthengine-api", 31 | "deprecated>=1.2.14", 32 | "flask_socketio" 33 | ] 34 | 35 | [[project.authors]] 36 | name = "LDC Research Repository" 37 | email = "remote-sensing@ldc.com" 38 | 39 | [project.license] 40 | text = "MIT" 41 | 42 | [project.readme] 43 | file = "README.rst" 44 | content-type = "text/x-rst" 45 | 46 | [project.urls] 47 | Homepage = "https://github.com/Louis-Dreyfus-Comany/geeservermap" 48 | 49 | [project.optional-dependencies] 50 | test = [ 51 | "pytest", 52 | "pytest-sugar", 53 | "pytest-cov", 54 | "pytest-deadfixtures" 55 | ] 56 | doc = [ 57 | "sphinx>=6.2.1", 58 | "pydata-sphinx-theme", 59 | "sphinx-copybutton", 60 | "sphinx-design", 61 | "sphinx-autoapi" 62 | ] 63 | 64 | [project.scripts] 65 | geeservermap = "geeservermap.main:run" 66 | 67 | [tool.hatch.build.targets.wheel] 68 | only-include = ["geeservermap"] 69 | 70 | [tool.hatch.envs.default] 71 | dependencies = [ 72 | "pre-commit", 73 | "commitizen", 74 | "nox" 75 | ] 76 | post-install-commands = ["pre-commit install"] 77 | 78 | [tool.commitizen] 79 | tag_format = "v$major.$minor.$patch$prerelease" 80 | update_changelog_on_bump = false 81 | version = "0.0.0rc2" 82 | version_files = [ 83 | "pyproject.toml:version", 84 | "geeservermap/__init__.py:__version__", 85 | "docs/conf.py:release", 86 | ] 87 | 88 | [tool.pytest.ini_options] 89 | testpaths = "tests" 90 | 91 | [tool.ruff] 92 | ignore-init-module-imports = true 93 | fix = true 94 | select = ["E", "F", "W", "I", "D", "RUF"] 95 | ignore = [ 96 | "E501", # line too long | Black take care of it 97 | "D212", # Multi-line docstring | We use D213 98 | ] 99 | 100 | [tool.ruff.flake8-quotes] 101 | docstring-quotes = "double" 102 | 103 | [tool.ruff.pydocstyle] 104 | convention = "google" 105 | 106 | [tool.coverage.run] 107 | source = ["geeservermap"] 108 | 109 | [tool.doc8] 110 | ignore = ["D001"] # we follow a 1 line = 1 paragraph style 111 | 112 | [tool.mypy] 113 | scripts_are_modules = true 114 | ignore_missing_imports = true 115 | install_types = true 116 | non_interactive = true 117 | warn_redundant_casts = true 118 | 119 | [tool.licensecheck] 120 | using = "PEP631" 121 | -------------------------------------------------------------------------------- /geeservermap/templates/map.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} {% block mapcss %} #map { width: {{ width }}px; 2 | height: {{ height }}px; padding: 0px; margin: 0px; } {% endblock mapcss %} {% 3 | block map %} 4 |
5 | 6 | 98 | {% endblock map %} 99 | -------------------------------------------------------------------------------- /geeservermap/async_jobs.py: -------------------------------------------------------------------------------- 1 | """Custom asynchronous job management for geeservermap.""" 2 | 3 | import atexit 4 | import time 5 | import uuid 6 | from contextlib import suppress 7 | from copy import deepcopy 8 | from threading import Thread 9 | 10 | 11 | class Async: 12 | """Custom asynchronous job management for geeservermap.""" 13 | 14 | _INTERVAL = 60 15 | _TIMEOUT = 300 16 | 17 | def __init__(self): 18 | """Initialize the asynchronous job manager.""" 19 | self._jobs = {} 20 | self.cleanup_running = False 21 | atexit.register(self._terminate) 22 | 23 | def _run_cleanup(self): 24 | """Run the cleanup thread for timed-out jobs.""" 25 | self.cleanup_thread = Thread(target=self._cleanup_timedout_jobs) 26 | self.cleanup_thread.start() 27 | 28 | # This MUST be called when flask wants to exit otherwise the process will hang for a while 29 | def _terminate(self): 30 | """Terminate the asynchronous job manager.""" 31 | self._jobs = None 32 | with suppress(Exception): 33 | self.cleanup_thread.join() 34 | 35 | def get_job_result(self, job_id): 36 | """Retrieve the result of a job by its ID.""" 37 | if not self._jobs: 38 | return None 39 | job = self._jobs.get(job_id) 40 | if job["state"] in ["finished", "failed"]: 41 | self.remove_job(job) 42 | return job 43 | 44 | def _create_job(self): 45 | """Create a new job.""" 46 | job = { 47 | "id": uuid.uuid4().hex, 48 | "ready": False, 49 | "result": None, 50 | "created": time.time(), 51 | "state": "created", 52 | "finished": None, 53 | } 54 | self._jobs[job["id"]] = job 55 | if not self.cleanup_running: 56 | self._run_cleanup() 57 | self.cleanup_running = True 58 | return job 59 | 60 | def _start_job(self, thread, job): 61 | """Start a job.""" 62 | job["state"] = "started" 63 | thread.daemon = True 64 | thread.start() 65 | 66 | def _finish_job(self, job, result): 67 | """Finish a job.""" 68 | job["ready"] = True 69 | job["state"] = "finished" 70 | if self._is_job_alive(job): 71 | job["result"] = result 72 | job["finished"] = time.time() 73 | 74 | def remove_job(self, job): 75 | """Remove a job from the manager.""" 76 | if self._jobs and job["id"] in self._jobs: 77 | # logger.info(f'removing job {job["id"]}') 78 | del self._jobs[job["id"]] 79 | 80 | def _is_job_alive(self, job): 81 | """Check if a job is still alive.""" 82 | return ( 83 | self._jobs 84 | and job is not None 85 | and job["id"] in self._jobs 86 | and job["ready"] is not None 87 | ) 88 | 89 | # Iterate though jobs every minute and remove the stale ones 90 | def _cleanup_timedout_jobs(self): 91 | """Cleanup timed-out jobs.""" 92 | next_cleanup_time = time.time() 93 | while self._jobs is not None: 94 | time.sleep(5) 95 | now = time.time() 96 | proxy = deepcopy(self._jobs) 97 | if proxy and now >= next_cleanup_time: 98 | for job in proxy.values(): 99 | if job["state"] == "finished" and ( 100 | job["finished"] + self._TIMEOUT > now 101 | ): 102 | self.remove_job(job) 103 | next_cleanup_time = time.time() + self._INTERVAL 104 | self.cleanup_running = False 105 | 106 | 107 | asyncgee = Async() 108 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | Thank you for your help improving **geeservermap**! 5 | 6 | **geeservermap** uses `nox `__ to automate several development-related tasks. 7 | Currently, the project uses four automation processes (called sessions) in ``noxfile.py``: 8 | 9 | - ``mypy``: to perform a mypy check on the lib; 10 | - ``test``: to run the test with pytest; 11 | - ``docs``: to build the documentation in the ``build`` folder; 12 | - ``lint``: to run the pre-commits in an isolated environment 13 | 14 | Every nox session is run in its own virtual environment, and the dependencies are installed automatically. 15 | 16 | To run a specific nox automation process, use the following command: 17 | 18 | .. code-block:: console 19 | 20 | nox -s 21 | 22 | For example: ``nox -s test`` or ``nox -s docs``. 23 | 24 | Workflow for contributing changes 25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 26 | 27 | We follow a typical GitHub workflow of: 28 | 29 | - Create a personal fork of this repo 30 | - Create a branch 31 | - Open a pull request 32 | - Fix findings of various linters and checks 33 | - Work through code review 34 | 35 | See the following sections for more details. 36 | 37 | Clone the repository 38 | ^^^^^^^^^^^^^^^^^^^^ 39 | 40 | First off, you'll need your own copy of **geeservermap** codebase. You can clone it for local development like so: 41 | 42 | Fork the repository so you have your own copy on GitHub. See the `GitHub forking guide for more information `__. 43 | 44 | Then, clone the repository locally so that you have a local copy to work on: 45 | 46 | .. code-block:: console 47 | 48 | git clone https://github.com//geeservermap 49 | cd geeservermap 50 | 51 | Then install the development version of the extension: 52 | 53 | .. code-block:: console 54 | 55 | pip install -e .[dev] 56 | 57 | This will install the **geeservermap** library, together with two additional tools: 58 | - `pre-commit `__ for automatically enforcing code standards and quality checks before commits. 59 | - `nox `__, for automating common development tasks. 60 | 61 | Lastly, activate the pre-commit hooks by running: 62 | 63 | .. code-block:: console 64 | 65 | pre-commit install 66 | 67 | This will install the necessary dependencies to run pre-commit every time you make a commit with Git. 68 | 69 | Contribute to the codebase 70 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 71 | 72 | Any larger updates to the codebase should include tests and documentation. The tests are located in the ``tests`` folder, and the documentation is located in the ``docs`` folder. 73 | 74 | To run the tests locally, use the following command: 75 | 76 | .. code-block:: console 77 | 78 | nox -s test 79 | 80 | See :ref:`below ` for more information on how to update the documentation. 81 | 82 | .. _contributing-docs: 83 | 84 | Contribute to the docs 85 | ^^^^^^^^^^^^^^^^^^^^^^ 86 | 87 | The documentation is built using `Sphinx `__ and deployed to `Read the Docs `__. 88 | 89 | To build the documentation locally, use the following command: 90 | 91 | .. code-block:: console 92 | 93 | nox -s docs 94 | 95 | For each pull request, the documentation is built and deployed to make it easier to review the changes in the PR. To access the docs build from a PR, click on the "Read the Docs" preview in the CI/CD jobs. 96 | 97 | Release new version 98 | ^^^^^^^^^^^^^^^^^^^ 99 | 100 | To release a new version, start by pushing a new bump from the local directory: 101 | 102 | .. code-block:: 103 | 104 | cz bump 105 | 106 | The commitizen-tool will detect the semantic version name based on the existing commits messages. 107 | 108 | Then push to Github. In Github design a new release using the same tag name nad the ``release.yaml`` job will send it to pipy. 109 | -------------------------------------------------------------------------------- /geeservermap/helpers.py: -------------------------------------------------------------------------------- 1 | """Helper functions for geeservermap.""" 2 | 3 | 4 | def visparamsStrToList(params): 5 | """Transform a string formatted as needed by ee.data.getMapId to a list. 6 | 7 | Args: 8 | params: to convert 9 | 10 | Returns: 11 | a list with the params 12 | """ 13 | proxy_bands = [] 14 | bands = params.split(",") 15 | for band in bands: 16 | proxy_bands.append(band.strip()) 17 | return proxy_bands 18 | 19 | 20 | def visparamsListToStr(params): 21 | """Transform a list to a string formatted as needed by ee.data.getMapId. 22 | 23 | Args: 24 | params: params to convert 25 | 26 | Returns: 27 | a string formatted as needed by ee.data.getMapId 28 | """ 29 | if not params: 30 | return params 31 | n = len(params) 32 | if n == 1: 33 | newbands = "{}".format(params[0]) 34 | elif n == 3: 35 | newbands = "{},{},{}".format(params[0], params[1], params[2]) 36 | else: 37 | newbands = "{}".format(params[0]) 38 | return newbands 39 | 40 | 41 | def getImageTile(image, visParams, visible=True): 42 | """Get image's tiles uri.""" 43 | proxy = {} 44 | params = visParams or {} 45 | 46 | # BANDS ############# 47 | def default_bands(image): 48 | bandnames = image.bandNames().getInfo() 49 | if len(bandnames) < 3: 50 | bands = [bandnames[0]] 51 | else: 52 | bands = [bandnames[0], bandnames[1], bandnames[2]] 53 | return bands 54 | 55 | bands = params.get("bands", default_bands(image)) 56 | 57 | # if the passed bands is a string formatted like required by GEE, get the 58 | # list out of it 59 | if isinstance(bands, str): 60 | bands_list = visparamsStrToList(bands) 61 | bands_str = visparamsListToStr(bands_list) 62 | 63 | # Transform list to getMapId format 64 | # ['b1', 'b2', 'b3'] == 'b1, b2, b3' 65 | if isinstance(bands, list): 66 | bands_list = bands 67 | bands_str = visparamsListToStr(bands) 68 | 69 | # Set proxy parameters 70 | proxy["bands"] = bands_str 71 | 72 | # MIN ################# 73 | themin = params.get("min") if "min" in params else "0" 74 | 75 | # if the passed min is a list, convert to the format required by GEE 76 | if isinstance(themin, list): 77 | themin = visparamsListToStr(themin) 78 | 79 | proxy["min"] = themin 80 | 81 | # MAX ################# 82 | def default_max(image, bands): 83 | proxy_maxs = [] 84 | maxs = { 85 | "float": 1, 86 | "double": 1, 87 | "int8": ((2**8) - 1) / 2, 88 | "uint8": (2**8) - 1, 89 | "int16": ((2**16) - 1) / 2, 90 | "uint16": (2**16) - 1, 91 | "int32": ((2**32) - 1) / 2, 92 | "uint32": (2**32) - 1, 93 | "int64": ((2**64) - 1) / 2, 94 | } 95 | for band in bands: 96 | ty = image.select([band]).getInfo()["bands"][0]["data_type"] 97 | try: 98 | themax = maxs[ty] 99 | except Exception: 100 | themax = 1 101 | proxy_maxs.append(themax) 102 | return proxy_maxs 103 | 104 | themax = params.get("max") if "max" in params else default_max(image, bands_list) 105 | 106 | # if the passed max is a list or the max is computed by the default function 107 | # convert to the format required by GEE 108 | if isinstance(themax, list): 109 | themax = visparamsListToStr(themax) 110 | 111 | proxy["max"] = themax 112 | 113 | # PALETTE 114 | if "palette" in params: 115 | if len(bands_list) == 1: 116 | palette = params.get("palette") 117 | if isinstance(palette, str): 118 | palette = visparamsStrToList(palette) 119 | toformat = "{}," * len(palette) 120 | palette = toformat[:-1].format(*palette) 121 | proxy["palette"] = palette 122 | else: 123 | print("Can't use palette parameter with more than one band") 124 | 125 | # Get the MapID and Token after applying parameters 126 | image_info = image.getMapId(proxy) 127 | fetcher = image_info["tile_fetcher"] 128 | tiles = fetcher.url_format 129 | attribution = ( 130 | 'Map Data © ' 131 | "Google Earth Engine " 132 | ) 133 | 134 | return { 135 | "url": tiles, 136 | "attribution": attribution, 137 | "visible": visible, 138 | "visParams": proxy, 139 | } 140 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.rst: -------------------------------------------------------------------------------- 1 | Contributor Covenant Code of Conduct 2 | ==================================== 3 | 4 | Our Pledge 5 | ---------- 6 | 7 | We as members, contributors, and leaders pledge to make participation in our 8 | community a harassment-free experience for everyone, regardless of age, body 9 | size, visible or invisible disability, ethnicity, sex characteristics, gender 10 | identity and expression, level of experience, education, socio-economic status, 11 | nationality, personal appearance, race, religion, or sexual identity 12 | and orientation. 13 | 14 | We pledge to act and interact in ways that contribute to an open, welcoming, 15 | diverse, inclusive, and healthy community. 16 | 17 | Our Standards 18 | ------------- 19 | 20 | Examples of behavior that contributes to a positive environment for our 21 | community include: 22 | 23 | * Demonstrating empathy and kindness toward other people 24 | * Being respectful of differing opinions, viewpoints, and experiences 25 | * Giving and gracefully accepting constructive feedback 26 | * Accepting responsibility and apologizing to those affected by our mistakes, 27 | and learning from the experience 28 | * Focusing on what is best not just for us as individuals, but for the 29 | overall community 30 | 31 | Examples of unacceptable behavior include: 32 | 33 | * The use of sexualized language or imagery, and sexual attention or 34 | advances of any kind 35 | * Trolling, insulting or derogatory comments, and personal or political attacks 36 | * Public or private harassment 37 | * Publishing others' private information, such as a physical or email 38 | address, without their explicit permission 39 | * Other conduct which could reasonably be considered inappropriate in a 40 | professional setting 41 | 42 | Enforcement Responsibilities 43 | ---------------------------- 44 | 45 | Community leaders are responsible for clarifying and enforcing our standards of 46 | acceptable behavior and will take appropriate and fair corrective action in 47 | response to any behavior that they deem inappropriate, threatening, offensive, 48 | or harmful. 49 | 50 | Community leaders have the right and responsibility to remove, edit, or reject 51 | comments, commits, code, wiki edits, issues, and other contributions that are 52 | not aligned to this Code of Conduct, and will communicate reasons for moderation 53 | decisions when appropriate. 54 | 55 | Scope 56 | ----- 57 | 58 | This Code of Conduct applies within all community spaces, and also applies when 59 | an individual is officially representing the community in public spaces. 60 | Examples of representing our community include using an official e-mail address, 61 | posting via an official social media account, or acting as an appointed 62 | representative at an online or offline event. 63 | 64 | Enforcement 65 | ----------- 66 | 67 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 68 | reported to the FAO team responsible for enforcement at 69 | pierrick.rambaud49@gmail.com. 70 | All complaints will be reviewed and investigated promptly and fairly. 71 | 72 | All community leaders are obligated to respect the privacy and security of the 73 | reporter of any incident. 74 | 75 | Enforcement Guidelines 76 | ---------------------- 77 | 78 | Community leaders will follow these Community Impact Guidelines in determining 79 | the consequences for any action they deem in violation of this Code of Conduct: 80 | 81 | Correction 82 | ^^^^^^^^^^ 83 | 84 | **Community Impact**: Use of inappropriate language or other behavior deemed 85 | unprofessional or unwelcome in the community. 86 | 87 | **Consequence**: A private, written warning from community leaders, providing 88 | clarity around the nature of the violation and an explanation of why the 89 | behavior was inappropriate. A public apology may be requested. 90 | 91 | Warning 92 | ^^^^^^^ 93 | 94 | **Community Impact**: A violation through a single incident or series 95 | of actions. 96 | 97 | **Consequence**: A warning with consequences for continued behavior. No 98 | interaction with the people involved, including unsolicited interaction with 99 | those enforcing the Code of Conduct, for a specified period of time. This 100 | includes avoiding interactions in community spaces as well as external channels 101 | like social media. Violating these terms may lead to a temporary or 102 | permanent ban. 103 | 104 | Temporary Ban 105 | ^^^^^^^^^^^^^ 106 | 107 | **Community Impact**: A serious violation of community standards, including 108 | sustained inappropriate behavior. 109 | 110 | **Consequence**: A temporary ban from any sort of interaction or public 111 | communication with the community for a specified period of time. No public or 112 | private interaction with the people involved, including unsolicited interaction 113 | with those enforcing the Code of Conduct, is allowed during this period. 114 | Violating these terms may lead to a permanent ban. 115 | 116 | Permanent Ban 117 | ^^^^^^^^^^^^^ 118 | 119 | **Community Impact**: Demonstrating a pattern of violation of community 120 | standards, including sustained inappropriate behavior, harassment of an 121 | individual, or aggression toward or disparagement of classes of individuals. 122 | 123 | **Consequence**: A permanent ban from any sort of public interaction within 124 | the community. 125 | 126 | Attribution 127 | ----------- 128 | 129 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 130 | version 2.0, available at 131 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 132 | 133 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 134 | enforcement ladder](https://github.com/mozilla/diversity). 135 | 136 | [homepage]: https://www.contributor-covenant.org 137 | 138 | For answers to common questions about this code of conduct, see the FAQ at 139 | https://www.contributor-covenant.org/faq. Translations are available at 140 | https://www.contributor-covenant.org/translations. 141 | -------------------------------------------------------------------------------- /geeservermap/elements/layers.py: -------------------------------------------------------------------------------- 1 | """Layer elements for GEE Server Map.""" 2 | 3 | from typing import Union 4 | 5 | import ee 6 | 7 | from .. import helpers 8 | 9 | 10 | class VisParams: 11 | """Visualization parameters to apply in a layer.""" 12 | 13 | def __init__( 14 | self, 15 | bands: list, 16 | min: Union[int, list], 17 | max: Union[int, list], 18 | palette=None, 19 | gain=None, 20 | bias=None, 21 | gamma=None, 22 | ): 23 | """Initialize visualization parameters.""" 24 | # Bands 25 | self.bands = self.__format_bands(bands) 26 | self._bands_len = len(self.bands) 27 | self.min = self.__format_param(min, "min") 28 | self.max = self.__format_param(max, "max") 29 | self.gain = self.__format_param(gain, "gain") 30 | self.bias = self.__format_param(bias, "bias") 31 | self.gamma = self.__format_param(gamma, "gamma") 32 | 33 | # Palette 34 | if palette: 35 | if len(self.bands) > 1: 36 | print("Can't use palette parameter with more than one band") 37 | palette = None 38 | self.palette = palette 39 | 40 | @staticmethod 41 | def __format_bands(bands): 42 | """Format bands list.""" 43 | if isinstance(bands, str): 44 | bands = [bands] 45 | if len(bands) < 3: 46 | bands = bands[0:1] 47 | elif len(bands) > 3: 48 | bands = bands[0:3] 49 | return bands 50 | 51 | def __format_param(self, value, param): 52 | """Format parameter value.""" 53 | if isinstance(value, (int, float)): 54 | return [value] * self._bands_len 55 | elif isinstance(value, str): 56 | return [float(value)] * self._bands_len 57 | elif isinstance(value, (list, tuple)): 58 | return [value[0]] * self._bands_len 59 | elif value is None: 60 | return value 61 | else: 62 | raise ValueError(f"Can't use {value} as {param} value") 63 | 64 | @staticmethod 65 | def __format_palette(palette): 66 | """Format palette list.""" 67 | return ",".join(palette) if palette else None 68 | 69 | @classmethod 70 | def from_image(cls, image, visParams=None): 71 | """Create visualization parameters from an image.""" 72 | visParams = visParams or {} 73 | if "bands" not in visParams: 74 | bands = image.bandNames().getInfo() 75 | else: 76 | bands = visParams["bands"] 77 | bands = cls.__format_bands(bands) 78 | visParams["bands"] = bands 79 | 80 | # Min and max 81 | btypes = None 82 | if "min" not in visParams: 83 | image = image.select(visParams["bands"]) 84 | btypes = image.bandTypes().getInfo() 85 | mins = [btype.get("min") for bname, btype in btypes.items()] 86 | mins = [m or 0 for m in mins] 87 | visParams["min"] = mins 88 | if "max" not in visParams: 89 | image = image.select(visParams["bands"]) 90 | if not btypes: 91 | btypes = image.bandTypes().getInfo() 92 | maxs = [btype.get("max") for bname, btype in btypes.items()] 93 | maxs = [m or 1 for m in maxs] 94 | visParams["max"] = maxs 95 | 96 | return cls(**visParams) 97 | 98 | def for_mapid(self): 99 | """Return params for using in Image.MapId.""" 100 | return { 101 | "bands": helpers.visparamsListToStr(self.bands), 102 | "min": helpers.visparamsListToStr(self.min), 103 | "max": helpers.visparamsListToStr(self.max), 104 | "palette": self.__format_palette(self.palette), 105 | "bias": helpers.visparamsListToStr(self.bias), 106 | "gain": helpers.visparamsListToStr(self.gain), 107 | "gamma": helpers.visparamsListToStr(self.gamma), 108 | } 109 | 110 | 111 | class MapLayer: 112 | """Map layer representation for GEE Server Map.""" 113 | 114 | ATTR = ( 115 | 'Map Data © ' 116 | "Google Earth Engine" 117 | ) 118 | 119 | def __init__(self, url, opacity, visible, attribution=ATTR): 120 | """Initialize the MapLayer instance.""" 121 | self.url = url 122 | if opacity > 1: 123 | print("opacity cannot be greater than 1, setting to 1") 124 | opacity = 1 125 | elif opacity < 0: 126 | print("opacity cannot be less than 0, setting to 0") 127 | opacity = 0 128 | self.opacity = opacity 129 | self.visible = visible 130 | self.attribution = attribution 131 | 132 | def info(self): 133 | """Get the message to send to the backend.""" 134 | return { 135 | "url": self.url, 136 | "attribution": self.attribution, 137 | "visible": self.visible, 138 | "opacity": self.opacity, 139 | } 140 | 141 | 142 | class Image: 143 | """Image representation for GEE Server Map.""" 144 | 145 | def __init__(self, image: ee.Image, visParams: VisParams): 146 | """Initialize the Image instance.""" 147 | self.image = image 148 | self.visParams = visParams 149 | 150 | def bands(self): 151 | """Get bands from visParams or from the image directly.""" 152 | if self.visParams.bands: 153 | return self.visParams.bands 154 | else: 155 | bandnames = self.image.bandNames().getInfo() 156 | if len(bandnames) < 3: 157 | bands = bandnames[0:1] 158 | else: 159 | bands = bandnames[0:3] 160 | return bands 161 | 162 | @property 163 | def url(self): 164 | """Image Tiles URL.""" 165 | params = self.visParams.for_mapid() 166 | # params.setdefault('bands', self.bands()) # set bands if not passed in visparams 167 | image_info = self.image.getMapId(params) 168 | fetcher = image_info["tile_fetcher"] 169 | tiles = fetcher.url_format 170 | return tiles 171 | 172 | def layer(self, opacity=1, visible=True): 173 | """Layer for adding to map.""" 174 | return MapLayer(self.url, opacity, visible) 175 | 176 | 177 | class Geometry: 178 | """Geometry representation for GEE Server Map.""" 179 | 180 | def __init__(self, geometry: ee.Geometry): 181 | """Initialize the Geometry instance.""" 182 | self.geometry = geometry 183 | 184 | def layer(self, opacity=1, visible=True): 185 | """Layer for adding to map.""" 186 | pass 187 | --------------------------------------------------------------------------------