transformez 0.2.0__tar.gz → 0.2.2__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 (33) hide show
  1. transformez-0.2.2/CHANGELOG.md +14 -0
  2. transformez-0.2.2/CITATION.cff +19 -0
  3. transformez-0.2.2/CONTRIBUTING.md +0 -0
  4. {transformez-0.2.0/src/transformez.egg-info → transformez-0.2.2}/PKG-INFO +13 -12
  5. transformez-0.2.2/pyproject.toml +75 -0
  6. transformez-0.2.2/src/transformez/__init__.py +101 -0
  7. transformez-0.2.2/src/transformez/_version.py +4 -0
  8. {transformez-0.2.0 → transformez-0.2.2}/src/transformez/cli.py +4 -1
  9. {transformez-0.2.0 → transformez-0.2.2}/src/transformez/grid_engine.py +4 -0
  10. transformez-0.2.2/src/transformez/modules.py +108 -0
  11. {transformez-0.2.0 → transformez-0.2.2}/src/transformez/transform.py +68 -54
  12. transformez-0.2.2/src/transformez/utils.py +96 -0
  13. transformez-0.2.2/tests/test_cli.py +36 -0
  14. transformez-0.2.0/PKG-INFO +0 -127
  15. transformez-0.2.0/pyproject.toml +0 -41
  16. transformez-0.2.0/setup.cfg +0 -4
  17. transformez-0.2.0/setup.py +0 -2
  18. transformez-0.2.0/src/transformez/__init__.py +0 -75
  19. transformez-0.2.0/src/transformez/utils.py +0 -43
  20. transformez-0.2.0/src/transformez.egg-info/SOURCES.txt +0 -22
  21. transformez-0.2.0/src/transformez.egg-info/dependency_links.txt +0 -1
  22. transformez-0.2.0/src/transformez.egg-info/entry_points.txt +0 -5
  23. transformez-0.2.0/src/transformez.egg-info/requires.txt +0 -4
  24. transformez-0.2.0/src/transformez.egg-info/top_level.txt +0 -1
  25. {transformez-0.2.0 → transformez-0.2.2}/AUTHORS.md +0 -0
  26. {transformez-0.2.0 → transformez-0.2.2}/LICENSE +0 -0
  27. {transformez-0.2.0 → transformez-0.2.2}/README.md +0 -0
  28. {transformez-0.2.0 → transformez-0.2.2}/src/transformez/definitions.py +0 -0
  29. {transformez-0.2.0 → transformez-0.2.2}/src/transformez/hooks.py +0 -0
  30. {transformez-0.2.0 → transformez-0.2.2}/src/transformez/htdp.py +0 -0
  31. {transformez-0.2.0 → transformez-0.2.2}/src/transformez/spatial.py +0 -0
  32. {transformez-0.2.0 → transformez-0.2.2}/src/transformez/srs.py +0 -0
  33. {transformez-0.2.0 → transformez-0.2.2}/src/transformez/vdatum.py +0 -0
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-02-10
9
+ ### Added
10
+
11
+ ### Changed
12
+ - Now uses raserio
13
+ - Renamed project to `transformez`.
14
+ - Refactored and decoupled from old cudem.vdatums
@@ -0,0 +1,19 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ authors:
4
+ - family-names: Love
5
+ given-names: Matthew
6
+ - family-names: Amante
7
+ given-names: Christopher
8
+ - family-names: MacFerrin
9
+ given-names: Michael
10
+ - family-names: Lim
11
+ given-names: Elliot
12
+ - family-names: Fisher
13
+ given-names: Matt
14
+
15
+ website: https://ciresdem.github.io/transformez/
16
+ title: "Transformez"
17
+ version: 0.1.0
18
+ date-released: 2026-02-10
19
+ url: "https://github.com/ciresdem/transformez"
File without changes
@@ -1,8 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transformez
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A standalone utility for vertical elevation datum transformations.
5
- Author: Matthew Love
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>
8
+ Maintainer-email: Matthew Love <matthew.love@colorado.edu>
6
9
  License: MIT License
7
10
 
8
11
  Copyright (c) 2010-2026 Regents of the University of Colorado
@@ -24,21 +27,19 @@ License: MIT License
24
27
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
28
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
29
  SOFTWARE.
27
- Project-URL: Homepage, https://github.com/ciresdem/transformez
28
- Project-URL: Issues, https://github.com/ciresdem/transformez/issues
30
+ License-File: AUTHORS.md
31
+ License-File: LICENSE
29
32
  Keywords: Geospatial
30
- Classifier: Programming Language :: Python :: 3
31
- Classifier: License :: OSI Approved :: MIT License
32
33
  Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 3
33
35
  Classifier: Topic :: Scientific/Engineering :: GIS
34
- Description-Content-Type: text/markdown
35
- License-File: LICENSE
36
- License-File: AUTHORS.md
36
+ Requires-Python: >=3.8
37
+ Requires-Dist: fetchez>0.3.3
37
38
  Requires-Dist: numpy<2.0.0
38
39
  Requires-Dist: pyproj
39
40
  Requires-Dist: rasterio
40
- Requires-Dist: fetchez>0.3.3
41
- Dynamic: license-file
41
+ Requires-Dist: scipy
42
+ Description-Content-Type: text/markdown
42
43
 
43
44
  # 🌍 Transformez ↕
44
45
 
