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.
@@ -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