maps4fs 1.8.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.
- 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
|