transformez 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,125 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Matthew Love"
5
+ __credits__ = "CIRES"
6
+
7
+ import os
8
+
9
+ # --- fix the PROJ_LIB path to work with rasterio/pyproj
10
+ def _find_proj_lib():
11
+ """Locate the best available PROJ_LIB path."""
12
+
13
+ try:
14
+ import rasterio
15
+ # Common path in wheels: site-packages/rasterio/proj_data
16
+ r_path = os.path.join(os.path.dirname(rasterio.__file__), 'proj_data')
17
+ if os.path.exists(os.path.join(r_path, 'proj.db')):
18
+ return r_path
19
+
20
+ # Linux wheels often put it in .libs adjacent to the package
21
+ # e.g. site-packages/rasterio.libs/proj.db
22
+ parent = os.path.dirname(os.path.dirname(rasterio.__file__))
23
+ libs = glob.glob(os.path.join(parent, 'rasterio.libs*'))
24
+ if libs:
25
+ # Look inside the libs folder
26
+ for root, _, files in os.walk(libs[0]):
27
+ if 'proj.db' in files:
28
+ return root
29
+ except ImportError:
30
+ pass
31
+
32
+ # Try PyProj's bundled data (Fallback)
33
+ try:
34
+ import pyproj
35
+ p_path = pyproj.datadir.get_data_dir()
36
+ if os.path.exists(os.path.join(p_path, 'proj.db')):
37
+ return p_path
38
+ except ImportError:
39
+ pass
40
+
41
+ return None
42
+
43
+ target_proj_lib = _find_proj_lib()
44
+
45
+ # Unset conflicting system/conda vars if we found a better one
46
+ if 'PROJ_LIB' in os.environ:
47
+ del os.environ['PROJ_LIB']
48
+
49
+ if target_proj_lib:
50
+ os.environ['PROJ_LIB'] = target_proj_lib
51
+ # print(f"DEBUG: PROJ_LIB set to {target_proj_lib}")
52
+
53
+ # # --- PROJ_LIB Fix (Run before any geo-imports) ---
54
+ # # This prevents "PROJ: proj_create_from_database: Cannot find proj.db" errors
55
+ # # when conflicting Conda/System PROJ installations exist.
56
+ # try:
57
+ # # 1. We must import pyproj first to let it find its own bundled data
58
+ # import os
59
+ # import pyproj
60
+
61
+ # # 2. Get the valid data directory from the wheel
62
+ # proj_lib = pyproj.datadir.get_data_dir()
63
+ # #print(proj_lib)
64
+ # # 3. Force the environment variable to use this valid path
65
+ # # (Overriding any bad global/Conda defaults)
66
+ # os.environ['PROJ_LIB'] = proj_lib
67
+
68
+ # except ImportError:
69
+ # # If pyproj isn't installed yet (e.g. during pip install), skip this.
70
+ # pass
71
+
72
+ # --- End PROJ_LIB Fix ---
73
+
74
+ from .hooks import TransformezHook
75
+ #from .modules import DatumGridFetcher
76
+ from fetchez.hooks.registry import HookRegistry
77
+ # form fetchez.registry import FetchezRegistry # Implicitly handled via setup_fetchez passing cls
78
+
79
+ def setup_fetchez(registry_cls):
80
+ """Called by fetchez when loading plugins.
81
+ Registers modules, hooks, and presets.
82
+ """
83
+
84
+ # module should gather necessary grids to do the transformation
85
+ # Register the Module with Fetchez
86
+ # registry_cls.register_module(
87
+ # mod_key='datum_grids',
88
+ # mod_cls=DatumGridFetcher,
89
+ # metadata={
90
+ # 'desc': 'Fetch NOAA VDatum and PROJ-CDN grids for a region',
91
+ # 'tags': ['vdatum', 'geoid', 'transformation']
92
+ # }
93
+ # )
94
+
95
+ # Register Fetchez Hooks
96
+ HookRegistry.register_hook(TransformezHook)
97
+
98
+ # Register Global Presets
99
+ from fetchez.presets import register_global_preset
100
+
101
+ register_global_preset(
102
+ name="make-shift-grid",
103
+ help_text="Download datum grids and composite them into a single GTX shift grid.",
104
+ hooks=[
105
+ {"name": "transformez", "args": {}}
106
+ ]
107
+ )
108
+
109
+
110
+ # "transform-pipeline": {
111
+ # "help_text": "Generate shift grid based on region, then apply it to files.",
112
+ # "hooks": [
113
+ # {
114
+ # "name": "transformez",
115
+ # "args": {"stage": "pre", "datum_in": "5703", "output_grid": "/tmp/shift.gtx"}
116
+ # },
117
+ # {
118
+ # "name": "transformez",
119
+ # "args": {"stage": "file", "apply": "True", "output_grid": "/tmp/shift.gtx"}
120
+ # },
121
+ # {
122
+ # "name": "audit"
123
+ # }
124
+ # ]
125
+ # }
transformez/cli.py ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ transformez.cli
6
+ ~~~~~~~~~~~~~
7
+
8
+ The transformez CLI.
9
+ Generates vertical transformation grids using the Fetchez-DLIM ecosystem.
10
+
11
+ :copyright: (c) 2010-2026 Regents of the University of Colorado
12
+ :license: MIT, see LICENSE for more details.
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import argparse
18
+ import logging
19
+
20
+ import pyproj
21
+ import rasterio
22
+
23
+ from . import __version__
24
+ from .transform import VerticalTransform
25
+ from .definitions import Datums
26
+ from .grid_engine import plot_grid, GridWriter, GridEngine
27
+
28
+ import fetchez
29
+ from fetchez import spatial
30
+ from fetchez import utils
31
+ from fetchez.spatial import parse_region, Region
32
+
33
+ logging.basicConfig(level=logging.INFO, format='[ %(levelname)s ] %(name)s: %(message)s', stream=sys.stderr)
34
+ logger = logging.getLogger(__name__)
35
+
36
+ logging.getLogger('fetchez').setLevel(logging.WARNING)
37
+
38
+ def parse_compound_datum(datum_arg):
39
+ """Parse a datum string that might contain a geoid override.
40
+ Format: "EPSG" or "EPSG:GEOID" or "NAME:GEOID"
41
+ """
42
+ if ':' in str(datum_arg):
43
+ parts = str(datum_arg).split(':')
44
+ datum = Datums.get_vdatum_by_name(parts[0])
45
+ geoid = parts[1]
46
+ return datum, geoid
47
+ else:
48
+ return Datums.get_vdatum_by_name(datum_arg), None
49
+
50
+
51
+ def get_grid_info(filename):
52
+ """Extract region, resolution, and SRS from a raster using Rasterio."""
53
+
54
+ with rasterio.open(filename) as ds:
55
+ bounds = ds.bounds # left, bottom, right, top
56
+ width = ds.width
57
+ height = ds.height
58
+ gt = ds.transform.to_gdal() # (c, a, b, f, d, e)
59
+ srs_wkt = ds.crs.to_wkt() if ds.crs else None
60
+
61
+ return {
62
+ 'te': (bounds.left, bounds.bottom, bounds.right, bounds.top),
63
+ 'region': (bounds.left, bounds.right, bounds.bottom, bounds.top),
64
+ 'nx': width,
65
+ 'ny': height,
66
+ 'gt': gt,
67
+ 'srs_wkt': srs_wkt
68
+ }
69
+
70
+
71
+ def transformez_cli():
72
+ parser = argparse.ArgumentParser(
73
+ description=f'%(prog)s ({__version__}): Generate a vertical transformation grid',
74
+ formatter_class=argparse.RawTextHelpFormatter,
75
+ epilog="CUDEM home page: <http://cudem.colorado.edu>"
76
+ )
77
+
78
+ input_grp = parser.add_mutually_exclusive_group(required=True)
79
+ input_grp.add_argument('-R', '--region', help=spatial.region_help_msg())
80
+ input_grp.add_argument('--dem', help='Input DEM to transform. Automatically sets Region and Resolution.')
81
+
82
+ sel_grp = parser.add_argument_group('Geospatial Selection')
83
+ sel_grp.add_argument('-E', '--increment', help='Grid resolution (e.g. 0.0001 or 1s) (Required if not using --dem).')
84
+
85
+ datum_group = parser.add_argument_group('Datum Configuration')
86
+ datum_group.add_argument('-I', '--vdatum_in', default='5703',
87
+ help='Input vertical datum. Format: "EPSG" or "EPSG:GEOID" (e.g. "5703:g2012a")')
88
+ datum_group.add_argument('-O', '--vdatum_out', default='7662',
89
+ help='Output vertical datum. Format: "EPSG" or "EPSG:GEOID"')
90
+ datum_group.add_argument('--epoch-in', type=float, default=1997.0,
91
+ help='Input coordinate epoch (decimal year).')
92
+ datum_group.add_argument('--epoch-out', type=float, default=1997.0,
93
+ help='Output coordinate epoch (decimal year).')
94
+
95
+ proc_group = parser.add_argument_group('Processing Options')
96
+ proc_group.add_argument('--preview', action='store_true', help='Plot the transformation grid (matplotlib) before processing.')
97
+ proc_group.add_argument('--output', help='Output filename (default auto-generated).')
98
+
99
+ sys_group = parser.add_argument_group('System & Logging')
100
+ sys_group.add_argument('-D', '--cache-dir', help='Directory for storing temporary grids.')
101
+ sys_group.add_argument('-k', '--keep-cache', action='store_true', help='Do not delete temporary files after run.')
102
+ sys_group.add_argument('-l', '--list-epsg', action='store_true', help='List supported EPSG codes/names and exit.')
103
+ sys_group.add_argument('-q', '--quiet', action='store_true', help='Suppress log output.')
104
+ sys_group.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
105
+
106
+ # Handle negative coordinates in arguments
107
+ fixed_argv = spatial.fix_argparse_region(sys.argv[1:])
108
+ args = parser.parse_args(fixed_argv)
109
+
110
+ if args.quiet:
111
+ logger.setLevel(logging.WARNING)
112
+
113
+ if args.list_epsg:
114
+ def _print_epsg(title, data):
115
+ print(f'{title}:')
116
+ for key, val in data.items():
117
+ print(f' {key}\t{val["name"]}')
118
+
119
+ _print_epsg('HTDP EPSG', Datums.HTDP)
120
+ _print_epsg('CDN EPSG', Datums.CDN)
121
+ _print_epsg('Tidal EPSG', Datums.TIDAL)
122
+ sys.exit(0)
123
+
124
+ cache_dir = args.cache_dir or os.path.join(os.path.expanduser('~'), '.transformez')
125
+ if not os.path.exists(cache_dir):
126
+ os.makedirs(cache_dir)
127
+
128
+ epsg_in, geoid_in = parse_compound_datum(args.vdatum_in)
129
+ epsg_out, geoid_out = parse_compound_datum(args.vdatum_out)
130
+
131
+ if args.dem:
132
+ if not os.path.exists(args.dem):
133
+ logger.error(f'Input DEM not found: {args.dem}')
134
+ sys.exit(1)
135
+
136
+ logger.info(f'Extracting grid info from DEM: {args.dem}')
137
+ info = get_grid_info(args.dem)
138
+ logger.info(f'Grid info is: {info}')
139
+
140
+ # Override region and dimensions from DEM
141
+ # info['te'] is (w, s, e, n)
142
+ region_obj = spatial.Region.from_list(info['region'])
143
+ nx = info['nx']
144
+ ny = info['ny']
145
+
146
+ if info.get('srs_wkt'):
147
+ pass
148
+
149
+ # Default Output Name for DEM
150
+ if not args.output:
151
+ base, ext = os.path.splitext(args.dem)
152
+ dst_grid = f'{base}_trans_{epsg_out}{ext}'
153
+ else:
154
+ dst_grid = args.output
155
+
156
+ elif args.region:
157
+ # parse_region returns a list of Region objects
158
+ # we're only using one for now.
159
+ these_regions = parse_region(args.region)
160
+ region_obj = these_regions[0]
161
+
162
+ try:
163
+ if '/' in args.increment:
164
+ inc_x, inc_y = [float(x) for x in args.increment.split('/')]
165
+ else:
166
+ inc_val = utils.str2inc(args.increment)
167
+ inc_x, inc_y = inc_val, inc_val
168
+
169
+ width = region_obj.width
170
+ height = region_obj.height
171
+ nx = int(width / inc_x)
172
+ ny = int(height / inc_y)
173
+ except Exception as e:
174
+ logger.error(f'Invalid increment: {args.increment}. {e}')
175
+ sys.exit(1)
176
+
177
+ if not args.output:
178
+ base, ext = 'transformez_trans', 'tif' # Default to TIF now
179
+ v_out_str = str(epsg_out)
180
+ if geoid_out:
181
+ v_out_str += f"_{geoid_out}"
182
+ dst_grid = f"{base}_{epsg_in}_{v_out_str}.{ext}"
183
+ else:
184
+ dst_grid = args.output
185
+ else:
186
+ logger.error("Region or DEM is required.")
187
+ sys.exit(1)
188
+
189
+ # Initialize Vertical Transform
190
+ vt = VerticalTransform(
191
+ region=region_obj,
192
+ nx=nx,
193
+ ny=ny,
194
+ epsg_in=epsg_in,
195
+ epsg_out=epsg_out,
196
+ geoid_in=geoid_in,
197
+ geoid_out=geoid_out,
198
+ epoch_in=args.epoch_in,
199
+ epoch_out=args.epoch_out,
200
+ cache_dir=cache_dir,
201
+ )
202
+
203
+ logger.info(f"Generating shift grid: {epsg_in} -> {epsg_out}")
204
+ shift_array, _ = vt._vertical_transform(vt.epsg_in, vt.epsg_out)
205
+
206
+ if shift_array is not None:
207
+ if args.preview:
208
+ plot_grid(
209
+ shift_array,
210
+ region=region_obj,
211
+ title=f"Shift: {epsg_in} -> {epsg_out}"
212
+ )
213
+
214
+ if args.dem:
215
+ # Apply to DEM
216
+ logger.info(f"Applying transformation to DEM...")
217
+ GridEngine.apply_vertical_shift(args.dem, shift_array, dst_grid)
218
+ else:
219
+ # Just write the grid
220
+ logger.info(f"Saving transformation grid to: {dst_grid}")
221
+ GridWriter.write(dst_grid, shift_array, region_obj)
222
+ return
223
+ else:
224
+ logger.error("Failed to generate transformation grid.")
225
+
226
+
227
+ if __name__ == '__main__':
228
+ transformez_cli()
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ transformez.definitions
6
+ ~~~~~~~~~~~~~
7
+
8
+ This file contains the various vertical datum transformation references
9
+ and definitions.
10
+
11
+ :copyright: (c) 2010-2026 Regents of the University of Colorado
12
+ :license: MIT, see LICENSE for more details.
13
+ """
14
+
15
+ import logging
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ class Datums:
20
+ """Class to manage vertical datum definitions and lookups."""
21
+
22
+ # =========================================================================
23
+ # Vertical Datum References
24
+ # =========================================================================
25
+ SURFACES = {
26
+ # --- Tidal Datums ---
27
+ 1089: {'name': 'mllw', 'description': 'Mean Lower Low Water', 'uncertainty': 0, 'epsg': 5866},
28
+ 5866: {'name': 'mllw', 'description': 'Mean Lower Low Water', 'uncertainty': 0, 'epsg': 5866},
29
+ 1091: {'name': 'mlw', 'description': 'Mean Low Water', 'uncertainty': 0, 'epsg': 1091},
30
+ 5869: {'name': 'mhhw', 'description': 'Mean Higher High Water', 'uncertainty': 0, 'epsg': 5869},
31
+ 5868: {'name': 'mhw', 'description': 'Mean High Water', 'uncertainty': 0, 'epsg': 5868},
32
+ 5714: {'name': 'msl', 'description': 'Mean Sea Level', 'uncertainty': 0, 'epsg': 5714},
33
+ 5713: {'name': 'mtl', 'description': 'Mean Tide Level', 'uncertainty': 0, 'epsg': 5713},
34
+
35
+ # --- Hydraulic / River Datums ---
36
+ # Columbia River Datum (No standard EPSG, using 0 placeholder or custom)
37
+ 0: {'name': 'crd', 'description': 'Columbia River Datum', 'uncertainty': 0, 'epsg': 0},
38
+
39
+ # IGLD 1985 (Dynamic Height)
40
+ 5609: {'name': 'IGLD85', 'description': 'International Great Lakes Datum 1985', 'uncertainty': 0, 'epsg': 5609},
41
+
42
+ # IGLD Low Water Datum (Chart Datum for Lakes)
43
+ # VDatum uses 'LWD_IGLD85' string
44
+ 9000: {'name': 'LWD_IGLD85', 'description': 'IGLD85 Low Water Datum', 'uncertainty': 0, 'epsg': 5609},
45
+
46
+ # --- Legacy Vertical ---
47
+ # NGVD29 is often best handled via VDatum (VERTCON) if PROJ isn't configured
48
+ 5702: {'name': 'NGVD29', 'description': 'National Geodetic Vertical Datum 1929', 'uncertainty': 0.05, 'epsg': 5702},
49
+ }
50
+
51
+ HTDP = {
52
+ 4269: {'name': 'NAD_83(2011/CORS96/2007)', 'description': '(North American plate fixed)', 'htdp_id': 1, 'uncertainty': .02, 'epoch': 1997.0},
53
+ 6781: {'name': 'NAD_83(2011/CORS96/2007)', 'description': '(North American plate fixed)', 'htdp_id': 1, 'uncertainty': .02, 'epoch': 1997.0},
54
+ 6319: {'name': 'NAD_83(2011/CORS96/2007)', 'description': '(North American plate fixed)', 'htdp_id': 1, 'uncertainty': .02, 'epoch': 1997.0},
55
+ 6321: {'name': 'NAD_83(PA11/PACP00)', 'description': '(Pacific plate fixed)', 'htdp_id': 2, 'uncertainty': .02, 'epoch': 1997.0},
56
+ 6324: {'name': 'NAD_83(MA11/MARP00)', 'description': '(Mariana plate fixed)', 'htdp_id': 3, 'uncertainty': .02, 'epoch': 1997.0},
57
+ 4979: {'name': 'WGS_84(original)', 'description': '(NAD_83(2011) used)', 'htdp_id': 4, 'uncertainty': 0, 'epoch': 1997.0},
58
+ 7815: {'name': 'WGS_84(original)', 'description': '(NAD_83(2011) used)', 'htdp_id': 4, 'uncertainty': 0, 'epoch': 1997.0},
59
+ 7816: {'name': 'WGS_84(original)', 'description': '(NAD_83(2011) used)', 'htdp_id': 4, 'uncertainty': 0, 'epoch': 1997.0},
60
+ 7656: {'name': 'WGS_84(G730)', 'description': '(ITRF91 used)', 'htdp_id': 5, 'uncertainty': 0, 'epoch': 1997.0},
61
+ 7657: {'name': 'WGS_84(G730)', 'description': '(ITRF91 used)', 'htdp_id': 5, 'uncertainty': 0, 'epoch': 1997.0},
62
+ 7658: {'name': 'WGS_84(G873)', 'description': '(ITRF94 used)', 'htdp_id': 6, 'uncertainty': 0, 'epoch': 1997.0},
63
+ 7659: {'name': 'WGS_84(G873)', 'description': '(ITRF94 used)', 'htdp_id': 6, 'uncertainty': 0, 'epoch': 1997.0},
64
+ 7660: {'name': 'WGS_84(G1150)', 'description': '(ITRF2000 used)', 'htdp_id': 7, 'uncertainty': 0, 'epoch': 1997.0},
65
+ 7661: {'name': 'WGS_84(G1150)', 'description': '(ITRF2000 used)', 'htdp_id': 7, 'uncertainty': 0, 'epoch': 1997.0},
66
+ 7662: {'name': 'WGS_84(G1674)', 'description': '(ITRF2008 used)', 'htdp_id': 8, 'uncertainty': 0, 'epoch': 2000.0},
67
+ 7663: {'name': 'WGS_84(G1674)', 'description': '(ITRF2008 used)', 'htdp_id': 8, 'uncertainty': 0, 'epoch': 2000.0},
68
+ 7664: {'name': 'WGS_84(G1762)', 'description': '(IGb08 used)', 'htdp_id': 9, 'uncertainty': 0, 'epoch': 2000.0},
69
+ 7665: {'name': 'WGS_84(G1762)', 'description': '(IGb08 used)', 'htdp_id': 9, 'uncertainty': 0, 'epoch': 2000.0},
70
+ 7666: {'name': 'WGS_84(G2139)', 'description': '(ITRF2014=IGS14=IGb14 used)', 'htdp_id': 10, 'uncertainty': 0, 'epoch': 1997.0},
71
+ 7667: {'name': 'WGS_84(G2139)', 'description': '(ITRF2014=IGS14=IGb14 used)', 'htdp_id': 10, 'uncertainty': 0, 'epoch': 1997.0},
72
+ 4910: {'name': 'ITRF88', 'description': '', 'htdp_id': 11, 'uncertainty': 0, 'epoch': 1988.0},
73
+ 4911: {'name': 'ITRF89', 'description': '', 'htdp_id': 12, 'uncertainty': 0, 'epoch': 1988.0},
74
+ 7901: {'name': 'ITRF89', 'description': '', 'htdp_id': 12, 'uncertainty': 0, 'epoch': 1988.0},
75
+ 7902: {'name': 'ITRF90', 'description': '(PNEOS90/NEOS90)', 'htdp_id': 13, 'uncertainty': 0, 'epoch': 1988.0},
76
+ 7903: {'name': 'ITRF91', 'description': '', 'htdp_id': 14, 'uncertainty': 0, 'epoch': 1988.0},
77
+ 7904: {'name': 'ITRF92', 'description': '', 'htdp_id': 15, 'uncertainty': 0, 'epoch': 1988.0},
78
+ 7905: {'name': 'ITRF93', 'description': '', 'htdp_id': 16, 'uncertainty': 0, 'epoch': 1988.0},
79
+ 7906: {'name': 'ITRF94', 'description': '', 'htdp_id': 17, 'uncertainty': 0, 'epoch': 1988.0},
80
+ 7907: {'name': 'ITRF96', 'description': '', 'htdp_id': 18, 'uncertainty': 0, 'epoch': 1996.0},
81
+ 7908: {'name': 'ITRF97', 'description': 'IGS97', 'htdp_id': 19, 'uncertainty': 0, 'epoch': 1997.0},
82
+ 7909: {'name': 'ITRF2000', 'description': 'IGS00/IGb00', 'htdp_id': 20, 'uncertainty': 0, 'epoch': 2000.0},
83
+ 7910: {'name': 'ITRF2005', 'description': 'IGS05', 'htdp_id': 21, 'uncertainty': 0, 'epoch': 2000.0},
84
+ 7911: {'name': 'ITRF2008', 'description': 'IGS08/IGb08', 'htdp_id': 22, 'uncertainty': 0, 'epoch': 2000.0},
85
+ 7912: {'name': 'ELLIPSOID', 'description': 'IGS14/IGb14/WGS84/ITRF2014 Ellipsoid', 'htdp_id': 23, 'uncertainty': 0, 'epoch': 2000.0},
86
+ 1322: {'name': 'ITRF2020', 'description': 'IGS20', 'htdp_id': 24, 'uncertainty': 0, 'epoch': 2000.0},
87
+
88
+ }
89
+
90
+ CDN = {
91
+ # CONUS / Alaska / Hawaii / PR / VI
92
+ 5703: {'name': 'NAVD88 height', 'vdatum_id': 'navd88:m:height', 'default_geoid': 'g2018', 'ellipsoid': 6319},
93
+ 6360: {'name': 'NAVD88 height (usFt)', 'default_geoid': 'g2018'},
94
+ 8228: {'name': 'NAVD88 height (Ft)', 'default_geoid': 'g2018'},
95
+
96
+ # Puerto Rico
97
+ 6641: {'name': 'PRVD02 height', 'vdatum_id': 'prvd02:m:height', 'default_geoid': 'g2018', 'ellipsoid': 6319},
98
+
99
+ # Virgin Islands
100
+ 6642: {'name': 'VIVD09 height', 'vdatum_id': 'vivd09:m:height', 'default_geoid': 'g2018', 'ellipsoid': 6319},
101
+
102
+ # Canada (CGVD2013 uses CGG2013 geoid)
103
+ # Note: You need to ensure 'CGG2013' is fetchable via your fetcher or map it to a filename
104
+ 6647: {'name': 'CGVD2013(CGG2013)', 'vdatum_id': 'cgvd2013:m:height', 'default_geoid': 'CGG2013'},
105
+
106
+ # Global EGM
107
+ 3855: {'name': 'EGM2008 height', 'vdatum_id': 'egm2008:m:height', 'default_geoid': 'egm2008'},
108
+ 5773: {'name': 'EGM96 height', 'vdatum_id': 'egm96:m:height', 'default_geoid': 'egm96'},
109
+
110
+ # # Ellipsoidal (Hubs) - No Geoid needed
111
+ # 6319: {'name': 'NAD83(2011)', 'vdatum_id': 'nad83_2011:m:height'},
112
+ # 4979: {'name': 'WGS84', 'vdatum_id': 'wgs84:m:height'},
113
+ }
114
+
115
+ GEOIDS = {
116
+ # Standard PROJ-CDN Geoids (Default provider is 'proj')
117
+ 'g2018': {'name': 'geoid 2018', 'uncertainty': .0127, 'provider': 'proj'},
118
+ 'g2012b': {'name': 'geoid 2012b', 'uncertainty': .017, 'provider': 'proj'},
119
+ 'geoid09': {'name': 'geoid 2009', 'uncertainty': .05, 'provider': 'proj'},
120
+
121
+ # New XGEOIDs via VDatum (Provider is 'vdatum')
122
+ 'xgeoid20b': {'name': 'xgeoid20b', 'uncertainty': .02, 'provider': 'vdatum'},
123
+ 'xgeoid19b': {'name': 'xgeoid19b', 'uncertainty': .02, 'provider': 'vdatum'},
124
+
125
+ 'egm2008': {'name': 'EGM2008', 'uncertainty': 0, 'provider': 'proj'},
126
+ 'egm96': {'name': 'EGM96', 'uncertainty': 0, 'provider': 'proj'},
127
+
128
+ 'CGG2013': {'name': 'CGG2013', 'uncertainty': 0.01, 'provider': 'proj'},
129
+ }
130
+
131
+ @classmethod
132
+ def get_default_geoid(cls, epsg):
133
+ """Return default geoid for a generic CDN EPSG, or None."""
134
+
135
+ try:
136
+ e_int = int(epsg)
137
+ except:
138
+ return None
139
+
140
+ if e_int in cls.CDN:
141
+ return cls.CDN[e_int].get('default_geoid')
142
+ return None
143
+
144
+
145
+ @classmethod
146
+ def get_vdatum_by_name(cls, datum_name):
147
+ """Return the vertical datum EPSG based on the vertical datum name."""
148
+
149
+ if datum_name is None:
150
+ return None
151
+
152
+ try:
153
+ datum_int = int(datum_name)
154
+ except (ValueError, TypeError):
155
+ datum_int = None
156
+
157
+ for frame_set in [cls.SURFACES, cls.HTDP, cls.CDN]:
158
+ if datum_int in frame_set:
159
+ return datum_int
160
+
161
+ for epsg, info in frame_set.items():
162
+ if str(datum_name).lower() in info['name'].lower():
163
+ return epsg
164
+
165
+ return None
166
+
167
+
168
+ @classmethod
169
+ def get_vdatum_id(cls, epsg):
170
+ """Retrieve the NOAA VDatum CLI string for an EPSG."""
171
+ if epsg in cls.SURFACES: return cls.SURFACES[epsg].get('vdatum_id')
172
+ if epsg in cls.CDN: return cls.CDN[epsg].get('vdatum_id')
173
+ if epsg == 6319: return 'nad83_2011:m:height'
174
+ return None
175
+
176
+
177
+ @classmethod
178
+ def get_frame_type(cls, epsg):
179
+ """Identify which frame set an EPSG belongs to."""
180
+
181
+ if epsg in cls.SURFACES:
182
+ return 'surface'
183
+ if epsg in cls.HTDP:
184
+ return 'htdp'
185
+ if epsg in cls.CDN:
186
+ return 'cdn'
187
+ return None