maps4fs 2.8.9__py3-none-any.whl → 2.9.1__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.
Potentially problematic release.
This version of maps4fs might be problematic. Click here for more details.
- maps4fs/generator/component/background.py +103 -35
- maps4fs/generator/component/base/component_mesh.py +48 -11
- maps4fs/generator/component/config.py +5 -0
- maps4fs/generator/component/dem.py +7 -0
- maps4fs/generator/component/grle.py +9 -0
- maps4fs/generator/component/i3d.py +4 -0
- maps4fs/generator/component/satellite.py +3 -0
- maps4fs/generator/component/texture.py +73 -46
- maps4fs/generator/map.py +70 -39
- maps4fs/generator/monitor.py +118 -0
- maps4fs/generator/statistics.py +10 -0
- maps4fs/generator/utils.py +11 -0
- maps4fs/logger.py +2 -1
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.1.dist-info}/METADATA +1 -1
- maps4fs-2.9.1.dist-info/RECORD +30 -0
- maps4fs-2.8.9.dist-info/RECORD +0 -29
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.1.dist-info}/WHEEL +0 -0
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.1.dist-info}/licenses/LICENSE.md +0 -0
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.1.dist-info}/top_level.txt +0 -0
|
@@ -19,6 +19,7 @@ from maps4fs.generator.component.base.component_image import ImageComponent
|
|
|
19
19
|
from maps4fs.generator.component.base.component_mesh import MeshComponent
|
|
20
20
|
from maps4fs.generator.component.dem import DEM
|
|
21
21
|
from maps4fs.generator.component.texture import Texture
|
|
22
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
22
23
|
from maps4fs.generator.settings import Parameters
|
|
23
24
|
|
|
24
25
|
SEGMENT_LENGTH = 2
|
|
@@ -38,6 +39,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
38
39
|
info, warning. If not provided, default logging will be used.
|
|
39
40
|
"""
|
|
40
41
|
|
|
42
|
+
@monitor_performance
|
|
41
43
|
def preprocess(self) -> None:
|
|
42
44
|
"""Registers the DEMs for the background terrain."""
|
|
43
45
|
self.stl_preview_path = os.path.join(self.previews_directory, "background_dem.stl")
|
|
@@ -76,6 +78,9 @@ class Background(MeshComponent, ImageComponent):
|
|
|
76
78
|
self.background_directory, "not_substracted.png"
|
|
77
79
|
)
|
|
78
80
|
self.not_resized_path: str = os.path.join(self.background_directory, "not_resized.png")
|
|
81
|
+
self.not_resized_with_foundations_path: str = os.path.join(
|
|
82
|
+
self.background_directory, "not_resized_with_foundations.png"
|
|
83
|
+
)
|
|
79
84
|
|
|
80
85
|
self.flatten_water_to: int | None = None
|
|
81
86
|
|
|
@@ -141,6 +146,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
141
146
|
"Mesh processing is disabled for the game, skipping water mesh processing."
|
|
142
147
|
)
|
|
143
148
|
|
|
149
|
+
@monitor_performance
|
|
144
150
|
def create_foundations(self, dem_image: np.ndarray) -> np.ndarray:
|
|
145
151
|
"""Creates foundations for buildings based on the DEM data.
|
|
146
152
|
|
|
@@ -246,6 +252,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
246
252
|
)
|
|
247
253
|
self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
|
|
248
254
|
|
|
255
|
+
@monitor_performance
|
|
249
256
|
def generate_obj_files(self) -> None:
|
|
250
257
|
"""Iterates over all dems and generates 3D obj files based on DEM data.
|
|
251
258
|
If at least one DEM file is missing, the generation will be stopped at all.
|
|
@@ -328,8 +335,9 @@ class Background(MeshComponent, ImageComponent):
|
|
|
328
335
|
return resolution
|
|
329
336
|
return Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE
|
|
330
337
|
|
|
338
|
+
@monitor_performance
|
|
331
339
|
def decimate_background_mesh(self) -> None:
|
|
332
|
-
"""
|
|
340
|
+
"""Decimates the background mesh based on the map size."""
|
|
333
341
|
if not self.assets.background_mesh or not os.path.isfile(self.assets.background_mesh):
|
|
334
342
|
self.logger.warning("Background mesh not found, cannot generate i3d background.")
|
|
335
343
|
return
|
|
@@ -362,6 +370,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
362
370
|
|
|
363
371
|
self.assets.decimated_background_mesh = decimated_save_path
|
|
364
372
|
|
|
373
|
+
@monitor_performance
|
|
365
374
|
def texture_background_mesh(self) -> None:
|
|
366
375
|
"""Textures the background mesh using satellite imagery."""
|
|
367
376
|
satellite_component = self.map.get_satellite_component()
|
|
@@ -427,6 +436,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
427
436
|
self.logger.error("Could not texture background mesh: %s", e)
|
|
428
437
|
return
|
|
429
438
|
|
|
439
|
+
@monitor_performance
|
|
430
440
|
def convert_background_mesh_to_i3d(self) -> bool:
|
|
431
441
|
"""Converts the textured background mesh to i3d format.
|
|
432
442
|
|
|
@@ -498,6 +508,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
498
508
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
499
509
|
f.write(content + "\n\n" + note)
|
|
500
510
|
|
|
511
|
+
@monitor_performance
|
|
501
512
|
def convert_water_mesh_to_i3d(self) -> bool:
|
|
502
513
|
"""Converts the line-based water mesh to i3d format.
|
|
503
514
|
|
|
@@ -553,6 +564,10 @@ class Background(MeshComponent, ImageComponent):
|
|
|
553
564
|
|
|
554
565
|
if self.map.dem_settings.add_foundations:
|
|
555
566
|
dem_data = self.create_foundations(dem_data)
|
|
567
|
+
cv2.imwrite(self.not_resized_with_foundations_path, dem_data)
|
|
568
|
+
self.logger.debug(
|
|
569
|
+
"Not resized DEM with foundations saved: %s", self.not_resized_with_foundations_path
|
|
570
|
+
)
|
|
556
571
|
|
|
557
572
|
output_size = self.scaled_size + 1
|
|
558
573
|
|
|
@@ -730,6 +745,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
730
745
|
cv2.imwrite(colored_dem_path, dem_data_colored)
|
|
731
746
|
return colored_dem_path
|
|
732
747
|
|
|
748
|
+
@monitor_performance
|
|
733
749
|
def create_background_textures(self) -> None:
|
|
734
750
|
"""Creates background textures for the map."""
|
|
735
751
|
layers_schema = self.map.texture_schema
|
|
@@ -782,6 +798,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
782
798
|
|
|
783
799
|
cv2.imwrite(self.water_resources_path, background_image)
|
|
784
800
|
|
|
801
|
+
@monitor_performance
|
|
785
802
|
def subtraction(self) -> None:
|
|
786
803
|
"""Subtracts the water depth from the DEM data where the water resources are located."""
|
|
787
804
|
if not self.water_resources_path:
|
|
@@ -967,6 +984,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
967
984
|
mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
|
|
968
985
|
return mesh
|
|
969
986
|
|
|
987
|
+
@monitor_performance
|
|
970
988
|
def generate_water_resources_obj(self) -> None:
|
|
971
989
|
"""Generates 3D obj files based on water resources data."""
|
|
972
990
|
self.logger.debug("Starting water resources generation...")
|
|
@@ -1033,13 +1051,24 @@ class Background(MeshComponent, ImageComponent):
|
|
|
1033
1051
|
|
|
1034
1052
|
self.plane_from_np(elevated_water, elevated_save_path, include_zeros=False)
|
|
1035
1053
|
|
|
1054
|
+
@monitor_performance
|
|
1036
1055
|
def flatten_roads(self) -> None:
|
|
1037
1056
|
"""Flattens the roads in the DEM data by averaging the height values along the road polylines."""
|
|
1038
|
-
|
|
1057
|
+
supported_files = [self.not_resized_with_foundations_path, self.not_resized_path]
|
|
1058
|
+
|
|
1059
|
+
base_image_path = None
|
|
1060
|
+
for supported_file in supported_files:
|
|
1061
|
+
if not supported_file or not os.path.isfile(supported_file):
|
|
1062
|
+
continue
|
|
1063
|
+
|
|
1064
|
+
base_image_path = supported_file
|
|
1065
|
+
break
|
|
1066
|
+
|
|
1067
|
+
if not base_image_path:
|
|
1039
1068
|
self.logger.warning("No DEM data found for flattening roads.")
|
|
1040
1069
|
return
|
|
1041
1070
|
|
|
1042
|
-
dem_image = cv2.imread(
|
|
1071
|
+
dem_image = cv2.imread(base_image_path, cv2.IMREAD_UNCHANGED)
|
|
1043
1072
|
if dem_image is None:
|
|
1044
1073
|
self.logger.warning("Failed to read DEM data.")
|
|
1045
1074
|
return
|
|
@@ -1076,41 +1105,80 @@ class Background(MeshComponent, ImageComponent):
|
|
|
1076
1105
|
total_length = polyline.length
|
|
1077
1106
|
self.logger.debug("Total length of the road polyline: %s", total_length)
|
|
1078
1107
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1108
|
+
# Step 1: Create complete road mask once
|
|
1109
|
+
road_mask = np.zeros(dem_image.shape, dtype=np.uint8)
|
|
1110
|
+
# OpenCV thickness is total width, not radius like Shapely buffer
|
|
1111
|
+
# So we need width * 4 to match the old buffer(width * 2) behavior
|
|
1112
|
+
line_thickness = int(width * 4)
|
|
1113
|
+
|
|
1114
|
+
# Get densely sampled points for smooth road
|
|
1115
|
+
dense_sample_distance = min(SEGMENT_LENGTH, total_length / 100) # At least 100 samples
|
|
1116
|
+
num_dense_points = max(100, int(total_length / dense_sample_distance))
|
|
1117
|
+
dense_distances = np.linspace(0, total_length, num_dense_points)
|
|
1118
|
+
dense_points = [polyline.interpolate(d) for d in dense_distances]
|
|
1119
|
+
dense_coords = np.array([(int(p.x), int(p.y)) for p in dense_points], dtype=np.int32)
|
|
1120
|
+
|
|
1121
|
+
# Draw entire road at once
|
|
1122
|
+
if len(dense_coords) > 1:
|
|
1123
|
+
cv2.polylines(road_mask, [dense_coords], False, 255, thickness=line_thickness)
|
|
1124
|
+
|
|
1125
|
+
# Step 2: Get all road pixels that need processing
|
|
1126
|
+
road_pixels = np.where(road_mask == 255)
|
|
1127
|
+
if len(road_pixels[0]) == 0:
|
|
1128
|
+
continue
|
|
1085
1129
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1130
|
+
road_y, road_x = road_pixels
|
|
1131
|
+
self.logger.debug("Processing %s road pixels", len(road_y))
|
|
1132
|
+
|
|
1133
|
+
# Step 3: Efficient distance-based smooth gradation
|
|
1134
|
+
# Use much larger segments (10-20x SEGMENT_LENGTH) but interpolate between them
|
|
1135
|
+
large_segment_length = SEGMENT_LENGTH * 15 # 30 units instead of 2
|
|
1136
|
+
num_large_segments = max(1, int(np.ceil(total_length / large_segment_length)))
|
|
1137
|
+
large_distances = np.linspace(0, total_length, num_large_segments + 1)
|
|
1138
|
+
|
|
1139
|
+
# Calculate elevation values at large segment points
|
|
1140
|
+
segment_elevations = []
|
|
1141
|
+
for dist in large_distances:
|
|
1142
|
+
sample_point = polyline.interpolate(dist)
|
|
1143
|
+
sample_x, sample_y = int(sample_point.x), int(sample_point.y)
|
|
1144
|
+
|
|
1145
|
+
# Sample elevation in small area around this point
|
|
1146
|
+
sample_radius = max(5, line_thickness // 4)
|
|
1147
|
+
y_min = max(0, sample_y - sample_radius)
|
|
1148
|
+
y_max = min(dem_image.shape[0], sample_y + sample_radius)
|
|
1149
|
+
x_min = max(0, sample_x - sample_radius)
|
|
1150
|
+
x_max = min(dem_image.shape[1], sample_x + sample_radius)
|
|
1151
|
+
|
|
1152
|
+
if y_max > y_min and x_max > x_min:
|
|
1153
|
+
sample_elevation = np.mean(dem_image[y_min:y_max, x_min:x_max]) # type: ignore
|
|
1154
|
+
else:
|
|
1155
|
+
sample_elevation = (
|
|
1156
|
+
dem_image[sample_y, sample_x] # type: ignore
|
|
1157
|
+
if 0 <= sample_y < dem_image.shape[0] and 0 <= sample_x < dem_image.shape[1]
|
|
1158
|
+
else 0
|
|
1159
|
+
)
|
|
1090
1160
|
|
|
1091
|
-
|
|
1161
|
+
segment_elevations.append(sample_elevation)
|
|
1092
1162
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
)
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
dem_image[mask == 255] = mean_value
|
|
1113
|
-
full_mask[mask == 255] = 255
|
|
1163
|
+
# Step 4: Interpolate elevations smoothly along entire road
|
|
1164
|
+
road_distances_from_start = []
|
|
1165
|
+
for i in range(len(road_y)): # pylint: disable=consider-using-enumerate
|
|
1166
|
+
px, py = road_x[i], road_y[i]
|
|
1167
|
+
# Find closest point on polyline (approximation)
|
|
1168
|
+
closest_point = polyline.interpolate(polyline.project(shapely.Point(px, py)))
|
|
1169
|
+
distance_along_road = polyline.project(closest_point)
|
|
1170
|
+
road_distances_from_start.append(distance_along_road)
|
|
1171
|
+
|
|
1172
|
+
road_distances_from_start = np.array(road_distances_from_start) # type: ignore
|
|
1173
|
+
|
|
1174
|
+
# Interpolate elevation values based on distance along road
|
|
1175
|
+
interpolated_elevations = np.interp(
|
|
1176
|
+
road_distances_from_start, large_distances, segment_elevations
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
# Step 5: Apply interpolated elevations to road pixels
|
|
1180
|
+
dem_image[road_y, road_x] = interpolated_elevations
|
|
1181
|
+
full_mask[road_y, road_x] = 255
|
|
1114
1182
|
|
|
1115
1183
|
main_dem_path = self.game.dem_file_path(self.map_directory)
|
|
1116
1184
|
dem_image = self.blur_by_mask(dem_image, full_mask, blur_radius=5)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Base class for all components that primarily used to work with meshes."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
import shutil
|
|
5
6
|
import xml.etree.ElementTree as ET
|
|
@@ -15,6 +16,8 @@ from tqdm import tqdm
|
|
|
15
16
|
from maps4fs.generator.component.base.component import Component
|
|
16
17
|
from maps4fs.generator.settings import Parameters
|
|
17
18
|
|
|
19
|
+
logger = logging.getLogger("maps4fs")
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
class MeshComponent(Component):
|
|
20
23
|
"""Base class for all components that primarily used to work with meshes."""
|
|
@@ -595,19 +598,53 @@ class MeshComponent(Component):
|
|
|
595
598
|
if has_uv:
|
|
596
599
|
xml_vertices.set("uv0", "true")
|
|
597
600
|
|
|
598
|
-
#
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
601
|
+
# Pre-format ALL strings using vectorized operations
|
|
602
|
+
pos_strings = np.array([f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}" for v in vertices])
|
|
603
|
+
|
|
604
|
+
normal_strings = None
|
|
605
|
+
if has_normals:
|
|
606
|
+
normal_strings = np.array(
|
|
607
|
+
[f"{n[0]:.6f} {n[1]:.6f} {n[2]:.6f}" for n in mesh.vertex_normals]
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if normal_strings is None:
|
|
611
|
+
logger.warning("Normals are missing despite has_normals being True. Can't write i3d.")
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
uv_strings = None
|
|
615
|
+
if has_uv:
|
|
616
|
+
uv_strings = np.array([f"{uv[0]:.6f} {uv[1]:.6f}" for uv in mesh.visual.uv])
|
|
617
|
+
|
|
618
|
+
if uv_strings is None:
|
|
619
|
+
logger.warning("UV coordinates are missing despite has_uv being True. Can't write i3d.")
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
# Batch process vertices for memory efficiency
|
|
623
|
+
batch_size = 2000
|
|
624
|
+
vertex_elements = []
|
|
625
|
+
|
|
626
|
+
for batch_start in tqdm(
|
|
627
|
+
range(0, len(vertices), batch_size), desc="Writing vertices", unit="batch"
|
|
628
|
+
):
|
|
629
|
+
batch_end = min(batch_start + batch_size, len(vertices))
|
|
630
|
+
batch_elements = []
|
|
631
|
+
|
|
632
|
+
for idx in range(batch_start, batch_end):
|
|
633
|
+
v_el = ET.Element("v")
|
|
634
|
+
v_el.set("p", pos_strings[idx])
|
|
635
|
+
|
|
636
|
+
if has_normals:
|
|
637
|
+
v_el.set("n", normal_strings[idx])
|
|
638
|
+
|
|
639
|
+
if has_uv:
|
|
640
|
+
v_el.set("t0", uv_strings[idx])
|
|
641
|
+
|
|
642
|
+
batch_elements.append(v_el)
|
|
603
643
|
|
|
604
|
-
|
|
605
|
-
n = mesh.vertex_normals[idx]
|
|
606
|
-
v_el.set("n", f"{n[0]:.6f} {n[1]:.6f} {n[2]:.6f}")
|
|
644
|
+
vertex_elements.extend(batch_elements)
|
|
607
645
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
v_el.set("t0", f"{uv[0]:.6f} {uv[1]:.6f}")
|
|
646
|
+
# Add all vertex elements at once
|
|
647
|
+
xml_vertices.extend(vertex_elements)
|
|
611
648
|
|
|
612
649
|
# Triangles block
|
|
613
650
|
xml_tris = ET.SubElement(shape, "Triangles")
|
|
@@ -10,6 +10,7 @@ import numpy as np
|
|
|
10
10
|
import maps4fs.generator.utils as mfsutils
|
|
11
11
|
from maps4fs.generator.component.base.component_image import ImageComponent
|
|
12
12
|
from maps4fs.generator.component.base.component_xml import XMLComponent
|
|
13
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
13
14
|
from maps4fs.generator.settings import Parameters
|
|
14
15
|
|
|
15
16
|
# Defines coordinates for country block on the license plate texture.
|
|
@@ -120,6 +121,7 @@ class Config(XMLComponent, ImageComponent):
|
|
|
120
121
|
|
|
121
122
|
self.create_qgis_scripts(layers)
|
|
122
123
|
|
|
124
|
+
@monitor_performance
|
|
123
125
|
def _adjust_fog(self) -> None:
|
|
124
126
|
"""Adjusts the fog settings in the environment XML file based on the DEM and height scale."""
|
|
125
127
|
self.logger.debug("Adjusting fog settings based on DEM and height scale...")
|
|
@@ -231,6 +233,7 @@ class Config(XMLComponent, ImageComponent):
|
|
|
231
233
|
|
|
232
234
|
return dem_maximum_meter, dem_minimum_meter
|
|
233
235
|
|
|
236
|
+
@monitor_performance
|
|
234
237
|
def _set_overview(self) -> None:
|
|
235
238
|
"""Generates and sets the overview image for the map."""
|
|
236
239
|
try:
|
|
@@ -347,6 +350,7 @@ class Config(XMLComponent, ImageComponent):
|
|
|
347
350
|
"HR",
|
|
348
351
|
}
|
|
349
352
|
|
|
353
|
+
@monitor_performance
|
|
350
354
|
def update_license_plates(self):
|
|
351
355
|
"""Updates license plates for the specified country."""
|
|
352
356
|
try:
|
|
@@ -565,6 +569,7 @@ class Config(XMLComponent, ImageComponent):
|
|
|
565
569
|
self.save_tree(tree, xml_path=i3d_path)
|
|
566
570
|
self.logger.debug("Updated licensePlatesPL.i3d texture reference to: %s", filename)
|
|
567
571
|
|
|
572
|
+
@monitor_performance
|
|
568
573
|
def _generate_license_plate_texture(
|
|
569
574
|
self,
|
|
570
575
|
license_plates_directory: str,
|
|
@@ -9,6 +9,7 @@ from pydtmdl import DTMProvider
|
|
|
9
9
|
|
|
10
10
|
import maps4fs.generator.config as mfscfg
|
|
11
11
|
from maps4fs.generator.component.base.component_image import ImageComponent
|
|
12
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
# pylint: disable=R0903, R0902
|
|
@@ -115,6 +116,7 @@ class DEM(ImageComponent):
|
|
|
115
116
|
except Exception as e:
|
|
116
117
|
self.logger.warning("Failed to update DEM info: %s.", e)
|
|
117
118
|
|
|
119
|
+
@monitor_performance
|
|
118
120
|
def process(self) -> None:
|
|
119
121
|
"""Reads DTM file, crops it to map size, normalizes and blurs it,
|
|
120
122
|
saves to map directory."""
|
|
@@ -190,6 +192,7 @@ class DEM(ImageComponent):
|
|
|
190
192
|
if self.rotation:
|
|
191
193
|
self.rotate_dem()
|
|
192
194
|
|
|
195
|
+
@monitor_performance
|
|
193
196
|
def normalize_data(self, data: np.ndarray, height_scale_value: int) -> np.ndarray:
|
|
194
197
|
"""Normalize DEM data to 16-bit unsigned integer range (0 to 65535).
|
|
195
198
|
|
|
@@ -209,6 +212,7 @@ class DEM(ImageComponent):
|
|
|
209
212
|
)
|
|
210
213
|
return normalized_data
|
|
211
214
|
|
|
215
|
+
@monitor_performance
|
|
212
216
|
def determine_height_scale(self, data: np.ndarray) -> int:
|
|
213
217
|
"""Determine height scale value using ceiling.
|
|
214
218
|
|
|
@@ -261,6 +265,7 @@ class DEM(ImageComponent):
|
|
|
261
265
|
)
|
|
262
266
|
return data
|
|
263
267
|
|
|
268
|
+
@monitor_performance
|
|
264
269
|
def apply_multiplier(self, data: np.ndarray) -> np.ndarray:
|
|
265
270
|
"""Apply multiplier to DEM data.
|
|
266
271
|
|
|
@@ -283,6 +288,7 @@ class DEM(ImageComponent):
|
|
|
283
288
|
)
|
|
284
289
|
return multiplied_data
|
|
285
290
|
|
|
291
|
+
@monitor_performance
|
|
286
292
|
def resize_to_output(self, data: np.ndarray) -> np.ndarray:
|
|
287
293
|
"""Resize DEM data to the output resolution.
|
|
288
294
|
|
|
@@ -296,6 +302,7 @@ class DEM(ImageComponent):
|
|
|
296
302
|
|
|
297
303
|
return resampled_data
|
|
298
304
|
|
|
305
|
+
@monitor_performance
|
|
299
306
|
def rotate_dem(self) -> None:
|
|
300
307
|
"""Rotate DEM image."""
|
|
301
308
|
self.logger.debug("Rotating DEM image by %s degrees.", self.rotation)
|
|
@@ -12,6 +12,7 @@ from tqdm import tqdm
|
|
|
12
12
|
from maps4fs.generator.component.base.component_image import ImageComponent
|
|
13
13
|
from maps4fs.generator.component.base.component_xml import XMLComponent
|
|
14
14
|
from maps4fs.generator.component.layer import Layer
|
|
15
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
15
16
|
from maps4fs.generator.settings import Parameters
|
|
16
17
|
|
|
17
18
|
# This value sums up the pixel value of the basic area type to convert it from "No Water" to "Near Water".
|
|
@@ -98,6 +99,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
98
99
|
|
|
99
100
|
return grle_schema
|
|
100
101
|
|
|
102
|
+
@monitor_performance
|
|
101
103
|
def process(self) -> None:
|
|
102
104
|
"""Generates InfoLayer PNG files based on the GRLE schema."""
|
|
103
105
|
grle_schema = self._read_grle_schema()
|
|
@@ -134,6 +136,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
134
136
|
self._process_environment()
|
|
135
137
|
self._process_indoor()
|
|
136
138
|
|
|
139
|
+
@monitor_performance
|
|
137
140
|
def previews(self) -> list[str]:
|
|
138
141
|
"""Returns a list of paths to the preview images (empty list).
|
|
139
142
|
The component does not generate any preview images so it returns an empty list.
|
|
@@ -173,6 +176,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
173
176
|
|
|
174
177
|
return preview_paths
|
|
175
178
|
|
|
179
|
+
@monitor_performance
|
|
176
180
|
def overlay_fields(self, farmlands_np: np.ndarray) -> np.ndarray | None:
|
|
177
181
|
"""Overlay fields on the farmlands preview image.
|
|
178
182
|
|
|
@@ -200,6 +204,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
200
204
|
# use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
|
|
201
205
|
return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
|
|
202
206
|
|
|
207
|
+
@monitor_performance
|
|
203
208
|
def _add_farmlands(self) -> None:
|
|
204
209
|
"""Adds farmlands to the InfoLayer PNG file."""
|
|
205
210
|
farmlands = []
|
|
@@ -297,6 +302,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
297
302
|
|
|
298
303
|
self.preview_paths["farmlands"] = info_layer_farmlands_path
|
|
299
304
|
|
|
305
|
+
@monitor_performance
|
|
300
306
|
def _add_plants(self) -> None:
|
|
301
307
|
"""Adds plants to the InfoLayer PNG file."""
|
|
302
308
|
grass_layer = self.map.get_texture_layer(by_usage="grass")
|
|
@@ -399,6 +405,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
399
405
|
|
|
400
406
|
self.logger.debug("Updated density map for fruits saved in %s.", density_map_fruit_path)
|
|
401
407
|
|
|
408
|
+
@monitor_performance
|
|
402
409
|
def create_island_of_plants(self, image: np.ndarray, count: int) -> np.ndarray:
|
|
403
410
|
"""Create an island of plants in the image.
|
|
404
411
|
|
|
@@ -495,6 +502,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
495
502
|
image_np[:, -1] = 0 # Right side
|
|
496
503
|
return image_np
|
|
497
504
|
|
|
505
|
+
@monitor_performance
|
|
498
506
|
def _process_environment(self) -> None:
|
|
499
507
|
info_layer_environment_path = self.game.get_environment_path(self.map_directory)
|
|
500
508
|
if not info_layer_environment_path or not os.path.isfile(info_layer_environment_path):
|
|
@@ -554,6 +562,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
554
562
|
self.logger.debug("Environment InfoLayer PNG file saved: %s.", info_layer_environment_path)
|
|
555
563
|
self.preview_paths["environment"] = info_layer_environment_path
|
|
556
564
|
|
|
565
|
+
@monitor_performance
|
|
557
566
|
def get_resized_weight(
|
|
558
567
|
self, layer: Layer, resize_to: int, dilations: int = 3
|
|
559
568
|
) -> np.ndarray | None:
|
|
@@ -13,6 +13,7 @@ import numpy as np
|
|
|
13
13
|
from tqdm import tqdm
|
|
14
14
|
|
|
15
15
|
from maps4fs.generator.component.base.component_xml import XMLComponent
|
|
16
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
16
17
|
from maps4fs.generator.settings import Parameters
|
|
17
18
|
|
|
18
19
|
NODE_ID_STARTING_VALUE = 2000
|
|
@@ -112,6 +113,7 @@ class I3d(XMLComponent):
|
|
|
112
113
|
|
|
113
114
|
self.save_tree(tree)
|
|
114
115
|
|
|
116
|
+
@monitor_performance
|
|
115
117
|
def _add_splines(self) -> None:
|
|
116
118
|
"""Adds splines to the map I3D file."""
|
|
117
119
|
splines_i3d_path = self.game.splines_file_path(self.map_directory)
|
|
@@ -240,6 +242,7 @@ class I3d(XMLComponent):
|
|
|
240
242
|
|
|
241
243
|
self.assets.splines = splines_i3d_path
|
|
242
244
|
|
|
245
|
+
@monitor_performance
|
|
243
246
|
def _add_fields(self) -> None:
|
|
244
247
|
"""Adds fields to the map I3D file."""
|
|
245
248
|
tree = self.get_tree()
|
|
@@ -498,6 +501,7 @@ class I3d(XMLComponent):
|
|
|
498
501
|
|
|
499
502
|
return choice(trees_by_leaf_type)
|
|
500
503
|
|
|
504
|
+
@monitor_performance
|
|
501
505
|
def _add_forests(self) -> None:
|
|
502
506
|
"""Adds forests to the map I3D file."""
|
|
503
507
|
tree_schema = self._read_tree_schema()
|
|
@@ -9,6 +9,7 @@ from pygmdl import save_image
|
|
|
9
9
|
|
|
10
10
|
import maps4fs.generator.config as mfscfg
|
|
11
11
|
from maps4fs.generator.component.base.component_image import ImageComponent
|
|
12
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
12
13
|
from maps4fs.generator.settings import Parameters
|
|
13
14
|
|
|
14
15
|
|
|
@@ -49,6 +50,7 @@ class Satellite(ImageComponent):
|
|
|
49
50
|
info, warning. If not provided, default logging will be used.
|
|
50
51
|
"""
|
|
51
52
|
|
|
53
|
+
@monitor_performance
|
|
52
54
|
def process(self) -> None:
|
|
53
55
|
"""Downloads the satellite images for the map."""
|
|
54
56
|
self.image_paths = []
|
|
@@ -126,6 +128,7 @@ class Satellite(ImageComponent):
|
|
|
126
128
|
|
|
127
129
|
return tasks
|
|
128
130
|
|
|
131
|
+
@monitor_performance
|
|
129
132
|
def previews(self) -> list[str]:
|
|
130
133
|
"""Returns the paths to the preview images.
|
|
131
134
|
|
|
@@ -7,10 +7,10 @@ import os
|
|
|
7
7
|
import shutil
|
|
8
8
|
import warnings
|
|
9
9
|
from collections import defaultdict
|
|
10
|
-
from time import perf_counter
|
|
11
10
|
from typing import Any, Callable, Generator, Optional
|
|
12
11
|
|
|
13
12
|
import cv2
|
|
13
|
+
import geopandas as gpd
|
|
14
14
|
import numpy as np
|
|
15
15
|
import osmnx as ox
|
|
16
16
|
import pandas as pd
|
|
@@ -21,6 +21,7 @@ from tqdm import tqdm
|
|
|
21
21
|
|
|
22
22
|
from maps4fs.generator.component.base.component_image import ImageComponent
|
|
23
23
|
from maps4fs.generator.component.layer import Layer
|
|
24
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
24
25
|
from maps4fs.generator.settings import Parameters
|
|
25
26
|
|
|
26
27
|
|
|
@@ -186,6 +187,7 @@ class Texture(ImageComponent):
|
|
|
186
187
|
for layer in self.layers:
|
|
187
188
|
self.assets[layer.name] = layer.path(self._weights_dir)
|
|
188
189
|
|
|
190
|
+
@monitor_performance
|
|
189
191
|
def add_borders(self) -> None:
|
|
190
192
|
"""Iterates over all the layers and picks the one which have the border propety defined.
|
|
191
193
|
Borders are distance from the edge of the map on each side (top, right, bottom, left).
|
|
@@ -272,6 +274,7 @@ class Texture(ImageComponent):
|
|
|
272
274
|
return layer
|
|
273
275
|
return None
|
|
274
276
|
|
|
277
|
+
@monitor_performance
|
|
275
278
|
def merge_into(self) -> None:
|
|
276
279
|
"""Merges the content of layers into their target layers."""
|
|
277
280
|
for layer in self.layers:
|
|
@@ -298,6 +301,7 @@ class Texture(ImageComponent):
|
|
|
298
301
|
cv2.imwrite(layer.path(self._weights_dir), np.zeros_like(layer_image))
|
|
299
302
|
self.logger.debug("Cleared layer %s.", layer.name)
|
|
300
303
|
|
|
304
|
+
@monitor_performance
|
|
301
305
|
def rotate_textures(self) -> None:
|
|
302
306
|
"""Rotates textures of the layers which have tags."""
|
|
303
307
|
if self.rotation:
|
|
@@ -320,6 +324,7 @@ class Texture(ImageComponent):
|
|
|
320
324
|
"Skipping rotation of layer %s because it has no tags.", layer.name
|
|
321
325
|
)
|
|
322
326
|
|
|
327
|
+
@monitor_performance
|
|
323
328
|
def scale_textures(self) -> None:
|
|
324
329
|
"""Resizes all the textures to the map output size."""
|
|
325
330
|
if not self.map.output_size:
|
|
@@ -364,6 +369,7 @@ class Texture(ImageComponent):
|
|
|
364
369
|
]
|
|
365
370
|
return {attr: getattr(self, attr, None) for attr in useful_attributes}
|
|
366
371
|
|
|
372
|
+
@monitor_performance
|
|
367
373
|
def _prepare_weights(self):
|
|
368
374
|
self.logger.debug("Starting preparing weights from %s layers.", len(self.layers))
|
|
369
375
|
|
|
@@ -428,6 +434,7 @@ class Texture(ImageComponent):
|
|
|
428
434
|
),
|
|
429
435
|
)
|
|
430
436
|
|
|
437
|
+
@monitor_performance
|
|
431
438
|
def draw(self) -> None:
|
|
432
439
|
"""Iterates over layers and fills them with polygons from OSM data."""
|
|
433
440
|
layers = self.layers_by_priority()
|
|
@@ -546,6 +553,7 @@ class Texture(ImageComponent):
|
|
|
546
553
|
}
|
|
547
554
|
info_layer_data[f"{layer.info_layer}_polylines"].append(linestring_entry) # type: ignore
|
|
548
555
|
|
|
556
|
+
@monitor_performance
|
|
549
557
|
def dissolve(self) -> None:
|
|
550
558
|
"""Dissolves textures of the layers with tags into sublayers for them to look more
|
|
551
559
|
natural in the game.
|
|
@@ -554,46 +562,52 @@ class Texture(ImageComponent):
|
|
|
554
562
|
files of the corresponding layer and saves the changes to the files.
|
|
555
563
|
"""
|
|
556
564
|
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
|
|
565
|
+
self.dissolve_layer(layer)
|
|
566
566
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
567
|
+
def dissolve_layer(self, layer: Layer) -> None:
|
|
568
|
+
"""Dissolves texture of the layer into sublayers."""
|
|
569
|
+
if not layer.tags:
|
|
570
|
+
self.logger.debug("Layer %s has no tags, there's nothing to dissolve.", layer.name)
|
|
571
|
+
return
|
|
572
|
+
layer_path = layer.path(self._weights_dir)
|
|
573
|
+
layer_paths = layer.paths(self._weights_dir)
|
|
571
574
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
)
|
|
576
|
-
continue
|
|
575
|
+
if len(layer_paths) < 2:
|
|
576
|
+
self.logger.debug("Layer %s has only one texture, skipping.", layer.name)
|
|
577
|
+
return
|
|
577
578
|
|
|
578
|
-
|
|
579
|
-
|
|
579
|
+
self.logger.debug("Dissolving layer from %s to %s.", layer_path, layer_paths)
|
|
580
|
+
# Check if the image contains any non-zero values, otherwise continue.
|
|
581
|
+
layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
|
|
582
|
+
if layer_image is None:
|
|
583
|
+
self.logger.debug("Layer %s image not found, skipping.", layer.name)
|
|
584
|
+
return
|
|
580
585
|
|
|
581
|
-
|
|
582
|
-
|
|
586
|
+
# Get mask of non-zero pixels. If there are no non-zero pixels, skip the layer.
|
|
587
|
+
mask = layer_image > 0
|
|
588
|
+
if not np.any(mask):
|
|
589
|
+
self.logger.debug(
|
|
590
|
+
"Layer %s does not contain any non-zero values, skipping.", layer.name
|
|
591
|
+
)
|
|
592
|
+
return
|
|
593
|
+
# Save the original image to use it for preview later, without combining the sublayers.
|
|
594
|
+
cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy()) # type: ignore
|
|
583
595
|
|
|
584
|
-
|
|
585
|
-
|
|
596
|
+
# Create random assignment array for all pixels
|
|
597
|
+
random_assignment = np.random.randint(0, layer.count, size=layer_image.shape)
|
|
586
598
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
599
|
+
# Create sublayers using vectorized operations.
|
|
600
|
+
sublayers = []
|
|
601
|
+
for i in range(layer.count):
|
|
602
|
+
# Create sublayer: 255 where (mask is True AND random_assignment == i)
|
|
603
|
+
sublayer = np.where((mask) & (random_assignment == i), 255, 0).astype(np.uint8)
|
|
604
|
+
sublayers.append(sublayer)
|
|
590
605
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
self.logger.debug("Sublayer %s saved.", sublayer_path)
|
|
606
|
+
# Save sublayers
|
|
607
|
+
for sublayer, sublayer_path in zip(sublayers, layer_paths):
|
|
608
|
+
cv2.imwrite(sublayer_path, sublayer)
|
|
595
609
|
|
|
596
|
-
|
|
610
|
+
self.logger.debug("Dissolved layer %s.", layer.name)
|
|
597
611
|
|
|
598
612
|
def draw_base_layer(self, cumulative_image: np.ndarray) -> None:
|
|
599
613
|
"""Draws base layer and saves it into the png file.
|
|
@@ -791,35 +805,47 @@ class Texture(ImageComponent):
|
|
|
791
805
|
ox_settings.use_cache = self.map.texture_settings.use_cache
|
|
792
806
|
ox_settings.requests_timeout = 30
|
|
793
807
|
|
|
808
|
+
objects = self.fetch_osm_data(tags)
|
|
809
|
+
if objects is None or objects.empty:
|
|
810
|
+
self.logger.debug("No objects found for tags: %s.", tags)
|
|
811
|
+
return
|
|
812
|
+
|
|
813
|
+
self.logger.debug("Fetched %s elements for tags: %s.", len(objects), tags)
|
|
814
|
+
|
|
815
|
+
method = self.linestrings_generator if yield_linestrings else self.polygons_generator
|
|
816
|
+
|
|
817
|
+
yield from method(objects, width, is_fieds)
|
|
818
|
+
|
|
819
|
+
@monitor_performance
|
|
820
|
+
def fetch_osm_data(self, tags: dict[str, str | list[str] | bool]) -> gpd.GeoDataFrame | None:
|
|
821
|
+
"""Fetches OSM data for given tags.
|
|
822
|
+
|
|
823
|
+
Arguments:
|
|
824
|
+
tags (dict[str, str | list[str] | bool]): Dictionary of tags to search for.
|
|
825
|
+
|
|
826
|
+
Returns:
|
|
827
|
+
gpd.GeoDataFrame | None: GeoDataFrame with OSM objects or None if no objects found.
|
|
828
|
+
"""
|
|
794
829
|
try:
|
|
795
830
|
if self.map.custom_osm is not None:
|
|
796
831
|
with warnings.catch_warnings():
|
|
797
832
|
warnings.simplefilter("ignore", FutureWarning)
|
|
798
833
|
objects = ox.features_from_xml(self.map.custom_osm, tags=tags)
|
|
799
834
|
else:
|
|
800
|
-
before_fetch = perf_counter()
|
|
801
835
|
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
836
|
except Exception as e:
|
|
808
837
|
self.logger.debug("Error fetching objects for tags: %s. Error: %s.", tags, e)
|
|
809
|
-
return
|
|
810
|
-
self.logger.debug("Fetched %s elements for tags: %s.", len(objects), tags)
|
|
811
|
-
|
|
812
|
-
method = self.linestrings_generator if yield_linestrings else self.polygons_generator
|
|
838
|
+
return None
|
|
813
839
|
|
|
814
|
-
|
|
840
|
+
return objects
|
|
815
841
|
|
|
816
842
|
def linestrings_generator(
|
|
817
|
-
self, objects:
|
|
843
|
+
self, objects: gpd.GeoDataFrame, *args, **kwargs
|
|
818
844
|
) -> Generator[list[tuple[int, int]], None, None]:
|
|
819
845
|
"""Generator which yields lists of point coordinates which represent LineStrings from OSM.
|
|
820
846
|
|
|
821
847
|
Arguments:
|
|
822
|
-
objects (
|
|
848
|
+
objects (gpd.GeoDataFrame): GeoDataFrame with OSM objects.
|
|
823
849
|
|
|
824
850
|
Yields:
|
|
825
851
|
Generator[list[tuple[int, int]], None, None]: List of point coordinates.
|
|
@@ -865,6 +891,7 @@ class Texture(ImageComponent):
|
|
|
865
891
|
polygon_np = self._to_np(polygon)
|
|
866
892
|
yield polygon_np
|
|
867
893
|
|
|
894
|
+
@monitor_performance
|
|
868
895
|
def previews(self) -> list[str]:
|
|
869
896
|
"""Invokes methods to generate previews. Returns list of paths to previews.
|
|
870
897
|
|
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
|
|
|
@@ -203,51 +209,76 @@ class Map:
|
|
|
203
209
|
Yields:
|
|
204
210
|
Generator[str, None, None]: Component names.
|
|
205
211
|
"""
|
|
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,
|
|
212
|
+
with performance_session() as session_id:
|
|
213
|
+
self.logger.info(
|
|
214
|
+
"Starting map generation. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
|
|
215
|
+
self.game.code,
|
|
217
216
|
self.coordinates,
|
|
218
217
|
self.size,
|
|
219
|
-
self.rotated_size,
|
|
220
218
|
self.rotation,
|
|
221
|
-
self.map_directory,
|
|
222
|
-
self.logger,
|
|
223
|
-
texture_custom_schema=self.texture_custom_schema, # type: ignore
|
|
224
|
-
tree_custom_schema=self.tree_custom_schema, # type: ignore
|
|
225
219
|
)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
220
|
+
generation_start = perf_counter()
|
|
221
|
+
|
|
222
|
+
for game_component in self.game.components:
|
|
223
|
+
component = game_component(
|
|
224
|
+
self.game,
|
|
225
|
+
self,
|
|
226
|
+
self.coordinates,
|
|
227
|
+
self.size,
|
|
228
|
+
self.rotated_size,
|
|
229
|
+
self.rotation,
|
|
230
|
+
self.map_directory,
|
|
231
|
+
self.logger,
|
|
232
|
+
texture_custom_schema=self.texture_custom_schema, # type: ignore
|
|
233
|
+
tree_custom_schema=self.tree_custom_schema, # type: ignore
|
|
238
234
|
)
|
|
239
|
-
self.
|
|
240
|
-
|
|
235
|
+
self.components.append(component)
|
|
236
|
+
|
|
237
|
+
yield component.__class__.__name__
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
component_start = perf_counter()
|
|
241
|
+
component.process()
|
|
242
|
+
component_finish = perf_counter()
|
|
243
|
+
self.logger.info(
|
|
244
|
+
"Component %s processed in %.2f seconds.",
|
|
245
|
+
component.__class__.__name__,
|
|
246
|
+
component_finish - component_start,
|
|
247
|
+
)
|
|
248
|
+
component.commit_generation_info()
|
|
249
|
+
except Exception as e:
|
|
250
|
+
self.logger.error(
|
|
251
|
+
"Error processing or committing generation info for component %s: %s",
|
|
252
|
+
component.__class__.__name__,
|
|
253
|
+
e,
|
|
254
|
+
)
|
|
255
|
+
self._update_main_settings({"error": str(e)})
|
|
256
|
+
raise e
|
|
257
|
+
|
|
258
|
+
generation_finish = perf_counter()
|
|
259
|
+
self.logger.info(
|
|
260
|
+
"Map generation completed in %.2f seconds.",
|
|
261
|
+
generation_finish - generation_start,
|
|
262
|
+
)
|
|
241
263
|
|
|
242
|
-
|
|
264
|
+
self._update_main_settings({"completed": True})
|
|
243
265
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
266
|
+
self.logger.info(
|
|
267
|
+
"Map generation completed. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
|
|
268
|
+
self.game.code,
|
|
269
|
+
self.coordinates,
|
|
270
|
+
self.size,
|
|
271
|
+
self.rotation,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
session_json = PerformanceMonitor().get_session_json(session_id)
|
|
275
|
+
if session_json:
|
|
276
|
+
report_filename = "performance_report.json"
|
|
277
|
+
with open(
|
|
278
|
+
os.path.join(self.map_directory, report_filename), "w", encoding="utf-8"
|
|
279
|
+
) as file:
|
|
280
|
+
json.dump(session_json, file, indent=4)
|
|
281
|
+
send_performance_report(session_json)
|
|
251
282
|
|
|
252
283
|
def _update_main_settings(self, data: dict[str, Any]) -> None:
|
|
253
284
|
"""Update main settings with provided data.
|
|
@@ -0,0 +1,118 @@
|
|
|
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.info(
|
|
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
|
maps4fs/generator/statistics.py
CHANGED
|
@@ -80,3 +80,13 @@ def send_survey(data: dict[str, Any]) -> None:
|
|
|
80
80
|
"""
|
|
81
81
|
endpoint = f"{STATS_HOST}/receive_survey"
|
|
82
82
|
post(endpoint, data)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def send_performance_report(data: dict[str, Any]) -> None:
|
|
86
|
+
"""Send performance report to the statistics server.
|
|
87
|
+
|
|
88
|
+
Arguments:
|
|
89
|
+
data (dict[str, Any]): The performance report data to send.
|
|
90
|
+
"""
|
|
91
|
+
endpoint = f"{STATS_HOST}/receive_performance_report"
|
|
92
|
+
post(endpoint, data)
|
maps4fs/generator/utils.py
CHANGED
|
@@ -158,3 +158,14 @@ def dump_json(filename: str, directory: str, data: dict[Any, Any] | Any | None)
|
|
|
158
158
|
save_path = os.path.join(directory, filename)
|
|
159
159
|
with open(save_path, "w", encoding="utf-8") as file:
|
|
160
160
|
json.dump(data, file, indent=4)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Singleton(type):
|
|
164
|
+
"""A metaclass for creating singleton classes."""
|
|
165
|
+
|
|
166
|
+
_instances: dict[Any, Any] = {}
|
|
167
|
+
|
|
168
|
+
def __call__(cls, *args, **kwargs):
|
|
169
|
+
if cls not in cls._instances:
|
|
170
|
+
cls._instances[cls] = super().__call__(*args, **kwargs)
|
|
171
|
+
return cls._instances[cls]
|
maps4fs/logger.py
CHANGED
|
@@ -14,8 +14,9 @@ class Logger(logging.Logger):
|
|
|
14
14
|
self,
|
|
15
15
|
level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO",
|
|
16
16
|
to_stdout: bool = True,
|
|
17
|
+
**kwargs,
|
|
17
18
|
):
|
|
18
|
-
super().__init__(LOGGER_NAME)
|
|
19
|
+
super().__init__(kwargs.pop("name", LOGGER_NAME))
|
|
19
20
|
self.setLevel(level)
|
|
20
21
|
self.stdout_handler = logging.StreamHandler(sys.stdout)
|
|
21
22
|
formatter = "%(name)s | %(levelname)s | %(asctime)s | %(message)s"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
maps4fs/__init__.py,sha256=5ixsCA5vgcIV0OrF9EJBm91Mmc_KfMiDRM-QyifMAvo,386
|
|
2
|
+
maps4fs/logger.py,sha256=aZAa9glzgvH6ySVDLelSPTwHfWZtpGK5YBl-ufNUsPg,801
|
|
3
|
+
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
|
4
|
+
maps4fs/generator/config.py,sha256=dHJLBt-Ua5rMMvUujAa3cuPSYvDA7tgfY7Z8tpvT_zo,7176
|
|
5
|
+
maps4fs/generator/game.py,sha256=_LNiH__7FeSGsPKsuvAGiktt5GcJQVqcQYtsFZNWGyM,16106
|
|
6
|
+
maps4fs/generator/map.py,sha256=ak74FNSDQXPqaB0gOaZqVo8Sy0fvf_3iXHg6FJkHPpA,15888
|
|
7
|
+
maps4fs/generator/monitor.py,sha256=IqNZ6vLlw3vS1dHQ3RPFcGHkKwRfPOPVFy2IR2-wWsE,3523
|
|
8
|
+
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
|
9
|
+
maps4fs/generator/settings.py,sha256=_QJL4ikQYLFOIB1zWqXjYvyLfoh3cr2RYb2IzsunMJg,13405
|
|
10
|
+
maps4fs/generator/statistics.py,sha256=ol0MTiehcCbQFfyYA7cKU-M4_cjiLCktnGbid4GYABU,2641
|
|
11
|
+
maps4fs/generator/utils.py,sha256=FOkJZrBifGLfEd33noL6AqfV_oW5cM-GQjXHNxzIKv4,5132
|
|
12
|
+
maps4fs/generator/component/__init__.py,sha256=s01yVVVi8R2xxNvflu2D6wTd9I_g73AMM2x7vAC7GX4,490
|
|
13
|
+
maps4fs/generator/component/background.py,sha256=c2bjK3DkQXvA6Gtb_hUM9m-7fIVgp2BxJp09c4ZY3_A,49434
|
|
14
|
+
maps4fs/generator/component/config.py,sha256=tI2RQaGIqBgJIi9KjYfMZZ8AWg_YVUm6KKsBHGV241g,31285
|
|
15
|
+
maps4fs/generator/component/dem.py,sha256=vMVJtU2jAS-2lfB9JsqodZsrUvY1h5xr3Dh5qk6txwk,11895
|
|
16
|
+
maps4fs/generator/component/grle.py,sha256=uOd0dP-TeS85SZ87wafVj7AQv8L5VSdg2O0pDrtm_p0,27500
|
|
17
|
+
maps4fs/generator/component/i3d.py,sha256=t6Y9JPXvKJ91wTBg_-noIsxDPk1OSdY9EwM5ZtgOl9Q,26771
|
|
18
|
+
maps4fs/generator/component/layer.py,sha256=bdy1XGOODyPqYUM3b_wEY2H9Piz-AaHsCDecl-qRHiM,6627
|
|
19
|
+
maps4fs/generator/component/satellite.py,sha256=1bPqd8JqAPqU0tEI9m-iuljMW9hXqlaCIxvq7kdpMY0,5219
|
|
20
|
+
maps4fs/generator/component/texture.py,sha256=FF2x6F2d5MRD3mls_m7z0JpnfmQVyoUkThJpXt1PMPM,38087
|
|
21
|
+
maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
|
22
|
+
maps4fs/generator/component/base/component.py,sha256=-7H3donrH19f0_rivNyI3fgLsiZkntXfGywEx4tOnM4,23924
|
|
23
|
+
maps4fs/generator/component/base/component_image.py,sha256=GXFkEFARNRkWkDiGSjvU4WX6f_8s6R1t2ZYqZflv1jk,9626
|
|
24
|
+
maps4fs/generator/component/base/component_mesh.py,sha256=UgtycniJ1anN81R9188D9xewGOFR22nA2TW9mmBGZlU,25878
|
|
25
|
+
maps4fs/generator/component/base/component_xml.py,sha256=MT-VhU2dEckLFxAgmxg6V3gnv11di_94Qq6atfpOLdc,5342
|
|
26
|
+
maps4fs-2.9.1.dist-info/licenses/LICENSE.md,sha256=Ptw8AkqJ60c4tRts6yuqGP_8B0dxwOGmJsp6YJ8dKqM,34328
|
|
27
|
+
maps4fs-2.9.1.dist-info/METADATA,sha256=sHHN63IF4C9aZplKg50vjhx5jkSwIhI8c49vPeJeGF4,10042
|
|
28
|
+
maps4fs-2.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
29
|
+
maps4fs-2.9.1.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
|
30
|
+
maps4fs-2.9.1.dist-info/RECORD,,
|
maps4fs-2.8.9.dist-info/RECORD
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
maps4fs/__init__.py,sha256=5ixsCA5vgcIV0OrF9EJBm91Mmc_KfMiDRM-QyifMAvo,386
|
|
2
|
-
maps4fs/logger.py,sha256=6sem0aFKQqtVjQ_yNu9iGcc-hqzLQUhfxco05K6nqow,763
|
|
3
|
-
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
|
4
|
-
maps4fs/generator/config.py,sha256=dHJLBt-Ua5rMMvUujAa3cuPSYvDA7tgfY7Z8tpvT_zo,7176
|
|
5
|
-
maps4fs/generator/game.py,sha256=_LNiH__7FeSGsPKsuvAGiktt5GcJQVqcQYtsFZNWGyM,16106
|
|
6
|
-
maps4fs/generator/map.py,sha256=ZZRU8x0feGbgeJgxc0D3N-mfiasyFXxj6gbGyl-WRzE,14528
|
|
7
|
-
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
|
8
|
-
maps4fs/generator/settings.py,sha256=_QJL4ikQYLFOIB1zWqXjYvyLfoh3cr2RYb2IzsunMJg,13405
|
|
9
|
-
maps4fs/generator/statistics.py,sha256=ciklaj_giopr2ShYE1_Xy9sJop9qWLmb_IZ-qgAq2b8,2348
|
|
10
|
-
maps4fs/generator/utils.py,sha256=ugdQ8C22NeiZLIlldLoEKCc7ioOefz4W-8qF2eOy9qU,4834
|
|
11
|
-
maps4fs/generator/component/__init__.py,sha256=s01yVVVi8R2xxNvflu2D6wTd9I_g73AMM2x7vAC7GX4,490
|
|
12
|
-
maps4fs/generator/component/background.py,sha256=tFWdYASUUZsJ8QiQrxxba-6aMECD3hvpd0pMm_UdrH8,46151
|
|
13
|
-
maps4fs/generator/component/config.py,sha256=OPDhQyALt6h8r70SU26qPcS1NwlfhuM97Q_LfjPJFKs,31127
|
|
14
|
-
maps4fs/generator/component/dem.py,sha256=FPqcXmFQg5MPaGuy4g5kxzvY1wbhozeCf-aNMCj5eaU,11687
|
|
15
|
-
maps4fs/generator/component/grle.py,sha256=0PC1K829wjD4y4d9qfIbnU29ebjflIPBbwIZx8FXwc8,27242
|
|
16
|
-
maps4fs/generator/component/i3d.py,sha256=RvpiW9skkZ6McyahC-AeIdPuSQjpXiFs1l0xOioJAu4,26638
|
|
17
|
-
maps4fs/generator/component/layer.py,sha256=bdy1XGOODyPqYUM3b_wEY2H9Piz-AaHsCDecl-qRHiM,6627
|
|
18
|
-
maps4fs/generator/component/satellite.py,sha256=JeQg4CGDSFir937ugbj30yliw6wftwfM9rCEc0uDcwc,5111
|
|
19
|
-
maps4fs/generator/component/texture.py,sha256=AjN1RsnVLtL7F13B_va6psKTTPc_0JCEznjT5mb1vKo,37309
|
|
20
|
-
maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
|
21
|
-
maps4fs/generator/component/base/component.py,sha256=-7H3donrH19f0_rivNyI3fgLsiZkntXfGywEx4tOnM4,23924
|
|
22
|
-
maps4fs/generator/component/base/component_image.py,sha256=GXFkEFARNRkWkDiGSjvU4WX6f_8s6R1t2ZYqZflv1jk,9626
|
|
23
|
-
maps4fs/generator/component/base/component_mesh.py,sha256=S5M_SU-FZz17-LgzTIM935ms1Vc4O06UQNTEN4e0INU,24729
|
|
24
|
-
maps4fs/generator/component/base/component_xml.py,sha256=MT-VhU2dEckLFxAgmxg6V3gnv11di_94Qq6atfpOLdc,5342
|
|
25
|
-
maps4fs-2.8.9.dist-info/licenses/LICENSE.md,sha256=Ptw8AkqJ60c4tRts6yuqGP_8B0dxwOGmJsp6YJ8dKqM,34328
|
|
26
|
-
maps4fs-2.8.9.dist-info/METADATA,sha256=c5om5AlUIKRcE13_Tu54VHdCwxEeK6S5y12up1CCvPQ,10042
|
|
27
|
-
maps4fs-2.8.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
28
|
-
maps4fs-2.8.9.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
|
29
|
-
maps4fs-2.8.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|