maps4fs 1.2.3__py3-none-any.whl → 1.4.1__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.

Potentially problematic release.


This version of maps4fs might be problematic. Click here for more details.

maps4fs/__init__.py CHANGED
@@ -1,4 +1,13 @@
1
1
  # pylint: disable=missing-module-docstring
2
2
  from maps4fs.generator.game import Game
3
- from maps4fs.generator.map import Map
3
+ from maps4fs.generator.map import (
4
+ BackgroundSettings,
5
+ DEMSettings,
6
+ GRLESettings,
7
+ I3DSettings,
8
+ Map,
9
+ SettingsModel,
10
+ SplineSettings,
11
+ TextureSettings,
12
+ )
4
13
  from maps4fs.logger import Logger
@@ -3,28 +3,26 @@ around the map."""
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import json
6
7
  import os
7
8
  import shutil
9
+ from copy import deepcopy
8
10
 
9
11
  import cv2
10
12
  import numpy as np
11
13
  import trimesh # type: ignore
12
14
 
13
15
  from maps4fs.generator.component import Component
14
- from maps4fs.generator.dem import (
15
- DEFAULT_BLUR_RADIUS,
16
- DEFAULT_MULTIPLIER,
17
- DEFAULT_PLATEAU,
18
- DEM,
19
- )
16
+ from maps4fs.generator.dem import DEM
17
+ from maps4fs.generator.texture import Texture
20
18
 
21
19
  DEFAULT_DISTANCE = 2048
22
- RESIZE_FACTOR = 1 / 8
23
20
  FULL_NAME = "FULL"
24
21
  FULL_PREVIEW_NAME = "PREVIEW"
25
22
  ELEMENTS = [FULL_NAME, FULL_PREVIEW_NAME]
26
23
 
27
24
 
25
+ # pylint: disable=R0902
28
26
  class Background(Component):
29
27
  """Component for creating 3D obj files based on DEM data around the map.
30
28
 
@@ -42,8 +40,8 @@ class Background(Component):
42
40
  # pylint: disable=R0801
43
41
  def preprocess(self) -> None:
44
42
  """Registers the DEMs for the background terrain."""
45
- self.light_version = self.kwargs.get("light_version", False)
46
43
  self.stl_preview_path: str | None = None
44
+ self.water_resources_path: str | None = None
47
45
 
48
46
  if self.rotation:
49
47
  self.logger.debug("Rotation is enabled: %s.", self.rotation)
@@ -51,34 +49,41 @@ class Background(Component):
51
49
  else:
52
50
  output_size_multiplier = 1
53
51
 
54
- background_size = self.map_size + DEFAULT_DISTANCE * 2
55
- rotated_size = int(background_size * output_size_multiplier)
52
+ self.background_size = self.map_size + DEFAULT_DISTANCE * 2
53
+ self.rotated_size = int(self.background_size * output_size_multiplier)
56
54
 
57
55
  self.background_directory = os.path.join(self.map_directory, "background")
56
+ self.water_directory = os.path.join(self.map_directory, "water")
58
57
  os.makedirs(self.background_directory, exist_ok=True)
58
+ os.makedirs(self.water_directory, exist_ok=True)
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
+ ]
64
+ self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
65
+ self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
59
66
 
60
- autoprocesses = [self.kwargs.get("auto_process", False), False]
61
67
  dems = []
62
68
 
63
- for name, autoprocess in zip(ELEMENTS, autoprocesses):
69
+ for name, autoprocess, output_path in zip(ELEMENTS, autoprocesses, self.output_paths):
64
70
  dem = DEM(
65
71
  self.game,
66
72
  self.map,
67
73
  self.coordinates,
68
- background_size,
69
- rotated_size,
74
+ self.background_size,
75
+ self.rotated_size,
70
76
  self.rotation,
71
77
  self.map_directory,
72
78
  self.logger,
73
- auto_process=autoprocess,
74
- blur_radius=self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS),
75
- multiplier=self.kwargs.get("multiplier", DEFAULT_MULTIPLIER),
76
- plateau=self.kwargs.get("plateau", DEFAULT_PLATEAU),
77
79
  )
78
80
  dem.preprocess()
