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.

@@ -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
- """ ""Decimates the background mesh based on the map size."""
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
- if not self.not_resized_path or not os.path.isfile(self.not_resized_path):
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(self.not_resized_path, cv2.IMREAD_UNCHANGED)
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
- current_distance = 0
1080
- segments: list[shapely.LineString] = []
1081
- while current_distance < total_length:
1082
- start_point = polyline.interpolate(current_distance)
1083
- end_distance = min(current_distance + SEGMENT_LENGTH, total_length)
1084
- end_point = polyline.interpolate(end_distance)
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
- # Create a small segment LineString
1087
- segment = shapely.LineString([start_point, end_point])
1088
- segments.append(segment)
1089
- current_distance += SEGMENT_LENGTH
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
- self.logger.debug("Number of segments created: %s", len(segments))
1161
+ segment_elevations.append(sample_elevation)
1092
1162
 
1093
- road_polygons: list[shapely.Polygon] = []
1094
- for segment in segments:
1095
- polygon = segment.buffer(
1096
- width * 2, resolution=4, cap_style="flat", join_style="mitre"
1097
- )
1098
- road_polygons.append(polygon)
1099
-
1100
- for polygon in road_polygons:
1101
- polygon_points = polygon.exterior.coords
1102
- road_np = self.polygon_points_to_np(polygon_points)
1103
- mask = np.zeros(dem_image.shape, dtype=np.uint8)
1104
-
1105
- try:
1106
- cv2.fillPoly(mask, [road_np], 255) # type: ignore
1107
- except Exception as e:
1108
- self.logger.debug("Could not create mask for road with error: %s", e)
1109
- continue
1110
-
1111
- mean_value = cv2.mean(dem_image, mask=mask)[0] # type: ignore
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
- # Write vertex data
599
- for idx in tqdm(range(len(vertices)), desc="Writing vertices", unit="vertex"):
600
- v = vertices[idx]
601
- v_el = ET.SubElement(xml_vertices, "v")
602
- v_el.set("p", f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}")
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
- if has_normals:
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
- if has_uv:
609
- uv = mesh.visual.uv[idx]
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
- if not layer.tags:
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
- self.logger.debug("Dissolving layer from %s to %s.", layer_path, layer_paths)
568
-
569
- # Check if the image contains any non-zero values, otherwise continue.
570
- layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
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
- if not np.any(layer_image): # type: ignore
573
- self.logger.debug(
574
- "Layer %s does not contain any non-zero values, skipping.", layer.name
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
- # Save the original image to use it for preview later, without combining the sublayers.
579
- cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy()) # type: ignore
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
- # Get the coordinates of non-zero values.
582
- non_zero_coords = np.column_stack(np.where(layer_image > 0)) # type: ignore
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
- # Prepare sublayers.
585
- sublayers = [np.zeros_like(layer_image) for _ in range(layer.count)]
596
+ # Create random assignment array for all pixels
597
+ random_assignment = np.random.randint(0, layer.count, size=layer_image.shape)
586
598
 
587
- # Randomly assign non-zero values to sublayers.
588
- for coord in non_zero_coords:
589
- sublayers[np.random.randint(0, layer.count)][coord[0], coord[1]] = 255 # type: ignore
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
- # Save the sublayers.
592
- for sublayer, sublayer_path in zip(sublayers, layer_paths):
593
- cv2.imwrite(sublayer_path, sublayer)
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
- self.logger.debug("Dissolved layer %s.", layer.name)
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
- yield from method(objects, width, is_fieds)
840
+ return objects
815
841
 
