maps4fs 2.8.9__tar.gz → 2.9.0__tar.gz

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.

Files changed (36) hide show
  1. {maps4fs-2.8.9 → maps4fs-2.9.0}/PKG-INFO +1 -1
  2. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/background.py +13 -1
  3. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/config.py +5 -0
  4. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/dem.py +7 -0
  5. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/grle.py +9 -0
  6. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/i3d.py +4 -0
  7. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/satellite.py +3 -0
  8. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/texture.py +35 -14
  9. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/map.py +70 -39
  10. maps4fs-2.9.0/maps4fs/generator/monitor.py +108 -0
  11. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/statistics.py +10 -0
  12. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/utils.py +11 -0
  13. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs.egg-info/PKG-INFO +1 -1
  14. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs.egg-info/SOURCES.txt +1 -0
  15. {maps4fs-2.8.9 → maps4fs-2.9.0}/pyproject.toml +1 -1
  16. {maps4fs-2.8.9 → maps4fs-2.9.0}/LICENSE.md +0 -0
  17. {maps4fs-2.8.9 → maps4fs-2.9.0}/README.md +0 -0
  18. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/__init__.py +0 -0
  19. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/__init__.py +0 -0
  20. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/__init__.py +0 -0
  21. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/base/__init__.py +0 -0
  22. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/base/component.py +0 -0
  23. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/base/component_image.py +0 -0
  24. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/base/component_mesh.py +0 -0
  25. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/base/component_xml.py +0 -0
  26. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/component/layer.py +0 -0
  27. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/config.py +0 -0
  28. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/game.py +0 -0
  29. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/qgis.py +0 -0
  30. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/generator/settings.py +0 -0
  31. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs/logger.py +0 -0
  32. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs.egg-info/dependency_links.txt +0 -0
  33. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs.egg-info/requires.txt +0 -0
  34. {maps4fs-2.8.9 → maps4fs-2.9.0}/maps4fs.egg-info/top_level.txt +0 -0
  35. {maps4fs-2.8.9 → maps4fs-2.9.0}/setup.cfg +0 -0
  36. {maps4fs-2.8.9 → maps4fs-2.9.0}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.8.9
3
+ Version: 2.9.0
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: GNU Affero General Public License v3.0
@@ -19,6 +19,7 @@ from maps4fs.generator.component.base.component_image import ImageComponent
19
19
  from maps4fs.generator.component.base.component_mesh import MeshComponent
20
20
  from maps4fs.generator.component.dem import DEM
21
21
  from maps4fs.generator.component.texture import Texture
22
+ from maps4fs.generator.monitor import monitor_performance
22
23
  from maps4fs.generator.settings import Parameters
23
24
 
24
25
  SEGMENT_LENGTH = 2
@@ -38,6 +39,7 @@ class Background(MeshComponent, ImageComponent):
38
39
  info, warning. If not provided, default logging will be used.
39
40
  """
40
41
 
42
+ @monitor_performance
41
43
  def preprocess(self) -> None:
42
44
  """Registers the DEMs for the background terrain."""
43
45
  self.stl_preview_path = os.path.join(self.previews_directory, "background_dem.stl")
@@ -141,6 +143,7 @@ class Background(MeshComponent, ImageComponent):
141
143
  "Mesh processing is disabled for the game, skipping water mesh processing."
142
144
  )
143
145
 
146
+ @monitor_performance
144
147
  def create_foundations(self, dem_image: np.ndarray) -> np.ndarray:
145
148
  """Creates foundations for buildings based on the DEM data.
146
149
 
@@ -246,6 +249,7 @@ class Background(MeshComponent, ImageComponent):
246
249
  )
247
250
  self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
248
251
 
252
+ @monitor_performance
249
253
  def generate_obj_files(self) -> None:
250
254
  """Iterates over all dems and generates 3D obj files based on DEM data.
251
255
  If at least one DEM file is missing, the generation will be stopped at all.
@@ -328,8 +332,9 @@ class Background(MeshComponent, ImageComponent):
328
332
  return resolution
329
333
  return Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE
330
334
 
335
+ @monitor_performance
331
336
  def decimate_background_mesh(self) -> None:
332
- """ ""Decimates the background mesh based on the map size."""
337
+ """Decimates the background mesh based on the map size."""
333
338
  if not self.assets.background_mesh or not os.path.isfile(self.assets.background_mesh):
334
339
  self.logger.warning("Background mesh not found, cannot generate i3d background.")
335
340
  return
@@ -362,6 +367,7 @@ class Background(MeshComponent, ImageComponent):
362
367
 
363
368
  self.assets.decimated_background_mesh = decimated_save_path
364
369
 
370
+ @monitor_performance
365
371
  def texture_background_mesh(self) -> None:
366
372
  """Textures the background mesh using satellite imagery."""
367
373
  satellite_component = self.map.get_satellite_component()
@@ -427,6 +433,7 @@ class Background(MeshComponent, ImageComponent):
427
433
  self.logger.error("Could not texture background mesh: %s", e)
428
434
  return
429
435
 
436
+ @monitor_performance
430
437
  def convert_background_mesh_to_i3d(self) -> bool:
431
438
  """Converts the textured background mesh to i3d format.
432
439
 
@@ -498,6 +505,7 @@ class Background(MeshComponent, ImageComponent):
498
505
  with open(file_path, "w", encoding="utf-8") as f:
499
506
  f.write(content + "\n\n" + note)
500
507
 
508
+ @monitor_performance
501
509
  def convert_water_mesh_to_i3d(self) -> bool:
502
510
  """Converts the line-based water mesh to i3d format.
503
511
 
@@ -730,6 +738,7 @@ class Background(MeshComponent, ImageComponent):
730
738
  cv2.imwrite(colored_dem_path, dem_data_colored)
731
739
  return colored_dem_path
732
740
 
741
+ @monitor_performance
733
742
  def create_background_textures(self) -> None:
734
743
  """Creates background textures for the map."""
735
744
  layers_schema = self.map.texture_schema
@@ -782,6 +791,7 @@ class Background(MeshComponent, ImageComponent):
782
791
 
783
792
  cv2.imwrite(self.water_resources_path, background_image)
784
793
 
794
+ @monitor_performance
785
795
  def subtraction(self) -> None:
786
796
  """Subtracts the water depth from the DEM data where the water resources are located."""
787
797
  if not self.water_resources_path:
@@ -967,6 +977,7 @@ class Background(MeshComponent, ImageComponent):
967
977
  mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
968
978
  return mesh
969
979
 
980
+ @monitor_performance
970
981
  def generate_water_resources_obj(self) -> None:
971
982
  """Generates 3D obj files based on water resources data."""
972
983
  self.logger.debug("Starting water resources generation...")
@@ -1033,6 +1044,7 @@ class Background(MeshComponent, ImageComponent):
1033
1044
 
1034
1045
  self.plane_from_np(elevated_water, elevated_save_path, include_zeros=False)
1035
1046
 
1047
+ @monitor_performance
1036
1048
  def flatten_roads(self) -> None:
1037
1049
  """Flattens the roads in the DEM data by averaging the height values along the road polylines."""
1038
1050
  if not self.not_resized_path or not os.path.isfile(self.not_resized_path):
@@ -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):
@@ -554,6 +562,7 @@ class GRLE(ImageComponent, XMLComponent):
554
562
  self.logger.debug("Environment InfoLayer PNG file saved: %s.", info_layer_environment_path)
555
563
  self.preview_paths["environment"] = info_layer_environment_path
556
564
 
565
+ @monitor_performance
557
566
  def get_resized_weight(
558
567
  self, layer: Layer, resize_to: int, dilations: int = 3
559
568
  ) -> np.ndarray | None:
@@ -13,6 +13,7 @@ import numpy as np
13
13
  from tqdm import tqdm
14
14
 
15
15
  from maps4fs.generator.component.base.component_xml import XMLComponent
16
+ from maps4fs.generator.monitor import monitor_performance
16
17
  from maps4fs.generator.settings import Parameters
17
18
 
18
19
  NODE_ID_STARTING_VALUE = 2000
@@ -112,6 +113,7 @@ class I3d(XMLComponent):
112
113
 
113
114
  self.save_tree(tree)
114
115
 
116
+ @monitor_performance
115
117
  def _add_splines(self) -> None:
116
118
  """Adds splines to the map I3D file."""
117
119
  splines_i3d_path = self.game.splines_file_path(self.map_directory)
@@ -240,6 +242,7 @@ class I3d(XMLComponent):
240
242
 
241
243
  self.assets.splines = splines_i3d_path
242
244
 
245
+ @monitor_performance
243
246
  def _add_fields(self) -> None:
244
247
  """Adds fields to the map I3D file."""
245
248
  tree = self.get_tree()
@@ -498,6 +501,7 @@ class I3d(XMLComponent):
498
501
 
499
502
  return choice(trees_by_leaf_type)
500
503
 
504
+ @monitor_performance
501
505
  def _add_forests(self) -> None:
502
506
  """Adds forests to the map I3D file."""
503
507
  tree_schema = self._read_tree_schema()
@@ -9,6 +9,7 @@ from pygmdl import save_image
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
  from maps4fs.generator.settings import Parameters
13
14
 
14
15
 
@@ -49,6 +50,7 @@ class Satellite(ImageComponent):
49
50
  info, warning. If not provided, default logging will be used.
50
51
  """