79
81
  dem.is_preview = self.is_preview(name) # type: ignore
80
- dem.set_output_resolution((rotated_size, rotated_size))
81
- dem.set_dem_path(os.path.join(self.background_directory, f"{name}.png"))
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)
82
87
  dems.append(dem)
83
88
 
84
89
  self.dems = dems
@@ -98,17 +103,27 @@ class Background(Component):
98
103
  """Launches the component processing. Iterates over all tiles and processes them
99
104
  as a result the DEM files will be saved, then based on them the obj files will be
100
105
  generated."""
106
+ self.create_background_textures()
107
+
101
108
  for dem in self.dems:
102
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)
113
+
114
+ if self.map.dem_settings.water_depth:
115
+ self.subtraction()
116
+
117
+ for dem in self.dems:
103
118
  if not dem.is_preview: # type: ignore
104
119
  cutted_dem_path = self.cutout(dem.dem_path)
105
120
  if self.game.additional_dem_name is not None:
106
121
  self.make_copy(cutted_dem_path, self.game.additional_dem_name)
107
122
 
108
- if not self.light_version:
123
+ if self.map.background_settings.generate_background:
109
124
  self.generate_obj_files()
110
- else:
111
- self.logger.info("Light version is enabled, obj files will not be generated.")
125
+ if self.map.background_settings.generate_water:
126
+ self.generate_water_resources_obj()
112
127
 
113
128
  def make_copy(self, dem_path: str, dem_name: str) -> None:
114
129
  """Copies DEM data to additional DEM file.
@@ -122,7 +137,7 @@ class Background(Component):
122
137
  additional_dem_path = os.path.join(dem_directory, dem_name)
123
138
 
124
139
  shutil.copyfile(dem_path, additional_dem_path)
125
- self.logger.info("Additional DEM data was copied to %s.", additional_dem_path)
140
+ self.logger.debug("Additional DEM data was copied to %s.", additional_dem_path)
126
141
 
127
142
  def info_sequence(self) -> dict[str, str | float | int]:
128
143
  """Returns a dictionary with information about the background terrain.
@@ -184,11 +199,12 @@ class Background(Component):
184
199
  self.plane_from_np(dem_data, save_path, is_preview=dem.is_preview) # type: ignore
185
200
 
186
201
  # pylint: disable=too-many-locals
187
- def cutout(self, dem_path: str) -> str:
202
+ def cutout(self, dem_path: str, save_path: str | None = None) -> str:
188
203
  """Cuts out the center of the DEM (the actual map) and saves it as a separate file.
189
204
 
190
205
  Arguments:
191
206
  dem_path (str): The path to the DEM file.
207
+ save_path (str, optional): The path where the cutout DEM file will be saved.
192
208
 
193
209
  Returns:
194
210
  str -- The path to the cutout DEM file.
@@ -203,6 +219,11 @@ class Background(Component):
203
219
  y2 = center[1] + half_size
204
220
  dem_data = dem_data[x1:x2, y1:y2]
205
221
 
222
+ if save_path:
223
+ cv2.imwrite(save_path, dem_data) # pylint: disable=no-member
224
+ self.logger.debug("Not resized DEM saved: %s", save_path)
225
+ return save_path
226
+
206
227
  output_size = self.map_size + 1
207
228
 
208
229
  main_dem_path = self.game.dem_file_path(self.map_directory)
@@ -223,19 +244,27 @@ class Background(Component):
223
244
  return main_dem_path
224
245
 
225
246
  # pylint: disable=too-many-locals
226
- def plane_from_np(self, dem_data: np.ndarray, save_path: str, is_preview: bool = False) -> None:
247
+ def plane_from_np(
248
+ self,
249
+ dem_data: np.ndarray,
250
+ save_path: str,
251
+ is_preview: bool = False,
252
+ include_zeros: bool = True,
253
+ ) -> None:
227
254
  """Generates a 3D obj file based on DEM data.
228
255
 
229
256
  Arguments:
230
257
  dem_data (np.ndarray) -- The DEM data as a numpy array.
231
258
  save_path (str) -- The path where the obj file will be saved.
232
259
  is_preview (bool, optional) -- If True, the preview mesh will be generated.
