Skip to content

Commit ab0f76d

Browse files
committed
Add a tool for cutting transect loops
1 parent d41d15f commit ab0f76d

File tree

2 files changed

+132
-1
lines changed

2 files changed

+132
-1
lines changed

ocean/transects/python/README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,12 @@ The required inputs are an MPAS mesh file and a geojson file or the name of an
4646
ocean transect from `geometric_features`. The required output is a filename
4747
with the masks and other information about the transect.
4848

49-
49+
## cut_closed_transect.py
50+
51+
If a transect is a closed loop, the path of edges and edge signs don't work
52+
correctly (the shortest path between the beginning and end of the transect is
53+
trivial and involves a single edge). To avoid this, we provide a tool for
54+
cutting a square (in lat/lon space) out of the transect to sever the loop.
55+
The user provides a latitude and longitude (used to locate the closest point)
56+
on the transect and the size of the square to cut out.
5057

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
5+
import numpy as np
6+
from geometric_features import (
7+
GeometricFeatures,
8+
read_feature_collection
9+
)
10+
from shapely.geometry import (
11+
mapping,
12+
Polygon,
13+
shape
14+
)
15+
16+
17+
def cut_transect(fc_transect, lat, lon, size, out_filename):
18+
"""
19+
Cut a square out of the given closed-loop transect to break the loop.
20+
"""
21+
22+
# find the closest point in the transect to the specificed lat/lon
23+
24+
feature = fc_transect.features[0]
25+
coordinates = feature['geometry']['coordinates']
26+
feature_type = feature['geometry']['type']
27+
if feature_type == 'LineString':
28+
coordinates = [coordinates]
29+
elif feature_type != 'MultiLineString':
30+
raise ValueError(
31+
f'Unexpected geometry type for transect {feature_type}')
32+
33+
min_dist = None
34+
center_lon = None
35+
center_lan = None
36+
for coords in coordinates:
37+
lon_local, lat_local = zip(*coords)
38+
dist = np.sqrt((np.array(lon_local) - lon)**2 +
39+
(np.array(lat_local) - lat)**2)
40+
index_min = np.argmin(dist)
41+
if min_dist is None or dist[index_min] < min_dist:
42+
center_lon = lon_local[index_min]
43+
center_lan = lat_local[index_min]
44+
min_dist = dist[index_min]
45+
46+
square = Polygon([(center_lon - 0.5 * size, center_lan - 0.5 * size),
47+
(center_lon - 0.5 * size, center_lan + 0.5 * size),
48+
(center_lon + 0.5 * size, center_lan + 0.5 * size),
49+
(center_lon + 0.5 * size, center_lan - 0.5 * size),
50+
(center_lon - 0.5 * size, center_lan - 0.5 * size)])
51+
52+
feature = fc_transect.features[0]
53+
transect_shape = shape(feature['geometry'])
54+
transect_shape = transect_shape.difference(square)
55+
56+
# now sort the coordinates so the start and end of the transect are at the
57+
# dividing point
58+
59+
feature['geometry'] = mapping(transect_shape)
60+
61+
feature_type = feature['geometry']['type']
62+
if feature_type == 'MultiLineString':
63+
coordinates = feature['geometry']['coordinates']
64+
65+
# reorder the LineStrings so the first one starts right after the cut
66+
67+
closest = None
68+
min_dist = None
69+
for index, coords in enumerate(coordinates):
70+
lon_first, lat_first = coords[0]
71+
dist = np.sqrt((lon_first - lon)**2 + (lat_first - lat)**2)
72+
if min_dist is None or dist < min_dist:
73+
closest = index
74+
min_dist = dist
75+
new_coords = list(coordinates[closest:])
76+
new_coords.extend(list(coordinates[:closest]))
77+
feature['geometry']['coordinates'] = tuple(new_coords)
78+
79+
fc_transect.to_geojson(out_filename)
80+
81+
82+
def main():
83+
84+
parser = argparse.ArgumentParser(description='''
85+
cut the given transect loop as close as possible to the given
86+
latitude and longitude''')
87+
parser.add_argument('-g', dest='geojson_filename',
88+
help='Geojson filename with transect', required=False)
89+
parser.add_argument('-f', dest='feature_name',
90+
help='Name of an ocean transect from '
91+
'geometric_features',
92+
required=False)
93+
parser.add_argument('--lat', dest='lat', type=float,
94+
help='The approx. latitude at which to cut the loop',
95+
required=True)
96+
parser.add_argument('--lon', dest='lon', type=float,
97+
help='The approx. longitude at which to cut the loop',
98+
required=True)
99+
parser.add_argument('--size', dest='size', type=float,
100+
help='The size in degrees of the square used to cut '
101+
'the loop',
102+
required=True)
103+
parser.add_argument('-o', dest='out_filename',
104+
help='The geojson file with the cut transect to write '
105+
'out',
106+
required=True)
107+
args = parser.parse_args()
108+
109+
if args.geojson_filename is None and args.feature_name is None:
110+
raise ValueError('Must supply either a geojson file or a transect '
111+
'name')
112+
113+
if args.geojson_filename is not None:
114+
fc_transect = read_feature_collection(args.geojson_filename)
115+
else:
116+
gf = GeometricFeatures()
117+
fc_transect = gf.read(componentName='ocean', objectType='transect',
118+
featureNames=[args.feature_name])
119+
120+
cut_transect(fc_transect, args.lat, args.lon, args.size, args.out_filename)
121+
122+
123+
if __name__ == '__main__':
124+
main()

0 commit comments

Comments
 (0)