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 CHANGED
@@ -1,11 +1,14 @@
1
1
  # pylint: disable=missing-module-docstring
2
+ from maps4fs.generator.dtm.dtm import DTMProvider
3
+ from maps4fs.generator.dtm.srtm import SRTM30Provider
4
+ from maps4fs.generator.dtm.usgs import USGSProvider
2
5
  from maps4fs.generator.game import Game
3
- from maps4fs.generator.map import (
6
+ from maps4fs.generator.map import Map
7
+ from maps4fs.generator.settings import (
4
8
  BackgroundSettings,
5
9
  DEMSettings,
6
10
  GRLESettings,
7
11
  I3DSettings,
8
- Map,
9
12
  SettingsModel,
10
13
  SplineSettings,
11
14
  TextureSettings,
@@ -57,36 +57,49 @@ 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")
61
+ if self.map.custom_background_path:
62
+ self.check_custom_background(self.map.custom_background_path)
63
+ shutil.copyfile(self.map.custom_background_path, self.output_path)
64
+
64
65
  self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
65
66
  self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
66
67
 
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)
68
+ self.dem = DEM(
69
+ self.game,
70
+ self.map,
71
+ self.coordinates,
72
+ self.background_size,
73
+ self.rotated_size,
74
+ self.rotation,
75
+ self.map_directory,
76
+ self.logger,
77
+ )
78
+ self.dem.preprocess()
79
+ self.dem.set_output_resolution((self.rotated_size, self.rotated_size))
80
+ self.dem.set_dem_path(self.output_path)
81
+
82
+ def check_custom_background(self, image_path: str) -> None:
83
+ """Checks if the custom background image meets the requirements.
84
+
85
+ Arguments:
86
+ image_path (str): The path to the custom background image.
87
+
88
+ Raises:
89
+ ValueError: If the custom background image does not meet the requirements.
90
+ """
91
+ image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
92
+ if image.shape[0] != image.shape[1]:
93
+ raise ValueError("The custom background image must be a square.")
94
+
95
+ if image.shape[0] != self.map_size + DEFAULT_DISTANCE * 2:
96
+ raise ValueError("The custom background image must have the size of the map + 4096.")
97
+
98
+ if len(image.shape) != 2:
99
+ raise ValueError("The custom background image must be a grayscale image.")
88
100
 
89
- self.dems = dems
101
+ if image.dtype != np.uint16:
102
+ raise ValueError("The custom background image must be a 16-bit grayscale image.")
90
103
 
91
104
  def is_preview(self, name: str) -> bool:
92
105
  """Checks if the DEM is a preview.
@@ -105,20 +118,18 @@ class Background(Component):
105
118
  generated."""
106
119
  self.create_background_textures()
107
120
 
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)
121
+ if not self.map.custom_background_path:
122
+ self.dem.process()
123
+
124
+ shutil.copyfile(self.dem.dem_path, self.not_substracted_path)
125
+ self.cutout(self.dem.dem_path, save_path=self.not_resized_path)
113
126
 
114
127
  if self.map.dem_settings.water_depth:
115
128
  self.subtraction()
116
129
 
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)
130
+ cutted_dem_path = self.cutout(self.dem.dem_path)
131
+ if self.game.additional_dem_name is not None:
132
+ self.make_copy(cutted_dem_path, self.game.additional_dem_name)
122
133
 
123
134
  if self.map.background_settings.generate_background:
124
135
  self.generate_obj_files()
