maps4fs 1.5.7__py3-none-any.whl → 1.7.1__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.
- maps4fs/__init__.py +3 -1
- maps4fs/generator/background.py +92 -8
- maps4fs/generator/component.py +23 -9
- maps4fs/generator/dem.py +12 -49
- maps4fs/generator/dtm/__init__.py +0 -0
- maps4fs/generator/{dtm.py → dtm/dtm.py} +59 -71
- maps4fs/generator/dtm/srtm.py +226 -0
- maps4fs/generator/dtm/usgs.py +351 -0
- maps4fs/generator/game.py +1 -1
- maps4fs/generator/grle.py +94 -28
- maps4fs/generator/i3d.py +20 -14
- maps4fs/generator/map.py +22 -2
- maps4fs/generator/satellite.py +1 -1
- maps4fs/generator/settings.py +41 -4
- maps4fs/generator/texture.py +107 -59
- {maps4fs-1.5.7.dist-info → maps4fs-1.7.1.dist-info}/METADATA +58 -16
- maps4fs-1.7.1.dist-info/RECORD +27 -0
- {maps4fs-1.5.7.dist-info → maps4fs-1.7.1.dist-info}/WHEEL +1 -1
- maps4fs-1.5.7.dist-info/RECORD +0 -24
- {maps4fs-1.5.7.dist-info → maps4fs-1.7.1.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.5.7.dist-info → maps4fs-1.7.1.dist-info}/top_level.txt +0 -0
maps4fs/__init__.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# pylint: disable=missing-module-docstring
|
2
|
-
from maps4fs.generator.dtm import DTMProvider
|
2
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
3
|
+
from maps4fs.generator.dtm.srtm import SRTM30Provider
|
4
|
+
from maps4fs.generator.dtm.usgs import USGSProvider
|
3
5
|
from maps4fs.generator.game import Game
|
4
6
|
from maps4fs.generator.map import Map
|
5
7
|
from maps4fs.generator.settings import (
|
maps4fs/generator/background.py
CHANGED
@@ -58,6 +58,10 @@ class Background(Component):
|
|
58
58
|
os.makedirs(self.water_directory, exist_ok=True)
|
59
59
|
|
60
60
|
self.output_path = os.path.join(self.background_directory, f"{FULL_NAME}.png")
|
61
|
+
if self.map.custom_background_path:
|
62
|
+
self.check_custom_background(self.map.custom_background_path)
|
63
|
+
shutil.copyfile(self.map.custom_background_path, self.output_path)
|
64
|
+
|
61
65
|
self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
|
62
66
|
self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
|
63
67
|
|
@@ -75,6 +79,28 @@ class Background(Component):
|
|
75
79
|
self.dem.set_output_resolution((self.rotated_size, self.rotated_size))
|
76
80
|
self.dem.set_dem_path(self.output_path)
|
77
81
|
|
82
|
+
def check_custom_background(self, image_path: str) -> None:
|
83
|
+
"""Checks if the custom background image meets the requirements.
|
84
|
+
|
85
|
+
Arguments:
|
86
|
+
image_path (str): The path to the custom background image.
|
87
|
+
|
88
|
+
Raises:
|
89
|
+
ValueError: If the custom background image does not meet the requirements.
|
90
|
+
"""
|
91
|
+
image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
92
|
+
if image.shape[0] != image.shape[1]:
|
93
|
+
raise ValueError("The custom background image must be a square.")
|
94
|
+
|
95
|
+
if image.shape[0] != self.map_size + DEFAULT_DISTANCE * 2:
|
96
|
+
raise ValueError("The custom background image must have the size of the map + 4096.")
|
97
|
+
|
98
|
+
if len(image.shape) != 2:
|
99
|
+
raise ValueError("The custom background image must be a grayscale image.")
|
100
|
+
|
101
|
+
if image.dtype != np.uint16:
|
102
|
+
raise ValueError("The custom background image must be a 16-bit grayscale image.")
|
103
|
+
|
78
104
|
def is_preview(self, name: str) -> bool:
|
79
105
|
"""Checks if the DEM is a preview.
|
80
106
|
|
@@ -91,7 +117,9 @@ class Background(Component):
|
|
91
117
|
as a result the DEM files will be saved, then based on them the obj files will be
|
92
118
|
generated."""
|
93
119
|
self.create_background_textures()
|
94
|
-
|
120
|
+
|
121
|
+
if not self.map.custom_background_path:
|
122
|
+
self.dem.process()
|
95
123
|
|
96
124
|
shutil.copyfile(self.dem.dem_path, self.not_substracted_path)
|
97
125
|
self.cutout(self.dem.dem_path, save_path=self.not_resized_path)
|
@@ -148,6 +176,9 @@ class Background(Component):
|
|
148
176
|
"east": east,
|
149
177
|
"west": west,
|
150
178
|
}
|
179
|
+
|
180
|
+
dem_info_sequence = self.dem.info_sequence()
|
181
|
+
data["DEM"] = dem_info_sequence
|
151
182
|
return data # type: ignore
|
152
183
|
|
153
184
|
def qgis_sequence(self) -> None:
|
@@ -176,7 +207,13 @@ class Background(Component):
|
|
176
207
|
self.logger.debug("Generating obj file in path: %s", save_path)
|
177
208
|
|
178
209
|
dem_data = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
179
|
-
self.plane_from_np(
|
210
|
+
self.plane_from_np(
|
211
|
+
dem_data,
|
212
|
+
save_path,
|
213
|
+
create_preview=True,
|
214
|
+
remove_center=self.map.background_settings.remove_center,
|
215
|
+
include_zeros=False,
|
216
|
+
) # type: ignore
|
180
217
|
|
181
218
|
# pylint: disable=too-many-locals
|
182
219
|
def cutout(self, dem_path: str, save_path: str | None = None) -> str:
|
@@ -219,16 +256,37 @@ class Background(Component):
|
|
219
256
|
)
|
220
257
|
|
221
258
|
cv2.imwrite(main_dem_path, resized_dem_data) # pylint: disable=no-member
|
222
|
-
self.logger.
|
259
|
+
self.logger.debug("DEM cutout saved: %s", main_dem_path)
|
223
260
|
|
224
261
|
return main_dem_path
|
225
262
|
|
226
|
-
|
263
|
+
def remove_center(self, dem_data: np.ndarray, resize_factor: float) -> np.ndarray:
|
264
|
+
"""Removes the center part of the DEM data.
|
265
|
+
|
266
|
+
Arguments:
|
267
|
+
dem_data (np.ndarray) -- The DEM data as a numpy array.
|
268
|
+
resize_factor (float) -- The resize factor of the DEM data.
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
np.ndarray -- The DEM data with the center part removed.
|
272
|
+
"""
|
273
|
+
center = (dem_data.shape[0] // 2, dem_data.shape[1] // 2)
|
274
|
+
half_size = int(self.map_size // 2 * resize_factor)
|
275
|
+
x1 = center[0] - half_size
|
276
|
+
x2 = center[0] + half_size
|
277
|
+
y1 = center[1] - half_size
|
278
|
+
y2 = center[1] + half_size
|
279
|
+
dem_data[x1:x2, y1:y2] = 0
|
280
|
+
return dem_data
|
281
|
+
|
282
|
+
# pylint: disable=R0913, R0917, R0915
|
227
283
|
def plane_from_np(
|
228
284
|
self,
|
229
285
|
dem_data: np.ndarray,
|
230
286
|
save_path: str,
|
231
287
|
include_zeros: bool = True,
|
288
|
+
create_preview: bool = False,
|
289
|
+
remove_center: bool = False,
|
232
290
|
) -> None:
|
233
291
|
"""Generates a 3D obj file based on DEM data.
|
234
292
|
|
@@ -236,11 +294,18 @@ class Background(Component):
|
|
236
294
|
dem_data (np.ndarray) -- The DEM data as a numpy array.
|
237
295
|
save_path (str) -- The path where the obj file will be saved.
|
238
296
|
include_zeros (bool, optional) -- If True, the mesh will include the zero height values.
|
297
|
+
create_preview (bool, optional) -- If True, a simplified mesh will be saved as an STL.
|
298
|
+
remove_center (bool, optional) -- If True, the center of the mesh will be removed.
|
299
|
+
This setting is used for a Background Terrain, where the center part where the
|
300
|
+
playable area is will be cut out.
|
239
301
|
"""
|
240
302
|
resize_factor = 1 / self.map.background_settings.resize_factor
|
241
303
|
dem_data = cv2.resize( # pylint: disable=no-member
|
242
304
|
dem_data, (0, 0), fx=resize_factor, fy=resize_factor
|
243
305
|
)
|
306
|
+
if remove_center:
|
307
|
+
dem_data = self.remove_center(dem_data, resize_factor)
|
308
|
+
self.logger.debug("Center removed from DEM data.")
|
244
309
|
self.logger.debug(
|
245
310
|
"DEM data resized to shape: %s with factor: %s", dem_data.shape, resize_factor
|
246
311
|
)
|
@@ -275,7 +340,10 @@ class Background(Component):
|
|
275
340
|
bottom_left = top_left + cols
|
276
341
|
bottom_right = bottom_left + 1
|
277
342
|
|
278
|
-
if
|
343
|
+
if (
|
344
|
+
ground in [z[i, j], z[i, j + 1], z[i + 1, j], z[i + 1, j + 1]]
|
345
|
+
and not include_zeros
|
346
|
+
):
|
279
347
|
skipped += 1
|
280
348
|
continue
|
281
349
|
|
@@ -294,16 +362,32 @@ class Background(Component):
|
|
294
362
|
mesh.apply_transform(rotation_matrix_z)
|
295
363
|
|
296
364
|
# if not include_zeros:
|
297
|
-
z_scaling_factor =
|
365
|
+
z_scaling_factor = self.get_z_scaling_factor()
|
298
366
|
self.logger.debug("Z scaling factor: %s", z_scaling_factor)
|
299
367
|
mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
|
300
368
|
|
369
|
+
old_faces = len(mesh.faces)
|
370
|
+
self.logger.debug("Mesh generated with %s faces.", old_faces)
|
371
|
+
|
372
|
+
if self.map.background_settings.apply_decimation:
|
373
|
+
percent = self.map.background_settings.decimation_percent / 100
|
374
|
+
mesh = mesh.simplify_quadric_decimation(
|
375
|
+
percent=percent, aggression=self.map.background_settings.decimation_agression
|
376
|
+
)
|
377
|
+
|
378
|
+
new_faces = len(mesh.faces)
|
379
|
+
decimation_percent = (old_faces - new_faces) / old_faces * 100
|
380
|
+
|
381
|
+
self.logger.debug(
|
382
|
+
"Mesh simplified to %s faces. Decimation percent: %s", new_faces, decimation_percent
|
383
|
+
)
|
384
|
+
|
301
385
|
mesh.export(save_path)
|
302
386
|
self.logger.debug("Obj file saved: %s", save_path)
|
303
387
|
|
304
|
-
if
|
388
|
+
if create_preview:
|
305
389
|
# Simplify the preview mesh to reduce the size of the file.
|
306
|
-
mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
|
390
|
+
# mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
|
307
391
|
|
308
392
|
# Apply scale to make the preview mesh smaller in the UI.
|
309
393
|
mesh.apply_scale([0.5, 0.5, 0.5])
|
maps4fs/generator/component.py
CHANGED
@@ -58,7 +58,7 @@ class Component:
|
|
58
58
|
self.logger = logger
|
59
59
|
self.kwargs = kwargs
|
60
60
|
|
61
|
-
self.logger.
|
61
|
+
self.logger.debug(
|
62
62
|
"Component %s initialized. Map size: %s, map rotated size: %s", # type: ignore
|
63
63
|
self.__class__.__name__,
|
64
64
|
self.map_size,
|
@@ -188,8 +188,7 @@ class Component:
|
|
188
188
|
self,
|
189
189
|
coordinates: tuple[float, float] | None = None,
|
190
190
|
distance: int | None = None,
|
191
|
-
|
192
|
-
) -> tuple[int, int, int, int]:
|
191
|
+
) -> tuple[float, float, float, float]:
|
193
192
|
"""Calculates the bounding box of the map from the coordinates and the height and
|
194
193
|
width of the map.
|
195
194
|
If coordinates and distance are not provided, the instance variables are used.
|
@@ -199,24 +198,23 @@ class Component:
|
|
199
198
|
of the map. Defaults to None.
|
200
199
|
distance (int, optional): The distance from the center of the map to the edge of the
|
201
200
|
map in all directions. Defaults to None.
|
202
|
-
project_utm (bool, optional): Whether to project the bounding box to UTM.
|
203
201
|
|
204
202
|
Returns:
|
205
|
-
tuple[
|
203
|
+
tuple[float, float, float, float]: The bounding box of the map.
|
206
204
|
"""
|
207
205
|
coordinates = coordinates or self.coordinates
|
208
206
|
distance = distance or int(self.map_rotated_size / 2)
|
209
207
|
|
210
208
|
west, south, east, north = ox.utils_geo.bbox_from_point( # type: ignore
|
211
|
-
coordinates,
|
209
|
+
coordinates,
|
210
|
+
dist=distance,
|
212
211
|
)
|
213
212
|
|
214
213
|
bbox = north, south, east, west
|
215
214
|
self.logger.debug(
|
216
|
-
"Calculated bounding box for component: %s: %s,
|
215
|
+
"Calculated bounding box for component: %s: %s, distance: %s",
|
217
216
|
self.__class__.__name__,
|
218
217
|
bbox,
|
219
|
-
project_utm,
|
220
218
|
distance,
|
221
219
|
)
|
222
220
|
return bbox
|
@@ -225,7 +223,7 @@ class Component:
|
|
225
223
|
"""Saves the bounding box of the map to the component instance from the coordinates and the
|
226
224
|
height and width of the map.
|
227
225
|
"""
|
228
|
-
self.bbox = self.get_bbox(
|
226
|
+
self.bbox = self.get_bbox()
|
229
227
|
self.logger.debug("Saved bounding box: %s", self.bbox)
|
230
228
|
|
231
229
|
@property
|
@@ -535,3 +533,19 @@ class Component:
|
|
535
533
|
interpolated_polyline.append(polyline[-1])
|
536
534
|
|
537
535
|
return interpolated_polyline
|
536
|
+
|
537
|
+
def get_z_scaling_factor(self) -> float:
|
538
|
+
"""Calculates the scaling factor for the Z axis based on the map settings.
|
539
|
+
|
540
|
+
Returns:
|
541
|
+
float -- The scaling factor for the Z axis.
|
542
|
+
"""
|
543
|
+
|
544
|
+
scaling_factor = 1 / self.map.dem_settings.multiplier
|
545
|
+
|
546
|
+
if self.map.shared_settings.height_scale_multiplier:
|
547
|
+
scaling_factor *= self.map.shared_settings.height_scale_multiplier
|
548
|
+
if self.map.shared_settings.mesh_z_scaling_factor:
|
549
|
+
scaling_factor *= 1 / self.map.shared_settings.mesh_z_scaling_factor
|
550
|
+
|
551
|
+
return scaling_factor
|
maps4fs/generator/dem.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""This module contains DEM class for processing Digital Elevation Model data."""
|
2
2
|
|
3
3
|
import os
|
4
|
+
from typing import Any
|
4
5
|
|
5
6
|
import cv2
|
6
7
|
import numpy as np
|
@@ -9,7 +10,7 @@ import numpy as np
|
|
9
10
|
from pympler import asizeof # type: ignore
|
10
11
|
|
11
12
|
from maps4fs.generator.component import Component
|
12
|
-
from maps4fs.generator.dtm import DTMProvider
|
13
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
13
14
|
|
14
15
|
|
15
16
|
# pylint: disable=R0903, R0902
|
@@ -63,6 +64,7 @@ class DEM(Component):
|
|
63
64
|
size=self.map_rotated_size,
|
64
65
|
directory=self.temp_dir,
|
65
66
|
logger=self.logger,
|
67
|
+
map=self.map,
|
66
68
|
)
|
67
69
|
|
68
70
|
@property
|
@@ -281,54 +283,15 @@ class DEM(Component):
|
|
281
283
|
"""
|
282
284
|
return []
|
283
285
|
|
284
|
-
def
|
285
|
-
"""
|
286
|
-
|
286
|
+
def info_sequence(self) -> dict[Any, Any] | None: # type: ignore
|
287
|
+
"""Returns the information sequence for the component. Must be implemented in the child
|
288
|
+
class. If the component does not have an information sequence, an empty dictionary must be
|
289
|
+
returned.
|
287
290
|
|
288
|
-
Arguments:
|
289
|
-
maximum_deviation (int): Maximum deviation in DEM data.
|
290
|
-
|
291
|
-
Returns:
|
292
|
-
float: Scaling factor for DEM data normalization.
|
293
|
-
"""
|
294
|
-
ESTIMATED_MAXIMUM_DEVIATION = 1000 # pylint: disable=C0103
|
295
|
-
scaling_factor = maximum_deviation / ESTIMATED_MAXIMUM_DEVIATION
|
296
|
-
return scaling_factor if scaling_factor < 1 else 1
|
297
|
-
|
298
|
-
def _normalize_dem(self, data: np.ndarray) -> np.ndarray:
|
299
|
-
"""Normalize DEM data to 16-bit unsigned integer using max height from settings.
|
300
|
-
Arguments:
|
301
|
-
data (np.ndarray): DEM data from SRTM file after cropping.
|
302
291
|
Returns:
|
303
|
-
|
292
|
+
dict[Any, Any]: The information sequence for the component.
|
304
293
|
"""
|
305
|
-
self.
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
min_height = data.min()
|
310
|
-
max_dev = max_height - min_height
|
311
|
-
self.logger.debug(
|
312
|
-
"Maximum deviation: %s with maximum at %s and minimum at %s.",
|
313
|
-
max_dev,
|
314
|
-
max_height,
|
315
|
-
min_height,
|
316
|
-
)
|
317
|
-
|
318
|
-
scaling_factor = self._get_scaling_factor(max_dev)
|
319
|
-
adjusted_max_height = int(65535 * scaling_factor)
|
320
|
-
self.logger.debug(
|
321
|
-
"Maximum deviation: %s. Scaling factor: %s. Adjusted max height: %s.",
|
322
|
-
max_dev,
|
323
|
-
scaling_factor,
|
324
|
-
adjusted_max_height,
|
325
|
-
)
|
326
|
-
normalized_data = (
|
327
|
-
(data - data.min()) / (data.max() - data.min()) * adjusted_max_height
|
328
|
-
).astype("uint16")
|
329
|
-
self.logger.debug(
|
330
|
-
"DEM data was normalized to %s - %s.",
|
331
|
-
normalized_data.min(),
|
332
|
-
normalized_data.max(),
|
333
|
-
)
|
334
|
-
return normalized_data
|
294
|
+
provider_info_sequence = self.dtm_provider.info_sequence()
|
295
|
+
if provider_info_sequence is None:
|
296
|
+
return {}
|
297
|
+
return provider_info_sequence
|
File without changes
|
@@ -4,11 +4,9 @@ and specific settings for downloading and processing the data."""
|
|
4
4
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
|
-
import
|
8
|
-
import math
|
7
|
+
from abc import ABC, abstractmethod
|
9
8
|
import os
|
10
|
-
import
|
11
|
-
from typing import Type
|
9
|
+
from typing import TYPE_CHECKING, Type
|
12
10
|
|
13
11
|
import numpy as np
|
14
12
|
import osmnx as ox # type: ignore
|
@@ -18,24 +16,30 @@ from pydantic import BaseModel
|
|
18
16
|
|
19
17
|
from maps4fs.logger import Logger
|
20
18
|
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
from maps4fs.generator.map import Map
|
21
|
+
|
21
22
|
|
22
23
|
class DTMProviderSettings(BaseModel):
|
23
24
|
"""Base class for DTM provider settings models."""
|
24
25
|
|
25
26
|
|
26
|
-
|
27
|
+
# pylint: disable=too-many-public-methods
|
28
|
+
class DTMProvider(ABC):
|
27
29
|
"""Base class for DTM providers."""
|
28
30
|
|
29
31
|
_code: str | None = None
|
30
32
|
_name: str | None = None
|
31
33
|
_region: str | None = None
|
32
34
|
_icon: str | None = None
|
33
|
-
_resolution: float | None = None
|
35
|
+
_resolution: float | str | None = None
|
34
36
|
|
35
37
|
_url: str | None = None
|
36
38
|
|
37
39
|
_author: str | None = None
|
40
|
+
_contributors: str | None = None
|
38
41
|
_is_community: bool = False
|
42
|
+
_is_base: bool = False
|
39
43
|
_settings: Type[DTMProviderSettings] | None = None
|
40
44
|
|
41
45
|
_instructions: str | None = None
|
@@ -48,6 +52,7 @@ class DTMProvider:
|
|
48
52
|
size: int,
|
49
53
|
directory: str,
|
50
54
|
logger: Logger,
|
55
|
+
map: Map | None = None, # pylint: disable=W0622
|
51
56
|
):
|
52
57
|
self._coordinates = coordinates
|
53
58
|
self._user_settings = user_settings
|
@@ -59,6 +64,27 @@ class DTMProvider:
|
|
59
64
|
os.makedirs(self._tile_directory, exist_ok=True)
|
60
65
|
|
61
66
|
self.logger = logger
|
67
|
+
self.map = map
|
68
|
+
|
69
|
+
self._data_info: dict[str, int | str | float] | None = None
|
70
|
+
|
71
|
+
@property
|
72
|
+
def data_info(self) -> dict[str, int | str | float] | None:
|
73
|
+
"""Information about the DTM data.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
dict: Information about the DTM data.
|
77
|
+
"""
|
78
|
+
return self._data_info
|
79
|
+
|
80
|
+
@data_info.setter
|
81
|
+
def data_info(self, value: dict[str, int | str | float] | None) -> None:
|
82
|
+
"""Set information about the DTM data.
|
83
|
+
|
84
|
+
Arguments:
|
85
|
+
value (dict): Information about the DTM data.
|
86
|
+
"""
|
87
|
+
self._data_info = value
|
62
88
|
|
63
89
|
@property
|
64
90
|
def coordinates(self) -> tuple[float, float]:
|
@@ -98,6 +124,24 @@ class DTMProvider:
|
|
98
124
|
"""
|
99
125
|
return cls._author
|
100
126
|
|
127
|
+
@classmethod
|
128
|
+
def contributors(cls) -> str | None:
|
129
|
+
"""Contributors of the provider.
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
str: Contributors of the provider.
|
133
|
+
"""
|
134
|
+
return cls._contributors
|
135
|
+
|
136
|
+
@classmethod
|
137
|
+
def is_base(cls) -> bool:
|
138
|
+
"""Is the provider a base provider.
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
bool: True if the provider is a base provider, False otherwise.
|
142
|
+
"""
|
143
|
+
return cls._is_base
|
144
|
+
|
101
145
|
@classmethod
|
102
146
|
def is_community(cls) -> bool:
|
103
147
|
"""Is the provider a community-driven project.
|
@@ -168,7 +212,8 @@ class DTMProvider:
|
|
168
212
|
"""
|
169
213
|
providers = {}
|
170
214
|
for provider in cls.__subclasses__():
|
171
|
-
|
215
|
+
if not provider.is_base():
|
216
|
+
providers[provider._code] = provider.description() # pylint: disable=W0212
|
172
217
|
return providers # type: ignore
|
173
218
|
|
174
219
|
def download_tile(self, output_path: str, **kwargs) -> bool:
|
@@ -213,6 +258,7 @@ class DTMProvider:
|
|
213
258
|
"""
|
214
259
|
raise NotImplementedError
|
215
260
|
|
261
|
+
@abstractmethod
|
216
262
|
def get_numpy(self) -> np.ndarray:
|
217
263
|
"""Get numpy array of the tile.
|
218
264
|
Resulting array must be 16 bit (signed or unsigned) integer and it should be already
|
@@ -264,70 +310,12 @@ class DTMProvider:
|
|
264
310
|
|
265
311
|
return data
|
266
312
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
_code = "srtm30"
|
272
|
-
_name = "SRTM 30 m"
|
273
|
-
_region = "Global"
|
274
|
-
_icon = "🌎"
|
275
|
-
_resolution = 30.0
|
276
|
-
|
277
|
-
_url = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
|
278
|
-
|
279
|
-
_author = "[iwatkot](https://github.com/iwatkot)"
|
280
|
-
|
281
|
-
def __init__(self, *args, **kwargs):
|
282
|
-
super().__init__(*args, **kwargs)
|
283
|
-
self.hgt_directory = os.path.join(self._tile_directory, "hgt")
|
284
|
-
self.gz_directory = os.path.join(self._tile_directory, "gz")
|
285
|
-
os.makedirs(self.hgt_directory, exist_ok=True)
|
286
|
-
os.makedirs(self.gz_directory, exist_ok=True)
|
287
|
-
|
288
|
-
def get_tile_parameters(self, *args, **kwargs) -> dict[str, str]:
|
289
|
-
"""Returns latitude band and tile name for SRTM tile from coordinates.
|
290
|
-
|
291
|
-
Arguments:
|
292
|
-
lat (float): Latitude.
|
293
|
-
lon (float): Longitude.
|
313
|
+
def info_sequence(self) -> dict[str, int | str | float] | None:
|
314
|
+
"""Returns the information sequence for the component. Must be implemented in the child
|
315
|
+
class. If the component does not have an information sequence, an empty dictionary must be
|
316
|
+
returned.
|
294
317
|
|
295
318
|
Returns:
|
296
|
-
dict:
|
319
|
+
dict[str, int | str | float] | None: Information sequence for the component.
|
297
320
|
"""
|
298
|
-
|
299
|
-
|
300
|
-
tile_latitude = math.floor(lat)
|
301
|
-
tile_longitude = math.floor(lon)
|
302
|
-
|
303
|
-
latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
|
304
|
-
if lon < 0:
|
305
|
-
tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
|
306
|
-
else:
|
307
|
-
tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
|
308
|
-
|
309
|
-
self.logger.debug(
|
310
|
-
"Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
|
311
|
-
)
|
312
|
-
return {"latitude_band": latitude_band, "tile_name": tile_name}
|
313
|
-
|
314
|
-
def get_numpy(self) -> np.ndarray:
|
315
|
-
"""Get numpy array of the tile.
|
316
|
-
|
317
|
-
Returns:
|
318
|
-
np.ndarray: Numpy array of the tile.
|
319
|
-
"""
|
320
|
-
tile_parameters = self.get_tile_parameters(*self.coordinates)
|
321
|
-
tile_name = tile_parameters["tile_name"]
|
322
|
-
decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
|
323
|
-
|
324
|
-
if not os.path.isfile(decompressed_tile_path):
|
325
|
-
compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
|
326
|
-
if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
|
327
|
-
raise FileNotFoundError(f"Tile {tile_name} not found.")
|
328
|
-
|
329
|
-
with gzip.open(compressed_tile_path, "rb") as f_in:
|
330
|
-
with open(decompressed_tile_path, "wb") as f_out:
|
331
|
-
shutil.copyfileobj(f_in, f_out)
|
332
|
-
|
333
|
-
return self.extract_roi(decompressed_tile_path)
|
321
|
+
return self.data_info
|