pycontrails 0.58.0__cp314-cp314-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2931 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cp314-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +757 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +667 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.58.0.dist-info/METADATA +180 -0
- pycontrails-0.58.0.dist-info/RECORD +122 -0
- pycontrails-0.58.0.dist-info/WHEEL +5 -0
- pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.58.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""Algorithm support for grid to polygon conversion.
|
|
2
|
+
|
|
3
|
+
See Also
|
|
4
|
+
--------
|
|
5
|
+
:meth:`pycontrails.MetDataArray.to_polygon_feature`
|
|
6
|
+
:meth:`pycontrails.MetDataArray.to_polygon_feature_collection`
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import numpy.typing as npt
|
|
17
|
+
|
|
18
|
+
from pycontrails.utils import dependencies
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import cv2
|
|
22
|
+
except ModuleNotFoundError as exc:
|
|
23
|
+
dependencies.raise_module_not_found_error(
|
|
24
|
+
name="polygon module",
|
|
25
|
+
package_name="opencv-python",
|
|
26
|
+
module_not_found_error=exc,
|
|
27
|
+
pycontrails_optional_package="vis",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import shapely
|
|
32
|
+
import shapely.errors
|
|
33
|
+
import shapely.geometry
|
|
34
|
+
import shapely.validation
|
|
35
|
+
except ModuleNotFoundError as exc:
|
|
36
|
+
dependencies.raise_module_not_found_error(
|
|
37
|
+
name="polygon module",
|
|
38
|
+
package_name="shapely",
|
|
39
|
+
module_not_found_error=exc,
|
|
40
|
+
pycontrails_optional_package="vis",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def buffer_and_clean(
|
|
45
|
+
contour: npt.NDArray[np.floating],
|
|
46
|
+
min_area: float,
|
|
47
|
+
convex_hull: bool,
|
|
48
|
+
epsilon: float,
|
|
49
|
+
precision: int | None,
|
|
50
|
+
buffer: float,
|
|
51
|
+
is_exterior: bool,
|
|
52
|
+
) -> shapely.Polygon | None:
|
|
53
|
+
"""Buffer and clean a contour.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
contour : npt.NDArray[np.floating]
|
|
58
|
+
Contour to buffer and clean. A 2d array of shape (n, 2) where n is the number
|
|
59
|
+
of vertices in the contour.
|
|
60
|
+
min_area : float
|
|
61
|
+
Minimum area of the polygon. If the area of the buffered contour is less than
|
|
62
|
+
this, return None.
|
|
63
|
+
convex_hull : bool
|
|
64
|
+
Whether to take the convex hull of the buffered contour.
|
|
65
|
+
epsilon : float
|
|
66
|
+
Epsilon value for polygon simplification. If 0, no simplification is performed.
|
|
67
|
+
precision : int | None
|
|
68
|
+
Precision of the output polygon. If None, no rounding is performed.
|
|
69
|
+
buffer : float
|
|
70
|
+
Buffer distance.
|
|
71
|
+
is_exterior : bool, optional
|
|
72
|
+
Whether the contour is an exterior contour. If True, the contour is buffered
|
|
73
|
+
with a larger buffer distance. The polygon orientation is CCW iff this is True.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
shapely.Polygon | None
|
|
78
|
+
Buffered and cleaned polygon. If the area of the buffered contour is less than
|
|
79
|
+
``min_area``, return None.
|
|
80
|
+
"""
|
|
81
|
+
if len(contour) == 1:
|
|
82
|
+
base = shapely.Point(contour)
|
|
83
|
+
elif len(contour) < 4:
|
|
84
|
+
base = shapely.LineString(contour)
|
|
85
|
+
else:
|
|
86
|
+
base = shapely.Polygon(contour)
|
|
87
|
+
|
|
88
|
+
if is_exterior:
|
|
89
|
+
# The contours computed by openCV go directly over array points
|
|
90
|
+
# with value 1. With marching squares, we expect the contours to
|
|
91
|
+
# be the midpoint between the 0-1 boundary. Apply a small buffer
|
|
92
|
+
# to the exterior contours to account for this.
|
|
93
|
+
polygon = base.buffer(buffer, quad_segs=1)
|
|
94
|
+
else:
|
|
95
|
+
try:
|
|
96
|
+
polygon = shapely.Polygon(base)
|
|
97
|
+
except shapely.errors.TopologicalError:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
if not polygon.is_valid:
|
|
101
|
+
polygon = polygon.buffer(buffer / 10.0, quad_segs=1)
|
|
102
|
+
assert polygon.is_valid, "Fail to make polygon valid after buffer"
|
|
103
|
+
|
|
104
|
+
if isinstance(polygon, shapely.MultiPolygon):
|
|
105
|
+
# In this case, there is often one large polygon and several small polygons
|
|
106
|
+
# Just extract the largest polygon, ignoring the others
|
|
107
|
+
polygon = max(polygon.geoms, key=lambda x: x.area)
|
|
108
|
+
|
|
109
|
+
# Remove all interior rings
|
|
110
|
+
if polygon.interiors:
|
|
111
|
+
polygon = shapely.Polygon(polygon.exterior)
|
|
112
|
+
|
|
113
|
+
if polygon.area < min_area:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
# Exterior polygons should have CCW orientation
|
|
117
|
+
if is_exterior != polygon.exterior.is_ccw:
|
|
118
|
+
polygon = polygon.reverse()
|
|
119
|
+
if convex_hull:
|
|
120
|
+
polygon = _take_convex_hull(polygon)
|
|
121
|
+
if epsilon:
|
|
122
|
+
polygon = _buffer_simplify_iterate(polygon, epsilon)
|
|
123
|
+
|
|
124
|
+
if precision is not None:
|
|
125
|
+
while precision < 10:
|
|
126
|
+
out = _round_polygon(polygon, precision)
|
|
127
|
+
if out.is_valid:
|
|
128
|
+
return out
|
|
129
|
+
precision += 1
|
|
130
|
+
|
|
131
|
+
warnings.warn("Could not round polygon to a valid geometry.")
|
|
132
|
+
|
|
133
|
+
return polygon
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _round_polygon(polygon: shapely.Polygon, precision: int) -> shapely.Polygon:
|
|
137
|
+
"""Round the coordinates of a polygon.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
polygon : shapely.Polygon
|
|
142
|
+
Polygon to round.
|
|
143
|
+
precision : int
|
|
144
|
+
Precision to use when rounding.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
shapely.Polygon
|
|
149
|
+
Polygon with rounded coordinates.
|
|
150
|
+
"""
|
|
151
|
+
if polygon.is_empty:
|
|
152
|
+
return polygon
|
|
153
|
+
|
|
154
|
+
exterior = np.round(np.asarray(polygon.exterior.coords), precision)
|
|
155
|
+
interiors = [np.round(np.asarray(i.coords), precision) for i in polygon.interiors]
|
|
156
|
+
return shapely.Polygon(exterior, interiors)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _contours_to_polygons(
|
|
160
|
+
contours: Sequence[npt.NDArray[np.floating]],
|
|
161
|
+
hierarchy: npt.NDArray[np.int_],
|
|
162
|
+
min_area: float,
|
|
163
|
+
convex_hull: bool,
|
|
164
|
+
epsilon: float,
|
|
165
|
+
longitude: npt.NDArray[np.floating] | None,
|
|
166
|
+
latitude: npt.NDArray[np.floating] | None,
|
|
167
|
+
precision: int | None,
|
|
168
|
+
buffer: float,
|
|
169
|
+
i: int = 0,
|
|
170
|
+
) -> list[shapely.Polygon]:
|
|
171
|
+
"""Convert the outputs of :func:`cv2.findContours` to :class:`shapely.Polygon`.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
contours : Sequence[npt.NDArray[np.floating]]
|
|
176
|
+
The contours output from :func:`cv2.findContours`.
|
|
177
|
+
hierarchy : npt.NDArray[np.int_]
|
|
178
|
+
The hierarchy output from :func:`cv2.findContours`.
|
|
179
|
+
min_area : float
|
|
180
|
+
Minimum area of a polygon to be included in the output.
|
|
181
|
+
convex_hull : bool
|
|
182
|
+
Whether to take the convex hull of each polygon.
|
|
183
|
+
epsilon : float
|
|
184
|
+
Epsilon value to use when simplifying the polygons.
|
|
185
|
+
longitude : npt.NDArray[np.floating] | None
|
|
186
|
+
Longitude values for the grid.
|
|
187
|
+
latitude : npt.NDArray[np.floating] | None
|
|
188
|
+
Latitude values for the grid.
|
|
189
|
+
precision : int | None
|
|
190
|
+
Precision to use when rounding the coordinates.
|
|
191
|
+
buffer : float
|
|
192
|
+
Buffer to apply to the contours when converting to polygons.
|
|
193
|
+
i : int, optional
|
|
194
|
+
The index of the contour to start with. Defaults to 0.
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
list[shapely.Polygon]
|
|
199
|
+
A list of polygons. Polygons with a parent-child relationship are merged into
|
|
200
|
+
a single polygon.
|
|
201
|
+
"""
|
|
202
|
+
out = []
|
|
203
|
+
while i != -1:
|
|
204
|
+
child_i, parent_i = hierarchy[i, 2:]
|
|
205
|
+
is_exterior = parent_i == -1
|
|
206
|
+
|
|
207
|
+
contour = contours[i][:, 0, ::-1]
|
|
208
|
+
i = hierarchy[i, 0]
|
|
209
|
+
if longitude is not None and latitude is not None:
|
|
210
|
+
lon_idx = contour[:, 0]
|
|
211
|
+
lat_idx = contour[:, 1]
|
|
212
|
+
|
|
213
|
+
# Calculate interpolated longitude and latitude values and recreate contour
|
|
214
|
+
lon = np.interp(lon_idx, np.arange(longitude.shape[0]), longitude)
|
|
215
|
+
lat = np.interp(lat_idx, np.arange(latitude.shape[0]), latitude)
|
|
216
|
+
contour = np.stack([lon, lat], axis=1)
|
|
217
|
+
|
|
218
|
+
polygon = buffer_and_clean(
|
|
219
|
+
contour,
|
|
220
|
+
min_area,
|
|
221
|
+
convex_hull,
|
|
222
|
+
epsilon,
|
|
223
|
+
precision,
|
|
224
|
+
buffer,
|
|
225
|
+
is_exterior,
|
|
226
|
+
)
|
|
227
|
+
if polygon is None:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if child_i != -1:
|
|
231
|
+
holes = _contours_to_polygons(
|
|
232
|
+
contours,
|
|
233
|
+
hierarchy,
|
|
234
|
+
min_area=min_area,
|
|
235
|
+
convex_hull=False,
|
|
236
|
+
epsilon=epsilon,
|
|
237
|
+
longitude=longitude,
|
|
238
|
+
latitude=latitude,
|
|
239
|
+
precision=precision,
|
|
240
|
+
buffer=buffer,
|
|
241
|
+
i=child_i,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
candidate = shapely.Polygon(polygon.exterior, [h.exterior for h in holes])
|
|
245
|
+
# Abundance of caution: check if the candidate is valid
|
|
246
|
+
# If the candidate isn't valid, ignore all the holes
|
|
247
|
+
# This can happen if there are many holes and the buffer operation
|
|
248
|
+
# causes the holes to overlap
|
|
249
|
+
if candidate.is_valid:
|
|
250
|
+
polygon = candidate
|
|
251
|
+
|
|
252
|
+
out.append(polygon)
|
|
253
|
+
return out
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def determine_buffer(
|
|
257
|
+
longitude: npt.NDArray[np.floating], latitude: npt.NDArray[np.floating]
|
|
258
|
+
) -> float:
|
|
259
|
+
"""Determine the proper buffer size to use when converting to polygons."""
|
|
260
|
+
|
|
261
|
+
ndigits = 6
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
d_lon = round(longitude[1] - longitude[0], ndigits)
|
|
265
|
+
d_lat = round(latitude[1] - latitude[0], ndigits)
|
|
266
|
+
except IndexError as e:
|
|
267
|
+
raise ValueError("Longitude and latitude must each have at least 2 elements.") from e
|
|
268
|
+
|
|
269
|
+
if d_lon != d_lat:
|
|
270
|
+
warnings.warn(
|
|
271
|
+
"Longitude and latitude are not evenly spaced. Buffer size may be inaccurate."
|
|
272
|
+
)
|
|
273
|
+
if not np.all(np.diff(longitude).round(ndigits) == d_lon):
|
|
274
|
+
warnings.warn("Longitude is not evenly spaced. Buffer size may be inaccurate.")
|
|
275
|
+
if not np.all(np.diff(latitude).round(ndigits) == d_lat):
|
|
276
|
+
warnings.warn("Latitude is not evenly spaced. Buffer size may be inaccurate.")
|
|
277
|
+
|
|
278
|
+
return min(d_lon, d_lat) / 2.0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def find_multipolygon(
|
|
282
|
+
arr: npt.NDArray[np.floating],
|
|
283
|
+
threshold: float,
|
|
284
|
+
min_area: float,
|
|
285
|
+
epsilon: float,
|
|
286
|
+
lower_bound: bool = True,
|
|
287
|
+
interiors: bool = True,
|
|
288
|
+
convex_hull: bool = False,
|
|
289
|
+
longitude: npt.NDArray[np.floating] | None = None,
|
|
290
|
+
latitude: npt.NDArray[np.floating] | None = None,
|
|
291
|
+
precision: int | None = None,
|
|
292
|
+
) -> shapely.MultiPolygon:
|
|
293
|
+
"""Compute a multipolygon from a 2d array.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
arr : npt.NDArray[np.floating]
|
|
298
|
+
Array to convert to a multipolygon. The array will be converted to a binary
|
|
299
|
+
array by comparing each element to ``threshold``. This binary array is then
|
|
300
|
+
passed into :func:`cv2.findContours` to find the contours.
|
|
301
|
+
threshold : float
|
|
302
|
+
Threshold to use when converting ``arr`` to a binary array.
|
|
303
|
+
min_area : float
|
|
304
|
+
Minimum area of a polygon to be included in the output.
|
|
305
|
+
epsilon : float
|
|
306
|
+
Epsilon value to use when simplifying the polygons. Passed into shapely's
|
|
307
|
+
:meth:`shapely.geometry.Polygon.simplify` method.
|
|
308
|
+
lower_bound : bool, optional
|
|
309
|
+
Whether to treat ``threshold`` as a lower or upper bound on values in polygon interiors.
|
|
310
|
+
By default, True.
|
|
311
|
+
interiors : bool, optional
|
|
312
|
+
Whether to include interior polygons. By default, True.
|
|
313
|
+
convex_hull : bool, optional
|
|
314
|
+
Experimental. Whether to take the convex hull of each polygon. By default, False.
|
|
315
|
+
longitude : npt.NDArray[np.floating] | None, optional
|
|
316
|
+
If provided, the coordinates values corresponding to the longitude dimensions of ``arr``.
|
|
317
|
+
The contour coordinates will be converted to longitude-latitude values by indexing
|
|
318
|
+
into this array. Defaults to None.
|
|
319
|
+
latitude : npt.NDArray[np.floating] | None, optional
|
|
320
|
+
If provided, the coordinates values corresponding to the latitude dimensions of ``arr``.
|
|
321
|
+
precision : int | None, optional
|
|
322
|
+
If provided, the precision to use when rounding the coordinates. Defaults to None.
|
|
323
|
+
|
|
324
|
+
Returns
|
|
325
|
+
-------
|
|
326
|
+
shapely.MultiPolygon
|
|
327
|
+
A multipolygon of the contours.
|
|
328
|
+
|
|
329
|
+
Raises
|
|
330
|
+
------
|
|
331
|
+
ValueError
|
|
332
|
+
If ``arr`` is not 2d.
|
|
333
|
+
"""
|
|
334
|
+
if arr.ndim != 2:
|
|
335
|
+
raise ValueError("Array must be 2d")
|
|
336
|
+
|
|
337
|
+
assert (longitude is None) == (latitude is None)
|
|
338
|
+
if longitude is not None:
|
|
339
|
+
assert latitude is not None
|
|
340
|
+
assert arr.shape == (*longitude.shape, *latitude.shape)
|
|
341
|
+
buffer = determine_buffer(longitude, latitude)
|
|
342
|
+
else:
|
|
343
|
+
buffer = 0.5
|
|
344
|
+
|
|
345
|
+
arr_bin = np.empty(arr.shape, dtype=np.uint8)
|
|
346
|
+
if lower_bound:
|
|
347
|
+
np.greater_equal(arr, threshold, out=arr_bin)
|
|
348
|
+
else:
|
|
349
|
+
np.less_equal(arr, threshold, out=arr_bin)
|
|
350
|
+
|
|
351
|
+
mode = cv2.RETR_CCOMP if interiors else cv2.RETR_EXTERNAL
|
|
352
|
+
contours, hierarchy = cv2.findContours(arr_bin, mode, cv2.CHAIN_APPROX_SIMPLE)
|
|
353
|
+
if not contours:
|
|
354
|
+
return shapely.MultiPolygon()
|
|
355
|
+
|
|
356
|
+
assert len(hierarchy) == 1
|
|
357
|
+
hierarchy = hierarchy[0]
|
|
358
|
+
|
|
359
|
+
polygons = _contours_to_polygons(
|
|
360
|
+
contours, # type: ignore[arg-type]
|
|
361
|
+
hierarchy,
|
|
362
|
+
min_area,
|
|
363
|
+
convex_hull,
|
|
364
|
+
epsilon,
|
|
365
|
+
longitude,
|
|
366
|
+
latitude,
|
|
367
|
+
precision,
|
|
368
|
+
buffer,
|
|
369
|
+
)
|
|
370
|
+
return _make_valid_multipolygon(polygons, convex_hull)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _take_convex_hull(polygon: shapely.Polygon) -> shapely.Polygon:
|
|
374
|
+
"""Take the convex hull of a linear ring and preserve the orientation.
|
|
375
|
+
|
|
376
|
+
Parameters
|
|
377
|
+
----------
|
|
378
|
+
polygon : shapely.Polygon
|
|
379
|
+
Linear ring to take the convex hull of.
|
|
380
|
+
|
|
381
|
+
Returns
|
|
382
|
+
-------
|
|
383
|
+
shapely.Polygon
|
|
384
|
+
Convex hull of the input.
|
|
385
|
+
"""
|
|
386
|
+
convex_hull = polygon.convex_hull
|
|
387
|
+
if polygon.exterior.is_ccw == convex_hull.exterior.is_ccw:
|
|
388
|
+
return convex_hull
|
|
389
|
+
return shapely.Polygon(convex_hull.exterior.reverse())
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _buffer_simplify_iterate(polygon: shapely.Polygon, epsilon: float) -> shapely.Polygon:
|
|
393
|
+
"""Simplify a linear ring by iterating over a larger buffer.
|
|
394
|
+
|
|
395
|
+
This function calls :func:`shapely.buffer` and :func:`shapely.simplify`
|
|
396
|
+
over a range of buffer distances. The buffer allows for the topology
|
|
397
|
+
of the contour to change, which is useful for simplifying contours.
|
|
398
|
+
Applying a buffer does introduce a slight bias towards the exterior
|
|
399
|
+
of the contour.
|
|
400
|
+
|
|
401
|
+
.. versionadded:: 0.38.0
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
polygon : shapely.Polygon
|
|
406
|
+
Linear ring to simplify.
|
|
407
|
+
epsilon : float
|
|
408
|
+
Passed as ``tolerance`` parameter into :func:`shapely.simplify`.
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
shapely.Polygon
|
|
413
|
+
Simplified linear ring as a :class:`shapely.Polygon`.
|
|
414
|
+
"""
|
|
415
|
+
# Try to simplify without a buffer first
|
|
416
|
+
# This seems to be computationally faster
|
|
417
|
+
out = polygon.simplify(epsilon, preserve_topology=False)
|
|
418
|
+
|
|
419
|
+
# In some rare situations, calling simplify actually gives a MultiPolygon.
|
|
420
|
+
# In this case, take the polygon with the largest area
|
|
421
|
+
if isinstance(out, shapely.MultiPolygon):
|
|
422
|
+
out = max(out.geoms, key=lambda x: x.area)
|
|
423
|
+
|
|
424
|
+
if out.is_simple and out.is_valid:
|
|
425
|
+
return out
|
|
426
|
+
|
|
427
|
+
# Applying a naive linear_ring.buffer(0) can destroy the polygon completely
|
|
428
|
+
# https://stackoverflow.com/a/20873812
|
|
429
|
+
|
|
430
|
+
is_ccw = polygon.exterior.is_ccw
|
|
431
|
+
|
|
432
|
+
# Values here are somewhat ad hoc: These seem to allow the algorithm to
|
|
433
|
+
# terminate and are not too computationally expensive
|
|
434
|
+
for i in range(1, 11):
|
|
435
|
+
distance = epsilon * i / 10.0
|
|
436
|
+
|
|
437
|
+
# Taking the buffer can change the orientation of the contour
|
|
438
|
+
out = polygon.buffer(distance, quad_segs=1)
|
|
439
|
+
if out.exterior.is_ccw != is_ccw:
|
|
440
|
+
out = shapely.Polygon(out.exterior.coords[::-1])
|
|
441
|
+
|
|
442
|
+
out = out.simplify(epsilon, preserve_topology=False)
|
|
443
|
+
if out.is_simple and out.is_valid:
|
|
444
|
+
return out
|
|
445
|
+
|
|
446
|
+
warnings.warn(
|
|
447
|
+
f"Could not simplify contour with epsilon {epsilon}. Try passing a smaller epsilon."
|
|
448
|
+
)
|
|
449
|
+
return polygon.simplify(epsilon, preserve_topology=True)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _make_valid_multipolygon(
|
|
453
|
+
polygons: list[shapely.Polygon], convex_hull: bool
|
|
454
|
+
) -> shapely.MultiPolygon:
|
|
455
|
+
"""Make a multipolygon valid.
|
|
456
|
+
|
|
457
|
+
This function attempts to make a multipolygon valid by iteratively
|
|
458
|
+
applying :func:`shapely.unary_union` to convert non-disjoint polygons
|
|
459
|
+
into disjoint polygons by merging them. If the multipolygon is still
|
|
460
|
+
invalid after 5 attempts, a warning is raised and the last
|
|
461
|
+
multipolygon is returned.
|
|
462
|
+
|
|
463
|
+
This function is needed because simplifying a contour can change the
|
|
464
|
+
geometry of it, which can cause non-disjoint polygons to be created.
|
|
465
|
+
|
|
466
|
+
.. versionadded:: 0.38.0
|
|
467
|
+
|
|
468
|
+
Parameters
|
|
469
|
+
----------
|
|
470
|
+
polygons : list[shapely.Polygon]
|
|
471
|
+
List of polygons to combine into a multipolygon.
|
|
472
|
+
convex_hull : bool
|
|
473
|
+
If True, take the convex hull of merged polygons.
|
|
474
|
+
|
|
475
|
+
Returns
|
|
476
|
+
-------
|
|
477
|
+
shapely.MultiPolygon
|
|
478
|
+
Valid multipolygon.
|
|
479
|
+
"""
|
|
480
|
+
mp = shapely.MultiPolygon(polygons)
|
|
481
|
+
if mp.is_empty:
|
|
482
|
+
return mp
|
|
483
|
+
|
|
484
|
+
n_attempts = 5
|
|
485
|
+
for _ in range(n_attempts):
|
|
486
|
+
if mp.is_valid:
|
|
487
|
+
return mp
|
|
488
|
+
|
|
489
|
+
mp = shapely.unary_union(mp)
|
|
490
|
+
if isinstance(mp, shapely.Polygon):
|
|
491
|
+
mp = shapely.MultiPolygon([mp])
|
|
492
|
+
|
|
493
|
+
# Fix the orientation of the polygons
|
|
494
|
+
mp = shapely.MultiPolygon([shapely.geometry.polygon.orient(p, sign=1) for p in mp.geoms])
|
|
495
|
+
|
|
496
|
+
if convex_hull:
|
|
497
|
+
mp = shapely.MultiPolygon([_take_convex_hull(p) for p in mp.geoms])
|
|
498
|
+
|
|
499
|
+
warnings.warn(
|
|
500
|
+
f"Could not make multipolygon valid after {n_attempts} attempts. "
|
|
501
|
+
"According to shapely, the multipolygon is invalid because: "
|
|
502
|
+
f"{shapely.validation.explain_validity(mp)}"
|
|
503
|
+
)
|
|
504
|
+
return mp
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def multipolygon_to_geojson(
|
|
508
|
+
multipolygon: shapely.MultiPolygon,
|
|
509
|
+
altitude: float | None,
|
|
510
|
+
properties: dict[str, Any] | None = None,
|
|
511
|
+
) -> dict[str, Any]:
|
|
512
|
+
"""Convert a shapely multipolygon to a GeoJSON feature.
|
|
513
|
+
|
|
514
|
+
Parameters
|
|
515
|
+
----------
|
|
516
|
+
multipolygon : shapely.MultiPolygon
|
|
517
|
+
Multipolygon to convert.
|
|
518
|
+
altitude : float | None
|
|
519
|
+
Altitude of the multipolygon. If provided, the multipolygon coordinates
|
|
520
|
+
will be given a z-coordinate.
|
|
521
|
+
properties : dict[str, Any] | None, optional
|
|
522
|
+
Properties to add to the GeoJSON feature.
|
|
523
|
+
|
|
524
|
+
Returns
|
|
525
|
+
-------
|
|
526
|
+
dict[str, Any]
|
|
527
|
+
GeoJSON feature with geometry type "MultiPolygon".
|
|
528
|
+
"""
|
|
529
|
+
coordinates = []
|
|
530
|
+
for polygon in multipolygon.geoms:
|
|
531
|
+
poly_coords = []
|
|
532
|
+
rings = polygon.exterior, *polygon.interiors
|
|
533
|
+
for ring in rings:
|
|
534
|
+
if altitude is None:
|
|
535
|
+
coords = np.asarray(ring.coords)
|
|
536
|
+
else:
|
|
537
|
+
shape = len(ring.coords), 3
|
|
538
|
+
coords = np.empty(shape)
|
|
539
|
+
coords[:, :2] = np.asarray(ring.coords)
|
|
540
|
+
coords[:, 2] = altitude
|
|
541
|
+
|
|
542
|
+
poly_coords.append(coords.tolist())
|
|
543
|
+
coordinates.append(poly_coords)
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
"type": "Feature",
|
|
547
|
+
"properties": properties or {},
|
|
548
|
+
"geometry": {"type": "MultiPolygon", "coordinates": coordinates},
|
|
549
|
+
}
|
|
Binary file
|