Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* **Reworked NOAA VDatum processing:** each regional coverage package is completed through its native geodetic chain, including modern IGS/xGEOID paths, before normalized coverages are mosaiced according to coverage priority.
* **Physical coastal context:** resolution-independent inland decay based on Dist2Coast distances, with VDatum-aware estuary/river coverage, inland buffers, configurable shoreline extension, and optional unrestricted extrapolation for hydrodynamic modeling.
* **Reference and system inspection:** expanded CLI commands for inspecting references, providers, transformation plans, external engines, and runtime configuration.
* `vdatum:mtl` (mean tide level) and `vdatum:dtl` (diurnal tide level) references.
* Common vertical datum names (e.g. `navd88`, `egm2008`, `egm96`, `prvd02`, `vivd09`, `cgvd2013`, `mean lower low water`) are accepted by `parse_reference()`.

### Changed

Expand All @@ -43,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Fixed Dist2Coast processing that could introduce edge artifacts or incorrectly discard zero-valued coastline cells as nodata.
* Fixed HTDP latitude/longitude handling and validated frame transformations against the configured HTDP engine version.
* Fixed Dist2Coast fetch failure. Now raises MissingGridException rather than returning None.
* Fixed VDatum model grids (e.g. xGEOID20B) being extracted into the current working directory instead of the cache directory.
* `InvalidReferenceError` and `UnsupportedReferenceError` now derive from `ReferenceInputError` (a `ValueError`) instead of Python's built-in `ReferenceError`.

### Deprecated

Expand Down
34 changes: 34 additions & 0 deletions src/transformez/reference/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ class HtdpFrameBinding:
unit_name="metre",
unit_to_metre=1.0,
),
"vdatum:mtl": VerticalReference(
id="vdatum:mtl",
name="NOAA VDatum Mean Tide Level",
kind=VerticalKind.TIDAL_HEIGHT,
axis_direction=AxisDirection.UP,
unit_name="metre",
unit_to_metre=1.0,
),
"vdatum:dtl": VerticalReference(
id="vdatum:dtl",
name="NOAA VDatum Diurnal Tide Level",
kind=VerticalKind.TIDAL_HEIGHT,
axis_direction=AxisDirection.UP,
unit_name="metre",
unit_to_metre=1.0,
),
"vdatum:xgeoid17b": VerticalReference(
id="vdatum:xgeoid17b",
name="NGS X-GEOID 17b",
Expand Down Expand Up @@ -213,6 +229,24 @@ class HtdpFrameBinding:
default_model="geoid:g2018",
global_proxy="global:hat",
),
"vdatum:mtl": OperationBinding(
reference_id="vdatum:mtl",
engine="vdatum_grid",
provider="vdatum",
provider_datum="mtl",
native_frame="EPSG:6319",
default_model="geoid:g2018",
global_proxy="global:mss",
),
"vdatum:dtl": OperationBinding(
reference_id="vdatum:dtl",
engine="vdatum_grid",
provider="vdatum",
provider_datum="dtl",
native_frame="EPSG:6319",
default_model="geoid:g2018",
global_proxy="global:mss",
),
"vdatum:xgeoid17b": OperationBinding(
reference_id="vdatum:xgeoid17b",
engine="vdatum_grid",
Expand Down
4 changes: 3 additions & 1 deletion src/transformez/reference/fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,9 @@ def _fetch_vdatum_model_grid(
)
continue

for grid_path in p_f_extract(archive_path, members=[archive_member]):
for grid_path in p_f_extract(
archive_path, outdir=self.cache_dir, members=[archive_member]
):
if grid_path.exists() and grid_path.suffix.casefold() == ".gtx":
grid_paths.append(grid_path)

Expand Down
43 changes: 39 additions & 4 deletions src/transformez/reference/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,46 @@
}