51
52
 
53
+ @monitor_performance
52
54
  def process(self) -> None:
53
55
  """Downloads the satellite images for the map."""
54
56
  self.image_paths = []
@@ -126,6 +128,7 @@ class Satellite(ImageComponent):
126
128
 
127
129
  return tasks
128
130
 
131
+ @monitor_performance
129
132
  def previews(self) -> list[str]:
130
133
  """Returns the paths to the preview images.
131
134
 
@@ -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
 
@@ -186,6 +187,7 @@ class Texture(ImageComponent):
186
187
  for layer in self.layers:
187
188
  self.assets[layer.name] = layer.path(self._weights_dir)
188
189
 
190
+ @monitor_performance
189
191
  def add_borders(self) -> None:
190
192
  """Iterates over all the layers and picks the one which have the border propety defined.
191
193
  Borders are distance from the edge of the map on each side (top, right, bottom, left).
@@ -272,6 +274,7 @@ class Texture(ImageComponent):
272
274
  return layer
273
275
  return None
274
276
 
277
+ @monitor_performance
275
278
  def merge_into(self) -> None:
276
279
  """Merges the content of layers into their target layers."""
277
280
  for layer in self.layers:
@@ -298,6 +301,7 @@ class Texture(ImageComponent):
298
301
  cv2.imwrite(layer.path(self._weights_dir), np.zeros_like(layer_image))
299
302
  self.logger.debug("Cleared layer %s.", layer.name)
300
303
 
304
+ @monitor_performance
301
305
  def rotate_textures(self) -> None:
302
306
  """Rotates textures of the layers which have tags."""
303
307
  if self.rotation:
@@ -320,6 +324,7 @@ class Texture(ImageComponent):
320
324
  "Skipping rotation of layer %s because it has no tags.", layer.name
321
325
  )
322
326
 
327
+ @monitor_performance
323
328
  def scale_textures(self) -> None:
324
329
  """Resizes all the textures to the map output size."""
325
330
  if not self.map.output_size:
@@ -364,6 +369,7 @@ class Texture(ImageComponent):
364
369
  ]
365
370
  return {attr: getattr(self, attr, None) for attr in useful_attributes}
366
371
 
372
+ @monitor_performance
367
373
  def _prepare_weights(self):
368
374
  self.logger.debug("Starting preparing weights from %s layers.", len(self.layers))
369
375
 
@@ -428,6 +434,7 @@ class Texture(ImageComponent):
428
434
  ),
