ausdem 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.
- ausdem-0.1.0/LICENSE +21 -0
- ausdem-0.1.0/PKG-INFO +153 -0
- ausdem-0.1.0/README.md +112 -0
- ausdem-0.1.0/ausdem/__init__.py +27 -0
- ausdem-0.1.0/ausdem/cli.py +76 -0
- ausdem-0.1.0/ausdem/client.py +89 -0
- ausdem-0.1.0/ausdem/core.py +127 -0
- ausdem-0.1.0/ausdem/datasets.py +118 -0
- ausdem-0.1.0/ausdem.egg-info/PKG-INFO +153 -0
- ausdem-0.1.0/ausdem.egg-info/SOURCES.txt +15 -0
- ausdem-0.1.0/ausdem.egg-info/dependency_links.txt +1 -0
- ausdem-0.1.0/ausdem.egg-info/entry_points.txt +2 -0
- ausdem-0.1.0/ausdem.egg-info/requires.txt +15 -0
- ausdem-0.1.0/ausdem.egg-info/top_level.txt +1 -0
- ausdem-0.1.0/pyproject.toml +59 -0
- ausdem-0.1.0/setup.cfg +4 -0
- ausdem-0.1.0/tests/test_ausdem.py +71 -0
ausdem-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sia Ghelichkhan
|
|
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.
|
ausdem-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ausdem
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia WCS services as xarray arrays.
|
|
5
|
+
Author-email: Sia Ghelichkhan <ghelichkhani.siavash@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/g-adopt/ausdem
|
|
8
|
+
Project-URL: Repository, https://github.com/g-adopt/ausdem
|
|
9
|
+
Project-URL: Issues, https://github.com/g-adopt/ausdem/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/g-adopt/ausdem/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Data source, https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data
|
|
12
|
+
Keywords: DEM,elevation,SRTM,LiDAR,Australia,Geoscience Australia,WCS
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: requests>=2.25
|
|
28
|
+
Requires-Dist: numpy
|
|
29
|
+
Requires-Dist: xarray
|
|
30
|
+
Requires-Dist: rioxarray
|
|
31
|
+
Requires-Dist: rasterio
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest; extra == "test"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest; extra == "dev"
|
|
36
|
+
Requires-Dist: build; extra == "dev"
|
|
37
|
+
Requires-Dist: twine; extra == "dev"
|
|
38
|
+
Requires-Dist: ruff; extra == "dev"
|
|
39
|
+
Requires-Dist: matplotlib; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# ausdem
|
|
43
|
+
|
|
44
|
+
[](https://github.com/g-adopt/ausdem/actions/workflows/test.yml)
|
|
45
|
+
[](https://pypi.org/project/ausdem/)
|
|
46
|
+
[](https://pypi.org/project/ausdem/)
|
|
47
|
+
[](LICENSE)
|
|
48
|
+
|
|
49
|
+
Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia
|
|
50
|
+
straight into Python as georeferenced [xarray](https://docs.xarray.dev) arrays.
|
|
51
|
+
|
|
52
|
+
It talks to GA's public [Web Coverage Services](https://services.ga.gov.au),
|
|
53
|
+
asking only for the bounding box you want rather than downloading whole
|
|
54
|
+
continental tiles. The data and services come from GA's
|
|
55
|
+
[Digital Elevation Data](https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data)
|
|
56
|
+
page.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install ausdem
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Requires `requests`, `numpy`, `xarray`, `rioxarray` and `rasterio` (pulled in
|
|
65
|
+
automatically).
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import ausdem
|
|
71
|
+
|
|
72
|
+
# A small area around Canberra (min_lon, min_lat, max_lon, max_lat)
|
|
73
|
+
dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
|
|
74
|
+
|
|
75
|
+
dem.plot() # it's a normal xarray DataArray
|
|
76
|
+
print(float(dem.max())) # highest point in the box
|
|
77
|
+
|
|
78
|
+
ausdem.save_geotiff(dem, "canberra.tif") # or: dem.rio.to_raster("canberra.tif")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The returned object is an `xarray.DataArray` with dimensions `(y, x)`, a NaN
|
|
82
|
+
no-data mask, and a `.rio` accessor (CRS, transform, `to_raster`). So you get
|
|
83
|
+
the array for analysis and can drop it to a GeoTIFF whenever you like.
|
|
84
|
+
|
|
85
|
+
### Choosing a dataset
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
ausdem.list_datasets()
|
|
89
|
+
# ['lidar_5m', 'srtm_1s_dem', 'srtm_1s_dem_h']
|
|
90
|
+
|
|
91
|
+
dem_h = ausdem.get_dem(bbox, dataset="srtm_1s_dem_h") # hydro-enforced
|
|
92
|
+
lidar = ausdem.get_dem(bbox, dataset="lidar_5m") # 5 m, where surveyed
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| key | product | resolution |
|
|
96
|
+
|-----------------|-------------------------------------------|------------|
|
|
97
|
+
| `srtm_1s_dem` | SRTM 1 Second DEM (bare earth), national | ~30 m |
|
|
98
|
+
| `srtm_1s_dem_h` | SRTM 1 Second DEM-H (hydro-enforced) | ~30 m |
|
|
99
|
+
| `lidar_5m` | LiDAR-derived 5 m DEM (surveyed areas) | 5 m |
|
|
100
|
+
|
|
101
|
+
The smoothed `DEM-S` product is not served over WCS; download it from the
|
|
102
|
+
[ELVIS portal](https://elevation.fsdf.org.au/). The LiDAR DEM only covers
|
|
103
|
+
surveyed areas (coastal zone, Murray-Darling floodplains, population centres);
|
|
104
|
+
requests outside coverage either come back as a no-data (NaN) tile or raise
|
|
105
|
+
`ausdem.WCSError`, depending on the area.
|
|
106
|
+
|
|
107
|
+
Pass `resolution=` (in degrees) to resample, e.g. `resolution=0.001` for a
|
|
108
|
+
coarser, lighter grid.
|
|
109
|
+
|
|
110
|
+
## Command line
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
ausdem 149.0 -35.4 149.1 -35.3 -o canberra.tif
|
|
114
|
+
ausdem 149.0 -35.4 149.1 -35.3 -o canberra_demh.tif -d srtm_1s_dem_h
|
|
115
|
+
ausdem --list
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Notes
|
|
119
|
+
|
|
120
|
+
Coordinates are decimal degrees. SRTM products are served in WGS84
|
|
121
|
+
(EPSG:4326) and the LiDAR product in GDA94 (EPSG:4283); for input bounding
|
|
122
|
+
boxes the two are interchangeable at this scale. Very large requests are
|
|
123
|
+
rejected client-side to avoid pulling huge rasters by accident; tile your area
|
|
124
|
+
or coarsen the resolution if you hit that.
|
|
125
|
+
|
|
126
|
+
## Example
|
|
127
|
+
|
|
128
|
+
There is a small standalone script in [`examples/`](examples/) that fetches a
|
|
129
|
+
DEM and plots it:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python examples/plot_dem.py # a box near Canberra, writes dem.png
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone https://github.com/g-adopt/ausdem
|
|
139
|
+
cd ausdem
|
|
140
|
+
pip install -e ".[dev]"
|
|
141
|
+
pytest -m "not network" # offline tests
|
|
142
|
+
pytest # include the live GA WCS test
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT, see [LICENSE](LICENSE).
|
|
148
|
+
|
|
149
|
+
## Citation
|
|
150
|
+
|
|
151
|
+
If you use ausdem in your work, please cite it. Release archives are deposited
|
|
152
|
+
on Zenodo and a DOI will be added here after the first release; in the meantime
|
|
153
|
+
see [CITATION.cff](CITATION.cff).
|
ausdem-0.1.0/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# ausdem
|
|
2
|
+
|
|
3
|
+
[](https://github.com/g-adopt/ausdem/actions/workflows/test.yml)
|
|
4
|
+
[](https://pypi.org/project/ausdem/)
|
|
5
|
+
[](https://pypi.org/project/ausdem/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia
|
|
9
|
+
straight into Python as georeferenced [xarray](https://docs.xarray.dev) arrays.
|
|
10
|
+
|
|
11
|
+
It talks to GA's public [Web Coverage Services](https://services.ga.gov.au),
|
|
12
|
+
asking only for the bounding box you want rather than downloading whole
|
|
13
|
+
continental tiles. The data and services come from GA's
|
|
14
|
+
[Digital Elevation Data](https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data)
|
|
15
|
+
page.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install ausdem
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires `requests`, `numpy`, `xarray`, `rioxarray` and `rasterio` (pulled in
|
|
24
|
+
automatically).
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import ausdem
|
|
30
|
+
|
|
31
|
+
# A small area around Canberra (min_lon, min_lat, max_lon, max_lat)
|
|
32
|
+
dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
|
|
33
|
+
|
|
34
|
+
dem.plot() # it's a normal xarray DataArray
|
|
35
|
+
print(float(dem.max())) # highest point in the box
|
|
36
|
+
|
|
37
|
+
ausdem.save_geotiff(dem, "canberra.tif") # or: dem.rio.to_raster("canberra.tif")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The returned object is an `xarray.DataArray` with dimensions `(y, x)`, a NaN
|
|
41
|
+
no-data mask, and a `.rio` accessor (CRS, transform, `to_raster`). So you get
|
|
42
|
+
the array for analysis and can drop it to a GeoTIFF whenever you like.
|
|
43
|
+
|
|
44
|
+
### Choosing a dataset
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
ausdem.list_datasets()
|
|
48
|
+
# ['lidar_5m', 'srtm_1s_dem', 'srtm_1s_dem_h']
|
|
49
|
+
|
|
50
|
+
dem_h = ausdem.get_dem(bbox, dataset="srtm_1s_dem_h") # hydro-enforced
|
|
51
|
+
lidar = ausdem.get_dem(bbox, dataset="lidar_5m") # 5 m, where surveyed
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| key | product | resolution |
|
|
55
|
+
|-----------------|-------------------------------------------|------------|
|
|
56
|
+
| `srtm_1s_dem` | SRTM 1 Second DEM (bare earth), national | ~30 m |
|
|
57
|
+
| `srtm_1s_dem_h` | SRTM 1 Second DEM-H (hydro-enforced) | ~30 m |
|
|
58
|
+
| `lidar_5m` | LiDAR-derived 5 m DEM (surveyed areas) | 5 m |
|
|
59
|
+
|
|
60
|
+
The smoothed `DEM-S` product is not served over WCS; download it from the
|
|
61
|
+
[ELVIS portal](https://elevation.fsdf.org.au/). The LiDAR DEM only covers
|
|
62
|
+
surveyed areas (coastal zone, Murray-Darling floodplains, population centres);
|
|
63
|
+
requests outside coverage either come back as a no-data (NaN) tile or raise
|
|
64
|
+
`ausdem.WCSError`, depending on the area.
|
|
65
|
+
|
|
66
|
+
Pass `resolution=` (in degrees) to resample, e.g. `resolution=0.001` for a
|
|
67
|
+
coarser, lighter grid.
|
|
68
|
+
|
|
69
|
+
## Command line
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
ausdem 149.0 -35.4 149.1 -35.3 -o canberra.tif
|
|
73
|
+
ausdem 149.0 -35.4 149.1 -35.3 -o canberra_demh.tif -d srtm_1s_dem_h
|
|
74
|
+
ausdem --list
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Notes
|
|
78
|
+
|
|
79
|
+
Coordinates are decimal degrees. SRTM products are served in WGS84
|
|
80
|
+
(EPSG:4326) and the LiDAR product in GDA94 (EPSG:4283); for input bounding
|
|
81
|
+
boxes the two are interchangeable at this scale. Very large requests are
|
|
82
|
+
rejected client-side to avoid pulling huge rasters by accident; tile your area
|
|
83
|
+
or coarsen the resolution if you hit that.
|
|
84
|
+
|
|
85
|
+
## Example
|
|
86
|
+
|
|
87
|
+
There is a small standalone script in [`examples/`](examples/) that fetches a
|
|
88
|
+
DEM and plots it:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python examples/plot_dem.py # a box near Canberra, writes dem.png
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/g-adopt/ausdem
|
|
98
|
+
cd ausdem
|
|
99
|
+
pip install -e ".[dev]"
|
|
100
|
+
pytest -m "not network" # offline tests
|
|
101
|
+
pytest # include the live GA WCS test
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT, see [LICENSE](LICENSE).
|
|
107
|
+
|
|
108
|
+
## Citation
|
|
109
|
+
|
|
110
|
+
If you use ausdem in your work, please cite it. Release archives are deposited
|
|
111
|
+
on Zenodo and a DOI will be added here after the first release; in the meantime
|
|
112
|
+
see [CITATION.cff](CITATION.cff).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""ausdem: fetch Australian DEM data from Geoscience Australia WCS services.
|
|
2
|
+
|
|
3
|
+
Quick start
|
|
4
|
+
-----------
|
|
5
|
+
>>> import ausdem
|
|
6
|
+
>>> dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3)) # around Canberra
|
|
7
|
+
>>> ausdem.save_geotiff(dem, "canberra.tif")
|
|
8
|
+
|
|
9
|
+
See :func:`ausdem.list_datasets` for the available DEM products.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .client import WCSError
|
|
13
|
+
from .core import get_dem, save_geotiff
|
|
14
|
+
from .datasets import DATASETS, DEFAULT_DATASET, get_dataset, list_datasets
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"get_dem",
|
|
20
|
+
"save_geotiff",
|
|
21
|
+
"list_datasets",
|
|
22
|
+
"get_dataset",
|
|
23
|
+
"DATASETS",
|
|
24
|
+
"DEFAULT_DATASET",
|
|
25
|
+
"WCSError",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Command-line interface: ``ausdem`` downloads a DEM to a GeoTIFF."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .core import get_dem, save_geotiff
|
|
9
|
+
from .datasets import DATASETS, DEFAULT_DATASET, list_datasets
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
p = argparse.ArgumentParser(
|
|
14
|
+
prog="ausdem",
|
|
15
|
+
description="Fetch Australian DEM data from Geoscience Australia.",
|
|
16
|
+
)
|
|
17
|
+
p.add_argument(
|
|
18
|
+
"bbox",
|
|
19
|
+
nargs=4,
|
|
20
|
+
type=float,
|
|
21
|
+
metavar=("MIN_LON", "MIN_LAT", "MAX_LON", "MAX_LAT"),
|
|
22
|
+
help="Bounding box in decimal degrees.",
|
|
23
|
+
)
|
|
24
|
+
p.add_argument("-o", "--output", required=True, help="Output GeoTIFF path.")
|
|
25
|
+
p.add_argument(
|
|
26
|
+
"-d",
|
|
27
|
+
"--dataset",
|
|
28
|
+
default=DEFAULT_DATASET,
|
|
29
|
+
choices=list_datasets(),
|
|
30
|
+
help=f"DEM product (default: {DEFAULT_DATASET}).",
|
|
31
|
+
)
|
|
32
|
+
p.add_argument(
|
|
33
|
+
"-r",
|
|
34
|
+
"--resolution",
|
|
35
|
+
type=float,
|
|
36
|
+
default=None,
|
|
37
|
+
help="Output pixel size in degrees (default: dataset native).",
|
|
38
|
+
)
|
|
39
|
+
p.add_argument(
|
|
40
|
+
"--list",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="List available datasets and exit.",
|
|
43
|
+
)
|
|
44
|
+
return p
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main(argv: list[str] | None = None) -> int:
|
|
48
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
49
|
+
|
|
50
|
+
if "--list" in argv:
|
|
51
|
+
for key in list_datasets():
|
|
52
|
+
print(f"{key:16s} {DATASETS[key].title}")
|
|
53
|
+
print(f"{'':16s} {DATASETS[key].description}")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
args = _build_parser().parse_args(argv)
|
|
57
|
+
try:
|
|
58
|
+
dem = get_dem(
|
|
59
|
+
tuple(args.bbox),
|
|
60
|
+
dataset=args.dataset,
|
|
61
|
+
resolution=args.resolution,
|
|
62
|
+
)
|
|
63
|
+
save_geotiff(dem, args.output)
|
|
64
|
+
except Exception as exc: # surface a clean message, not a traceback
|
|
65
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
print(
|
|
69
|
+
f"wrote {args.output} [{args.dataset}] "
|
|
70
|
+
f"shape={tuple(dem.sizes[d] for d in ('y', 'x'))}"
|
|
71
|
+
)
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Low-level WCS GetCoverage client for the Geoscience Australia DEM services.
|
|
2
|
+
|
|
3
|
+
The GA services are ArcGIS MapServer WCS endpoints. WCS 2.0.1 GetCoverage is
|
|
4
|
+
unreliable on these endpoints, but WCS 1.0.0 ``GetCoverage`` returns a clean
|
|
5
|
+
32-bit float GeoTIFF, so that is what we use here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Tuple
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from .datasets import Dataset
|
|
15
|
+
|
|
16
|
+
# ArcGIS returns an HTML/XML error body (not a TIFF) when a request is invalid
|
|
17
|
+
# or falls entirely outside the data extent.
|
|
18
|
+
_TIFF_MAGIC = (b"II*\x00", b"MM\x00*")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WCSError(RuntimeError):
|
|
22
|
+
"""Raised when the WCS service returns an error instead of a coverage."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_error(content: bytes) -> str:
|
|
26
|
+
"""Extract a human-readable message from an ArcGIS error body."""
|
|
27
|
+
text = content.decode("utf-8", errors="replace")
|
|
28
|
+
# Strip tags so both the ArcGIS HTML and OGC XML errors read cleanly.
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
text = re.sub(r"<[^>]+>", " ", text)
|
|
32
|
+
text = " ".join(text.split())
|
|
33
|
+
return text[:500] if text else "empty response"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_coverage(
|
|
37
|
+
dataset: Dataset,
|
|
38
|
+
bbox: Tuple[float, float, float, float],
|
|
39
|
+
res_x: float,
|
|
40
|
+
res_y: float,
|
|
41
|
+
timeout: float = 120.0,
|
|
42
|
+
session: requests.Session | None = None,
|
|
43
|
+
) -> bytes:
|
|
44
|
+
"""Issue a WCS 1.0.0 GetCoverage request and return GeoTIFF bytes.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
dataset:
|
|
49
|
+
The :class:`~ausdem.datasets.Dataset` to query.
|
|
50
|
+
bbox:
|
|
51
|
+
``(min_lon, min_lat, max_lon, max_lat)`` in the dataset's request CRS.
|
|
52
|
+
res_x, res_y:
|
|
53
|
+
Output pixel size in CRS units (degrees).
|
|
54
|
+
timeout:
|
|
55
|
+
Per-request timeout in seconds.
|
|
56
|
+
session:
|
|
57
|
+
Optional :class:`requests.Session` for connection reuse.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
bytes
|
|
62
|
+
The raw GeoTIFF payload.
|
|
63
|
+
"""
|
|
64
|
+
min_x, min_y, max_x, max_y = bbox
|
|
65
|
+
params = {
|
|
66
|
+
"service": "WCS",
|
|
67
|
+
"version": "1.0.0",
|
|
68
|
+
"request": "GetCoverage",
|
|
69
|
+
"coverage": dataset.coverage,
|
|
70
|
+
"CRS": dataset.crs,
|
|
71
|
+
"RESPONSE_CRS": dataset.crs,
|
|
72
|
+
"BBOX": f"{min_x},{min_y},{max_x},{max_y}",
|
|
73
|
+
"RESX": repr(res_x),
|
|
74
|
+
"RESY": repr(res_y),
|
|
75
|
+
"FORMAT": "GeoTIFF",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
http = session or requests
|
|
79
|
+
resp = http.get(dataset.url, params=params, timeout=timeout)
|
|
80
|
+
resp.raise_for_status()
|
|
81
|
+
|
|
82
|
+
content = resp.content
|
|
83
|
+
if not content.startswith(_TIFF_MAGIC):
|
|
84
|
+
raise WCSError(
|
|
85
|
+
f"{dataset.key}: service did not return a GeoTIFF. "
|
|
86
|
+
f"This usually means the bounding box is outside the data extent "
|
|
87
|
+
f"or too large. Service said: {_parse_error(content)}"
|
|
88
|
+
)
|
|
89
|
+
return content
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""High-level API: fetch a DEM as an xarray DataArray and save it as GeoTIFF."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import rioxarray # noqa: F401 (registers the .rio accessor on xarray objects)
|
|
10
|
+
import xarray as xr
|
|
11
|
+
|
|
12
|
+
from .client import get_coverage
|
|
13
|
+
from .datasets import DEFAULT_DATASET, get_dataset, list_datasets # noqa: F401
|
|
14
|
+
|
|
15
|
+
# Values at or below this magnitude are treated as the float "no data" sentinel
|
|
16
|
+
# that the GA services use for sea / outside-coverage pixels.
|
|
17
|
+
_NODATA_THRESHOLD = -1e30
|
|
18
|
+
|
|
19
|
+
# Refuse absurdly large requests by default so a typo in the bbox does not pull
|
|
20
|
+
# hundreds of megapixels off the service.
|
|
21
|
+
_MAX_PIXELS = 25_000_000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _open_geotiff(content: bytes, mask: bool) -> xr.DataArray:
|
|
25
|
+
"""Decode GeoTIFF bytes into an in-memory, georeferenced DataArray."""
|
|
26
|
+
from rasterio.io import MemoryFile
|
|
27
|
+
|
|
28
|
+
with MemoryFile(content) as memfile:
|
|
29
|
+
with memfile.open() as src:
|
|
30
|
+
da = rioxarray.open_rasterio(src).load()
|
|
31
|
+
|
|
32
|
+
# Collapse the singleton band dimension: a DEM is a single 2-D surface.
|
|
33
|
+
if "band" in da.dims and da.sizes["band"] == 1:
|
|
34
|
+
da = da.squeeze("band", drop=True)
|
|
35
|
+
|
|
36
|
+
da = da.astype("float32")
|
|
37
|
+
if mask:
|
|
38
|
+
sentinel = (da <= _NODATA_THRESHOLD) | da.isnull()
|
|
39
|
+
da = da.where(~sentinel)
|
|
40
|
+
da.rio.write_nodata(np.nan, inplace=True)
|
|
41
|
+
da.name = "elevation"
|
|
42
|
+
da.attrs.setdefault("units", "m")
|
|
43
|
+
return da
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_dem(
|
|
47
|
+
bbox: Tuple[float, float, float, float],
|
|
48
|
+
dataset: str = DEFAULT_DATASET,
|
|
49
|
+
resolution: float | Tuple[float, float] | None = None,
|
|
50
|
+
mask: bool = True,
|
|
51
|
+
timeout: float = 120.0,
|
|
52
|
+
) -> xr.DataArray:
|
|
53
|
+
"""Fetch a DEM for ``bbox`` and return it as an xarray DataArray.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
bbox:
|
|
58
|
+
``(min_lon, min_lat, max_lon, max_lat)`` in decimal degrees.
|
|
59
|
+
dataset:
|
|
60
|
+
Dataset key. One of :func:`ausdem.list_datasets` (default
|
|
61
|
+
``"srtm_1s_dem"``). Use ``"srtm_1s_dem_h"`` for the hydrologically
|
|
62
|
+
enforced surface or ``"lidar_5m"`` for the 5 m LiDAR DEM where covered.
|
|
63
|
+
resolution:
|
|
64
|
+
Output pixel size in degrees. A single float sets both axes; a
|
|
65
|
+
``(res_x, res_y)`` tuple sets them independently. Defaults to the
|
|
66
|
+
dataset's native resolution.
|
|
67
|
+
mask:
|
|
68
|
+
If True (default), replace the service's no-data sentinel with NaN.
|
|
69
|
+
timeout:
|
|
70
|
+
Per-request timeout in seconds.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
xarray.DataArray
|
|
75
|
+
A georeferenced 2-D array (``y``, ``x``) with a ``.rio`` accessor.
|
|
76
|
+
Save it with :func:`save_geotiff` or ``da.rio.to_raster("out.tif")``.
|
|
77
|
+
|
|
78
|
+
Examples
|
|
79
|
+
--------
|
|
80
|
+
>>> import ausdem
|
|
81
|
+
>>> dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
|
|
82
|
+
>>> dem.shape
|
|
83
|
+
(360, 360)
|
|
84
|
+
>>> ausdem.save_geotiff(dem, "canberra.tif")
|
|
85
|
+
"""
|
|
86
|
+
ds = get_dataset(dataset)
|
|
87
|
+
|
|
88
|
+
min_lon, min_lat, max_lon, max_lat = bbox
|
|
89
|
+
if min_lon >= max_lon or min_lat >= max_lat:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"bbox must be (min_lon, min_lat, max_lon, max_lat) with "
|
|
92
|
+
f"min < max; got {bbox!r}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if resolution is None:
|
|
96
|
+
res_x, res_y = ds.res_x, ds.res_y
|
|
97
|
+
elif isinstance(resolution, (int, float)):
|
|
98
|
+
res_x = res_y = float(resolution)
|
|
99
|
+
else:
|
|
100
|
+
res_x, res_y = (float(v) for v in resolution)
|
|
101
|
+
if res_x <= 0 or res_y <= 0:
|
|
102
|
+
raise ValueError(f"resolution must be positive; got {resolution!r}")
|
|
103
|
+
|
|
104
|
+
n_pixels = math.ceil((max_lon - min_lon) / res_x) * math.ceil(
|
|
105
|
+
(max_lat - min_lat) / res_y
|
|
106
|
+
)
|
|
107
|
+
if n_pixels > _MAX_PIXELS:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Request would be ~{n_pixels:,} pixels (> {_MAX_PIXELS:,}). "
|
|
110
|
+
"Use a smaller bbox or a coarser `resolution`."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
content = get_coverage(ds, bbox, res_x, res_y, timeout=timeout)
|
|
114
|
+
da = _open_geotiff(content, mask=mask)
|
|
115
|
+
da.attrs["dataset"] = ds.key
|
|
116
|
+
da.attrs["source"] = ds.title
|
|
117
|
+
da.attrs["service_url"] = ds.url
|
|
118
|
+
return da
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def save_geotiff(da: xr.DataArray, path: str) -> str:
|
|
122
|
+
"""Write a DataArray returned by :func:`get_dem` to a GeoTIFF file.
|
|
123
|
+
|
|
124
|
+
Returns the path written, so it can be used inline.
|
|
125
|
+
"""
|
|
126
|
+
da.rio.to_raster(path)
|
|
127
|
+
return path
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Registry of Geoscience Australia DEM datasets exposed over WCS.
|
|
2
|
+
|
|
3
|
+
Each entry describes a Web Coverage Service (WCS) endpoint published by
|
|
4
|
+
Geoscience Australia together with the metadata needed to issue a
|
|
5
|
+
``GetCoverage`` request: the coverage identifier, the coordinate reference
|
|
6
|
+
system the service accepts for requests, and the native ground resolution.
|
|
7
|
+
|
|
8
|
+
The catalogue page these come from is:
|
|
9
|
+
https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
_BASE = "https://services.ga.gov.au/gis/services/{service}/MapServer/WCSServer"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Dataset:
|
|
21
|
+
"""Description of a single GA DEM coverage served over WCS."""
|
|
22
|
+
|
|
23
|
+
key: str
|
|
24
|
+
title: str
|
|
25
|
+
service: str
|
|
26
|
+
#: Coverage identifier used by the WCS 1.0.0 ``coverage`` parameter.
|
|
27
|
+
coverage: str
|
|
28
|
+
#: CRS the service accepts for request BBOX and output (EPSG code).
|
|
29
|
+
crs: str
|
|
30
|
+
#: Native pixel size in CRS units (degrees) along x and y.
|
|
31
|
+
res_x: float
|
|
32
|
+
res_y: float
|
|
33
|
+
description: str
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def url(self) -> str:
|
|
37
|
+
return _BASE.format(service=self.service)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# One arc-second is 1/3600 of a degree.
|
|
41
|
+
_ARCSEC = 1.0 / 3600.0
|
|
42
|
+
|
|
43
|
+
DATASETS = {
|
|
44
|
+
"srtm_1s_dem": Dataset(
|
|
45
|
+
key="srtm_1s_dem",
|
|
46
|
+
title="SRTM-derived 1 Second DEM (bare earth)",
|
|
47
|
+
service="DEM_SRTM_1Second_2024",
|
|
48
|
+
coverage="1",
|
|
49
|
+
crs="EPSG:4326",
|
|
50
|
+
res_x=_ARCSEC,
|
|
51
|
+
res_y=_ARCSEC,
|
|
52
|
+
description=(
|
|
53
|
+
"National ~30 m bare-earth Digital Elevation Model derived from the "
|
|
54
|
+
"February 2000 Shuttle Radar Topography Mission, with vegetation "
|
|
55
|
+
"offsets removed."
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
"srtm_1s_dem_h": Dataset(
|
|
59
|
+
key="srtm_1s_dem_h",
|
|
60
|
+
title="SRTM-derived 1 Second DEM-H (hydrologically enforced)",
|
|
61
|
+
service="DEM_SRTM_1Second_Hydro_Enforced_2024",
|
|
62
|
+
coverage="1",
|
|
63
|
+
crs="EPSG:4326",
|
|
64
|
+
res_x=_ARCSEC,
|
|
65
|
+
res_y=_ARCSEC,
|
|
66
|
+
description=(
|
|
67
|
+
"National ~30 m hydrologically enforced DEM (DEM-H): the smoothed "
|
|
68
|
+
"SRTM DEM conditioned with drainage enforcement so that water flows "
|
|
69
|
+
"consistently downhill. Best choice for hydrological modelling."
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
"lidar_5m": Dataset(
|
|
73
|
+
key="lidar_5m",
|
|
74
|
+
title="LiDAR-derived 5 m DEM of Australia",
|
|
75
|
+
service="DEM_LiDAR_5m_2025",
|
|
76
|
+
coverage="1",
|
|
77
|
+
# This service only accepts GDA94 geographic (EPSG:4283), not 4326.
|
|
78
|
+
crs="EPSG:4283",
|
|
79
|
+
res_x=5.5063478185957097e-05,
|
|
80
|
+
res_y=5.16012325277870332e-05,
|
|
81
|
+
description=(
|
|
82
|
+
"5 m bare-earth DEM compiled from 200+ LiDAR surveys (2001-2015). "
|
|
83
|
+
"Coverage is limited to surveyed areas: the populated coastal zone, "
|
|
84
|
+
"the Murray-Darling floodplains and individual population centres. "
|
|
85
|
+
"Requests outside surveyed areas return no data."
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#: Datasets that exist in the SRTM 1 Second package but are NOT served over WCS
|
|
91
|
+
#: (download-only via the ELVIS portal). Kept here for discoverability.
|
|
92
|
+
DOWNLOAD_ONLY = {
|
|
93
|
+
"srtm_1s_dem_s": (
|
|
94
|
+
"SRTM-derived 1 Second DEM-S (smoothed). Not available as a WCS "
|
|
95
|
+
"coverage; download from the ELVIS portal: https://elevation.fsdf.org.au/"
|
|
96
|
+
),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
DEFAULT_DATASET = "srtm_1s_dem"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_dataset(key: str) -> Dataset:
|
|
103
|
+
"""Return the :class:`Dataset` for ``key`` or raise a helpful error."""
|
|
104
|
+
try:
|
|
105
|
+
return DATASETS[key]
|
|
106
|
+
except KeyError:
|
|
107
|
+
available = ", ".join(sorted(DATASETS))
|
|
108
|
+
hint = ""
|
|
109
|
+
if key in DOWNLOAD_ONLY:
|
|
110
|
+
hint = f"\n'{key}' is download-only: {DOWNLOAD_ONLY[key]}"
|
|
111
|
+
raise KeyError(
|
|
112
|
+
f"Unknown dataset '{key}'. Available WCS datasets: {available}.{hint}"
|
|
113
|
+
) from None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def list_datasets() -> list[str]:
|
|
117
|
+
"""Return the keys of all datasets available over WCS."""
|
|
118
|
+
return sorted(DATASETS)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ausdem
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia WCS services as xarray arrays.
|
|
5
|
+
Author-email: Sia Ghelichkhan <ghelichkhani.siavash@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/g-adopt/ausdem
|
|
8
|
+
Project-URL: Repository, https://github.com/g-adopt/ausdem
|
|
9
|
+
Project-URL: Issues, https://github.com/g-adopt/ausdem/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/g-adopt/ausdem/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Data source, https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data
|
|
12
|
+
Keywords: DEM,elevation,SRTM,LiDAR,Australia,Geoscience Australia,WCS
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: requests>=2.25
|
|
28
|
+
Requires-Dist: numpy
|
|
29
|
+
Requires-Dist: xarray
|
|
30
|
+
Requires-Dist: rioxarray
|
|
31
|
+
Requires-Dist: rasterio
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest; extra == "test"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest; extra == "dev"
|
|
36
|
+
Requires-Dist: build; extra == "dev"
|
|
37
|
+
Requires-Dist: twine; extra == "dev"
|
|
38
|
+
Requires-Dist: ruff; extra == "dev"
|
|
39
|
+
Requires-Dist: matplotlib; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# ausdem
|
|
43
|
+
|
|
44
|
+
[](https://github.com/g-adopt/ausdem/actions/workflows/test.yml)
|
|
45
|
+
[](https://pypi.org/project/ausdem/)
|
|
46
|
+
[](https://pypi.org/project/ausdem/)
|
|
47
|
+
[](LICENSE)
|
|
48
|
+
|
|
49
|
+
Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia
|
|
50
|
+
straight into Python as georeferenced [xarray](https://docs.xarray.dev) arrays.
|
|
51
|
+
|
|
52
|
+
It talks to GA's public [Web Coverage Services](https://services.ga.gov.au),
|
|
53
|
+
asking only for the bounding box you want rather than downloading whole
|
|
54
|
+
continental tiles. The data and services come from GA's
|
|
55
|
+
[Digital Elevation Data](https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data)
|
|
56
|
+
page.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install ausdem
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Requires `requests`, `numpy`, `xarray`, `rioxarray` and `rasterio` (pulled in
|
|
65
|
+
automatically).
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import ausdem
|
|
71
|
+
|
|
72
|
+
# A small area around Canberra (min_lon, min_lat, max_lon, max_lat)
|
|
73
|
+
dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
|
|
74
|
+
|
|
75
|
+
dem.plot() # it's a normal xarray DataArray
|
|
76
|
+
print(float(dem.max())) # highest point in the box
|
|
77
|
+
|
|
78
|
+
ausdem.save_geotiff(dem, "canberra.tif") # or: dem.rio.to_raster("canberra.tif")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The returned object is an `xarray.DataArray` with dimensions `(y, x)`, a NaN
|
|
82
|
+
no-data mask, and a `.rio` accessor (CRS, transform, `to_raster`). So you get
|
|
83
|
+
the array for analysis and can drop it to a GeoTIFF whenever you like.
|
|
84
|
+
|
|
85
|
+
### Choosing a dataset
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
ausdem.list_datasets()
|
|
89
|
+
# ['lidar_5m', 'srtm_1s_dem', 'srtm_1s_dem_h']
|
|
90
|
+
|
|
91
|
+
dem_h = ausdem.get_dem(bbox, dataset="srtm_1s_dem_h") # hydro-enforced
|
|
92
|
+
lidar = ausdem.get_dem(bbox, dataset="lidar_5m") # 5 m, where surveyed
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| key | product | resolution |
|
|
96
|
+
|-----------------|-------------------------------------------|------------|
|
|
97
|
+
| `srtm_1s_dem` | SRTM 1 Second DEM (bare earth), national | ~30 m |
|
|
98
|
+
| `srtm_1s_dem_h` | SRTM 1 Second DEM-H (hydro-enforced) | ~30 m |
|
|
99
|
+
| `lidar_5m` | LiDAR-derived 5 m DEM (surveyed areas) | 5 m |
|
|
100
|
+
|
|
101
|
+
The smoothed `DEM-S` product is not served over WCS; download it from the
|
|
102
|
+
[ELVIS portal](https://elevation.fsdf.org.au/). The LiDAR DEM only covers
|
|
103
|
+
surveyed areas (coastal zone, Murray-Darling floodplains, population centres);
|
|
104
|
+
requests outside coverage either come back as a no-data (NaN) tile or raise
|
|
105
|
+
`ausdem.WCSError`, depending on the area.
|
|
106
|
+
|
|
107
|
+
Pass `resolution=` (in degrees) to resample, e.g. `resolution=0.001` for a
|
|
108
|
+
coarser, lighter grid.
|
|
109
|
+
|
|
110
|
+
## Command line
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
ausdem 149.0 -35.4 149.1 -35.3 -o canberra.tif
|
|
114
|
+
ausdem 149.0 -35.4 149.1 -35.3 -o canberra_demh.tif -d srtm_1s_dem_h
|
|
115
|
+
ausdem --list
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Notes
|
|
119
|
+
|
|
120
|
+
Coordinates are decimal degrees. SRTM products are served in WGS84
|
|
121
|
+
(EPSG:4326) and the LiDAR product in GDA94 (EPSG:4283); for input bounding
|
|
122
|
+
boxes the two are interchangeable at this scale. Very large requests are
|
|
123
|
+
rejected client-side to avoid pulling huge rasters by accident; tile your area
|
|
124
|
+
or coarsen the resolution if you hit that.
|
|
125
|
+
|
|
126
|
+
## Example
|
|
127
|
+
|
|
128
|
+
There is a small standalone script in [`examples/`](examples/) that fetches a
|
|
129
|
+
DEM and plots it:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python examples/plot_dem.py # a box near Canberra, writes dem.png
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone https://github.com/g-adopt/ausdem
|
|
139
|
+
cd ausdem
|
|
140
|
+
pip install -e ".[dev]"
|
|
141
|
+
pytest -m "not network" # offline tests
|
|
142
|
+
pytest # include the live GA WCS test
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT, see [LICENSE](LICENSE).
|
|
148
|
+
|
|
149
|
+
## Citation
|
|
150
|
+
|
|
151
|
+
If you use ausdem in your work, please cite it. Release archives are deposited
|
|
152
|
+
on Zenodo and a DOI will be added here after the first release; in the meantime
|
|
153
|
+
see [CITATION.cff](CITATION.cff).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
ausdem/__init__.py
|
|
5
|
+
ausdem/cli.py
|
|
6
|
+
ausdem/client.py
|
|
7
|
+
ausdem/core.py
|
|
8
|
+
ausdem/datasets.py
|
|
9
|
+
ausdem.egg-info/PKG-INFO
|
|
10
|
+
ausdem.egg-info/SOURCES.txt
|
|
11
|
+
ausdem.egg-info/dependency_links.txt
|
|
12
|
+
ausdem.egg-info/entry_points.txt
|
|
13
|
+
ausdem.egg-info/requires.txt
|
|
14
|
+
ausdem.egg-info/top_level.txt
|
|
15
|
+
tests/test_ausdem.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ausdem
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ausdem"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia WCS services as xarray arrays."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{ name = "Sia Ghelichkhan", email = "ghelichkhani.siavash@gmail.com" }]
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
keywords = ["DEM", "elevation", "SRTM", "LiDAR", "Australia", "Geoscience Australia", "WCS"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"requests>=2.25",
|
|
29
|
+
"numpy",
|
|
30
|
+
"xarray",
|
|
31
|
+
"rioxarray",
|
|
32
|
+
"rasterio",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
test = ["pytest"]
|
|
37
|
+
dev = ["pytest", "build", "twine", "ruff", "matplotlib"]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
ausdem = "ausdem.cli:main"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/g-adopt/ausdem"
|
|
44
|
+
Repository = "https://github.com/g-adopt/ausdem"
|
|
45
|
+
Issues = "https://github.com/g-adopt/ausdem/issues"
|
|
46
|
+
Changelog = "https://github.com/g-adopt/ausdem/blob/main/CHANGELOG.md"
|
|
47
|
+
"Data source" = "https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.dynamic]
|
|
50
|
+
version = { attr = "ausdem.__version__" }
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
include = ["ausdem*"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
markers = ["network: test that hits the live GA WCS service"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 88
|
ausdem-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Tests for ausdem.
|
|
2
|
+
|
|
3
|
+
The network test is marked so it can be skipped offline:
|
|
4
|
+
pytest -m "not network"
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
import ausdem
|
|
11
|
+
from ausdem import core
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_list_datasets():
|
|
15
|
+
keys = ausdem.list_datasets()
|
|
16
|
+
assert "srtm_1s_dem" in keys
|
|
17
|
+
assert "srtm_1s_dem_h" in keys
|
|
18
|
+
assert "lidar_5m" in keys
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_unknown_dataset_hint():
|
|
22
|
+
with pytest.raises(KeyError):
|
|
23
|
+
ausdem.get_dataset("does_not_exist")
|
|
24
|
+
# DEM-S should point the user at the download-only note.
|
|
25
|
+
with pytest.raises(KeyError, match="download-only"):
|
|
26
|
+
ausdem.get_dataset("srtm_1s_dem_s")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_bad_bbox():
|
|
30
|
+
with pytest.raises(ValueError):
|
|
31
|
+
ausdem.get_dem((149.1, -35.4, 149.0, -35.3)) # min_lon > max_lon
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_pixel_guard():
|
|
35
|
+
# Continental bbox at native res blows past the pixel cap before any I/O.
|
|
36
|
+
with pytest.raises(ValueError, match="pixels"):
|
|
37
|
+
ausdem.get_dem((113.0, -44.0, 154.0, -10.0), resolution=0.0002777)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_open_geotiff_masks_sentinel():
|
|
41
|
+
# Build a tiny GeoTIFF in memory with a sentinel value and check masking.
|
|
42
|
+
from rasterio.io import MemoryFile
|
|
43
|
+
from rasterio.transform import from_origin
|
|
44
|
+
|
|
45
|
+
data = np.array([[10.0, -3.4e38], [20.0, 30.0]], dtype="float32")
|
|
46
|
+
with MemoryFile() as mem:
|
|
47
|
+
with mem.open(
|
|
48
|
+
driver="GTiff",
|
|
49
|
+
height=2,
|
|
50
|
+
width=2,
|
|
51
|
+
count=1,
|
|
52
|
+
dtype="float32",
|
|
53
|
+
crs="EPSG:4326",
|
|
54
|
+
transform=from_origin(149.0, -35.0, 0.1, 0.1),
|
|
55
|
+
) as dst:
|
|
56
|
+
dst.write(data, 1)
|
|
57
|
+
content = mem.read()
|
|
58
|
+
|
|
59
|
+
da = core._open_geotiff(content, mask=True)
|
|
60
|
+
assert da.name == "elevation"
|
|
61
|
+
assert bool(np.isnan(da.values).any())
|
|
62
|
+
assert np.nanmax(da.values) == 30.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.network
|
|
66
|
+
def test_get_dem_live():
|
|
67
|
+
dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
|
|
68
|
+
assert dem.dims == ("y", "x")
|
|
69
|
+
assert dem.rio.crs is not None
|
|
70
|
+
assert np.isfinite(dem.values).any()
|
|
71
|
+
assert dem.attrs["dataset"] == "srtm_1s_dem"
|