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.
@@ -7,10 +7,10 @@ import os
7
7
  import shutil
8
8
  import warnings
9
9
  from collections import defaultdict
10
- from time import perf_counter
11
10
  from typing import Any, Callable, Generator, Optional
12
11
 
13
12
  import cv2
13
+ import geopandas as gpd
14
14
  import numpy as np
15
15
  import osmnx as ox
16
16
  import pandas as pd
@@ -21,6 +21,7 @@ from tqdm import tqdm
21
21
 
22
22
  from maps4fs.generator.component.base.component_image import ImageComponent
23
23
  from maps4fs.generator.component.layer import Layer
24
+ from maps4fs.generator.monitor import monitor_performance
24
25
  from maps4fs.generator.settings import Parameters
25
26
 
26
27
 
@@ -167,6 +168,14 @@ class Texture(ImageComponent):
167
168
  """
168
169
  return [layer for layer in self.layers if layer.indoor]
169
170
 
171
+ def get_building_category_layers(self) -> list[Layer]:
172
+ """Returns layers which have building category defined.
173
+
174
+ Returns:
175
+ list[Layer]: List of layers which have building category defined.
176
+ """
177
+ return [layer for layer in self.layers if layer.building_category is not None]
178
+
170
179
  def process(self) -> None:
171
180
  """Processes the data to generate textures."""
172
181
  self._prepare_weights()
@@ -186,6 +195,7 @@ class Texture(ImageComponent):
186
195
  for layer in self.layers:
187
196
  self.assets[layer.name] = layer.path(self._weights_dir)
188
197
 
198
+ @monitor_performance
189
199
  def add_borders(self) -> None:
190
200
  """Iterates over all the layers and picks the one which have the border propety defined.
191
201
  Borders are distance from the edge of the map on each side (top, right, bottom, left).
@@ -272,6 +282,7 @@ class Texture(ImageComponent):
272
282
  return layer
273
283
  return None
274
284
 
285
+ @monitor_performance
275
286
  def merge_into(self) -> None:
276
287
  """Merges the content of layers into their target layers."""
277
288
  for layer in self.layers:
@@ -298,6 +309,7 @@ class Texture(ImageComponent):
298
309
  cv2.imwrite(layer.path(self._weights_dir), np.zeros_like(layer_image))
299
310
  self.logger.debug("Cleared layer %s.", layer.name)
300
311
 
312
+ @monitor_performance
301
313
  def rotate_textures(self) -> None:
302
314
  """Rotates textures of the layers which have tags."""
303
315
  if self.rotation:
@@ -320,6 +332,7 @@ class Texture(ImageComponent):
320
332
  "Skipping rotation of layer %s because it has no tags.", layer.name
321
333
  )
322
334
 
335
+ @monitor_performance
323
336
  def scale_textures(self) -> None:
324
337
  """Resizes all the textures to the map output size."""
325
338
  if not self.map.output_size:
@@ -364,6 +377,7 @@ class Texture(ImageComponent):
364
377
  ]
365
378
  return {attr: getattr(self, attr, None) for attr in useful_attributes}
366
379
 
380
+ @monitor_performance
367
381
  def _prepare_weights(self):
368
382
  self.logger.debug("Starting preparing weights from %s layers.", len(self.layers))
369
383
 
@@ -428,6 +442,7 @@ class Texture(ImageComponent):
428
442
  ),
429
443
  )
430
444
 
445
+ @monitor_performance
431
446
  def draw(self) -> None:
432
447
  """Iterates over layers and fills them with polygons from OSM data."""
433
448
  layers = self.layers_by_priority()
@@ -462,8 +477,11 @@ class Texture(ImageComponent):
462
477
  self._draw_layer(layer, info_layer_data, layer_image) # type: ignore
463
478
  self._add_roads(layer, info_layer_data)
464
479
 
465
- output_image = cv2.bitwise_and(layer_image, mask) # type: ignore
466
- cumulative_image = cv2.bitwise_or(cumulative_image, output_image) # type: ignore
480
+ if not layer.external:
481
+ output_image = cv2.bitwise_and(layer_image, mask) # type: ignore
482
+ cumulative_image = cv2.bitwise_or(cumulative_image, output_image) # type: ignore
483
+ else:
484
+ output_image = layer_image # type: ignore
467
485
 
468
486
  cv2.imwrite(layer_path, output_image)
469
487
  self.logger.debug("Texture %s saved.", layer_path)
@@ -543,9 +561,11 @@ class Texture(ImageComponent):
543
561
  "points": linestring,
544
562
  "tags": str(layer.tags),
545
563
  "width": layer.width,
564
+ "road_texture": layer.road_texture,
546
565
  }
547
566
  info_layer_data[f"{layer.info_layer}_polylines"].append(linestring_entry) # type: ignore
548
567
 
568
+ @monitor_performance
549
569
  def dissolve(self) -> None:
550
570
  """Dissolves textures of the layers with tags into sublayers for them to look more
551
571
  natural in the game.
@@ -554,46 +574,52 @@ class Texture(ImageComponent):
554
574
  files of the corresponding layer and saves the changes to the files.
555
575
  """
556
576
  for layer in tqdm(self.layers, desc="Dissolving textures", unit="layer"):
557
- if not layer.tags:
558
- self.logger.debug("Layer %s has no tags, there's nothing to dissolve.", layer.name)
559
- continue
560
- layer_path = layer.path(self._weights_dir)
561
- layer_paths = layer.paths(self._weights_dir)
562
-
563
- if len(layer_paths) < 2:
564
- self.logger.debug("Layer %s has only one texture, skipping.", layer.name)
565
- continue
566
-
567
- self.logger.debug("Dissolving layer from %s to %s.", layer_path, layer_paths)
577
+ self.dissolve_layer(layer)
568
578
 
569
- # Check if the image contains any non-zero values, otherwise continue.
570
- layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
579
+ def dissolve_layer(self, layer: Layer) -> None:
580
+ """Dissolves texture of the layer into sublayers."""
581
+ if not layer.tags:
582
+ self.logger.debug("Layer %s has no tags, there's nothing to dissolve.", layer.name)
583
+ return
584
+ layer_path = layer.path(self._weights_dir)
585
+ layer_paths = layer.paths(self._weights_dir)
571
586
 
572
- if not np.any(layer_image): # type: ignore
573
- self.logger.debug(
574
- "Layer %s does not contain any non-zero values, skipping.", layer.name
575
- )
576
- continue
587
+ if len(layer_paths) < 2:
588
+ self.logger.debug("Layer %s has only one texture, skipping.", layer.name)
589
+ return
577
590
 
578
- # Save the original image to use it for preview later, without combining the sublayers.
579
- cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy()) # type: ignore
591
+ self.logger.debug("Dissolving layer from %s to %s.", layer_path, layer_paths)
592
+ # Check if the image contains any non-zero values, otherwise continue.
593
+ layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
594
+ if layer_image is None:
595
+ self.logger.debug("Layer %s image not found, skipping.", layer.name)
596
+ return
580
597
 
581
- # Get the coordinates of non-zero values.
582
- non_zero_coords = np.column_stack(np.where(layer_image > 0)) # type: ignore
598
+ # Get mask of non-zero pixels. If there are no non-zero pixels, skip the layer.
599
+ mask = layer_image > 0
600
+ if not np.any(mask):
601
+ self.logger.debug(
602
+ "Layer %s does not contain any non-zero values, skipping.", layer.name
603
+ )
604
+ return
605
+ # Save the original image to use it for preview later, without combining the sublayers.
606
+ cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy()) # type: ignore
583
607
 
584
- # Prepare sublayers.
585
- sublayers = [np.zeros_like(layer_image) for _ in range(layer.count)]
608
+ # Create random assignment array for all pixels
609
+ random_assignment = np.random.randint(0, layer.count, size=layer_image.shape)
586
610
 
587
- # Randomly assign non-zero values to sublayers.
588
- for coord in non_zero_coords:
589
- sublayers[np.random.randint(0, layer.count)][coord[0], coord[1]] = 255 # type: ignore
611
+ # Create sublayers using vectorized operations.
612
+ sublayers = []
613
+ for i in range(layer.count):
614
+ # Create sublayer: 255 where (mask is True AND random_assignment == i)
615
+ sublayer = np.where((mask) & (random_assignment == i), 255, 0).astype(np.uint8)
616
+ sublayers.append(sublayer)
590
617
 
591
- # Save the sublayers.
592
- for sublayer, sublayer_path in zip(sublayers, layer_paths):
593
- cv2.imwrite(sublayer_path, sublayer)
594
- self.logger.debug("Sublayer %s saved.", sublayer_path)
618
+ # Save sublayers
619
+ for sublayer, sublayer_path in zip(sublayers, layer_paths):
620
+ cv2.imwrite(sublayer_path, sublayer)
595
621
 
596
- self.logger.debug("Dissolved layer %s.", layer.name)
622
+ self.logger.debug("Dissolved layer %s.", layer.name)
597
623
 
598
624
  def draw_base_layer(self, cumulative_image: np.ndarray) -> None:
599
625
  """Draws base layer and saves it into the png file.
@@ -789,37 +815,49 @@ class Texture(ImageComponent):
789
815
  is_fieds = info_layer == "fields"
790
816
 
791
817
  ox_settings.use_cache = self.map.texture_settings.use_cache
792
- ox_settings.requests_timeout = 30
818
+ ox_settings.requests_timeout = 10
793
819
 
820
+ objects = self.fetch_osm_data(tags)
821
+ if objects is None or objects.empty:
822
+ self.logger.debug("No objects found for tags: %s.", tags)
823
+ return
824
+
825
+ self.logger.debug("Fetched %s elements for tags: %s.", len(objects), tags)
826
+
827
+ method = self.linestrings_generator if yield_linestrings else self.polygons_generator
828
+
829
+ yield from method(objects, width, is_fieds)
830
+
831
+ @monitor_performance
832
+ def fetch_osm_data(self, tags: dict[str, str | list[str] | bool]) -> gpd.GeoDataFrame | None:
833
+ """Fetches OSM data for given tags.
834
+
835
+ Arguments:
836
+ tags (dict[str, str | list[str] | bool]): Dictionary of tags to search for.
837
+
838
+ Returns:
839
+ gpd.GeoDataFrame | None: GeoDataFrame with OSM objects or None if no objects found.
840
+ """
794
841
  try:
795
842
  if self.map.custom_osm is not None:
796
843
  with warnings.catch_warnings():
797
844
  warnings.simplefilter("ignore", FutureWarning)
798
845
  objects = ox.features_from_xml(self.map.custom_osm, tags=tags)
799
846
  else:
800
- before_fetch = perf_counter()
801
847
  objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
802
- after_fetch = perf_counter()
803
- fetched_in = after_fetch - before_fetch
804
- self.logger.info(
805
- "Fetched OSMNX objects for tags: %s in %.2f seconds.", tags, fetched_in
806
- )
807
848
  except Exception as e:
808
- self.logger.debug("Error fetching objects for tags: %s. Error: %s.", tags, e)
809
- return
810
- self.logger.debug("Fetched %s elements for tags: %s.", len(objects), tags)
849
+ self.logger.warning("Error fetching objects for tags: %s. Error: %s.", tags, e)
850
+ return None
811
851
 
812
- method = self.linestrings_generator if yield_linestrings else self.polygons_generator
813
-
814
- yield from method(objects, width, is_fieds)
852
+ return objects
815
853
 
816
854
  def linestrings_generator(
817
- self, objects: pd.core.frame.DataFrame, *args, **kwargs
855
+ self, objects: gpd.GeoDataFrame, *args, **kwargs
818
856
  ) -> Generator[list[tuple[int, int]], None, None]:
819
857
  """Generator which yields lists of point coordinates which represent LineStrings from OSM.
820
858
 
821
859
  Arguments:
822
- objects (pd.core.frame.DataFrame): Dataframe with OSM objects.
860
+ objects (gpd.GeoDataFrame): GeoDataFrame with OSM objects.
823
861
 
824
862
  Yields:
825
863
  Generator[list[tuple[int, int]], None, None]: List of point coordinates.
@@ -865,6 +903,7 @@ class Texture(ImageComponent):
865
903
  polygon_np = self._to_np(polygon)
866
904
  yield polygon_np
867
905
 
906
+ @monitor_performance
868
907
  def previews(self) -> list[str]:
869
908
  """Invokes methods to generate previews. Returns list of paths to previews.
870
909
 
@@ -40,7 +40,7 @@ logger.info(
40
40
  )
41
41
 
42
42
  TEMPLATES_STRUCTURE = {
43
- "fs25": ["texture_schemas", "tree_schemas", "map_templates"],
43
+ "fs25": ["texture_schemas", "tree_schemas", "buildings_schemas", "map_templates"],
44
44
  "fs22": ["texture_schemas", "map_templates"],
45
45
  }
46
46
 
@@ -51,11 +51,19 @@ def ensure_templates():
51
51
  If MFS_TEMPLATES_DIR is empty or doesn't exist, clone the maps4fsdata
52
52
  repository and run the preparation script to populate it.
53
53
  """
54
-
55
54
  # Check if templates directory exists and has content
56
- if os.path.exists(MFS_TEMPLATES_DIR) and os.listdir(MFS_TEMPLATES_DIR):
57
- logger.info("Templates directory already exists and contains data: %s", MFS_TEMPLATES_DIR)
58
- return
55
+ if os.path.exists(MFS_TEMPLATES_DIR): # and os.listdir(MFS_TEMPLATES_DIR):
56
+ logger.info("Templates directory already exists: %s", MFS_TEMPLATES_DIR)
57
+
58
+ files = [
59
+ entry
60
+ for entry in os.listdir(MFS_TEMPLATES_DIR)
61
+ if os.path.isfile(os.path.join(MFS_TEMPLATES_DIR, entry))
62
+ ]
63
+
64
+ if files:
65
+ logger.info("Templates directory contains files and will not be modified.")
66
+ return
59
67
 
60
68
  logger.info("Templates directory is empty or missing, preparing data...")
61
69
 
@@ -82,15 +90,31 @@ def ensure_templates():
82
90
  text=True,
83
91
  )
84
92
 
85
- # Make the preparation script executable
86
- prep_script = os.path.join(clone_dir, "prepare_data.sh")
93
+ if os.name == "nt":
94
+ logger.info("Detected Windows OS, running PowerShell preparation script...")
95
+ prep_script = os.path.join(clone_dir, "prepare_data.ps1")
96
+ for_subprocess = [
97
+ "powershell",
98
+ "-ExecutionPolicy",
99
+ "Bypass",
100
+ "-File",
101
+ "prepare_data.ps1",
102
+ ]
103
+ else:
104
+ logger.info("Detected non-Windows OS, running bash preparation script...")
105
+ prep_script = os.path.join(clone_dir, "prepare_data.sh")
106
+ for_subprocess = ["./prepare_data.sh"]
107
+
87
108
  if os.path.exists(prep_script):
88
- os.chmod(prep_script, 0o755)
109
+ try:
110
+ os.chmod(prep_script, 0o755)
111
+ except Exception as e:
112
+ logger.warning("Could not set execute permissions on script: %s", str(e))
89
113
 
90
114
  logger.info("Running data preparation script...")
91
115
  # Run the preparation script from the cloned directory
92
116
  subprocess.run(
93
- ["./prepare_data.sh"], cwd=clone_dir, check=True, capture_output=True, text=True
117
+ for_subprocess, cwd=clone_dir, check=True, capture_output=True, text=True
94
118
  )
95
119
 
96
120
  # Copy the generated data directory to templates directory
@@ -140,6 +164,27 @@ def ensure_template_subdirs() -> None:
140
164
  logger.info("Templates directory is ready at: %s", MFS_TEMPLATES_DIR)
141
165
 
142
166
 
167
+ def reload_templates() -> None:
168
+ """Reload templates by removing existing files and re-preparing them.
169
+ Does not affect nested directories containing user data.
170
+ If needed, the files should be removed manually.
171
+ """
172
+ logger.info("Reloading templates...")
173
+ # Remove files from the templates directory.
174
+ # But do not remove nested directories, because they contain user data.
175
+ # Only remove files in the top-level templates directory.
176
+ for item in os.listdir(MFS_TEMPLATES_DIR):
177
+ item_path = os.path.join(MFS_TEMPLATES_DIR, item)
178
+ if os.path.isfile(item_path):
179
+ try:
180
+ os.remove(item_path)
181
+ except Exception as e:
182
+ logger.warning("Could not remove file %s: %s", item_path, str(e))
183
+ ensure_templates()
184
+ ensure_template_subdirs()
185
+ logger.info("Templates reloaded successfully.")
186
+
187
+
143
188
  ensure_templates()
144
189
  ensure_template_subdirs()
145
190
 
@@ -160,8 +205,31 @@ SAT_CACHE_DIR = os.path.join(MFS_CACHE_DIR, "sat")
160
205
 
161
206
  osmnx_cache = os.path.join(MFS_CACHE_DIR, "osmnx")
162
207
  osmnx_data = os.path.join(MFS_CACHE_DIR, "odata")
163
- os.makedirs(osmnx_cache, exist_ok=True)
164
- os.makedirs(osmnx_data, exist_ok=True)
208
+
209
+ CACHE_DIRS = [DTM_CACHE_DIR, SAT_CACHE_DIR, osmnx_cache, osmnx_data]
210
+
211
+
212
+ def create_cache_dirs() -> None:
213
+ """Create cache directories if they do not exist."""
214
+ logger.info("Ensuring cache directories exist...")
215
+ for cache_dir in CACHE_DIRS:
216
+ os.makedirs(cache_dir, exist_ok=True)
217
+ logger.debug("Cache directory ensured: %s", cache_dir)
218
+ logger.info("All cache directories are ready.")
219
+
220
+
221
+ def clean_cache() -> None:
222
+ """Clean all cache directories by removing and recreating them."""
223
+ logger.info("Cleaning cache directories...")
224
+ for cache_dir in CACHE_DIRS:
225
+ if os.path.exists(cache_dir):
226
+ shutil.rmtree(cache_dir)
227
+ logger.debug("Removed cache directory: %s", cache_dir)
228
+ create_cache_dirs()
229
+ logger.info("Cache directories cleaned and recreated.")
230
+
231
+
232
+ create_cache_dirs()
165
233
 
166
234
 
167
235
  ox_settings.cache_folder = osmnx_cache
maps4fs/generator/game.py CHANGED
@@ -5,12 +5,15 @@ template file and specific settings for map generation."""
5
5
  from __future__ import annotations
6
6
 
7
7
  import os
8
+ from typing import Callable
8
9
 
9
10
  import maps4fs.generator.config as mfscfg
10
11
  from maps4fs.generator.component.background import Background
12
+ from maps4fs.generator.component.building import Building
11
13
  from maps4fs.generator.component.config import Config
12
14
  from maps4fs.generator.component.grle import GRLE
13
15
  from maps4fs.generator.component.i3d import I3d
16
+ from maps4fs.generator.component.road import Road
14
17
  from maps4fs.generator.component.satellite import Satellite
15
18
  from maps4fs.generator.component.texture import Texture
16
19
 
@@ -37,6 +40,7 @@ class Game:
37
40
  _texture_schema_file: str | None = None
38
41
  _grle_schema_file: str | None = None
39
42
  _tree_schema_file: str | None = None
43
+ _buildings_schema_file: str | None = None
40
44
  _i3d_processing: bool = True
41
45
  _plants_processing: bool = True
42
46
  _environment_processing: bool = True
@@ -45,7 +49,7 @@ class Game:
45
49
  _mesh_processing: bool = True
46
50
 
47
51
  # Order matters! Some components depend on others.
48
- components = [Satellite, Texture, Background, GRLE, I3d, Config]
52
+ components = [Satellite, Texture, Background, GRLE, I3d, Config, Road, Building]
49
53
 
50
54
  def __init__(self, map_template_path: str | None = None):
51
55
  if map_template_path:
@@ -72,6 +76,11 @@ class Game:
72
76
  else:
73
77
  self._tree_schema = os.path.join(mfscfg.MFS_TEMPLATES_DIR, self._tree_schema_file) # type: ignore
74
78
 
79
+ if not self._buildings_schema_file:
80
+ self._buildings_schema_file = None
81
+ else:
82
+ self._buildings_schema_file = os.path.join(mfscfg.MFS_TEMPLATES_DIR, self._buildings_schema_file) # type: ignore
83
+
75
84
  def set_components_by_names(self, component_names: list[str]) -> None:
76
85
  """Sets the components used for map generation by their names.
77
86
 
@@ -159,6 +168,19 @@ class Game:
159
168
  raise ValueError("Tree layers schema path not set.")
160
169
  return self._tree_schema
161
170
 
171
+ @property
172
+ def buildings_schema(self) -> str:
173
+ """Returns the path to the buildings layers schema file.
174
+
175
+ Raises:
176
+ ValueError: If the buildings layers schema path is not set.
177
+
178
+ Returns:
179
+ str: The path to the buildings layers schema file."""
180
+ if not self._buildings_schema_file:
181
+ raise ValueError("Buildings layers schema path not set.")
182
+ return self._buildings_schema_file
183
+
162
184
  def dem_file_path(self, map_directory: str) -> str:
163
185
  """Returns the path to the DEM file.
164
186
 
@@ -166,8 +188,7 @@ class Game:
166
188
  map_directory (str): The path to the map directory.
167
189
 
168
190
  Returns:
169
- str: The path to the DEM file.
170
- """
191
+ str: The path to the DEM file."""
171
192
  raise NotImplementedError
172
193
 
173
194
  def weights_dir_path(self, map_directory: str) -> str:
@@ -341,6 +362,45 @@ class Game:
341
362
  bool: True if the mesh should be processed, False otherwise."""
342
363
  return self._mesh_processing
343
364
 
365
+ def validate_template(self, map_directory: str) -> None:
366
+ """Validates that all required files exist in the map template directory.
367
+
368
+ Arguments:
369
+ map_directory (str): The path to the map directory.
370
+
371
+ Raises:
372
+ FileNotFoundError: If any required files are missing from the template.
373
+ """
374
+ all_files = []
375
+ for root, _, files in os.walk(map_directory):
376
+ for file in files:
377
+ all_files.append(os.path.join(root, file))
378
+
379
+ missing_files = []
380
+ for func in self.required_file_methods():
381
+ try:
382
+ required_filepath = func(map_directory)
383
+ except NotImplementedError:
384
+ continue
385
+ if required_filepath not in all_files:
386
+ missing_files.append(required_filepath)
387
+ if missing_files:
388
+ raise FileNotFoundError(f"The following files are not found: {missing_files}.")
389
+
390
+ def required_file_methods(self) -> list[Callable[[str], str]]:
391
+ """Returns a list of methods that return paths to required files for map generation.
392
+
393
+ Returns:
394
+ list[Callable[[str], str]]: List of methods that take a map directory path
395
+ and return file paths that are required for the map template.
396
+ """
397
+ return [
398
+ self.map_xml_path,
399
+ self.i3d_file_path,
400
+ self.get_environment_xml_path,
401
+ self.get_farmlands_xml_path,
402
+ ]
403
+
344
404
 
345
405
  class FS22(Game):
346
406
  """Class used to define the game version FS22."""
@@ -396,6 +456,7 @@ class FS25(Game):
396
456
  _texture_schema_file = "fs25-texture-schema.json"
397
457
  _grle_schema_file = "fs25-grle-schema.json"
398
458
  _tree_schema_file = "fs25-tree-schema.json"
459
+ _buildings_schema_file = "fs25-buildings-schema.json"
399
460
 
400
461
  def dem_file_path(self, map_directory: str) -> str:
401
462
  """Returns the path to the DEM file.
maps4fs/generator/map.py CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import shutil
8
+ from time import perf_counter
8
9
  from typing import Any, Generator
9
10
 
10
11
  from pydtmdl import DTMProvider
@@ -14,8 +15,13 @@ import maps4fs.generator.config as mfscfg
14
15
  import maps4fs.generator.utils as mfsutils
15
16
  from maps4fs.generator.component import Background, Component, Layer, Satellite, Texture
16
17
  from maps4fs.generator.game import Game
18
+ from maps4fs.generator.monitor import PerformanceMonitor, performance_session
17
19
  from maps4fs.generator.settings import GenerationSettings, MainSettings, SharedSettings
18
- from maps4fs.generator.statistics import send_advanced_settings, send_main_settings
20
+ from maps4fs.generator.statistics import (
21
+ send_advanced_settings,
22
+ send_main_settings,
23
+ send_performance_report,
24
+ )
19
25
  from maps4fs.logger import Logger
20
26
 
21
27
 
@@ -99,6 +105,7 @@ class Map:
99
105
  self.i3d_settings = generation_settings.i3d_settings
100
106
  self.texture_settings = generation_settings.texture_settings
101
107
  self.satellite_settings = generation_settings.satellite_settings
108
+ self.building_settings = generation_settings.building_settings
102
109
  self.process_settings()
103
110
 
104
111
  self.logger = logger if logger else Logger()
@@ -117,11 +124,13 @@ class Map:
117
124
  os.makedirs(self.map_directory, exist_ok=True)
118
125
  self.texture_custom_schema = kwargs.get("texture_custom_schema", None)
119
126
  self.tree_custom_schema = kwargs.get("tree_custom_schema", None)
127
+ self.buildings_custom_schema = kwargs.get("buildings_custom_schema", None)
120
128
 
121
129
  json_data = {
122
130
  "generation_settings.json": generation_settings_json,
123
131
  "texture_custom_schema.json": self.texture_custom_schema,
124
132
  "tree_custom_schema.json": self.tree_custom_schema,
133
+ "buildings_custom_schema.json": self.buildings_custom_schema,
125
134
  }
126
135
 
127
136
  for filename, data in json_data.items():
@@ -203,51 +212,76 @@ class Map:
203
212
  Yields:
204
213
  Generator[str, None, None]: Component names.
205
214
  """
206
- self.logger.debug(
207
- "Starting map generation. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
208
- self.game.code,
209
- self.coordinates,
210
- self.size,
211
- self.rotation,
212
- )
213
- for game_component in self.game.components:
214
- component = game_component(
215
- self.game,
216
- self,
215
+ with performance_session() as session_id:
216
+ self.logger.info(
217
+ "Starting map generation. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
218
+ self.game.code,
217
219
  self.coordinates,
218
220
  self.size,
219
- self.rotated_size,
220
221
  self.rotation,
221
- self.map_directory,
222
- self.logger,
223
- texture_custom_schema=self.texture_custom_schema, # type: ignore
224
- tree_custom_schema=self.tree_custom_schema, # type: ignore
225
222
  )
226
- self.components.append(component)
227
-
228
- yield component.__class__.__name__
229
-
230
- try:
231
- component.process()
232
- component.commit_generation_info()
233
- except Exception as e:
234
- self.logger.error(
235
- "Error processing or committing generation info for component %s: %s",
236
- component.__class__.__name__,
237
- e,
223
+ generation_start = perf_counter()
224
+
225
+ for game_component in self.game.components:
226
+ component = game_component(
227
+ self.game,
228
+ self,
229
+ self.coordinates,
230
+ self.size,
231
+ self.rotated_size,
232
+ self.rotation,
233
+ self.map_directory,
234
+ self.logger,
235
+ texture_custom_schema=self.texture_custom_schema, # type: ignore
236
+ tree_custom_schema=self.tree_custom_schema, # type: ignore
238
237
  )
239
- self._update_main_settings({"error": str(e)})
240
- raise e
238
+ self.components.append(component)
239
+
240
+ yield component.__class__.__name__
241
+
242
+ try:
243
+ component_start = perf_counter()
244
+ component.process()
245
+ component_finish = perf_counter()
246
+ self.logger.info(
247
+ "Component %s processed in %.2f seconds.",
248
+ component.__class__.__name__,
249
+ component_finish - component_start,
250
+ )
251
+ component.commit_generation_info()
252
+ except Exception as e:
253
+ self.logger.error(
254
+ "Error processing or committing generation info for component %s: %s",
255
+ component.__class__.__name__,
256
+ e,
257
+ )
258
+ self._update_main_settings({"error": str(e)})
259
+ raise e
260
+
261
+ generation_finish = perf_counter()
262
+ self.logger.info(
263
+ "Map generation completed in %.2f seconds.",
264
+ generation_finish - generation_start,
265
+ )
241
266
 
242
- self._update_main_settings({"completed": True})
267
+ self._update_main_settings({"completed": True})
243
268
 
244
- self.logger.debug(
245
- "Map generation completed. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
246
- self.game.code,
247
- self.coordinates,
248
- self.size,
249
- self.rotation,
250
- )
269
+ self.logger.info(
270
+ "Map generation completed. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
271
+ self.game.code,
272
+ self.coordinates,
273
+ self.size,
274
+ self.rotation,
275
+ )
276
+
277
+ session_json = PerformanceMonitor().get_session_json(session_id)
278
+ if session_json:
279
+ report_filename = "performance_report.json"
280
+ with open(
281
+ os.path.join(self.map_directory, report_filename), "w", encoding="utf-8"
282
+ ) as file:
283
+ json.dump(session_json, file, indent=4)
284
+ send_performance_report(session_json)
251
285
 
252
286
  def _update_main_settings(self, data: dict[str, Any]) -> None:
253
287
  """Update main settings with provided data.