geoloupe 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geoloupe-0.1.0/.dockerignore +11 -0
- geoloupe-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +30 -0
- geoloupe-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- geoloupe-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +15 -0
- geoloupe-0.1.0/.github/dependabot.yml +18 -0
- geoloupe-0.1.0/.github/pull_request_template.md +10 -0
- geoloupe-0.1.0/.github/workflows/ci.yml +82 -0
- geoloupe-0.1.0/.github/workflows/release.yml +78 -0
- geoloupe-0.1.0/.gitignore +28 -0
- geoloupe-0.1.0/.pre-commit-config.yaml +18 -0
- geoloupe-0.1.0/ARCHITECTURE.md +279 -0
- geoloupe-0.1.0/CHANGELOG.md +37 -0
- geoloupe-0.1.0/CITATION.cff +18 -0
- geoloupe-0.1.0/CODE_OF_CONDUCT.md +10 -0
- geoloupe-0.1.0/CONTRIBUTING.md +111 -0
- geoloupe-0.1.0/Dockerfile +18 -0
- geoloupe-0.1.0/LICENSE +202 -0
- geoloupe-0.1.0/PKG-INFO +282 -0
- geoloupe-0.1.0/README.md +214 -0
- geoloupe-0.1.0/RELEASING.md +71 -0
- geoloupe-0.1.0/ROADMAP.md +144 -0
- geoloupe-0.1.0/SECURITY.md +24 -0
- geoloupe-0.1.0/deploy/huggingface/Dockerfile +13 -0
- geoloupe-0.1.0/deploy/huggingface/README.md +18 -0
- geoloupe-0.1.0/docs/benchmarks.md +104 -0
- geoloupe-0.1.0/docs/configuration.md +243 -0
- geoloupe-0.1.0/docs/data-tiles.md +73 -0
- geoloupe-0.1.0/docs/decisions.md +204 -0
- geoloupe-0.1.0/docs/research.md +527 -0
- geoloupe-0.1.0/examples/README.md +17 -0
- geoloupe-0.1.0/examples/api_client.py +78 -0
- geoloupe-0.1.0/examples/arco-era5.yaml +14 -0
- geoloupe-0.1.0/examples/fetch_ncei_stations.py +65 -0
- geoloupe-0.1.0/examples/fetch_public_samples.py +57 -0
- geoloupe-0.1.0/examples/hpc/build_overviews.sbatch +45 -0
- geoloupe-0.1.0/examples/notebook_quickstart.py +80 -0
- geoloupe-0.1.0/examples/public-s3.yaml +20 -0
- geoloupe-0.1.0/examples/sentinel2.yaml +44 -0
- geoloupe-0.1.0/frontend/.prettierrc.json +1 -0
- geoloupe-0.1.0/frontend/e2e/gallery.mjs +118 -0
- geoloupe-0.1.0/frontend/e2e/smoke.mjs +162 -0
- geoloupe-0.1.0/frontend/e2e/tour.mjs +96 -0
- geoloupe-0.1.0/frontend/eslint.config.js +24 -0
- geoloupe-0.1.0/frontend/index.html +14 -0
- geoloupe-0.1.0/frontend/package-lock.json +3643 -0
- geoloupe-0.1.0/frontend/package.json +39 -0
- geoloupe-0.1.0/frontend/public/countries-110m.geojson +1 -0
- geoloupe-0.1.0/frontend/public/favicon.svg +1 -0
- geoloupe-0.1.0/frontend/src/App.tsx +99 -0
- geoloupe-0.1.0/frontend/src/api/client.ts +121 -0
- geoloupe-0.1.0/frontend/src/api/types.ts +212 -0
- geoloupe-0.1.0/frontend/src/components/AddData.tsx +107 -0
- geoloupe-0.1.0/frontend/src/components/Colorbar.tsx +154 -0
- geoloupe-0.1.0/frontend/src/components/Gallery.tsx +102 -0
- geoloupe-0.1.0/frontend/src/components/Icon.tsx +38 -0
- geoloupe-0.1.0/frontend/src/components/Inspector.tsx +266 -0
- geoloupe-0.1.0/frontend/src/components/LayerPanel.tsx +538 -0
- geoloupe-0.1.0/frontend/src/components/MapCanvas.tsx +374 -0
- geoloupe-0.1.0/frontend/src/components/MapView.tsx +403 -0
- geoloupe-0.1.0/frontend/src/components/StatusBar.tsx +104 -0
- geoloupe-0.1.0/frontend/src/components/TimeBar.tsx +148 -0
- geoloupe-0.1.0/frontend/src/components/TimeSeriesChart.tsx +141 -0
- geoloupe-0.1.0/frontend/src/components/Toolbar.tsx +250 -0
- geoloupe-0.1.0/frontend/src/lib/activity.ts +18 -0
- geoloupe-0.1.0/frontend/src/lib/basemaps.ts +60 -0
- geoloupe-0.1.0/frontend/src/lib/classes.ts +74 -0
- geoloupe-0.1.0/frontend/src/lib/colormap.ts +152 -0
- geoloupe-0.1.0/frontend/src/lib/datatile.ts +69 -0
- geoloupe-0.1.0/frontend/src/lib/exportImage.ts +79 -0
- geoloupe-0.1.0/frontend/src/lib/format.ts +30 -0
- geoloupe-0.1.0/frontend/src/lib/gallery.ts +57 -0
- geoloupe-0.1.0/frontend/src/lib/lib.test.ts +378 -0
- geoloupe-0.1.0/frontend/src/lib/mapRegistry.ts +10 -0
- geoloupe-0.1.0/frontend/src/lib/mercator.ts +62 -0
- geoloupe-0.1.0/frontend/src/lib/points.ts +106 -0
- geoloupe-0.1.0/frontend/src/lib/protocol.ts +111 -0
- geoloupe-0.1.0/frontend/src/lib/tiles.ts +76 -0
- geoloupe-0.1.0/frontend/src/lib/timeline.ts +85 -0
- geoloupe-0.1.0/frontend/src/lib/urlState.ts +79 -0
- geoloupe-0.1.0/frontend/src/lib/useFetch.ts +32 -0
- geoloupe-0.1.0/frontend/src/lib/useLatest.ts +10 -0
- geoloupe-0.1.0/frontend/src/lib/vectors.ts +81 -0
- geoloupe-0.1.0/frontend/src/lib/view.ts +141 -0
- geoloupe-0.1.0/frontend/src/main.tsx +38 -0
- geoloupe-0.1.0/frontend/src/state/store.ts +389 -0
- geoloupe-0.1.0/frontend/src/styles.css +1171 -0
- geoloupe-0.1.0/frontend/src/vite-env.d.ts +1 -0
- geoloupe-0.1.0/frontend/tsconfig.json +19 -0
- geoloupe-0.1.0/frontend/vite.config.ts +22 -0
- geoloupe-0.1.0/pyproject.toml +135 -0
- geoloupe-0.1.0/scripts/benchmark.py +92 -0
- geoloupe-0.1.0/scripts/build_colormaps.py +99 -0
- geoloupe-0.1.0/scripts/ci_local.sh +65 -0
- geoloupe-0.1.0/src/geoloupe/__init__.py +24 -0
- geoloupe-0.1.0/src/geoloupe/api/__init__.py +0 -0
- geoloupe-0.1.0/src/geoloupe/api/app.py +735 -0
- geoloupe-0.1.0/src/geoloupe/catalog.py +782 -0
- geoloupe-0.1.0/src/geoloupe/cli.py +306 -0
- geoloupe-0.1.0/src/geoloupe/colormaps.json +207 -0
- geoloupe-0.1.0/src/geoloupe/colormaps.py +79 -0
- geoloupe-0.1.0/src/geoloupe/config.py +400 -0
- geoloupe-0.1.0/src/geoloupe/demo.py +415 -0
- geoloupe-0.1.0/src/geoloupe/engine.py +614 -0
- geoloupe-0.1.0/src/geoloupe/gallery.yaml +314 -0
- geoloupe-0.1.0/src/geoloupe/grid.py +509 -0
- geoloupe-0.1.0/src/geoloupe/overviews.py +457 -0
- geoloupe-0.1.0/src/geoloupe/points.py +861 -0
- geoloupe-0.1.0/src/geoloupe/query.py +326 -0
- geoloupe-0.1.0/src/geoloupe/readers.py +468 -0
- geoloupe-0.1.0/src/geoloupe/server.py +131 -0
- geoloupe-0.1.0/src/geoloupe/stac.py +189 -0
- geoloupe-0.1.0/src/geoloupe/static/assets/index-CFeR8hOI.css +1 -0
- geoloupe-0.1.0/src/geoloupe/static/assets/index-Cbm8G5M-.js +825 -0
- geoloupe-0.1.0/src/geoloupe/static/assets/maplibre-gl-worker-CMkqA50c.mjs +10 -0
- geoloupe-0.1.0/src/geoloupe/static/countries-110m.geojson +1 -0
- geoloupe-0.1.0/src/geoloupe/static/favicon.svg +1 -0
- geoloupe-0.1.0/src/geoloupe/static/index.html +15 -0
- geoloupe-0.1.0/src/geoloupe/styles.py +69 -0
- geoloupe-0.1.0/src/geoloupe/tiling.py +104 -0
- geoloupe-0.1.0/src/geoloupe/timeaxis.py +256 -0
- geoloupe-0.1.0/src/geoloupe/vectors.py +270 -0
- geoloupe-0.1.0/tests/__init__.py +0 -0
- geoloupe-0.1.0/tests/conftest.py +76 -0
- geoloupe-0.1.0/tests/test_api.py +199 -0
- geoloupe-0.1.0/tests/test_categorical.py +131 -0
- geoloupe-0.1.0/tests/test_curvilinear.py +177 -0
- geoloupe-0.1.0/tests/test_engine.py +266 -0
- geoloupe-0.1.0/tests/test_gallery.py +241 -0
- geoloupe-0.1.0/tests/test_open.py +78 -0
- geoloupe-0.1.0/tests/test_overviews_config_readers.py +225 -0
- geoloupe-0.1.0/tests/test_points.py +251 -0
- geoloupe-0.1.0/tests/test_regressions.py +222 -0
- geoloupe-0.1.0/tests/test_s3.py +72 -0
- geoloupe-0.1.0/tests/test_satellite.py +177 -0
- geoloupe-0.1.0/tests/test_stac.py +140 -0
- geoloupe-0.1.0/tests/test_tiling_grid_time.py +196 -0
- geoloupe-0.1.0/tests/test_vectors.py +167 -0
- geoloupe-0.1.0/uv.lock +3764 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something does not work as described
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what
|
|
7
|
+
attributes:
|
|
8
|
+
label: What happened, and what did you expect?
|
|
9
|
+
validations:
|
|
10
|
+
required: true
|
|
11
|
+
- type: textarea
|
|
12
|
+
id: steps
|
|
13
|
+
attributes:
|
|
14
|
+
label: How to reproduce
|
|
15
|
+
description: The command or configuration (YAML), the dataset (a public URL if possible), and the steps in the browser.
|
|
16
|
+
render: shell
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: logs
|
|
21
|
+
attributes:
|
|
22
|
+
label: Server log or browser console messages
|
|
23
|
+
render: text
|
|
24
|
+
- type: input
|
|
25
|
+
id: version
|
|
26
|
+
attributes:
|
|
27
|
+
label: Versions
|
|
28
|
+
description: "Output of `geoloupe --version`, your Python version and browser"
|
|
29
|
+
validations:
|
|
30
|
+
required: true
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Feature or data support request
|
|
2
|
+
description: A new feature, file format, grid type or dataset to support
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: need
|
|
7
|
+
attributes:
|
|
8
|
+
label: What do you want to do, and why?
|
|
9
|
+
description: Describe the research task. A public example dataset helps a lot.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: idea
|
|
14
|
+
attributes:
|
|
15
|
+
label: Ideas for how it could work (optional)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Keep dependencies current with grouped pull requests (one per ecosystem and interval).
|
|
2
|
+
version: 2
|
|
3
|
+
updates:
|
|
4
|
+
- package-ecosystem: github-actions
|
|
5
|
+
directory: /
|
|
6
|
+
schedule: {interval: weekly}
|
|
7
|
+
groups:
|
|
8
|
+
actions: {patterns: ["*"]}
|
|
9
|
+
- package-ecosystem: uv
|
|
10
|
+
directory: /
|
|
11
|
+
schedule: {interval: monthly}
|
|
12
|
+
groups:
|
|
13
|
+
python: {patterns: ["*"]}
|
|
14
|
+
- package-ecosystem: npm
|
|
15
|
+
directory: /frontend
|
|
16
|
+
schedule: {interval: monthly}
|
|
17
|
+
groups:
|
|
18
|
+
web: {patterns: ["*"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## What and why
|
|
2
|
+
|
|
3
|
+
<!-- The problem this solves, and how. Link the issue: "Fixes #123". -->
|
|
4
|
+
|
|
5
|
+
## Checklist
|
|
6
|
+
|
|
7
|
+
- [ ] Tests added or updated (`uv run pytest`, `npm test`)
|
|
8
|
+
- [ ] `scripts/ci_local.sh` passes, or the same checks by hand (ruff, mypy, eslint, build)
|
|
9
|
+
- [ ] Docs updated (README, docs/, CHANGELOG.md under "Unreleased")
|
|
10
|
+
- [ ] For UI changes: a screenshot, and the browser smoke test (`node e2e/smoke.mjs`)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
workflow_dispatch: # "Run workflow" button in the Actions tab
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
python:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v6
|
|
17
|
+
- run: uv sync --all-extras --python ${{ matrix.python }}
|
|
18
|
+
- run: uv run ruff check src tests scripts examples
|
|
19
|
+
- run: uv run ruff format --check src tests scripts examples
|
|
20
|
+
- run: uv run mypy
|
|
21
|
+
- run: uv run pytest
|
|
22
|
+
|
|
23
|
+
web:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
defaults:
|
|
26
|
+
run:
|
|
27
|
+
working-directory: frontend
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: actions/setup-node@v4
|
|
31
|
+
with:
|
|
32
|
+
node-version: 24
|
|
33
|
+
cache: npm
|
|
34
|
+
cache-dependency-path: frontend/package-lock.json
|
|
35
|
+
- run: npm ci
|
|
36
|
+
- run: npm run lint
|
|
37
|
+
- run: npm test
|
|
38
|
+
- run: npm run build
|
|
39
|
+
|
|
40
|
+
e2e:
|
|
41
|
+
needs: [python, web]
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: astral-sh/setup-uv@v6
|
|
46
|
+
- uses: actions/setup-node@v4
|
|
47
|
+
with:
|
|
48
|
+
node-version: 24
|
|
49
|
+
- run: uv sync
|
|
50
|
+
- run: uv run geoloupe demo-data demo --days 200
|
|
51
|
+
- run: uv run geoloupe serve demo/demo.yaml --port 8765 --open-root demo &
|
|
52
|
+
- run: cd frontend && npm ci && npx playwright install --with-deps chromium && npm run build
|
|
53
|
+
- run: timeout 300 bash -c 'until curl -sf http://127.0.0.1:8765/api/health; do sleep 2; done'
|
|
54
|
+
- run: cd frontend && node e2e/smoke.mjs http://127.0.0.1:8765 shots
|
|
55
|
+
- uses: actions/upload-artifact@v4
|
|
56
|
+
if: always()
|
|
57
|
+
with:
|
|
58
|
+
name: screenshots
|
|
59
|
+
path: frontend/shots
|
|
60
|
+
|
|
61
|
+
# The gallery reads public data from AWS, Google Cloud and NOAA: an outage there must not make
|
|
62
|
+
# CI red, so this job only reports.
|
|
63
|
+
gallery:
|
|
64
|
+
needs: [python, web]
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
continue-on-error: true
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
- uses: astral-sh/setup-uv@v6
|
|
70
|
+
- uses: actions/setup-node@v4
|
|
71
|
+
with:
|
|
72
|
+
node-version: 24
|
|
73
|
+
- run: uv sync --all-extras
|
|
74
|
+
- run: uv run geoloupe gallery --port 8766 &
|
|
75
|
+
- run: cd frontend && npm ci && npx playwright install --with-deps chromium
|
|
76
|
+
- run: timeout 180 bash -c 'until curl -sf http://127.0.0.1:8766/api/health; do sleep 2; done'
|
|
77
|
+
- run: cd frontend && node e2e/gallery.mjs http://127.0.0.1:8766 gallery-shots
|
|
78
|
+
- uses: actions/upload-artifact@v4
|
|
79
|
+
if: always()
|
|
80
|
+
with:
|
|
81
|
+
name: gallery-screenshots
|
|
82
|
+
path: frontend/gallery-shots
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
# Push a tag "vX.Y.Z" (equal to __version__ in src/geoloupe/__init__.py) to publish that version on
|
|
3
|
+
# PyPI and create a GitHub release. PyPI accepts the upload through "trusted publishing" (OpenID
|
|
4
|
+
# Connect): no password or token is stored anywhere. See RELEASING.md for the one-time setup.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
workflow_dispatch: # build and check only (no upload): a dry run
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v6
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 24
|
|
19
|
+
cache: npm
|
|
20
|
+
cache-dependency-path: frontend/package-lock.json
|
|
21
|
+
- name: The tag equals the package version
|
|
22
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
23
|
+
run: |
|
|
24
|
+
v=$(sed -n 's/^__version__ = "\(.*\)"/\1/p' src/geoloupe/__init__.py)
|
|
25
|
+
if [ "v$v" != "$GITHUB_REF_NAME" ]; then
|
|
26
|
+
echo "tag $GITHUB_REF_NAME does not match __version__ $v"; exit 1
|
|
27
|
+
fi
|
|
28
|
+
- name: Build the web client from this commit (shipped inside the wheel)
|
|
29
|
+
run: cd frontend && npm ci && npm run build
|
|
30
|
+
- run: uv build
|
|
31
|
+
- run: uvx twine check dist/*
|
|
32
|
+
- name: Smoke test the wheel in a clean environment
|
|
33
|
+
run: |
|
|
34
|
+
uv venv /tmp/wheel-test
|
|
35
|
+
uv pip install --python /tmp/wheel-test/bin/python dist/*.whl
|
|
36
|
+
/tmp/wheel-test/bin/geoloupe --version
|
|
37
|
+
/tmp/wheel-test/bin/geoloupe gallery --show-config > /dev/null
|
|
38
|
+
cd /tmp && /tmp/wheel-test/bin/geoloupe demo-data demo --days 20
|
|
39
|
+
(/tmp/wheel-test/bin/geoloupe serve demo/demo.yaml --port 8799 &)
|
|
40
|
+
timeout 120 bash -c 'until curl -sf http://127.0.0.1:8799/api/health; do sleep 2; done'
|
|
41
|
+
curl -sf http://127.0.0.1:8799/ > /dev/null
|
|
42
|
+
- uses: actions/upload-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: dist
|
|
45
|
+
path: dist/
|
|
46
|
+
|
|
47
|
+
publish:
|
|
48
|
+
needs: build
|
|
49
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
environment:
|
|
52
|
+
name: pypi
|
|
53
|
+
url: https://pypi.org/project/geoloupe/
|
|
54
|
+
permissions:
|
|
55
|
+
id-token: write # trusted publishing
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/download-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
62
|
+
|
|
63
|
+
github-release:
|
|
64
|
+
needs: publish
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
permissions:
|
|
67
|
+
contents: write
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/download-artifact@v4
|
|
70
|
+
with:
|
|
71
|
+
name: dist
|
|
72
|
+
path: dist/
|
|
73
|
+
- name: Create the GitHub release with the built files
|
|
74
|
+
env:
|
|
75
|
+
GH_TOKEN: ${{ github.token }}
|
|
76
|
+
run: >
|
|
77
|
+
gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY"
|
|
78
|
+
--title "GeoLoupe ${GITHUB_REF_NAME#v}" --generate-notes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.mypy_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
|
|
12
|
+
# Web client (the built client in src/geoloupe/static IS committed: the package ships it)
|
|
13
|
+
frontend/node_modules
|
|
14
|
+
frontend/dist
|
|
15
|
+
frontend/test-results/
|
|
16
|
+
frontend/playwright-report/
|
|
17
|
+
|
|
18
|
+
# Data and outputs (regenerable)
|
|
19
|
+
demo/
|
|
20
|
+
samples/
|
|
21
|
+
*.overviews.zarr/
|
|
22
|
+
slurm_logs/
|
|
23
|
+
*.out
|
|
24
|
+
|
|
25
|
+
# Editors / OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# pre-commit install → runs on every commit
|
|
2
|
+
repos:
|
|
3
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
4
|
+
rev: v0.14.0
|
|
5
|
+
hooks:
|
|
6
|
+
- id: ruff-check
|
|
7
|
+
args: [--fix]
|
|
8
|
+
- id: ruff-format
|
|
9
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
10
|
+
rev: v6.0.0
|
|
11
|
+
hooks:
|
|
12
|
+
- id: end-of-file-fixer
|
|
13
|
+
exclude: ^src/geoloupe/(static|colormaps\.json)
|
|
14
|
+
- id: trailing-whitespace
|
|
15
|
+
exclude: ^src/geoloupe/static/
|
|
16
|
+
- id: check-yaml
|
|
17
|
+
- id: check-added-large-files
|
|
18
|
+
args: [--maxkb=2000]
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# GeoLoupe architecture
|
|
2
|
+
|
|
3
|
+
This document describes how GeoLoupe works, why it is built this way, and which alternatives were
|
|
4
|
+
considered. The survey of existing tools is in [docs/research.md](docs/research.md); the individual
|
|
5
|
+
decisions are logged in [docs/decisions.md](docs/decisions.md).
|
|
6
|
+
|
|
7
|
+
## 1. Goals and constraints
|
|
8
|
+
|
|
9
|
+
GeoLoupe should let a researcher open large gridded datasets and explore them like a web map:
|
|
10
|
+
pan, zoom, step through time, read values, compare datasets. The hard requirements are:
|
|
11
|
+
|
|
12
|
+
1. **Seamless multi-resolution.** A coarse global dataset and a fine regional one (or several) must
|
|
13
|
+
form one map: coarse when zoomed out, fine where it exists when zoomed in, without visible seams
|
|
14
|
+
and without the user switching layers.
|
|
15
|
+
2. **Never load what is not visible.** A 20 880 × 43 200 × 13 864 grid (a 30″ ≈ 1 km daily climate dataset) cannot be
|
|
16
|
+
read for a zoomed-out view. Every request must have a bounded cost.
|
|
17
|
+
3. **Scientific honesty.** The user must be able to tell what they see: which dataset, at which
|
|
18
|
+
resolution, aggregated or native. Clicked values must be the stored values.
|
|
19
|
+
4. **Formats as they are.** Data stays where and how it is (Zarr, NetCDF, GRIB, GeoTIFF, local or
|
|
20
|
+
object storage). Preprocessing is optional and only adds speed.
|
|
21
|
+
5. **Simple to run** on a laptop, a workstation, or an HPC compute node behind an SSH tunnel or
|
|
22
|
+
JupyterHub proxy.
|
|
23
|
+
|
|
24
|
+
## 2. Overview
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Browser Python server (FastAPI)
|
|
28
|
+
┌─────────────────────────────────┐ ┌─────────────────────────────────────────────────────┐
|
|
29
|
+
│ React UI (panels, time, charts, │ │ API /api/layers/{id}/tiles/{z}/{x}/{y}.bin .png │
|
|
30
|
+
│ gallery) · state (zustand, URL)│ HTTP │ /preview.png /point /timeseries /stats /catalog │
|
|
31
|
+
│ MapLibre GL JS v6 │◄──────►│ │ │
|
|
32
|
+
│ raster source "geoloupe://" │ float │ TileEngine: per tile and per source │
|
|
33
|
+
│ │ │ tiles │ level choice → read budget → resample → composite │
|
|
34
|
+
│ protocol handler: │ │ │ │
|
|
35
|
+
│ fetch float tile → cache │ │ Catalog: datasets → sources (levels) → layers │
|
|
36
|
+
│ → colormap (CPU) → ImageBitmap│ │ │ │
|
|
37
|
+
│ hover values from the cache │ │ Readers: xarray (zarr, netcdf, grib, geotiff, refs) │
|
|
38
|
+
└─────────────────────────────────┘ └─────────────────────┬───────────────────────────────┘
|
|
39
|
+
│ lazy window reads
|
|
40
|
+
┌───────────────────────────┴──────────────────────────┐
|
|
41
|
+
│ native datasets (never modified) + external overviews │
|
|
42
|
+
│ local disk, Lustre, S3/GCS/HTTP (fsspec) │
|
|
43
|
+
└──────────────────────────────────────────────────────┘
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Four ideas carry the design:
|
|
47
|
+
|
|
48
|
+
| Idea | What it gives |
|
|
49
|
+
|---|---|
|
|
50
|
+
| **Float data tiles, coloured in the browser** | Colormap and range changes are instant (no new requests); hover shows the value of the displayed pixel; difference maps are computed in the browser from two cached tiles. |
|
|
51
|
+
| **Per-tile level choice with a read budget** | Every tile reads at most `max_read_cells` grid cells. The engine picks the coarsest level that is still as fine as a screen pixel; if even that is too expensive, the source is skipped at this zoom and the UI says so. |
|
|
52
|
+
| **Per-pixel compositing of several datasets** | A layer can stack sources of different resolution. Per pixel, the finest source with data wins; coarser sources fill the rest. This is the "Google Maps" behaviour, and no existing server does it across datasets. |
|
|
53
|
+
| **External overviews in the native grid** | Optional pyramids (block means at 2×, 4×, … coarser) stored next to, not inside, the source. Values stay traceable to source cells; one pyramid serves any display projection. |
|
|
54
|
+
|
|
55
|
+
## 3. Data model
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Config (YAML) Catalog (runtime)
|
|
59
|
+
───────────── ─────────────────────────────────────────────────────────────
|
|
60
|
+
datasets: ──► DatasetEntry (status ok/error, attrs)
|
|
61
|
+
path / levels └─ Source = one variable of one dataset
|
|
62
|
+
overviews ├─ levels: [Level, …] finest first
|
|
63
|
+
styles │ Level = lazy DataArray + Grid + TimeAxis + origin
|
|
64
|
+
├─ extra_dims (e.g. plev, member)
|
|
65
|
+
layers: ──► Layer = [Source, …] in priority order + style + union of times
|
|
66
|
+
sources (every gridded variable is also its own layer "<dataset>:<variable>")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
* **Grid** ([grid.py](src/geoloupe/grid.py)) describes a rectilinear grid: 1-D cell centres for x and
|
|
70
|
+
y and a CRS. It finds the spatial dimensions (CF `axis`, `standard_name`, common names), the CRS (config
|
|
71
|
+
override, CF `grid_mapping` with `crs_wkt`/`spatial_ref` or CF parameters, or lat/lon coordinates),
|
|
72
|
+
and turns coordinates into *fractional cell positions* (regular axes by arithmetic, irregular ones such
|
|
73
|
+
as Gaussian latitudes by binary search). Longitudes wrap: global grids in 0…360 or −180…180 both
|
|
74
|
+
work, and so do regional grids that cross the antimeridian.
|
|
75
|
+
* **TimeAxis** ([timeaxis.py](src/geoloupe/timeaxis.py)) labels times in ISO 8601 and matches
|
|
76
|
+
requests to the nearest time within a tolerance (default: half a step for monthly or coarser data, at
|
|
77
|
+
most 12 h otherwise, so sparsely sampled daily data never show a neighbouring day). It works in each axis'
|
|
78
|
+
own calendar, so cftime calendars (`noleap`, `360_day`) from climate models are supported.
|
|
79
|
+
* **Levels** come from (a) the dataset itself, if it is a multiscale Zarr group, (b) an explicit list
|
|
80
|
+
of copies at other resolutions (`levels:`), or (c) an external overview store (`overviews:`). Each
|
|
81
|
+
level has its own time axis: overviews may exist for only part of the period, and the engine then
|
|
82
|
+
falls back to finer levels for the other times.
|
|
83
|
+
|
|
84
|
+
### Point observations
|
|
85
|
+
|
|
86
|
+
Point sources ([points.py](src/geoloupe/points.py)) are tables (CSV, Parquet, GeoParquet; long or wide,
|
|
87
|
+
columns detected) or CF timeSeries NetCDF/Zarr. Every variable is a point layer `<source>:<variable>`, and
|
|
88
|
+
any point layer can be drawn over a gridded layer. For a layer and a day, the server returns the
|
|
89
|
+
stations in a compact JSON list, read from Parquet with filter push-down (only that day; hive `year=`
|
|
90
|
+
partitions pruned) and cached per day; time axes of very large tables come from Parquet statistics. The browser draws them as
|
|
91
|
+
circles with the layer's colour scale (a MapLibre `interpolate` expression built from the same lookup
|
|
92
|
+
table), so observation and grid are compared by eye; hover adds the numbers and their difference.
|
|
93
|
+
|
|
94
|
+
## 4. A tile request, step by step
|
|
95
|
+
|
|
96
|
+
`GET /api/layers/tas/tiles/7/67/45.bin?time=1978-07-15`
|
|
97
|
+
|
|
98
|
+
1. **Cache.** The engine's LRU cache (bytes-bounded) is keyed by (layer, tile, time, dims). Hits return
|
|
99
|
+
immediately.
|
|
100
|
+
2. **For each source of the layer, in priority order** (finest first by default):
|
|
101
|
+
1. *Zoom window and time.* Sources outside their configured zoom window, or without data at this
|
|
102
|
+
time (within the tolerance), are skipped and the reason is recorded.
|
|
103
|
+
2. *Level choice.* The screen pixel size is computed in the source's CRS units (lat/lon grids:
|
|
104
|
+
degrees in x and y; projected grids: a pyproj transform at the tile centre). The preferred level
|
|
105
|
+
is the **coarsest level whose cells are no larger than a pixel in both directions**, so no detail
|
|
106
|
+
that the screen could show is lost. Levels without the requested time are not considered.
|
|
107
|
+
3. *Read budget.* The read window (the grid cells the tile covers, clipped to the grid) is computed
|
|
108
|
+
from the cell positions of the tile's pixel edges. If it exceeds `max_read_cells` the next
|
|
109
|
+
coarser level is tried; if none fits, the source is skipped with reason `cost`. A regional
|
|
110
|
+
source is cheap even at zoom 0, because the window is clipped to its extent.
|
|
111
|
+
4. *Read.* One `isel` per contiguous column run (two runs at the antimeridian), through xarray's
|
|
112
|
+
lazy backend arrays — no dask graph. CF decoding (`scale_factor`, `_FillValue`) gives floats with
|
|
113
|
+
NaN for missing data.
|
|
114
|
+
5. *Resample to the tile.*
|
|
115
|
+
* **Lat/lon grids (separable fast path).** Web Mercator is separable, so the cells of each output
|
|
116
|
+
row depend only on latitude and the cells of each column only on longitude. Each pixel takes
|
|
117
|
+
the **mean of the cells whose centres lie inside it**, computed exactly for all pixels at once
|
|
118
|
+
with an integral image (summed-area table, float64) of values and valid counts. When a pixel is
|
|
119
|
+
smaller than a cell, it takes the cell under its centre. `min`/`max` and `nearest` are also
|
|
120
|
+
available; `nearest` is the default for categorical variables (`flag_values`, land cover).
|
|
121
|
+
* **Projected grids** (LAEA, rotated pole, UTM, …): pixel centres are transformed with pyproj and
|
|
122
|
+
the containing cell is taken (nearest). Build overviews for proper means at low zoom.
|
|
123
|
+
6. *Unit transform* (`scale`, `offset` from the layer config).
|
|
124
|
+
7. *Composite.* Pixels still empty take this source's finite values; the source index is recorded
|
|
125
|
+
per pixel. When no pixel is empty, the remaining sources are not read at all.
|
|
126
|
+
3. **Encode.** Body = 256 × 256 float32 values, byte-shuffled, followed by 256 × 256 uint8 source
|
|
127
|
+
indices; gzip; headers `X-GeoLoupe-Sources` (source:level:pixels) and `X-GeoLoupe-Skipped`
|
|
128
|
+
(source:reason). An empty tile is HTTP 204 with the same headers. See
|
|
129
|
+
[docs/data-tiles.md](docs/data-tiles.md).
|
|
130
|
+
|
|
131
|
+
In the browser, MapLibre decides which tiles are visible and requests `geoloupe://<spec>/{z}/{x}/{y}`.
|
|
132
|
+
The protocol handler fetches the data tile (from its own cache if possible), colours it with the
|
|
133
|
+
current colour scale and returns an `ImageBitmap`. A "spec" is an immutable description of what to
|
|
134
|
+
draw (layer, time, dims, colour scale, optional subtrahend); every change makes a new spec and a new
|
|
135
|
+
MapLibre source, added on top of the old one, which is removed only after the new tiles have loaded.
|
|
136
|
+
That double buffering keeps time animation and colour changes free of flicker.
|
|
137
|
+
|
|
138
|
+
### Curvilinear grids
|
|
139
|
+
|
|
140
|
+
A grid with 2-D longitude/latitude keeps its cells as they are: `Grid.locate()` finds the nearest cell
|
|
141
|
+
centre of every pixel with a KD-tree on 3-D unit vectors (no special cases at the date line or the
|
|
142
|
+
poles) and leaves pixels empty beyond ¾ of a cell. The resulting per-pixel (row, col) plan is the same
|
|
143
|
+
as for projected grids, so reading, compositing, queries and exports are shared. Coarser levels are
|
|
144
|
+
views of every 2nd, 4th, … cell, so low zooms stay within the read budget.
|
|
145
|
+
|
|
146
|
+
Small grids (≤ `field_cache_cells`, e.g. ERA5's 1440 × 721 or a 1° ocean model) are read whole
|
|
147
|
+
once per time step and every tile cuts its window from memory. Stores whose chunks hold a whole
|
|
148
|
+
field, or many time steps (CMIP6), would otherwise be decompressed again for every tile.
|
|
149
|
+
|
|
150
|
+
### Mosaics and STAC
|
|
151
|
+
|
|
152
|
+
A `mosaic:` (a list of files) or a `stac:` search is expanded when the catalog opens into hidden
|
|
153
|
+
member datasets, one per file, opened in parallel. Each variable becomes one composite layer over the
|
|
154
|
+
members, in order, so the first file with data at a pixel wins (for STAC: the best item by
|
|
155
|
+
`sortby`). RGB layers record the channel of each source; the engine composites each channel over its
|
|
156
|
+
sources the same way, so true colour works across many scenes.
|
|
157
|
+
|
|
158
|
+
### Vector overlays
|
|
159
|
+
|
|
160
|
+
`vectors:` files are read once per server (pyogrio/GDAL, or pyarrow for GeoParquet), reprojected to
|
|
161
|
+
lon/lat, optionally filtered and simplified, and sent as one GeoJSON document
|
|
162
|
+
(`/api/vectors/{id}.geojson`, gzip, cached by the browser). MapLibre tiles and simplifies it per zoom
|
|
163
|
+
on the client (geojson-vt) and draws it between the data and the station circles. Polygons always get
|
|
164
|
+
a fill layer, transparent when no fill colour is set, so that hovering inside a region finds it. This
|
|
165
|
+
suits overlays up to a few hundred thousand features (limit `max_vector_mb`); larger collections need
|
|
166
|
+
vector tiles (roadmap).
|
|
167
|
+
|
|
168
|
+
### Featured views and previews
|
|
169
|
+
|
|
170
|
+
A configuration can list `views:` (layer, time, place, colour scale, optional comparison). The
|
|
171
|
+
client shows them as a start screen (the gallery) when the URL has no state. A view is exactly the
|
|
172
|
+
state of a shareable link: opening it sets the URL hash, and the normal start-up restores it, so there
|
|
173
|
+
is no second code path. Each card has a preview from `GET /api/layers/{id}/preview.png?center=lon,lat&zoom=z&width&height`:
|
|
174
|
+
a Web Mercator window of any size centred on a point, stitched from the same (cached) tiles as the
|
|
175
|
+
map and coloured once (colormap, RGB stretch or class colours).
|
|
176
|
+
|
|
177
|
+
## 5. What the user sees, and what it means
|
|
178
|
+
|
|
179
|
+
GeoLoupe shows three kinds of numbers. They are deliberately different:
|
|
180
|
+
|
|
181
|
+
| Where | Value | Resolution |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| Map pixel and **hover** read-out | The value of the displayed pixel: a mean of the source cells inside the pixel (or an overview level, itself a block mean) | Display resolution; the read-out names the source and level |
|
|
184
|
+
| **Click** (inspector table, time series) | The stored value of the native grid cell under the point, for every source | Native resolution |
|
|
185
|
+
| **Export** (NetCDF) | The native cells inside the box, with the source attributes | Native resolution |
|
|
186
|
+
|
|
187
|
+
The **provenance badge** at the top of the map always lists the sources and levels on screen, with
|
|
188
|
+
their share of pixels and whether they are native or overview data, and says when a source is hidden
|
|
189
|
+
because it is too expensive at this zoom.
|
|
190
|
+
|
|
191
|
+
Known approximations (documented, not hidden):
|
|
192
|
+
|
|
193
|
+
* Block means on lat/lon grids are **not area-weighted**. Across a block of height Δ (radians) the
|
|
194
|
+
cos(latitude) cell areas vary by about tan(φ)·Δ: 0.9 % for a 0.5° block at 45°, 3 % for a 1° block at
|
|
195
|
+
60°. The effect on the mean is smaller still (it is a weight error, not a value error) and grows toward
|
|
196
|
+
the poles. Overviews use the same unweighted mean; area weighting is on the roadmap.
|
|
197
|
+
* A screen pixel takes the cells whose **centres** fall inside it, not an area-weighted share of
|
|
198
|
+
partly covered cells (that would be conservative remapping). At the edges of fine data this can
|
|
199
|
+
shift a boundary by up to half a cell.
|
|
200
|
+
* **Difference maps** (A − B) subtract two layers sampled on the same screen pixels, each at its own
|
|
201
|
+
best level. They show where A and B differ at the displayed resolution; they are not a regridding.
|
|
202
|
+
* **Overview edges.** When a grid size is not a multiple of the factor, the last overview row/column
|
|
203
|
+
is a block mean of the remaining cells but is placed as if the block were full, up to f/2 native
|
|
204
|
+
cells beyond the grid edge (e.g. 0.67° past 90° S at factor 4 of a 0.25° grid with 522 rows).
|
|
205
|
+
* **Compositing** assumes the sources describe the same quantity in the same units (unit conversion
|
|
206
|
+
is configured per source). It does not blend at the boundary: the fine source simply wins where it
|
|
207
|
+
has data.
|
|
208
|
+
|
|
209
|
+
## 6. Technology choices and alternatives
|
|
210
|
+
|
|
211
|
+
| Component | Choice | Alternatives considered | Why |
|
|
212
|
+
|---|---|---|---|
|
|
213
|
+
| Data access | **xarray** (lazy, no dask by default) with zarr-python 3, h5netcdf/scipy, cfgrib, rioxarray, kerchunk refs | Format-specific readers; GDAL for everything | xarray is the common model of the Pangeo ecosystem, decodes CF metadata and supports every needed format. Dask graphs add overhead for small window reads, so it is only used for multi-file NetCDF. |
|
|
214
|
+
| Tile engine | **Own engine** on numpy + pyproj (~500 lines) | TiTiler / rio-tiler `XarrayReader`; xpublish-tiles | Neither returns float data tiles with automatic level selection, and neither composites several datasets of different resolution per pixel. rio-tiler warps through GDAL for every tile and has no multiscale choice for xarray. xpublish-tiles (the closest match, with excellent grid coverage) renders PNG on the server. The engine copies their best ideas (cost budget, advertised minimum zoom, separable lat/lon path). |
|
|
215
|
+
| Multi-resolution storage | **External overviews**, block means in the native grid, Zarr v3 with GeoZarr `multiscales` + `proj` + `spatial` v0.1 metadata | Web Mercator pyramids (ndpyramid); on-the-fly coarsening only; overviews inside the source store | Native grid keeps values traceable and serves any projection. External stores work with read-only archives and do not duplicate level 0. The GeoZarr conventions let other tools (xpublish-tiles, zarr-layer, OpenLayers GeoZarr, titiler-eopf) read the same pyramid. On-the-fly coarsening alone cannot serve a 900 M-cell grid at zoom 0. |
|
|
216
|
+
| API | **FastAPI** | Starlette alone; xpublish plugin; Django | Typed endpoints, automatic OpenAPI docs at `/api/docs`, the standard in this ecosystem (TiTiler, xpublish). |
|
|
217
|
+
| Tile format | **float32, byte-shuffled, gzip** + uint8 source map | PNG (server colormap); `.npy` (TiTiler); LERC; RGB-packed floats for MapLibre `raster-dem` | Exact values, NaN for missing data, simple to decode, about 2× better compression than unshuffled floats. PNG would make every colour change a refetch and loses the values. |
|
|
218
|
+
| Map library | **MapLibre GL JS v6** + custom protocol | deck.gl `TileLayer` + custom shader; OpenLayers WebGLTile; CesiumJS; Leaflet | MapLibre handles tiling, LOD, caching, globe projection and vector basemaps with labels above the data. The protocol handler is about 80 lines. OpenLayers' WebGL tiles are the strongest float-raster alternative (GPU colormaps, any CRS) and are the documented fallback. deck.gl adds weight for no gain at this stage; Cesium is for full 3D; Leaflet has no WebGL. |
|
|
219
|
+
| Colouring | **CPU** in the protocol handler (≈0.3 ms per tile) | WebGL2 custom layer; `raster-dem` + `color-relief` | Simple, exact NaN transparency, same LUT as the server. A GPU path is on the roadmap if profiling shows a need. |
|
|
220
|
+
| UI | **React 19 + zustand + uPlot** | Vanilla TS; Svelte; Plotly/ECharts | React is what most contributors know; zustand is a 1 kB store; uPlot draws long daily series (13 000+ points) fast and small. |
|
|
221
|
+
| Colormaps | One **LUT file** (matplotlib, cmocean, Crameri) for server and client | matplotlib at runtime | Identical colours everywhere, no matplotlib dependency at run time. |
|
|
222
|
+
|
|
223
|
+
## 7. Performance
|
|
224
|
+
|
|
225
|
+
Measured on one compute node (72-core Arm CPU, data on a busy Lustre file system) with a large private
|
|
226
|
+
store — a 30″ ≈ 1 km global daily dataset (20 880 × 43 200 cells × 13 864 days, uint16, sharded Zarr
|
|
227
|
+
v3) over a 0.25° reanalysis — and with public data read over the internet. Full numbers, conditions
|
|
228
|
+
and the script: [docs/benchmarks.md](docs/benchmarks.md).
|
|
229
|
+
|
|
230
|
+
| Request | Time |
|
|
231
|
+
|---|---|
|
|
232
|
+
| Composite tile, zoom 2–4, 1 km data from overviews | 14–33 ms |
|
|
233
|
+
| Composite tile, zoom 4–5, 1 km data native (≤ 6 M cells read, no overviews) | 76–95 ms |
|
|
234
|
+
| Composite tile, zoom ≥ 6, 1 km data native | 6–14 ms |
|
|
235
|
+
| Tile from the server cache | 3–20 ms (incl. gzip of the response) |
|
|
236
|
+
| Point query (native cells of all sources) | 9–10 ms |
|
|
237
|
+
| One year of daily values at a point, two sources | 4.7 s cold, 0.3–0.5 s warm |
|
|
238
|
+
| Remote Sentinel-2 COG tile over HTTPS (internal overview chosen per zoom) | 0.3–0.9 s cold |
|
|
239
|
+
| Remote ARCO-ERA5 tile over HTTPS (1.3 million hourly steps) | 1.0–1.5 s cold |
|
|
240
|
+
| 16 cold Sentinel-2 RGB tiles (3 remote COGs each), in parallel | 2.0 s (13.3 s with a global read lock) |
|
|
241
|
+
| Opening the whole gallery (ERA5 Zarr, 6 DEM + 4 Sentinel-2 + 1 WorldCover COGs, 21 station CSVs) | 5–15 s |
|
|
242
|
+
| Overview build, two variables, one year of 1 km daily data, 32 workers | 18.5 min, 12 GB |
|
|
243
|
+
|
|
244
|
+
Data tiles are 10–100 kB after gzip (a PNG of the same tile: ~30 kB). The main levers are the chunk
|
|
245
|
+
layout (720 × 720 inner chunks make windows cheap; whole-globe chunks make point series read a full
|
|
246
|
+
field per time step) and overviews for low zoom. A fresh process on a busy parallel file system pays
|
|
247
|
+
tens of seconds for imports and first reads, so the server warms up its main layers in a background
|
|
248
|
+
thread at start.
|
|
249
|
+
|
|
250
|
+
## 8. Deployment
|
|
251
|
+
|
|
252
|
+
* **Local:** `geoloupe serve config.yaml` (uvicorn). The web client is a static bundle inside the
|
|
253
|
+
Python package, so no Node.js is needed to run it.
|
|
254
|
+
* **HPC:** run on a compute node; open through an SSH tunnel, or through jupyter-server-proxy. All
|
|
255
|
+
client URLs are relative, so a path prefix such as `/user/<name>/proxy/8000/` works.
|
|
256
|
+
* **Notebook:** `geoloupe.show({"era5": ds})` starts the server in a background thread and displays
|
|
257
|
+
the map inline (any in-memory or lazily opened xarray Dataset).
|
|
258
|
+
* **Security:** the API is read-only. It serves only datasets named in the configuration; static file
|
|
259
|
+
paths are confined to the bundled client. There is no authentication (bind to `127.0.0.1`, the
|
|
260
|
+
default, or put it behind an authenticating proxy). CORS is open by default for read-only use and is
|
|
261
|
+
configurable.
|
|
262
|
+
|
|
263
|
+
### Opening data at run time
|
|
264
|
+
|
|
265
|
+
`POST /api/datasets` ("Add data") is off unless the server allows remote URLs (`--allow-open`) or
|
|
266
|
+
local folders (`--open-root`); local paths are resolved (symlinks, `..`) and must stay below a
|
|
267
|
+
root. The new dataset is opened by a small catalog of its own and merged into the running one, so
|
|
268
|
+
data already open is not touched; vector and point files go to their registries.
|
|
269
|
+
|
|
270
|
+
## 9. Extension points
|
|
271
|
+
|
|
272
|
+
| To add … | Change |
|
|
273
|
+
|---|---|
|
|
274
|
+
| A file format | a branch in [readers.py](src/geoloupe/readers.py) returning an xarray Dataset |
|
|
275
|
+
| A grid type (curvilinear, UGRID, HEALPix) | a new planner in [engine.py](src/geoloupe/engine.py) next to `plan_separable` / `plan_projected` |
|
|
276
|
+
| An aggregation | `resample()` in the engine and `block_reduce()` in [overviews.py](src/geoloupe/overviews.py) |
|
|
277
|
+
| A colormap | an entry in [scripts/build_colormaps.py](scripts/build_colormaps.py), then regenerate |
|
|
278
|
+
| A tile matrix set (polar stereographic, EPSG:4326 tiles) | `tiling.Tile` (MapLibre v6.15 supports custom projections) |
|
|
279
|
+
| An API endpoint | [api/app.py](src/geoloupe/api/app.py) |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to GeoLoupe. The format follows [Keep a Changelog](https://keepachangelog.com/),
|
|
4
|
+
and versions follow [Semantic Versioning](https://semver.org/) (before 1.0, minor versions may change
|
|
5
|
+
the configuration format; such changes are listed here).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-06
|
|
10
|
+
|
|
11
|
+
First public version.
|
|
12
|
+
|
|
13
|
+
### Data
|
|
14
|
+
- Gridded data from Zarr v2/v3, NetCDF 3/4 (also many files as one), GRIB, GeoTIFF/COG and
|
|
15
|
+
kerchunk references; local, HTTPS, S3 (public buckets need no setup) and GCS.
|
|
16
|
+
- Grids: regular and irregular lat/lon in either longitude convention, projected CRSs, rotated pole,
|
|
17
|
+
and curvilinear grids with 2-D longitude/latitude (ocean models, WRF, CMIP6 native grids).
|
|
18
|
+
- Layers that combine datasets of different resolution per pixel; mosaics of many files; STAC
|
|
19
|
+
searches and items; RGB composites (true and false colour); categorical data with class names
|
|
20
|
+
and colours; fixed-time sources for static fields.
|
|
21
|
+
- Stations and point data (CSV, Parquet, GeoParquet, CF timeSeries NetCDF) as layers and overlays.
|
|
22
|
+
- Vector overlays (GeoJSON, Shapefile, GeoPackage, FlatGeobuf, KML, GML, GeoParquet).
|
|
23
|
+
- Overview pyramids in the native grid (`geoloupe overviews build`, GeoZarr conventions v0.1).
|
|
24
|
+
|
|
25
|
+
### Web client
|
|
26
|
+
- Float data tiles coloured in the browser: instant colour changes, real values on hover and click,
|
|
27
|
+
a badge that names the data and resolution on screen.
|
|
28
|
+
- Time slider and playback, time series, side-by-side and difference comparison, PNG and NetCDF
|
|
29
|
+
export, shareable links, a gallery of featured views, "Add data" (off unless allowed).
|
|
30
|
+
|
|
31
|
+
### Tools
|
|
32
|
+
- `geoloupe serve | gallery | info | demo-data | overviews build`; `geoloupe.show()` in notebooks.
|
|
33
|
+
- The gallery: ERA5, Copernicus DEM, Sentinel-2 (STAC), ESA WorldCover, a CMIP6 climate model ocean,
|
|
34
|
+
NOAA stations and Natural Earth overlays, all read live from public archives.
|
|
35
|
+
|
|
36
|
+
[Unreleased]: https://github.com/salmanmohebi/geoloupe/compare/v0.1.0...HEAD
|
|
37
|
+
[0.1.0]: https://github.com/salmanmohebi/geoloupe/releases/tag/v0.1.0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use GeoLoupe in your research, please cite it as below."
|
|
3
|
+
title: "GeoLoupe: interactive, multi-resolution exploration of large geodata in the browser"
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Mohebi
|
|
7
|
+
given-names: Salman
|
|
8
|
+
license: Apache-2.0
|
|
9
|
+
repository-code: "https://github.com/salmanmohebi/geoloupe"
|
|
10
|
+
keywords:
|
|
11
|
+
- xarray
|
|
12
|
+
- zarr
|
|
13
|
+
- climate data
|
|
14
|
+
- geospatial
|
|
15
|
+
- visualization
|
|
16
|
+
- web map
|
|
17
|
+
version: 0.1.0
|
|
18
|
+
date-released: "2026-11-06"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of conduct
|
|
2
|
+
|
|
3
|
+
GeoLoupe follows the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
|
4
|
+
|
|
5
|
+
In short: be respectful and welcoming, assume good intent, and focus on what is best for the
|
|
6
|
+
community and the science. Harassment and personal attacks are not accepted in issues, pull
|
|
7
|
+
requests, discussions or any other project space.
|
|
8
|
+
|
|
9
|
+
To report unacceptable behaviour, contact the maintainer privately (see the maintainer's GitHub
|
|
10
|
+
profile). Reports are handled confidentially.
|