@@ -149,32 +160,33 @@ class Background(Component):
149
160
  """
150
161
  self.qgis_sequence()
151
162
 
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)
163
+ north, south, east, west = self.dem.bbox
164
+ epsg3857_string = self.dem.get_epsg3857_string()
165
+ epsg3857_string_with_margin = self.dem.get_epsg3857_string(add_margin=True)
157
166
 
158
167
  data = {
159
- "center_latitude": dem.coordinates[0],
160
- "center_longitude": dem.coordinates[1],
168
+ "center_latitude": self.dem.coordinates[0],
169
+ "center_longitude": self.dem.coordinates[1],
161
170
  "epsg3857_string": epsg3857_string,
162
171
  "epsg3857_string_with_margin": epsg3857_string_with_margin,
163
- "height": dem.map_size,
164
- "width": dem.map_size,
172
+ "height": self.dem.map_size,
173
+ "width": self.dem.map_size,
165
174
  "north": north,
166
175
  "south": south,
167
176
  "east": east,
168
177
  "west": west,
169
178
  }
179
+
180
+ dem_info_sequence = self.dem.info_sequence()
181
+ data["DEM"] = dem_info_sequence
170
182
  return data # type: ignore
171
183
 
172
184
  def qgis_sequence(self) -> None:
173
185
  """Generates QGIS scripts for creating bounding box layers and rasterizing them."""
174
- qgis_layer = (f"Background_{FULL_NAME}", *self.dems[0].get_espg3857_bbox())
186
+ qgis_layer = (f"Background_{FULL_NAME}", *self.dem.get_espg3857_bbox())
175
187
  qgis_layer_with_margin = (
176
188
  f"Background_{FULL_NAME}_margin",
177
- *self.dems[0].get_espg3857_bbox(add_margin=True),
189
+ *self.dem.get_espg3857_bbox(add_margin=True),
178
190
  )
179
191
  self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
180
192
 
@@ -182,21 +194,26 @@ class Background(Component):
182
194
  """Iterates over all dems and generates 3D obj files based on DEM data.
183
195
  If at least one DEM file is missing, the generation will be stopped at all.
184
196
  """
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
197
+ if not os.path.isfile(self.dem.dem_path):
198
+ self.logger.warning(
199
+ "DEM file not found, generation will be stopped: %s", self.dem.dem_path
200
+ )
201
+ return
191
202
 
192
- self.logger.debug("DEM file for found: %s", dem.dem_path)
203
+ self.logger.debug("DEM file for found: %s", self.dem.dem_path)
193
204
 
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)
205
+ filename = os.path.splitext(os.path.basename(self.dem.dem_path))[0]
206
+ save_path = os.path.join(self.background_directory, f"{filename}.obj")
207
+ self.logger.debug("Generating obj file in path: %s", save_path)
197
208
 
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
209
+ dem_data = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
210
+ self.plane_from_np(
211
+ dem_data,
212
+ save_path,
213
+ create_preview=True,
214
+ remove_center=self.map.background_settings.remove_center,
215
+ include_zeros=False,
216
+ ) # type: ignore
200
217
 
201
218
  # pylint: disable=too-many-locals
202
219
  def cutout(self, dem_path: str, save_path: str | None = None) -> str:
@@ -239,30 +256,56 @@ class Background(Component):
239
256
  )
240
257
 
241
258
  cv2.imwrite(main_dem_path, resized_dem_data) # pylint: disable=no-member
242
- self.logger.info("DEM cutout saved: %s", main_dem_path)
259
+ self.logger.debug("DEM cutout saved: %s", main_dem_path)
243
260
 
244
261
  return main_dem_path
245
262
 
246
- # pylint: disable=too-many-locals
263
+ def remove_center(self, dem_data: np.ndarray, resize_factor: float) -> np.ndarray:
264
+ """Removes the center part of the DEM data.
265
+
266
+ Arguments:
267
+ dem_data (np.ndarray) -- The DEM data as a numpy array.
268
+ resize_factor (float) -- The resize factor of the DEM data.
269
+
270
+ Returns:
271
+ np.ndarray -- The DEM data with the center part removed.
272
+ """
273
+ center = (dem_data.shape[0] // 2, dem_data.shape[1] // 2)
274
+ half_size = int(self.map_size // 2 * resize_factor)
275
+ x1 = center[0] - half_size
276
+ x2 = center[0] + half_size
277
+ y1 = center[1] - half_size
278
+ y2 = center[1] + half_size
279
+ dem_data[x1:x2, y1:y2] = 0
280
+ return dem_data
281
+
282
+ # pylint: disable=R0913, R0917, R0915
247
283
  def plane_from_np(
248
284
  self,
249
285
  dem_data: np.ndarray,
250
286
  save_path: str,
251
- is_preview: bool = False,
252
287
  include_zeros: bool = True,
288
+ create_preview: bool = False,
289
+ remove_center: bool = False,
253
290
  ) -> None:
254
291
  """Generates a 3D obj file based on DEM data.
