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.
- {maps4fs-2.9.38 → maps4fs-2.9.41}/PKG-INFO +2 -3
- {maps4fs-2.9.38 → maps4fs-2.9.41}/README.md +1 -2
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/__init__.py +1 -1
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/building.py +24 -11
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/config.py +18 -6
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/road.py +29 -14
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/texture.py +1 -1
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/config.py +2 -2
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/map.py +77 -54
- maps4fs-2.9.41/maps4fs/generator/monitor.py +217 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/settings.py +2 -2
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/statistics.py +2 -2
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/logger.py +16 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/PKG-INFO +2 -3
- {maps4fs-2.9.38 → maps4fs-2.9.41}/pyproject.toml +1 -1
- maps4fs-2.9.38/maps4fs/generator/monitor.py +0 -118
- {maps4fs-2.9.38 → maps4fs-2.9.41}/LICENSE.md +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/__init__.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/__init__.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/background.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/__init__.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/component.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/component_image.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/component_mesh.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/base/component_xml.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/dem.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/grle.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/i3d.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/layer.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/component/satellite.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/game.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/qgis.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs/generator/utils.py +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/SOURCES.txt +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/dependency_links.txt +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/requires.txt +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/maps4fs.egg-info/top_level.txt +0 -0
- {maps4fs-2.9.38 → maps4fs-2.9.41}/setup.cfg +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
706
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
491
|
-
#
|
|
492
|
-
for i
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
648
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
"Map generation completed in %.2f seconds.",
|
|
264
|
-
generation_finish - generation_start,
|
|
265
|
-
)
|
|
277
|
+
finally:
|
|
278
|
+
self._save_metrics(session_id)
|
|
266
279
|
|
|
267
|
-
|
|
280
|
+
def _save_metrics(self, session_id: str) -> None:
|
|
281
|
+
"""Save logs and performance metrics to JSON files.
|
|
268
282
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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 (
|
|
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:
|
|
294
|
+
tolerance_factor: int = 30
|
|
295
295
|
|
|
296
296
|
|
|
297
297
|
class GenerationSettings(BaseModel):
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|