816
842
  def linestrings_generator(
817
- self, objects: pd.core.frame.DataFrame, *args, **kwargs
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 (pd.core.frame.DataFrame): Dataframe with OSM 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 send_advanced_settings, send_main_settings
20
+ from maps4fs.generator.statistics import (
21
+ send_advanced_settings,
22
+ send_main_settings,
23
+ send_performance_report,
24
+ )
19
25
  from maps4fs.logger import Logger
20
26
 
21
27
 
@@ -203,51 +209,76 @@ class Map:
203
209
  Yields:
204
210
  Generator[str, None, None]: Component names.
205
211
  """
206
- self.logger.debug(
207
- "Starting map generation. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
208
- self.game.code,
209
- self.coordinates,
210
- self.size,
211
- self.rotation,
212
- )
213
- for game_component in self.game.components:
214
- component = game_component(
215
- self.game,
216
- self,
212
+ with performance_session() as session_id:
213
+ self.logger.info(
214
+ "Starting map generation. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
215
+ self.game.code,
217
216
  self.coordinates,
218
217
  self.size,
219
- self.rotated_size,
220
218
  self.rotation,
221
- self.map_directory,
222
- self.logger,
223
- texture_custom_schema=self.texture_custom_schema, # type: ignore
224
- tree_custom_schema=self.tree_custom_schema, # type: ignore
225
219
  )
226
- self.components.append(component)
227
-
228
- yield component.__class__.__name__
229
-
230
- try:
231
- component.process()
232
- component.commit_generation_info()
233
- except Exception as e:
234
- self.logger.error(
235
- "Error processing or committing generation info for component %s: %s",
236
- component.__class__.__name__,
237
- e,
220
+ generation_start = perf_counter()
221
+
222
+ for game_component in self.game.components:
223
+ component = game_component(
224
+ self.game,
225
+ self,
226
+ self.coordinates,
227
+ self.size,
228
+ self.rotated_size,
229
+ self.rotation,
230
+ self.map_directory,
231
+ self.logger,
232
+ texture_custom_schema=self.texture_custom_schema, # type: ignore
233
+ tree_custom_schema=self.tree_custom_schema, # type: ignore
238
234
  )
239
- self._update_main_settings({"error": str(e)})
240
- raise e
235
+ self.components.append(component)
236
+
237
+ yield component.__class__.__name__
238
+
239
+ try:
240
+ component_start = perf_counter()
241
+ component.process()
242
+ component_finish = perf_counter()
243
+ self.logger.info(
244
+ "Component %s processed in %.2f seconds.",
245
+ component.__class__.__name__,
246
+ component_finish - component_start,
247
+ )
248
+ component.commit_generation_info()
249
+ except Exception as e:
250
+ self.logger.error(
251
+ "Error processing or committing generation info for component %s: %s",
252
+ component.__class__.__name__,
253
+ e,
254
+ )
255
+ self._update_main_settings({"error": str(e)})
256
+ raise e
257
+
258
+ generation_finish = perf_counter()
259
+ self.logger.info(
260
+ "Map generation completed in %.2f seconds.",
261
+ generation_finish - generation_start,
262
+ )
241
263
 
242
- self._update_main_settings({"completed": True})
264
+ self._update_main_settings({"completed": True})
243
265
 
244
- self.logger.debug(
245
- "Map generation completed. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
246
- self.game.code,
247
- self.coordinates,
248
- self.size,
249
- self.rotation,
250
- )
266
+ self.logger.info(
267
+ "Map generation completed. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
268
+ self.game.code,
269
+ self.coordinates,
270
+ self.size,
271
+ self.rotation,
272
+ )
273
+
274
+ session_json = PerformanceMonitor().get_session_json(session_id)
275
+ if session_json:
276
+ report_filename = "performance_report.json"
277
+ with open(
278
+ os.path.join(self.map_directory, report_filename), "w", encoding="utf-8"
279
+ ) as file:
280
+ json.dump(session_json, file, indent=4)
281
+ send_performance_report(session_json)
251
282
 
252
283
  def _update_main_settings(self, data: dict[str, Any]) -> None:
253
284
  """Update main settings with provided data.
@@ -0,0 +1,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
@@ -80,3 +80,13 @@ def send_survey(data: dict[str, Any]) -> None:
80
80
  """
81
81
  endpoint = f"{STATS_HOST}/receive_survey"
82
82
  post(endpoint, data)
83
+
84
+
85
+ def send_performance_report(data: dict[str, Any]) -> None:
86
+ """Send performance report to the statistics server.
87
+
88
+ Arguments:
89
+ data (dict[str, Any]): The performance report data to send.
90
+ """
91
+ endpoint = f"{STATS_HOST}/receive_performance_report"
92
+ post(endpoint, data)
@@ -158,3 +158,14 @@ def dump_json(filename: str, directory: str, data: dict[Any, Any] | Any | None)
158
158
  save_path = os.path.join(directory, filename)
159
159
  with open(save_path, "w", encoding="utf-8") as file:
160
160
  json.dump(data, file, indent=4)
161
+
162
+
163
+ class Singleton(type):
164
+ """A metaclass for creating singleton classes."""
165
+
166
+ _instances: dict[Any, Any] = {}
167
+
168
+ def __call__(cls, *args, **kwargs):
169
+ if cls not in cls._instances:
170
+ cls._instances[cls] = super().__call__(*args, **kwargs)
171
+ return cls._instances[cls]
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.8.9
3
+ Version: 2.9.1
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: GNU Affero General Public License v3.0
@@ -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,,
@@ -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,,