cht_utils 2.0.0__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.
- cht_utils/__init__.py +28 -0
- cht_utils/cog/__init__.py +6 -0
- cht_utils/cog/geotiff_to_cog.py +79 -0
- cht_utils/cog/netcdf_to_cog.py +85 -0
- cht_utils/cog/xyz_to_cog.py +86 -0
- cht_utils/colors/__init__.py +6 -0
- cht_utils/colors/colors.py +117 -0
- cht_utils/fileio/__init__.py +21 -0
- cht_utils/fileio/deltares_ini.py +326 -0
- cht_utils/fileio/json_js.py +72 -0
- cht_utils/fileio/pli_file.py +233 -0
- cht_utils/fileio/tekal.py +234 -0
- cht_utils/fileio/xml.py +184 -0
- cht_utils/fileio/yaml.py +39 -0
- cht_utils/fileops/__init__.py +25 -0
- cht_utils/fileops/fileops.py +344 -0
- cht_utils/interpolation/__init__.py +5 -0
- cht_utils/interpolation/interpolation.py +152 -0
- cht_utils/maps/__init__.py +2 -0
- cht_utils/maps/fileops.py +191 -0
- cht_utils/maps/flood_map.py +1231 -0
- cht_utils/maps/topobathy_map.py +463 -0
- cht_utils/maps/utils.py +700 -0
- cht_utils/physics/__init__.py +8 -0
- cht_utils/physics/deshoal.py +63 -0
- cht_utils/physics/disper.py +91 -0
- cht_utils/physics/runup_vo21.py +229 -0
- cht_utils/physics/waves.py +59 -0
- cht_utils/probabilistic/__init__.py +5 -0
- cht_utils/probabilistic/prob_maps.py +263 -0
- cht_utils/remote/__init__.py +4 -0
- cht_utils/remote/s3.py +380 -0
- cht_utils/remote/sftp.py +192 -0
- cht_utils-2.0.0.dist-info/METADATA +30 -0
- cht_utils-2.0.0.dist-info/RECORD +39 -0
- cht_utils-2.0.0.dist-info/WHEEL +5 -0
- cht_utils-2.0.0.dist-info/licenses/LICENSE +21 -0
- cht_utils-2.0.0.dist-info/top_level.txt +1 -0
- cht_utils-2.0.0.dist-info/zip-safe +1 -0
cht_utils/maps/utils.py
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
"""Utility functions for tile coordinate conversions, elevation/PNG encoding, and interpolation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import glob
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
from PIL import Image
|
|
12
|
+
from scipy.interpolate import RegularGridInterpolator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_zoom_level_for_resolution(dx: float) -> int:
|
|
16
|
+
"""Determine the tile zoom level that matches a given spatial resolution.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
dx : float
|
|
21
|
+
Desired pixel size in metres.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
int
|
|
26
|
+
Zoom level (0-23) whose native pixel size is just below *dx*.
|
|
27
|
+
"""
|
|
28
|
+
dxy = 156543.03 / 2 ** np.arange(24)
|
|
29
|
+
izoom = np.where(dxy < dx)[0]
|
|
30
|
+
if len(izoom) == 0:
|
|
31
|
+
izoom = 23
|
|
32
|
+
else:
|
|
33
|
+
izoom = int(izoom[0])
|
|
34
|
+
return izoom
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_zoom_level(npixels: int, lat_range: list[float], max_zoom: int) -> int:
|
|
38
|
+
"""Determine the zoom level needed to cover a latitude range with a given pixel count.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
npixels : int
|
|
43
|
+
Number of pixels available in the latitude direction.
|
|
44
|
+
lat_range : list[float]
|
|
45
|
+
Two-element list ``[lat_min, lat_max]`` in degrees.
|
|
46
|
+
max_zoom : int
|
|
47
|
+
Maximum allowed zoom level.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
int
|
|
52
|
+
Appropriate zoom level.
|
|
53
|
+
"""
|
|
54
|
+
dxr = (lat_range[1] - lat_range[0]) * 111111 / npixels
|
|
55
|
+
dxy = 156543.03 / 2 ** np.arange(max_zoom + 1)
|
|
56
|
+
izoom = np.where(dxy < dxr)[0]
|
|
57
|
+
if len(izoom) == 0:
|
|
58
|
+
izoom = max_zoom
|
|
59
|
+
else:
|
|
60
|
+
izoom = izoom[0]
|
|
61
|
+
return izoom
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def webmercator_to_lat_lon(easting: float, northing: float) -> tuple[float, float]:
|
|
65
|
+
"""Convert Web Mercator coordinates to latitude and longitude.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
easting : float
|
|
70
|
+
Web Mercator easting in metres.
|
|
71
|
+
northing : float
|
|
72
|
+
Web Mercator northing in metres.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
tuple[float, float]
|
|
77
|
+
``(latitude, longitude)`` in degrees.
|
|
78
|
+
"""
|
|
79
|
+
lon = (easting / 20037508.34) * 180
|
|
80
|
+
lat = (180 / math.pi) * (
|
|
81
|
+
2 * math.atan(math.exp(northing / 20037508.34 * math.pi)) - (math.pi / 2)
|
|
82
|
+
)
|
|
83
|
+
return lat, lon
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def lat_lon_to_webmercator(lat: float, lon: float) -> tuple[float, float]:
|
|
87
|
+
"""Convert latitude and longitude to Web Mercator coordinates.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
lat : float
|
|
92
|
+
Latitude in degrees.
|
|
93
|
+
lon : float
|
|
94
|
+
Longitude in degrees.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
tuple[float, float]
|
|
99
|
+
``(x, y)`` in Web Mercator metres.
|
|
100
|
+
"""
|
|
101
|
+
x = lon * 20037508.34 / 180
|
|
102
|
+
y = (math.log(math.tan((90 + lat) * math.pi / 360)) / math.pi) * 20037508.34
|
|
103
|
+
return x, y
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def lat_lon_to_tile_indices(lat: float, lon: float, zoom: int) -> tuple[int, int]:
|
|
107
|
+
"""Convert latitude/longitude to slippy-map tile indices.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
lat : float
|
|
112
|
+
Latitude in degrees.
|
|
113
|
+
lon : float
|
|
114
|
+
Longitude in degrees.
|
|
115
|
+
zoom : int
|
|
116
|
+
Zoom level.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
tuple[int, int]
|
|
121
|
+
``(tile_x, tile_y)`` column and row indices.
|
|
122
|
+
"""
|
|
123
|
+
tile_x = int((lon + 180) / 360 * (2**zoom))
|
|
124
|
+
tile_y = int(
|
|
125
|
+
(
|
|
126
|
+
1
|
|
127
|
+
- (
|
|
128
|
+
math.log(math.tan(math.radians(lat)) + 1 / math.cos(math.radians(lat)))
|
|
129
|
+
/ math.pi
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
/ 2
|
|
133
|
+
* (2**zoom)
|
|
134
|
+
)
|
|
135
|
+
return tile_x, tile_y
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def xy2num(easting: float, northing: float, zoom: int) -> tuple[int, int]:
|
|
139
|
+
"""Convert Web Mercator coordinates to tile indices.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
easting : float
|
|
144
|
+
Web Mercator easting in metres.
|
|
145
|
+
northing : float
|
|
146
|
+
Web Mercator northing in metres.
|
|
147
|
+
zoom : int
|
|
148
|
+
Zoom level.
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
tuple[int, int]
|
|
153
|
+
``(tile_x, tile_y)`` column and row indices.
|
|
154
|
+
"""
|
|
155
|
+
lat, lon = webmercator_to_lat_lon(easting, northing)
|
|
156
|
+
ix, it = lat_lon_to_tile_indices(lat, lon, zoom)
|
|
157
|
+
return ix, it
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def deg2num(lat_deg: float, lon_deg: float, zoom: int) -> tuple[int, int]:
|
|
161
|
+
"""Return the column and row index of a slippy tile for a given lat/lon.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
lat_deg : float
|
|
166
|
+
Latitude in degrees.
|
|
167
|
+
lon_deg : float
|
|
168
|
+
Longitude in degrees.
|
|
169
|
+
zoom : int
|
|
170
|
+
Zoom level.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
tuple[int, int]
|
|
175
|
+
``(xtile, ytile)`` column and row indices.
|
|
176
|
+
"""
|
|
177
|
+
lat_rad = math.radians(lat_deg)
|
|
178
|
+
n = 2**zoom
|
|
179
|
+
xtile = int((lon_deg + 180.0) / 360.0 * n)
|
|
180
|
+
ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
|
|
181
|
+
return (xtile, ytile)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def num2deg(xtile: int, ytile: int, zoom: int) -> tuple[float, float]:
|
|
185
|
+
"""Return the upper-left latitude and longitude of a slippy tile.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
xtile : int
|
|
190
|
+
Tile column index.
|
|
191
|
+
ytile : int
|
|
192
|
+
Tile row index.
|
|
193
|
+
zoom : int
|
|
194
|
+
Zoom level.
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
tuple[float, float]
|
|
199
|
+
``(latitude, longitude)`` of the upper-left corner in degrees.
|
|
200
|
+
"""
|
|
201
|
+
n = 2**zoom
|
|
202
|
+
lon_deg = xtile / n * 360.0 - 180.0
|
|
203
|
+
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
|
|
204
|
+
lat_deg = math.degrees(lat_rad)
|
|
205
|
+
return (lat_deg, lon_deg)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def num2xy(xtile: int, ytile: int, zoom: int) -> tuple[float, float]:
|
|
209
|
+
"""Return the upper-left Web Mercator x/y of a slippy tile.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
xtile : int
|
|
214
|
+
Tile column index.
|
|
215
|
+
ytile : int
|
|
216
|
+
Tile row index.
|
|
217
|
+
zoom : int
|
|
218
|
+
Zoom level.
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
tuple[float, float]
|
|
223
|
+
``(x, y)`` in Web Mercator metres.
|
|
224
|
+
"""
|
|
225
|
+
n = 2**zoom
|
|
226
|
+
lon_deg = xtile / n * 360.0 - 180.0
|
|
227
|
+
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
|
|
228
|
+
lat_deg = math.degrees(lat_rad)
|
|
229
|
+
x, y = lat_lon_to_webmercator(lat_deg, lon_deg)
|
|
230
|
+
return x, y
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def num2deg_ll(xtile: int, ytile: int, zoom: int) -> tuple[float, float]:
|
|
234
|
+
"""Return the lower-left latitude and longitude of a slippy tile (old format).
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
xtile : int
|
|
239
|
+
Tile column index.
|
|
240
|
+
ytile : int
|
|
241
|
+
Tile row index.
|
|
242
|
+
zoom : int
|
|
243
|
+
Zoom level.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
tuple[float, float]
|
|
248
|
+
``(latitude, longitude)`` of the lower-left corner in degrees.
|
|
249
|
+
"""
|
|
250
|
+
n = 2**zoom
|
|
251
|
+
lon_deg = xtile / n * 360.0 - 180.0
|
|
252
|
+
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
|
|
253
|
+
lat_deg = math.degrees(-lat_rad)
|
|
254
|
+
return (lat_deg, lon_deg)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def num2deg_ur(xtile: int, ytile: int, zoom: int) -> tuple[float, float]:
|
|
258
|
+
"""Return the upper-right latitude and longitude of a slippy tile (old format).
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
xtile : int
|
|
263
|
+
Tile column index.
|
|
264
|
+
ytile : int
|
|
265
|
+
Tile row index.
|
|
266
|
+
zoom : int
|
|
267
|
+
Zoom level.
|
|
268
|
+
|
|
269
|
+
Returns
|
|
270
|
+
-------
|
|
271
|
+
tuple[float, float]
|
|
272
|
+
``(latitude, longitude)`` of the upper-right corner in degrees.
|
|
273
|
+
"""
|
|
274
|
+
n = 2**zoom
|
|
275
|
+
lon_deg = (xtile + 1) / n * 360.0 - 180.0
|
|
276
|
+
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * (ytile + 1) / n)))
|
|
277
|
+
lat_deg = math.degrees(-lat_rad)
|
|
278
|
+
return (lat_deg, lon_deg)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_lower_left_corner(tile_x: int, tile_y: int, zoom: int) -> tuple[float, float]:
|
|
282
|
+
"""Return the lower-left Web Mercator coordinates of a tile.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
tile_x : int
|
|
287
|
+
Tile column index.
|
|
288
|
+
tile_y : int
|
|
289
|
+
Tile row index.
|
|
290
|
+
zoom : int
|
|
291
|
+
Zoom level.
|
|
292
|
+
|
|
293
|
+
Returns
|
|
294
|
+
-------
|
|
295
|
+
tuple[float, float]
|
|
296
|
+
``(ll_x, ll_y)`` lower-left corner in Web Mercator metres.
|
|
297
|
+
"""
|
|
298
|
+
total_size = 20037508.34 * 2
|
|
299
|
+
tile_size = total_size / (2**zoom)
|
|
300
|
+
ll_x = tile_x * tile_size - 20037508.34
|
|
301
|
+
ll_y = 20037508.34 - (tile_y + 1) * tile_size
|
|
302
|
+
return ll_x, ll_y
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def elevation2png(
|
|
306
|
+
val: NDArray[np.floating],
|
|
307
|
+
png_file: str,
|
|
308
|
+
encoder: str = "terrarium",
|
|
309
|
+
encoder_vmin: float = 0.0,
|
|
310
|
+
encoder_vmax: float = 1.0,
|
|
311
|
+
compress_level: int = 6,
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Convert a 256x256 NumPy array to a PNG tile using a specified encoder.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
val : NDArray[np.floating]
|
|
318
|
+
256x256 array of elevation or data values.
|
|
319
|
+
png_file : str
|
|
320
|
+
Output PNG file path.
|
|
321
|
+
encoder : str
|
|
322
|
+
Encoding scheme. One of ``"terrarium"``, ``"terrarium16"``, ``"uint8"``,
|
|
323
|
+
``"uint16"``, ``"uint24"``, ``"uint32"``, ``"float8"``, ``"float16"``,
|
|
324
|
+
``"float24"``, ``"float32"``.
|
|
325
|
+
encoder_vmin : float
|
|
326
|
+
Minimum value for float encoders.
|
|
327
|
+
encoder_vmax : float
|
|
328
|
+
Maximum value for float encoders.
|
|
329
|
+
compress_level : int
|
|
330
|
+
PNG compression level (0-9).
|
|
331
|
+
|
|
332
|
+
Raises
|
|
333
|
+
------
|
|
334
|
+
ValueError
|
|
335
|
+
If values exceed the range supported by the chosen encoder.
|
|
336
|
+
"""
|
|
337
|
+
if encoder == "terrarium":
|
|
338
|
+
rgb = np.zeros((256, 256, 3), "uint8")
|
|
339
|
+
val += 32768.0
|
|
340
|
+
rgb[:, :, 0] = np.floor(val / 256).astype(int)
|
|
341
|
+
rgb[:, :, 1] = np.floor(val % 256)
|
|
342
|
+
rgb[:, :, 2] = np.floor((val - np.floor(val)) * 256).astype(int)
|
|
343
|
+
elif encoder == "terrarium16":
|
|
344
|
+
rgb = np.zeros((256, 256, 3), "uint8")
|
|
345
|
+
val += 32768.0
|
|
346
|
+
rgb[:, :, 0] = np.floor(val / 256).astype(int)
|
|
347
|
+
rgb[:, :, 1] = np.floor(val % 256).astype(int)
|
|
348
|
+
elif encoder == "uint8":
|
|
349
|
+
if np.any(val >= 255):
|
|
350
|
+
raise ValueError(
|
|
351
|
+
"Some values in are equal to or larger than 255. This is not allowed for encoder 'uint8'."
|
|
352
|
+
)
|
|
353
|
+
rgb = np.zeros((256, 256, 3), "uint8") + 255
|
|
354
|
+
r = val + 0
|
|
355
|
+
r[np.where(val < 0)] = 255
|
|
356
|
+
rgb[:, :, 0] = r
|
|
357
|
+
elif encoder == "uint16":
|
|
358
|
+
if np.any(val >= 65535):
|
|
359
|
+
raise ValueError(
|
|
360
|
+
"Some values are equal to or larger than 65535. This is not allowed for encoder 'uint16'."
|
|
361
|
+
)
|
|
362
|
+
rgb = np.zeros((256, 256, 3), "uint8") + 255
|
|
363
|
+
r = (val // 256) % 256
|
|
364
|
+
g = val % 256
|
|
365
|
+
r[np.where(val < 0)] = 255
|
|
366
|
+
g[np.where(val < 0)] = 255
|
|
367
|
+
rgb[:, :, 0] = r
|
|
368
|
+
rgb[:, :, 1] = g
|
|
369
|
+
elif encoder == "uint24":
|
|
370
|
+
if np.any(val >= 16777215):
|
|
371
|
+
raise ValueError(
|
|
372
|
+
"Some values are equal to or larger than 16777215. This is not allowed for encoder 'uint24'."
|
|
373
|
+
)
|
|
374
|
+
rgb = np.zeros((256, 256, 3), "uint8") + 255
|
|
375
|
+
r = (val // 256**2) % 256
|
|
376
|
+
g = (val // 256) % 256
|
|
377
|
+
b = val % 256
|
|
378
|
+
r[np.where(val < 0)] = 255
|
|
379
|
+
g[np.where(val < 0)] = 255
|
|
380
|
+
b[np.where(val < 0)] = 255
|
|
381
|
+
rgb[:, :, 0] = r
|
|
382
|
+
rgb[:, :, 1] = g
|
|
383
|
+
rgb[:, :, 2] = b
|
|
384
|
+
elif encoder == "uint32":
|
|
385
|
+
if np.any(val >= 4294967295):
|
|
386
|
+
raise ValueError(
|
|
387
|
+
"Some values are equal to or larger than 4294967295. This is not allowed for encoder 'uint32'."
|
|
388
|
+
)
|
|
389
|
+
rgb = np.zeros((256, 256, 4), "uint8") + 255
|
|
390
|
+
r = (val // 256**3) % 256
|
|
391
|
+
g = (val // 256**2) % 256
|
|
392
|
+
b = (val // 256) % 256
|
|
393
|
+
a = val % 256
|
|
394
|
+
r[np.where(val < 0)] = 255
|
|
395
|
+
g[np.where(val < 0)] = 255
|
|
396
|
+
b[np.where(val < 0)] = 255
|
|
397
|
+
a[np.where(val < 0)] = 255
|
|
398
|
+
rgb[:, :, 0] = r
|
|
399
|
+
rgb[:, :, 1] = g
|
|
400
|
+
rgb[:, :, 2] = b
|
|
401
|
+
rgb[:, :, 3] = a
|
|
402
|
+
elif encoder == "float8":
|
|
403
|
+
val = np.maximum(val, encoder_vmin)
|
|
404
|
+
val = np.minimum(val, encoder_vmax)
|
|
405
|
+
val = val - encoder_vmin
|
|
406
|
+
i = np.floor(val * 254 / (encoder_vmax - encoder_vmin)).astype(int) + 1
|
|
407
|
+
i[np.isnan(val)] = 0
|
|
408
|
+
rgb = np.zeros((256, 256, 3), "uint8")
|
|
409
|
+
rgb[:, :, 0] = i
|
|
410
|
+
elif encoder == "float16":
|
|
411
|
+
val = np.maximum(val, encoder_vmin)
|
|
412
|
+
val = np.minimum(val, encoder_vmax)
|
|
413
|
+
val = val - encoder_vmin
|
|
414
|
+
i = np.floor(val * 65534 / (encoder_vmax - encoder_vmin)).astype(int) + 1
|
|
415
|
+
i[np.isnan(val)] = 0
|
|
416
|
+
rgb = np.zeros((256, 256, 3), "uint8")
|
|
417
|
+
rgb[:, :, 0] = (i // 256) % 256
|
|
418
|
+
rgb[:, :, 1] = i % 256
|
|
419
|
+
elif encoder == "float24":
|
|
420
|
+
val = np.maximum(val, encoder_vmin)
|
|
421
|
+
val = np.minimum(val, encoder_vmax)
|
|
422
|
+
val = val - encoder_vmin
|
|
423
|
+
i = np.floor(val * 16777214 / (encoder_vmax - encoder_vmin)).astype(int) + 1
|
|
424
|
+
i[np.isnan(val)] = 0
|
|
425
|
+
rgb = np.zeros((256, 256, 3), "uint8")
|
|
426
|
+
rgb[:, :, 0] = (i // 256**2) % 256
|
|
427
|
+
rgb[:, :, 1] = (i // 256) % 256
|
|
428
|
+
rgb[:, :, 2] = i % 256
|
|
429
|
+
elif encoder == "float32":
|
|
430
|
+
val = np.maximum(val, encoder_vmin)
|
|
431
|
+
val = np.minimum(val, encoder_vmax)
|
|
432
|
+
val = val - encoder_vmin
|
|
433
|
+
i = np.floor(val * 4294967294 / (encoder_vmax - encoder_vmin)).astype(int) + 1
|
|
434
|
+
i[np.isnan(val)] = 0
|
|
435
|
+
rgb = np.zeros((256, 256, 4), "uint8")
|
|
436
|
+
rgb[:, :, 0] = (i // 256**3) % 256
|
|
437
|
+
rgb[:, :, 1] = (i // 256**2) % 256
|
|
438
|
+
rgb[:, :, 2] = (i // 256) % 256
|
|
439
|
+
rgb[:, :, 3] = i % 256
|
|
440
|
+
|
|
441
|
+
img = Image.fromarray(rgb)
|
|
442
|
+
img.save(png_file, compress_level=compress_level)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def png2elevation(
|
|
446
|
+
png_file: str,
|
|
447
|
+
encoder: str = "terrarium",
|
|
448
|
+
encoder_vmin: float = 0.0,
|
|
449
|
+
encoder_vmax: float = 1.0,
|
|
450
|
+
) -> NDArray[np.floating]:
|
|
451
|
+
"""Convert a PNG tile back to an elevation array using a specified encoder.
|
|
452
|
+
|
|
453
|
+
Parameters
|
|
454
|
+
----------
|
|
455
|
+
png_file : str
|
|
456
|
+
Input PNG file path.
|
|
457
|
+
encoder : str
|
|
458
|
+
Encoding scheme used when the tile was created.
|
|
459
|
+
encoder_vmin : float
|
|
460
|
+
Minimum value for float encoders.
|
|
461
|
+
encoder_vmax : float
|
|
462
|
+
Maximum value for float encoders.
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
NDArray[np.floating]
|
|
467
|
+
256x256 array of decoded elevation or data values.
|
|
468
|
+
"""
|
|
469
|
+
img = Image.open(png_file)
|
|
470
|
+
if encoder == "terrarium":
|
|
471
|
+
rgb = np.array(img.convert("RGB")).astype(float)
|
|
472
|
+
elevation = (rgb[:, :, 0] * 256 + rgb[:, :, 1] + rgb[:, :, 2] / 256) - 32768.0
|
|
473
|
+
elevation[np.where(elevation < -32767.0)] = np.nan
|
|
474
|
+
elif encoder == "terrarium16":
|
|
475
|
+
rgb = np.array(img.convert("RGB")).astype(float)
|
|
476
|
+
elevation = (rgb[:, :, 0] * 256 + rgb[:, :, 1]) - 32768.0
|
|
477
|
+
elevation[np.where(elevation < -32767.0)] = np.nan
|
|
478
|
+
elif encoder == "uint8":
|
|
479
|
+
rgb = np.array(img.convert("RGB")).astype(int)
|
|
480
|
+
elevation = rgb[:, :, 0]
|
|
481
|
+
elevation[np.where(elevation == 255)] = -1
|
|
482
|
+
elif encoder == "uint16":
|
|
483
|
+
rgb = np.array(img.convert("RGB")).astype(int)
|
|
484
|
+
elevation = rgb[:, :, 0] * 256 + rgb[:, :, 1]
|
|
485
|
+
elevation[np.where(elevation == 65535)] = -1
|
|
486
|
+
elif encoder == "uint24":
|
|
487
|
+
rgb = np.array(img.convert("RGB")).astype(int)
|
|
488
|
+
elevation = rgb[:, :, 0] * 65536 + rgb[:, :, 1] * 256 + rgb[:, :, 2]
|
|
489
|
+
elevation[np.where(elevation == 16777215)] = -1
|
|
490
|
+
elif encoder == "uint32":
|
|
491
|
+
rgb = np.array(img.convert("RGBA")).astype(int)
|
|
492
|
+
elevation = (
|
|
493
|
+
rgb[:, :, 0] * 16777216
|
|
494
|
+
+ rgb[:, :, 1] * 65536
|
|
495
|
+
+ rgb[:, :, 2] * 256
|
|
496
|
+
+ rgb[:, :, 3]
|
|
497
|
+
)
|
|
498
|
+
elevation[np.where(elevation == 4294967295)] = -1
|
|
499
|
+
elif encoder == "float8":
|
|
500
|
+
rgb = np.array(img.convert("RGB")).astype(float)
|
|
501
|
+
i = rgb[:, :, 0]
|
|
502
|
+
elevation = encoder_vmin + (encoder_vmax - encoder_vmin) * i / 254
|
|
503
|
+
elevation[np.where(i == 0)] = np.nan
|
|
504
|
+
elif encoder == "float16":
|
|
505
|
+
rgb = np.array(img.convert("RGB")).astype(float)
|
|
506
|
+
i = rgb[:, :, 0] * 256 + rgb[:, :, 1]
|
|
507
|
+
elevation = encoder_vmin + (encoder_vmax - encoder_vmin) * i / 65534
|
|
508
|
+
elevation[np.where(i == 0)] = np.nan
|
|
509
|
+
elif encoder == "float24":
|
|
510
|
+
rgb = np.array(img.convert("RGB")).astype(float)
|
|
511
|
+
i = rgb[:, :, 0] * 65536 + rgb[:, :, 1] * 256 + rgb[:, :, 2]
|
|
512
|
+
elevation = encoder_vmin + (encoder_vmax - encoder_vmin) * i / 16777214
|
|
513
|
+
elevation[np.where(i == 0)] = np.nan
|
|
514
|
+
elif encoder == "float32":
|
|
515
|
+
rgb = np.array(img.convert("RGBA")).astype(float)
|
|
516
|
+
i = (
|
|
517
|
+
rgb[:, :, 0] * 16777216
|
|
518
|
+
+ rgb[:, :, 1] * 65536
|
|
519
|
+
+ rgb[:, :, 2] * 256
|
|
520
|
+
+ rgb[:, :, 3]
|
|
521
|
+
)
|
|
522
|
+
elevation = encoder_vmin + (encoder_vmax - encoder_vmin) * i / 4294967294
|
|
523
|
+
elevation[np.where(i == 0)] = np.nan
|
|
524
|
+
return elevation
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def png2int(png_file: str, idummy: int) -> NDArray[np.signedinteger]:
|
|
528
|
+
"""Convert a PNG tile to an integer index array.
|
|
529
|
+
|
|
530
|
+
Parameters
|
|
531
|
+
----------
|
|
532
|
+
png_file : str
|
|
533
|
+
Input PNG file path.
|
|
534
|
+
idummy : int
|
|
535
|
+
Value to assign to pixels that decode as the maximum RGBA integer (no-data).
|
|
536
|
+
|
|
537
|
+
Returns
|
|
538
|
+
-------
|
|
539
|
+
NDArray[np.signedinteger]
|
|
540
|
+
256x256 integer index array.
|
|
541
|
+
"""
|
|
542
|
+
image = Image.open(png_file)
|
|
543
|
+
rgba = np.array(image.convert("RGBA")).astype(int)
|
|
544
|
+
ind = (
|
|
545
|
+
(rgba[:, :, 0] * 256**3)
|
|
546
|
+
+ (rgba[:, :, 1] * 256**2)
|
|
547
|
+
+ (rgba[:, :, 2] * 256)
|
|
548
|
+
+ rgba[:, :, 3]
|
|
549
|
+
)
|
|
550
|
+
ind[np.where(ind == 4294967295)] = idummy
|
|
551
|
+
return ind
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def int2png(val: NDArray[np.signedinteger], png_file: str) -> None:
|
|
555
|
+
"""Convert an integer index array to a PNG tile.
|
|
556
|
+
|
|
557
|
+
Parameters
|
|
558
|
+
----------
|
|
559
|
+
val : NDArray[np.signedinteger]
|
|
560
|
+
256x256 integer array. Negative values are encoded as no-data (all 255).
|
|
561
|
+
png_file : str
|
|
562
|
+
Output PNG file path.
|
|
563
|
+
"""
|
|
564
|
+
rgba = np.zeros((256, 256, 4), "uint8") + 255
|
|
565
|
+
r = (val // 256**3) % 256
|
|
566
|
+
g = (val // 256**2) % 256
|
|
567
|
+
b = (val // 256) % 256
|
|
568
|
+
a = val % 256
|
|
569
|
+
r[np.where(val < 0)] = 255
|
|
570
|
+
g[np.where(val < 0)] = 255
|
|
571
|
+
b[np.where(val < 0)] = 255
|
|
572
|
+
a[np.where(val < 0)] = 255
|
|
573
|
+
rgba[:, :, 0] = r
|
|
574
|
+
rgba[:, :, 1] = g
|
|
575
|
+
rgba[:, :, 2] = b
|
|
576
|
+
rgba[:, :, 3] = a
|
|
577
|
+
img = Image.fromarray(rgba)
|
|
578
|
+
img.save(png_file)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def makedir(path: str) -> None:
|
|
582
|
+
"""Create a directory (and parents) if it does not already exist.
|
|
583
|
+
|
|
584
|
+
Parameters
|
|
585
|
+
----------
|
|
586
|
+
path : str
|
|
587
|
+
Directory path to create.
|
|
588
|
+
"""
|
|
589
|
+
if not os.path.exists(path):
|
|
590
|
+
os.makedirs(path)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def list_files(src: str) -> list[str]:
|
|
594
|
+
"""List files matching a glob pattern.
|
|
595
|
+
|
|
596
|
+
Parameters
|
|
597
|
+
----------
|
|
598
|
+
src : str
|
|
599
|
+
Glob pattern (e.g. ``"/data/*.png"``).
|
|
600
|
+
|
|
601
|
+
Returns
|
|
602
|
+
-------
|
|
603
|
+
list[str]
|
|
604
|
+
List of matching file paths.
|
|
605
|
+
"""
|
|
606
|
+
file_list = []
|
|
607
|
+
full_list = glob.glob(src)
|
|
608
|
+
for item in full_list:
|
|
609
|
+
if os.path.isfile(item):
|
|
610
|
+
file_list.append(item)
|
|
611
|
+
return file_list
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def list_folders(src: str, basename: bool = False) -> list[str]:
|
|
615
|
+
"""List directories matching a glob pattern.
|
|
616
|
+
|
|
617
|
+
Parameters
|
|
618
|
+
----------
|
|
619
|
+
src : str
|
|
620
|
+
Glob pattern.
|
|
621
|
+
basename : bool
|
|
622
|
+
If True, return only the directory base names instead of full paths.
|
|
623
|
+
|
|
624
|
+
Returns
|
|
625
|
+
-------
|
|
626
|
+
list[str]
|
|
627
|
+
List of matching directory paths (or base names).
|
|
628
|
+
"""
|
|
629
|
+
folder_list = []
|
|
630
|
+
full_list = glob.glob(src)
|
|
631
|
+
for item in full_list:
|
|
632
|
+
if os.path.isdir(item):
|
|
633
|
+
if basename:
|
|
634
|
+
folder_list.append(os.path.basename(item))
|
|
635
|
+
else:
|
|
636
|
+
folder_list.append(item)
|
|
637
|
+
|
|
638
|
+
return folder_list
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def interp2(
|
|
642
|
+
x0: NDArray[np.floating],
|
|
643
|
+
y0: NDArray[np.floating],
|
|
644
|
+
z0: NDArray[np.floating],
|
|
645
|
+
x1: NDArray[np.floating],
|
|
646
|
+
y1: NDArray[np.floating],
|
|
647
|
+
) -> NDArray[np.floating]:
|
|
648
|
+
"""Bilinear interpolation from a regular grid onto scattered target points.
|
|
649
|
+
|
|
650
|
+
Parameters
|
|
651
|
+
----------
|
|
652
|
+
x0 : NDArray[np.floating]
|
|
653
|
+
1-D x-coordinates of the source grid.
|
|
654
|
+
y0 : NDArray[np.floating]
|
|
655
|
+
1-D y-coordinates of the source grid.
|
|
656
|
+
z0 : NDArray[np.floating]
|
|
657
|
+
2-D source values, shape ``(len(y0), len(x0))``.
|
|
658
|
+
x1 : NDArray[np.floating]
|
|
659
|
+
2-D target x-coordinates.
|
|
660
|
+
y1 : NDArray[np.floating]
|
|
661
|
+
2-D target y-coordinates, same shape as *x1*.
|
|
662
|
+
|
|
663
|
+
Returns
|
|
664
|
+
-------
|
|
665
|
+
NDArray[np.floating]
|
|
666
|
+
Interpolated values at target locations, same shape as *x1*.
|
|
667
|
+
"""
|
|
668
|
+
f = RegularGridInterpolator((y0, x0), z0, bounds_error=False, fill_value=np.nan)
|
|
669
|
+
sz = x1.shape
|
|
670
|
+
x1 = x1.reshape(sz[0] * sz[1])
|
|
671
|
+
y1 = y1.reshape(sz[0] * sz[1])
|
|
672
|
+
z1 = f((y1, x1)).reshape(sz)
|
|
673
|
+
return z1
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def binary_search(
|
|
677
|
+
val_array: NDArray[np.number], vals: NDArray[np.number]
|
|
678
|
+
) -> NDArray[np.signedinteger]:
|
|
679
|
+
"""Find indices of *vals* within a sorted *val_array* using binary search.
|
|
680
|
+
|
|
681
|
+
Parameters
|
|
682
|
+
----------
|
|
683
|
+
val_array : NDArray[np.number]
|
|
684
|
+
Sorted 1-D array of reference values.
|
|
685
|
+
vals : NDArray[np.number]
|
|
686
|
+
Values to look up.
|
|
687
|
+
|
|
688
|
+
Returns
|
|
689
|
+
-------
|
|
690
|
+
NDArray[np.signedinteger]
|
|
691
|
+
Index into *val_array* for each element of *vals*, or -1 if not found.
|
|
692
|
+
"""
|
|
693
|
+
indx = np.searchsorted(val_array, vals)
|
|
694
|
+
not_ok = np.where(indx == len(val_array))[0]
|
|
695
|
+
indx[np.where(indx == len(val_array))[0]] = 0
|
|
696
|
+
is_ok = np.where(val_array[indx] == vals)[0]
|
|
697
|
+
indices = np.zeros(len(vals), dtype=int) - 1
|
|
698
|
+
indices[is_ok] = indx[is_ok]
|
|
699
|
+
indices[not_ok] = -1
|
|
700
|
+
return indices
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Coastal and wave physics utilities."""
|
|
2
|
+
|
|
3
|
+
from cht_utils.physics.deshoal import deshoal as deshoal
|
|
4
|
+
from cht_utils.physics.deshoal import wavecelerity as wavecelerity
|
|
5
|
+
from cht_utils.physics.disper import disper as disper
|
|
6
|
+
from cht_utils.physics.disper import disper_fentonmckee as disper_fentonmckee
|
|
7
|
+
from cht_utils.physics.runup_vo21 import runup_vo21 as runup_vo21
|
|
8
|
+
from cht_utils.physics.waves import split_waves_guza as split_waves_guza
|