Skip to content

Commit 1582f8a

Browse files
committed
Support US survey foot and international foot units (#1045)
State Plane zones in us-ft (136 zones) and ft (28 zones) now use the Numba fast path. The Krueger/LCC kernels compute in metres internally, then divide by the unit conversion factor (0.3048006 for us-ft, 0.3048 for ft) on output. x_0/y_0 from PROJ4 dicts are already in metres regardless of the units parameter.
1 parent 4564c6d commit 1582f8a

File tree

1 file changed

+49
-15
lines changed

1 file changed

+49
-15
lines changed

xrspatial/reproject/_projections.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ def _lcc_params(crs):
199199
if d.get('proj') != 'lcc':
200200
return None
201201

202+
units = d.get('units', 'm')
203+
_UNIT_TO_METER = {'m': 1.0, 'us-ft': 0.3048006096012192, 'ft': 0.3048}
204+
to_meter = _UNIT_TO_METER.get(units)
205+
if to_meter is None:
206+
return None
207+
202208
lat_1 = math.radians(d.get('lat_1', d.get('lat_0', 0.0)))
203209
lat_2 = math.radians(d.get('lat_2', lat_1))
204210
lat_0 = math.radians(d.get('lat_0', 0.0))
@@ -231,10 +237,10 @@ def _lcc_params(crs):
231237
(1.0 + e * sinphi0) / (1.0 - e * sinphi0), e / 2.0)
232238
rho0 = a * k0_param * c * math.pow(ts0, n)
233239

234-
fe = d.get('x_0', 0.0)
240+
fe = d.get('x_0', 0.0) # always in metres in PROJ4 dict
235241
fn = d.get('y_0', 0.0)
236242

237-
return lon_0, n, c, rho0, k0_param, fe, fn
243+
return lon_0, n, c, rho0, k0_param, fe, fn, to_meter
238244

239245

240246
@njit(nogil=True, cache=True)
@@ -1228,16 +1234,25 @@ def _tmerc_params(crs):
12281234
return None
12291235
if d.get('proj') != 'tmerc':
12301236
return None
1231-
# Only handle meter-based CRS; non-meter units (us-ft, ft) need
1232-
# conversion that we don't implement yet.
1237+
1238+
# Unit conversion: false easting/northing from to_dict() are in
1239+
# the CRS's native units. The Krueger series works in metres,
1240+
# so we convert fe/fn to metres and return to_meter so the caller
1241+
# can scale the final projected coordinates.
12331242
units = d.get('units', 'm')
1234-
if units not in ('m', None):
1235-
return None
1243+
_UNIT_TO_METER = {
1244+
'm': 1.0,
1245+
'us-ft': 0.3048006096012192, # US survey foot
1246+
'ft': 0.3048, # international foot
1247+
}
1248+
to_meter = _UNIT_TO_METER.get(units)
1249+
if to_meter is None:
1250+
return None # unsupported unit
12361251

12371252
lon_0 = math.radians(d.get('lon_0', 0.0))
12381253
lat_0 = math.radians(d.get('lat_0', 0.0))
12391254
k0 = float(d.get('k_0', d.get('k', 1.0)))
1240-
fe = d.get('x_0', 0.0)
1255+
fe = d.get('x_0', 0.0) # always in metres in PROJ4 dict
12411256
fn = d.get('y_0', 0.0)
12421257

12431258
# Compute Zb: northing offset for the origin latitude.
@@ -1260,7 +1275,7 @@ def _tmerc_params(crs):
12601275
dCn_val = _clenshaw_complex_py(_ALPHA, sin2Z, cos2Z, 0.0, 1.0)
12611276
Zb = -Qn * (Z + dCn_val)
12621277

1263-
return lon_0, k0, fe, fn, Zb
1278+
return lon_0, k0, fe, fn, Zb, to_meter
12641279

12651280

