Skip to content

Commit d67cfdc

Browse files
committed
Wrap project
1 parent 955a55e commit d67cfdc

File tree

4 files changed

+274
-0
lines changed

4 files changed

+274
-0
lines changed

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Operations on tabular data:
6969

7070
blockmean
7171
blockmedian
72+
project
7273
surface
7374

7475
Operations on grids:

pygmt/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
grdtrack,
4040
info,
4141
makecpt,
42+
project,
4243
surface,
4344
which,
4445
x2sys_cross,

pygmt/src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from pygmt.src.meca import meca
2626
from pygmt.src.plot import plot
2727
from pygmt.src.plot3d import plot3d
28+
from pygmt.src.project import project
2829
from pygmt.src.subplot import set_panel, subplot
2930
from pygmt.src.surface import surface
3031
from pygmt.src.text import text_ as text # "text" is an argument within "text_"

pygmt/src/project.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
"""
2+
project - Project data onto lines or great circles, or generate tracks.
3+
"""
4+
import pandas as pd
5+
from pygmt.clib import Session
6+
from pygmt.exceptions import GMTInvalidInput
7+
from pygmt.helpers import (
8+
GMTTempFile,
9+
build_arg_string,
10+
data_kind,
11+
dummy_context,
12+
fmt_docstring,
13+
kwargs_to_strings,
14+
use_alias,
15+
)
16+
17+
18+
@fmt_docstring
19+
@use_alias(
20+
A="azimuth",
21+
E="endpoint",
22+
F="flags",
23+
G="generate",
24+
L="length",
25+
N="flatearth",
26+
Q="maptypeunits",
27+
S="sort",
28+
T="rotationpole",
29+
V="verbose",
30+
W="width",
31+
Z="ellipse",
32+
f="coltypes",
33+
)
34+
@kwargs_to_strings(E="sequence", L="sequence", T="sequence", W="sequence")
35+
def project(points, center, outfile=None, **kwargs):
36+
"""
37+
Project data onto lines or great circles, or generate tracks.
38+
39+
Project reads arbitrary (x, y [, z]) data and returns any combination of
40+
(x, y, z, p, q, r, s), where (p, q) are the coordinates in the projection,
41+
(r, s) is the position in the (x, y) coordinate system of the point on the
42+
profile (q = 0 path) closest to (x, y), and z is all remaining columns in
43+
the input (beyond the required x and y columns).
44+
45+
Alternatively, Project may be used to generate (r, s, p) triples at equal
46+
increments ``dist`` along a profile. In this case (``generate`` option),
47+
the value of ``points`` is ignored (you can use, e.g., ``points=None``).
48+
49+
Projections are defined in any (but only) one of three ways:
50+
51+
(Definition 1) By a ``center`` and an ``azimuth`` in degrees
52+
clockwise from North.
53+
54+
(Definition 2) By a ``center`` and ``endpoint`` of the projection path.
55+
56+
(Definition 3) By a ``center`` and a ``rotationpole`` position.
57+
58+
To spherically project data along a great circle path, an oblique
59+
coordinate system is created which has its equator along that path, and the
60+
zero meridian through the Center. Then the oblique longitude (p)
61+
corresponds to the distance from the Center along the great circle, and the
62+
oblique latitude (q) corresponds to the distance perpendicular to the great
63+
circle path. When moving in the increasing (p) direction, (toward B or in
64+
the azimuth direction), the positive (q) direction is to your left. If a
65+
Pole has been specified, then the positive (q) direction is toward the
66+
pole.
67+
68+
To specify an oblique projection, use the ``rotationpole`` option to set
69+
the pole. Then the equator of the projection is already determined and the
70+
``center`` option is used to locate the p = 0 meridian. The center cx/cy
71+
will be taken as a point through which the p = 0 meridian passes. If you do
72+
not care to choose a particular point, use the South pole (ox = 0, oy =
73+
-90).
74+
75+
Data can be selectively windowed by using the ``length`` and ``width``
76+
options. If ``width`` is used, the projection width is set to use only
77+
points with w_min < q < w_max. If ``length`` is set, then the length is set
78+
to use only those points with l_min < p < l_max. If the ``endpoint`` option
79+
has been used to define the projection, then ``length="w"`` may be used to
80+
window the length of the projection to exactly the span from O to B.
81+
82+
Flat Earth (Cartesian) coordinate transformations can also be made. Set
83+
``flatearth=True`` and remember that azimuth is clockwise from North (the
84+
y axis), NOT the usual cartesian theta, which is counterclockwise from the
85+
x axis. azimuth = 90 - theta.
86+
87+
No assumptions are made regarding the units for x, y, r, s, p, q, dist,
88+
l_min, l_max, w_min, w_max. If -Q is selected, map units are assumed and x,
89+
y, r, s must be in degrees and p, q, dist, l_min, l_max, w_min, w_max will
90+
be in km.
91+
92+
Calculations of specific great-circle and geodesic distances or for
93+
back-azimuths or azimuths are better done using :gmt-docs:`mapproject` as
94+
project is strictly spherical.
95+
96+
Project is CASE SENSITIVE: use lower case for the **xyzpqrs** letters
97+
in ``flags``.
98+
99+
{aliases}
100+
101+
Parameters
102+
----------
103+
points : pandas.DataFrame or str
104+
Either a table with (x, y) or (lon, lat) values in the first two
105+
columns, or a filename (e.g. csv, txt format). More columns may be
106+
present.
107+
108+
center : str or list
109+
*cx*/*cy*.
110+
*cx/cy* sets the origin of the projection, in Definition 1 or 2. If
111+
Definition 3 is used, then *cx/cy* are the coordinates of a
112+
point through which the oblique zero meridian (*p* = 0) should pass.
113+
The *cx/cy* is not required to be 90 degrees from the pole.
114+
115+
azimuth : float or str
116+
defines the azimuth of the projection (Definition 1).
117+
118+
endpoint : str or list
119+
*bx*/*by*.
120+
*bx/by* defines the end point of the projection path (Definition 2).
121+
122+
flags : str
123+
Specify your desired output using any combination of **xyzpqrs**, in
124+
any order [Default is **xyzpqrs**]. Do not space between the letters.
125+
Use lower case. The output will be columns of values corresponding to
126+
your ``flags``. The **z** flag is special and refers to all numerical
127+
columns beyond the leading **x** and **y** in your input record. The
128+
**z** flag also includes any trailing text (which is placed at the end
129+
of the record regardless of the order of **z** in ``flags``). **Note**:
130+
If ``generate`` is True, then the output order is hardwired to be
131+
**rsp** and ``flags`` is not allowed.
132+
133+
generate : str
134+
*dist* [/*colat*][**+c**|**h**].
135+
Generate mode. No input is read and the value of ``points`` is ignored
136+
(you can use, e.g., ``points=None``). Create (*r*, *s*, *p*) output
137+
points every *dist* units of *p*. See `maptypeunits` option.
138+
Alternatively, append */colat* for a small circle instead [Default is a
139+
colatitude of 90, i.e., a great circle]. If setting a pole with
140+
``rotationpole`` and you want the small circle to go through *cx*/*cy*,
141+
append **+c** to compute the required colatitude. Use ``center`` and
142+
``endpoint`` to generate a circle that goes through the center and end
143+
point. Note, in this case the center and end point cannot be farther
144+
apart than 2*| *colat* |. Finally, if you append **+h** then we will
145+
report the position of the pole as part of the segment header [no
146+
header].
147+
148+
length : str or list
149+
[**w**| *l_min*/*l_max*].
150+
Length controls. Project only those points whose *p* coordinate is
151+
within *l_min* < *p* < *l_max*. If ``endpoint`` has been set, then you
152+
may alternatively use **w** to stay within the distance from ``center``
153+
to ``endpoint``.
154+
155+
flatearth : bool
156+
If `True`, Make a Cartesian coordinate transformation in the plane.
157+
[Default uses spherical trigonometry.]
158+
159+
maptypeunits : bool
160+
If `True`, project assumes *x*, *y*, *r*, *s* are in degrees while *p*,
161+
*q*, *dist*, *l_min*, *l_max*, *w_min*, *w_max* are in km. If not set
162+
(or `False`), then all these are assumed to be in the same units.
163+
164+
sort : bool
165+
Sort the output into increasing *p* order. Useful when projecting
166+
random data into a sequential profile.
167+
168+
rotationpole : str or list
169+
*px*/*py*.
170+
*px/py* sets the position of the rotation pole of the projection.
171+
(Definition 3).
172+
173+
{V}
174+
175+
width : str or list
176+
*w_min*/*w_max*
177+
Width controls. Project only those points whose *q* coordinate is
178+
within *w_min* < *q* < *w_max*.
179+
180+
ellipse : str
181+
*major*/*minor*/*azimuth* [**+e** | **n**].
182+
Used in conjunction with ``center`` (sets its center) and ``generate``
183+
(sets the distance increment) to create the coordinates of an ellipse
184+
with *major* and *minor* axes given in km (unless ``flatearth`` is
185+
given for a Cartesian ellipse) and the *azimuth* of the major axis in
186+
degrees. Append **+e** to adjust the increment set via ``generate`` so
187+
that the the ellipse has equal distance increments [Default uses the
188+
given increment and closes the ellipse]. Instead, append **+n** to set
189+
a specific number of unique equidistant points via ``generate``. For
190+
degenerate ellipses you can just supply a single *diameter* instead. A
191+
geographic diameter may be specified in any desired unit other than km
192+
[Default] by appending the unit (e.g., 3d for degrees); if so we assume
193+
the increment is also given in the same unit (see `Units`_). **Note**:
194+
For the Cartesian ellipse (which requires ``flatearth``), we expect
195+
*direction* counter-clockwise from the horizontal instead of an
196+
*azimuth*.
197+
198+
outfile : str
199+
Required if ``points`` is a file. The file name for the output ASCII
200+
file.
201+
202+
{f}
203+
204+
Returns
205+
-------
206+
track: pandas.DataFrame or None
207+
Return type depends on whether the ``outfile`` parameter is set:
208+
209+
- :class:`pandas.DataFrame` table with (x, y, ..., newcolname) if
210+
``outfile`` is not set
211+
- None if ``outfile`` is set (track output will be stored in file set
212+
by ``outfile``)
213+
"""
214+
# center ("C") is a required positional argument, so it cannot be
215+
# processed by decorator `@kwargs_to_strings`
216+
kwargs["C"] = "/".join(f"{item}" for item in center)
217+
218+
with GMTTempFile(suffix=".csv") as tmpfile:
219+
if outfile is None: # Output to tmpfile if outfile is not set
220+
outfile = tmpfile.name
221+
with Session() as lib:
222+
if "G" not in kwargs:
223+
# Store the pandas.DataFrame points table in virtualfile
224+
if data_kind(points) == "matrix":
225+
table_context = lib.virtualfile_from_matrix(points.values)
226+
elif data_kind(points) == "file":
227+
if outfile is None:
228+
raise GMTInvalidInput("Please pass in a str to 'outfile'")
229+
table_context = dummy_context(points)
230+
else:
231+
raise GMTInvalidInput(f"Unrecognized data type {type(points)}")
232+
233+
# Run project on the temporary (csv) points table
234+
with table_context as csvfile:
235+
arg_str = " ".join(
236+
[csvfile, build_arg_string(kwargs), "->" + outfile]
237+
)
238+
lib.call_module(module="project", args=arg_str)
239+
else:
240+
arg_str = " ".join([build_arg_string(kwargs), "->" + outfile])
241+
lib.call_module(module="project", args=arg_str)
242+
243+
# if user did not set outfile, return pd.DataFrame
244+
if outfile == tmpfile.name:
245+
if "G" in kwargs:
246+
column_names = list("rsp")
247+
else:
248+
# Set output column names according to the "F" flag or use
249+
# default value
250+
if "F" in kwargs:
251+
column_names = list(kwargs["F"])
252+
else:
253+
column_names = list("xyzpqrs")
254+
# Find the indexes of "x", "y" and "z" column and
255+
# replace with input column names
256+
i_x = column_names.index("x")
257+
i_y = column_names.index("y")
258+
i_z = column_names.index("z")
259+
input_column_names = points.columns.to_list()
260+
column_names[i_x] = input_column_names[0]
261+
column_names[i_y] = input_column_names[1]
262+
# "z" can be actually more than one column
263+
column_names.pop(i_z)
264+
for col in reversed(input_column_names[2:]):
265+
column_names.insert(i_z, col)
266+
result = pd.read_csv(tmpfile.name, sep="\t", names=column_names)
267+
# return None if outfile set, output in outfile
268+
elif outfile != tmpfile.name:
269+
result = None
270+
271+
return result

0 commit comments

Comments
 (0)