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