12661281
# ---------------------------------------------------------------------------
@@ -1354,21 +1369,31 @@ def try_numba_transform(src_crs, tgt_crs, chunk_bounds, chunk_shape):
13541369
if _is_geographic_wgs84_or_nad83(src_epsg):
13551370
tmerc_p = _tmerc_params(tgt_crs)
13561371
if tmerc_p is not None:
1357-
lon0, k0, fe, fn, Zb = tmerc_p
1372+
lon0, k0, fe, fn, Zb, to_m = tmerc_p
13581373
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,
1374+
# Input coords are in native CRS units; convert to metres
1375+
if to_m != 1.0:
1376+
out_x_m = out_x_flat * to_m
1377+
out_y_m = out_y_flat * to_m
1378+
else:
1379+
out_x_m = out_x_flat
1380+
out_y_m = out_y_flat
1381+
tmerc_inverse(out_x_m, out_y_m, src_x_flat, src_y_flat,
13611382
lon0, k0, fe, fn + Zb, Qn, _BETA, _CGB)
13621383
return (src_y_flat.reshape(height, width),
13631384
src_x_flat.reshape(height, width))
13641385

13651386
if _is_geographic_wgs84_or_nad83(tgt_epsg):
13661387
tmerc_p = _tmerc_params(src_crs)
13671388
if tmerc_p is not None:
1368-
lon0, k0, fe, fn, Zb = tmerc_p
1389+
lon0, k0, fe, fn, Zb, to_m = tmerc_p
13691390
Qn = k0 * _A_RECT
1391+
# tmerc_forward outputs metres; convert back to native units
13701392
tmerc_forward(out_x_flat, out_y_flat, src_x_flat, src_y_flat,
13711393
lon0, k0, fe, fn + Zb, Qn, _ALPHA, _CBG)
1394+
if to_m != 1.0:
1395+
src_x_flat /= to_m
1396+
src_y_flat /= to_m
13721397
return (src_y_flat.reshape(height, width),
13731398
src_x_flat.reshape(height, width))
13741399

@@ -1392,18 +1417,27 @@ def try_numba_transform(src_crs, tgt_crs, chunk_bounds, chunk_shape):
13921417
if _is_geographic_wgs84_or_nad83(src_epsg):
13931418
params = _lcc_params(tgt_crs)
13941419
if params is not None:
1395-
lon0, nn, c, rho0, k0, fe, fn = params
1396-
lcc_inverse(out_x_flat, out_y_flat, src_x_flat, src_y_flat,
1420+
lon0, nn, c, rho0, k0, fe, fn, to_m = params
1421+
if to_m != 1.0:
1422+
out_x_m = out_x_flat * to_m
1423+
out_y_m = out_y_flat * to_m
1424+
else:
1425+
out_x_m = out_x_flat
1426+
out_y_m = out_y_flat
1427+
lcc_inverse(out_x_m, out_y_m, src_x_flat, src_y_flat,
13971428
lon0, nn, c, rho0, k0, fe, fn, _WGS84_E, _WGS84_A)
13981429
return (src_y_flat.reshape(height, width),
13991430
src_x_flat.reshape(height, width))
14001431

14011432
if _is_geographic_wgs84_or_nad83(tgt_epsg):
14021433
params = _lcc_params(src_crs)
14031434
if params is not None:
1404-
lon0, nn, c, rho0, k0, fe, fn = params
1435+
lon0, nn, c, rho0, k0, fe, fn, to_m = params
14051436
lcc_forward(out_x_flat, out_y_flat, src_x_flat, src_y_flat,
14061437
lon0, nn, c, rho0, k0, fe, fn, _WGS84_E, _WGS84_A)
1438+
if to_m != 1.0:
1439+
src_x_flat /= to_m
1440+
src_y_flat /= to_m
14071441
return (src_y_flat.reshape(height, width),
14081442
src_x_flat.reshape(height, width))
14091443

0 commit comments

Comments
 (0)