@@ -124,4 +125,4 @@ shift, unc = vt._vertical_transform(vt.epsg_in, vt.epsg_out)
124
125
  ## License
125
126
 
126
127
  This project is licensed under the MIT License - see the [LICENSE](https://github.com/ciresdem/transformez/blob/main/LICENSE) file for details.
127
- Copyright (c) 2010-2026 Regents of the University of Colorado
128
+ Copyright (c) 2010-2026 Regents of the University of Colorado
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.5.0", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = 'transformez'
7
+ description = 'A standalone utility for vertical elevation datum transformations.'
8
+ readme = "README.md"
9
+ requires-python = ">=3.8"
10
+ license = { file = "LICENSE" }
11
+ dynamic = ["version"]
12
+ authors = [
13
+ {name = "Matthew Love", email = "matthew.love@colorado.edu"},
14
+ {name = "Christopher Amante", email = "christopher.amante@colorado.edu"},
15
+ {name = "Elliot Lim", email = "elliot.lim@colorado.edu"},
16
+ {name = "Michael MacFerrin", email = "michael.macferrin@colorado.edu"},
17
+ ]
18
+ maintainers = [
19
+ {name = "Matthew Love", email = "matthew.love@colorado.edu"},
20
+ ]
21
+ classifiers = [
22
+ 'Programming Language :: Python :: 3',
23
+ 'Operating System :: OS Independent',
24
+ 'Topic :: Scientific/Engineering :: GIS',
25
+ ]
26
+ dependencies = [
27
+ 'numpy<2.0.0',
28
+ 'pyproj',
29
+ 'rasterio',
30
+ 'fetchez>0.3.3',
31
+ 'scipy',
32
+ ]
33
+
34
+ keywords = ["Geospatial"]
35
+
36
+ [dependency-groups]
37
+ test = [
38
+ "pytest",
39
+ "pytest-cov",
40
+ ]
41
+
42
+ [project.entry-points."fetchez.plugins"]
43
+ transformez = "transformez"
44
+
45
+ [project.scripts]
46
+ transformez = 'transformez.cli:transformez_cli'
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/continuous-dems/transformez"
50
+ Issues = "https://github.com/continuous-dems/transformez/issues"
51
+
52
+ # --- Build configuration ---
53
+
54
+ [tool.hatch.version]
55
+ source = "vcs"
56
+
57
+ [tool.hatch.build.hooks.version]
58
+ path = "src/transformez/_version.py"
59
+
60
+ [tool.hatch.build.targets.wheel]
61
+ packages = ["src/transformez"]
62
+
63
+ [tool.hatch.build.targets.sdist]
64
+ include = [
65
+ "src/transformez",
66
+ "tests",
67
+ "CHANGELOG.md",
68
+ "CITATION.cff",
69
+ "CONTRIBUTING.md",
70
+ "AUTHORS.md",
71
+ ]
72
+ exclude = [
73
+ ".github",
74
+ ]
75
+
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ transformez
6
+ ~~~~~~~~~~~~~
7
+
8
+ :copyright: (c) 2010-2026 Regents of the University of Colorado
9
+ :license: MIT, see LICENSE for more details.
10
+ """
11
+
12
+ __author__ = "Matthew Love"
13
+ __credits__ = "CIRES"
14
+
15
+ try:
16
+ from transformez._version import __version__
17
+ 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
+ import warnings
22
+
23
+ warnings.warn(
24
+ "Importing 'transformez' outside a proper installation."
25
+ " It's highly recommended to install the package from a stable release or"
26
+ " in editable mode.",
27
+ stacklevel=2,
28
+ )
29
+ __version__ = "dev"
30
+
31
+
32
+ import os
33
+ import glob
34
+ # from .hooks import TransformezHook
35
+ # from fetchez.hooks.registry import HookRegistry
36
+ from fetchez.registry import FetchezRegistry
37
+
38
+ from .modules import TransformezMod
39
+
40
+ def _find_proj_lib():
41
+ """Locate the best available PROJ_LIB path."""
42
+
43
+ try:
44
+ import rasterio
45
+ r_path = os.path.join(os.path.dirname(rasterio.__file__), "proj_data")
46
+ if os.path.exists(os.path.join(r_path, "proj.db")):
47
+ return r_path
48
+
49
+ parent = os.path.dirname(os.path.dirname(rasterio.__file__))
50
+ libs = glob.glob(os.path.join(parent, "rasterio.libs*"))
51
+ if libs:
52
+ for root, _, files in os.walk(libs[0]):
53
+ if "proj.db" in files:
54
+ return root
55
+ except ImportError:
56
+ pass
57
+
58
+ try:
59
+ import pyproj
60
+ p_path = pyproj.datadir.get_data_dir()
61
+ if os.path.exists(os.path.join(p_path, "proj.db")):
62
+ return p_path
63
+ except ImportError:
64
+ pass
65
+
66
+ return None
67
+
68
+ target_proj_lib = _find_proj_lib()
69
+
70
+ if "PROJ_LIB" in os.environ:
71
+ del os.environ["PROJ_LIB"]
72
+
73
+ if target_proj_lib:
74
+ os.environ["PROJ_LIB"] = target_proj_lib
75
+
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)
@@ -0,0 +1,4 @@
1
+ # This file is auto-generated by Hatchling. As such, do not:
2
+ # - modify
3
+ # - track in version control e.g. be sure to add to .gitignore
4
+ __version__ = VERSION = '0.2.2'
@@ -73,7 +73,7 @@ def list_supported_datums():
73
73
 
74
74
  def transformez_cli():
75
75
  parser = argparse.ArgumentParser(
76
- description="Global Vertical Datum Transformer",
76
+ description=f"{utils.CYAN}%(prog)s{utils.RESET} ({__version__}) :: Global Vertical Datum Transformer",
77
77
  epilog="Examples:\n"
78
78
  " transformez -R -166/-164/63/64 -I mllw -O 4979\n"
79
79
  " transformez input_dem.tif -I mllw -O 5703:geoid=g2012b",
@@ -100,6 +100,9 @@ def transformez_cli():
100
100
  grp_sys.add_argument('--list-datums', action='store_true', help='List supported datums.')
101
101
  grp_sys.add_argument('--cache-dir', help='Override cache directory.')
102
102
  grp_sys.add_argument('--verbose', action='store_true', help='Enable debug logging.')
103
+ grp_sys.add_argument(
104
+ "-v", "--version", action="version", version=f"%(prog)s {__version__}"
105
+ )
103
106
 
104
107
  args = parser.parse_args()
105
108
 
@@ -165,6 +165,10 @@ class GridWriter:
165
165
  def write(filename, data, region):
166
166
  """Write a vertical shift grid using Rasterio."""
167
167
 
168
+ dirname = os.path.dirname(filename)
169
+ if dirname and not os.path.exists(dirname):
170
+ os.makedirs(dirname)
171
+
168
172
  if not filename.endswith('.tif'):
169
173
  filename = os.path.splitext(filename)[0] + '.tif'
170
174
 
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ transformez.modules
6
+ ~~~~~~~~~~~~~
7
+
8
+ Some modules for `fetchez`
9
+
10
+ :copyright: (c) 2010-2026 Regents of the University of Colorado
11
+ :license: MIT, see LICENSE for more details.
12
+ """
13
+
14
+ import os
15
+ import logging
16
+ from fetchez import core, cli
17
+ from transformez.transform import VerticalTransform
18
+ from transformez.grid_engine import GridWriter
19
+ from transformez.definitions import Datums
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ #!/usr/bin/env python
25
+ # -*- coding: utf-8 -*-
26
+
27
+ """
28
+ transformez.modules.transformez_mod
29
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30
+
31
+ """
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ @cli.cli_opts(
36
+ help_text="Generate a vertical shift grid (e.g. MLLW to NAVD88).",
37
+ src_datum="Source Datum (e.g. 'mllw', '5703', '4979').",
38
+ dst_datum="Destination Datum (e.g. '5703:geoid=g2012b').",
39
+ increment="Grid resolution (default: 3s).",
40
+ output_name="Optional output filename override."
41
+ )
42
+ class TransformezMod(core.FetchModule):
43
+ """A dynamic Fetchez module that generates vertical shift grids on demand.
44
+
45
+ Usage:
46
+ ... transformez --src-datum mllw --dst-datum 5703
47
+ """
48
+
49
+ def __init__(self, src_datum='5703', dst_datum='4979', increment='3s', output_name=None, **kwargs):
50
+ super().__init__(name="transformez", **kwargs)
51
+ self.src_datum = src_datum
52
+ self.dst_datum = dst_datum
53
+ self.increment = increment
54
+ self.output_name = output_name
55
+
56
+ s_name = str(self.src_datum).replace(':', '_')
57
+ d_name = str(self.dst_datum).replace(':', '_')
58
+ w, e, s, n = self.region
59
+ self.dst_fn = os.path.join(self._outdir, f"shift_{s_name}_to_{d_name}_{w}_{s}.tif")
60
+
61
+ def run(self):
62
+ from fetchez import utils
63
+ try:
64
+ inc_val = utils.str2inc(self.increment)
65
+ nx = int(self.region.width / inc_val)
66
+ ny = int(self.region.height / inc_val)
67
+ except Exception:
68
+ logger.warning(f"Invalid increment '{self.increment}', defaulting to 3s (~0.000833).")
69
+ # Default roughly 3 arc-seconds (approx 90m)
70
+ nx = int(self.region.width / 0.00083333333)
71
+ ny = int(self.region.height / 0.00083333333)
72
+
73
+ def parse_d(d_str):
74
+ if ':' in str(d_str):
75
+ parts = d_str.split(':')
76
+ geoid = parts[1].split('=')[1] if 'geoid=' in parts[1] else parts[1]
77
+ return parts[0], geoid
78
+ return d_str, None
79
+
80
+ epsg_in, geoid_in = parse_d(self.src_datum)
81
+ epsg_out, geoid_out = parse_d(self.dst_datum)
82
+
83
+ vt = VerticalTransform(
84
+ region=self.region,
85
+ nx=nx, ny=ny,
86
+ epsg_in=epsg_in, epsg_out=epsg_out,
87
+ geoid_in=geoid_in, geoid_out=geoid_out,
88
+ )
89
+
90
+ logger.info(f"Generating shift grid: {self.src_datum} -> {self.dst_datum}...")
91
+ shift_array, _ = vt._vertical_transform(vt.epsg_in, vt.epsg_out)
92
+
93
+ if shift_array is None:
94
+ logger.error("Transformation failed (No coverage or invalid datums).")
95
+ return
96
+
97
+ GridWriter.write(self.dst_fn, shift_array, self.region)
98
+
99
+ self.add_entry_to_results(
100
+ url=f"file://{self.dst_fn}",
101
+ dst_fn=self.dst_fn,
102
+ data_type="gtiff",
103
+ meta={
104
+ "src_datum": self.src_datum,
105
+ "dst_datum": self.dst_datum,
106
+ "generator": "transformez"
107
+ }
108
+ )
@@ -70,6 +70,7 @@ class VerticalTransform:
70
70
 
71
71
  def _get_native_ellipsoid(self, epsg, ref_type):
72
72
  """Helper to identify the native frame of a datum."""
73
+
73
74
  if ref_type == 'surface':
74
75
  # NOAA VDatum = NAD83, Global = WGS84
75
76
  region = Datums.SURFACES[epsg].get('region')
@@ -80,7 +81,7 @@ class VerticalTransform:
80
81
  elif ref_type == 'htdp':
81
82
  # If it's a Frame, it is its own native ellipsoid
82
83
  return epsg
83
- return WGS84_EPSG # Default fallback
84
+ return WGS84_EPSG # Default
84
85
 
85
86
  def fetch_grid(self, module_name, **kwargs):
86
87
  """Generic fetcher wrapper."""
@@ -158,34 +159,58 @@ class VerticalTransform:
158
159
  logger.error(f" [HTDP] Failed: {e}")
159
160
  return np.zeros((self.ny, self.nx))
160
161
 
162
+ def _fetch_geoid_with_fallback(self, target_geoid):
163
+ """Fetches a geoid grid. If the primary geoid lacks coverage (e.g., GEOID18 in AK),
164
+ it automatically falls back to older, compatible models.
165
+ """
166
+
167
+ # Ordered list of preferred US geoids (Newest to Oldest)
168
+ us_geoids = ['g2018', 'g2012b', 'geoid09']
169
+
170
+ if target_geoid in us_geoids:
171
+ start_idx = us_geoids.index(target_geoid)
172
+ geoids_to_try = us_geoids[start_idx:]
173
+ else:
174
+ geoids_to_try = [target_geoid]
175
+
176
+ for g in geoids_to_try:
177
+ geoid_def = Datums.GEOIDS.get(g, {})
178
+ provider = geoid_def.get('provider', 'proj')
179
+ grid = self._get_grid(provider, g)
180
+
181
+ if np.any(grid):
182
+ if g != target_geoid and self.verbose:
183
+ logger.info(f" [Geoid Fallback] '{target_geoid}' lacks coverage here. Falling back to '{g}'.")
184
+ return grid, g
185
+
186
+ return np.zeros((self.ny, self.nx)), target_geoid
187
+
161
188
  # =========================================================================
162
189
  # Chains
163
190
  # =========================================================================
164
191
  def _get_vdatum_chain(self, datum_name, geoid_name):
165
192
  """Builds shift: Tidal -> [NAD83 Native]."""
166
-
167
193
  total_shift = np.zeros((self.ny, self.nx))
168
194
  desc = []
169
195
 
170
196
  # Tidal -> LMSL
171
- if datum_name not in ["msl", "5714", "lmsl"]:
172
- grid = self._get_grid("vdatum", datum_name)
173
- if not np.any(grid):
174
- return None, f"Missing Tidal Grid: {datum_name}"
175
-
197
+ if datum_name not in ['msl', '5714', 'lmsl']:
198
+ grid = self._get_grid('vdatum', datum_name)
199
+ if not np.any(grid): return None, f"Missing Tidal Grid: {datum_name}"
176
200
  total_shift += grid
177
201
  desc.append(f"({datum_name}->LMSL)")
178
202
 
179
203
  # LMSL -> Ortho (TSS)
180
- tss = self._get_grid("vdatum", "tss")
204
+ tss = self._get_grid('vdatum', 'tss')
181
205
  total_shift += tss
182
206
  desc.append("TSS(LMSL->NAVD88)")
183
207
 
184
- # Ortho -> NAD83 (Geoid)
185
- actual_geoid = geoid_name if geoid_name else "g2018"
186
- geoid = self._get_grid("proj", actual_geoid)
187
- total_shift += geoid
188
- desc.append(f"Geoid({actual_geoid}->NAD83)")
208
+ # Ortho -> NAD83 (Smart Geoid Fallback)
209
+ actual_geoid = geoid_name if geoid_name else 'g2018'
210
+ geoid_grid, used_geoid = self._fetch_geoid_with_fallback(actual_geoid)
211
+
212
+ total_shift += geoid_grid
213
+ desc.append(f"Geoid({used_geoid}->NAD83)")
189
214
 
190
215
  return total_shift, " + ".join(desc)
191
216
 
@@ -219,8 +244,13 @@ class VerticalTransform:
219
244
  mss_name = model_def["grids"].get("mss")
220
245
  if mss_name:
221
246
  mss_grid = self._get_grid(provider, mss_name)
247
+ if provider == 'seanoe' or 'fes' in model.lower():
248
+ mss_grid -= 0.70
249
+ desc.append("MSS->WGS84(TP_Corr)")
250
+ else:
251
+ desc.append("MSS->Ellipsoid")
252
+
222
253
  total_shift += mss_grid
223
- desc.append("MSS->Ellipsoid")
224
254
 
225
255
  if not desc:
226
256
  return total_shift, "Global Chain (Empty)"
@@ -232,50 +262,38 @@ class VerticalTransform:
232
262
  # =========================================================================
233
263
  def _step_to_hub(self, epsg, ref_type, geoid=None, epoch=None):
234
264
  shift = np.zeros((self.ny, self.nx))
235
- if epsg == self.hub_epsg:
236
- return shift, "Already at Hub"
265
+ if epsg == self.hub_epsg: return shift, "Already at Hub"
237
266
 
238
- # Determine the Native Ellipsoid of this specific Input
239
267
  native_epsg = self._get_native_ellipsoid(epsg, ref_type)
240
-
241
- # Calculate Input -> Native
242
268
  chain_shift = None
243
269
  chain_desc = ""
244
270
 
245
- if ref_type == "surface":
246
- datum_name = Datums.SURFACES[epsg]["name"]
247
- region_tag = Datums.SURFACES[epsg].get("region")
271
+ if ref_type == 'surface':
272
+ datum_name = Datums.SURFACES[epsg]['name']
273
+ region_tag = Datums.SURFACES[epsg].get('region')
248
274
 
249
- if region_tag == "usa":
275
+ if region_tag == 'usa':
250
276
  s, d = self._get_vdatum_chain(datum_name, geoid)
251
- # Fallback
252
277
  if s is None:
253
- # Switch to Global Chain (Native=WGS84)
254
278
  native_epsg = WGS84_EPSG
255
279
  proxy_name = Datums.get_global_proxy(datum_name)
256
280
  if proxy_name:
257
- s, d = self._get_global_chain(proxy_name, model="fes2014")
258
- d = f"Global({proxy_name}) [Proxy] -> WGS84"
259
-
281
+ s, d = self._get_global_chain(proxy_name, model='fes2014')
282
+ # d = f"Global({proxy_name}) [Proxy] -> WGS84"
260
283
  chain_shift, chain_desc = s, d
261
284
 
262
- elif region_tag == "global":
285
+ elif region_tag == 'global':
263
286
  chain_shift, chain_desc = self._get_global_chain(datum_name)
264
287
 
265
- elif ref_type == "cdn":
266
- # Ortho -> Native
267
- target_geoid = geoid if geoid else "g2018"
268
- geoid_def = Datums.GEOIDS.get(target_geoid, {})
269
- provider = geoid_def.get("provider", "proj")
270
- chain_shift = self._get_grid(provider, target_geoid)
271
- chain_desc = f"Ortho(via {target_geoid}) -> Frame({native_epsg})"
288
+ elif ref_type == 'cdn':
289
+ target_geoid = geoid if geoid else 'g2018'
290
+ chain_shift, used_geoid = self._fetch_geoid_with_fallback(target_geoid)
291
+ chain_desc = f"Ortho(via {used_geoid}) -> Frame({native_epsg})"
272
292
 
273
- elif ref_type == "htdp":
274
- # Frame is already Native
293
+ elif ref_type == 'htdp':
275
294
  chain_shift = np.zeros((self.ny, self.nx))
276
295
  chain_desc = f"Frame({epsg})"
277
296
 
278
- # --- Native -> Hub ---
279
297
  if chain_shift is not None:
280
298
  if native_epsg != self.hub_epsg:
281
299
  htdp_shift = self._get_htdp_shift(native_epsg, self.hub_epsg, epoch, self.epoch_out)
@@ -287,12 +305,9 @@ class VerticalTransform:
287
305
 
288
306
  def _step_from_hub(self, epsg, ref_type, geoid=None, epoch=None):
289
307
  shift = np.zeros((self.ny, self.nx))
290
- if epsg == self.hub_epsg:
291
- return shift, "Remain at Hub"
308
+ if epsg == self.hub_epsg: return shift, "Remain at Hub"
292
309
 
293
310
  native_epsg = self._get_native_ellipsoid(epsg, ref_type)
294
-
295
- # --- Hub -> Native ---
296
311
  total_out = np.zeros((self.ny, self.nx))
297
312
  desc_parts = []
298
313
 
@@ -301,26 +316,25 @@ class VerticalTransform:
301
316
  total_out += htdp_shift
302
317
  desc_parts.append(f"Hub({self.hub_epsg}->{native_epsg})")
303
318
 
304
- # --- Native -> Output ---
305
- if ref_type == "surface":
306
- datum_name = Datums.SURFACES[epsg]["name"]
307
- chain_geoid = geoid if geoid else "g2018"
319
+ if ref_type == 'surface':
320
+ datum_name = Datums.SURFACES[epsg]['name']
321
+ chain_geoid = geoid if geoid else 'g2018'
308
322
  s, d = self._get_vdatum_chain(datum_name, chain_geoid)
309
323
  if s is None:
310
324
  return np.zeros((self.ny, self.nx)), "FAILED Output Chain"
311
325
 
312
- # Subtract because Chain is Input->Native
313
326
  total_out -= s
314
327
  desc_parts.append(f"Native -> VDatum({datum_name})")
315
328
 
316
- elif ref_type == "cdn":
317
- target_geoid = geoid if geoid else "g2018"
318
- geoid_def = Datums.GEOIDS.get(target_geoid, {})
319
- provider = geoid_def.get("provider", "proj")
320
- geoid_grid = self._get_grid(provider, target_geoid)
329
+ elif ref_type == 'cdn':
330
+ target_geoid = geoid if geoid else 'g2018'
331
+ geoid_grid, used_geoid = self._fetch_geoid_with_fallback(target_geoid)
332
+
333
+ if not np.any(geoid_grid):
334
+ logger.warning(f"Geoid {target_geoid} (and fallbacks) not found/covered.")
321
335
 
322
336
  total_out -= geoid_grid
323
- desc_parts.append(f"Native -> Ortho(via {target_geoid})")
337
+ desc_parts.append(f"Native -> Ortho(via {used_geoid})")
324
338
 
325
339
  return total_out, " + ".join(desc_parts)
326
340
 
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ transformez.utils
6
+ ~~~~~~~~~~~~~
7
+
8
+ This holds various utility functions.
9
+
10
+ :copyright: (c) 2010-2026 Regents of the University of Colorado
11
+ :license: MIT, see LICENSE for more details.
12
+ """
13
+
14
+ import os
15
+ import subprocess
16
+ import logging
17
+ import numpy as np
18
+ import rasterio
19
+
20
+ logger = logging.getLogger(__name__)
21
+ cmd_exists = lambda x: any(os.access(os.path.join(path, x), os.X_OK)
22
+ for path in os.environ['PATH'].split(os.pathsep))
23
+
24
+
25
+ def run_cmd(args):
26
+ """Standalone replacement for utils.run_cmd using subprocess."""
27
+
28
+ logger.info(f"Running: {' '.join(args) if isinstance(args, list) else args}")
29
+
30
+ result = subprocess.run(
31
+ args,
32
+ shell=False if isinstance(args, list) else True,
33
+ capture_output=True,
34
+ text=True
35
+ )
36
+ return result.stdout, result.returncode
37
+
38
+
39
+ def cmd_check(cmd_str, cmd_vers_str):
40
+ """check system for availability of 'cmd_str'"""
41
+
42
+ if cmd_exists(cmd_str):
43
+ cmd_vers, status = run_cmd(f"{cmd_vers_str}")
44
+ return cmd_vers.rstrip()
45
+ return b"0"
46
+
47
+
48
+ class RasterQuery:
49
+ """Raster query for point clouds.
50
+ Pre-loads raster data and inverse transform to rapidly query (X, Y) arrays.
51
+ """
52
+
53
+ def __init__(self, filename, default_nodata=0.0):
54
+ if not filename or not os.path.exists(filename):
55
+ raise FileNotFoundError(f"Raster not found: {filename}")
56
+
57
+ self.default_nodata = default_nodata
58
+
59
+ with rasterio.open(filename) as src:
60
+ self.data = src.read(1)
61
+ self.transform = src.transform
62
+ self.inv_transform = ~src.transform
63
+ self.bounds = src.bounds
64
+ self.width = src.width
65
+ self.height = src.height
66
+
67
+ if src.nodata is not None:
68
+ self.data[self.data == src.nodata] = self.default_nodata
69
+ self.data = np.nan_to_num(self.data, nan=self.default_nodata)
70
+
71
+ def query(self, x, y):
72
+ """query the raster at given X, Y numpy arrays using pure vectorized affine math."""
73
+
74
+ q_x = np.asarray(x).copy()
75
+ q_y = np.asarray(y)
76
+
77
+ if self.bounds.left < 0 and np.any(q_x > 180):
78
+ q_x = np.where(q_x > 180, q_x - 360, q_x)
79
+ elif self.bounds.left >= 0 and np.any(q_x < 0):
80
+ q_x = np.where(q_x < 0, q_x + 360, q_x)
81
+
82
+ cols_f, rows_f = self.inv_transform * (q_x, q_y)
83
+
84
+ cols = np.floor(cols_f).astype(int)
85
+ rows = np.floor(rows_f).astype(int)
86
+
87
+ valid = (
88
+ (rows >= 0) & (rows < self.height) &
89
+ (cols >= 0) & (cols < self.width)
90
+ )
91
+
92
+ results = np.full_like(q_x, self.default_nodata, dtype=self.data.dtype)
93
+ if np.any(valid):
94
+ results[valid] = self.data[rows[valid], cols[valid]]
95
+
96
+ return results
@@ -0,0 +1,36 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ # CMD will run Transformez
5
+ CMD = [sys.executable, "-m", "transformez.cli"]
6
+
7
+
8
+ def run_transformez(args):
9
+ """Run transformez and return result."""
10
+
11
+ return subprocess.run(CMD + args, capture_output=True, text=True)
12
+
13
+
14
+ def test_help():
15
+ """Does the help menu work?"""
16
+
17
+ result = run_transformez(["--help"])
18
+ assert result.returncode == 0
19
+
20
+
21
+ def test_version():
22
+ """Does version print?"""
23
+
24
+ result = run_transformez(["--version"])
25
+ assert result.returncode == 0
26
+
27
+
28
+ def test_list_modules():
29
+ """Can we list datums without crashing?"""
30
+
31
+ result = run_transformez(["--list-datums"])
32
+ assert result.returncode == 0
33
+ assert "lat" in result.stdout
34
+ assert "mllw" in result.stdout
35
+ assert "NAVD88" in result.stdout
36
+ assert "g2018" in result.stdout
@@ -1,127 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: transformez
3
- Version: 0.2.0
4
- Summary: A standalone utility for vertical elevation datum transformations.
5
- Author: Matthew Love
6
- License: MIT License
7
-
8
- Copyright (c) 2010-2026 Regents of the University of Colorado
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
- Project-URL: Homepage, https://github.com/ciresdem/transformez
28
- Project-URL: Issues, https://github.com/ciresdem/transformez/issues
29
- Keywords: Geospatial
30
- Classifier: Programming Language :: Python :: 3
31
- Classifier: License :: OSI Approved :: MIT License
32
- Classifier: Operating System :: OS Independent
33
- Classifier: Topic :: Scientific/Engineering :: GIS
34
- Description-Content-Type: text/markdown
35
- License-File: LICENSE
36
- License-File: AUTHORS.md
37
- Requires-Dist: numpy<2.0.0
38
- Requires-Dist: pyproj
39
- Requires-Dist: rasterio
40
- Requires-Dist: fetchez>0.3.3
41
- Dynamic: license-file
42
-
43
- # 🌍 Transformez ↕
44
-
45
- **Global vertical datum transformations, simplified.**
46
-
47
- *Transformez Les Données*
48
-
49
- > 🚀 **v0.2.0:** Now supporting global tidal transformations via FES2014 & SEANOE.
50
-
51
- **Transformez** is a standalone Python engine for converting geospatial data between vertical datums (e.g., `MLLW` ↔ `NAVD88` ↔ `Ellipsoid`).
52
-
53
- ---
54
-
55
- ## Installation
56
-
57
- ```bash
58
- pip install transformez
59
- ```
60
-
61
- *Requires [htdp](https://geodesy.noaa.gov/TOOLS/Htdp/Htdp.shtml) to be in your system PATH for frame transformations.*
62
-
63
- ## Usage
64
-
65
- **Generate a vertical shift grid for anywhere on Earth.**
66
-
67
- ```bash
68
- # Transform MLLW to WGS84 Ellipsoid in Norton Sound, AK
69
- # (Where NOAA has no coverage!)
70
- transformez -R -166/-164/63/64 -E 3s \
71
- --input-datum mllw \
72
- --output-datum 4979 \
73
- --output shift_ak.tif
74
- ```
75
-
76
- **Transform a raster directly.** Transformez reads the bounds/resolution from the file.
77
-
78
- ```bash
79
- transformez --dem input_bathymetry.tif \
80
- --input-datum "mllw" \
81
- --output-datum "5703:geoid=geoid12b" \
82
- --output output_navd88.tif
83
- ```
84
-
85
- **Integrate directly into your download pipeline.**
86
-
87
- ```bash
88
- # Download GEBCO and shift EGM96 to WGS84 on the fly
89
- fetchez gebco ... --hook transformez:datum_in=5773,datum_out=4979
90
- ```
91
-
92
- ## Python API
93
-
94
- ```python
95
- from transformez.transform import VerticalTransform
96
- from fetchez.spatial import Region
97
-
98
- # Define a region in India (Bay of Bengal)
99
- region = Region(80, 85, 10, 15)
100
-
101
- # Initialize Transformer
102
- # Requesting "MLLW" in India triggers the Global Fallback automatically
103
- vt = VerticalTransform(
104
- region=region,
105
- nx=1000, ny=1000,
106
- epsg_in="mllw", # Will resolve to FES2014 LAT
107
- epsg_out="epsg:4979" # WGS84 Ellipsoid
108
- )
109
-
110
- # Generate Shift
111
- shift, unc = vt._vertical_transform(vt.epsg_in, vt.epsg_out)
112
- ```
113
-
114
- ## Supported Datums
115
-
116
- * **Tidal**: mllw, mhhw, msl, lat
117
-
118
- * **Ellipsoidal**: 4979 (WGS84), 6319 (NAD83 2011)
119
-
120
- * **Orthometric**: 5703 (NAVD88), egm2008, egm96
121
-
122
- * **Geoids**: g2018, g2012b, geoid09, xgeoid20b
123
-
124
- ## License
125
-
126
- This project is licensed under the MIT License - see the [LICENSE](https://github.com/ciresdem/transformez/blob/main/LICENSE) file for details.
127
- Copyright (c) 2010-2026 Regents of the University of Colorado
@@ -1,41 +0,0 @@
1
- [build-system]
2
- requires = ['setuptools>=61.0']
3
- build-backend = 'setuptools.build_meta'
4
-
5
- [project]
6
- name = 'transformez'
7
- version = '0.2.0'
8
- description = 'A standalone utility for vertical elevation datum transformations.'
9
- readme = "README.md"
10
- authors = [{ name = 'Matthew Love' }]
11
- license = { file = 'LICENSE' }
12
- classifiers = [
13
- 'Programming Language :: Python :: 3',
14
- 'License :: OSI Approved :: MIT License',
15
- 'Operating System :: OS Independent',
16
- 'Topic :: Scientific/Engineering :: GIS',
17
- ]
18
- dependencies = [
19
- 'numpy<2.0.0',
20
- 'pyproj',
21
- 'rasterio',
22
- 'fetchez>0.3.3',
23
- ]
24
-
25
- keywords = ["Geospatial"]
26
-
27
- [project.entry-points."fetchez.plugins"]
28
- transformez = "transformez"
29
-
30
- [project.scripts]
31
- transformez = 'transformez.cli:transformez_cli'
32
-
33
- [project.urls]
34
- Homepage = "https://github.com/ciresdem/transformez"
35
- Issues = "https://github.com/ciresdem/transformez/issues"
36
-
37
- [tool.setuptools.packages.find]
38
- where = ["src"] # Look for packages inside the 'src' folder
39
- include = ["transformez*"] # Include the package named 'transformez'
40
- exclude = ["tests*", "docs*", "scripts*", "examples*"]
41
- namespaces = false
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
@@ -1,2 +0,0 @@
1
- from setuptools import setup
2
- setup()
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- transformez
6
- ~~~~~~~~~~~~~
7
-
8
- :copyright: (c) 2010-2026 Regents of the University of Colorado
9
- :license: MIT, see LICENSE for more details.
10
- """
11
-
12
- __version__ = "0.2.0"
13
- __author__ = "Matthew Love"
14
- __credits__ = "CIRES"
15
-
16
- import os
17
- import glob
18
- from .hooks import TransformezHook
19
- from fetchez.hooks.registry import HookRegistry
20
- # from fetchez.registry import FetchezRegistry
21
-
22
- def _find_proj_lib():
23
- """Locate the best available PROJ_LIB path."""
24
-
25
- try:
26
- import rasterio
27
- r_path = os.path.join(os.path.dirname(rasterio.__file__), "proj_data")
28
- if os.path.exists(os.path.join(r_path, "proj.db")):
29
- return r_path
30
-
31
- parent = os.path.dirname(os.path.dirname(rasterio.__file__))
32
- libs = glob.glob(os.path.join(parent, "rasterio.libs*"))
33
- if libs:
34
- for root, _, files in os.walk(libs[0]):
35
- if "proj.db" in files:
36
- return root
37
- except ImportError:
38
- pass
39
-
40
- try:
41
- import pyproj
42
- p_path = pyproj.datadir.get_data_dir()
43
- if os.path.exists(os.path.join(p_path, "proj.db")):
44
- return p_path
45
- except ImportError:
46
- pass
47
-
48
- return None
49
-
50
- target_proj_lib = _find_proj_lib()
51
-
52
- if "PROJ_LIB" in os.environ:
53
- del os.environ["PROJ_LIB"]
54
-
55
- if target_proj_lib:
56
- os.environ["PROJ_LIB"] = target_proj_lib
57
-
58
- def setup_fetchez(registry_cls):
59
- """Called by fetchez when loading plugins.
60
-
61
- Registers modules, hooks, and presets.
62
- """
63
-
64
- HookRegistry.register_hook(TransformezHook)
65
-
66
- from fetchez.presets import register_global_preset
67
- register_global_preset(
68
- name="make-shift-grid",
69
- help_text="Download datum grids and composite them into a single shift grid.",
70
- hooks=[
71
- {"name": "transformez", "args": {}}
72
- ]
73
- )
74
-
75
- # setup_fetchez(FetchezRegistry)
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- transformez.utils
6
- ~~~~~~~~~~~~~
7
-
8
- This holds various utility functions.
9
-
10
- :copyright: (c) 2010-2026 Regents of the University of Colorado
11
- :license: MIT, see LICENSE for more details.
12
- """
13
-
14
- import os
15
- import subprocess
16
- import logging
17
-
18
- logger = logging.getLogger(__name__)
19
- cmd_exists = lambda x: any(os.access(os.path.join(path, x), os.X_OK)
20
- for path in os.environ['PATH'].split(os.pathsep))
21
-
22
-
23
- def run_cmd(args):
24
- """Standalone replacement for utils.run_cmd using subprocess."""
25
-
26
- logger.info(f"Running: {' '.join(args) if isinstance(args, list) else args}")
27
-
28
- result = subprocess.run(
29
- args,
30
- shell=False if isinstance(args, list) else True,
31
- capture_output=True,
32
- text=True
33
- )
34
- return result.stdout, result.returncode
35
-
36
-
37
- def cmd_check(cmd_str, cmd_vers_str):
38
- """check system for availability of 'cmd_str'"""
39
-
40
- if cmd_exists(cmd_str):
41
- cmd_vers, status = run_cmd(f"{cmd_vers_str}")
42
- return cmd_vers.rstrip()
43
- return b"0"
@@ -1,22 +0,0 @@
1
- AUTHORS.md
2
- LICENSE
3
- README.md
4
- pyproject.toml
5
- setup.py
6
- src/transformez/__init__.py
7
- src/transformez/cli.py
8
- src/transformez/definitions.py
9
- src/transformez/grid_engine.py
10
- src/transformez/hooks.py
11
- src/transformez/htdp.py
12
- src/transformez/spatial.py
13
- src/transformez/srs.py
14
- src/transformez/transform.py
15
- src/transformez/utils.py
16
- src/transformez/vdatum.py
17
- src/transformez.egg-info/PKG-INFO
18
- src/transformez.egg-info/SOURCES.txt
19
- src/transformez.egg-info/dependency_links.txt
20
- src/transformez.egg-info/entry_points.txt
21
- src/transformez.egg-info/requires.txt
22
- src/transformez.egg-info/top_level.txt
@@ -1,5 +0,0 @@
1
- [console_scripts]
2
- transformez = transformez.cli:transformez_cli
3
-
4
- [fetchez.plugins]
5
- transformez = transformez
@@ -1,4 +0,0 @@
1
- numpy<2.0.0
2
- pyproj
3
- rasterio
4
- fetchez>0.3.3
@@ -1 +0,0 @@
1
- transformez
File without changes
File without changes
File without changes