maps4fs 2.8.9__py3-none-any.whl → 2.9.37__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.
@@ -10,6 +10,7 @@ import numpy as np
10
10
  import maps4fs.generator.utils as mfsutils
11
11
  from maps4fs.generator.component.base.component_image import ImageComponent
12
12
  from maps4fs.generator.component.base.component_xml import XMLComponent
13
+ from maps4fs.generator.monitor import monitor_performance
13
14
  from maps4fs.generator.settings import Parameters
14
15
 
15
16
  # Defines coordinates for country block on the license plate texture.
@@ -120,6 +121,7 @@ class Config(XMLComponent, ImageComponent):
120
121
 
121
122
  self.create_qgis_scripts(layers)
122
123
 
124
+ @monitor_performance
123
125
  def _adjust_fog(self) -> None:
124
126
  """Adjusts the fog settings in the environment XML file based on the DEM and height scale."""
125
127
  self.logger.debug("Adjusting fog settings based on DEM and height scale...")
@@ -231,6 +233,7 @@ class Config(XMLComponent, ImageComponent):
231
233
 
232
234
  return dem_maximum_meter, dem_minimum_meter
233
235
 
236
+ @monitor_performance
234
237
  def _set_overview(self) -> None:
235
238
  """Generates and sets the overview image for the map."""
236
239
  try:
@@ -347,6 +350,7 @@ class Config(XMLComponent, ImageComponent):
347
350
  "HR",
348
351
  }
349
352
 
353
+ @monitor_performance
350
354
  def update_license_plates(self):
351
355
  """Updates license plates for the specified country."""
352
356
  try:
@@ -565,6 +569,7 @@ class Config(XMLComponent, ImageComponent):
565
569
  self.save_tree(tree, xml_path=i3d_path)
566
570
  self.logger.debug("Updated licensePlatesPL.i3d texture reference to: %s", filename)
567
571
 
572
+ @monitor_performance
568
573
  def _generate_license_plate_texture(
569
574
  self,
570
575
  license_plates_directory: str,
@@ -9,6 +9,7 @@ from pydtmdl import DTMProvider
9
9
 
10
10
  import maps4fs.generator.config as mfscfg
11
11
  from maps4fs.generator.component.base.component_image import ImageComponent
12
+ from maps4fs.generator.monitor import monitor_performance
12
13
 
13
14
 
14
15
  # pylint: disable=R0903, R0902
@@ -115,6 +116,7 @@ class DEM(ImageComponent):
115
116
  except Exception as e:
116
117
  self.logger.warning("Failed to update DEM info: %s.", e)
117
118
 
119
+ @monitor_performance
118
120
  def process(self) -> None:
119
121
  """Reads DTM file, crops it to map size, normalizes and blurs it,
120
122
  saves to map directory."""
@@ -190,6 +192,7 @@ class DEM(ImageComponent):
190
192
  if self.rotation:
191
193
  self.rotate_dem()
192
194
 
195
+ @monitor_performance
193
196
  def normalize_data(self, data: np.ndarray, height_scale_value: int) -> np.ndarray:
194
197
  """Normalize DEM data to 16-bit unsigned integer range (0 to 65535).
195
198
 
@@ -209,6 +212,7 @@ class DEM(ImageComponent):
209
212
  )
210
213
  return normalized_data
211
214
 
215
+ @monitor_performance
212
216
  def determine_height_scale(self, data: np.ndarray) -> int:
213
217
  """Determine height scale value using ceiling.
214
218
 
@@ -261,6 +265,7 @@ class DEM(ImageComponent):
261
265
  )
262
266
  return data
263
267
 
268
+ @monitor_performance
264
269
  def apply_multiplier(self, data: np.ndarray) -> np.ndarray:
265
270
  """Apply multiplier to DEM data.
266
271
 
@@ -283,6 +288,7 @@ class DEM(ImageComponent):
283
288
  )
284
289
  return multiplied_data
285
290
 
291
+ @monitor_performance
286
292
  def resize_to_output(self, data: np.ndarray) -> np.ndarray:
287
293
  """Resize DEM data to the output resolution.
288
294
 
@@ -296,6 +302,7 @@ class DEM(ImageComponent):
296
302
 
297
303
  return resampled_data
298
304
 
305
+ @monitor_performance
299
306
  def rotate_dem(self) -> None:
300
307
  """Rotate DEM image."""
301
308
  self.logger.debug("Rotating DEM image by %s degrees.", self.rotation)
@@ -12,6 +12,7 @@ from tqdm import tqdm
12
12
  from maps4fs.generator.component.base.component_image import ImageComponent
13
13
  from maps4fs.generator.component.base.component_xml import XMLComponent
14
14
  from maps4fs.generator.component.layer import Layer
15
+ from maps4fs.generator.monitor import monitor_performance
15
16
  from maps4fs.generator.settings import Parameters
16
17
 
17
18
  # This value sums up the pixel value of the basic area type to convert it from "No Water" to "Near Water".
@@ -98,6 +99,7 @@ class GRLE(ImageComponent, XMLComponent):
98
99
 
99
100
  return grle_schema
100
101
 
102
+ @monitor_performance
101
103
  def process(self) -> None:
102
104
  """Generates InfoLayer PNG files based on the GRLE schema."""
103
105
  grle_schema = self._read_grle_schema()
@@ -134,6 +136,7 @@ class GRLE(ImageComponent, XMLComponent):
134
136
  self._process_environment()
135
137
  self._process_indoor()
136
138
 
139
+ @monitor_performance
137
140
  def previews(self) -> list[str]:
138
141
  """Returns a list of paths to the preview images (empty list).
139
142
  The component does not generate any preview images so it returns an empty list.
@@ -173,6 +176,7 @@ class GRLE(ImageComponent, XMLComponent):
173
176
 
174
177
  return preview_paths
175
178
 
179
+ @monitor_performance
176
180
  def overlay_fields(self, farmlands_np: np.ndarray) -> np.ndarray | None:
177
181
  """Overlay fields on the farmlands preview image.
178
182
 
@@ -200,6 +204,7 @@ class GRLE(ImageComponent, XMLComponent):
200
204
  # use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
201
205
  return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
202
206
 
207
+ @monitor_performance
203
208
  def _add_farmlands(self) -> None:
204
209
  """Adds farmlands to the InfoLayer PNG file."""
205
210
  farmlands = []
@@ -297,6 +302,7 @@ class GRLE(ImageComponent, XMLComponent):
297
302
 
298
303
  self.preview_paths["farmlands"] = info_layer_farmlands_path
299
304
 
305
+ @monitor_performance
300
306
  def _add_plants(self) -> None:
301
307
  """Adds plants to the InfoLayer PNG file."""
302
308
  grass_layer = self.map.get_texture_layer(by_usage="grass")
@@ -399,6 +405,7 @@ class GRLE(ImageComponent, XMLComponent):
399
405
 
400
406
  self.logger.debug("Updated density map for fruits saved in %s.", density_map_fruit_path)
401
407
 
408
+ @monitor_performance
402
409
  def create_island_of_plants(self, image: np.ndarray, count: int) -> np.ndarray:
403
410
  """Create an island of plants in the image.
404
411
 
@@ -495,6 +502,7 @@ class GRLE(ImageComponent, XMLComponent):
495
502
  image_np[:, -1] = 0 # Right side
496
503
  return image_np
497
504
 
505
+ @monitor_performance
498
506
  def _process_environment(self) -> None:
499
507
  info_layer_environment_path = self.game.get_environment_path(self.map_directory)
500
508
  if not info_layer_environment_path or not os.path.isfile(info_layer_environment_path):
@@ -534,6 +542,17 @@ class GRLE(ImageComponent, XMLComponent):
534
542
 
535
543
  for layer in texture_component.get_area_type_layers():
536
544
  pixel_value = area_type_to_pixel_value(layer.area_type) # type: ignore
545
+ # * Not enabled for now.
546
+ # * If the layer is invisible, we need to draw the mask from the info layer.
547
+ # if layer.invisible:
548
+ # self.logger.debug("Processing invisible area type layer: %s.", layer.name)
549
+ # if layer.info_layer:
550
+ # self.logger.debug("Info layer available: %s.", layer.info_layer)
551
+ # weight_image = self.draw_invisible_layer_mask(layer, environment_size)
552
+ # else:
553
+ # self.logger.debug("No info layer available for layer: %s.", layer.name)
554
+ # continue
555
+ # else:
537
556
  weight_image = self.get_resized_weight(layer, environment_size) # type: ignore
538
557
  if weight_image is None:
539
558
  self.logger.warning("Weight image for area type layer not found in %s.", layer.name)
@@ -554,6 +573,53 @@ class GRLE(ImageComponent, XMLComponent):
554
573
  self.logger.debug("Environment InfoLayer PNG file saved: %s.", info_layer_environment_path)
555
574
  self.preview_paths["environment"] = info_layer_environment_path
556
575
 
576
+ # def draw_invisible_layer_mask(self, layer: Layer, resize_to: int) -> np.ndarray:
577
+ # """Draw the mask for the invisible layer.
578
+
579
+ # Arguments:
580
+ # layer (Layer): The layer for which to draw the mask.
581
+ # resize_to (int): The size to which the mask should be resized.
582
+
583
+ # Returns:
584
+ # np.ndarray: The resized mask.
585
+ # """
586
+ # mask = np.zeros((self.map.size, self.map.size), dtype=np.uint8)
587
+ # polygons = self.get_infolayer_data(Parameters.TEXTURES, layer.info_layer)
588
+ # self.logger.debug("Found %d polygons in info layer %s.", len(polygons), layer.info_layer)
589
+
590
+ # for polygon in polygons:
591
+ # try:
592
+ # fitted_polygon = self.fit_object_into_bounds(
593
+ # polygon_points=polygon,
594
+ # # margin=self.map.grle_settings.farmland_margin,
595
+ # angle=self.rotation,
596
+ # )
597
+ # except ValueError as e:
598
+ # self.logger.debug(
599
+ # "Polygon could not be fitted into the map bounds with error: %s",
600
+ # e,
601
+ # )
602
+ # continue
603
+ # polygon_np = self.polygon_points_to_np(fitted_polygon)
604
+
605
+ # try:
606
+ # cv2.fillPoly(mask, [polygon_np], (float(255),)) # type: ignore
607
+ # except Exception as e:
608
+ # self.logger.debug(
609
+ # "Polygon could not be added to the mask with error: %s",
610
+ # e,
611
+ # )
612
+ # continue
613
+
614
+ # resized_mask = cv2.resize(
615
+ # mask,
616
+ # (resize_to, resize_to),
617
+ # interpolation=cv2.INTER_NEAREST,
618
+ # )
619
+
620
+ # return resized_mask
621
+
622
+ @monitor_performance
557
623
  def get_resized_weight(
558
624
  self, layer: Layer, resize_to: int, dilations: int = 3
559
625
  ) -> np.ndarray | None:
@@ -12,12 +12,14 @@ import cv2
12
12
  import numpy as np
13
13
  from tqdm import tqdm
14
14
 
15
+ from maps4fs.generator.component.base.component_image import ImageComponent
15
16
  from maps4fs.generator.component.base.component_xml import XMLComponent
17
+ from maps4fs.generator.monitor import monitor_performance
16
18
  from maps4fs.generator.settings import Parameters
17
19
 
18
20
  NODE_ID_STARTING_VALUE = 2000
19
21
  SPLINES_NODE_ID_STARTING_VALUE = 5000
20
- TREE_NODE_ID_STARTING_VALUE = 10000
22
+ TREE_NODE_ID_STARTING_VALUE = 30000
21
23
 
22
24
  FIELDS_ATTRIBUTES = [
23
25
  ("angle", "integer", "0"),
@@ -29,7 +31,7 @@ FIELDS_ATTRIBUTES = [
29
31
  ]
30
32
 
31
33
 
32
- class I3d(XMLComponent):
34
+ class I3d(XMLComponent, ImageComponent):
33
35
  """Component for map i3d file settings and configuration.
34
36
 
35
37
  Arguments:
@@ -112,6 +114,7 @@ class I3d(XMLComponent):
112
114
 
113
115
  self.save_tree(tree)
114
116
 
117
+ @monitor_performance
115
118
  def _add_splines(self) -> None:
116
119
  """Adds splines to the map I3D file."""
117
120
  splines_i3d_path = self.game.splines_file_path(self.map_directory)
@@ -240,6 +243,7 @@ class I3d(XMLComponent):
240
243
 
241
244
  self.assets.splines = splines_i3d_path
242
245
 
246
+ @monitor_performance
243
247
  def _add_fields(self) -> None:
244
248
  """Adds fields to the map I3D file."""
245
249
  tree = self.get_tree()
@@ -498,6 +502,7 @@ class I3d(XMLComponent):
498
502
 
499
503
  return choice(trees_by_leaf_type)
500
504
 
505
+ @monitor_performance
501
506
  def _add_forests(self) -> None:
502
507
  """Adds forests to the map I3D file."""
503
508
  tree_schema = self._read_tree_schema()
@@ -686,9 +691,13 @@ class I3d(XMLComponent):
686
691
 
687
692
  return recommended_step if not current_step else max(recommended_step, current_step)
688
693
 
689
- def get_not_resized_dem(self) -> np.ndarray | None:
694
+ def get_not_resized_dem(self, with_foundations: bool = False) -> np.ndarray | None:
690
695
  """Reads the not resized DEM image from the background component.
691
696
 
697
+ Arguments:
698
+ with_foundations (bool, optional): Whether to get the DEM with foundations.
699
+ Defaults to False.
700
+
692
701
  Returns:
693
702
  np.ndarray | None: The not resized DEM image or None if the image could not be read.
694
703
  """
@@ -697,11 +706,60 @@ class I3d(XMLComponent):
697
706
  self.logger.warning("Background component not found.")
698
707
  return None
699
708
 
700
- if not background_component.not_resized_path:
709
+ dem_path = (
710
+ background_component.not_resized_with_foundations_path
711
+ if with_foundations
712
+ else background_component.not_resized_path
713
+ )
714
+
715
+ if not dem_path or not os.path.isfile(dem_path):
701
716
  self.logger.warning("Not resized DEM path not found.")
702
717
  return None
703
718
 
704
- not_resized_dem = cv2.imread(background_component.not_resized_path, cv2.IMREAD_UNCHANGED)
719
+ not_resized_dem = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED)
720
+
721
+ return not_resized_dem
722
+
723
+ def get_not_resized_dem_with_foundations(
724
+ self, allow_fallback: bool = False
725
+ ) -> np.ndarray | None:
726
+ """Gets the not resized DEM with foundations. If the DEM with foundations is not found
727
+ and allow_fallback is True, the method returns the not resized DEM without foundations.
728
+
729
+ Arguments:
730
+ allow_fallback (bool, optional): Whether to allow fallback to DEM without
731
+ foundations. Defaults to False.
732
+
733
+ Returns:
734
+ np.ndarray | None: The not resized DEM image or None if the image could not be read.
735
+ """
736
+ dem_with_foundations = self.get_not_resized_dem(with_foundations=True)
737
+
738
+ if dem_with_foundations is not None:
739
+ return dem_with_foundations
740
+ self.logger.warning("Not resized DEM with foundations not found.")
741
+ if allow_fallback:
742
+ return self.get_not_resized_dem(with_foundations=False)
743
+ return None
744
+
745
+ def get_not_resized_dem_with_flattened_roads(self) -> np.ndarray | None:
746
+ """Gets the not resized DEM with flattened roads.
747
+
748
+ Returns:
749
+ np.ndarray | None: The not resized DEM image or None if the image could not be read.
750
+ """
751
+ background_component = self.map.get_background_component()
752
+ if not background_component:
753
+ self.logger.warning("Background component not found.")
754
+ return None
755
+
756
+ dem_path = background_component.not_resized_with_flattened_roads_path
757
+
758
+ if not dem_path or not os.path.isfile(dem_path):
759
+ self.logger.warning("Not resized DEM with flattened roads path not found.")
760
+ return None
761
+
762
+ not_resized_dem = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED)
705
763
 
706
764
  return not_resized_dem
707
765
 
@@ -21,6 +21,16 @@ class Layer:
21
21
  usage (str | None): Usage of the layer.
22
22
  background (bool): Flag to determine if the layer is a background.
23
23
  invisible (bool): Flag to determine if the layer is invisible.
24
+ procedural (list[str] | None): List of procedural textures to apply.
25
+ border (int | None): Border size in pixels.
26
+ precise_tags (dict[str, str | list[str]] | None): Dictionary of precise tags to search for.
27
+ precise_usage (str | None): Precise usage of the layer.
28
+ area_type (str | None): Type of the area (e.g., residential, commercial).
29
+ area_water (bool): Flag to determine if the area is water.
30
+ indoor (bool): Flag to determine if the layer is indoor.
31
+ merge_into (str | None): Name of the layer to merge into.
32
+ building_category (str | None): Category of the building.
33
+ external (bool): External layers not being used by the game directly.
24
34
 
25
35
  Attributes:
26
36
  name (str): Name of the layer.
@@ -50,6 +60,9 @@ class Layer:
50
60
  area_water: bool = False,
51
61
  indoor: bool = False,
52
62
  merge_into: str | None = None,
63
+ building_category: str | None = None,
64
+ external: bool = False,
65
+ road_texture: str | None = None,
53
66
  ):
54
67
  self.name = name
55
68
  self.count = count
@@ -70,6 +83,9 @@ class Layer:
70
83
  self.area_water = area_water
71
84
  self.indoor = indoor
72
85
  self.merge_into = merge_into
86
+ self.building_category = building_category
87
+ self.external = external
88
+ self.road_texture = road_texture
73
89
 
74
90
  def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
75
91
  """Returns dictionary with layer data.
@@ -96,6 +112,9 @@ class Layer:
96
112
  "area_water": self.area_water,
97
113
  "indoor": self.indoor,
98
114
  "merge_into": self.merge_into,
115
+ "building_category": self.building_category,
116
+ "external": self.external,
117
+ "road_texture": self.road_texture,
99
118
  }
100
119
 
101
120
  data = {k: v for k, v in data.items() if v is not None}