maps4fs 1.5.0__py3-none-any.whl → 1.6.91__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 +5 -2
- maps4fs/generator/background.py +161 -113
- maps4fs/generator/component.py +34 -1
- maps4fs/generator/dem.py +33 -154
- maps4fs/generator/dtm/__init__.py +0 -0
- maps4fs/generator/dtm/dtm.py +321 -0
- maps4fs/generator/dtm/srtm.py +226 -0
- maps4fs/generator/dtm/usgs.py +351 -0
- maps4fs/generator/game.py +2 -1
- maps4fs/generator/grle.py +94 -28
- maps4fs/generator/i3d.py +17 -21
- maps4fs/generator/map.py +38 -135
- maps4fs/generator/satellite.py +92 -0
- maps4fs/generator/settings.py +187 -0
- maps4fs/generator/texture.py +65 -12
- {maps4fs-1.5.0.dist-info → maps4fs-1.6.91.dist-info}/METADATA +55 -11
- maps4fs-1.6.91.dist-info/RECORD +27 -0
- {maps4fs-1.5.0.dist-info → maps4fs-1.6.91.dist-info}/WHEEL +1 -1
- maps4fs-1.5.0.dist-info/RECORD +0 -21
- {maps4fs-1.5.0.dist-info → maps4fs-1.6.91.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.5.0.dist-info → maps4fs-1.6.91.dist-info}/top_level.txt +0 -0
maps4fs/generator/dem.py
CHANGED
@@ -1,19 +1,16 @@
|
|
1
1
|
"""This module contains DEM class for processing Digital Elevation Model data."""
|
2
2
|
|
3
|
-
import gzip
|
4
|
-
import math
|
5
3
|
import os
|
6
|
-
import
|
4
|
+
from typing import Any
|
7
5
|
|
8
6
|
import cv2
|
9
7
|
import numpy as np
|
10
|
-
|
11
|
-
import
|
8
|
+
|
9
|
+
# import rasterio # type: ignore
|
12
10
|
from pympler import asizeof # type: ignore
|
13
11
|
|
14
12
|
from maps4fs.generator.component import Component
|
15
|
-
|
16
|
-
SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
|
13
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
17
14
|
|
18
15
|
|
19
16
|
# pylint: disable=R0903, R0902
|
@@ -61,7 +58,14 @@ class DEM(Component):
|
|
61
58
|
self.blur_radius,
|
62
59
|
)
|
63
60
|
|
64
|
-
self.
|
61
|
+
self.dtm_provider: DTMProvider = self.map.dtm_provider( # type: ignore
|
62
|
+
coordinates=self.coordinates,
|
63
|
+
user_settings=self.map.dtm_provider_settings,
|
64
|
+
size=self.map_rotated_size,
|
65
|
+
directory=self.temp_dir,
|
66
|
+
logger=self.logger,
|
67
|
+
map=self.map,
|
68
|
+
)
|
65
69
|
|
66
70
|
@property
|
67
71
|
def dem_path(self) -> str:
|
@@ -132,36 +136,29 @@ class DEM(Component):
|
|
132
136
|
def process(self) -> None:
|
133
137
|
"""Reads SRTM file, crops it to map size, normalizes and blurs it,
|
134
138
|
saves to map directory."""
|
135
|
-
north, south, east, west = self.bbox
|
136
139
|
|
137
140
|
dem_output_resolution = self.output_resolution
|
138
141
|
self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
|
139
142
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
+
try:
|
144
|
+
data = self.dtm_provider.get_numpy()
|
145
|
+
except Exception as e: # pylint: disable=W0718
|
146
|
+
self.logger.error("Failed to get DEM data from SRTM: %s.", e)
|
143
147
|
self._save_empty_dem(dem_output_resolution)
|
144
148
|
return
|
145
149
|
|
146
|
-
|
147
|
-
self.logger.
|
148
|
-
|
149
|
-
|
150
|
-
"Window parameters. Column offset: %s, row offset: %s, width: %s, height: %s.",
|
151
|
-
window.col_off,
|
152
|
-
window.row_off,
|
153
|
-
window.width,
|
154
|
-
window.height,
|
155
|
-
)
|
156
|
-
data = src.read(1, window=window)
|
150
|
+
if len(data.shape) != 2:
|
151
|
+
self.logger.error("DTM provider returned incorrect data: more than 1 channel.")
|
152
|
+
self._save_empty_dem(dem_output_resolution)
|
153
|
+
return
|
157
154
|
|
158
|
-
if
|
159
|
-
self.logger.
|
155
|
+
if data.dtype not in ["int16", "uint16"]:
|
156
|
+
self.logger.error("DTM provider returned incorrect data type: %s.", data.dtype)
|
160
157
|
self._save_empty_dem(dem_output_resolution)
|
161
158
|
return
|
162
159
|
|
163
160
|
self.logger.debug(
|
164
|
-
"DEM data was
|
161
|
+
"DEM data was retrieved from DTM provider. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
165
162
|
data.shape,
|
166
163
|
data.dtype,
|
167
164
|
data.min(),
|
@@ -184,11 +181,7 @@ class DEM(Component):
|
|
184
181
|
resampled_data.dtype,
|
185
182
|
)
|
186
183
|
|
187
|
-
if self.
|
188
|
-
self.logger.debug("Auto processing is enabled, will normalize DEM data.")
|
189
|
-
resampled_data = self._normalize_dem(resampled_data)
|
190
|
-
else:
|
191
|
-
self.logger.debug("Auto processing is disabled, DEM data will not be normalized.")
|
184
|
+
if self.multiplier != 1:
|
192
185
|
resampled_data = resampled_data * self.multiplier
|
193
186
|
|
194
187
|
self.logger.debug(
|
@@ -276,81 +269,6 @@ class DEM(Component):
|
|
276
269
|
output_width=output_width,
|
277
270
|
)
|
278
271
|
|
279
|
-
def _tile_info(self, lat: float, lon: float) -> tuple[str, str]:
|
280
|
-
"""Returns latitude band and tile name for SRTM tile from coordinates.
|
281
|
-
|
282
|
-
Arguments:
|
283
|
-
lat (float): Latitude.
|
284
|
-
lon (float): Longitude.
|
285
|
-
|
286
|
-
Returns:
|
287
|
-
tuple[str, str]: Latitude band and tile name.
|
288
|
-
"""
|
289
|
-
tile_latitude = math.floor(lat)
|
290
|
-
tile_longitude = math.floor(lon)
|
291
|
-
|
292
|
-
latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
|
293
|
-
if lon < 0:
|
294
|
-
tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
|
295
|
-
else:
|
296
|
-
tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
|
297
|
-
|
298
|
-
self.logger.debug(
|
299
|
-
"Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
|
300
|
-
)
|
301
|
-
return latitude_band, tile_name
|
302
|
-
|
303
|
-
def _download_tile(self) -> str | None:
|
304
|
-
"""Downloads SRTM tile from Amazon S3 using coordinates.
|
305
|
-
|
306
|
-
Returns:
|
307
|
-
str: Path to compressed tile or None if download failed.
|
308
|
-
"""
|
309
|
-
latitude_band, tile_name = self._tile_info(*self.coordinates)
|
310
|
-
compressed_file_path = os.path.join(self.gz_dir, f"{tile_name}.hgt.gz")
|
311
|
-
url = SRTM.format(latitude_band=latitude_band, tile_name=tile_name)
|
312
|
-
self.logger.debug("Trying to get response from %s...", url)
|
313
|
-
response = requests.get(url, stream=True, timeout=10)
|
314
|
-
|
315
|
-
if response.status_code == 200:
|
316
|
-
self.logger.debug("Response received. Saving to %s...", compressed_file_path)
|
317
|
-
with open(compressed_file_path, "wb") as f:
|
318
|
-
for chunk in response.iter_content(chunk_size=8192):
|
319
|
-
f.write(chunk)
|
320
|
-
self.logger.debug("Compressed tile successfully downloaded.")
|
321
|
-
else:
|
322
|
-
self.logger.error("Response was failed with status code %s.", response.status_code)
|
323
|
-
return None
|
324
|
-
|
325
|
-
return compressed_file_path
|
326
|
-
|
327
|
-
def _srtm_tile(self) -> str | None:
|
328
|
-
"""Determines SRTM tile name from coordinates downloads it if necessary, and decompresses.
|
329
|
-
|
330
|
-
Returns:
|
331
|
-
str: Path to decompressed tile or None if download failed.
|
332
|
-
"""
|
333
|
-
latitude_band, tile_name = self._tile_info(*self.coordinates)
|
334
|
-
self.logger.debug("SRTM tile name %s from latitude band %s.", tile_name, latitude_band)
|
335
|
-
|
336
|
-
decompressed_file_path = os.path.join(self.hgt_dir, f"{tile_name}.hgt")
|
337
|
-
if os.path.isfile(decompressed_file_path):
|
338
|
-
self.logger.debug(
|
339
|
-
"Decompressed tile already exists: %s, skipping download.",
|
340
|
-
decompressed_file_path,
|
341
|
-
)
|
342
|
-
return decompressed_file_path
|
343
|
-
|
344
|
-
compressed_file_path = self._download_tile()
|
345
|
-
if not compressed_file_path:
|
346
|
-
self.logger.error("Download from SRTM failed, DEM file will be filled with zeros.")
|
347
|
-
return None
|
348
|
-
with gzip.open(compressed_file_path, "rb") as f_in:
|
349
|
-
with open(decompressed_file_path, "wb") as f_out:
|
350
|
-
shutil.copyfileobj(f_in, f_out)
|
351
|
-
self.logger.debug("Tile decompressed to %s.", decompressed_file_path)
|
352
|
-
return decompressed_file_path
|
353
|
-
|
354
272
|
def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
|
355
273
|
"""Saves empty DEM file filled with zeros."""
|
356
274
|
dem_data = np.zeros(dem_output_resolution, dtype="uint16")
|
@@ -365,54 +283,15 @@ class DEM(Component):
|
|
365
283
|
"""
|
366
284
|
return []
|
367
285
|
|
368
|
-
def
|
369
|
-
"""
|
370
|
-
|
371
|
-
|
372
|
-
Arguments:
|
373
|
-
maximum_deviation (int): Maximum deviation in DEM data.
|
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.
|
374
290
|
|
375
291
|
Returns:
|
376
|
-
|
292
|
+
dict[Any, Any]: The information sequence for the component.
|
377
293
|
"""
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
def _normalize_dem(self, data: np.ndarray) -> np.ndarray:
|
383
|
-
"""Normalize DEM data to 16-bit unsigned integer using max height from settings.
|
384
|
-
Arguments:
|
385
|
-
data (np.ndarray): DEM data from SRTM file after cropping.
|
386
|
-
Returns:
|
387
|
-
np.ndarray: Normalized DEM data.
|
388
|
-
"""
|
389
|
-
self.logger.debug("Starting DEM data normalization.")
|
390
|
-
# Calculate the difference between the maximum and minimum values in the DEM data.
|
391
|
-
|
392
|
-
max_height = data.max()
|
393
|
-
min_height = data.min()
|
394
|
-
max_dev = max_height - min_height
|
395
|
-
self.logger.debug(
|
396
|
-
"Maximum deviation: %s with maximum at %s and minimum at %s.",
|
397
|
-
max_dev,
|
398
|
-
max_height,
|
399
|
-
min_height,
|
400
|
-
)
|
401
|
-
|
402
|
-
scaling_factor = self._get_scaling_factor(max_dev)
|
403
|
-
adjusted_max_height = int(65535 * scaling_factor)
|
404
|
-
self.logger.debug(
|
405
|
-
"Maximum deviation: %s. Scaling factor: %s. Adjusted max height: %s.",
|
406
|
-
max_dev,
|
407
|
-
scaling_factor,
|
408
|
-
adjusted_max_height,
|
409
|
-
)
|
410
|
-
normalized_data = (
|
411
|
-
(data - data.min()) / (data.max() - data.min()) * adjusted_max_height
|
412
|
-
).astype("uint16")
|
413
|
-
self.logger.debug(
|
414
|
-
"DEM data was normalized to %s - %s.",
|
415
|
-
normalized_data.min(),
|
416
|
-
normalized_data.max(),
|
417
|
-
)
|
418
|
-
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
|
@@ -0,0 +1,321 @@
|
|
1
|
+
"""This module contains the DTMProvider class and its subclasses. DTMProvider class is used to
|
2
|
+
define different providers of digital terrain models (DTM) data. Each provider has its own URL
|
3
|
+
and specific settings for downloading and processing the data."""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
import os
|
9
|
+
from typing import TYPE_CHECKING, Type
|
10
|
+
|
11
|
+
import numpy as np
|
12
|
+
import osmnx as ox # type: ignore
|
13
|
+
import rasterio # type: ignore
|
14
|
+
import requests
|
15
|
+
from pydantic import BaseModel
|
16
|
+
|
17
|
+
from maps4fs.logger import Logger
|
18
|
+
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
from maps4fs.generator.map import Map
|
21
|
+
|
22
|
+
|
23
|
+
class DTMProviderSettings(BaseModel):
|
24
|
+
"""Base class for DTM provider settings models."""
|
25
|
+
|
26
|
+
|
27
|
+
# pylint: disable=too-many-public-methods
|
28
|
+
class DTMProvider(ABC):
|
29
|
+
"""Base class for DTM providers."""
|
30
|
+
|
31
|
+
_code: str | None = None
|
32
|
+
_name: str | None = None
|
33
|
+
_region: str | None = None
|
34
|
+
_icon: str | None = None
|
35
|
+
_resolution: float | str | None = None
|
36
|
+
|
37
|
+
_url: str | None = None
|
38
|
+
|
39
|
+
_author: str | None = None
|
40
|
+
_contributors: str | None = None
|
41
|
+
_is_community: bool = False
|
42
|
+
_is_base: bool = False
|
43
|
+
_settings: Type[DTMProviderSettings] | None = None
|
44
|
+
|
45
|
+
_instructions: str | None = None
|
46
|
+
|
47
|
+
# pylint: disable=R0913, R0917
|
48
|
+
def __init__(
|
49
|
+
self,
|
50
|
+
coordinates: tuple[float, float],
|
51
|
+
user_settings: DTMProviderSettings | None,
|
52
|
+
size: int,
|
53
|
+
directory: str,
|
54
|
+
logger: Logger,
|
55
|
+
map: Map | None = None, # pylint: disable=W0622
|
56
|
+
):
|
57
|
+
self._coordinates = coordinates
|
58
|
+
self._user_settings = user_settings
|
59
|
+
self._size = size
|
60
|
+
|
61
|
+
if not self._code:
|
62
|
+
raise ValueError("Provider code must be defined.")
|
63
|
+
self._tile_directory = os.path.join(directory, self._code)
|
64
|
+
os.makedirs(self._tile_directory, exist_ok=True)
|
65
|
+
|
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
|
88
|
+
|
89
|
+
@property
|
90
|
+
def coordinates(self) -> tuple[float, float]:
|
91
|
+
"""Coordinates of the center point of the DTM data.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
tuple: Latitude and longitude of the center point.
|
95
|
+
"""
|
96
|
+
return self._coordinates
|
97
|
+
|
98
|
+
@property
|
99
|
+
def size(self) -> int:
|
100
|
+
"""Size of the DTM data in meters.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
int: Size of the DTM data.
|
104
|
+
"""
|
105
|
+
return self._size
|
106
|
+
|
107
|
+
@property
|
108
|
+
def url(self) -> str | None:
|
109
|
+
"""URL of the provider."""
|
110
|
+
return self._url
|
111
|
+
|
112
|
+
def formatted_url(self, **kwargs) -> str:
|
113
|
+
"""Formatted URL of the provider."""
|
114
|
+
if not self.url:
|
115
|
+
raise ValueError("URL must be defined.")
|
116
|
+
return self.url.format(**kwargs)
|
117
|
+
|
118
|
+
@classmethod
|
119
|
+
def author(cls) -> str | None:
|
120
|
+
"""Author of the provider.
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
str: Author of the provider.
|
124
|
+
"""
|
125
|
+
return cls._author
|
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
|
+
|
145
|
+
@classmethod
|
146
|
+
def is_community(cls) -> bool:
|
147
|
+
"""Is the provider a community-driven project.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
bool: True if the provider is a community-driven project, False otherwise.
|
151
|
+
"""
|
152
|
+
return cls._is_community
|
153
|
+
|
154
|
+
@classmethod
|
155
|
+
def settings(cls) -> Type[DTMProviderSettings] | None:
|
156
|
+
"""Settings model of the provider.
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
Type[DTMProviderSettings]: Settings model of the provider.
|
160
|
+
"""
|
161
|
+
return cls._settings
|
162
|
+
|
163
|
+
@classmethod
|
164
|
+
def instructions(cls) -> str | None:
|
165
|
+
"""Instructions for using the provider.
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
str: Instructions for using the provider.
|
169
|
+
"""
|
170
|
+
return cls._instructions
|
171
|
+
|
172
|
+
@property
|
173
|
+
def user_settings(self) -> DTMProviderSettings | None:
|
174
|
+
"""User settings of the provider.
|
175
|
+
|
176
|
+
Returns:
|
177
|
+
DTMProviderSettings: User settings of the provider.
|
178
|
+
"""
|
179
|
+
return self._user_settings
|
180
|
+
|
181
|
+
@classmethod
|
182
|
+
def description(cls) -> str:
|
183
|
+
"""Description of the provider.
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
str: Provider description.
|
187
|
+
"""
|
188
|
+
return f"{cls._icon} {cls._region} [{cls._resolution} m/px] {cls._name}"
|
189
|
+
|
190
|
+
@classmethod
|
191
|
+
def get_provider_by_code(cls, code: str) -> Type[DTMProvider] | None:
|
192
|
+
"""Get a provider by its code.
|
193
|
+
|
194
|
+
Arguments:
|
195
|
+
code (str): Provider code.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
DTMProvider: Provider class or None if not found.
|
199
|
+
"""
|
200
|
+
for provider in cls.__subclasses__():
|
201
|
+
if provider._code == code: # pylint: disable=W0212
|
202
|
+
return provider
|
203
|
+
return None
|
204
|
+
|
205
|
+
@classmethod
|
206
|
+
def get_provider_descriptions(cls) -> dict[str, str]:
|
207
|
+
"""Get descriptions of all providers, where keys are provider codes and
|
208
|
+
values are provider descriptions.
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
dict: Provider descriptions.
|
212
|
+
"""
|
213
|
+
providers = {}
|
214
|
+
for provider in cls.__subclasses__():
|
215
|
+
if not provider.is_base():
|
216
|
+
providers[provider._code] = provider.description() # pylint: disable=W0212
|
217
|
+
return providers # type: ignore
|
218
|
+
|
219
|
+
def download_tile(self, output_path: str, **kwargs) -> bool:
|
220
|
+
"""Download a tile from the provider.
|
221
|
+
|
222
|
+
Arguments:
|
223
|
+
output_path (str): Path to save the downloaded tile.
|
224
|
+
|
225
|
+
Returns:
|
226
|
+
bool: True if the tile was downloaded successfully, False otherwise.
|
227
|
+
"""
|
228
|
+
url = self.formatted_url(**kwargs)
|
229
|
+
response = requests.get(url, stream=True, timeout=10)
|
230
|
+
if response.status_code == 200:
|
231
|
+
with open(output_path, "wb") as file:
|
232
|
+
for chunk in response.iter_content(chunk_size=1024):
|
233
|
+
file.write(chunk)
|
234
|
+
return True
|
235
|
+
return False
|
236
|
+
|
237
|
+
def get_or_download_tile(self, output_path: str, **kwargs) -> str | None:
|
238
|
+
"""Get or download a tile from the provider.
|
239
|
+
|
240
|
+
Arguments:
|
241
|
+
output_path (str): Path to save the downloaded tile.
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
str: Path to the downloaded tile or None if the tile not exists and was
|
245
|
+
not downloaded.
|
246
|
+
"""
|
247
|
+
if not os.path.exists(output_path):
|
248
|
+
if not self.download_tile(output_path, **kwargs):
|
249
|
+
return None
|
250
|
+
return output_path
|
251
|
+
|
252
|
+
def get_tile_parameters(self, *args, **kwargs) -> dict:
|
253
|
+
"""Get parameters for the tile, that will be used to format the URL.
|
254
|
+
Must be implemented in subclasses.
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
dict: Tile parameters to format the URL.
|
258
|
+
"""
|
259
|
+
raise NotImplementedError
|
260
|
+
|
261
|
+
@abstractmethod
|
262
|
+
def get_numpy(self) -> np.ndarray:
|
263
|
+
"""Get numpy array of the tile.
|
264
|
+
Resulting array must be 16 bit (signed or unsigned) integer and it should be already
|
265
|
+
windowed to the bounding box of ROI. It also must have only one channel.
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
np.ndarray: Numpy array of the tile.
|
269
|
+
"""
|
270
|
+
raise NotImplementedError
|
271
|
+
|
272
|
+
def get_bbox(self) -> tuple[float, float, float, float]:
|
273
|
+
"""Get bounding box of the tile based on the center point and size.
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
tuple: Bounding box of the tile (north, south, east, west).
|
277
|
+
"""
|
278
|
+
west, south, east, north = ox.utils_geo.bbox_from_point( # type: ignore
|
279
|
+
self.coordinates, dist=self.size // 2, project_utm=False
|
280
|
+
)
|
281
|
+
bbox = north, south, east, west
|
282
|
+
return bbox
|
283
|
+
|
284
|
+
def extract_roi(self, tile_path: str) -> np.ndarray:
|
285
|
+
"""Extract region of interest (ROI) from the GeoTIFF file.
|
286
|
+
|
287
|
+
Arguments:
|
288
|
+
tile_path (str): Path to the GeoTIFF file.
|
289
|
+
|
290
|
+
Raises:
|
291
|
+
ValueError: If the tile does not contain any data.
|
292
|
+
|
293
|
+
Returns:
|
294
|
+
np.ndarray: Numpy array of the ROI.
|
295
|
+
"""
|
296
|
+
north, south, east, west = self.get_bbox()
|
297
|
+
with rasterio.open(tile_path) as src:
|
298
|
+
self.logger.debug("Opened tile, shape: %s, dtype: %s.", src.shape, src.dtypes[0])
|
299
|
+
window = rasterio.windows.from_bounds(west, south, east, north, src.transform)
|
300
|
+
self.logger.debug(
|
301
|
+
"Window parameters. Column offset: %s, row offset: %s, width: %s, height: %s.",
|
302
|
+
window.col_off,
|
303
|
+
window.row_off,
|
304
|
+
window.width,
|
305
|
+
window.height,
|
306
|
+
)
|
307
|
+
data = src.read(1, window=window)
|
308
|
+
if not data.size > 0:
|
309
|
+
raise ValueError("No data in the tile.")
|
310
|
+
|
311
|
+
return data
|
312
|
+
|
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.
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
dict[str, int | str | float] | None: Information sequence for the component.
|
320
|
+
"""
|
321
|
+
return self.data_info
|