bluemesh2d 0.1.1.dev0__py3-none-any.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.
- bluemesh2d/_version.py +24 -0
- bluemesh2d/aabb_tree/findball.py +121 -0
- bluemesh2d/aabb_tree/findtria.py +299 -0
- bluemesh2d/aabb_tree/maketree.py +147 -0
- bluemesh2d/aabb_tree/mapvert.py +72 -0
- bluemesh2d/aabb_tree/queryset.py +83 -0
- bluemesh2d/aabb_tree/scantree.py +124 -0
- bluemesh2d/dependencies.py +83 -0
- bluemesh2d/feedback.py +142 -0
- bluemesh2d/geom_util/boundary_util.py +216 -0
- bluemesh2d/geom_util/getiso.py +194 -0
- bluemesh2d/geom_util/poly_util.py +795 -0
- bluemesh2d/geom_util/proj_util.py +250 -0
- bluemesh2d/geomesh_util/border_util.py +283 -0
- bluemesh2d/geomesh_util/depth_field.py +333 -0
- bluemesh2d/geomesh_util/grd_util.py +1000 -0
- bluemesh2d/geomesh_util/interpolation_mesh.py +318 -0
- bluemesh2d/geomesh_util/merge_circumcenters.py +268 -0
- bluemesh2d/geomesh_util/water_polygon.py +406 -0
- bluemesh2d/hfun_util/build_hfun.py +589 -0
- bluemesh2d/hfun_util/hfun_dispersion.py +96 -0
- bluemesh2d/hfun_util/lfshfn.py +110 -0
- bluemesh2d/hfun_util/limhfn.py +65 -0
- bluemesh2d/hfun_util/make_constant_hfun.py +32 -0
- bluemesh2d/hfun_util/make_depth_hfun.py +51 -0
- bluemesh2d/hfun_util/smooth_and_precomput.py +188 -0
- bluemesh2d/hfun_util/trihfn.py +84 -0
- bluemesh2d/hjac_util/limgrad.py +105 -0
- bluemesh2d/mesh_ball/cdtbal1.py +38 -0
- bluemesh2d/mesh_ball/cdtbal2.py +104 -0
- bluemesh2d/mesh_ball/inv_2x2.py +61 -0
- bluemesh2d/mesh_ball/inv_3x3.py +72 -0
- bluemesh2d/mesh_ball/pwrbal2.py +134 -0
- bluemesh2d/mesh_ball/tribal2.py +27 -0
- bluemesh2d/mesh_cost/relhfn.py +62 -0
- bluemesh2d/mesh_cost/triang.py +59 -0
- bluemesh2d/mesh_cost/triarea.py +51 -0
- bluemesh2d/mesh_cost/trideg.py +44 -0
- bluemesh2d/mesh_cost/triscr.py +43 -0
- bluemesh2d/mesh_file/bnd_util.py +664 -0
- bluemesh2d/mesh_file/loadmsh.py +165 -0
- bluemesh2d/mesh_file/ugrid.py +308 -0
- bluemesh2d/mesh_util/cfmtri.py +118 -0
- bluemesh2d/mesh_util/deltri.py +131 -0
- bluemesh2d/mesh_util/idxtri.py +53 -0
- bluemesh2d/mesh_util/isfeat.py +90 -0
- bluemesh2d/mesh_util/minlen.py +46 -0
- bluemesh2d/mesh_util/setset.py +49 -0
- bluemesh2d/mesh_util/tricon.py +90 -0
- bluemesh2d/mesh_util/tridiv.py +187 -0
- bluemesh2d/meshgen.py +202 -0
- bluemesh2d/ortho_merge/constants.py +27 -0
- bluemesh2d/ortho_merge/geometry.py +64 -0
- bluemesh2d/ortho_merge/ortho_merge_iter.py +812 -0
- bluemesh2d/ortho_merge/orthogonalize.py +2989 -0
- bluemesh2d/pipeline.py +277 -0
- bluemesh2d/poly_data/airfoil.msh +958 -0
- bluemesh2d/poly_data/channel.msh +212 -0
- bluemesh2d/poly_data/islands.msh +13819 -0
- bluemesh2d/poly_data/lake.msh +612 -0
- bluemesh2d/poly_data/river.msh +690 -0
- bluemesh2d/poly_test/inpoly.py +105 -0
- bluemesh2d/poly_test/inpoly_mat.py +104 -0
- bluemesh2d/refine.py +972 -0
- bluemesh2d/smood.py +623 -0
- bluemesh2d/smooth.py +522 -0
- bluemesh2d/tricost.py +426 -0
- bluemesh2d/tridemo.py +723 -0
- bluemesh2d/triread.py +53 -0
- bluemesh2d-0.1.1.dev0.dist-info/METADATA +139 -0
- bluemesh2d-0.1.1.dev0.dist-info/RECORD +74 -0
- bluemesh2d-0.1.1.dev0.dist-info/WHEEL +5 -0
- bluemesh2d-0.1.1.dev0.dist-info/licenses/LICENSE +674 -0
- bluemesh2d-0.1.1.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Stage 1: bathymetry raster -> water-domain polygon (working CRS)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from ..feedback import (
|
|
6
|
+
_NullFeedback,
|
|
7
|
+
_available_ram_bytes,
|
|
8
|
+
_check,
|
|
9
|
+
_warn_if_ram_risk,
|
|
10
|
+
)
|
|
11
|
+
from ..geom_util.proj_util import _raster_crs, bundled_raster_data_env
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _flag_fixed_vertices(p, ext_boundary, tol):
|
|
15
|
+
"""Return a PolygonZ copy of ``p`` with Z=1 on vertices on ``ext_boundary``.
|
|
16
|
+
|
|
17
|
+
Vertices farther than ``tol`` from the extent boundary get Z=0. The Z
|
|
18
|
+
flag travels inside the polygon geometry to stage 3, which keeps the
|
|
19
|
+
flagged vertices fixed while resampling. The first and last vertex of
|
|
20
|
+
each fixed run (the junctions with the coastline) are additionally
|
|
21
|
+
projected exactly onto the extent boundary -- a sub-``tol`` move -- so
|
|
22
|
+
the run ends at the true intersection; no other vertex is moved.
|
|
23
|
+
"""
|
|
24
|
+
import numpy as np
|
|
25
|
+
from shapely.geometry import Point, Polygon
|
|
26
|
+
|
|
27
|
+
def ring3(coords):
|
|
28
|
+
pts = np.asarray(coords, dtype=float)[:, :2]
|
|
29
|
+
closed = len(pts) > 1 and np.allclose(pts[0], pts[-1])
|
|
30
|
+
core = pts[:-1] if closed else pts
|
|
31
|
+
n = len(core)
|
|
32
|
+
flag = np.array(
|
|
33
|
+
[ext_boundary.distance(Point(x, y)) <= tol for x, y in core])
|
|
34
|
+
out = [(x, y, 1.0 if f else 0.0)
|
|
35
|
+
for (x, y), f in zip(core, flag)]
|
|
36
|
+
if n >= 2:
|
|
37
|
+
for i in range(n):
|
|
38
|
+
if flag[i] and (not flag[i - 1] or not flag[(i + 1) % n]):
|
|
39
|
+
q = ext_boundary.interpolate(
|
|
40
|
+
ext_boundary.project(Point(core[i])))
|
|
41
|
+
out[i] = (q.x, q.y, 1.0)
|
|
42
|
+
if closed:
|
|
43
|
+
out.append(out[0])
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
return Polygon(ring3(p.exterior.coords),
|
|
47
|
+
[ring3(r.coords) for r in p.interiors])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _corner_vertices(geom, min_deviation_deg=30.0):
|
|
51
|
+
"""Vertices of a polygon outline where the direction turns sharply.
|
|
52
|
+
|
|
53
|
+
Used for the generated (buffered raster extent) clip domain, which has
|
|
54
|
+
no user-drawn vertices: its geometric corners -- where the boundary
|
|
55
|
+
direction deviates by at least ``min_deviation_deg`` -- play the role
|
|
56
|
+
the extent polygon's original vertices play, and are kept fixed.
|
|
57
|
+
"""
|
|
58
|
+
import numpy as np
|
|
59
|
+
|
|
60
|
+
corners = []
|
|
61
|
+
polys = geom.geoms if hasattr(geom, "geoms") else [geom]
|
|
62
|
+
for p in polys:
|
|
63
|
+
if p.geom_type != "Polygon":
|
|
64
|
+
continue
|
|
65
|
+
for ring in [p.exterior] + list(p.interiors):
|
|
66
|
+
pts = np.asarray(ring.coords, dtype=float)[:, :2]
|
|
67
|
+
if len(pts) > 1 and np.allclose(pts[0], pts[-1]):
|
|
68
|
+
pts = pts[:-1]
|
|
69
|
+
if len(pts) < 3:
|
|
70
|
+
continue
|
|
71
|
+
prev = np.roll(pts, 1, axis=0)
|
|
72
|
+
nxt = np.roll(pts, -1, axis=0)
|
|
73
|
+
a1 = np.arctan2(*(pts - prev).T[::-1])
|
|
74
|
+
a2 = np.arctan2(*(nxt - pts).T[::-1])
|
|
75
|
+
dev = np.abs((a2 - a1 + np.pi) % (2.0 * np.pi) - np.pi)
|
|
76
|
+
corners.extend(pts[dev >= np.radians(min_deviation_deg)])
|
|
77
|
+
return np.asarray(corners, dtype=float).reshape(-1, 2)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _valid_parts(parts):
|
|
81
|
+
"""Repair invalid polygon parts (2D) and flatten any multi results.
|
|
82
|
+
|
|
83
|
+
The raw ``coast ∩ domain`` intersection can be invalid (self-touching
|
|
84
|
+
rings); it must be repaired BEFORE the Z flags are attached, because the
|
|
85
|
+
usual repair (``buffer(0)``) drops Z coordinates.
|
|
86
|
+
"""
|
|
87
|
+
out = []
|
|
88
|
+
for p in parts:
|
|
89
|
+
if not p.is_valid:
|
|
90
|
+
p = p.buffer(0)
|
|
91
|
+
if p.geom_type == "Polygon":
|
|
92
|
+
if not p.is_empty:
|
|
93
|
+
out.append(p)
|
|
94
|
+
elif hasattr(p, "geoms"):
|
|
95
|
+
out.extend(g for g in p.geoms
|
|
96
|
+
if g.geom_type == "Polygon" and not g.is_empty)
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _prune_nonoriginal_fixed(p, keep_xy, tol=1e-3, unflag_only=False):
|
|
101
|
+
"""Demote fixed (Z=1) vertices that are neither run endpoints nor listed.
|
|
102
|
+
|
|
103
|
+
``keep_xy`` holds the extent polygon's *original* vertex coordinates
|
|
104
|
+
(working CRS). With ``unflag_only=False`` (extent-polygon case) the other
|
|
105
|
+
fixed vertices -- densification points sitting on straight extent edges
|
|
106
|
+
-- are deleted from the ring; the output keeps just the original extent
|
|
107
|
+
vertices plus the coastline-junction points of each fixed run. With
|
|
108
|
+
``unflag_only=True`` (generated/buffered domain, whose shape lives in
|
|
109
|
+
those vertices) they are kept geometrically but set free (Z=0), so only
|
|
110
|
+
the coastline junctions stay fixed. Free (Z=0) vertices are never
|
|
111
|
+
touched.
|
|
112
|
+
"""
|
|
113
|
+
import numpy as np
|
|
114
|
+
from shapely.geometry import Polygon
|
|
115
|
+
|
|
116
|
+
keep_xy = np.asarray(keep_xy, dtype=float).reshape(-1, 2)
|
|
117
|
+
|
|
118
|
+
def ring3(coords):
|
|
119
|
+
pts = np.asarray(coords, dtype=float)
|
|
120
|
+
closed = len(pts) > 1 and np.allclose(pts[0], pts[-1])
|
|
121
|
+
core = pts[:-1] if closed else pts
|
|
122
|
+
n = len(core)
|
|
123
|
+
flag = core[:, 2] > 0.5
|
|
124
|
+
keep = np.ones(n, dtype=bool)
|
|
125
|
+
for i in range(n):
|
|
126
|
+
if not flag[i]:
|
|
127
|
+
continue
|
|
128
|
+
if not flag[i - 1] or not flag[(i + 1) % n]:
|
|
129
|
+
continue # run endpoint: coastline junction, always kept
|
|
130
|
+
if keep_xy.size:
|
|
131
|
+
d2 = np.sum((keep_xy - core[i, :2]) ** 2, axis=1)
|
|
132
|
+
if d2.min() <= tol * tol:
|
|
133
|
+
continue # original extent vertex, kept
|
|
134
|
+
if unflag_only:
|
|
135
|
+
core[i, 2] = 0.0
|
|
136
|
+
else:
|
|
137
|
+
keep[i] = False
|
|
138
|
+
core = core[keep]
|
|
139
|
+
out = [tuple(c) for c in core]
|
|
140
|
+
if closed and out:
|
|
141
|
+
out.append(out[0])
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
return Polygon(ring3(p.exterior.coords),
|
|
145
|
+
[ring3(r.coords) for r in p.interiors])
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _contour_read_budget():
|
|
149
|
+
"""Max grid cells to read for contouring, sized to available RAM.
|
|
150
|
+
|
|
151
|
+
Contour extraction works on float copies (~4x the band), so the peak is
|
|
152
|
+
~16 bytes/cell; keep it under ~15% of available RAM, with a hard ceiling
|
|
153
|
+
so contourpy stays responsive and a floor so small machines still work.
|
|
154
|
+
"""
|
|
155
|
+
avail = _available_ram_bytes() or 4_000_000_000
|
|
156
|
+
return int(min(120_000_000, max(4_000_000, 0.15 * avail / 16.0)))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _read_band_for_contour(src, bbox, max_cells):
|
|
160
|
+
"""Read band 1 clipped to ``bbox`` and decimated to fit ``max_cells``.
|
|
161
|
+
|
|
162
|
+
Only the requested window is read (never the full raster), and it is
|
|
163
|
+
down-sampled on read when still too large -- so an arbitrarily large
|
|
164
|
+
bathymetry raster is contoured without materialising it in memory.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
src : rasterio dataset
|
|
169
|
+
bbox : tuple or None
|
|
170
|
+
``(xmin, ymin, xmax, ymax)`` in the raster CRS, or ``None`` for the
|
|
171
|
+
whole raster.
|
|
172
|
+
max_cells : int
|
|
173
|
+
Upper bound on the number of cells actually read.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
zdat : ndarray (rows, cols)
|
|
178
|
+
lon, lat : ndarray
|
|
179
|
+
Pixel x/y coordinates of the columns/rows (raster CRS).
|
|
180
|
+
step : int
|
|
181
|
+
Decimation factor applied (1 = full resolution of the window).
|
|
182
|
+
clipped : bool
|
|
183
|
+
Whether a bbox window was used.
|
|
184
|
+
"""
|
|
185
|
+
import numpy as np
|
|
186
|
+
from rasterio.transform import Affine
|
|
187
|
+
from rasterio.windows import Window
|
|
188
|
+
|
|
189
|
+
W, H = src.width, src.height
|
|
190
|
+
if bbox is not None:
|
|
191
|
+
xmin, ymin, xmax, ymax = bbox
|
|
192
|
+
inv = ~src.transform
|
|
193
|
+
cols, rows = [], []
|
|
194
|
+
for x in (xmin, xmax):
|
|
195
|
+
for y in (ymin, ymax):
|
|
196
|
+
c, r = inv * (x, y)
|
|
197
|
+
cols.append(c)
|
|
198
|
+
rows.append(r)
|
|
199
|
+
col_off = max(0, int(np.floor(min(cols))) - 2)
|
|
200
|
+
row_off = max(0, int(np.floor(min(rows))) - 2)
|
|
201
|
+
col_end = min(W, int(np.ceil(max(cols))) + 2)
|
|
202
|
+
row_end = min(H, int(np.ceil(max(rows))) + 2)
|
|
203
|
+
if col_end <= col_off or row_end <= row_off:
|
|
204
|
+
raise RuntimeError("The extent polygon does not overlap the raster.")
|
|
205
|
+
else:
|
|
206
|
+
col_off, row_off, col_end, row_end = 0, 0, W, H
|
|
207
|
+
|
|
208
|
+
win = Window(col_off, row_off, col_end - col_off, row_end - row_off)
|
|
209
|
+
win_w, win_h = int(win.width), int(win.height)
|
|
210
|
+
step = 1
|
|
211
|
+
if win_w * win_h > max_cells:
|
|
212
|
+
step = int(np.ceil((win_w * win_h / float(max_cells)) ** 0.5))
|
|
213
|
+
out_w = max(1, win_w // step)
|
|
214
|
+
out_h = max(1, win_h // step)
|
|
215
|
+
|
|
216
|
+
zdat = src.read(1, window=win, out_shape=(out_h, out_w))
|
|
217
|
+
t = src.window_transform(win) * Affine.scale(win_w / out_w, win_h / out_h)
|
|
218
|
+
# rasterio transforms are corner-referenced (t*(0,0) = top-left corner of
|
|
219
|
+
# the first cell), but the contouring places zdat[i,j] AT (lon[j], lat[i]).
|
|
220
|
+
# For Area rasters the value is the cell CENTRE, so offset by half a cell
|
|
221
|
+
# (Point rasters: offset 0) to avoid shifting the extracted coastline.
|
|
222
|
+
from bluemesh2d.geomesh_util.depth_field import _cell_center_offset
|
|
223
|
+
off = _cell_center_offset(src)
|
|
224
|
+
lon = (t * (np.arange(out_w) + off, np.zeros(out_w)))[0]
|
|
225
|
+
lat = (t * (np.zeros(out_h), np.arange(out_h) + off))[1]
|
|
226
|
+
return zdat, lon, lat, step, (bbox is not None)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def extract_water_polygon(raster_path, coast_zmax=2.0, domain_buffer=-0.05,
|
|
230
|
+
deep_zmax=None, extent_geom=None, keep_largest=True,
|
|
231
|
+
return_domain=False, invert_z=False,
|
|
232
|
+
nodata_value=None, feedback=None):
|
|
233
|
+
"""Extract the water domain from a raster.
|
|
234
|
+
|
|
235
|
+
The water region is ``z <= coast_zmax``; if `deep_zmax` is given (an
|
|
236
|
+
elevation, e.g. -300), only the *band* between the two levels is kept
|
|
237
|
+
(``deep_zmax < z <= coast_zmax``). The domain extent is `extent_geom`
|
|
238
|
+
(a shapely polygon in the raster CRS) if provided -- the raster is then
|
|
239
|
+
clipped to it *before* the contour extraction and `domain_buffer` is
|
|
240
|
+
ignored -- otherwise the raster's data extent, grown (>0) or shrunk (<0)
|
|
241
|
+
by `domain_buffer` with ``buffer_area``. With `keep_largest` (default),
|
|
242
|
+
only the single largest water polygon is returned, dropping small
|
|
243
|
+
disconnected water bodies.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
raster_path : str
|
|
248
|
+
Path to the bathymetry raster (elevation, positive up).
|
|
249
|
+
coast_zmax : float, optional
|
|
250
|
+
Wet threshold (m); the water region is ``z <= coast_zmax``. Default
|
|
251
|
+
is 2.0.
|
|
252
|
+
domain_buffer : float, optional
|
|
253
|
+
Buffer factor applied to the raster's data extent (>0 grows, <0
|
|
254
|
+
shrinks), ignored when `extent_geom` is given. Default is -0.05.
|
|
255
|
+
deep_zmax : float or None, optional
|
|
256
|
+
Deep elevation level (m); if given, only the band
|
|
257
|
+
``deep_zmax < z <= coast_zmax`` is kept. Default is ``None``.
|
|
258
|
+
extent_geom : shapely.geometry.base.BaseGeometry or None, optional
|
|
259
|
+
Domain extent polygon in the raster CRS; if given, the raster is
|
|
260
|
+
clipped to it before contour extraction. Default is ``None``.
|
|
261
|
+
keep_largest : bool, optional
|
|
262
|
+
If ``True`` (default), keep only the single largest water polygon.
|
|
263
|
+
return_domain : bool, optional
|
|
264
|
+
If ``True``, also return the domain polygon actually used for the
|
|
265
|
+
clip (the extent polygon, or the buffered raster extent), in the
|
|
266
|
+
working CRS -- exactly the geometry the water polygon's cut lies on,
|
|
267
|
+
e.g. for flagging fixed vertices. Default is ``False``.
|
|
268
|
+
invert_z : bool, optional
|
|
269
|
+
Treat the raster as depth (positive down) by flipping its sign before
|
|
270
|
+
the coastline logic. Default ``False`` (elevation, positive up).
|
|
271
|
+
nodata_value : float or None, optional
|
|
272
|
+
Elevation (positive up) assigned to nodata / non-finite pixels.
|
|
273
|
+
``None`` (default) treats them as deep water so the sea (stored as
|
|
274
|
+
nodata) is always included regardless of ``coast_zmax``.
|
|
275
|
+
feedback : object or None, optional
|
|
276
|
+
Feedback sink exposing ``pushInfo``/``isCanceled`` (see
|
|
277
|
+
:class:`_NullFeedback`); a no-op sink is used if ``None``.
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
-------
|
|
281
|
+
poly : shapely.geometry.Polygon or shapely.geometry.MultiPolygon
|
|
282
|
+
Water domain polygon in the working CRS.
|
|
283
|
+
utm_crs : pyproj.CRS
|
|
284
|
+
Working CRS. Equal to `raster_crs` when the raster is already
|
|
285
|
+
projected (e.g. UTM); otherwise a local UTM CRS.
|
|
286
|
+
raster_crs : pyproj.CRS
|
|
287
|
+
CRS of the input raster.
|
|
288
|
+
domain_u : shapely geometry, only when ``return_domain=True``
|
|
289
|
+
Clip domain in the working CRS.
|
|
290
|
+
"""
|
|
291
|
+
feedback = feedback or _NullFeedback()
|
|
292
|
+
import numpy as np
|
|
293
|
+
import pyproj
|
|
294
|
+
import rasterio
|
|
295
|
+
from shapely.geometry import MultiPolygon
|
|
296
|
+
|
|
297
|
+
from bluemesh2d.geom_util.proj_util import get_local_utm_crs, reproject_geometry
|
|
298
|
+
from bluemesh2d.geom_util.poly_util import buffer_area
|
|
299
|
+
from bluemesh2d.geom_util.getiso import getiso_polygon
|
|
300
|
+
|
|
301
|
+
feedback.pushInfo("Reading raster ...")
|
|
302
|
+
# Read only the study window and decimate on read when the raster is too
|
|
303
|
+
# large, so an oversized bathymetry raster is clipped BEFORE (and while)
|
|
304
|
+
# reading -- never materialised in full.
|
|
305
|
+
max_cells = _contour_read_budget()
|
|
306
|
+
with bundled_raster_data_env(), rasterio.open(raster_path) as src:
|
|
307
|
+
from bluemesh2d.geom_util.proj_util import require_georeferenced
|
|
308
|
+
require_georeferenced(src)
|
|
309
|
+
raster_crs = _raster_crs(src)
|
|
310
|
+
nodata = src.nodata
|
|
311
|
+
bbox = tuple(extent_geom.bounds) if extent_geom is not None else None
|
|
312
|
+
zdat, lon, lat, step, clipped = _read_band_for_contour(
|
|
313
|
+
src, bbox, max_cells)
|
|
314
|
+
_check(feedback)
|
|
315
|
+
|
|
316
|
+
# optional Z reversal so the coastline logic (water = z <= coast_zmax)
|
|
317
|
+
# sees elevation positive up
|
|
318
|
+
zdat = np.asarray(zdat, dtype=float)
|
|
319
|
+
bad = ~np.isfinite(zdat)
|
|
320
|
+
if nodata is not None:
|
|
321
|
+
bad |= (zdat == nodata)
|
|
322
|
+
if invert_z:
|
|
323
|
+
zdat = -zdat
|
|
324
|
+
if bad.any():
|
|
325
|
+
# nodata pixels default to (deep) water, so they stay below the
|
|
326
|
+
# coastline level whatever its sign -- the usual raster convention
|
|
327
|
+
# where the sea is stored as nodata. A user-set ``nodata_value`` (an
|
|
328
|
+
# elevation, positive up) overrides this.
|
|
329
|
+
zdat[bad] = -1.0e30 if nodata_value is None else float(nodata_value)
|
|
330
|
+
|
|
331
|
+
if clipped:
|
|
332
|
+
feedback.pushInfo(
|
|
333
|
+
f"Raster clipped to the extent polygon: {zdat.shape[1]} x "
|
|
334
|
+
f"{zdat.shape[0]} px")
|
|
335
|
+
if domain_buffer:
|
|
336
|
+
feedback.pushInfo(
|
|
337
|
+
"Domain buffer is ignored when an extent polygon is provided.")
|
|
338
|
+
if step > 1:
|
|
339
|
+
feedback.pushInfo(
|
|
340
|
+
f"Raster too large to read at full resolution; contouring at "
|
|
341
|
+
f"1/{step} resolution ({zdat.shape[1]} x {zdat.shape[0]} px). "
|
|
342
|
+
"Provide a (smaller) extent polygon for a full-resolution "
|
|
343
|
+
"coastline.")
|
|
344
|
+
feedback.setProgress(15)
|
|
345
|
+
|
|
346
|
+
utm_crs = get_local_utm_crs(raster_crs, lon, lat)
|
|
347
|
+
|
|
348
|
+
feedback.pushInfo("Extracting coastline ...")
|
|
349
|
+
coast = MultiPolygon(getiso_polygon(lon, lat, zdat, zmax=coast_zmax))
|
|
350
|
+
if deep_zmax is not None:
|
|
351
|
+
# keep only the band between the two levels: deep_zmax < z <= coast_zmax
|
|
352
|
+
if deep_zmax >= coast_zmax:
|
|
353
|
+
raise RuntimeError(
|
|
354
|
+
f"The deep level ({deep_zmax}) must be below the coastline "
|
|
355
|
+
f"level ({coast_zmax}).")
|
|
356
|
+
feedback.pushInfo(f"Clipping to depth band {deep_zmax} < z <= {coast_zmax} ...")
|
|
357
|
+
deep = getiso_polygon(lon, lat, zdat, zmax=deep_zmax)
|
|
358
|
+
if deep:
|
|
359
|
+
coast = coast.difference(MultiPolygon(deep))
|
|
360
|
+
_check(feedback)
|
|
361
|
+
feedback.setProgress(60)
|
|
362
|
+
|
|
363
|
+
feedback.pushInfo("Building domain extent ...")
|
|
364
|
+
if extent_geom is not None:
|
|
365
|
+
domain = extent_geom
|
|
366
|
+
else:
|
|
367
|
+
domain = MultiPolygon(
|
|
368
|
+
getiso_polygon(lon, lat, zdat, zmax=float(np.max(zdat)) + 1.0))
|
|
369
|
+
if domain_buffer:
|
|
370
|
+
domain = buffer_area(domain, domain_buffer)
|
|
371
|
+
_check(feedback)
|
|
372
|
+
feedback.setProgress(85)
|
|
373
|
+
|
|
374
|
+
if utm_crs == raster_crs: # projected input: work natively in the tif CRS
|
|
375
|
+
domain_u = domain
|
|
376
|
+
poly = coast.intersection(domain_u)
|
|
377
|
+
else:
|
|
378
|
+
domain_u = reproject_geometry(domain, raster_crs, utm_crs)
|
|
379
|
+
poly = reproject_geometry(coast, raster_crs, utm_crs).intersection(
|
|
380
|
+
domain_u)
|
|
381
|
+
if poly.is_empty:
|
|
382
|
+
raise RuntimeError(
|
|
383
|
+
"No water polygon found: the coastline/domain intersection is empty. "
|
|
384
|
+
"Check the raster and the coastline level.")
|
|
385
|
+
|
|
386
|
+
# the intersection may be a Polygon, MultiPolygon or GeometryCollection
|
|
387
|
+
if poly.geom_type == "Polygon":
|
|
388
|
+
parts = [poly]
|
|
389
|
+
else:
|
|
390
|
+
parts = [g for g in poly.geoms if g.geom_type == "Polygon"]
|
|
391
|
+
if not parts:
|
|
392
|
+
raise RuntimeError("No polygonal water region in the intersection.")
|
|
393
|
+
feedback.pushInfo(f"Water region(s): {len(parts)}")
|
|
394
|
+
if keep_largest and len(parts) > 1:
|
|
395
|
+
poly = max(parts, key=lambda g: g.area)
|
|
396
|
+
feedback.pushInfo(
|
|
397
|
+
f"Keeping only the largest region ({poly.area / 1e6:.0f} km2).")
|
|
398
|
+
if return_domain:
|
|
399
|
+
return poly, utm_crs, raster_crs, domain_u
|
|
400
|
+
return poly, utm_crs, raster_crs
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ===========================================================================
|
|
404
|
+
# Stage 2: raster -> gradient-limited mesh-size (hfun) raster
|
|
405
|
+
# ===========================================================================
|
|
406
|
+
|