geokachel 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.
- geokachel-0.1.0/.github/workflows/ci.yml +60 -0
- geokachel-0.1.0/.github/workflows/release.yml +92 -0
- geokachel-0.1.0/.github/workflows/sources.yml +85 -0
- geokachel-0.1.0/.gitignore +9 -0
- geokachel-0.1.0/CONTRIBUTING.md +84 -0
- geokachel-0.1.0/LICENSE +21 -0
- geokachel-0.1.0/NOTICE +49 -0
- geokachel-0.1.0/PKG-INFO +155 -0
- geokachel-0.1.0/README.md +126 -0
- geokachel-0.1.0/geokachel/__init__.py +140 -0
- geokachel-0.1.0/geokachel/addressing.py +224 -0
- geokachel-0.1.0/geokachel/cli.py +161 -0
- geokachel-0.1.0/geokachel/health.py +267 -0
- geokachel-0.1.0/geokachel/net.py +141 -0
- geokachel-0.1.0/geokachel/orthophotos.py +100 -0
- geokachel-0.1.0/geokachel/py.typed +0 -0
- geokachel-0.1.0/geokachel/remote_zip.py +128 -0
- geokachel-0.1.0/geokachel/surface_sources.py +190 -0
- geokachel-0.1.0/geokachel/terrain_sources.py +199 -0
- geokachel-0.1.0/geokachel/tiff.py +312 -0
- geokachel-0.1.0/geokachel/tiff_codec.py +153 -0
- geokachel-0.1.0/geokachel/tile_cache.py +121 -0
- geokachel-0.1.0/geokachel/tile_entries.py +353 -0
- geokachel-0.1.0/geokachel/tile_grid.py +214 -0
- geokachel-0.1.0/geokachel/tile_index.py +120 -0
- geokachel-0.1.0/geokachel/tile_naming.py +134 -0
- geokachel-0.1.0/geokachel/tile_sources.py +131 -0
- geokachel-0.1.0/geokachel/tile_terms.py +60 -0
- geokachel-0.1.0/geokachel/tile_zip.py +121 -0
- geokachel-0.1.0/geokachel/utm.py +119 -0
- geokachel-0.1.0/geokachel/xyz.py +105 -0
- geokachel-0.1.0/pyproject.toml +72 -0
- geokachel-0.1.0/tests/fixtures/rp_dgm1_metalink.meta4 +24 -0
- geokachel-0.1.0/tests/test_health.py +250 -0
- geokachel-0.1.0/tests/test_remote_zip.py +105 -0
- geokachel-0.1.0/tests/test_surface_sources.py +94 -0
- geokachel-0.1.0/tests/test_terrain_sources.py +83 -0
- geokachel-0.1.0/tests/test_tiff.py +199 -0
- geokachel-0.1.0/tests/test_tile_cache.py +78 -0
- geokachel-0.1.0/tests/test_tile_index.py +77 -0
- geokachel-0.1.0/tests/test_tile_sources.py +179 -0
- geokachel-0.1.0/tests/test_tile_zip.py +131 -0
- geokachel-0.1.0/tests/test_utm.py +85 -0
- geokachel-0.1.0/tests/test_xyz.py +171 -0
- geokachel-0.1.0/tests/tiff_builders.py +217 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Every push and pull request. Fast on purpose: no network, no portals.
|
|
2
|
+
#
|
|
3
|
+
# The tests here never touch a state surveying office. Everything they check is
|
|
4
|
+
# built in the test — archives, grids, indexes, malformed headers — because a
|
|
5
|
+
# suite that needs sixteen public portals to be up fails on their maintenance
|
|
6
|
+
# window and teaches people to ignore red. Whether those portals still serve
|
|
7
|
+
# what this registry claims is `sources.yml`, weekly, and it opens an issue
|
|
8
|
+
# instead of failing.
|
|
9
|
+
name: ci
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
push:
|
|
13
|
+
branches: [main]
|
|
14
|
+
pull_request:
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
strategy:
|
|
23
|
+
fail-fast: false
|
|
24
|
+
matrix:
|
|
25
|
+
python: ["3.11", "3.12", "3.13"]
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
28
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
29
|
+
with:
|
|
30
|
+
python-version: ${{ matrix.python }}
|
|
31
|
+
- run: pip install -e ".[dev]"
|
|
32
|
+
- run: ruff check .
|
|
33
|
+
- run: mypy geokachel
|
|
34
|
+
- run: pytest -q
|
|
35
|
+
|
|
36
|
+
build:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
40
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
41
|
+
with:
|
|
42
|
+
python-version: "3.13"
|
|
43
|
+
- run: pip install --upgrade build twine
|
|
44
|
+
- run: python -m build
|
|
45
|
+
# A wheel that claims to be typed and ships no marker silently un-types
|
|
46
|
+
# every consumer. That happened once; it does not happen again.
|
|
47
|
+
- name: The wheel must carry py.typed and both licence files
|
|
48
|
+
run: |
|
|
49
|
+
set -euo pipefail
|
|
50
|
+
python - <<'PY'
|
|
51
|
+
import glob, zipfile, sys
|
|
52
|
+
names = zipfile.ZipFile(sorted(glob.glob("dist/*.whl"))[-1]).namelist()
|
|
53
|
+
need = {"py.typed": any(n.endswith("geokachel/py.typed") for n in names),
|
|
54
|
+
"LICENSE": any(n.endswith("licenses/LICENSE") for n in names),
|
|
55
|
+
"NOTICE": any(n.endswith("licenses/NOTICE") for n in names)}
|
|
56
|
+
for what, there in need.items():
|
|
57
|
+
print(f"{'ok ' if there else 'MISSING'} {what}")
|
|
58
|
+
sys.exit(0 if all(need.values()) else 1)
|
|
59
|
+
PY
|
|
60
|
+
- run: twine check dist/*
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Publish to PyPI when a version tag is pushed.
|
|
2
|
+
#
|
|
3
|
+
# **No token anywhere.** This uses PyPI Trusted Publishing (OIDC): GitHub mints
|
|
4
|
+
# a short-lived identity for this exact workflow in this exact repository, and
|
|
5
|
+
# PyPI trusts that instead of a long-lived secret. Nothing to leak, nothing to
|
|
6
|
+
# rotate, nothing to paste into a terminal.
|
|
7
|
+
#
|
|
8
|
+
# One-time setup on PyPI — https://pypi.org/manage/account/publishing/ :
|
|
9
|
+
#
|
|
10
|
+
# PyPI Project Name geokachel
|
|
11
|
+
# Owner JanderHungrige
|
|
12
|
+
# Repository name geokachel
|
|
13
|
+
# Workflow name release.yml
|
|
14
|
+
# Environment name pypi
|
|
15
|
+
#
|
|
16
|
+
# Then, to release:
|
|
17
|
+
#
|
|
18
|
+
# git tag v0.1.0 && git push origin v0.1.0
|
|
19
|
+
#
|
|
20
|
+
# A PyPI version can never be reused, even after deletion, so the tag is the
|
|
21
|
+
# point of no return. `test` below publishes to TestPyPI first if you want a
|
|
22
|
+
# rehearsal: push a tag ending in `rc1`.
|
|
23
|
+
name: release
|
|
24
|
+
|
|
25
|
+
on:
|
|
26
|
+
push:
|
|
27
|
+
tags:
|
|
28
|
+
- "v[0-9]+.[0-9]+.[0-9]+"
|
|
29
|
+
- "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
|
30
|
+
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
build:
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
39
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
40
|
+
with:
|
|
41
|
+
python-version: "3.13"
|
|
42
|
+
|
|
43
|
+
- name: The tag and the version must agree
|
|
44
|
+
run: |
|
|
45
|
+
set -euo pipefail
|
|
46
|
+
tagged="${GITHUB_REF_NAME#v}"
|
|
47
|
+
declared=$(python -c "import tomllib,pathlib; \
|
|
48
|
+
print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
|
|
49
|
+
echo "tag $tagged, pyproject $declared"
|
|
50
|
+
# A version published under the wrong number cannot be taken back.
|
|
51
|
+
[ "$tagged" = "$declared" ] || {
|
|
52
|
+
echo "::error::tag v$tagged does not match pyproject $declared"; exit 1; }
|
|
53
|
+
|
|
54
|
+
- run: pip install --upgrade build
|
|
55
|
+
- run: python -m build
|
|
56
|
+
|
|
57
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
|
|
62
|
+
rehearse:
|
|
63
|
+
# A tag ending in rc goes to TestPyPI and no further.
|
|
64
|
+
if: contains(github.ref_name, 'rc')
|
|
65
|
+
needs: build
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
environment: testpypi
|
|
68
|
+
permissions:
|
|
69
|
+
id-token: write
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
72
|
+
with:
|
|
73
|
+
name: dist
|
|
74
|
+
path: dist/
|
|
75
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
76
|
+
with:
|
|
77
|
+
repository-url: https://test.pypi.org/legacy/
|
|
78
|
+
|
|
79
|
+
publish:
|
|
80
|
+
if: ${{ !contains(github.ref_name, 'rc') }}
|
|
81
|
+
needs: build
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
environment: pypi
|
|
84
|
+
permissions:
|
|
85
|
+
# The whole mechanism: a short-lived OIDC identity instead of a secret.
|
|
86
|
+
id-token: write
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
89
|
+
with:
|
|
90
|
+
name: dist
|
|
91
|
+
path: dist/
|
|
92
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Are the state surveying offices still serving what the registry says?
|
|
2
|
+
#
|
|
3
|
+
# Deliberately NOT part of the CI suite. A suite that needs
|
|
4
|
+
# sixteen state portals to be up fails on their maintenance window, teaches
|
|
5
|
+
# people to ignore red, and says nothing about this repository's code.
|
|
6
|
+
#
|
|
7
|
+
# So this job does not fail. When something has moved it opens an issue — or
|
|
8
|
+
# adds to the open one — and stays green, because a red build nobody can fix by
|
|
9
|
+
# changing code is a red build everybody learns to scroll past.
|
|
10
|
+
name: sources
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
schedule:
|
|
14
|
+
# Monday, early, off the hour: sixteen public offices, not an API.
|
|
15
|
+
- cron: "17 4 * * 1"
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
issues: write
|
|
21
|
+
|
|
22
|
+
concurrency:
|
|
23
|
+
group: sources
|
|
24
|
+
cancel-in-progress: false
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
ask:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
timeout-minutes: 20
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
32
|
+
|
|
33
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.13"
|
|
36
|
+
|
|
37
|
+
- run: pip install .
|
|
38
|
+
|
|
39
|
+
- name: Ask every source whether it is still there
|
|
40
|
+
id: ask
|
|
41
|
+
env:
|
|
42
|
+
# Say who is asking. These offices publish at their own expense, and
|
|
43
|
+
# a request from a CI runner with no contact in it is rude.
|
|
44
|
+
GEOKACHEL_USER_AGENT: >-
|
|
45
|
+
geokachel/0.1 (+https://github.com/${{ github.repository }};
|
|
46
|
+
weekly source check)
|
|
47
|
+
run: |
|
|
48
|
+
set +e
|
|
49
|
+
geokachel check > report.txt 2>&1
|
|
50
|
+
echo "moved=$?" >> "$GITHUB_OUTPUT"
|
|
51
|
+
cat report.txt
|
|
52
|
+
exit 0
|
|
53
|
+
|
|
54
|
+
- name: Say so, once, where somebody will see it
|
|
55
|
+
if: steps.ask.outputs.moved != '0'
|
|
56
|
+
env:
|
|
57
|
+
GH_TOKEN: ${{ github.token }}
|
|
58
|
+
RUN: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
59
|
+
run: |
|
|
60
|
+
set -euo pipefail
|
|
61
|
+
{
|
|
62
|
+
echo "A source this project reads has changed. It is not a code failure —"
|
|
63
|
+
echo "a state has moved its files, rotated a share token, renamed a coverage"
|
|
64
|
+
echo "or retired a tile."
|
|
65
|
+
echo
|
|
66
|
+
echo "Re-read it from the source and correct the registry and its doc:"
|
|
67
|
+
echo "an entry is a request that was answered, and this one no longer is."
|
|
68
|
+
echo
|
|
69
|
+
echo '```'
|
|
70
|
+
cat report.txt
|
|
71
|
+
echo '```'
|
|
72
|
+
echo
|
|
73
|
+
echo "[The run]($RUN)"
|
|
74
|
+
} > body.md
|
|
75
|
+
|
|
76
|
+
open=$(gh issue list --label sources --state open \
|
|
77
|
+
--json number --jq '.[0].number // empty')
|
|
78
|
+
if [ -n "$open" ]; then
|
|
79
|
+
gh issue comment "$open" --body-file body.md
|
|
80
|
+
else
|
|
81
|
+
gh label create sources --color BFD4F2 \
|
|
82
|
+
--description "A state moved its data" 2>/dev/null || true
|
|
83
|
+
gh issue create --title "A source has moved" \
|
|
84
|
+
--label sources --body-file body.md
|
|
85
|
+
fi
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
The most useful contribution is **a state that is missing, or a state that
|
|
4
|
+
moved** — and both follow one rule.
|
|
5
|
+
|
|
6
|
+
## An entry is a request that was answered
|
|
7
|
+
|
|
8
|
+
Never a portal page that says a thing exists. Before a source goes in the
|
|
9
|
+
registry, somebody has to have asked for a real tile and got real bytes back,
|
|
10
|
+
and the entry records which tile and how many bytes:
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
TileSource(
|
|
14
|
+
name="xx-dgm1", **terms("XX"), product=TileProduct.DGM1, tile_km=1,
|
|
15
|
+
fmt="GeoTIFF",
|
|
16
|
+
_url=lambda e, n: f"https://…/{e}_{n}.tif",
|
|
17
|
+
_name=lambda e, n: f"{e}_{n}",
|
|
18
|
+
vertical_step_m=0.01,
|
|
19
|
+
probed_bytes=2_558_672, # what came back
|
|
20
|
+
probed_tile=(690, 5334), # when asked for this square kilometre
|
|
21
|
+
)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`geokachel check xx-` then asks again, forever.
|
|
25
|
+
|
|
26
|
+
## A credit is a licence obligation
|
|
27
|
+
|
|
28
|
+
`attribution` is required and has no default, on purpose. Put the publisher's
|
|
29
|
+
**exact** wording in it, copied from their own terms page — not a tidied
|
|
30
|
+
version, not a translation. `dl-de/by-2-0` and `CC BY 4.0` both require the
|
|
31
|
+
named credit, and a height shown without it is a height used outside its
|
|
32
|
+
licence.
|
|
33
|
+
|
|
34
|
+
If a licence forbids this kind of use, or charges for it, the state gets **no
|
|
35
|
+
entry**, however a catalogue describes it. That judgement is made once here so
|
|
36
|
+
that nobody downstream has to make it.
|
|
37
|
+
|
|
38
|
+
## Three things that will surprise you
|
|
39
|
+
|
|
40
|
+
They surprised us, each measured rather than assumed:
|
|
41
|
+
|
|
42
|
+
- **A status code is not an answer.** One state's index lists tiles that 404;
|
|
43
|
+
another answers a stale row with HTTP 200 and an HTML apology. `health.py`
|
|
44
|
+
checks the bytes, not the status.
|
|
45
|
+
- **A tile is not always full.** Border tiles come short — 730,232 lines
|
|
46
|
+
instead of a million — with holes inside a row, and several states emit no
|
|
47
|
+
NoData marker at all: a cell nobody surveyed is simply an absent line. So
|
|
48
|
+
values are placed by their own coordinates, never by counting.
|
|
49
|
+
- **Politeness is load-bearing.** Sixteen offices publish this at their own
|
|
50
|
+
expense, with no quota and no contract. Keep the delay, keep the cap, and set
|
|
51
|
+
`GEOKACHEL_USER_AGENT` to something with a contact in it.
|
|
52
|
+
|
|
53
|
+
## Running it
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e ".[dev]"
|
|
57
|
+
ruff check . && mypy geokachel && pytest -q
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**No test may touch the network.** Archives, grids, indexes and malformed
|
|
61
|
+
headers are all built inside the tests. A suite that needs sixteen state
|
|
62
|
+
portals to be up fails on their maintenance window and teaches people to ignore
|
|
63
|
+
red. Whether the portals still serve what the registry claims is a separate,
|
|
64
|
+
weekly job that opens an issue instead of failing a build.
|
|
65
|
+
|
|
66
|
+
## Releasing
|
|
67
|
+
|
|
68
|
+
`pyproject.toml` version, then a tag:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git tag v0.2.0 && git push origin v0.2.0
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Trusted Publishing does the rest — there is no token. A tag ending in `rc1`
|
|
75
|
+
goes to TestPyPI instead. A PyPI version can never be reused, even after
|
|
76
|
+
deletion, so the tag is the point of no return; CI refuses a tag that does not
|
|
77
|
+
match the version in `pyproject.toml`.
|
|
78
|
+
|
|
79
|
+
## Where this came from
|
|
80
|
+
|
|
81
|
+
Extracted from [NinaNatur](https://github.com/JanderHungrige/NinaNatur), a
|
|
82
|
+
garden-planning app that needed to know how much sun a flower bed gets. The
|
|
83
|
+
pre-extraction history, and the design notes behind most of these decisions,
|
|
84
|
+
are in that repository under `.mdd/docs/` — docs 102 to 110.
|
geokachel-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JanderHungrige
|
|
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.
|
geokachel-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
geokachel
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 JanderHungrige. Licensed under the MIT License (see LICENSE).
|
|
5
|
+
|
|
6
|
+
The MIT licence covers **this software**. It does not, and cannot, relicense
|
|
7
|
+
the data this software fetches.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
The data is somebody else's
|
|
11
|
+
---------------------------
|
|
12
|
+
|
|
13
|
+
This library fetches open geodata published by the German state surveying
|
|
14
|
+
authorities, by the Bundesamt für Kartographie und Geodäsie, and by the
|
|
15
|
+
Copernicus programme. Each dataset is licensed by whoever published it —
|
|
16
|
+
`dl-de/by-2-0`, `dl-de/zero-2-0` or `CC BY 4.0` — and **most of them require a
|
|
17
|
+
named credit wherever the data is shown**.
|
|
18
|
+
|
|
19
|
+
A height displayed without its credit is a height used outside its licence.
|
|
20
|
+
|
|
21
|
+
Every source in this registry carries the exact credit its licence asks for, in
|
|
22
|
+
the words the publisher chose, as a required field on the source itself. It is
|
|
23
|
+
not optional metadata and it is not decoration:
|
|
24
|
+
|
|
25
|
+
from geokachel import ground_tiles_for
|
|
26
|
+
source = ground_tiles_for("BY")
|
|
27
|
+
print(source.attribution)
|
|
28
|
+
# Datenquelle: Bayerische Vermessungsverwaltung – www.geodaten.bayern.de
|
|
29
|
+
|
|
30
|
+
If you show a number this library gave you, show that string with it.
|
|
31
|
+
|
|
32
|
+
A source whose licence forbids this kind of use is not in the registry at all,
|
|
33
|
+
however a catalogue describes it — that check happens once, here, rather than
|
|
34
|
+
in every program that installs this.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Not affiliated
|
|
38
|
+
--------------
|
|
39
|
+
|
|
40
|
+
geokachel is not affiliated with, endorsed by, or produced by any
|
|
41
|
+
Landesvermessungsamt or Landesbetrieb, the Arbeitsgemeinschaft der
|
|
42
|
+
Vermessungsverwaltungen der Länder der Bundesrepublik Deutschland (AdV), the
|
|
43
|
+
Bundesamt für Kartographie und Geodäsie (BKG), the European Space Agency, or
|
|
44
|
+
the Copernicus programme.
|
|
45
|
+
|
|
46
|
+
Where those names and brands appear in this software, they appear because a
|
|
47
|
+
licence requires the publisher to be named as the source of the data — never to
|
|
48
|
+
suggest a relationship. In particular the string "GeoBasis-DE" is the surveying
|
|
49
|
+
authorities' own brand and appears only inside the credit that they require.
|
geokachel-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: geokachel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official German open elevation, surface, building and point-cloud data — one interface, all sixteen Bundesländer, with the credit each licence requires.
|
|
5
|
+
Project-URL: Homepage, https://github.com/JanderHungrige/geokachel
|
|
6
|
+
Project-URL: Source, https://github.com/JanderHungrige/geokachel
|
|
7
|
+
Project-URL: Issues, https://github.com/JanderHungrige/geokachel/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/JanderHungrige/geokachel/releases
|
|
9
|
+
Author: JanderHungrige
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: NOTICE
|
|
13
|
+
Keywords: bundesland,citygml,dem,deutschland,dgm1,dl-de,dom,dsm,dtm,elevation,geodata,geotiff,germany,hoehendaten,laz,lidar,lod2,open-data,opendata,pointcloud,utm
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: defusedxml>=0.7
|
|
22
|
+
Requires-Dist: numpy>=1.26
|
|
23
|
+
Requires-Dist: requests>=2.31
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# geokachel
|
|
31
|
+
|
|
32
|
+
**Official German open elevation, surface, building and point-cloud data — one
|
|
33
|
+
interface, all sixteen Bundesländer, with the credit each licence requires.**
|
|
34
|
+
|
|
35
|
+
Germany publishes superb elevation data and publishes it sixteen different
|
|
36
|
+
ways. Every state runs its own surveying office, and they agree about almost
|
|
37
|
+
nothing: where the UTM zone goes in a filename, whether a tile is one kilometre
|
|
38
|
+
or two, whether a grid starts on an even easting or an odd one, whether you get
|
|
39
|
+
a GeoTIFF or a million lines of text, whether there is a tile to address at all
|
|
40
|
+
or only a twelve-gigabyte regional archive.
|
|
41
|
+
|
|
42
|
+
This is one interface over all of it, plus a record of which requests were
|
|
43
|
+
actually answered, and when.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install geokachel
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from geokachel import ground_tiles_for, read_raster, net
|
|
51
|
+
|
|
52
|
+
source = ground_tiles_for("BY")
|
|
53
|
+
print(source.attribution)
|
|
54
|
+
# Datenquelle: Bayerische Vermessungsverwaltung – www.geodaten.bayern.de
|
|
55
|
+
|
|
56
|
+
url = source.url_for(690, 5334) # UTM32 kilometres, Munich
|
|
57
|
+
raster = read_raster(net.get_bytes(url)) # 1000 x 1000 metres of ground
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## What is in it
|
|
61
|
+
|
|
62
|
+
| | states | what you get |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **Ground** (DGM1) | **16 — all of them** | 1 m terrain, ±10–30 cm |
|
|
65
|
+
| **Surface** (DOM / nDOM / bDOM) | 15 | what stands on it, 0.2–1 m |
|
|
66
|
+
| **Buildings** (LoD2 CityGML) | 13 | measured height *and* surveyed roof shape |
|
|
67
|
+
| **Point clouds** (LAZ) | 7 | the returns themselves, 4–20 pts/m² |
|
|
68
|
+
|
|
69
|
+
Plus Copernicus GLO-30 as a worldwide fallback, and the sixteen state coverage
|
|
70
|
+
services (WCS) where a state runs one.
|
|
71
|
+
|
|
72
|
+
## Three rules it keeps
|
|
73
|
+
|
|
74
|
+
**An entry is a request that was answered**, dated — never a portal page that
|
|
75
|
+
says a thing exists. Every entry records the tile it was verified at and the
|
|
76
|
+
bytes that came back, which is what makes `geokachel check` possible.
|
|
77
|
+
|
|
78
|
+
**A credit is a licence obligation, not a caption.** `dl-de/by-2-0` and
|
|
79
|
+
`CC BY 4.0` both require a named credit. A height displayed without it is a
|
|
80
|
+
height used outside its licence, so `attribution` is a required field on every
|
|
81
|
+
source and carries the publisher's exact wording. A source whose licence
|
|
82
|
+
forbids this kind of use is not in the registry at all. See [NOTICE](NOTICE).
|
|
83
|
+
|
|
84
|
+
**A gap is a gap.** No state borrows its neighbour's ground, and nothing
|
|
85
|
+
unsurveyed is guessed at: it is `NaN`, and it says so.
|
|
86
|
+
|
|
87
|
+
## Three ways a tile has an address
|
|
88
|
+
|
|
89
|
+
Handled behind one function, because a caller should not have to care:
|
|
90
|
+
|
|
91
|
+
- **computed** — the name is arithmetic on two UTM kilometre numbers. Most
|
|
92
|
+
states. A tile's address is a template and two integers, never a lookup.
|
|
93
|
+
- **listed** — the name carries a survey year (2005 in one tile, 2025 in its
|
|
94
|
+
neighbour), so the state's own index is read once. What is taken out of it is
|
|
95
|
+
a *file name*; the scheme, the host and the shape of the address stay ours,
|
|
96
|
+
so a compromised index cannot redirect a fetch.
|
|
97
|
+
- **archived** — the state publishes no tile at all, only 0.5–12.5 GB regional
|
|
98
|
+
archives. A zip keeps its index at its **end**, so one member comes out over
|
|
99
|
+
HTTP range requests: **0.38 % of a 559 MB file**, measured.
|
|
100
|
+
|
|
101
|
+
## Checking that it is all still there
|
|
102
|
+
|
|
103
|
+
States move their files. In the two days this registry was built, four of them
|
|
104
|
+
changed underneath it — one moved host entirely, one still lists tiles that
|
|
105
|
+
404, one carries dead share tokens, and one answers a stale row with **HTTP 200
|
|
106
|
+
and an HTML apology**. A checker that read status codes would have called all
|
|
107
|
+
four healthy.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
geokachel check # every source, a few kB each, ~2 minutes
|
|
111
|
+
geokachel check sn- rp- # just these
|
|
112
|
+
geokachel check --quiet # only what is wrong; exit 1 if anything is
|
|
113
|
+
geokachel credits BY TH # the exact credit those states require
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A source passes only when the bytes are what it claims — `II*` for a TIFF,
|
|
117
|
+
`LASF` for a cloud, a root element for CityGML, numbers for a text grid, a
|
|
118
|
+
directory that still holds the square kilometre it held before.
|
|
119
|
+
|
|
120
|
+
It is deliberately **not** a test suite: one that needs sixteen state portals
|
|
121
|
+
to be up fails on their maintenance window and teaches people to ignore red.
|
|
122
|
+
It is a command with an exit code, meant for a scheduled job.
|
|
123
|
+
|
|
124
|
+
## What it deliberately does not do
|
|
125
|
+
|
|
126
|
+
**It does not choose your coordinate frame.** You get a north-up raster in the
|
|
127
|
+
source's own UTM with `NaN` for unknown. Putting that on your own axes is
|
|
128
|
+
yours, because the right answer differs for a shadow model, a flood model and a
|
|
129
|
+
map — and because UTM grid north is up to 2.3° off true north in Germany, which
|
|
130
|
+
some callers must correct for and others must not.
|
|
131
|
+
|
|
132
|
+
**It does not geocode.** `state=` is a required argument. A library that
|
|
133
|
+
reverse-geocoded every call would rate-limit whoever looped over ten thousand
|
|
134
|
+
plots, and that is not a failure to inflict from inside a dependency.
|
|
135
|
+
|
|
136
|
+
**It does not fetch anything itself.** Every function that needs bytes takes a
|
|
137
|
+
callable. `geokachel.net` is a polite default — a pause between requests, a
|
|
138
|
+
size cap, a User-Agent — and you should set `geokachel.net.USER_AGENT` to
|
|
139
|
+
something with your own contact in it before pulling anything in anger. These
|
|
140
|
+
are public offices publishing at their own expense.
|
|
141
|
+
|
|
142
|
+
## Installing
|
|
143
|
+
|
|
144
|
+
Requires Python 3.11+. Depends on `numpy`, `defusedxml` and `requests` — no
|
|
145
|
+
GDAL, no rasterio, no pyproj. The GeoTIFF reader and the UTM projection are
|
|
146
|
+
both here, because a hundred-megabyte wheel to read a single-band raster is a
|
|
147
|
+
poor trade.
|
|
148
|
+
|
|
149
|
+
## Licence
|
|
150
|
+
|
|
151
|
+
MIT, for the software. The *data* is somebody else's and carries its own terms
|
|
152
|
+
— read [NOTICE](NOTICE) before you publish anything derived from it.
|
|
153
|
+
|
|
154
|
+
Not affiliated with any Landesvermessungsamt, the AdV, the BKG, ESA or
|
|
155
|
+
Copernicus. Their names appear only inside the credits their licences require.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# geokachel
|
|
2
|
+
|
|
3
|
+
**Official German open elevation, surface, building and point-cloud data — one
|
|
4
|
+
interface, all sixteen Bundesländer, with the credit each licence requires.**
|
|
5
|
+
|
|
6
|
+
Germany publishes superb elevation data and publishes it sixteen different
|
|
7
|
+
ways. Every state runs its own surveying office, and they agree about almost
|
|
8
|
+
nothing: where the UTM zone goes in a filename, whether a tile is one kilometre
|
|
9
|
+
or two, whether a grid starts on an even easting or an odd one, whether you get
|
|
10
|
+
a GeoTIFF or a million lines of text, whether there is a tile to address at all
|
|
11
|
+
or only a twelve-gigabyte regional archive.
|
|
12
|
+
|
|
13
|
+
This is one interface over all of it, plus a record of which requests were
|
|
14
|
+
actually answered, and when.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install geokachel
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from geokachel import ground_tiles_for, read_raster, net
|
|
22
|
+
|
|
23
|
+
source = ground_tiles_for("BY")
|
|
24
|
+
print(source.attribution)
|
|
25
|
+
# Datenquelle: Bayerische Vermessungsverwaltung – www.geodaten.bayern.de
|
|
26
|
+
|
|
27
|
+
url = source.url_for(690, 5334) # UTM32 kilometres, Munich
|
|
28
|
+
raster = read_raster(net.get_bytes(url)) # 1000 x 1000 metres of ground
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What is in it
|
|
32
|
+
|
|
33
|
+
| | states | what you get |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| **Ground** (DGM1) | **16 — all of them** | 1 m terrain, ±10–30 cm |
|
|
36
|
+
| **Surface** (DOM / nDOM / bDOM) | 15 | what stands on it, 0.2–1 m |
|
|
37
|
+
| **Buildings** (LoD2 CityGML) | 13 | measured height *and* surveyed roof shape |
|
|
38
|
+
| **Point clouds** (LAZ) | 7 | the returns themselves, 4–20 pts/m² |
|
|
39
|
+
|
|
40
|
+
Plus Copernicus GLO-30 as a worldwide fallback, and the sixteen state coverage
|
|
41
|
+
services (WCS) where a state runs one.
|
|
42
|
+
|
|
43
|
+
## Three rules it keeps
|
|
44
|
+
|
|
45
|
+
**An entry is a request that was answered**, dated — never a portal page that
|
|
46
|
+
says a thing exists. Every entry records the tile it was verified at and the
|
|
47
|
+
bytes that came back, which is what makes `geokachel check` possible.
|
|
48
|
+
|
|
49
|
+
**A credit is a licence obligation, not a caption.** `dl-de/by-2-0` and
|
|
50
|
+
`CC BY 4.0` both require a named credit. A height displayed without it is a
|
|
51
|
+
height used outside its licence, so `attribution` is a required field on every
|
|
52
|
+
source and carries the publisher's exact wording. A source whose licence
|
|
53
|
+
forbids this kind of use is not in the registry at all. See [NOTICE](NOTICE).
|
|
54
|
+
|
|
55
|
+
**A gap is a gap.** No state borrows its neighbour's ground, and nothing
|
|
56
|
+
unsurveyed is guessed at: it is `NaN`, and it says so.
|
|
57
|
+
|
|
58
|
+
## Three ways a tile has an address
|
|
59
|
+
|
|
60
|
+
Handled behind one function, because a caller should not have to care:
|
|
61
|
+
|
|
62
|
+
- **computed** — the name is arithmetic on two UTM kilometre numbers. Most
|
|
63
|
+
states. A tile's address is a template and two integers, never a lookup.
|
|
64
|
+
- **listed** — the name carries a survey year (2005 in one tile, 2025 in its
|
|
65
|
+
neighbour), so the state's own index is read once. What is taken out of it is
|
|
66
|
+
a *file name*; the scheme, the host and the shape of the address stay ours,
|
|
67
|
+
so a compromised index cannot redirect a fetch.
|
|
68
|
+
- **archived** — the state publishes no tile at all, only 0.5–12.5 GB regional
|
|
69
|
+
archives. A zip keeps its index at its **end**, so one member comes out over
|
|
70
|
+
HTTP range requests: **0.38 % of a 559 MB file**, measured.
|
|
71
|
+
|
|
72
|
+
## Checking that it is all still there
|
|
73
|
+
|
|
74
|
+
States move their files. In the two days this registry was built, four of them
|
|
75
|
+
changed underneath it — one moved host entirely, one still lists tiles that
|
|
76
|
+
404, one carries dead share tokens, and one answers a stale row with **HTTP 200
|
|
77
|
+
and an HTML apology**. A checker that read status codes would have called all
|
|
78
|
+
four healthy.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
geokachel check # every source, a few kB each, ~2 minutes
|
|
82
|
+
geokachel check sn- rp- # just these
|
|
83
|
+
geokachel check --quiet # only what is wrong; exit 1 if anything is
|
|
84
|
+
geokachel credits BY TH # the exact credit those states require
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A source passes only when the bytes are what it claims — `II*` for a TIFF,
|
|
88
|
+
`LASF` for a cloud, a root element for CityGML, numbers for a text grid, a
|
|
89
|
+
directory that still holds the square kilometre it held before.
|
|
90
|
+
|
|
91
|
+
It is deliberately **not** a test suite: one that needs sixteen state portals
|
|
92
|
+
to be up fails on their maintenance window and teaches people to ignore red.
|
|
93
|
+
It is a command with an exit code, meant for a scheduled job.
|
|
94
|
+
|
|
95
|
+
## What it deliberately does not do
|
|
96
|
+
|
|
97
|
+
**It does not choose your coordinate frame.** You get a north-up raster in the
|
|
98
|
+
source's own UTM with `NaN` for unknown. Putting that on your own axes is
|
|
99
|
+
yours, because the right answer differs for a shadow model, a flood model and a
|
|
100
|
+
map — and because UTM grid north is up to 2.3° off true north in Germany, which
|
|
101
|
+
some callers must correct for and others must not.
|
|
102
|
+
|
|
103
|
+
**It does not geocode.** `state=` is a required argument. A library that
|
|
104
|
+
reverse-geocoded every call would rate-limit whoever looped over ten thousand
|
|
105
|
+
plots, and that is not a failure to inflict from inside a dependency.
|
|
106
|
+
|
|
107
|
+
**It does not fetch anything itself.** Every function that needs bytes takes a
|
|
108
|
+
callable. `geokachel.net` is a polite default — a pause between requests, a
|
|
109
|
+
size cap, a User-Agent — and you should set `geokachel.net.USER_AGENT` to
|
|
110
|
+
something with your own contact in it before pulling anything in anger. These
|
|
111
|
+
are public offices publishing at their own expense.
|
|
112
|
+
|
|
113
|
+
## Installing
|
|
114
|
+
|
|
115
|
+
Requires Python 3.11+. Depends on `numpy`, `defusedxml` and `requests` — no
|
|
116
|
+
GDAL, no rasterio, no pyproj. The GeoTIFF reader and the UTM projection are
|
|
117
|
+
both here, because a hundred-megabyte wheel to read a single-band raster is a
|
|
118
|
+
poor trade.
|
|
119
|
+
|
|
120
|
+
## Licence
|
|
121
|
+
|
|
122
|
+
MIT, for the software. The *data* is somebody else's and carries its own terms
|
|
123
|
+
— read [NOTICE](NOTICE) before you publish anything derived from it.
|
|
124
|
+
|
|
125
|
+
Not affiliated with any Landesvermessungsamt, the AdV, the BKG, ESA or
|
|
126
|
+
Copernicus. Their names appear only inside the credits their licences require.
|