Skip to content

Commit 2fe4ac1

Browse files
michaelayeclaude
andcommitted
feat: write ISIS-compatible Mapping PVL sidecar + enable -h for help
Two usability improvements: 1. csm2map now writes a `.pvl` sidecar file alongside every GeoTIFF output, containing an ISIS-compatible Mapping group with projection name, body radii, lat/lon type/direction/domain, ground range, pixel resolution, UpperLeftCornerX/Y, grid dimensions, and an AlgorithmName group identifying csm2map. This gives users transitioning from cam2map a familiar metadata format — parseable by the same tools they use on ISIS cube labels (pvl.load, catlab patterns, custom scripts). 2. Added `-h` as a help shortcut alongside `--help` for all commands via Typer's context_settings. Matches the convention most CLI users expect. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 199987d commit 2fe4ac1

3 files changed

Lines changed: 112 additions & 1 deletion

File tree

src/isistools/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def _ensure_cwd() -> None:
3535
name="isistools",
3636
help="Python review tools for ISIS3 coregistration workflows.",
3737
no_args_is_help=True,
38+
context_settings={"help_option_names": ["-h", "--help"]},
3839
)
3940

4041

src/isistools/processing/project.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
compute_transform_dense,
2424
validate_coarse_vs_dense,
2525
)
26-
from isistools.processing.writers import write_geotiff
26+
from isistools.processing.writers import write_geotiff, write_mapping_pvl
2727

2828
console = Console()
2929

@@ -273,6 +273,8 @@ def project(
273273
with _stage(timings, "write_output"):
274274
if output_format == "geotiff":
275275
result = write_geotiff(output_path, projected, grid, nodata=0.0)
276+
pvl_path = write_mapping_pvl(output_path, grid, body)
277+
console.print(f" Mapping sidecar: {pvl_path.name}")
276278
else:
277279
msg = "ISIS cube output not yet implemented"
278280
raise NotImplementedError(msg)

src/isistools/processing/writers.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
"""Output writers for map-projected data."""
22

3+
from __future__ import annotations
4+
35
from pathlib import Path
6+
from typing import TYPE_CHECKING
47

58
import numpy as np
69
import rasterio
710

811
from isistools.processing.grid import OutputGrid
912

13+
if TYPE_CHECKING:
14+
from isistools.processing.camera import TargetBody
15+
1016

1117
def write_geotiff(
1218
output_path: str | Path,
@@ -71,3 +77,105 @@ def write_geotiff(
7177
dst.write(data[b], b + 1)
7278

7379
return output_path
80+
81+
82+
# Reverse map from proj4 identifiers back to ISIS projection names.
83+
# Only needs to cover what ISIS_TO_PROJ4 in geo/projections.py maps.
84+
_PROJ4_TO_ISIS = {
85+
"eqc": "Equirectangular",
86+
"sinu": "Sinusoidal",
87+
"merc": "Mercator",
88+
"tmerc": "TransverseMercator",
89+
"stere": "PolarStereographic",
90+
"ortho": "Orthographic",
91+
"lcc": "LambertConformal",
92+
"laea": "LambertAzimuthalEqualArea",
93+
"nsper": "PointPerspective",
94+
"moll": "Mollweide",
95+
"robin": "Robinson",
96+
}
97+
98+
99+
def write_mapping_pvl(
100+
output_path: Path,
101+
grid: OutputGrid,
102+
body: "TargetBody",
103+
) -> Path:
104+
"""Write an ISIS-compatible Mapping PVL sidecar next to a GeoTIFF.
105+
106+
The sidecar carries the same metadata that ISIS ``cam2map`` writes
107+
into a projected cube's ``IsisCube.Mapping`` group: projection name,
108+
body radii, lat/lon type and direction, ground range, pixel
109+
resolution, and the ``UpperLeftCornerX/Y`` that pin the grid origin.
110+
This makes the GeoTIFF interoperable with ISIS workflows that expect
111+
a PVL Mapping group — e.g. ``automos``, ``mapmos``, or scripts that
112+
parse ``catlab`` output.
113+
114+
Parameters
115+
----------
116+
output_path : Path
117+
Path to the GeoTIFF whose sidecar this is. The PVL is written to
118+
``output_path.with_suffix('.pvl')``.
119+
grid : OutputGrid
120+
Grid definition (CRS, affine, dimensions, ground range).
121+
body : TargetBody
122+
Target body identity and ellipsoid.
123+
124+
Returns
125+
-------
126+
Path to the written ``.pvl`` file.
127+
"""
128+
# Extract the proj4 projection ID from the CRS so we can reverse-map
129+
# to the ISIS projection name.
130+
proj4 = grid.crs.to_proj4()
131+
proj_id = None
132+
center_lon = 0.0
133+
center_lat = 0.0
134+
for part in proj4.split():
135+
if part.startswith("+proj="):
136+
proj_id = part.split("=", 1)[1]
137+
elif part.startswith("+lon_0="):
138+
center_lon = float(part.split("=", 1)[1])
139+
elif part.startswith("+lat_ts="):
140+
center_lat = float(part.split("=", 1)[1])
141+
elif part.startswith("+lat_0="):
142+
# Some projections use lat_0 instead of lat_ts
143+
center_lat = float(part.split("=", 1)[1])
144+
145+
isis_proj_name = _PROJ4_TO_ISIS.get(proj_id, proj_id or "Unknown")
146+
147+
pvl_path = output_path.with_suffix(".pvl")
148+
149+
lines = [
150+
"Group = Mapping",
151+
f" ProjectionName = {isis_proj_name}",
152+
f" TargetName = {body.name}",
153+
f" EquatorialRadius = {body.radius_equatorial_m:.1f} <meters>",
154+
f" PolarRadius = {body.radius_polar_m:.1f} <meters>",
155+
" LatitudeType = Planetocentric",
156+
" LongitudeDirection = PositiveEast",
157+
" LongitudeDomain = 360",
158+
f" CenterLatitude = {center_lat}",
159+
f" CenterLongitude = {center_lon}",
160+
f" MinimumLatitude = {grid.lat_min}",
161+
f" MaximumLatitude = {grid.lat_max}",
162+
f" MinimumLongitude = {grid.lon_min}",
163+
f" MaximumLongitude = {grid.lon_max}",
164+
f" PixelResolution = {grid.resolution} <meters/pixel>",
165+
f" UpperLeftCornerX = {grid.transform.c} <meters>",
166+
f" UpperLeftCornerY = {grid.transform.f} <meters>",
167+
"End_Group",
168+
"",
169+
"Group = Dimensions",
170+
f" Samples = {grid.width}",
171+
f" Lines = {grid.height}",
172+
"End_Group",
173+
"",
174+
"Group = AlgorithmName",
175+
" Name = csm2map",
176+
" Version = isistools",
177+
"End_Group",
178+
]
179+
180+
pvl_path.write_text("\n".join(lines) + "\n")
181+
return pvl_path

0 commit comments

Comments
 (0)