maps4fs 1.8.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- maps4fs/__init__.py +22 -0
- maps4fs/generator/__init__.py +1 -0
- maps4fs/generator/background.py +625 -0
- maps4fs/generator/component.py +553 -0
- maps4fs/generator/config.py +109 -0
- maps4fs/generator/dem.py +297 -0
- maps4fs/generator/dtm/__init__.py +0 -0
- maps4fs/generator/dtm/base/wcs.py +71 -0
- maps4fs/generator/dtm/base/wms.py +70 -0
- maps4fs/generator/dtm/bavaria.py +113 -0
- maps4fs/generator/dtm/dtm.py +637 -0
- maps4fs/generator/dtm/england.py +31 -0
- maps4fs/generator/dtm/hessen.py +31 -0
- maps4fs/generator/dtm/niedersachsen.py +39 -0
- maps4fs/generator/dtm/nrw.py +30 -0
- maps4fs/generator/dtm/srtm.py +127 -0
- maps4fs/generator/dtm/usgs.py +87 -0
- maps4fs/generator/dtm/utils.py +61 -0
- maps4fs/generator/game.py +247 -0
- maps4fs/generator/grle.py +470 -0
- maps4fs/generator/i3d.py +624 -0
- maps4fs/generator/map.py +275 -0
- maps4fs/generator/qgis.py +196 -0
- maps4fs/generator/satellite.py +92 -0
- maps4fs/generator/settings.py +187 -0
- maps4fs/generator/texture.py +893 -0
- maps4fs/logger.py +46 -0
- maps4fs/toolbox/__init__.py +1 -0
- maps4fs/toolbox/background.py +63 -0
- maps4fs/toolbox/custom_osm.py +67 -0
- maps4fs/toolbox/dem.py +112 -0
- maps4fs-1.8.0.dist-info/LICENSE.md +190 -0
- maps4fs-1.8.0.dist-info/METADATA +693 -0
- maps4fs-1.8.0.dist-info/RECORD +36 -0
- maps4fs-1.8.0.dist-info/WHEEL +5 -0
- maps4fs-1.8.0.dist-info/top_level.txt +1 -0
maps4fs/generator/dem.py
ADDED
@@ -0,0 +1,297 @@
|
|
1
|
+
"""This module contains DEM class for processing Digital Elevation Model data."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
import cv2
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
# import rasterio # type: ignore
|
10
|
+
from pympler import asizeof # type: ignore
|
11
|
+
|
12
|
+
from maps4fs.generator.component import Component
|
13
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
14
|
+
|
15
|
+
|
16
|
+
# pylint: disable=R0903, R0902
|
17
|
+
class DEM(Component):
|
18
|
+
"""Component for processing Digital Elevation Model data.
|
19
|
+
|
20
|
+
Arguments:
|
21
|
+
game (Game): The game instance for which the map is generated.
|
22
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
23
|
+
map_size (int): The size of the map in pixels.
|
24
|
+
map_rotated_size (int): The size of the map in pixels after rotation.
|
25
|
+
rotation (int): The rotation angle of the map.
|
26
|
+
map_directory (str): The directory where the map files are stored.
|
27
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
28
|
+
info, warning. If not provided, default logging will be used.
|
29
|
+
"""
|
30
|
+
|
31
|
+
def preprocess(self) -> None:
|
32
|
+
self._dem_path = self.game.dem_file_path(self.map_directory)
|
33
|
+
self.temp_dir = "temp"
|
34
|
+
self.hgt_dir = os.path.join(self.temp_dir, "hgt")
|
35
|
+
self.gz_dir = os.path.join(self.temp_dir, "gz")
|
36
|
+
os.makedirs(self.hgt_dir, exist_ok=True)
|
37
|
+
os.makedirs(self.gz_dir, exist_ok=True)
|
38
|
+
|
39
|
+
self.logger.debug("Map size: %s x %s.", self.map_size, self.map_size)
|
40
|
+
self.logger.debug(
|
41
|
+
"Map rotated size: %s x %s.", self.map_rotated_size, self.map_rotated_size
|
42
|
+
)
|
43
|
+
|
44
|
+
self.output_resolution = self.get_output_resolution()
|
45
|
+
self.logger.debug("Output resolution for DEM data: %s.", self.output_resolution)
|
46
|
+
|
47
|
+
blur_radius = self.map.dem_settings.blur_radius
|
48
|
+
if blur_radius is None or blur_radius <= 0:
|
49
|
+
# We'll disable blur if the radius is 0 or negative.
|
50
|
+
blur_radius = 0
|
51
|
+
elif blur_radius % 2 == 0:
|
52
|
+
blur_radius += 1
|
53
|
+
self.blur_radius = blur_radius
|
54
|
+
self.multiplier = self.map.dem_settings.multiplier
|
55
|
+
self.logger.debug(
|
56
|
+
"DEM value multiplier is %s, blur radius is %s.",
|
57
|
+
self.multiplier,
|
58
|
+
self.blur_radius,
|
59
|
+
)
|
60
|
+
|
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
|
+
)
|
69
|
+
|
70
|
+
@property
|
71
|
+
def dem_path(self) -> str:
|
72
|
+
"""Returns path to the DEM file.
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
str: Path to the DEM file.
|
76
|
+
"""
|
77
|
+
return self._dem_path
|
78
|
+
|
79
|
+
# pylint: disable=W0201
|
80
|
+
def set_dem_path(self, dem_path: str) -> None:
|
81
|
+
"""Set path to the DEM file.
|
82
|
+
|
83
|
+
Arguments:
|
84
|
+
dem_path (str): Path to the DEM file.
|
85
|
+
"""
|
86
|
+
self._dem_path = dem_path
|
87
|
+
|
88
|
+
# pylint: disable=W0201
|
89
|
+
def set_output_resolution(self, output_resolution: tuple[int, int]) -> None:
|
90
|
+
"""Set output resolution for DEM data (width, height).
|
91
|
+
|
92
|
+
Arguments:
|
93
|
+
output_resolution (tuple[int, int]): Output resolution for DEM data.
|
94
|
+
"""
|
95
|
+
self.output_resolution = output_resolution
|
96
|
+
|
97
|
+
def get_output_resolution(self, use_original: bool = False) -> tuple[int, int]:
|
98
|
+
"""Get output resolution for DEM data.
|
99
|
+
|
100
|
+
Arguments:
|
101
|
+
use_original (bool, optional): If True, will use original map size. Defaults to False.
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
tuple[int, int]: Output resolution for DEM data.
|
105
|
+
"""
|
106
|
+
map_size = self.map_size if use_original else self.map_rotated_size
|
107
|
+
|
108
|
+
dem_size = int((map_size / 2) * self.game.dem_multipliyer)
|
109
|
+
|
110
|
+
self.logger.debug(
|
111
|
+
"DEM size multiplier is %s, DEM size: %sx%s, use original: %s.",
|
112
|
+
self.game.dem_multipliyer,
|
113
|
+
dem_size,
|
114
|
+
dem_size,
|
115
|
+
use_original,
|
116
|
+
)
|
117
|
+
return dem_size, dem_size
|
118
|
+
|
119
|
+
def to_ground(self, data: np.ndarray) -> np.ndarray:
|
120
|
+
"""Receives the signed 16-bit integer array and converts it to the ground level.
|
121
|
+
If the min value is negative, it will become zero value and the rest of the values
|
122
|
+
will be shifted accordingly.
|
123
|
+
"""
|
124
|
+
# For examlem, min value was -50, it will become 0 and for all values we'll +50.
|
125
|
+
|
126
|
+
if data.min() < 0:
|
127
|
+
self.logger.debug("Array contains negative values, will be shifted to the ground.")
|
128
|
+
data = data + abs(data.min())
|
129
|
+
|
130
|
+
self.logger.debug(
|
131
|
+
"Array was shifted to the ground. Min: %s, max: %s.", data.min(), data.max()
|
132
|
+
)
|
133
|
+
return data
|
134
|
+
|
135
|
+
# pylint: disable=no-member
|
136
|
+
def process(self) -> None:
|
137
|
+
"""Reads SRTM file, crops it to map size, normalizes and blurs it,
|
138
|
+
saves to map directory."""
|
139
|
+
|
140
|
+
dem_output_resolution = self.output_resolution
|
141
|
+
self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
|
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)
|
147
|
+
self._save_empty_dem(dem_output_resolution)
|
148
|
+
return
|
149
|
+
|
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
|
154
|
+
|
155
|
+
if data.dtype not in ["int16", "uint16"]:
|
156
|
+
self.logger.error("DTM provider returned incorrect data type: %s.", data.dtype)
|
157
|
+
self._save_empty_dem(dem_output_resolution)
|
158
|
+
return
|
159
|
+
|
160
|
+
self.logger.debug(
|
161
|
+
"DEM data was retrieved from DTM provider. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
162
|
+
data.shape,
|
163
|
+
data.dtype,
|
164
|
+
data.min(),
|
165
|
+
data.max(),
|
166
|
+
)
|
167
|
+
|
168
|
+
data = self.to_ground(data)
|
169
|
+
|
170
|
+
resampled_data = cv2.resize(
|
171
|
+
data, dem_output_resolution, interpolation=cv2.INTER_LINEAR
|
172
|
+
).astype("uint16")
|
173
|
+
|
174
|
+
size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
|
175
|
+
self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
|
176
|
+
|
177
|
+
self.logger.debug(
|
178
|
+
"Maximum value in resampled data: %s, minimum value: %s. Data type: %s.",
|
179
|
+
resampled_data.max(),
|
180
|
+
resampled_data.min(),
|
181
|
+
resampled_data.dtype,
|
182
|
+
)
|
183
|
+
|
184
|
+
if self.multiplier != 1:
|
185
|
+
resampled_data = resampled_data * self.multiplier
|
186
|
+
|
187
|
+
self.logger.debug(
|
188
|
+
"DEM data was multiplied by %s. Min: %s, max: %s. Data type: %s.",
|
189
|
+
self.multiplier,
|
190
|
+
resampled_data.min(),
|
191
|
+
resampled_data.max(),
|
192
|
+
resampled_data.dtype,
|
193
|
+
)
|
194
|
+
|
195
|
+
size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
|
196
|
+
self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
|
197
|
+
|
198
|
+
# Clip values to 16-bit unsigned integer range.
|
199
|
+
resampled_data = np.clip(resampled_data, 0, 65535)
|
200
|
+
resampled_data = resampled_data.astype("uint16")
|
201
|
+
self.logger.debug(
|
202
|
+
"DEM data was multiplied by %s and clipped to 16-bit unsigned integer range. "
|
203
|
+
"Min: %s, max: %s.",
|
204
|
+
self.multiplier,
|
205
|
+
resampled_data.min(),
|
206
|
+
resampled_data.max(),
|
207
|
+
)
|
208
|
+
|
209
|
+
self.logger.debug(
|
210
|
+
"DEM data was resampled. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
211
|
+
resampled_data.shape,
|
212
|
+
resampled_data.dtype,
|
213
|
+
resampled_data.min(),
|
214
|
+
resampled_data.max(),
|
215
|
+
)
|
216
|
+
|
217
|
+
if self.blur_radius > 0:
|
218
|
+
resampled_data = cv2.GaussianBlur(
|
219
|
+
resampled_data, (self.blur_radius, self.blur_radius), sigmaX=40, sigmaY=40
|
220
|
+
)
|
221
|
+
self.logger.debug(
|
222
|
+
"Gaussion blur applied to DEM data with kernel size %s.",
|
223
|
+
self.blur_radius,
|
224
|
+
)
|
225
|
+
|
226
|
+
self.logger.debug(
|
227
|
+
"DEM data was blurred. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
228
|
+
resampled_data.shape,
|
229
|
+
resampled_data.dtype,
|
230
|
+
resampled_data.min(),
|
231
|
+
resampled_data.max(),
|
232
|
+
)
|
233
|
+
|
234
|
+
if self.map.dem_settings.plateau:
|
235
|
+
# Plateau is a flat area with a constant height.
|
236
|
+
# So we just add this value to each pixel of the DEM.
|
237
|
+
# And also need to ensure that there will be no values with height greater than
|
238
|
+
# it's allowed in 16-bit unsigned integer.
|
239
|
+
|
240
|
+
resampled_data += self.map.dem_settings.plateau
|
241
|
+
resampled_data = np.clip(resampled_data, 0, 65535)
|
242
|
+
|
243
|
+
self.logger.debug(
|
244
|
+
"Plateau with height %s was added to DEM data. Min: %s, max: %s.",
|
245
|
+
self.map.dem_settings.plateau,
|
246
|
+
resampled_data.min(),
|
247
|
+
resampled_data.max(),
|
248
|
+
)
|
249
|
+
|
250
|
+
cv2.imwrite(self._dem_path, resampled_data)
|
251
|
+
self.logger.debug("DEM data was saved to %s.", self._dem_path)
|
252
|
+
|
253
|
+
if self.rotation:
|
254
|
+
self.rotate_dem()
|
255
|
+
|
256
|
+
def rotate_dem(self) -> None:
|
257
|
+
"""Rotate DEM image."""
|
258
|
+
self.logger.debug("Rotating DEM image by %s degrees.", self.rotation)
|
259
|
+
output_width, output_height = self.get_output_resolution(use_original=True)
|
260
|
+
|
261
|
+
self.logger.debug(
|
262
|
+
"Output resolution for rotated DEM: %s x %s.", output_width, output_height
|
263
|
+
)
|
264
|
+
|
265
|
+
self.rotate_image(
|
266
|
+
self._dem_path,
|
267
|
+
self.rotation,
|
268
|
+
output_height=output_height,
|
269
|
+
output_width=output_width,
|
270
|
+
)
|
271
|
+
|
272
|
+
def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
|
273
|
+
"""Saves empty DEM file filled with zeros."""
|
274
|
+
dem_data = np.zeros(dem_output_resolution, dtype="uint16")
|
275
|
+
cv2.imwrite(self._dem_path, dem_data)
|
276
|
+
self.logger.warning("DEM data filled with zeros and saved to %s.", self._dem_path)
|
277
|
+
|
278
|
+
def previews(self) -> list:
|
279
|
+
"""This component does not have previews, returns empty list.
|
280
|
+
|
281
|
+
Returns:
|
282
|
+
list: Empty list.
|
283
|
+
"""
|
284
|
+
return []
|
285
|
+
|
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.
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
dict[Any, Any]: The information sequence for the component.
|
293
|
+
"""
|
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,71 @@
|
|
1
|
+
"""This module contains the base WCS provider."""
|
2
|
+
|
3
|
+
from abc import abstractmethod
|
4
|
+
import os
|
5
|
+
|
6
|
+
from owslib.wcs import WebCoverageService
|
7
|
+
from tqdm import tqdm
|
8
|
+
|
9
|
+
from maps4fs.generator.dtm import utils
|
10
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
11
|
+
|
12
|
+
|
13
|
+
# pylint: disable=too-many-locals
|
14
|
+
class WCSProvider(DTMProvider):
|
15
|
+
"""Generic provider of WCS sources."""
|
16
|
+
|
17
|
+
_is_base = True
|
18
|
+
_wcs_version = "2.0.1"
|
19
|
+
_source_crs: str = "EPSG:4326"
|
20
|
+
_tile_size: float = 0.02
|
21
|
+
|
22
|
+
@abstractmethod
|
23
|
+
def get_wcs_parameters(self, tile: tuple[float, float, float, float]) -> dict:
|
24
|
+
"""Get the parameters for the WCS request.
|
25
|
+
|
26
|
+
Arguments:
|
27
|
+
tile (tuple): The tile to download.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
dict: The parameters for the WCS request.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(self, *args, **kwargs):
|
34
|
+
super().__init__(*args, **kwargs)
|
35
|
+
self.shared_tiff_path = os.path.join(self._tile_directory, "shared")
|
36
|
+
os.makedirs(self.shared_tiff_path, exist_ok=True)
|
37
|
+
|
38
|
+
def download_tiles(self) -> list[str]:
|
39
|
+
bbox = self.get_bbox()
|
40
|
+
bbox = utils.transform_bbox(bbox, self._source_crs)
|
41
|
+
tiles = utils.tile_bbox(bbox, self._tile_size)
|
42
|
+
|
43
|
+
all_tif_files = self.download_all_tiles(tiles)
|
44
|
+
return all_tif_files
|
45
|
+
|
46
|
+
def download_all_tiles(self, tiles: list[tuple[float, float, float, float]]) -> list[str]:
|
47
|
+
"""Download tiles from the NI provider.
|
48
|
+
|
49
|
+
Arguments:
|
50
|
+
tiles (list): List of tiles to download.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
list: List of paths to the downloaded GeoTIFF files.
|
54
|
+
"""
|
55
|
+
all_tif_files = []
|
56
|
+
wcs = WebCoverageService(
|
57
|
+
self._url,
|
58
|
+
version=self._wcs_version,
|
59
|
+
# auth=Authentication(verify=False),
|
60
|
+
timeout=600,
|
61
|
+
)
|
62
|
+
for tile in tqdm(tiles, desc="Downloading tiles", unit="tile"):
|
63
|
+
file_name = "_".join(map(str, tile)) + ".tif"
|
64
|
+
file_path = os.path.join(self.shared_tiff_path, file_name)
|
65
|
+
if not os.path.exists(file_path):
|
66
|
+
output = wcs.getCoverage(**self.get_wcs_parameters(tile))
|
67
|
+
with open(file_path, "wb") as f:
|
68
|
+
f.write(output.read())
|
69
|
+
|
70
|
+
all_tif_files.append(file_path)
|
71
|
+
return all_tif_files
|
@@ -0,0 +1,70 @@
|
|
1
|
+
"""This module contains the base WMS provider."""
|
2
|
+
|
3
|
+
from abc import abstractmethod
|
4
|
+
import os
|
5
|
+
|
6
|
+
from owslib.wms import WebMapService
|
7
|
+
|
8
|
+
from maps4fs.generator.dtm import utils
|
9
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
10
|
+
|
11
|
+
|
12
|
+
# pylint: disable=too-many-locals
|
13
|
+
class WMSProvider(DTMProvider):
|
14
|
+
"""Generic provider of WMS sources."""
|
15
|
+
|
16
|
+
_is_base = True
|
17
|
+
_wms_version = "1.3.0"
|
18
|
+
_source_crs: str = "EPSG:4326"
|
19
|
+
_tile_size: float = 0.02
|
20
|
+
|
21
|
+
@abstractmethod
|
22
|
+
def get_wms_parameters(self, tile: tuple[float, float, float, float]) -> dict:
|
23
|
+
"""Get the parameters for the WMS request.
|
24
|
+
|
25
|
+
Arguments:
|
26
|
+
tile (tuple): The tile to download.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
dict: The parameters for the WMS request.
|
30
|
+
"""
|
31
|
+
|
32
|
+
def __init__(self, *args, **kwargs):
|
33
|
+
super().__init__(*args, **kwargs)
|
34
|
+
self.shared_tiff_path = os.path.join(self._tile_directory, "shared")
|
35
|
+
os.makedirs(self.shared_tiff_path, exist_ok=True)
|
36
|
+
|
37
|
+
def download_tiles(self) -> list[str]:
|
38
|
+
bbox = self.get_bbox()
|
39
|
+
bbox = utils.transform_bbox(bbox, self._source_crs)
|
40
|
+
tiles = utils.tile_bbox(bbox, self._tile_size)
|
41
|
+
|
42
|
+
all_tif_files = self.download_all_tiles(tiles)
|
43
|
+
return all_tif_files
|
44
|
+
|
45
|
+
def download_all_tiles(self, tiles: list[tuple[float, float, float, float]]) -> list[str]:
|
46
|
+
"""Download tiles from the WMS provider.
|
47
|
+
|
48
|
+
Arguments:
|
49
|
+
tiles (list): List of tiles to download.
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
list: List of paths to the downloaded GeoTIFF files.
|
53
|
+
"""
|
54
|
+
all_tif_files = []
|
55
|
+
wms = WebMapService(
|
56
|
+
self._url,
|
57
|
+
version=self._wms_version,
|
58
|
+
# auth=Authentication(verify=False),
|
59
|
+
timeout=600,
|
60
|
+
)
|
61
|
+
for tile in tiles:
|
62
|
+
file_name = "_".join(map(str, tile)) + ".tif"
|
63
|
+
file_path = os.path.join(self.shared_tiff_path, file_name)
|
64
|
+
if not os.path.exists(file_path):
|
65
|
+
output = wms.getmap(**self.get_wms_parameters(tile))
|
66
|
+
with open(file_path, "wb") as f:
|
67
|
+
f.write(output.read())
|
68
|
+
|
69
|
+
all_tif_files.append(file_path)
|
70
|
+
return all_tif_files
|
@@ -0,0 +1,113 @@
|
|
1
|
+
"""This module contains provider of Bavaria data."""
|
2
|
+
|
3
|
+
import hashlib
|
4
|
+
import os
|
5
|
+
from xml.etree import ElementTree as ET
|
6
|
+
|
7
|
+
import requests
|
8
|
+
|
9
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
10
|
+
|
11
|
+
|
12
|
+
class BavariaProvider(DTMProvider):
|
13
|
+
"""Provider of Bavaria Digital terrain model (DTM) 1m data.
|
14
|
+
Data is provided by the 'Bayerische Vermessungsverwaltung' and available
|
15
|
+
at https://geodaten.bayern.de/opengeodata/OpenDataDetail.html?pn=dgm1 under CC BY 4.0 license.
|
16
|
+
"""
|
17
|
+
|
18
|
+
_code = "bavaria"
|
19
|
+
_name = "Bavaria DGM1"
|
20
|
+
_region = "DE"
|
21
|
+
_icon = "🇩🇪"
|
22
|
+
_resolution = 1
|
23
|
+
_author = "[H4rdB4se](https://github.com/H4rdB4se)"
|
24
|
+
_is_community = True
|
25
|
+
_instructions = None
|
26
|
+
_extents = (50.56, 47.25, 13.91, 8.95)
|
27
|
+
|
28
|
+
def __init__(self, *args, **kwargs):
|
29
|
+
super().__init__(*args, **kwargs)
|
30
|
+
self.tiff_path = os.path.join(self._tile_directory, "tiffs")
|
31
|
+
os.makedirs(self.tiff_path, exist_ok=True)
|
32
|
+
self.meta4_path = os.path.join(self._tile_directory, "meta4")
|
33
|
+
os.makedirs(self.meta4_path, exist_ok=True)
|
34
|
+
|
35
|
+
def download_tiles(self) -> list[str]:
|
36
|
+
download_urls = self.get_meta_file_from_coords()
|
37
|
+
all_tif_files = self.download_tif_files(download_urls, self.tiff_path)
|
38
|
+
return all_tif_files
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
def get_meta_file_name(north: float, south: float, east: float, west: float) -> str:
|
42
|
+
"""Generate a hashed file name for the .meta4 file.
|
43
|
+
|
44
|
+
Arguments:
|
45
|
+
north (float): Northern latitude.
|
46
|
+
south (float): Southern latitude.
|
47
|
+
east (float): Eastern longitude.
|
48
|
+
west (float): Western longitude.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
str: Hashed file name.
|
52
|
+
"""
|
53
|
+
coordinates = f"{north}_{south}_{east}_{west}"
|
54
|
+
hash_object = hashlib.md5(coordinates.encode())
|
55
|
+
hashed_file_name = "download_" + hash_object.hexdigest() + ".meta4"
|
56
|
+
return hashed_file_name
|
57
|
+
|
58
|
+
def get_meta_file_from_coords(self) -> list[str]:
|
59
|
+
"""Download .meta4 (xml format) file
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
list: List of download URLs.
|
63
|
+
"""
|
64
|
+
(north, south, east, west) = self.get_bbox()
|
65
|
+
file_path = os.path.join(self.meta4_path, self.get_meta_file_name(north, south, east, west))
|
66
|
+
if not os.path.exists(file_path):
|
67
|
+
try:
|
68
|
+
# Make the GET request
|
69
|
+
response = requests.post(
|
70
|
+
"https://geoservices.bayern.de/services/poly2metalink/metalink/dgm1",
|
71
|
+
(
|
72
|
+
f"SRID=4326;POLYGON(({west} {south},{east} {south},"
|
73
|
+
f"{east} {north},{west} {north},{west} {south}))"
|
74
|
+
),
|
75
|
+
stream=True,
|
76
|
+
timeout=60,
|
77
|
+
)
|
78
|
+
|
79
|
+
# Check if the request was successful (HTTP status code 200)
|
80
|
+
if response.status_code == 200:
|
81
|
+
# Write the content of the response to the file
|
82
|
+
with open(file_path, "wb") as meta_file:
|
83
|
+
for chunk in response.iter_content(chunk_size=8192): # Download in chunks
|
84
|
+
meta_file.write(chunk)
|
85
|
+
self.logger.debug("File downloaded successfully: %s", file_path)
|
86
|
+
else:
|
87
|
+
self.logger.error("Download error. HTTP Status Code: %s", response.status_code)
|
88
|
+
except requests.exceptions.RequestException as e:
|
89
|
+
self.logger.error("Failed to get data. Error: %s", e)
|
90
|
+
else:
|
91
|
+
self.logger.debug("File already exists: %s", file_path)
|
92
|
+
return self.extract_urls_from_xml(file_path)
|
93
|
+
|
94
|
+
def extract_urls_from_xml(self, file_path: str) -> list[str]:
|
95
|
+
"""Extract URLs from the XML file.
|
96
|
+
|
97
|
+
Arguments:
|
98
|
+
file_path (str): Path to the XML file.
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
list: List of URLs.
|
102
|
+
"""
|
103
|
+
urls: list[str] = []
|
104
|
+
root = ET.parse(file_path).getroot()
|
105
|
+
namespace = {"ml": "urn:ietf:params:xml:ns:metalink"}
|
106
|
+
|
107
|
+
for file in root.findall(".//ml:file", namespace):
|
108
|
+
url = file.find("ml:url", namespace)
|
109
|
+
if url is not None:
|
110
|
+
urls.append(str(url.text))
|
111
|
+
|
112
|
+
self.logger.debug("Received %s urls", len(urls))
|
113
|
+
return urls
|