# Common short names for vertical references that PROJ does not recognise on its
# own. Keys are casefolded with internal whitespace collapsed. Names PROJ already
# resolves (e.g. "NAVD88 height", "WGS84") are deliberately left to PROJ.
COMMON_NAMES = {
# NAVD88
"navd88": "epsg:5703",
"navd 88": "epsg:5703",
"navd-88": "epsg:5703",
"north american vertical datum 1988": "epsg:5703",
# Tidal datums spelled out
"mean lower low water": "vdatum:mllw",
"mean low water": "vdatum:mlw",
"mean high water": "vdatum:mhw",
"mean sea level": "vdatum:msl",
# US territories
"prvd02": "epsg:6641",
"vivd09": "epsg:6642",
"asvd02": "epsg:6643",
# Canada
"cgvd2013": "epsg:6647",
"cgvd2013(cgg2013)": "epsg:6647",
# Global geoid models
"egm2008": "epsg:3855",
"egm 2008": "epsg:3855",
"egm08": "epsg:3855",
"egm96": "epsg:5773",
"egm 96": "epsg:5773",
}


class ReferenceInputError(ValueError):
"""Base error for invalid or unsupported reference input."""


class InvalidReferenceError(ReferenceError):
pass
class InvalidReferenceError(ReferenceInputError):
"""The reference could not be parsed or is not recognised."""


class UnsupportedReferenceError(ReferenceError):
pass
class UnsupportedReferenceError(ReferenceInputError):
"""The reference is valid but cannot be used by transformez."""


def warn_legacy_alias(old: str, new: str):
Expand Down Expand Up @@ -267,6 +297,11 @@ def parse_reference(value: ReferenceInput) -> ParsedReference:
warn_legacy_alias(text, alias)
return parse_reference(alias)

# Common short names (navd88, egm2008, ...)
common = COMMON_NAMES.get(" ".join(text.casefold().split()))
if common is not None:
return parse_reference(common)

# Transformez custom namespaces
prefix = text.partition(":")[0].casefold()
if prefix in CUSTOM_REFERENCE_PREFIXES:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,6 @@ def fake_extract(src, **kwargs):
assert calls == [
(
archive,
{"members": ["CONUSPAC.gtx"]},
{"outdir": tmp_path, "members": ["CONUSPAC.gtx"]},
)
]
64 changes: 64 additions & 0 deletions tests/test_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from transformez.reference.parser import (
parse_reference,
InvalidReferenceError,
ReferenceInputError,
UnsupportedReferenceError,
)
from transformez.reference.types import ParsedReference, VerticalKind, AxisDirection
Expand Down Expand Up @@ -131,3 +132,66 @@ def test_parse_vertical_crs():
assert ref.horizontal is None
assert ref.vertical is not None
assert ref.vertical.kind is VerticalKind.GRAVITY_RELATED_HEIGHT


@pytest.mark.parametrize(
"name, epsg",
[
("navd88", 5703),
("NAVD 88", 5703),
("egm2008", 3855),
("EGM96", 5773),
("prvd02", 6641),
("vivd09", 6642),
("asvd02", 6643),
("cgvd2013", 6647),
("North American Vertical Datum 1988", 5703),
],
)
def test_common_names(name, epsg):
"""Ensure common short names resolve to their EPSG vertical references."""

ref = parse_reference(name)
assert ref.horizontal is None
assert ref.vertical.id == f"epsg:{epsg}"


@pytest.mark.parametrize(
"name, ref_id",
[
("mean lower low water", "vdatum:mllw"),
("Mean Low Water", "vdatum:mlw"),
("mean high water", "vdatum:mhw"),
("mean sea level", "vdatum:msl"),
],
)
def test_common_tidal_names(name, ref_id):
"""Ensure spelled-out tidal datum names resolve to VDatum references."""

assert parse_reference(name).vertical.id == ref_id


def test_common_names_leave_proj_names_alone():
"""Names PROJ already knows must keep resolving through PROJ."""

assert parse_reference("wgs84").horizontal.to_epsg() == 4326
assert parse_reference("NAVD88 height").vertical.id == "epsg:5703"


@pytest.mark.parametrize("name", ["vdatum:mtl", "vdatum:dtl"])
def test_mtl_dtl_references(name):
ref = parse_reference(name)
assert ref.vertical.id == name
assert ref.vertical.kind == VerticalKind.TIDAL_HEIGHT


def test_reference_errors_share_a_value_error_base():
"""All reference errors can be caught together, including as ValueError."""

for exc in (InvalidReferenceError, UnsupportedReferenceError):
assert issubclass(exc, ReferenceInputError)
assert issubclass(exc, ValueError)
assert not issubclass(exc, ReferenceError)

with pytest.raises(ValueError):
parse_reference("not_a_datum:12345")
Loading