maps4fs 2.9.38__tar.gz → 2.9.41__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.
Files changed (39) hide show
  1. {maps4fs-2.9.38 → maps4fs-2.9.41}/PKG-INFO +2 -3
  2. {maps4fs-2.9.38 → maps4fs-2.9.41}/README.md +1 -2
  3. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/__init__.py +1 -1
  4. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/building.py +24 -11
  5. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/config.py +18 -6
  6. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/road.py +29 -14
  7. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/texture.py +1 -1
  8. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/config.py +2 -2
  9. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/map.py +77 -54
  10. maps4fs-2.9.41/maps4fs/generator/monitor.py +217 -0
  11. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/settings.py +2 -2
  12. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/statistics.py +2 -2
  13. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/logger.py +16 -0
  14. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/PKG-INFO +2 -3
  15. {maps4fs-2.9.38 → maps4fs-2.9.41}/pyproject.toml +1 -1
  16. maps4fs-2.9.38/maps4fs/generator/monitor.py +0 -118
  17. {maps4fs-2.9.38 → maps4fs-2.9.41}/LICENSE.md +0 -0
  18. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/__init__.py +0 -0
  19. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/__init__.py +0 -0
  20. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/background.py +0 -0
  21. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/__init__.py +0 -0
  22. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/component.py +0 -0
  23. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/component_image.py +0 -0
  24. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/component_mesh.py +0 -0
  25. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/component_xml.py +0 -0
  26. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/dem.py +0 -0
  27. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/grle.py +0 -0
  28. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/i3d.py +0 -0
  29. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/layer.py +0 -0
  30. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/satellite.py +0 -0
  31. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/game.py +0 -0
  32. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/qgis.py +0 -0
  33. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/utils.py +0 -0
  34. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/SOURCES.txt +0 -0
  35. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/dependency_links.txt +0 -0
  36. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/requires.txt +0 -0
  37. {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/top_level.txt +0 -0
  38. {maps4fs-2.9.38 → maps4fs-2.9.41}/setup.cfg +0 -0
  39. {maps4fs-2.9.38 → maps4fs-2.9.41}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.9.38
3
+ Version: 2.9.41
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License-Expression: CC-BY-NC-4.0
@@ -94,11 +94,10 @@ Dynamic: license-file
94
94
  📦 **Giants Editor Ready** - Import and start building immediately<br>
95
95
 
96
96
  🗺️ **Advanced Customization** - [Custom OSM maps](https://maps4fs.gitbook.io/docs/advanced-topics/custom_osm) and elevation data<br>
97
- 🔌 **API Integration** - Generate maps programmatically via [API](https://github.com/iwatkot/maps4fsapi)<br>
98
97
  📚 **Complete Documentation** - [Detailed guides](https://maps4fs.gitbook.io/docs) and video tutorials<br>
99
98
 
100
99
  <p align="center">
101
- <img src="https://github.com/iwatkot/maps4fs/releases/download/2.9.34/mfsg.gif"><br>
100
+ <img src="https://github.com/iwatkot/maps4fs/releases/download/2.9.40/mfsg2.gif"><br>
102
101
  <i>Example of map generated with Maps4FS with no manual edits.</i>
103
102
  </p>
104
103
 
@@ -64,11 +64,10 @@
64
64
  📦 **Giants Editor Ready** - Import and start building immediately<br>
65
65
 
66
66
  🗺️ **Advanced Customization** - [Custom OSM maps](https://maps4fs.gitbook.io/docs/advanced-topics/custom_osm) and elevation data<br>
67
- 🔌 **API Integration** - Generate maps programmatically via [API](https://github.com/iwatkot/maps4fsapi)<br>
68
67
  📚 **Complete Documentation** - [Detailed guides](https://maps4fs.gitbook.io/docs) and video tutorials<br>
69
68
 
70
69
  <p align="center">
71
- <img src="https://github.com/iwatkot/maps4fs/releases/download/2.9.34/mfsg.gif"><br>
70
+ <img src="https://github.com/iwatkot/maps4fs/releases/download/2.9.40/mfsg2.gif"><br>
72
71
  <i>Example of map generated with Maps4FS with no manual edits.</i>
73
72
  </p>
74
73
 
@@ -6,5 +6,5 @@ import maps4fs.generator.component as component
6
6
  import maps4fs.generator.settings as settings
7
7
  from maps4fs.generator.game import Game
8
8
  from maps4fs.generator.map import Map
9
+ from maps4fs.generator.monitor import Logger
9
10
  from maps4fs.generator.settings import GenerationSettings, MainSettings
10
- from maps4fs.logger import Logger
@@ -2,7 +2,7 @@
2
2
 
3
3
  import json
4
4
  import os
5
- from typing import NamedTuple
5
+ from typing import Any, NamedTuple
6
6
  from xml.etree import ElementTree as ET
7
7
 
8
8
  import cv2
@@ -320,6 +320,7 @@ class Building(I3d):
320
320
 
321
321
  def preprocess(self) -> None:
322
322
  """Preprocess and prepare buildings schema and buildings map image."""
323
+ self.info: dict[str, Any] = {}
323
324
  try:
324
325
  buildings_schema_path = self.game.buildings_schema
325
326
  except ValueError as e:
@@ -350,7 +351,7 @@ class Building(I3d):
350
351
  else:
351
352
  self.buildings_schema = custom_buildings_schema
352
353
 
353
- self.logger.info(
354
+ self.logger.debug(
354
355
  "Buildings schema loaded successfully with %d objects.", len(self.buildings_schema)
355
356
  )
356
357
 
@@ -398,7 +399,7 @@ class Building(I3d):
398
399
 
399
400
  # Save the buildings map image
400
401
  cv2.imwrite(self.buildings_map_path, buildings_map_image)
401
- self.logger.info("Building categories map saved to: %s", self.buildings_map_path)
402
+ self.logger.debug("Building categories map saved to: %s", self.buildings_map_path)
402
403
 
403
404
  building_entries = []
404
405
  for building_entry in self.buildings_schema:
@@ -418,13 +419,17 @@ class Building(I3d):
418
419
 
419
420
  self.buildings_collection = BuildingEntryCollection(building_entries, region, ignore_region)
420
421
 
422
+ self.info["building_region"] = region
423
+ self.info["ignore_building_region"] = ignore_region
424
+ self.info["total_buildings_in_schema"] = len(self.buildings_collection.entries)
425
+
421
426
  if ignore_region:
422
- self.logger.info(
427
+ self.logger.debug(
423
428
  "Buildings collection created with %d buildings ignoring region restrictions.",
424
429
  len(self.buildings_collection.entries),
425
430
  )
426
431
  else:
427
- self.logger.info(
432
+ self.logger.debug(
428
433
  "Buildings collection created with %d buildings for region '%s'.",
429
434
  len(self.buildings_collection.entries),
430
435
  region,
@@ -465,7 +470,7 @@ class Building(I3d):
465
470
  self.logger.warning("Buildings data not found in textures info layer.")
466
471
  return
467
472
 
468
- self.logger.info("Found %d building entries to process.", len(buildings))
473
+ self.logger.debug("Found %d building entries to process.", len(buildings))
469
474
 
470
475
  # Initialize tracking for XML modifications
471
476
  tree = self.get_tree()
@@ -537,7 +542,7 @@ class Building(I3d):
537
542
  category=category,
538
543
  width=width,
539
544
  depth=depth,
540
- tolerance=self.map.building_settings.tolerance_factor,
545
+ tolerance=self.map.building_settings.tolerance_factor / 100,
541
546
  )
542
547
 
543
548
  if best_match:
@@ -626,11 +631,14 @@ class Building(I3d):
626
631
  continue
627
632
 
628
633
  added_buildings_count = node_id_counter - (BUILDINGS_STARTING_NODE_ID + 1000)
629
- self.logger.info("Total buildings placed: %d of %d", added_buildings_count, len(buildings))
634
+ self.logger.debug("Total buildings placed: %d of %d", added_buildings_count, len(buildings))
635
+
636
+ self.info["total_buildings_placed"] = added_buildings_count
637
+ self.info["total_buildings_attempted"] = len(buildings)
630
638
 
631
639
  # Save the modified XML tree
632
640
  self.save_tree(tree)
633
- self.logger.info("Buildings placement completed and saved to map.i3d")
641
+ self.logger.debug("Buildings placement completed and saved to map.i3d")
634
642
 
635
643
  def _get_polygon_dimensions_and_rotation(
636
644
  self, polygon_points: np.ndarray
@@ -702,5 +710,10 @@ class Building(I3d):
702
710
  scene_node.append(buildings_group)
703
711
  return buildings_group
704
712
 
705
- def info_sequence(self) -> dict[str, dict[str, str | float | int]]:
706
- return {}
713
+ def info_sequence(self) -> dict[str, Any]:
714
+ """Return information about the building processing as a dictionary.
715
+
716
+ Returns:
717
+ dict[str, Any]: Information about building processing.
718
+ """
719
+ return self.info
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ from typing import Any
6
7
 
7
8
  import cv2
8
9
  import numpy as np
@@ -40,6 +41,7 @@ class Config(XMLComponent, ImageComponent):
40
41
 
41
42
  def preprocess(self) -> None:
42
43
  """Gets the path to the map XML file and saves it to the instance variable."""
44
+ self.info: dict[str, Any] = {}
43
45
  self.xml_path = self.game.map_xml_path(self.map_directory)
44
46
  self.fog_parameters: dict[str, int] = {}
45
47
 
@@ -106,6 +108,8 @@ class Config(XMLComponent, ImageComponent):
106
108
  if self.fog_parameters:
107
109
  data["Fog"] = self.fog_parameters # type: ignore
108
110
 
111
+ data.update(self.info)
112
+
109
113
  return data # type: ignore
110
114
 
111
115
  def qgis_sequence(self) -> None:
@@ -360,6 +364,7 @@ class Config(XMLComponent, ImageComponent):
360
364
  return
361
365
 
362
366
  country_name = mfsutils.get_country_by_coordinates(self.map.coordinates).lower()
367
+ self.info["license_plate_country_name"] = country_name
363
368
  if country_name not in self.supported_countries:
364
369
  self.logger.warning(
365
370
  "License plates processing is not supported for country: %s.", country_name
@@ -369,8 +374,10 @@ class Config(XMLComponent, ImageComponent):
369
374
  # Get license plate country code and EU format.
370
375
  country_code = self.supported_countries[country_name]
371
376
  eu_format = country_code in self.eu_countries
377
+ self.info["license_plate_country_code"] = country_code
378
+ self.info["license_plate_eu_format"] = eu_format
372
379
 
373
- self.logger.info(
380
+ self.logger.debug(
374
381
  "Updating license plates for country: %s, EU format: %s",
375
382
  country_name,
376
383
  eu_format,
@@ -402,7 +409,7 @@ class Config(XMLComponent, ImageComponent):
402
409
  COUNTRY_CODE_BOTTOM,
403
410
  )
404
411
 
405
- self.logger.info("License plates updated successfully")
412
+ self.logger.debug("License plates updated successfully")
406
413
  except Exception as e:
407
414
  self.logger.error("Failed to update license plates: %s", e)
408
415
  return
@@ -484,12 +491,17 @@ class Config(XMLComponent, ImageComponent):
484
491
  # 1. Update license plate prefix to ensure max 3 letters, uppercase.
485
492
  license_plate_prefix = license_plate_prefix.upper()[:3]
486
493
 
487
- # 2. Position X values for the letters.
494
+ # 2. Pad the prefix to exactly 3 characters with spaces if needed.
495
+ license_plate_prefix = license_plate_prefix.ljust(3)
496
+ self.info["license_plate_prefix"] = license_plate_prefix
497
+
498
+ # 3. Position X values for the letters.
488
499
  pos_x_values = ["-0.1712", "-0.1172", "-0.0632"] # ? DO WE REALLY NEED THEM?
489
500
 
490
- # 3. Update only the first 3 values (prefix letters), leave others intact.
491
- # Find and update nodes 0|0, 0|1, 0|2 specifically.
492
- for i, letter in enumerate(license_plate_prefix):
501
+ # 4. Update all 3 positions (0|0, 0|1, 0|2) to ensure proper formatting.
502
+ # Always process exactly 3 positions, padding with spaces as needed.
503
+ for i in range(3):
504
+ letter = license_plate_prefix[i] # This will be a space if padding was applied
493
505
  target_node = f"0|{i}"
494
506
  # Find existing value with this node ID.
495
507
  existing_value = None
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  import shutil
5
5
  from collections import defaultdict
6
- from typing import NamedTuple
6
+ from typing import Any, NamedTuple
7
7
 
8
8
  import numpy as np
9
9
  import shapely
@@ -42,6 +42,7 @@ class Road(I3d, MeshComponent):
42
42
 
43
43
  def preprocess(self) -> None:
44
44
  """Preprocess the road data before generation."""
45
+ self.info: dict[str, Any] = {}
45
46
 
46
47
  def process(self):
47
48
  """Process and generate roads for the map."""
@@ -63,8 +64,13 @@ class Road(I3d, MeshComponent):
63
64
  if road_texture:
64
65
  roads_by_texture[road_texture].append(road_info)
65
66
 
67
+ self.info["road_textures"] = list(roads_by_texture.keys())
68
+ self.info["total_OSM_roads"] = len(road_infos)
69
+
70
+ fitted_roads_count = 0
71
+ patches_created_count = 0
66
72
  for texture, roads_polylines in roads_by_texture.items():
67
- self.logger.info("Processing roads with texture: %s", texture)
73
+ self.logger.debug("Processing roads with texture: %s", texture)
68
74
 
69
75
  # The texture name is represents the name of texture file without extension
70
76
  # for easy reference if the texture uses various extensions.
@@ -106,9 +112,10 @@ class Road(I3d, MeshComponent):
106
112
 
107
113
  road_entries.append(RoadEntry(linestring=linestring, width=width))
108
114
 
109
- self.logger.info("Total found for mesh generation: %d", len(road_entries))
115
+ self.logger.debug("Total found for mesh generation: %d", len(road_entries))
110
116
 
111
117
  if road_entries:
118
+ fitted_roads_count += len(road_entries)
112
119
  # 1. Apply smart interpolation to make linestrings smoother,
113
120
  # but carefully, ensuring that points are not too close to each other.
114
121
  # Otherwise it may lead to artifacts in the mesh.
@@ -123,9 +130,13 @@ class Road(I3d, MeshComponent):
123
130
  patches_road_entries: list[RoadEntry] = self.get_patches_linestrings(
124
131
  split_road_entries
125
132
  )
133
+ patches_created_count += len(patches_road_entries)
126
134
  split_road_entries.extend(patches_road_entries)
127
135
  self.generate_road_mesh(split_road_entries, texture)
128
136
 
137
+ self.info["total_fitted_roads"] = fitted_roads_count
138
+ self.info["total_patches_created"] = patches_created_count
139
+
129
140
  def smart_interpolation(self, road_entries: list[RoadEntry]) -> list[RoadEntry]:
130
141
  """Apply smart interpolation to road linestrings.
131
142
  Making sure that result polylines do not have points too close to each other.
@@ -214,7 +225,7 @@ class Road(I3d, MeshComponent):
214
225
  )
215
226
  interpolated_entries.append(RoadEntry(linestring, width, z_offset))
216
227
 
217
- self.logger.info(
228
+ self.logger.debug(
218
229
  "Smart interpolation complete. Processed %d roads.", len(interpolated_entries)
219
230
  )
220
231
  return interpolated_entries
@@ -249,7 +260,7 @@ class Road(I3d, MeshComponent):
249
260
  num_segments = int(np.ceil(road_length / max_road_length))
250
261
  segment_length = road_length / num_segments
251
262
 
252
- self.logger.info(
263
+ self.logger.debug(
253
264
  "Splitting road (%.2fm) into %d segments of ~%.2fm each",
254
265
  road_length,
255
266
  num_segments,
@@ -276,7 +287,7 @@ class Road(I3d, MeshComponent):
276
287
  except Exception as e:
277
288
  self.logger.warning("Failed to split road segment %d: %s", i, e)
278
289
 
279
- self.logger.info(
290
+ self.logger.debug(
280
291
  "Road splitting complete: %d roads -> %d segments",
281
292
  len(road_entries),
282
293
  len(split_entries),
@@ -376,7 +387,7 @@ class Road(I3d, MeshComponent):
376
387
  self.logger.debug("Failed to create patch linestring: %s", e)
377
388
  continue
378
389
 
379
- self.logger.info("Generated %d patch segments for T-junctions", len(patches))
390
+ self.logger.debug("Generated %d patch segments for T-junctions", len(patches))
380
391
  return patches
381
392
 
382
393
  def find_texture_file(self, templates_directory: str, texture_base_name: str) -> str:
@@ -419,7 +430,7 @@ class Road(I3d, MeshComponent):
419
430
  )
420
431
 
421
432
  shutil.copyfile(texture_path, dst_texture_path)
422
- self.logger.info("Texture copied to %s", dst_texture_path)
433
+ self.logger.debug("Texture copied to %s", dst_texture_path)
423
434
 
424
435
  obj_output_path = os.path.join(road_mesh_directory, f"roads_{texture}.obj")
425
436
  mtl_output_path = os.path.join(road_mesh_directory, f"roads_{texture}.mtl")
@@ -483,7 +494,7 @@ class Road(I3d, MeshComponent):
483
494
  texture_tile_size = 10.0 # meters - how many meters before texture repeats
484
495
 
485
496
  patches_count = sum(1 for entry in road_entries if entry.z_offset > 0)
486
- self.logger.info(
497
+ self.logger.debug(
487
498
  "Creating mesh for %d roads (%d patches with z-offset)",
488
499
  len(road_entries),
489
500
  patches_count,
@@ -607,7 +618,7 @@ class Road(I3d, MeshComponent):
607
618
  mtl_file.write("illum 2\n") # Illumination model
608
619
  mtl_file.write(f"map_Kd {texture_filename}\n") # Diffuse texture map
609
620
 
610
- self.logger.info("MTL file written to %s", mtl_output_path)
621
+ self.logger.debug("MTL file written to %s", mtl_output_path)
611
622
 
612
623
  # Write OBJ file
613
624
  with open(obj_output_path, "w", encoding="utf-8") as obj_file:
@@ -636,13 +647,17 @@ class Road(I3d, MeshComponent):
636
647
  f"{face[2] + 1}/{face[2] + 1}\n"
637
648
  )
638
649
 
639
- self.logger.info(
650
+ self.logger.debug(
640
651
  "OBJ file written to %s with %d vertices and %d faces",
641
652
  obj_output_path,
642
653
  len(vertices),
643
654
  len(faces),
644
655
  )
645
656
 
646
- def info_sequence(self):
647
- """Returns information about the component."""
648
- return {}
657
+ def info_sequence(self) -> dict[str, Any]:
658
+ """Returns information about the road processing as a dictionary.
659
+
660
+ Returns:
661
+ dict[str, Any]: Information about road processing.
662
+ """
663
+ return self.info
@@ -846,7 +846,7 @@ class Texture(ImageComponent):
846
846
  else:
847
847
  objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
848
848
  except Exception as e:
849
- self.logger.warning("Error fetching objects for tags: %s. Error: %s.", tags, e)
849
+ self.logger.debug("Error fetching objects for tags: %s. Error: %s.", tags, e)
850
850
  return None
851
851
 
852
852
  return objects
@@ -7,11 +7,11 @@ import tempfile
7
7
 
8
8
  from osmnx import settings as ox_settings
9
9
 
10
- from maps4fs.logger import Logger
10
+ from maps4fs.generator.monitor import Logger
11
11
 
12
12
  TQDM_DISABLE = os.getenv("TQDM_DISABLE", "0") == "1"
13
13
 
14
- logger = Logger()
14
+ logger = Logger(name="MAPS4FS.CONFIG")
15
15
 
16
16
  MFS_TEMPLATES_DIR = os.path.join(os.getcwd(), "templates")
17
17
 
@@ -15,14 +15,13 @@ import maps4fs.generator.config as mfscfg
15
15
  import maps4fs.generator.utils as mfsutils
16
16
  from maps4fs.generator.component import Background, Component, Layer, Satellite, Texture
17
17
  from maps4fs.generator.game import Game
18
- from maps4fs.generator.monitor import PerformanceMonitor, performance_session
18
+ from maps4fs.generator.monitor import Logger, PerformanceMonitor, performance_session
19
19
  from maps4fs.generator.settings import GenerationSettings, MainSettings, SharedSettings
20
20
  from maps4fs.generator.statistics import (
21
21
  send_advanced_settings,
22
22
  send_main_settings,
23
23
  send_performance_report,
24
24
  )
25
- from maps4fs.logger import Logger
26
25
 
27
26
 
28
27
  class Map:
@@ -222,66 +221,90 @@ class Map:
222
221
  )
223
222
  generation_start = perf_counter()
224
223
 
225
- for game_component in self.game.components:
226
- component = game_component(
227
- self.game,
228
- self,
224
+ try:
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
237
+ )
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
+ )
266
+
267
+ self._update_main_settings({"completed": True})
268
+
269
+ self.logger.info(
270
+ "Map generation completed. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
271
+ self.game.code,
229
272
  self.coordinates,
230
273
  self.size,
231
- self.rotated_size,
232
274
  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
237
275
  )
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
276
 
261
- generation_finish = perf_counter()
262
- self.logger.info(
263
- "Map generation completed in %.2f seconds.",
264
- generation_finish - generation_start,
265
- )
277
+ finally:
278
+ self._save_metrics(session_id)
266
279
 
267
- self._update_main_settings({"completed": True})
280
+ def _save_metrics(self, session_id: str) -> None:
281
+ """Save logs and performance metrics to JSON files.
268
282
 
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
- )
283
+ Arguments:
284
+ session_id (str): Session ID.
285
+ """
286
+ try:
287
+ logs_json = self.logger.group_by_level(session_id)
288
+ if logs_json:
289
+ logs_filename = "generation_logs.json"
290
+ with open(
291
+ os.path.join(self.map_directory, logs_filename), "w", encoding="utf-8"
292
+ ) as file:
293
+ json.dump(logs_json, file, indent=4)
294
+ except Exception as e:
295
+ self.logger.error("Error saving logs to JSON: %s", e)
276
296
 
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)
297
+ try:
298
+ session_json = PerformanceMonitor().pop_session_json(session_id)
299
+ if session_json:
300
+ report_filename = "performance_report.json"
301
+ with open(
302
+ os.path.join(self.map_directory, report_filename), "w", encoding="utf-8"
303
+ ) as file:
304
+ json.dump(session_json, file, indent=4)
305
+ send_performance_report(session_json)
306
+ except Exception as e:
307
+ self.logger.error("Error saving performance report to JSON: %s", e)
285
308
 
286
309
  def _update_main_settings(self, data: dict[str, Any]) -> None:
287
310
  """Update main settings with provided data.
@@ -0,0 +1,217 @@
1
+ """Module for performance monitoring during map generation."""
2
+
3
+ import functools
4
+ import logging
5
+ import os
6
+ import sys
7
+ import threading
8
+ import uuid
9
+ from collections import defaultdict
10
+ from contextlib import contextmanager
11
+ from datetime import datetime
12
+ from time import perf_counter
13
+ from typing import Callable, Generator, Literal
14
+
15
+ from maps4fs.generator.utils import Singleton
16
+
17
+ _local = threading.local()
18
+ MFS_LOG_LEVEL = "MFS_LOG_LEVEL"
19
+ SUPPORTED_LOG_LEVELS = {
20
+ 10: "DEBUG",
21
+ 20: "INFO",
22
+ 30: "WARNING",
23
+ 40: "ERROR",
24
+ }
25
+
26
+
27
+ class Logger(logging.Logger):
28
+ """Handles logging to stdout with timestamps and session tracking."""
29
+
30
+ def __init__(
31
+ self,
32
+ name: str = "MAPS4FS",
33
+ level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO",
34
+ **kwargs,
35
+ ):
36
+ log_level = os.getenv(MFS_LOG_LEVEL, level)
37
+ if log_level not in SUPPORTED_LOG_LEVELS.values():
38
+ log_level = "INFO"
39
+ super().__init__(name)
40
+ self.setLevel(level)
41
+
42
+ # Standard stdout handler
43
+ self.stdout_handler = logging.StreamHandler(sys.stdout)
44
+ formatter = "%(name)s | %(levelname)s | %(asctime)s | %(message)s"
45
+ self.fmt = formatter
46
+ self.stdout_handler.setFormatter(logging.Formatter(formatter))
47
+ self.addHandler(self.stdout_handler)
48
+
49
+ # Session storage - simple dict of lists
50
+ self.session_logs: dict[str, list[dict[str, str]]] = defaultdict(list)
51
+
52
+ def _capture_to_session(self, level: int, msg, args):
53
+ """Capture log to session storage regardless of logger level."""
54
+ try:
55
+ session_id = get_current_session()
56
+ if session_id:
57
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]
58
+ formatted_msg = msg % args if args else str(msg)
59
+ level_name = SUPPORTED_LOG_LEVELS.get(level, "INFO")
60
+ log_entry = {"level": level_name, "timestamp": timestamp, "message": formatted_msg}
61
+ self.session_logs[session_id].append(log_entry)
62
+ except Exception:
63
+ pass
64
+
65
+ def debug(self, msg, *args, **kwargs):
66
+ """Override debug to always capture in session storage."""
67
+ self._capture_to_session(logging.DEBUG, msg, args)
68
+ super().debug(msg, *args, **kwargs)
69
+
70
+ def info(self, msg, *args, **kwargs):
71
+ """Override info to always capture in session storage."""
72
+ self._capture_to_session(logging.INFO, msg, args)
73
+ super().info(msg, *args, **kwargs)
74
+
75
+ def warning(self, msg, *args, **kwargs):
76
+ """Override warning to always capture in session storage."""
77
+ self._capture_to_session(logging.WARNING, msg, args)
78
+ super().warning(msg, *args, **kwargs)
79
+
80
+ def error(self, msg, *args, **kwargs):
81
+ """Override error to always capture in session storage."""
82
+ self._capture_to_session(logging.ERROR, msg, args)
83
+ super().error(msg, *args, **kwargs)
84
+
85
+ def pop_session_logs(self, session_id: str) -> list[dict[str, str]]:
86
+ """Pop logs for a specific session.
87
+
88
+ Arguments:
89
+ session_id (str): The session ID.
90
+
91
+ Returns:
92
+ list[dict[str, str]]: List of log entries for the session.
93
+ """
94
+ return self.session_logs.pop(session_id, [])
95
+
96
+ def group_by_level(self, session_id: str) -> dict[str, list[dict[str, str]]]:
97
+ """Group logs by level for a specific session.
98
+
99
+ Arguments:
100
+ session_id (str): The session ID.
101
+
102
+ Returns:
103
+ dict[str, list[dict[str, str]]]: Logs grouped by level.
104
+ """
105
+ session_logs = self.pop_session_logs(session_id)
106
+ grouped_logs: dict[str, list[dict[str, str]]] = defaultdict(list)
107
+ for log in session_logs:
108
+ level = log.get("level")
109
+ if level:
110
+ grouped_logs[level].append(log)
111
+
112
+ return grouped_logs
113
+
114
+
115
+ logger = Logger(name="MAPS4FS_MONITOR")
116
+
117
+
118
+ def get_current_session() -> str | None:
119
+ """Get the current session name from thread-local storage."""
120
+ return getattr(_local, "current_session", None)
121
+
122
+
123
+ @contextmanager
124
+ def performance_session(session_id: str | None = None) -> Generator[str, None, None]:
125
+ """Context manager for performance monitoring session.
126
+
127
+ Arguments:
128
+ session_id (str, optional): Custom session ID. If None, generates UUID.
129
+ """
130
+ if session_id is None:
131
+ session_id = str(uuid.uuid4())
132
+
133
+ _local.current_session = session_id
134
+
135
+ try:
136
+ yield session_id
137
+ finally:
138
+ _local.current_session = None
139
+
140
+
141
+ class PerformanceMonitor(metaclass=Singleton):
142
+ """Singleton class for monitoring performance metrics."""
143
+
144
+ def __init__(self) -> None:
145
+ self.sessions: dict[str, dict[str, dict[str, float]]] = defaultdict(
146
+ lambda: defaultdict(lambda: defaultdict(float))
147
+ )
148
+
149
+ def add_record(self, session: str, component: str, function: str, time_taken: float) -> None:
150
+ """Add a performance record.
151
+
152
+ Arguments:
153
+ session (str): The session name.
154
+ component (str): The component/class name.
155
+ function (str): The function/method name.
156
+ time_taken (float): Time taken in seconds.
157
+ """
158
+ self.sessions[session][component][function] += time_taken
159
+
160
+ def pop_session_json(self, session: str) -> dict[str, dict[str, float]]:
161
+ """Pop performance data for a session in JSON-serializable format.
162
+
163
+ Arguments:
164
+ session (str): The session name.
165
+
166
+ Returns:
167
+ dict[str, dict[str, float]]: Performance data.
168
+ """
169
+ return self.sessions.pop(session, {})
170
+
171
+
172
+ def monitor_performance(func: Callable) -> Callable:
173
+ """Decorator to monitor performance of methods/functions.
174
+
175
+ Arguments:
176
+ func (callable) -- The function to be monitored.
177
+
178
+ Returns:
179
+ callable -- The wrapped function with performance monitoring.
180
+ """
181
+
182
+ @functools.wraps(func)
183
+ def wrapper(*args, **kwargs):
184
+ if args and hasattr(args[0], "__class__"):
185
+ class_name = args[0].__class__.__name__
186
+ elif args and hasattr(args[0], "__name__"):
187
+ class_name = args[0].__name__
188
+ elif "." in func.__qualname__:
189
+ class_name = func.__qualname__.split(".")[0]
190
+ else:
191
+ class_name = None
192
+
193
+ function_name = func.__name__
194
+
195
+ start = perf_counter()
196
+ result = func(*args, **kwargs)
197
+ end = perf_counter()
198
+ time_taken = round(end - start, 5)
199
+
200
+ session_name = get_current_session()
201
+
202
+ try:
203
+ if session_name and time_taken > 0.001 and class_name:
204
+ PerformanceMonitor().add_record(session_name, class_name, function_name, time_taken)
205
+ logger.debug(
206
+ "[PERFORMANCE] %s | %s | %s | %s",
207
+ session_name,
208
+ class_name,
209
+ function_name,
210
+ time_taken,
211
+ )
212
+ except Exception:
213
+ pass
214
+
215
+ return result
216
+
217
+ return wrapper
@@ -285,13 +285,13 @@ class BuildingSettings(SettingsModel):
285
285
  Attributes:
286
286
  generate_buildings (bool): generate buildings on the map.
287
287
  region (Literal["auto", "all", "EU", "US"]): region for the buildings.
288
- tolerance_factor (float): tolerance factor representing allowed dimension difference
288
+ tolerance_factor (int): tolerance factor representing allowed dimension difference
289
289
  between OSM building footprint and the building model footprint.
290
290
  """
291
291
 
292
292
  generate_buildings: bool = True
293
293
  region: Literal["auto", "all", "EU", "US"] = "auto"
294
- tolerance_factor: float = 0.3
294
+ tolerance_factor: int = 30
295
295
 
296
296
 
297
297
  class GenerationSettings(BaseModel):
@@ -6,9 +6,9 @@ from typing import Any
6
6
 
7
7
  import requests
8
8
 
9
- from maps4fs.logger import Logger
9
+ from maps4fs.generator.monitor import Logger
10
10
 
11
- logger = Logger()
11
+ logger = Logger(name="MAPS4FS.STATISTICS")
12
12
 
13
13
  try:
14
14
  from dotenv import load_dotenv
@@ -1,5 +1,6 @@
1
1
  """This module contains the Logger class for logging to the file and stdout."""
2
2
 
3
+ import inspect
3
4
  import logging
4
5
  import sys
5
6
  from typing import Literal
@@ -16,6 +17,21 @@ class Logger(logging.Logger):
16
17
  to_stdout: bool = True,
17
18
  **kwargs,
18
19
  ):
20
+ print(
21
+ "The maps4fs.logger.Logger class is deprecated and will be removed in future versions. "
22
+ "Switch to maps4fs.generator.monitoring.Logger instead.",
23
+ )
24
+
25
+ # Show detailed information from where the instantiation is happening.
26
+ frame = inspect.currentframe()
27
+ if frame is not None:
28
+ caller_frame = frame.f_back
29
+ if caller_frame is not None:
30
+ info = inspect.getframeinfo(caller_frame)
31
+ print(
32
+ f"Logger instantiated from file: {info.filename}, line: {info.lineno}, function: {info.function}"
33
+ )
34
+
19
35
  super().__init__(kwargs.pop("name", LOGGER_NAME))
20
36
  self.setLevel(level)
21
37
  self.stdout_handler = logging.StreamHandler(sys.stdout)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.9.38
3
+ Version: 2.9.41
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License-Expression: CC-BY-NC-4.0
@@ -94,11 +94,10 @@ Dynamic: license-file
94
94
  📦 **Giants Editor Ready** - Import and start building immediately<br>
95
95
 
96
96
  🗺️ **Advanced Customization** - [Custom OSM maps](https://maps4fs.gitbook.io/docs/advanced-topics/custom_osm) and elevation data<br>
97
- 🔌 **API Integration** - Generate maps programmatically via [API](https://github.com/iwatkot/maps4fsapi)<br>
98
97
  📚 **Complete Documentation** - [Detailed guides](https://maps4fs.gitbook.io/docs) and video tutorials<br>
99
98
 
100
99
  <p align="center">
101
- <img src="https://github.com/iwatkot/maps4fs/releases/download/2.9.34/mfsg.gif"><br>
100
+ <img src="https://github.com/iwatkot/maps4fs/releases/download/2.9.40/mfsg2.gif"><br>
102
101
  <i>Example of map generated with Maps4FS with no manual edits.</i>
103
102
  </p>
104
103
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maps4fs"
7
- version = "2.9.38"
7
+ version = "2.9.41"
8
8
  description = "Generate map templates for Farming Simulator from real places."
9
9
  authors = [{name = "iwatkot", email = "iwatkot@gmail.com"}]
10
10
  readme = "README.md"
@@ -1,118 +0,0 @@
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
- from maps4fs.logger import Logger
13
-
14
- logger = Logger(name="MAPS4FS_MONITOR")
15
-
16
- _local = threading.local()
17
-
18
-
19
- def get_current_session() -> str | None:
20
- """Get the current session name from thread-local storage."""
21
- return getattr(_local, "current_session", None)
22
-
23
-
24
- @contextmanager
25
- def performance_session(session_id: str | None = None) -> Generator[str, None, None]:
26
- """Context manager for performance monitoring session.
27
-
28
- Arguments:
29
- session_id (str, optional): Custom session ID. If None, generates UUID.
30
- """
31
- if session_id is None:
32
- session_id = str(uuid.uuid4())
33
-
34
- _local.current_session = session_id
35
-
36
- try:
37
- yield session_id
38
- finally:
39
- _local.current_session = None
40
-
41
-
42
- class PerformanceMonitor(metaclass=Singleton):
43
- """Singleton class for monitoring performance metrics."""
44
-
45
- def __init__(self) -> None:
46
- self.sessions: dict[str, dict[str, dict[str, float]]] = defaultdict(
47
- lambda: defaultdict(lambda: defaultdict(float))
48
- )
49
-
50
- def add_record(self, session: str, component: str, function: str, time_taken: float) -> None:
51
- """Add a performance record.
52
-
53
- Arguments:
54
- session (str): The session name.
55
- component (str): The component/class name.
56
- function (str): The function/method name.
57
- time_taken (float): Time taken in seconds.
58
- """
59
- self.sessions[session][component][function] += time_taken
60
-
61
- def get_session_json(self, session: str) -> dict[str, dict[str, float]]:
62
- """Get performance data for a session in JSON-serializable format.
63
-
64
- Arguments:
65
- session (str): The session name.
66
-
67
- Returns:
68
- dict[str, dict[str, float]]: Performance data.
69
- """
70
- return self.sessions.get(session, {})
71
-
72
-
73
- def monitor_performance(func: Callable) -> Callable:
74
- """Decorator to monitor performance of methods/functions.
75
-
76
- Arguments:
77
- func (callable) -- The function to be monitored.
78
-
79
- Returns:
80
- callable -- The wrapped function with performance monitoring.
81
- """
82
-
83
- @functools.wraps(func)
84
- def wrapper(*args, **kwargs):
85
- if args and hasattr(args[0], "__class__"):
86
- class_name = args[0].__class__.__name__
87
- elif args and hasattr(args[0], "__name__"):
88
- class_name = args[0].__name__
89
- elif "." in func.__qualname__:
90
- class_name = func.__qualname__.split(".")[0]
91
- else:
92
- class_name = None
93
-
94
- function_name = func.__name__
95
-
96
- start = perf_counter()
97
- result = func(*args, **kwargs)
98
- end = perf_counter()
99
- time_taken = round(end - start, 5)
100
-
101
- session_name = get_current_session()
102
-
103
- try:
104
- if session_name and time_taken > 0.001 and class_name:
105
- PerformanceMonitor().add_record(session_name, class_name, function_name, time_taken)
106
- logger.debug(
107
- "[PERFORMANCE] %s | %s | %s | %s",
108
- session_name,
109
- class_name,
110
- function_name,
111
- time_taken,
112
- )
113
- except Exception:
114
- pass
115
-
116
- return result
117
-
118
- return wrapper
File without changes
File without changes