|
| 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