├── 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 |
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 |
--------------------------------------------------------------------------------