Skip to content

Commit 0910f97

Browse files
authored
ENH: Added Proj.get_factors() (#548)
1 parent 2319b64 commit 0910f97

File tree

7 files changed

+339
-14
lines changed

7 files changed

+339
-14
lines changed

docs/api/proj.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ pyproj.Proj
1818
:inherited-members:
1919
:special-members: __init__, __call__
2020
:show-inheritance:
21+
22+
23+
pyproj.proj.Factors
24+
-------------------
25+
26+
.. autoclass:: pyproj.proj.Factors
27+
:members:

docs/history.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Change Log
22
==========
33

4+
2.6.0
5+
~~~~~
6+
* ENH: Added Proj.get_factors() (issue #503)
7+
48
2.5.0
59
~~~~~
610
* Wheels contain PROJ version is 6.3.1

pyproj/_proj.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
include "proj.pxi"
22

3-
cdef class Proj:
3+
cdef class _Proj:
44
cdef PJ * projobj
55
cdef PJ_CONTEXT* context
66
cdef PJ_PROJ_INFO projobj_info

pyproj/_proj.pyx

Lines changed: 175 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,76 @@
11
include "base.pxi"
22

3+
import copy
34
import warnings
5+
from collections import namedtuple
46

57
cimport cython
68

79
from pyproj._datadir cimport pyproj_context_initialize
810
from pyproj.compat import cstrencode, pystrdecode
911
from pyproj.exceptions import ProjError
1012

11-
# # version number string for PROJ
13+
# version number string for PROJ
1214
proj_version_str = "{0}.{1}.{2}".format(
1315
PROJ_VERSION_MAJOR,
1416
PROJ_VERSION_MINOR,
1517
PROJ_VERSION_PATCH
1618
)
1719

18-
cdef class Proj:
20+
21+
Factors = namedtuple("Factors",
22+
[
23+
"meridional_scale",
24+
"parallel_scale",
25+
"areal_scale",
26+
"angular_distortion",
27+
"meridian_parallel_angle",
28+
"meridian_convergence",
29+
"tissot_semimajor",
30+
"tissot_semiminor",
31+
"dx_dlam",
32+
"dx_dphi",
33+
"dy_dlam",
34+
"dy_dphi",
35+
]
36+
)
37+
Factors.__doc__ = """
38+
.. versionadded:: 2.6.0
39+
40+
These are the scaling and angular distortion factors.
41+
42+
See `PJ_FACTORS documentation <https://proj.org/development/reference/datatypes.html?highlight=pj_factors#c.PJ_FACTORS>`__
43+
44+
Parameters
45+
----------
46+
meridional_scale: List[float]
47+
Meridional scale at coordinate.
48+
parallel_scale: List[float]
49+
Parallel scale at coordinate.
50+
areal_scale: List[float]
51+
Areal scale factor at coordinate.
52+
angular_distortion: List[float]
53+
Angular distortion at coordinate.
54+
meridian_parallel_angle: List[float]
55+
Meridian/parallel angle at coordinate.
56+
meridian_convergence: List[float]
57+
Meridian convergence at coordinate. Sometimes also described as *grid declination*.
58+
tissot_semimajor: List[float]
59+
Maximum scale factor.
60+
tissot_semiminor: List[float]
61+
Minimum scale factor.
62+
dx_dlam: List[float]
63+
Partial derivative of coordinate.
64+
dx_dphi: List[float]
65+
Partial derivative of coordinate.
66+
dy_dlam: List[float]
67+
Partial derivative of coordinate.
68+
dy_dphi: List[float]
69+
Partial derivative of coordinate.
70+
"""
71+
72+
73+
cdef class _Proj:
1974
def __cinit__(self):
2075
self.projobj = NULL
2176
self.context = NULL
@@ -187,19 +242,134 @@ cdef class Proj:
187242
ybuff.data[iii] = projlonlatout.uv.v
188243
ProjError.clear()
189244

245+
@cython.boundscheck(False)
246+
@cython.wraparound(False)
247+
def _get_factors(self, longitude, latitude, bint radians, bint errcheck):
248+
"""
249+
Calculates the projection factors PJ_FACTORS
250+
251+
Equivalent to `proj -S` command line.
252+
"""
253+
cdef PyBuffWriteManager lonbuff = PyBuffWriteManager(longitude)
254+
cdef PyBuffWriteManager latbuff = PyBuffWriteManager(longitude)
255+
256+
if not lonbuff.len or not (lonbuff.len == latbuff.len):
257+
raise ProjError('longitude and latitude must be same size')
258+
259+
# prepare the factors output
260+
meridional_scale = copy.copy(longitude)
261+
parallel_scale = copy.copy(longitude)
262+
areal_scale = copy.copy(longitude)
263+
angular_distortion = copy.copy(longitude)
264+
meridian_parallel_angle = copy.copy(longitude)
265+
meridian_convergence = copy.copy(longitude)
266+
tissot_semimajor = copy.copy(longitude)
267+
tissot_semiminor = copy.copy(longitude)
268+
dx_dlam = copy.copy(longitude)
269+
dx_dphi = copy.copy(longitude)
270+
dy_dlam = copy.copy(longitude)
271+
dy_dphi = copy.copy(longitude)
272+
cdef PyBuffWriteManager meridional_scale_buff = PyBuffWriteManager(meridional_scale)
273+
cdef PyBuffWriteManager parallel_scale_buff = PyBuffWriteManager(parallel_scale)
274+
cdef PyBuffWriteManager areal_scale_buff = PyBuffWriteManager(areal_scale)
275+
cdef PyBuffWriteManager angular_distortion_buff = PyBuffWriteManager(angular_distortion)
276+
cdef PyBuffWriteManager meridian_parallel_angle_buff = PyBuffWriteManager(meridian_parallel_angle)
277+
cdef PyBuffWriteManager meridian_convergence_buff = PyBuffWriteManager(meridian_convergence)
278+
cdef PyBuffWriteManager tissot_semimajor_buff = PyBuffWriteManager(tissot_semimajor)
279+
cdef PyBuffWriteManager tissot_semiminor_buff = PyBuffWriteManager(tissot_semiminor)
280+
cdef PyBuffWriteManager dx_dlam_buff = PyBuffWriteManager(dx_dlam)
281+
cdef PyBuffWriteManager dx_dphi_buff = PyBuffWriteManager(dx_dphi)
282+
cdef PyBuffWriteManager dy_dlam_buff = PyBuffWriteManager(dy_dlam)
283+
cdef PyBuffWriteManager dy_dphi_buff = PyBuffWriteManager(dy_dphi)
284+
285+
# calculate the factors
286+
cdef PJ_COORD pj_coord = proj_coord(0, 0, 0, HUGE_VAL)
287+
cdef PJ_FACTORS pj_factors
288+
cdef int errno = 0
289+
cdef bint invalid_coord = 0
290+
cdef Py_ssize_t iii
291+
with nogil:
292+
for iii in range(lonbuff.len):
293+
pj_coord.uv.u = lonbuff.data[iii]
294+
pj_coord.uv.v = latbuff.data[iii]
295+
if not radians:
296+
pj_coord.uv.u *= _DG2RAD
297+
pj_coord.uv.v *= _DG2RAD
298+
299+
# set both to HUGE_VAL if inf or nan
300+
proj_errno_reset(self.projobj)
301+
if pj_coord.uv.v == HUGE_VAL \
302+
or pj_coord.uv.v != pj_coord.uv.v \
303+
or pj_coord.uv.u == HUGE_VAL \
304+
or pj_coord.uv.u != pj_coord.uv.u:
305+
invalid_coord = True
306+
else:
307+
invalid_coord = False
308+
pj_factors = proj_factors(self.projobj, pj_coord)
309+
310+
errno = proj_errno(self.projobj)
311+
if errcheck and errno:
312+
with gil:
313+
raise ProjError("proj error: {}".format(
314+
pystrdecode(proj_errno_string(errno))))
315+
316+
if errno or invalid_coord:
317+
meridional_scale_buff.data[iii] = HUGE_VAL
318+
parallel_scale_buff.data[iii] = HUGE_VAL
319+
areal_scale_buff.data[iii] = HUGE_VAL
320+
angular_distortion_buff.data[iii] = HUGE_VAL
321+
meridian_parallel_angle_buff.data[iii] = HUGE_VAL
322+
meridian_convergence_buff.data[iii] = HUGE_VAL
323+
tissot_semimajor_buff.data[iii] = HUGE_VAL
324+
tissot_semiminor_buff.data[iii] = HUGE_VAL
325+
dx_dlam_buff.data[iii] = HUGE_VAL
326+
dx_dphi_buff.data[iii] = HUGE_VAL
327+
dy_dlam_buff.data[iii] = HUGE_VAL
328+
dy_dphi_buff.data[iii] = HUGE_VAL
329+
else:
330+
meridional_scale_buff.data[iii] = pj_factors.meridional_scale
331+
parallel_scale_buff.data[iii] = pj_factors.parallel_scale
332+
areal_scale_buff.data[iii] = pj_factors.areal_scale
333+
angular_distortion_buff.data[iii] = pj_factors.angular_distortion * _RAD2DG
334+
meridian_parallel_angle_buff.data[iii] = pj_factors.meridian_parallel_angle * _RAD2DG
335+
meridian_convergence_buff.data[iii] = pj_factors.meridian_convergence * _RAD2DG
336+
tissot_semimajor_buff.data[iii] = pj_factors.tissot_semimajor
337+
tissot_semiminor_buff.data[iii] = pj_factors.tissot_semiminor
338+
dx_dlam_buff.data[iii] = pj_factors.dx_dlam
339+
dx_dphi_buff.data[iii] = pj_factors.dx_dphi
340+
dy_dlam_buff.data[iii] = pj_factors.dy_dlam
341+
dy_dphi_buff.data[iii] = pj_factors.dy_dphi
342+
343+
ProjError.clear()
344+
345+
return Factors(
346+
meridional_scale=meridional_scale,
347+
parallel_scale=parallel_scale,
348+
areal_scale=areal_scale,
349+
angular_distortion=angular_distortion,
350+
meridian_parallel_angle=meridian_parallel_angle,
351+
meridian_convergence=meridian_convergence,
352+
tissot_semimajor=tissot_semimajor,
353+
tissot_semiminor=tissot_semiminor,
354+
dx_dlam=dx_dlam,
355+
dx_dphi=dx_dphi,
356+
dy_dlam=dy_dlam,
357+
dy_dphi=dy_dphi,
358+
)
359+
190360
def __repr__(self):
191361
return "Proj('{srs}', preserve_units=True)".format(srs=self.srs)
192362

193-
def _is_exact_same(self, Proj other):
363+
def _is_exact_same(self, _Proj other):
194364
return proj_is_equivalent_to(
195365
self.projobj, other.projobj, PJ_COMP_STRICT) == 1
196366

197-
def _is_equivalent(self, Proj other):
367+
def _is_equivalent(self, _Proj other):
198368
return proj_is_equivalent_to(
199369
self.projobj, other.projobj, PJ_COMP_EQUIVALENT) == 1
200370

201371
def is_exact_same(self, other):
202372
"""Compares Proj objects to see if they are exactly the same."""
203-
if not isinstance(other, Proj):
373+
if not isinstance(other, _Proj):
204374
return False
205375
return self._is_exact_same(other)

pyproj/proj.pxi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,3 +434,19 @@ cdef extern from "proj.h":
434434
PROJ_GRID_AVAILABILITY_USED_FOR_SORTING
435435
PROJ_GRID_AVAILABILITY_DISCARD_OPERATION_IF_MISSING_GRID
436436
PROJ_GRID_AVAILABILITY_IGNORED
437+
438+
ctypedef struct PJ_FACTORS:
439+
double meridional_scale
440+
double parallel_scale
441+
double areal_scale
442+
double angular_distortion
443+
double meridian_parallel_angle
444+
double meridian_convergence
445+
double tissot_semimajor
446+
double tissot_semiminor
447+
double dx_dlam
448+
double dx_dphi
449+
double dy_dlam
450+
double dy_dphi
451+
452+
PJ_FACTORS proj_factors(PJ *P, PJ_COORD lp) nogil

pyproj/proj.py

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,16 @@
3737
import re
3838
import warnings
3939

40-
from pyproj import _proj
4140
from pyproj._list import get_proj_operations_map
41+
from pyproj._proj import Factors, _Proj, proj_version_str # noqa: F401
4242
from pyproj.compat import cstrencode, pystrdecode
4343
from pyproj.crs import CRS
4444
from pyproj.utils import _convertback, _copytobuffer
4545

46-
# import numpy as np
47-
proj_version_str = _proj.proj_version_str
48-
4946
pj_list = get_proj_operations_map()
5047

5148

52-
class Proj(_proj.Proj):
49+
class Proj(_Proj):
5350
"""
5451
Performs cartographic transformations (converts from
5552
longitude,latitude to native map projection x,y coordinates and
@@ -204,6 +201,73 @@ def __call__(self, *args, **kw):
204201
outy = _convertback(yisfloat, yislist, xistuple, iny)
205202
return outx, outy
206203

204+
def get_factors(
205+
self, longitude, latitude, radians=False, errcheck=False,
206+
):
207+
"""
208+
.. versionadded:: 2.6.0
209+
210+
Calculate various cartographic properties, such as scale factors, angular
211+
distortion and meridian convergence. Depending on the underlying projection
212+
values will be calculated either numerically (default) or analytically.
213+
214+
The function also calculates the partial derivatives of the given
215+
coordinate.
216+
217+
Parameters
218+
----------
219+
longitude: scalar or array (numpy or python)
220+
Input longitude coordinate(s).
221+
latitude: scalar or array (numpy or python)
222+
Input latitude coordinate(s).
223+
radians: boolean, optional
224+
If True, will expect input data to be in radians.
225+
Default is False (degrees).
226+
errcheck: boolean, optional (default False)
227+
If True an exception is raised if the errors are found in the process.
228+
By default errcheck=False and ``inf`` is returned.
229+
230+
Returns
231+
-------
232+
Factors
233+
"""
234+
# process inputs, making copies that support buffer API.
235+
inx, xisfloat, xislist, xistuple = _copytobuffer(longitude)
236+
iny, yisfloat, yislist, yistuple = _copytobuffer(latitude)
237+
238+
# calculate the factors
239+
factors = self._get_factors(inx, iny, radians=radians, errcheck=errcheck)
240+
241+
# if inputs were lists, tuples or floats, convert back.
242+
return Factors(
243+
meridional_scale=_convertback(
244+
xisfloat, xislist, xistuple, factors.meridional_scale
245+
),
246+
parallel_scale=_convertback(
247+
xisfloat, xislist, xistuple, factors.parallel_scale
248+
),
249+
areal_scale=_convertback(xisfloat, xislist, xistuple, factors.areal_scale),
250+
angular_distortion=_convertback(
251+
xisfloat, xislist, xistuple, factors.angular_distortion
252+
),
253+
meridian_parallel_angle=_convertback(
254+
xisfloat, xislist, xistuple, factors.meridian_parallel_angle
255+
),
256+
meridian_convergence=_convertback(
257+
xisfloat, xislist, xistuple, factors.meridian_convergence
258+
),
259+
tissot_semimajor=_convertback(
260+
xisfloat, xislist, xistuple, factors.tissot_semimajor
261+
),
262+
tissot_semiminor=_convertback(
263+
xisfloat, xislist, xistuple, factors.tissot_semiminor
264+
),
265+
dx_dlam=_convertback(xisfloat, xislist, xistuple, factors.dx_dlam),
266+
dx_dphi=_convertback(xisfloat, xislist, xistuple, factors.dx_dphi),
267+
dy_dlam=_convertback(xisfloat, xislist, xistuple, factors.dy_dlam),
268+
dy_dphi=_convertback(xisfloat, xislist, xistuple, factors.dy_dphi),
269+
)
270+
207271
def definition_string(self):
208272
"""Returns formal definition string for projection
209273

0 commit comments

Comments
 (0)