429
435
  )
430
436
 
437
+ @monitor_performance
431
438
  def draw(self) -> None:
432
439
  """Iterates over layers and fills them with polygons from OSM data."""
433
440
  layers = self.layers_by_priority()
@@ -546,6 +553,7 @@ class Texture(ImageComponent):
546
553
  }
547
554
  info_layer_data[f"{layer.info_layer}_polylines"].append(linestring_entry) # type: ignore
548
555
 
556
+ @monitor_performance
549
557
  def dissolve(self) -> None:
550
558
  """Dissolves textures of the layers with tags into sublayers for them to look more
551
559
  natural in the game.
@@ -791,35 +799,47 @@ class Texture(ImageComponent):
791
799
  ox_settings.use_cache = self.map.texture_settings.use_cache
792
800
  ox_settings.requests_timeout = 30
793
801
 
802
+ objects = self.fetch_osm_data(tags)
803
+ if objects is None or objects.empty:
804
+ self.logger.debug("No objects found for tags: %s.", tags)
805
+ return
806
+
807
+ self.logger.debug("Fetched %s elements for tags: %s.", len(objects), tags)
808
+
809
+ method = self.linestrings_generator if yield_linestrings else self.polygons_generator
810
+
811
+ yield from method(objects, width, is_fieds)
812
+
813
+ @monitor_performance
814
+ def fetch_osm_data(self, tags: dict[str, str | list[str] | bool]) -> gpd.GeoDataFrame | None:
815
+ """Fetches OSM data for given tags.
816
+
817
+ Arguments:
818
+ tags (dict[str, str | list[str] | bool]): Dictionary of tags to search for.
819
+
820
+ Returns:
821
+ gpd.GeoDataFrame | None: GeoDataFrame with OSM objects or None if no objects found.
822
+ """
794
823
  try:
795
824
  if self.map.custom_osm is not None:
796
825
  with warnings.catch_warnings():
797
826
  warnings.simplefilter("ignore", FutureWarning)
798
827
  objects = ox.features_from_xml(self.map.custom_osm, tags=tags)
799
828
  else:
800
- before_fetch = perf_counter()
801
829
  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
830
  except Exception as e:
808
831
  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)
832
+ return None
811
833
 
812
- method = self.linestrings_generator if yield_linestrings else self.polygons_generator
813
-
814
- yield from method(objects, width, is_fieds)
834
+ return objects
815
835
 
816
836
  def linestrings_generator(
817
- self, objects: pd.core.frame.DataFrame, *args, **kwargs
837
+ self, objects: gpd.GeoDataFrame, *args, **kwargs
818
838
  ) -> Generator[list[tuple[int, int]], None, None]:
819
839
  """Generator which yields lists of point coordinates which represent LineStrings from OSM.
820
840
 
821
841
  Arguments:
822
- objects (pd.core.frame.DataFrame): Dataframe with OSM objects.
842
+ objects (gpd.GeoDataFrame): GeoDataFrame with OSM objects.
823
843
 
824
844
  Yields:
825
845
  Generator[list[tuple[int, int]], None, None]: List of point coordinates.
@@ -865,6 +885,7 @@ class Texture(ImageComponent):
865
885
  polygon_np = self._to_np(polygon)
866
886
  yield polygon_np
867
887
 
888
+ @monitor_performance
868
889
  def previews(self) -> list[str]:
869
890
  """Invokes methods to generate previews. Returns list of paths to previews.
870
891
 
@@ -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
 
@@ -203,51 +209,76 @@ class Map:
203
209
  Yields:
204
210
  Generator[str, None, None]: Component names.
205
211
  """
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,
212
+ with performance_session() as session_id:
213
+ self.logger.info(
214
+ "Starting map generation. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
215
+ self.game.code,
217
216
  self.coordinates,
218
217
  self.size,
219
- self.rotated_size,
220
218
  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
219
  )
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,
220
+ generation_start = perf_counter()
221
+
222
+ for game_component in self.game.components:
223
+ component = game_component(
224
+ self.game,
225
+ self,
226
+ self.coordinates,
227
+ self.size,
228
+ self.rotated_size,
229
+ self.rotation,
230
+ self.map_directory,
231
+ self.logger,
232
+ texture_custom_schema=self.texture_custom_schema, # type: ignore
233
+ tree_custom_schema=self.tree_custom_schema, # type: ignore
238
234
  )