260
+ include_zeros (bool, optional) -- If True, the mesh will include the zero height values.
233
261
  """
262
+ resize_factor = 1 / self.map.background_settings.resize_factor
234
263
  dem_data = cv2.resize( # pylint: disable=no-member
235
- dem_data, (0, 0), fx=RESIZE_FACTOR, fy=RESIZE_FACTOR
264
+ dem_data, (0, 0), fx=resize_factor, fy=resize_factor
236
265
  )
237
266
  self.logger.debug(
238
- "DEM data resized to shape: %s with factor: %s", dem_data.shape, RESIZE_FACTOR
267
+ "DEM data resized to shape: %s with factor: %s", dem_data.shape, resize_factor
239
268
  )
240
269
 
241
270
  # Invert the height values.
@@ -247,6 +276,9 @@ class Background(Component):
247
276
  x, y = np.meshgrid(x, y)
248
277
  z = dem_data
249
278
 
279
+ ground = z.max()
280
+ self.logger.debug("Ground level: %s", ground)
281
+
250
282
  self.logger.debug(
251
283
  "Starting to generate a mesh for with shape: %s x %s. This may take a while...",
252
284
  cols,
@@ -256,6 +288,8 @@ class Background(Component):
256
288
  vertices = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
257
289
  faces = []
258
290
 
291
+ skipped = 0
292
+
259
293
  for i in range(rows - 1):
260
294
  for j in range(cols - 1):
261
295
  top_left = i * cols + j
@@ -263,9 +297,15 @@ class Background(Component):
263
297
  bottom_left = top_left + cols
264
298
  bottom_right = bottom_left + 1
265
299
 
300
+ if ground in [z[i, j], z[i, j + 1], z[i + 1, j], z[i + 1, j + 1]]:
301
+ skipped += 1
302
+ continue
303
+
266
304
  faces.append([top_left, bottom_left, bottom_right])
267
305
  faces.append([top_left, bottom_right, top_right])
268
306
 
307
+ self.logger.debug("Skipped faces: %s", skipped)
308
+
269
309
  faces = np.array(faces) # type: ignore
270
310
  mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
271
311
 
@@ -283,13 +323,14 @@ class Background(Component):
283
323
  mesh.apply_scale([0.5, 0.5, 0.5])
284
324
  self.mesh_to_stl(mesh)
285
325
  else:
286
- multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
287
- if multiplier != 1:
288
- z_scaling_factor = 1 / multiplier
289
- else:
290
- z_scaling_factor = 1 / 2**5
291
- self.logger.debug("Z scaling factor: %s", z_scaling_factor)
292
- mesh.apply_scale([1 / RESIZE_FACTOR, 1 / RESIZE_FACTOR, z_scaling_factor])
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])
293
334
 
294
335
  mesh.export(save_path)
295
336
  self.logger.debug("Obj file saved: %s", save_path)
@@ -305,7 +346,7 @@ class Background(Component):
305
346
  preview_path = os.path.join(self.previews_directory, "background_dem.stl")
306
347
  mesh.export(preview_path)
307
348
 
308
- self.logger.info("STL file saved: %s", preview_path)
349
+ self.logger.debug("STL file saved: %s", preview_path)
309
350
 
310
351
  self.stl_preview_path = preview_path # pylint: disable=attribute-defined-outside-init
311
352
 
@@ -413,3 +454,122 @@ class Background(Component):
413
454
 
414
455
  cv2.imwrite(colored_dem_path, dem_data_colored)
415
456
  return colored_dem_path
457
+
458
+ def create_background_textures(self) -> None:
459
+ """Creates background textures for the map."""
460
+ if not os.path.isfile(self.game.texture_schema):
461
+ self.logger.warning("Texture schema file not found: %s", self.game.texture_schema)
462
+ return
463
+
464
+ with open(self.game.texture_schema, "r", encoding="utf-8") as f:
465
+ layers_schema = json.load(f)
466
+
467
+ background_layers = []
468
+ for layer in layers_schema:
469
+ if layer.get("background") is True:
470
+ layer_copy = deepcopy(layer)
471
+ layer_copy["count"] = 1
472
+ layer_copy["name"] = f"{layer['name']}_background"
473
+ background_layers.append(layer_copy)
474
+
475
+ if not background_layers:
476
+ return
477
+
478
+ self.background_texture = Texture( # pylint: disable=W0201
479
+ self.game,
480
+ self.map,
481
+ self.coordinates,
482
+ self.background_size,
483
+ self.rotated_size,
484
+ rotation=self.rotation,
485
+ map_directory=self.map_directory,
486
+ logger=self.logger,
487
+ texture_custom_schema=background_layers, # type: ignore
488
+ )
489
+
490
+ self.background_texture.preprocess()
491
+ self.background_texture.process()
492
+
493
+ processed_layers = self.background_texture.get_background_layers()
494
+ weights_directory = self.game.weights_dir_path(self.map_directory)
495
+ background_paths = [layer.path(weights_directory) for layer in processed_layers]
496
+ self.logger.debug("Found %s background textures.", len(background_paths))
497
+
498
+ if not background_paths:
499
+ self.logger.warning("No background textures found.")
500
+ return
501
+
502
+ # Merge all images into one.
503
+ background_image = np.zeros((self.background_size, self.background_size), dtype=np.uint8)
504
+ for path in background_paths:
505
+ layer = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
506
+ background_image = cv2.add(background_image, layer) # type: ignore
507
+
508
+ background_save_path = os.path.join(self.water_directory, "water_resources.png")
509
+ cv2.imwrite(background_save_path, background_image)
510
+ self.logger.debug("Background texture saved: %s", background_save_path)
511
+ self.water_resources_path = background_save_path # pylint: disable=W0201
512
+
513
+ def subtraction(self) -> None:
514
+ """Subtracts the water depth from the DEM data where the water resources are located."""
515
+ if not self.water_resources_path:
516
+ self.logger.warning("Water resources texture not found.")
517
+ return
518
+
519
+ # Single channeled 8 bit image, where the water have values of 255, and the rest 0.
520
+ water_resources_image = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
521
+ mask = water_resources_image == 255
522
+
523
+ # Make mask a little bit smaller (1 pixel).
524
+ mask = cv2.erode(mask.astype(np.uint8), np.ones((3, 3), np.uint8), iterations=1).astype(
525
+ bool
526
+ )
527
+
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)
532
+
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
536
+
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)
540
+
541
+ def generate_water_resources_obj(self) -> None:
542
+ """Generates 3D obj files based on water resources data."""
543
+ if not self.water_resources_path:
544
+ self.logger.warning("Water resources texture not found.")
545
+ return
546
+
547
+ # Single channeled 8 bit image, where the water have values of 255, and the rest 0.
548
+ plane_water = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
549
+ dilated_plane_water = cv2.dilate(
550
+ plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
551
+ ).astype(np.uint8)
552
+ 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
+ )
556
+
557
+ # Single channeled 16 bit DEM image of terrain.
558
+ background_dem = cv2.imread(self.not_substracted_path, cv2.IMREAD_UNCHANGED)
559
+
560
+ # Remove all the values from the background dem where the plane_water is 0.
561
+ background_dem[plane_water == 0] = 0
562
+
563
+ # Dilate the background dem to make the water more smooth.
564
+ elevated_water = cv2.dilate(background_dem, np.ones((3, 3), np.uint16), iterations=10)
565
+
566
+ # Use the background dem as a mask to prevent the original values from being overwritten.
567
+ mask = background_dem > 0
568
+
569
+ # Combine the dilated background dem with non-dilated background dem.
570
+ elevated_water = np.where(mask, background_dem, elevated_water)
571
+ elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
572
+
573
+ self.plane_from_np(
574
+ elevated_water, elevated_save_path, is_preview=False, include_zeros=False
575
+ )
@@ -7,11 +7,11 @@ import os
7
7
  from copy import deepcopy
8
8
  from typing import TYPE_CHECKING, Any
9
9
 
10
- import cv2
10
+ import cv2 # type: ignore
11
11
  import osmnx as ox # type: ignore
12
12
  from pyproj import Transformer
13
13
  from shapely.affinity import rotate, translate # type: ignore
14
- from shapely.geometry import Polygon, box # type: ignore
14
+ from shapely.geometry import LineString, Polygon, box # type: ignore
15
15
 
16
16
  from maps4fs.generator.qgis import save_scripts
17
17
 
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
20
20
  from maps4fs.generator.map import Map
21
21
 
22
22
 
23
- # pylint: disable=R0801, R0903, R0902, R0904
23
+ # pylint: disable=R0801, R0903, R0902, R0904, R0913, R0917
24
24
  class Component:
25
25
  """Base class for all map generation components.
26
26
 
@@ -46,7 +46,7 @@ class Component:
46
46
  rotation: int,
47
47
  map_directory: str,
48
48
  logger: Any = None,
49
- **kwargs, # pylint: disable=W0613, R0913, R0917
49
+ **kwargs: dict[str, Any],
50
50
  ):
51
51
  self.game = game
52
52
  self.map = map
@@ -58,6 +58,13 @@ class Component:
58
58
  self.logger = logger
59
59
  self.kwargs = kwargs
60
60
 
61
+ self.logger.info(
62
+ "Component %s initialized. Map size: %s, map rotated size: %s", # type: ignore
63
+ self.__class__.__name__,
64
+ self.map_size,
65
+ self.map_rotated_size,
66
+ )
67
+
61
68
  os.makedirs(self.previews_directory, exist_ok=True)
62
69
  os.makedirs(self.scripts_directory, exist_ok=True)
63
70
  os.makedirs(self.info_layers_directory, exist_ok=True)
@@ -330,57 +337,80 @@ class Component:
330
337
 
331
338
  return cs_x, cs_y
332
339
 
333
- def fit_polygon_into_bounds(
334
- self, polygon_points: list[tuple[int, int]], margin: int = 0, angle: int = 0
340
+ # pylint: disable=R0914
341
+ def fit_object_into_bounds(
342
+ self,
343
+ polygon_points: list[tuple[int, int]] | None = None,
344
+ linestring_points: list[tuple[int, int]] | None = None,
345
+ margin: int = 0,
346
+ angle: int = 0,
335
347
  ) -> list[tuple[int, int]]:
336
348
  """Fits a polygon into the bounds of the map.
337
349
 
338
350
  Arguments:
339
351
  polygon_points (list[tuple[int, int]]): The points of the polygon.
352
+ linestring_points (list[tuple[int, int]]): The points of the linestring.
340
353
  margin (int, optional): The margin to add to the polygon. Defaults to 0.
341
354
  angle (int, optional): The angle to rotate the polygon by. Defaults to 0.
342
355
 
343
356
  Returns:
344
357
  list[tuple[int, int]]: The points of the polygon fitted into the map bounds.
345
358
  """
359
+ if polygon_points is None and linestring_points is None:
360
+ raise ValueError("Either polygon or linestring points must be provided.")
361
+
346
362
  min_x = min_y = 0
347
363
  max_x = max_y = self.map_size
348
364
 
349
- polygon = Polygon(polygon_points)
365
+ object_type = Polygon if polygon_points else LineString
366
+
367
+ # polygon = Polygon(polygon_points)
368
+ osm_object = object_type(polygon_points or linestring_points)
350
369
 
351
370
  if angle:
352
371
  center_x = center_y = self.map_rotated_size // 2
353
372
  self.logger.debug(
354
- "Rotating the polygon by %s degrees with center at %sx%s",
373
+ "Rotating the osm_object by %s degrees with center at %sx%s",
355
374
  angle,
356
375
  center_x,
357
376
  center_y,
358
377
  )
359
- polygon = rotate(polygon, -angle, origin=(center_x, center_y))
378
+ osm_object = rotate(osm_object, -angle, origin=(center_x, center_y))
360
379
  offset = (self.map_size / 2) - (self.map_rotated_size / 2)
361
- self.logger.debug("Translating the polygon by %s", offset)
362
- polygon = translate(polygon, xoff=offset, yoff=offset)
363
- self.logger.debug("Rotated and translated polygon.")
380
+ self.logger.debug("Translating the osm_object by %s", offset)
381
+ osm_object = translate(osm_object, xoff=offset, yoff=offset)
382
+ self.logger.debug("Rotated and translated the osm_object.")
364
383
 
365
- if margin:
366
- polygon = polygon.buffer(margin, join_style="mitre")
367
- if polygon.is_empty:
368
- raise ValueError("The polygon is empty after adding the margin.")
384
+ if margin and object_type is Polygon:
385
+ osm_object = osm_object.buffer(margin, join_style="mitre")
386
+ if osm_object.is_empty:
387
+ raise ValueError("The osm_object is empty after adding the margin.")
369
388
 
370
389
  # Create a bounding box for the map bounds
371
390
  bounds = box(min_x, min_y, max_x, max_y)
372
391
 
373
- # Intersect the polygon with the bounds to fit it within the map
374
- fitted_polygon = polygon.intersection(bounds)
375
- self.logger.debug("Fitted the polygon into the bounds: %s", bounds)
392
+ # Intersect the osm_object with the bounds to fit it within the map
393
+ try:
394
+ fitted_osm_object = osm_object.intersection(bounds)
395
+ self.logger.debug("Fitted the osm_object into the bounds: %s", bounds)
396
+ except Exception as e:
397
+ raise ValueError( # pylint: disable=W0707
398
+ f"Could not fit the osm_object into the bounds: {e}"
399
+ )
376
400
 
377
- if not isinstance(fitted_polygon, Polygon):
378
- raise ValueError("The fitted polygon is not a valid polygon.")
401
+ if not isinstance(fitted_osm_object, object_type):
402
+ raise ValueError("The fitted osm_object is not valid (probably splitted into parts).")
379
403
 
380
404
  # Return the fitted polygon points
381
- as_list = list(fitted_polygon.exterior.coords)
405
+ if object_type is Polygon:
406
+ as_list = list(fitted_osm_object.exterior.coords)
407
+ elif object_type is LineString:
408
+ as_list = list(fitted_osm_object.coords)
409
+ else:
410
+ raise ValueError("The object type is not supported.")
411
+
382
412
  if not as_list:
383
- raise ValueError("The fitted polygon has no points.")
413
+ raise ValueError("The fitted osm_object has no points.")
384
414
  return as_list
385
415
 
386
416
  def get_infolayer_path(self, layer_name: str) -> str | None:
@@ -463,3 +493,35 @@ class Component:
463
493
  self.logger.debug("Shape of the cropped image: %s", cropped.shape)
464
494
 
465
495
  cv2.imwrite(output_path, cropped)
496
+
497
+ @staticmethod
498
+ def interpolate_points(
499
+ polyline: list[tuple[int, int]], num_points: int = 4
500
+ ) -> list[tuple[int, int]]:
501
+ """Receives a list of tuples, which represents a polyline. Add additional points
502
+ between the existing points to make the polyline smoother.
503
+
504
+ Arguments:
505
+ polyline (list[tuple[int, int]]): The list of points to interpolate.
506
+ num_points (int): The number of additional points to add between each pair of points.
507
+
508
+ Returns:
509
+ list[tuple[int, int]]: The list of points with additional points.
510
+ """
511
+ if not polyline or num_points < 1:
512
+ return polyline
513
+
514
+ interpolated_polyline = []
515
+ for i in range(len(polyline) - 1):
516
+ p1 = polyline[i]
517
+ p2 = polyline[i + 1]
518
+ interpolated_polyline.append(p1)
519
+ for j in range(1, num_points + 1):
520
+ new_point = (
521
+ p1[0] + (p2[0] - p1[0]) * j / (num_points + 1),
522
+ p1[1] + (p2[1] - p1[1]) * j / (num_points + 1),
523
+ )
524
+ interpolated_polyline.append((int(new_point[0]), int(new_point[1])))
525
+ interpolated_polyline.append(polyline[-1])
526
+
527
+ return interpolated_polyline
@@ -38,7 +38,7 @@ class Config(Component):
38
38
  self.logger.warning("Map XML file not found: %s.", self._map_xml_path)
39
39
  return
40
40
  tree = ET.parse(self._map_xml_path)
41
- self.logger.info("Map XML file loaded from: %s.", self._map_xml_path)
41
+ self.logger.debug("Map XML file loaded from: %s.", self._map_xml_path)
42
42
  root = tree.getroot()
43
43
  for map_elem in root.iter("map"):
44
44
  map_elem.set("width", str(self.map_size))
maps4fs/generator/dem.py CHANGED
@@ -14,9 +14,6 @@ from pympler import asizeof # type: ignore
14
14
  from maps4fs.generator.component import Component
15
15
 
16
16
  SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
17
- DEFAULT_MULTIPLIER = 1
18
- DEFAULT_BLUR_RADIUS = 35
19
- DEFAULT_PLATEAU = 0
20
17
 
21
18
 
22
19
  # pylint: disable=R0903, R0902
@@ -50,20 +47,21 @@ class DEM(Component):
50
47
  self.output_resolution = self.get_output_resolution()
51
48
  self.logger.debug("Output resolution for DEM data: %s.", self.output_resolution)
52
49
 
53
- self.multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
54
- blur_radius = self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS)
50
+ blur_radius = self.map.dem_settings.blur_radius
55
51
  if blur_radius is None or blur_radius <= 0:
56
52
  # We'll disable blur if the radius is 0 or negative.
57
53
  blur_radius = 0
58
54
  elif blur_radius % 2 == 0:
59
55
  blur_radius += 1
60
56
  self.blur_radius = blur_radius
57
+ self.multiplier = self.map.dem_settings.multiplier
61
58
  self.logger.debug(
62
- "DEM value multiplier is %s, blur radius is %s.", self.multiplier, self.blur_radius
59
+ "DEM value multiplier is %s, blur radius is %s.",
60
+ self.multiplier,
61
+ self.blur_radius,
63
62
  )
64
63
 
65
- self.auto_process = self.kwargs.get("auto_process", False)
66
- self.plateau = self.kwargs.get("plateau", False)
64
+ self.auto_process = self.map.dem_settings.auto_process
67
65
 
68
66
  @property
69
67
  def dem_path(self) -> str:
@@ -240,24 +238,24 @@ class DEM(Component):
240
238
  resampled_data.max(),
241
239
  )
242
240
 
243
- if self.plateau:
241
+ if self.map.dem_settings.plateau:
244
242
  # Plateau is a flat area with a constant height.
245
243
  # So we just add this value to each pixel of the DEM.
246
244
  # And also need to ensure that there will be no values with height greater than
247
245
  # it's allowed in 16-bit unsigned integer.
248
246
 
249
- resampled_data += self.plateau
247
+ resampled_data += self.map.dem_settings.plateau
250
248
  resampled_data = np.clip(resampled_data, 0, 65535)
251
249
 
252
250
  self.logger.debug(
253
251
  "Plateau with height %s was added to DEM data. Min: %s, max: %s.",
254
- self.plateau,
252
+ self.map.dem_settings.plateau,
255
253
  resampled_data.min(),
256
254
  resampled_data.max(),
257
255
  )
258
256
 
259
257
  cv2.imwrite(self._dem_path, resampled_data)
260
- self.logger.info("DEM data was saved to %s.", self._dem_path)
258
+ self.logger.debug("DEM data was saved to %s.", self._dem_path)
261
259
 
262
260
  if self.rotation:
263
261
  self.rotate_dem()
@@ -403,7 +401,7 @@ class DEM(Component):
403
401
 
404
402
  scaling_factor = self._get_scaling_factor(max_dev)
405
403
  adjusted_max_height = int(65535 * scaling_factor)
406
- self.logger.info(
404
+ self.logger.debug(
407
405
  "Maximum deviation: %s. Scaling factor: %s. Adjusted max height: %s.",
408
406
  max_dev,
409
407
  scaling_factor,
maps4fs/generator/game.py CHANGED
@@ -39,7 +39,7 @@ class Game:
39
39
  _tree_schema: str | None = None
40
40
 
41
41
  # Order matters! Some components depend on others.
42
- components = [Texture, I3d, GRLE, Background, Config]
42
+ components = [Texture, GRLE, Background, I3d, Config]
43
43
 
44
44
  def __init__(self, map_template_path: str | None = None):
45
45
  if map_template_path: