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.
Files changed (45) hide show
  1. geokachel-0.1.0/.github/workflows/ci.yml +60 -0
  2. geokachel-0.1.0/.github/workflows/release.yml +92 -0
  3. geokachel-0.1.0/.github/workflows/sources.yml +85 -0
  4. geokachel-0.1.0/.gitignore +9 -0
  5. geokachel-0.1.0/CONTRIBUTING.md +84 -0
  6. geokachel-0.1.0/LICENSE +21 -0
  7. geokachel-0.1.0/NOTICE +49 -0
  8. geokachel-0.1.0/PKG-INFO +155 -0
  9. geokachel-0.1.0/README.md +126 -0
  10. geokachel-0.1.0/geokachel/__init__.py +140 -0
  11. geokachel-0.1.0/geokachel/addressing.py +224 -0
  12. geokachel-0.1.0/geokachel/cli.py +161 -0
  13. geokachel-0.1.0/geokachel/health.py +267 -0
  14. geokachel-0.1.0/geokachel/net.py +141 -0
  15. geokachel-0.1.0/geokachel/orthophotos.py +100 -0
  16. geokachel-0.1.0/geokachel/py.typed +0 -0
  17. geokachel-0.1.0/geokachel/remote_zip.py +128 -0
  18. geokachel-0.1.0/geokachel/surface_sources.py +190 -0
  19. geokachel-0.1.0/geokachel/terrain_sources.py +199 -0
  20. geokachel-0.1.0/geokachel/tiff.py +312 -0
  21. geokachel-0.1.0/geokachel/tiff_codec.py +153 -0
  22. geokachel-0.1.0/geokachel/tile_cache.py +121 -0
  23. geokachel-0.1.0/geokachel/tile_entries.py +353 -0
  24. geokachel-0.1.0/geokachel/tile_grid.py +214 -0
  25. geokachel-0.1.0/geokachel/tile_index.py +120 -0
  26. geokachel-0.1.0/geokachel/tile_naming.py +134 -0
  27. geokachel-0.1.0/geokachel/tile_sources.py +131 -0
  28. geokachel-0.1.0/geokachel/tile_terms.py +60 -0
  29. geokachel-0.1.0/geokachel/tile_zip.py +121 -0
  30. geokachel-0.1.0/geokachel/utm.py +119 -0
  31. geokachel-0.1.0/geokachel/xyz.py +105 -0
  32. geokachel-0.1.0/pyproject.toml +72 -0
  33. geokachel-0.1.0/tests/fixtures/rp_dgm1_metalink.meta4 +24 -0
  34. geokachel-0.1.0/tests/test_health.py +250 -0
  35. geokachel-0.1.0/tests/test_remote_zip.py +105 -0
  36. geokachel-0.1.0/tests/test_surface_sources.py +94 -0
  37. geokachel-0.1.0/tests/test_terrain_sources.py +83 -0
  38. geokachel-0.1.0/tests/test_tiff.py +199 -0
  39. geokachel-0.1.0/tests/test_tile_cache.py +78 -0
  40. geokachel-0.1.0/tests/test_tile_index.py +77 -0
  41. geokachel-0.1.0/tests/test_tile_sources.py +179 -0
  42. geokachel-0.1.0/tests/test_tile_zip.py +131 -0
  43. geokachel-0.1.0/tests/test_utm.py +85 -0
  44. geokachel-0.1.0/tests/test_xyz.py +171 -0
  45. 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,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
@@ -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.
@@ -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.
@@ -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.