239
- self._update_main_settings({"error": str(e)})
240
- raise e
235
+ self.components.append(component)
236
+
237
+ yield component.__class__.__name__
238
+
239
+ try:
240
+ component_start = perf_counter()
241
+ component.process()
242
+ component_finish = perf_counter()
243
+ self.logger.info(
244
+ "Component %s processed in %.2f seconds.",
245
+ component.__class__.__name__,
246
+ component_finish - component_start,
247
+ )
248
+ component.commit_generation_info()
249
+ except Exception as e:
250
+ self.logger.error(
251
+ "Error processing or committing generation info for component %s: %s",
252
+ component.__class__.__name__,
253
+ e,
254
+ )
255
+ self._update_main_settings({"error": str(e)})
256
+ raise e
257
+
258
+ generation_finish = perf_counter()
259
+ self.logger.info(
260
+ "Map generation completed in %.2f seconds.",
261
+ generation_finish - generation_start,
262
+ )
241
263
 
242
- self._update_main_settings({"completed": True})
264
+ self._update_main_settings({"completed": True})
243
265
 
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
- )
266
+ self.logger.info(
267
+ "Map generation completed. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
268
+ self.game.code,
269
+ self.coordinates,
270
+ self.size,
271
+ self.rotation,
272
+ )
273
+
274
+ session_json = PerformanceMonitor().get_session_json(session_id)
275
+ if session_json:
276
+ report_filename = "performance_report.json"
277
+ with open(
278
+ os.path.join(self.map_directory, report_filename), "w", encoding="utf-8"
279
+ ) as file:
280
+ json.dump(session_json, file, indent=4)
281
+ send_performance_report(session_json)
251
282
 
252
283
  def _update_main_settings(self, data: dict[str, Any]) -> None:
253
284
  """Update main settings with provided data.
@@ -0,0 +1,108 @@
1
+ """Module for performance monitoring during map generation."""
2
+
3
+ import functools
4
+ import threading
5
+ import uuid
6
+ from collections import defaultdict
7
+ from contextlib import contextmanager
8
+ from time import perf_counter
9
+ from typing import Callable, Generator
10
+
11
+ from maps4fs.generator.utils import Singleton
12
+
13
+ _local = threading.local()
14
+
15
+
16
+ def get_current_session() -> str | None:
17
+ """Get the current session name from thread-local storage."""
18
+ return getattr(_local, "current_session", None)
19
+
20
+
21
+ @contextmanager
22
+ def performance_session(session_id: str | None = None) -> Generator[str, None, None]:
23
+ """Context manager for performance monitoring session.
24
+
25
+ Arguments:
26
+ session_id (str, optional): Custom session ID. If None, generates UUID.
27
+ """
28
+ if session_id is None:
29
+ session_id = str(uuid.uuid4())
30
+
31
+ _local.current_session = session_id
32
+
33
+ try:
34
+ yield session_id
35
+ finally:
36
+ _local.current_session = None
37
+
38
+
39
+ class PerformanceMonitor(metaclass=Singleton):
40
+ """Singleton class for monitoring performance metrics."""
41
+
42
+ def __init__(self) -> None:
43
+ self.sessions: dict[str, dict[str, dict[str, float]]] = defaultdict(
44
+ lambda: defaultdict(lambda: defaultdict(float))
45
+ )
46
+
47
+ def add_record(self, session: str, component: str, function: str, time_taken: float) -> None:
48
+ """Add a performance record.
49
+
50
+ Arguments:
51
+ session (str): The session name.
52
+ component (str): The component/class name.
53
+ function (str): The function/method name.
54
+ time_taken (float): Time taken in seconds.
55
+ """
56
+ self.sessions[session][component][function] += time_taken
57
+
58
+ def get_session_json(self, session: str) -> dict[str, dict[str, float]]:
59
+ """Get performance data for a session in JSON-serializable format.
60
+
61
+ Arguments:
62
+ session (str): The session name.
63
+
64
+ Returns:
65
+ dict[str, dict[str, float]]: Performance data.
66
+ """
67
+ return self.sessions.get(session, {})
68
+
69
+
70
+ def monitor_performance(func: Callable) -> Callable:
71
+ """Decorator to monitor performance of methods/functions.
72
+
73
+ Arguments:
74
+ func (callable) -- The function to be monitored.
75
+
76
+ Returns:
77
+ callable -- The wrapped function with performance monitoring.
78
+ """
79
+
80
+ @functools.wraps(func)
81
+ def wrapper(*args, **kwargs):
82
+ if args and hasattr(args[0], "__class__"):
83
+ class_name = args[0].__class__.__name__
84
+ elif args and hasattr(args[0], "__name__"):
85
+ class_name = args[0].__name__
86
+ elif "." in func.__qualname__:
87
+ class_name = func.__qualname__.split(".")[0]
88
+ else:
89
+ class_name = None
90
+
91
+ function_name = func.__name__
92
+
93
+ start = perf_counter()
94
+ result = func(*args, **kwargs)
95
+ end = perf_counter()
96
+ time_taken = round(end - start, 5)
97
+
98
+ session_name = get_current_session()
99
+
100
+ try:
101
+ if session_name and time_taken > 0.001 and class_name:
102
+ PerformanceMonitor().add_record(session_name, class_name, function_name, time_taken)
103
+ except Exception:
104
+ pass
105
+
106
+ return result
107
+
108
+ return wrapper
@@ -80,3 +80,13 @@ def send_survey(data: dict[str, Any]) -> None:
80
80
  """
81
81
  endpoint = f"{STATS_HOST}/receive_survey"
82
82
  post(endpoint, data)
83
+
84
+
85
+ def send_performance_report(data: dict[str, Any]) -> None:
86
+ """Send performance report to the statistics server.
87
+
88
+ Arguments:
89
+ data (dict[str, Any]): The performance report data to send.
90
+ """
91
+ endpoint = f"{STATS_HOST}/receive_performance_report"
92
+ post(endpoint, data)
@@ -158,3 +158,14 @@ def dump_json(filename: str, directory: str, data: dict[Any, Any] | Any | None)
158
158
  save_path = os.path.join(directory, filename)
159
159
  with open(save_path, "w", encoding="utf-8") as file:
160
160
  json.dump(data, file, indent=4)
161
+
162
+
163
+ class Singleton(type):
164
+ """A metaclass for creating singleton classes."""
165
+
166
+ _instances: dict[Any, Any] = {}
167
+
168
+ def __call__(cls, *args, **kwargs):
169
+ if cls not in cls._instances:
170
+ cls._instances[cls] = super().__call__(*args, **kwargs)
171
+ return cls._instances[cls]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.8.9
3
+ Version: 2.9.0
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: GNU Affero General Public License v3.0
@@ -12,6 +12,7 @@ maps4fs/generator/__init__.py
12
12
  maps4fs/generator/config.py
13
13
  maps4fs/generator/game.py
14
14
  maps4fs/generator/map.py
15
+ maps4fs/generator/monitor.py
15
16
  maps4fs/generator/qgis.py
16
17
  maps4fs/generator/settings.py
17
18
  maps4fs/generator/statistics.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maps4fs"
7
- version = "2.8.9"
7
+ version = "2.9.0"
8
8
  description = "Generate map templates for Farming Simulator from real places."
9
9
  authors = [{name = "iwatkot", email = "iwatkot@gmail.com"}]
10
10
  license = {text = "GNU Affero General Public License v3.0"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes