transformez 0.2.2__tar.gz → 0.3.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 (25) hide show
  1. transformez-0.3.0/PKG-INFO +179 -0
  2. transformez-0.3.0/README.md +132 -0
  3. {transformez-0.2.2 → transformez-0.3.0}/pyproject.toml +34 -6
  4. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/__init__.py +6 -35
  5. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/_version.py +1 -1
  6. transformez-0.3.0/src/transformez/api.py +194 -0
  7. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/cli.py +30 -15
  8. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/definitions.py +4 -3
  9. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/hooks.py +12 -5
  10. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/htdp.py +83 -16
  11. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/modules.py +9 -14
  12. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/spatial.py +39 -39
  13. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/srs.py +19 -3
  14. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/transform.py +51 -6
  15. {transformez-0.2.2 → transformez-0.3.0}/tests/test_cli.py +5 -1
  16. transformez-0.2.2/PKG-INFO +0 -128
  17. transformez-0.2.2/README.md +0 -85
  18. {transformez-0.2.2 → transformez-0.3.0}/AUTHORS.md +0 -0
  19. {transformez-0.2.2 → transformez-0.3.0}/CHANGELOG.md +0 -0
  20. {transformez-0.2.2 → transformez-0.3.0}/CITATION.cff +0 -0
  21. {transformez-0.2.2 → transformez-0.3.0}/CONTRIBUTING.md +0 -0
  22. {transformez-0.2.2 → transformez-0.3.0}/LICENSE +0 -0
  23. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/grid_engine.py +0 -0
  24. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/utils.py +0 -0
  25. {transformez-0.2.2 → transformez-0.3.0}/src/transformez/vdatum.py +0 -0
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: transformez
3
+ Version: 0.3.0
4
+ Summary: A standalone utility for vertical elevation datum transformations.
5
+ Project-URL: Homepage, https://github.com/continuous-dems/transformez
6
+ Project-URL: Issues, https://github.com/continuous-dems/transformez/issues
7
+ Author-email: Matthew Love <matthew.love@colorado.edu>, Christopher Amante <christopher.amante@colorado.edu>, Elliot Lim <elliot.lim@colorado.edu>, Michael MacFerrin <michael.macferrin@colorado.edu>, Matt Fisher <mfisher87@gmail.com>
8
+ Maintainer-email: Matthew Love <matthew.love@colorado.edu>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2010-2026 Regents of the University of Colorado
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: AUTHORS.md
31
+ License-File: LICENSE
32
+ Keywords: Geospatial
33
+ Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Topic :: Scientific/Engineering :: GIS
36
+ Requires-Python: >=3.12
37
+ Requires-Dist: fetchez>=0.5.0
38
+ Requires-Dist: numpy>1.24
39
+ Requires-Dist: pyproj>3.6.1
40
+ Requires-Dist: rasterio>1.4.0
41
+ Requires-Dist: scipy
42
+ Provides-Extra: full
43
+ Requires-Dist: matplotlib; extra == 'full'
44
+ Provides-Extra: preview
45
+ Requires-Dist: matplotlib; extra == 'preview'
46
+ Description-Content-Type: text/markdown
47
+
48
+ <!-- <p align="center"> -->
49
+ <!-- <a href="https://github.com/continuous-dems"> -->
50
+ <!-- <img src="https://github.com/continuous-dems/fetchez/blob/modules/docs/source/_static/continuous_dems_logo.svg" height="80" alt="Continuous DEMs Logo"> -->
51
+ <!-- </a> -->
52
+ <!-- </p> -->
53
+ <h1 align="center">Transformez</h1>
54
+ <p align="center"><strong>Global vertical datum transformations, simplified.</strong></p>
55
+
56
+ <p align="center">
57
+ <a href="https://github.com/continuous-dems/transformez"><img src="https://img.shields.io/badge/version-0.3.0-blue.svg" alt="Version"></a>
58
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
59
+ <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.12+-yellow.svg" alt="Python"></a>
60
+ <a href="https://badge.fury.io/py/transformez"><img src="https://badge.fury.io/py/transformez.svg" alt="PyPI version"></a>
61
+ <a href="https://cudem.zulip.org"><img src="https://img.shields.io/badge/zulip-join_chat-brightgreen.svg" alt="Project Chat"></a>
62
+ </p>
63
+
64
+ **Transformez** is a standalone Python engine for converting geospatial data between vertical datums (e.g., `MLLW` ↔ `NAVD88` ↔ `Ellipsoid`).
65
+
66
+ ---
67
+
68
+ ![Shift Grid Example](docs/images/mllw2nvd.png)
69
+ *(Above: A generated vertical shift grid transforming MLLW to NAVD88)*
70
+
71
+ ## Installation
72
+
73
+ ### Prerequisites: HTDP
74
+ Transformez relies on the NGS Horizontal Time-Dependent Positioning (HTDP) software to perform highly accurate plate tectonic and frame transformations. **You must install this separately.**
75
+
76
+ **For Windows:**
77
+ 1. Download the pre-compiled executable (`htdp.exe`) directly from the [NOAA HTDP page](https://geodesy.noaa.gov/TOOLS/Htdp/Htdp.shtml).
78
+ 2. Place `htdp.exe` in a directory that is in your system's `PATH` (e.g., `C:\Windows\System32` or a custom scripts folder).
79
+
80
+ **For Linux / macOS:**
81
+
82
+ You will need a Fortran compiler (like `gfortran`) to compile the source code.
83
+
84
+ ```bash
85
+ # 1. Download the Fortran source code
86
+ wget https://geodesy.noaa.gov/TOOLS/Htdp/HTDP-download.zip
87
+ unzip HTDP-download.zip
88
+
89
+ # 2. Compile it
90
+ gfortran -o htdp htdp.f
91
+
92
+ # 3. Move it to your PATH
93
+ sudo mv htdp /usr/local/bin/
94
+ ```
95
+
96
+ ### Install Transformez
97
+ Once HTDP is accessible in your terminal, install the python package:
98
+
99
+ ```bash
100
+ pip install transformez
101
+ ```
102
+
103
+ ## Usage
104
+
105
+ **Generate a vertical shift grid for anywhere on Earth.**
106
+
107
+ ```bash
108
+ # Transform MLLW to WGS84 Ellipsoid in Norton Sound, AK
109
+ # (Where NOAA has no coverage!)
110
+ transformez -R -166/-164/63/64 -E 3s \
111
+ --input-datum mllw \
112
+ --output-datum 4979 \
113
+ --output shift_ak.tif
114
+ ```
115
+
116
+ **Transform a raster directly.** Transformez reads the bounds/resolution from the file.
117
+
118
+ ```bash
119
+ transformez --dem input_bathymetry.tif \
120
+ --input-datum "mllw" \
121
+ --output-datum "5703:geoid=geoid12b" \
122
+ --output output_navd88.tif
123
+ ```
124
+
125
+ **Integrate directly into your download pipeline.**
126
+
127
+ ```bash
128
+ # Download GEBCO and shift EGM96 to WGS84 on the fly
129
+ fetchez gebco ... --hook transformez:datum_in=5773,datum_out=4979
130
+ ```
131
+
132
+ ## Python API
133
+
134
+ Transformez provides a high-level API for embedding transformations directly into your Python scripts, Jupyter Notebooks, or automated pipelines.
135
+
136
+ ```python
137
+ import transformez
138
+
139
+ # ---------------------------------------------------------
140
+ # Generate a Shift Grid
141
+ # ---------------------------------------------------------
142
+ # Returns a 2D numpy array. Optionally saves to a file.
143
+ # Requesting "mllw" in India triggers the Global Fallback (FES2014) automatically.
144
+
145
+ shift_array = transformez.generate_grid(
146
+ region=[80, 85, 10, 15], # [West, East, South, North]
147
+ increment="3s", # Grid resolution
148
+ datum_in="mllw",
149
+ datum_out="4979", # WGS84 Ellipsoid
150
+ out_fn="india_shift.tif" # Optional: Save to disk
151
+ )
152
+
153
+ # ---------------------------------------------------------
154
+ # Transform an Existing Raster
155
+ # ---------------------------------------------------------
156
+ # Applies the datum shift directly to a DEM and saves the result.
157
+
158
+ out_file = transformez.transform_raster(
159
+ input_raster="my_dem_mllw.tif",
160
+ datum_in="mllw",
161
+ datum_out="5703:g2012b", # NAVD88 using specific GEOID12B
162
+ output_raster="my_dem_navd88.tif"
163
+ )
164
+ ```
165
+
166
+ ## Supported Datums
167
+
168
+ * **Tidal**: mllw, mhhw, msl, lat
169
+
170
+ * **Ellipsoidal**: 4979 (WGS84), 6319 (NAD83 2011)
171
+
172
+ * **Orthometric**: 5703 (NAVD88), egm2008, egm96
173
+
174
+ * **Geoids**: g2018, g2012b, geoid09, xgeoid20b
175
+
176
+ ## License
177
+
178
+ This project is licensed under the MIT License - see the [LICENSE](https://github.com/ciresdem/transformez/blob/main/LICENSE) file for details.
179
+ Copyright (c) 2010-2026 Regents of the University of Colorado
@@ -0,0 +1,132 @@
1
+ <!-- <p align="center"> -->
2
+ <!-- <a href="https://github.com/continuous-dems"> -->
3
+ <!-- <img src="https://github.com/continuous-dems/fetchez/blob/modules/docs/source/_static/continuous_dems_logo.svg" height="80" alt="Continuous DEMs Logo"> -->
4
+ <!-- </a> -->
5
+ <!-- </p> -->
6
+ <h1 align="center">Transformez</h1>
7
+ <p align="center"><strong>Global vertical datum transformations, simplified.</strong></p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/continuous-dems/transformez"><img src="https://img.shields.io/badge/version-0.3.0-blue.svg" alt="Version"></a>
11
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
12
+ <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.12+-yellow.svg" alt="Python"></a>
13
+ <a href="https://badge.fury.io/py/transformez"><img src="https://badge.fury.io/py/transformez.svg" alt="PyPI version"></a>
14
+ <a href="https://cudem.zulip.org"><img src="https://img.shields.io/badge/zulip-join_chat-brightgreen.svg" alt="Project Chat"></a>
15
+ </p>
16
+
17
+ **Transformez** is a standalone Python engine for converting geospatial data between vertical datums (e.g., `MLLW` ↔ `NAVD88` ↔ `Ellipsoid`).
18
+
19
+ ---
20
+
21
+ ![Shift Grid Example](docs/images/mllw2nvd.png)
22
+ *(Above: A generated vertical shift grid transforming MLLW to NAVD88)*
23
+
24
+ ## Installation
25
+
26
+ ### Prerequisites: HTDP
27
+ Transformez relies on the NGS Horizontal Time-Dependent Positioning (HTDP) software to perform highly accurate plate tectonic and frame transformations. **You must install this separately.**
28
+
29
+ **For Windows:**
30
+ 1. Download the pre-compiled executable (`htdp.exe`) directly from the [NOAA HTDP page](https://geodesy.noaa.gov/TOOLS/Htdp/Htdp.shtml).
31
+ 2. Place `htdp.exe` in a directory that is in your system's `PATH` (e.g., `C:\Windows\System32` or a custom scripts folder).
32
+
33
+ **For Linux / macOS:**
34
+
35
+ You will need a Fortran compiler (like `gfortran`) to compile the source code.
36
+
37
+ ```bash
38
+ # 1. Download the Fortran source code
39
+ wget https://geodesy.noaa.gov/TOOLS/Htdp/HTDP-download.zip
40
+ unzip HTDP-download.zip
41
+
42
+ # 2. Compile it
43
+ gfortran -o htdp htdp.f
44
+
45
+ # 3. Move it to your PATH
46
+ sudo mv htdp /usr/local/bin/
47
+ ```
48
+
49
+ ### Install Transformez
50
+ Once HTDP is accessible in your terminal, install the python package:
51
+
52
+ ```bash
53
+ pip install transformez
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ **Generate a vertical shift grid for anywhere on Earth.**
59
+
60
+ ```bash
61
+ # Transform MLLW to WGS84 Ellipsoid in Norton Sound, AK
62
+ # (Where NOAA has no coverage!)
63
+ transformez -R -166/-164/63/64 -E 3s \
64
+ --input-datum mllw \
65
+ --output-datum 4979 \
66
+ --output shift_ak.tif
67
+ ```
68
+
69
+ **Transform a raster directly.** Transformez reads the bounds/resolution from the file.
70
+
71
+ ```bash
72
+ transformez --dem input_bathymetry.tif \
73
+ --input-datum "mllw" \
74
+ --output-datum "5703:geoid=geoid12b" \
75
+ --output output_navd88.tif
76
+ ```
77
+
78
+ **Integrate directly into your download pipeline.**
79
+
80
+ ```bash
81
+ # Download GEBCO and shift EGM96 to WGS84 on the fly
82
+ fetchez gebco ... --hook transformez:datum_in=5773,datum_out=4979
83
+ ```
84
+
85
+ ## Python API
86
+
87
+ Transformez provides a high-level API for embedding transformations directly into your Python scripts, Jupyter Notebooks, or automated pipelines.
88
+
89
+ ```python
90
+ import transformez
91
+
92
+ # ---------------------------------------------------------
93
+ # Generate a Shift Grid
94
+ # ---------------------------------------------------------
95
+ # Returns a 2D numpy array. Optionally saves to a file.
96
+ # Requesting "mllw" in India triggers the Global Fallback (FES2014) automatically.
97
+
98
+ shift_array = transformez.generate_grid(
99
+ region=[80, 85, 10, 15], # [West, East, South, North]
100
+ increment="3s", # Grid resolution
101
+ datum_in="mllw",
102
+ datum_out="4979", # WGS84 Ellipsoid
103
+ out_fn="india_shift.tif" # Optional: Save to disk
104
+ )
105
+
106
+ # ---------------------------------------------------------
107
+ # Transform an Existing Raster
108
+ # ---------------------------------------------------------
109
+ # Applies the datum shift directly to a DEM and saves the result.
110
+
111
+ out_file = transformez.transform_raster(
112
+ input_raster="my_dem_mllw.tif",
113
+ datum_in="mllw",
114
+ datum_out="5703:g2012b", # NAVD88 using specific GEOID12B
115
+ output_raster="my_dem_navd88.tif"
116
+ )
117
+ ```
118
+
119
+ ## Supported Datums
120
+
121
+ * **Tidal**: mllw, mhhw, msl, lat
122
+
123
+ * **Ellipsoidal**: 4979 (WGS84), 6319 (NAD83 2011)
124
+
125
+ * **Orthometric**: 5703 (NAVD88), egm2008, egm96
126
+
127
+ * **Geoids**: g2018, g2012b, geoid09, xgeoid20b
128
+
129
+ ## License
130
+
131
+ This project is licensed under the MIT License - see the [LICENSE](https://github.com/ciresdem/transformez/blob/main/LICENSE) file for details.
132
+ Copyright (c) 2010-2026 Regents of the University of Colorado
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
  name = 'transformez'
7
7
  description = 'A standalone utility for vertical elevation datum transformations.'
8
8
  readme = "README.md"
9
- requires-python = ">=3.8"
9
+ requires-python = ">=3.12"
10
10
  license = { file = "LICENSE" }
11
11
  dynamic = ["version"]
12
12
  authors = [
@@ -14,6 +14,7 @@ authors = [
14
14
  {name = "Christopher Amante", email = "christopher.amante@colorado.edu"},
15
15
  {name = "Elliot Lim", email = "elliot.lim@colorado.edu"},
16
16
  {name = "Michael MacFerrin", email = "michael.macferrin@colorado.edu"},
17
+ {name = "Matt Fisher", email = "mfisher87@gmail.com"},
17
18
  ]
18
19
  maintainers = [
19
20
  {name = "Matthew Love", email = "matthew.love@colorado.edu"},
@@ -24,22 +25,30 @@ classifiers = [
24
25
  'Topic :: Scientific/Engineering :: GIS',
25
26
  ]
26
27
  dependencies = [
27
- 'numpy<2.0.0',
28
- 'pyproj',
29
- 'rasterio',
30
- 'fetchez>0.3.3',
28
+ 'numpy>1.24',
29
+ 'pyproj>3.6.1',
30
+ 'rasterio>1.4.0',
31
+ 'fetchez>=0.5.0',
31
32
  'scipy',
32
33
  ]
33
34
 
34
35
  keywords = ["Geospatial"]
35
36
 
37
+ [project.optional-dependencies]
38
+ preview = ["matplotlib"]
39
+
40
+ full = ["transformez[preview]"]
41
+
36
42
  [dependency-groups]
43
+ dev = [
44
+ "mypy",
45
+ ]
37
46
  test = [
38
47
  "pytest",
39
48
  "pytest-cov",
40
49
  ]
41
50
 
42
- [project.entry-points."fetchez.plugins"]
51
+ [project.entry-points."fetchez.modules"]
43
52
  transformez = "transformez"
44
53
 
45
54
  [project.scripts]
@@ -73,3 +82,22 @@ exclude = [
73
82
  ".github",
74
83
  ]
75
84
 
85
+ # --- Devtools configuration ---
86
+
87
+ [tool.mypy]
88
+ mypy_path = "src"
89
+ packages = ["transformez"]
90
+ python_version = "3.12"
91
+ disable_error_code = ["import-untyped"]
92
+ # strict = true
93
+
94
+ [[tool.mypy.overrides]]
95
+ module = [
96
+ "matplotlib.*",
97
+ "rasterio.*",
98
+ "scipy.*",
99
+ "pyproj.*",
100
+ "fetchez.*",
101
+ ]
102
+ ignore_missing_imports = true
103
+ follow_untyped_imports = true
@@ -5,19 +5,15 @@
5
5
  transformez
6
6
  ~~~~~~~~~~~~~
7
7
 
8
+ Initialize the API and fetchez extension.
9
+
8
10
  :copyright: (c) 2010-2026 Regents of the University of Colorado
9
11
  :license: MIT, see LICENSE for more details.
10
12
  """
11
13
 
12
- __author__ = "Matthew Love"
13
- __credits__ = "CIRES"
14
-
15
14
  try:
16
15
  from transformez._version import __version__
17
16
  except ImportError:
18
- # Fallback when using the package from source without installing
19
- # in editable mode with pip (nobody should do this):
20
- # <https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs>
21
17
  import warnings
22
18
 
23
19
  warnings.warn(
@@ -29,12 +25,12 @@ except ImportError:
29
25
  __version__ = "dev"
30
26
 
31
27
 
28
+ from .api import generate_grid, transform_raster
29
+
32
30
  import os
33
31
  import glob
34
- # from .hooks import TransformezHook
35
- # from fetchez.hooks.registry import HookRegistry
36
- from fetchez.registry import FetchezRegistry
37
32
 
33
+ # Expose the module for fetchez
38
34
  from .modules import TransformezMod
39
35
 
40
36
  def _find_proj_lib():
@@ -73,29 +69,4 @@ if "PROJ_LIB" in os.environ:
73
69
  if target_proj_lib:
74
70
  os.environ["PROJ_LIB"] = target_proj_lib
75
71
 
76
- def setup_fetchez(registry_cls):
77
- """Called by fetchez when loading plugins.
78
-
79
- Registers modules, hooks, and presets.
80
- """
81
-
82
- registry_cls.register_module(
83
- 'transformez',
84
- TransformezMod,
85
- metadata={
86
- 'desc': 'Generate vertical datum shift grids on-demand.',
87
- "tags": ["vdatum", "transformation", "shift-grid"],
88
- "category": "Tools"
89
- }
90
- )
91
-
92
- # HookRegistry.register_hook(TransformezHook)
93
- # from fetchez.presets import register_global_preset
94
- # register_global_preset(
95
- # name="make-shift-grid",
96
- # help_text="Download datum grids and composite them into a single shift grid.",
97
- # hooks=[
98
- # {"name": "transformez", "args": {}}
99
- # ]
100
- # )
101
- setup_fetchez(FetchezRegistry)
72
+ __all__ = ["generate_grid", "transform_raster", "TransformezMod"]
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.2.2'
4
+ __version__ = VERSION = '0.3.0'
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ transformez.api
6
+ ~~~~~~~~~~~~~~~
7
+ High-level Python Interface for Transformez.
8
+
9
+ Usage:
10
+ import transformez
11
+
12
+ # Generate a shift grid (returns a numpy array)
13
+ shift_array = transformez.generate_grid(
14
+ region=[-90, -89, 29, 30],
15
+ increment="3s",
16
+ datum_in="mllw",
17
+ datum_out="5703"
18
+ )
19
+
20
+ # Transform an existing DEM directly
21
+ out_file = transformez.transform_raster(
22
+ input_raster="my_dem_mllw.tif",
23
+ datum_in="mllw",
24
+ datum_out="5703:g2012b",
25
+ output_raster="my_dem_navd88.tif"
26
+ )
27
+ """
28
+
29
+ import os
30
+ import logging
31
+ import numpy as np
32
+ from typing import List, Union, Optional, Tuple
33
+
34
+ from .transform import VerticalTransform
35
+ from .definitions import Datums
36
+ from .grid_engine import GridWriter, GridEngine
37
+ from fetchez.spatial import parse_region, Region
38
+ from fetchez import utils
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ def _parse_datum(datum_arg: str) -> Tuple[Optional[int], Optional[str]]:
44
+ """Helper to parse compound datum strings (e.g. '5703:g2012b')."""
45
+
46
+ if not datum_arg:
47
+ return None, None
48
+ s = str(datum_arg)
49
+ if ':' in s:
50
+ parts = s.split(':')
51
+ geoid_part = parts[1]
52
+ geoid = (
53
+ geoid_part.split('=')[1] if 'geoid=' in geoid_part else geoid_part
54
+ )
55
+ return Datums.get_vdatum_by_name(parts[0]), geoid
56
+ return Datums.get_vdatum_by_name(s), None
57
+
58
+
59
+ def generate_grid(
60
+ region: Union[List[float], str, Region],
61
+ increment: Union[str, float],
62
+ datum_in: str,
63
+ datum_out: str,
64
+ out_fn: Optional[str] = None,
65
+ cache_dir: Optional[str] = None,
66
+ verbose: bool = False
67
+ ) -> Optional[np.ndarray]:
68
+ """Generate a vertical shift grid for a specific region.
69
+
70
+ Args:
71
+ region: Bounds as [W, E, S, N], a 'loc:' string, or a Region object.
72
+ increment: Resolution (e.g., '3s' or 0.0008333).
73
+ datum_in: Source datum (e.g., 'mllw', '5703').
74
+ datum_out: Target datum (e.g., '4979', '6319').
75
+ out_fn: If provided, saves the grid to this file (.tif or .gtx).
76
+ cache_dir: Path to store downloaded grids.
77
+ verbose: Enable debug logging.
78
+
79
+ Returns:
80
+ np.ndarray: The 2D vertical shift grid, or None if failed.
81
+ """
82
+
83
+ if isinstance(region, Region):
84
+ region_obj = region
85
+ else:
86
+ regions = parse_region(region)
87
+ if not regions:
88
+ logger.error(f"Could not parse region: {region}")
89
+ return None
90
+ region_obj = regions[0]
91
+
92
+ try:
93
+ inc_val = utils.str2inc(str(increment))
94
+ nx = int(region_obj.width / inc_val)
95
+ ny = int(region_obj.height / inc_val)
96
+ except Exception as e:
97
+ logger.error(f"Invalid increment '{increment}': {e}")
98
+ return None
99
+
100
+ epsg_in, geoid_in = _parse_datum(datum_in)
101
+ epsg_out, geoid_out = _parse_datum(datum_out)
102
+
103
+ if not epsg_in or not epsg_out:
104
+ logger.error(f"Invalid datum specified: {datum_in} -> {datum_out}")
105
+ return None
106
+
107
+ vt = VerticalTransform(
108
+ region=region_obj,
109
+ nx=nx, ny=ny,
110
+ epsg_in=epsg_in, epsg_out=epsg_out,
111
+ geoid_in=geoid_in, geoid_out=geoid_out,
112
+ cache_dir=cache_dir,
113
+ verbose=verbose
114
+ )
115
+
116
+ shift_array, _ = vt._vertical_transform(vt.epsg_in, vt.epsg_out)
117
+
118
+ if shift_array is None:
119
+ logger.error("Transformation failed to generate a grid.")
120
+ return None
121
+
122
+ if out_fn:
123
+ GridWriter.write(out_fn, shift_array, [region_obj.xmin, region_obj.xmax, region_obj.ymin, region_obj.ymax])
124
+ logger.info(f"Saved shift grid to {out_fn}")
125
+
126
+ return shift_array
127
+
128
+
129
+ def transform_raster(
130
+ input_raster: str,
131
+ datum_in: str,
132
+ datum_out: str,
133
+ output_raster: Optional[str] = None,
134
+ cache_dir: Optional[str] = None,
135
+ verbose: bool = False
136
+ ) -> Optional[str]:
137
+ """Apply a vertical datum transformation directly to an existing raster file.
138
+
139
+ Args:
140
+ input_raster: Path to the input DEM.
141
+ datum_in: Source datum of the DEM.
142
+ datum_out: Target datum for the output DEM.
143
+ output_raster: Path to save the transformed DEM. If None, auto-generates a name.
144
+ cache_dir: Path to store downloaded grids.
145
+ verbose: Enable debug logging.
146
+
147
+ Returns:
148
+ str: The path to the transformed output raster, or None if failed.
149
+ """
150
+
151
+ import rasterio
152
+
153
+ if not os.path.exists(input_raster):
154
+ logger.error(f"Input raster not found: {input_raster}")
155
+ return None
156
+
157
+ with rasterio.open(input_raster) as src:
158
+ bounds = src.bounds
159
+ region_obj = (
160
+ Region(bounds.left, bounds.right, bounds.bottom, bounds.top)
161
+ )
162
+ nx, ny = src.width, src.height
163
+
164
+ epsg_in, geoid_in = _parse_datum(datum_in)
165
+ epsg_out, geoid_out = _parse_datum(datum_out)
166
+
167
+ if not epsg_in or not epsg_out:
168
+ logger.error(f"Invalid datum specified: {datum_in} -> {datum_out}")
169
+ return None
170
+
171
+ if not output_raster:
172
+ base, ext = os.path.splitext(input_raster)
173
+ output_raster = f"{base}_trans_{datum_out.replace(':', '_')}{ext}"
174
+
175
+ vt = VerticalTransform(
176
+ region=region_obj,
177
+ nx=nx, ny=ny,
178
+ epsg_in=epsg_in, epsg_out=epsg_out,
179
+ geoid_in=geoid_in, geoid_out=geoid_out,
180
+ cache_dir=cache_dir,
181
+ verbose=verbose
182
+ )
183
+
184
+ shift_array, _ = vt._vertical_transform(vt.epsg_in, vt.epsg_out)
185
+
186
+ if shift_array is None:
187
+ logger.error("Failed to generate shift array for the raster bounds.")
188
+ return None
189
+
190
+ success = GridEngine.apply_vertical_shift(input_raster, shift_array, output_raster)
191
+
192
+ if success:
193
+ return output_raster
194
+ return None