Skip to content

Commit 4564c6d

Browse files
committed
Add generic tmerc dispatch for State Plane zones (#1045)
State Plane zones that use Transverse Mercator (Maine, New Hampshire, Wisconsin, etc.) now hit the Numba fast path. Uses the same Krueger 6th-order series as UTM, with a Zb offset for non-zero lat_0. Meter-based zones only; US survey foot zones fall back to pyproj.
1 parent f155a78 commit 4564c6d

File tree

1 file changed

+100
-0
lines changed

1 file changed

+100
-0
lines changed

xrspatial/reproject/_projections.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,36 @@ def _tmerc_coefficients(n):
10051005
_ALPHA, _BETA, _CBG, _CGB, _A_RECT = _tmerc_coefficients(_WGS84_N)
10061006

10071007

1008+
def _clenshaw_sin_py(coeffs, angle):
1009+
"""Pure-Python version of _clenshaw_sin for use in setup code."""
1010+
N = len(coeffs)
1011+
X = 2.0 * math.cos(2.0 * angle)
1012+
u0 = 0.0
1013+
u1 = 0.0
1014+
for k in range(N - 1, -1, -1):
1015+
t = X * u0 - u1 + coeffs[k]
1016+
u1 = u0
1017+
u0 = t
1018+
return math.sin(2.0 * angle) * u0
1019+
1020+
1021+
def _clenshaw_complex_py(coeffs, sin2Cn, cos2Cn, sinh2Ce, cosh2Ce):
1022+
"""Pure-Python version of _clenshaw_complex for use in setup code.
1023+
1024+
Returns just dCn (real part).
1025+
"""
1026+
N = len(coeffs)
1027+
r = 2.0 * cos2Cn * cosh2Ce
1028+
im = -2.0 * sin2Cn * sinh2Ce
1029+
hr = 0.0; hi = 0.0; hr1 = 0.0; hi1 = 0.0
1030+
for k in range(N - 1, -1, -1):
1031+
hr2 = hr1; hi2 = hi1; hr1 = hr; hi1 = hi
1032+
hr = -hr2 + r * hr1 - im * hi1 + coeffs[k]
1033+
hi = -hi2 + im * hr1 + r * hi1
1034+
dCn = sin2Cn * cosh2Ce * hr - cos2Cn * sinh2Ce * hi
1035+
return dCn
1036+
1037+
10081038
@njit(nogil=True, cache=True)
10091039
def _clenshaw_sin(coeffs, angle):
10101040
"""Evaluate SUM_{k=1}^{N} coeffs[k-1] * sin(2*k*angle) via Clenshaw."""
@@ -1185,6 +1215,54 @@ def _utm_params(epsg_code):
11851215
return lon0, k0, false_e, false_n
11861216

11871217

1218+
def _tmerc_params(crs):
1219+
"""Extract generic Transverse Mercator parameters from a pyproj CRS.
1220+
1221+
Handles State Plane, national grids, and any other tmerc definition.
1222+
Returns (lon0_rad, k0, false_easting, false_northing, Zb) or None.
1223+
Zb is the Krueger northing offset for non-zero lat_0.
1224+
"""
1225+
try:
1226+
d = crs.to_dict()
1227+
except Exception:
1228+
return None
1229+
if d.get('proj') != 'tmerc':
1230+
return None
1231+
# Only handle meter-based CRS; non-meter units (us-ft, ft) need
1232+
# conversion that we don't implement yet.
1233+
units = d.get('units', 'm')
1234+
if units not in ('m', None):
1235+
return None
1236+
1237+
lon_0 = math.radians(d.get('lon_0', 0.0))
1238+
lat_0 = math.radians(d.get('lat_0', 0.0))
1239+
k0 = float(d.get('k_0', d.get('k', 1.0)))
1240+
fe = d.get('x_0', 0.0)
1241+
fn = d.get('y_0', 0.0)
1242+
1243+
# Compute Zb: northing offset for the origin latitude.
1244+
# For lat_0=0 (UTM), Zb=0.
1245+
Qn = k0 * _A_RECT
1246+
if abs(lat_0) < 1e-14:
1247+
Zb = 0.0
1248+
else:
1249+
# Conformal latitude of origin
1250+
Z = lat_0 + _clenshaw_sin_py(_CBG, lat_0)
1251+
# Forward Krueger correction at Ce=0 (central meridian)
1252+
sin2Z = math.sin(2.0 * Z)
1253+
cos2Z = math.cos(2.0 * Z)
1254+
dCn = 0.0
1255+
for k in range(5, -1, -1):
1256+
dCn = cos2Z * dCn + _ALPHA[k] * sin2Z
1257+
# This is a simplified Clenshaw for Ce=0 (sinh=0, cosh=1)
1258+
# Actually, use the proper complex Clenshaw with Ce=0:
1259+
# sin2=sin(2Z), cos2=cos(2Z), sinh2=0, cosh2=1
1260+
dCn_val = _clenshaw_complex_py(_ALPHA, sin2Z, cos2Z, 0.0, 1.0)
1261+
Zb = -Qn * (Z + dCn_val)
1262+
1263+
return lon_0, k0, fe, fn, Zb
1264+
1265+
11881266
# ---------------------------------------------------------------------------
11891267
# Dispatch: detect fast-path CRS pairs
11901268
# ---------------------------------------------------------------------------
@@ -1272,6 +1350,28 @@ def try_numba_transform(src_crs, tgt_crs, chunk_bounds, chunk_shape):
12721350
return (src_y_flat.reshape(height, width),
12731351
src_x_flat.reshape(height, width))
12741352

1353+
# --- Generic Transverse Mercator (State Plane, national grids, etc.) ---
1354+
if _is_geographic_wgs84_or_nad83(src_epsg):
1355+
tmerc_p = _tmerc_params(tgt_crs)
1356+
if tmerc_p is not None:
1357+
lon0, k0, fe, fn, Zb = tmerc_p
1358+
Qn = k0 * _A_RECT
1359+
# fn already includes false northing; Zb is the origin offset
1360+
tmerc_inverse(out_x_flat, out_y_flat, src_x_flat, src_y_flat,
1361+
lon0, k0, fe, fn + Zb, Qn, _BETA, _CGB)
1362+
return (src_y_flat.reshape(height, width),
1363+
src_x_flat.reshape(height, width))
1364+
1365+
if _is_geographic_wgs84_or_nad83(tgt_epsg):
1366+
tmerc_p = _tmerc_params(src_crs)
1367+
if tmerc_p is not None:
1368+
lon0, k0, fe, fn, Zb = tmerc_p
1369+
Qn = k0 * _A_RECT
1370+
tmerc_forward(out_x_flat, out_y_flat, src_x_flat, src_y_flat,
1371+
lon0, k0, fe, fn + Zb, Qn, _ALPHA, _CBG)
1372+
return (src_y_flat.reshape(height, width),
1373+
src_x_flat.reshape(height, width))
1374+
12751375
# --- Ellipsoidal Mercator (EPSG:3395) ---
12761376
if _is_geographic_wgs84_or_nad83(src_epsg) and tgt_epsg == 3395:
12771377
emerc_inverse(out_x_flat, out_y_flat, src_x_flat, src_y_flat,

0 commit comments

Comments
 (0)