255
292
 
256
293
  Arguments:
257
294
  dem_data (np.ndarray) -- The DEM data as a numpy array.
258
295
  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
296
  include_zeros (bool, optional) -- If True, the mesh will include the zero height values.
297
+ create_preview (bool, optional) -- If True, a simplified mesh will be saved as an STL.
298
+ remove_center (bool, optional) -- If True, the center of the mesh will be removed.
299
+ This setting is used for a Background Terrain, where the center part where the
300
+ playable area is will be cut out.
261
301
  """
262
302
  resize_factor = 1 / self.map.background_settings.resize_factor
263
303
  dem_data = cv2.resize( # pylint: disable=no-member
264
304
  dem_data, (0, 0), fx=resize_factor, fy=resize_factor
265
305
  )
306
+ if remove_center:
307
+ dem_data = self.remove_center(dem_data, resize_factor)
308
+ self.logger.debug("Center removed from DEM data.")
266
309
  self.logger.debug(
267
310
  "DEM data resized to shape: %s with factor: %s", dem_data.shape, resize_factor
268
311
  )
@@ -297,7 +340,10 @@ class Background(Component):
297
340
  bottom_left = top_left + cols
298
341
  bottom_right = bottom_left + 1
299
342
 
300
- if ground in [z[i, j], z[i, j + 1], z[i + 1, j], z[i + 1, j + 1]]:
343
+ if (
344
+ ground in [z[i, j], z[i, j + 1], z[i + 1, j], z[i + 1, j + 1]]
345
+ and not include_zeros
346
+ ):
301
347
  skipped += 1
302
348
  continue
303
349
 
@@ -315,25 +361,37 @@ class Background(Component):
315
361
  mesh.apply_transform(rotation_matrix_y)
316
362
  mesh.apply_transform(rotation_matrix_z)
317
363
 
318
- if is_preview:
364
+ # if not include_zeros:
365
+ z_scaling_factor = self.get_z_scaling_factor()
366
+ self.logger.debug("Z scaling factor: %s", z_scaling_factor)
367
+ mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
368
+
369
+ old_faces = len(mesh.faces)
370
+ self.logger.debug("Mesh generated with %s faces.", old_faces)
371
+
372
+ if self.map.background_settings.apply_decimation:
373
+ percent = self.map.background_settings.decimation_percent / 100
374
+ mesh = mesh.simplify_quadric_decimation(
375
+ percent=percent, aggression=self.map.background_settings.decimation_agression
376
+ )
377
+
378
+ new_faces = len(mesh.faces)
379
+ decimation_percent = (old_faces - new_faces) / old_faces * 100
380
+
381
+ self.logger.debug(
382
+ "Mesh simplified to %s faces. Decimation percent: %s", new_faces, decimation_percent
383
+ )
384
+
385
+ mesh.export(save_path)
386
+ self.logger.debug("Obj file saved: %s", save_path)
387
+
388
+ if create_preview:
319
389
  # Simplify the preview mesh to reduce the size of the file.
320
- mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
390
+ # mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
321
391
 
322
392
  # Apply scale to make the preview mesh smaller in the UI.
323
393
  mesh.apply_scale([0.5, 0.5, 0.5])
324
394
  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
395
 
338
396
  def mesh_to_stl(self, mesh: trimesh.Trimesh) -> None:
339
397
  """Converts the mesh to an STL file and saves it in the previews directory.
@@ -358,25 +416,22 @@ class Background(Component):
358
416
  list[str] -- A list of paths to the previews.
359
417
  """
360
418
  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)
419
+
420
+ background_dem_preview_path = os.path.join(self.previews_directory, "background_dem.png")
421
+ background_dem_preview_image = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED)
422
+
423
+ background_dem_preview_image = cv2.resize(
424
+ background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
425
+ )
426
+ background_dem_preview_image = cv2.normalize( # type: ignore
427
+ background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
428
+ )
429
+ background_dem_preview_image = cv2.cvtColor(
430
+ background_dem_preview_image, cv2.COLOR_GRAY2BGR
431
+ )
432
+
433
+ cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
434
+ preview_paths.append(background_dem_preview_path)
380
435
 
381
436
  if self.stl_preview_path:
382
437
  preview_paths.append(self.stl_preview_path)
@@ -525,18 +580,15 @@ class Background(Component):
525
580
  bool
526
581
  )
527
582
 
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)
583
+ dem_image = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
532
584
 
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
585
+ # Create a mask where water_resources_image is 255 (or not 0)
586
+ # Subtract water_depth from dem_image where mask is True
587
+ dem_image[mask] = dem_image[mask] - self.map.dem_settings.water_depth
536
588
 
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)
589
+ # Save the modified dem_image back to the output path
590
+ cv2.imwrite(self.output_path, dem_image)
591
+ self.logger.debug("Water depth subtracted from DEM data: %s", self.output_path)
540
592
 
541
593
  def generate_water_resources_obj(self) -> None:
542
594
  """Generates 3D obj files based on water resources data."""
@@ -550,9 +602,7 @@ class Background(Component):
550
602
  plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
551
603
  ).astype(np.uint8)
552
604
  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
- )
605
+ self.plane_from_np(dilated_plane_water, plane_save_path, include_zeros=False)
556
606
 
557
607
  # Single channeled 16 bit DEM image of terrain.
558
608
  background_dem = cv2.imread(self.not_substracted_path, cv2.IMREAD_UNCHANGED)
@@ -570,6 +620,4 @@ class Background(Component):
570
620
  elevated_water = np.where(mask, background_dem, elevated_water)
571
621
  elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
572
622
 
573
- self.plane_from_np(
574
- elevated_water, elevated_save_path, is_preview=False, include_zeros=False
575
- )
623
+ self.plane_from_np(elevated_water, elevated_save_path, include_zeros=False)
@@ -58,7 +58,7 @@ class Component:
58
58
  self.logger = logger
59
59
  self.kwargs = kwargs
60
60
 
61
- self.logger.info(
61
+ self.logger.debug(
62
62
  "Component %s initialized. Map size: %s, map rotated size: %s", # type: ignore
63
63
  self.__class__.__name__,
64
64
  self.map_size,
@@ -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.
@@ -525,3 +535,26 @@ class Component:
525
535
  interpolated_polyline.append(polyline[-1])
526
536
 
527
537
  return interpolated_polyline
538
+
539
+ def get_z_scaling_factor(self) -> float:
540
+ """Calculates the scaling factor for the Z axis based on the map settings.
541
+
542
+ Returns:
543
+ float -- The scaling factor for the Z axis.
544
+ """
545
+
546
+ scaling_factor = 1 / self.map.dem_settings.multiplier
547
+ self.logger.debug("Z scaling factor including DEM multiplier: %s", scaling_factor)
548
+
549
+ if self.map.shared_settings.height_scale_multiplier:
550
+ scaling_factor *= self.map.shared_settings.height_scale_multiplier
551
+ self.logger.debug(
552
+ "Z scaling factor including height scale multiplier: %s", scaling_factor
553
+ )
554
+ if self.map.shared_settings.mesh_z_scaling_factor:
555
+ scaling_factor *= 1 / self.map.shared_settings.mesh_z_scaling_factor
556
+ self.logger.debug(
557
+ "Z scaling factor including mesh z scaling factor: %s", scaling_factor
558
+ )
559
+
560
+ return scaling_factor