maps4fs 1.5.0__py3-none-any.whl → 1.5.7__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 CHANGED
@@ -1,11 +1,12 @@
1
1
  # pylint: disable=missing-module-docstring
2
+ from maps4fs.generator.dtm import DTMProvider
2
3
  from maps4fs.generator.game import Game
3
- from maps4fs.generator.map import (
4
+ from maps4fs.generator.map import Map
5
+ from maps4fs.generator.settings import (
4
6
  BackgroundSettings,
5
7
  DEMSettings,
6
8
  GRLESettings,
7
9
  I3DSettings,
8
- Map,
9
10
  SettingsModel,
10
11
  SplineSettings,
11
12
  TextureSettings,
@@ -57,36 +57,23 @@ class Background(Component):
57
57
  os.makedirs(self.background_directory, exist_ok=True)
58
58
  os.makedirs(self.water_directory, exist_ok=True)
59
59
 
60
- autoprocesses = [self.map.dem_settings.auto_process, False]
61
- self.output_paths = [
62
- os.path.join(self.background_directory, f"{name}.png") for name in ELEMENTS
63
- ]
60
+ self.output_path = os.path.join(self.background_directory, f"{FULL_NAME}.png")
64
61
  self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
65
62
  self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
66
63
 
67
- dems = []
68
-
69
- for name, autoprocess, output_path in zip(ELEMENTS, autoprocesses, self.output_paths):
70
- dem = DEM(
71
- self.game,
72
- self.map,
73
- self.coordinates,
74
- self.background_size,
75
- self.rotated_size,
76
- self.rotation,
77
- self.map_directory,
78
- self.logger,
79
- )
80
- dem.preprocess()
81
- dem.is_preview = self.is_preview(name) # type: ignore
82
- if dem.is_preview: # type: ignore
83
- dem.multiplier = 1
84
- dem.auto_process = autoprocess
85
- dem.set_output_resolution((self.rotated_size, self.rotated_size))
86
- dem.set_dem_path(output_path)
87
- dems.append(dem)
88
-
89
- self.dems = dems
64
+ self.dem = DEM(
65
+ self.game,
66
+ self.map,
67
+ self.coordinates,
68
+ self.background_size,
69
+ self.rotated_size,
70
+ self.rotation,
71
+ self.map_directory,
72
+ self.logger,
73
+ )
74
+ self.dem.preprocess()
75
+ self.dem.set_output_resolution((self.rotated_size, self.rotated_size))
76
+ self.dem.set_dem_path(self.output_path)
90
77
 
91
78
  def is_preview(self, name: str) -> bool:
92
79
  """Checks if the DEM is a preview.
@@ -104,21 +91,17 @@ class Background(Component):
104
91
  as a result the DEM files will be saved, then based on them the obj files will be
105
92
  generated."""
106
93
  self.create_background_textures()
94
+ self.dem.process()
107
95
 
108
- for dem in self.dems:
109
- dem.process()
110
- if not dem.is_preview: # type: ignore
111
- shutil.copyfile(dem.dem_path, self.not_substracted_path)
112
- self.cutout(dem.dem_path, save_path=self.not_resized_path)
96
+ shutil.copyfile(self.dem.dem_path, self.not_substracted_path)
97
+ self.cutout(self.dem.dem_path, save_path=self.not_resized_path)
113
98
 
114
99
  if self.map.dem_settings.water_depth:
115
100
  self.subtraction()
116
101
 
117
- for dem in self.dems:
118
- if not dem.is_preview: # type: ignore
119
- cutted_dem_path = self.cutout(dem.dem_path)
120
- if self.game.additional_dem_name is not None:
121
- self.make_copy(cutted_dem_path, self.game.additional_dem_name)
102
+ cutted_dem_path = self.cutout(self.dem.dem_path)
103
+ if self.game.additional_dem_name is not None:
104
+ self.make_copy(cutted_dem_path, self.game.additional_dem_name)
122
105
 
123
106
  if self.map.background_settings.generate_background:
124
107
  self.generate_obj_files()
@@ -149,19 +132,17 @@ class Background(Component):
149
132
  """
150
133
  self.qgis_sequence()
151
134
 
152
- dem = self.dems[0]
153
-
154
- north, south, east, west = dem.bbox
155
- epsg3857_string = dem.get_epsg3857_string()
156
- epsg3857_string_with_margin = dem.get_epsg3857_string(add_margin=True)
135
+ north, south, east, west = self.dem.bbox
136
+ epsg3857_string = self.dem.get_epsg3857_string()
137
+ epsg3857_string_with_margin = self.dem.get_epsg3857_string(add_margin=True)
157
138
 
158
139
  data = {
159
- "center_latitude": dem.coordinates[0],
160
- "center_longitude": dem.coordinates[1],
140
+ "center_latitude": self.dem.coordinates[0],
141
+ "center_longitude": self.dem.coordinates[1],
161
142
  "epsg3857_string": epsg3857_string,
162
143
  "epsg3857_string_with_margin": epsg3857_string_with_margin,
163
- "height": dem.map_size,
164
- "width": dem.map_size,
144
+ "height": self.dem.map_size,
145
+ "width": self.dem.map_size,
165
146
  "north": north,
166
147
  "south": south,
167
148
  "east": east,
@@ -171,10 +152,10 @@ class Background(Component):
171
152
 
172
153
  def qgis_sequence(self) -> None:
173
154
  """Generates QGIS scripts for creating bounding box layers and rasterizing them."""
174
- qgis_layer = (f"Background_{FULL_NAME}", *self.dems[0].get_espg3857_bbox())
155
+ qgis_layer = (f"Background_{FULL_NAME}", *self.dem.get_espg3857_bbox())
175
156
  qgis_layer_with_margin = (
176
157
  f"Background_{FULL_NAME}_margin",
177
- *self.dems[0].get_espg3857_bbox(add_margin=True),
158
+ *self.dem.get_espg3857_bbox(add_margin=True),
178
159
  )
179
160
  self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
180
161
 
@@ -182,21 +163,20 @@ class Background(Component):
182
163
  """Iterates over all dems and generates 3D obj files based on DEM data.
183
164
  If at least one DEM file is missing, the generation will be stopped at all.
184
165
  """
185
- for dem in self.dems:
186
- if not os.path.isfile(dem.dem_path):
187
- self.logger.warning(
188
- "DEM file not found, generation will be stopped: %s", dem.dem_path
189
- )
190
- return
166
+ if not os.path.isfile(self.dem.dem_path):
167
+ self.logger.warning(
168
+ "DEM file not found, generation will be stopped: %s", self.dem.dem_path
169
+ )
170
+ return
191
171
 
192
- self.logger.debug("DEM file for found: %s", dem.dem_path)
172
+ self.logger.debug("DEM file for found: %s", self.dem.dem_path)
193
173
 
194
- filename = os.path.splitext(os.path.basename(dem.dem_path))[0]
195
- save_path = os.path.join(self.background_directory, f"{filename}.obj")
196
- self.logger.debug("Generating obj file in path: %s", save_path)
174
+ filename = os.path.splitext(os.path.basename(self.dem.dem_path))[0]
175
+ save_path = os.path.join(self.background_directory, f"{filename}.obj")
176
+ self.logger.debug("Generating obj file in path: %s", save_path)
197
177
 
198
- dem_data = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
199
- self.plane_from_np(dem_data, save_path, is_preview=dem.is_preview) # type: ignore
178
+ dem_data = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
179
+ self.plane_from_np(dem_data, save_path) # type: ignore
200
180
 
201
181
  # pylint: disable=too-many-locals
202
182
  def cutout(self, dem_path: str, save_path: str | None = None) -> str:
@@ -248,7 +228,6 @@ class Background(Component):
248
228
  self,
249
229
  dem_data: np.ndarray,
250
230
  save_path: str,
251
- is_preview: bool = False,
252
231
  include_zeros: bool = True,
253
232
  ) -> None:
254
233
  """Generates a 3D obj file based on DEM data.
@@ -256,7 +235,6 @@ class Background(Component):
256
235
  Arguments:
257
236
  dem_data (np.ndarray) -- The DEM data as a numpy array.
258
237
  save_path (str) -- The path where the obj file will be saved.
259
- is_preview (bool, optional) -- If True, the preview mesh will be generated.
260
238
  include_zeros (bool, optional) -- If True, the mesh will include the zero height values.
261
239
  """
262
240
  resize_factor = 1 / self.map.background_settings.resize_factor
@@ -315,25 +293,21 @@ class Background(Component):
315
293
  mesh.apply_transform(rotation_matrix_y)
316
294
  mesh.apply_transform(rotation_matrix_z)
317
295
 
318
- if is_preview:
296
+ # if not include_zeros:
297
+ z_scaling_factor = 1 / self.map.dem_settings.multiplier
298
+ self.logger.debug("Z scaling factor: %s", z_scaling_factor)
299
+ mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
300
+
301
+ mesh.export(save_path)
302
+ self.logger.debug("Obj file saved: %s", save_path)
303
+
304
+ if include_zeros:
319
305
  # Simplify the preview mesh to reduce the size of the file.
320
306
  mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
321
307
 
322
308
  # Apply scale to make the preview mesh smaller in the UI.
323
309
  mesh.apply_scale([0.5, 0.5, 0.5])
324
310
  self.mesh_to_stl(mesh)
325
- else:
326
- if not include_zeros:
327
- multiplier = self.map.dem_settings.multiplier
328
- if multiplier != 1:
329
- z_scaling_factor = 1 / multiplier
330
- else:
331
- z_scaling_factor = 1 / 2**5
332
- self.logger.debug("Z scaling factor: %s", z_scaling_factor)
333
- mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
334
-
335
- mesh.export(save_path)
336
- self.logger.debug("Obj file saved: %s", save_path)
337
311
 
338
312
  def mesh_to_stl(self, mesh: trimesh.Trimesh) -> None:
339
313
  """Converts the mesh to an STL file and saves it in the previews directory.
@@ -358,25 +332,22 @@ class Background(Component):
358
332
  list[str] -- A list of paths to the previews.
359
333
  """
360
334
  preview_paths = self.dem_previews(self.game.dem_file_path(self.map_directory))
361
- for dem in self.dems:
362
- if dem.is_preview: # type: ignore
363
- background_dem_preview_path = os.path.join(
364
- self.previews_directory, "background_dem.png"
365
- )
366
- background_dem_preview_image = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED)
367
-
368
- background_dem_preview_image = cv2.resize(
369
- background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
370
- )
371
- background_dem_preview_image = cv2.normalize( # type: ignore
372
- background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
373
- )
374
- background_dem_preview_image = cv2.cvtColor(
375
- background_dem_preview_image, cv2.COLOR_GRAY2BGR
376
- )
377
-
378
- cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
379
- preview_paths.append(background_dem_preview_path)
335
+
336
+ background_dem_preview_path = os.path.join(self.previews_directory, "background_dem.png")
337
+ background_dem_preview_image = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED)
338
+
339
+ background_dem_preview_image = cv2.resize(
340
+ background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
341
+ )
342
+ background_dem_preview_image = cv2.normalize( # type: ignore
343
+ background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
344
+ )
345
+ background_dem_preview_image = cv2.cvtColor(
346
+ background_dem_preview_image, cv2.COLOR_GRAY2BGR
347
+ )
348
+
349
+ cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
350
+ preview_paths.append(background_dem_preview_path)
380
351
 
381
352
  if self.stl_preview_path:
382
353
  preview_paths.append(self.stl_preview_path)
@@ -525,18 +496,15 @@ class Background(Component):
525
496
  bool
526
497
  )
527
498
 
528
- for output_path in self.output_paths:
529
- if FULL_PREVIEW_NAME in output_path:
530
- continue
531
- dem_image = cv2.imread(output_path, cv2.IMREAD_UNCHANGED)
499
+ dem_image = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
532
500
 
533
- # Create a mask where water_resources_image is 255 (or not 0)
534
- # Subtract water_depth from dem_image where mask is True
535
- dem_image[mask] = dem_image[mask] - self.map.dem_settings.water_depth
501
+ # Create a mask where water_resources_image is 255 (or not 0)
502
+ # Subtract water_depth from dem_image where mask is True
503
+ dem_image[mask] = dem_image[mask] - self.map.dem_settings.water_depth
536
504
 
537
- # Save the modified dem_image back to the output path
538
- cv2.imwrite(output_path, dem_image)
539
- self.logger.debug("Water depth subtracted from DEM data: %s", output_path)
505
+ # Save the modified dem_image back to the output path
506
+ cv2.imwrite(self.output_path, dem_image)
507
+ self.logger.debug("Water depth subtracted from DEM data: %s", self.output_path)
540
508
 
541
509
  def generate_water_resources_obj(self) -> None:
542
510
  """Generates 3D obj files based on water resources data."""
@@ -550,9 +518,7 @@ class Background(Component):
550
518
  plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
551
519
  ).astype(np.uint8)
552
520
  plane_save_path = os.path.join(self.water_directory, "plane_water.obj")
553
- self.plane_from_np(
554
- dilated_plane_water, plane_save_path, is_preview=False, include_zeros=False
555
- )
521
+ self.plane_from_np(dilated_plane_water, plane_save_path, include_zeros=False)
556
522
 
557
523
  # Single channeled 16 bit DEM image of terrain.
558
524
  background_dem = cv2.imread(self.not_substracted_path, cv2.IMREAD_UNCHANGED)
@@ -570,6 +536,4 @@ class Background(Component):
570
536
  elevated_water = np.where(mask, background_dem, elevated_water)
571
537
  elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
572
538
 
573
- self.plane_from_np(
574
- elevated_water, elevated_save_path, is_preview=False, include_zeros=False
575
- )
539
+ self.plane_from_np(elevated_water, elevated_save_path, include_zeros=False)
@@ -68,6 +68,7 @@ class Component:
68
68
  os.makedirs(self.previews_directory, exist_ok=True)
69
69
  os.makedirs(self.scripts_directory, exist_ok=True)
70
70
  os.makedirs(self.info_layers_directory, exist_ok=True)
71
+ os.makedirs(self.satellite_directory, exist_ok=True)
71
72
 
72
73
  self.save_bbox()
73
74
  self.preprocess()
@@ -123,6 +124,15 @@ class Component:
123
124
  """
124
125
  return os.path.join(self.map_directory, "scripts")
125
126
 
127
+ @property
128
+ def satellite_directory(self) -> str:
129
+ """The directory where the satellite images are stored.
130
+
131
+ Returns:
132
+ str: The directory where the satellite images are stored.
133
+ """
134
+ return os.path.join(self.map_directory, "satellite")
135
+
126
136
  @property
127
137
  def generation_info_path(self) -> str:
128
138
  """The path to the generation info JSON file.
maps4fs/generator/dem.py CHANGED
@@ -1,19 +1,15 @@
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 shutil
7
4
 
8
5
  import cv2
9
6
  import numpy as np
10
- import rasterio # type: ignore
11
- import requests
7
+
8
+ # import rasterio # type: ignore
12
9
  from pympler import asizeof # type: ignore
13
10
 
14
11
  from maps4fs.generator.component import Component
15
-
16
- SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
12
+ from maps4fs.generator.dtm import DTMProvider
17
13
 
18
14
 
19
15
  # pylint: disable=R0903, R0902
@@ -61,7 +57,13 @@ class DEM(Component):
61
57
  self.blur_radius,
62
58
  )
63
59
 
64
- self.auto_process = self.map.dem_settings.auto_process
60
+ self.dtm_provider: DTMProvider = self.map.dtm_provider( # type: ignore
61
+ coordinates=self.coordinates,
62
+ user_settings=self.map.dtm_provider_settings,
63
+ size=self.map_rotated_size,
64
+ directory=self.temp_dir,
65
+ logger=self.logger,
66
+ )
65
67
 
66
68
  @property
67
69
  def dem_path(self) -> str:
@@ -132,36 +134,29 @@ class DEM(Component):
132
134
  def process(self) -> None:
133
135
  """Reads SRTM file, crops it to map size, normalizes and blurs it,
134
136
  saves to map directory."""
135
- north, south, east, west = self.bbox
136
137
 
137
138
  dem_output_resolution = self.output_resolution
138
139
  self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
139
140
 
140
- tile_path = self._srtm_tile()
141
- if not tile_path:
142
- self.logger.warning("Tile was not downloaded, DEM file will be filled with zeros.")
141
+ try:
142
+ data = self.dtm_provider.get_numpy()
143
+ except Exception as e: # pylint: disable=W0718
144
+ self.logger.error("Failed to get DEM data from SRTM: %s.", e)
143
145
  self._save_empty_dem(dem_output_resolution)
144
146
  return
145
147
 
146
- with rasterio.open(tile_path) as src:
147
- self.logger.debug("Opened tile, shape: %s, dtype: %s.", src.shape, src.dtypes[0])
148
- window = rasterio.windows.from_bounds(west, south, east, north, src.transform)
149
- self.logger.debug(
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)
148
+ if len(data.shape) != 2:
149
+ self.logger.error("DTM provider returned incorrect data: more than 1 channel.")
150
+ self._save_empty_dem(dem_output_resolution)
151
+ return
157
152
 
158
- if not data.size > 0:
159
- self.logger.warning("DEM data is empty, DEM file will be filled with zeros.")
153
+ if data.dtype not in ["int16", "uint16"]:
154
+ self.logger.error("DTM provider returned incorrect data type: %s.", data.dtype)
160
155
  self._save_empty_dem(dem_output_resolution)
161
156
  return
162
157
 
163
158
  self.logger.debug(
164
- "DEM data was read from SRTM file. Shape: %s, dtype: %s. Min: %s, max: %s.",
159
+ "DEM data was retrieved from DTM provider. Shape: %s, dtype: %s. Min: %s, max: %s.",
165
160
  data.shape,
166
161
  data.dtype,
167
162
  data.min(),
@@ -184,11 +179,7 @@ class DEM(Component):
184
179
  resampled_data.dtype,
185
180
  )
186
181
 
187
- if self.auto_process:
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.")
182
+ if self.multiplier != 1:
192
183
  resampled_data = resampled_data * self.multiplier
193
184
 
194
185
  self.logger.debug(
@@ -276,81 +267,6 @@ class DEM(Component):
276
267
  output_width=output_width,
277
268
  )
278
269
 
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
270
  def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
355
271
  """Saves empty DEM file filled with zeros."""
356
272
  dem_data = np.zeros(dem_output_resolution, dtype="uint16")