@@ -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 )
10091039def _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