maps4fs 2.0.6__py3-none-any.whl → 2.0.7__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.
@@ -10,6 +10,8 @@ from typing import Any
10
10
 
11
11
  import cv2
12
12
  import numpy as np
13
+ import shapely
14
+ import trimesh
13
15
  from tqdm import tqdm
14
16
  from trimesh import Trimesh
15
17
 
@@ -144,7 +146,7 @@ class Background(MeshComponent, ImageComponent):
144
146
  self.logger.debug("Could not create mask for building with error: %s", e)
145
147
  continue
146
148
 
147
- mean_value = cv2.mean(dem_image, mask=mask)[0]
149
+ mean_value = cv2.mean(dem_image, mask=mask)[0] # type: ignore
148
150
  mean_value = np.round(mean_value).astype(dem_image.dtype)
149
151
  self.logger.debug("Mean value of the building area: %s", mean_value)
150
152
 
@@ -231,13 +233,13 @@ class Background(MeshComponent, ImageComponent):
231
233
  if self.map.output_size is not None:
232
234
  scaled_background_size = int(self.background_size * self.map.size_scale)
233
235
  dem_data = cv2.resize(
234
- dem_data,
236
+ dem_data, # type: ignore
235
237
  (scaled_background_size, scaled_background_size),
236
238
  interpolation=cv2.INTER_NEAREST,
237
239
  )
238
240
 
239
241
  self.plane_from_np(
240
- dem_data,
242
+ dem_data, # type: ignore
241
243
  save_path,
242
244
  create_preview=True,
243
245
  remove_center=self.map.background_settings.remove_center,
@@ -255,7 +257,7 @@ class Background(MeshComponent, ImageComponent):
255
257
  """
256
258
  dem_data = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED)
257
259
  half_size = self.map_size // 2
258
- dem_data = self.cut_out_np(dem_data, half_size, return_cutout=True)
260
+ dem_data = self.cut_out_np(dem_data, half_size, return_cutout=True) # type: ignore
259
261
 
260
262
  if save_path:
261
263
  cv2.imwrite(save_path, dem_data)
@@ -362,7 +364,7 @@ class Background(MeshComponent, ImageComponent):
362
364
  background_dem_preview_image = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
363
365
 
364
366
  background_dem_preview_image = cv2.resize(
365
- background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
367
+ background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4 # type: ignore
366
368
  )
367
369
  background_dem_preview_image = cv2.normalize(
368
370
  background_dem_preview_image,
@@ -411,7 +413,7 @@ class Background(MeshComponent, ImageComponent):
411
413
  self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
412
414
 
413
415
  dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
414
- dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
416
+ dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB) # type: ignore
415
417
  cv2.imwrite(grayscale_dem_path, dem_data_rgb)
416
418
  return grayscale_dem_path
417
419
 
@@ -435,7 +437,7 @@ class Background(MeshComponent, ImageComponent):
435
437
  dem_data_normalized = np.empty_like(dem_data)
436
438
 
437
439
  # Normalize the DEM data to the range [0, 255]
438
- cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
440
+ cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX) # type: ignore
439
441
  dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
440
442
 
441
443
  cv2.imwrite(colored_dem_path, dem_data_colored)
@@ -470,6 +472,7 @@ class Background(MeshComponent, ImageComponent):
470
472
  logger=self.logger,
471
473
  texture_custom_schema=background_layers, # type: ignore
472
474
  skip_scaling=True, # type: ignore
475
+ info_layer_path=os.path.join(self.info_layers_directory, "background.json"), # type: ignore
473
476
  )
474
477
 
475
478
  self.background_texture.preprocess()
@@ -513,8 +516,8 @@ class Background(MeshComponent, ImageComponent):
513
516
  )
514
517
 
515
518
  dem_image = self.subtract_by_mask(
516
- dem_image,
517
- water_resources_image,
519
+ dem_image, # type: ignore
520
+ water_resources_image, # type: ignore
518
521
  int(self.map.dem_settings.water_depth * z_scaling_factor),
519
522
  )
520
523
 
@@ -534,8 +537,148 @@ class Background(MeshComponent, ImageComponent):
534
537
 
535
538
  return blur_power
536
539
 
540
+ def generate_linebased_water(self) -> None:
541
+ """Generates water resources based on line-based polylines from the background info layer.
542
+ It creates polygons from the polylines, fits them into the map bounds, and generates a mesh.
543
+ """
544
+ self.logger.debug("Starting line-based water generation...")
545
+
546
+ water_polylines = self.get_infolayer_data(Parameters.BACKGROUND, Parameters.WATER_POLYLINES)
547
+ self.logger.debug(
548
+ "Found %s water polylines in background info layer.", len(water_polylines) # type: ignore
549
+ )
550
+ if not water_polylines:
551
+ self.logger.warning("No water polylines found in background info layer.")
552
+ return
553
+
554
+ polygons: list[shapely.Polygon] = []
555
+ for polyline in water_polylines:
556
+ points = polyline["points"]
557
+ width = polyline["width"]
558
+ if not points or len(points) < 2:
559
+ self.logger.warning("Skipping polyline with insufficient points: %s", polyline)
560
+ continue
561
+
562
+ # Create a shapely LineString from the points
563
+ line = shapely.geometry.LineString(points)
564
+ # Create a buffer around the line to create a polygon
565
+ if width <= 0:
566
+ self.logger.warning("Skipping polyline with non-positive width: %s", polyline)
567
+ continue
568
+
569
+ polygon = line.buffer(
570
+ width + Parameters.WATER_ADD_WIDTH, cap_style=shapely.geometry.CAP_STYLE.square
571
+ )
572
+ if polygon.is_empty:
573
+ self.logger.warning("Skipping empty polygon created from polyline: %s", polyline)
574
+ continue
575
+
576
+ # Ensure the polygon is valid and not empty
577
+ if not polygon.is_valid:
578
+ self.logger.warning("Invalid polygon created from polyline, skipping: %s", polyline)
579
+ continue
580
+
581
+ polygons.append(polygon)
582
+
583
+ fitted_polygons = []
584
+ for polygon in polygons:
585
+ try:
586
+ fitted_polygon_points = self.fit_object_into_bounds(
587
+ polygon_points=polygon.exterior.coords,
588
+ angle=self.rotation,
589
+ canvas_size=self.background_size,
590
+ )
591
+ fitted_polygon = shapely.Polygon(fitted_polygon_points)
592
+ fitted_polygons.append(fitted_polygon)
593
+ except Exception as e:
594
+ self.logger.warning(
595
+ "Could not fit polygon into bounds with error: %s, polygon: %s", e, polygon
596
+ )
597
+ continue
598
+
599
+ if not fitted_polygons:
600
+ self.logger.warning("No valid water polygons created from polylines.")
601
+ return
602
+
603
+ # Create a mesh from the 3D polygons
604
+ mesh = self.mesh_from_3d_polygons(fitted_polygons)
605
+ if mesh is None:
606
+ self.logger.warning("No mesh could be created from the water polygons.")
607
+ return
608
+ self.logger.debug("Created mesh from %s water polygons.", len(fitted_polygons))
609
+
610
+ mesh = self.rotate_mesh(mesh)
611
+ mesh = self.invert_faces(mesh)
612
+
613
+ line_based_save_path = os.path.join(self.water_directory, "line_based_water.obj")
614
+ mesh.export(line_based_save_path)
615
+ self.logger.debug("Line-based water mesh saved to %s", line_based_save_path)
616
+
617
+ def mesh_from_3d_polygons(self, polygons: list[shapely.Polygon]) -> Trimesh | None:
618
+ """Create a simple mesh from a list of 3D shapely Polygons.
619
+ Each polygon must be flat (all Z the same or nearly the same for each polygon).
620
+ Returns a single Trimesh mesh.
621
+
622
+ Arguments:
623
+ polygons (list[shapely.Polygon]): List of 3D shapely Polygons to create the mesh from.
624
+
625
+ Returns:
626
+ Trimesh: A single Trimesh object containing the mesh created from the polygons.
627
+ """
628
+
629
+ all_vertices = []
630
+ all_faces = []
631
+ vertex_offset = 0
632
+
633
+ not_resized_dem = cv2.imread(self.not_resized_path, cv2.IMREAD_UNCHANGED)
634
+
635
+ for polygon in polygons:
636
+ # Get exterior 3D coordinates
637
+ exterior_coords = np.array(polygon.exterior.coords)
638
+ # Project to 2D for triangulation
639
+ exterior_2d = exterior_coords[:, :2]
640
+ poly_2d = shapely.geometry.Polygon(
641
+ exterior_2d, [np.array(ring.coords)[:, :2] for ring in polygon.interiors]
642
+ )
643
+
644
+ # Triangulate in 2D
645
+ vertices_2d, faces = trimesh.creation.triangulate_polygon(poly_2d)
646
+ # tris.vertices is 2D, tris.faces are indices
647
+
648
+ # Map 2D triangulated vertices back to 3D by matching to original 3D coords
649
+ vertices_3d = []
650
+ for v in vertices_2d:
651
+ # Find closest original 2D point to get Z
652
+ dists = np.linalg.norm(exterior_2d - v[:2], axis=1)
653
+ idx = np.argmin(dists)
654
+ # z = exterior_coords[idx, 2]
655
+ z = self.get_z_coordinate_from_dem(
656
+ not_resized_dem, exterior_coords[idx, 0], exterior_coords[idx, 1] # type: ignore
657
+ )
658
+ vertices_3d.append([v[0], v[1], z])
659
+ vertices_3d = np.array(vertices_3d) # type: ignore
660
+
661
+ faces = faces + vertex_offset
662
+ all_vertices.append(vertices_3d)
663
+ all_faces.append(faces)
664
+ vertex_offset += len(vertices_3d)
665
+
666
+ if not all_vertices:
667
+ return None
668
+
669
+ vertices = np.vstack(all_vertices)
670
+ faces = np.vstack(all_faces)
671
+ mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
672
+ return mesh
673
+
537
674
  def generate_water_resources_obj(self) -> None:
538
675
  """Generates 3D obj files based on water resources data."""
676
+ self.logger.debug("Starting water resources generation...")
677
+ try:
678
+ self.generate_linebased_water()
679
+ except Exception as e:
680
+ self.logger.error("Error during line-based water generation: %s", e)
681
+
539
682
  if not os.path.isfile(self.water_resources_path):
540
683
  self.logger.warning("Water resources texture not found.")
541
684
  return
@@ -544,12 +687,12 @@ class Background(MeshComponent, ImageComponent):
544
687
  plane_water = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
545
688
 
546
689
  # Check if the image contains non-zero values.
547
- if not np.any(plane_water):
690
+ if not np.any(plane_water): # type: ignore
548
691
  self.logger.debug("Water resources image is empty, skipping water generation.")
549
692
  return
550
693
 
551
694
  dilated_plane_water = cv2.dilate(
552
- plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
695
+ plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5 # type: ignore
553
696
  ).astype(np.uint8)
554
697
  plane_save_path = os.path.join(self.water_directory, "plane_water.obj")
555
698
  self.plane_from_np(dilated_plane_water, plane_save_path, include_zeros=False)
@@ -560,12 +703,12 @@ class Background(MeshComponent, ImageComponent):
560
703
  if self.map.output_size is not None:
561
704
  scaled_background_size = int(self.background_size * self.map.size_scale)
562
705
  plane_water = cv2.resize(
563
- plane_water,
706
+ plane_water, # type: ignore
564
707
  (scaled_background_size, scaled_background_size),
565
708
  interpolation=cv2.INTER_NEAREST,
566
709
  )
567
710
  background_dem = cv2.resize(
568
- background_dem,
711
+ background_dem, # type: ignore
569
712
  (scaled_background_size, scaled_background_size),
570
713
  interpolation=cv2.INTER_NEAREST,
571
714
  )
@@ -574,20 +717,20 @@ class Background(MeshComponent, ImageComponent):
574
717
  # Apply Gaussian blur to the background dem.
575
718
  blur_power = self._get_blur_power()
576
719
  background_dem = cv2.GaussianBlur(
577
- background_dem, (blur_power, blur_power), sigmaX=blur_power, sigmaY=blur_power
720
+ background_dem, (blur_power, blur_power), sigmaX=blur_power, sigmaY=blur_power # type: ignore
578
721
  )
579
722
 
580
723
  # Remove all the values from the background dem where the plane_water is 0.
581
- background_dem[plane_water == 0] = 0
724
+ background_dem[plane_water == 0] = 0 # type: ignore
582
725
 
583
726
  # Dilate the background dem to make the water more smooth.
584
- elevated_water = cv2.dilate(background_dem, np.ones((3, 3), np.uint16), iterations=10)
727
+ elevated_water = cv2.dilate(background_dem, np.ones((3, 3), np.uint16), iterations=10) # type: ignore
585
728
 
586
729
  # Use the background dem as a mask to prevent the original values from being overwritten.
587
- mask = background_dem > 0
730
+ mask = background_dem > 0 # type: ignore
588
731
 
589
732
  # Combine the dilated background dem with non-dilated background dem.
590
- elevated_water = np.where(mask, background_dem, elevated_water)
733
+ elevated_water = np.where(mask, background_dem, elevated_water) # type: ignore
591
734
  elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
592
735
 
593
736
  self.assets.water_mesh = elevated_save_path
@@ -8,6 +8,7 @@ from copy import deepcopy
8
8
  from typing import TYPE_CHECKING, Any
9
9
 
10
10
  import cv2
11
+ import numpy as np
11
12
  import osmnx as ox
12
13
  from pyproj import Transformer
13
14
  from shapely.affinity import rotate, translate
@@ -363,6 +364,7 @@ class Component:
363
364
  margin: int = 0,
364
365
  angle: int = 0,
365
366
  border: int = 0,
367
+ canvas_size: int | None = None,
366
368
  ) -> list[tuple[int, int]]:
367
369
  """Fits a polygon into the bounds of the map.
368
370
 
@@ -379,8 +381,10 @@ class Component:
379
381
  if polygon_points is None and linestring_points is None:
380
382
  raise ValueError("Either polygon or linestring points must be provided.")
381
383
 
384
+ limit = canvas_size or self.scaled_size
385
+
382
386
  min_x = min_y = 0 + border
383
- max_x = max_y = self.scaled_size - border
387
+ max_x = max_y = limit - border
384
388
 
385
389
  object_type = Polygon if polygon_points else LineString
386
390
 
@@ -586,3 +590,24 @@ class Component:
586
590
  int: The output size of the map or the map size.
587
591
  """
588
592
  return self.map_size if self.map.output_size is None else self.map.output_size
593
+
594
+ def get_z_coordinate_from_dem(self, not_resized_dem: np.ndarray, x: int, y: int) -> float:
595
+ """Gets the Z coordinate from the DEM image for the given coordinates.
596
+
597
+ Arguments:
598
+ not_resized_dem (np.ndarray): The not resized DEM image.
599
+ x (int): The x coordinate.
600
+ y (int): The y coordinate.
601
+
602
+ Returns:
603
+ float: The Z coordinate.
604
+ """
605
+ dem_x_size, dem_y_size = not_resized_dem.shape
606
+
607
+ x = int(max(0, min(x, dem_x_size - 1)))
608
+ y = int(max(0, min(y, dem_y_size - 1)))
609
+
610
+ z = not_resized_dem[y, x]
611
+ z *= self.get_z_scaling_factor(ignore_height_scale_multiplier=True)
612
+
613
+ return z
@@ -252,3 +252,18 @@ class MeshComponent(Component):
252
252
  distance = int(round(x_size) / 2)
253
253
  mesh_copy.apply_translation([-distance, distance, 0])
254
254
  return mesh_copy
255
+
256
+ @staticmethod
257
+ def invert_faces(mesh: trimesh.Trimesh) -> trimesh.Trimesh:
258
+ """
259
+ Inverts the faces (normals) of the mesh by reversing the order of indices in each face.
260
+
261
+ Arguments:
262
+ mesh (trimesh.Trimesh): The mesh whose faces are to be inverted.
263
+
264
+ Returns:
265
+ trimesh.Trimesh: A new mesh with inverted faces.
266
+ """
267
+ mesh_copy = mesh.copy()
268
+ mesh_copy.faces = mesh_copy.faces[:, ::-1]
269
+ return mesh_copy
@@ -118,14 +118,14 @@ class GRLE(ImageComponent, XMLComponent):
118
118
  # Resize the preview image to the maximum size allowed for previews.
119
119
  image = cv2.imread(preview_path, cv2.IMREAD_GRAYSCALE)
120
120
  if (
121
- image.shape[0] > Parameters.PREVIEW_MAXIMUM_SIZE
122
- or image.shape[1] > Parameters.PREVIEW_MAXIMUM_SIZE
121
+ image.shape[0] > Parameters.PREVIEW_MAXIMUM_SIZE # type: ignore
122
+ or image.shape[1] > Parameters.PREVIEW_MAXIMUM_SIZE # type: ignore
123
123
  ):
124
124
  image = cv2.resize(
125
- image, (Parameters.PREVIEW_MAXIMUM_SIZE, Parameters.PREVIEW_MAXIMUM_SIZE)
125
+ image, (Parameters.PREVIEW_MAXIMUM_SIZE, Parameters.PREVIEW_MAXIMUM_SIZE) # type: ignore
126
126
  )
127
127
  image_normalized = np.empty_like(image)
128
- cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX)
128
+ cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX) # type: ignore
129
129
  image_colored = cv2.applyColorMap(image_normalized, cv2.COLORMAP_JET)
130
130
  cv2.imwrite(save_path, image_colored)
131
131
  preview_paths.append(save_path)
@@ -163,7 +163,7 @@ class GRLE(ImageComponent, XMLComponent):
163
163
  return None
164
164
  fields_np = cv2.imread(fields_layer_path)
165
165
  # Resize fields_np to the same size as farmlands_np.
166
- fields_np = cv2.resize(fields_np, (farmlands_np.shape[1], farmlands_np.shape[0]))
166
+ fields_np = cv2.resize(fields_np, (farmlands_np.shape[1], farmlands_np.shape[0])) # type: ignore
167
167
 
168
168
  # use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
169
169
  return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
@@ -235,7 +235,7 @@ class GRLE(ImageComponent, XMLComponent):
235
235
  break
236
236
 
237
237
  try:
238
- cv2.fillPoly(image, [farmland_np], (float(farmland_id),))
238
+ cv2.fillPoly(image, [farmland_np], (float(farmland_id),)) # type: ignore
239
239
  except Exception as e:
240
240
  self.logger.debug(
241
241
  "Farmland %s could not be added to the InfoLayer PNG file with error: %s",
@@ -257,9 +257,9 @@ class GRLE(ImageComponent, XMLComponent):
257
257
 
258
258
  # Replace all the zero values on the info layer image with 255.
259
259
  if self.map.grle_settings.fill_empty_farmlands:
260
- image[image == 0] = 255
260
+ image[image == 0] = 255 # type: ignore
261
261
 
262
- cv2.imwrite(info_layer_farmlands_path, image)
262
+ cv2.imwrite(info_layer_farmlands_path, image) # type: ignore
263
263
 
264
264
  self.assets.farmlands = info_layer_farmlands_path
265
265
 
@@ -303,8 +303,8 @@ class GRLE(ImageComponent, XMLComponent):
303
303
  # Density map of the fruits is 2X size of the base image, so we need to resize it.
304
304
  # We'll resize the base image to make it bigger, so we can compare the values.
305
305
  grass_image = cv2.resize(
306
- grass_image,
307
- (grass_image.shape[1] * 2, grass_image.shape[0] * 2),
306
+ grass_image, # type: ignore
307
+ (grass_image.shape[1] * 2, grass_image.shape[0] * 2), # type: ignore
308
308
  interpolation=cv2.INTER_NEAREST,
309
309
  )
310
310
  if forest_image is not None:
@@ -352,15 +352,15 @@ class GRLE(ImageComponent, XMLComponent):
352
352
  # Three channeled 8-bit image, where non-zero values are the
353
353
  # different types of plants (only in the R channel).
354
354
  density_map_fruits = cv2.imread(density_map_fruit_path, cv2.IMREAD_UNCHANGED)
355
- self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape)
355
+ self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape) # type: ignore
356
356
 
357
357
  # Put the updated base image as the B channel in the density map.
358
- density_map_fruits[:, :, 0] = grass_image_copy
358
+ density_map_fruits[:, :, 0] = grass_image_copy # type: ignore
359
359
  self.logger.debug("Updated base image added as the B channel in the density map.")
360
360
 
361
361
  # Save the updated density map.
362
362
  # Ensure that order of channels is correct because CV2 uses BGR and we need RGB.
363
- density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB)
363
+ density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB) # type: ignore
364
364
  cv2.imwrite(density_map_fruit_path, density_map_fruits)
365
365
 
366
366
  self.assets.plants = density_map_fruit_path
@@ -543,7 +543,7 @@ class I3d(XMLComponent):
543
543
 
544
544
  forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
545
545
  for x, y in self.non_empty_pixels(
546
- forest_image, step=self.map.i3d_settings.forest_density
546
+ forest_image, step=self.map.i3d_settings.forest_density # type: ignore
547
547
  ):
548
548
  shifted_x, shifted_y = self.randomize_coordinates(
549
549
  (x, y),
@@ -638,24 +638,3 @@ class I3d(XMLComponent):
638
638
  not_resized_dem = cv2.imread(background_component.not_resized_path, cv2.IMREAD_UNCHANGED)
639
639
 
640
640
  return not_resized_dem
641
-
642
- def get_z_coordinate_from_dem(self, not_resized_dem: np.ndarray, x: int, y: int) -> float:
643
- """Gets the Z coordinate from the DEM image for the given coordinates.
644
-
645
- Arguments:
646
- not_resized_dem (np.ndarray): The not resized DEM image.
647
- x (int): The x coordinate.
648
- y (int): The y coordinate.
649
-
650
- Returns:
651
- float: The Z coordinate.
652
- """
653
- dem_x_size, dem_y_size = not_resized_dem.shape
654
-
655
- x = int(max(0, min(x, dem_x_size - 1)))
656
- y = int(max(0, min(y, dem_y_size - 1)))
657
-
658
- z = not_resized_dem[y, x]
659
- z *= self.get_z_scaling_factor(ignore_height_scale_multiplier=True)
660
-
661
- return z
@@ -44,7 +44,10 @@ class Texture(ImageComponent):
44
44
  os.makedirs(self.procedural_dir, exist_ok=True)
45
45
 
46
46
  self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
47
- self.info_layer_path = os.path.join(self.info_layers_directory, "textures.json")
47
+ if not self.kwargs.get("info_layer_path"):
48
+ self.info_layer_path = os.path.join(self.info_layers_directory, "textures.json")
49
+ else:
50
+ self.info_layer_path = self.kwargs["info_layer_path"] # type: ignore
48
51
 
49
52
  def read_layers(self, layers_schema: list[dict[str, Any]]) -> None:
50
53
  """Reads layers from the schema.
@@ -174,9 +177,9 @@ class Texture(ImageComponent):
174
177
  if not border:
175
178
  continue
176
179
 
177
- self.transfer_border(layer_image, base_layer_image, border)
180
+ self.transfer_border(layer_image, base_layer_image, border) # type: ignore
178
181
 
179
- cv2.imwrite(layer.path(self._weights_dir), layer_image)
182
+ cv2.imwrite(layer.path(self._weights_dir), layer_image) # type: ignore
180
183
  self.logger.debug("Borders added to layer %s.", layer.name)
181
184
 
182
185
  if base_layer_image is not None:
@@ -261,7 +264,7 @@ class Texture(ImageComponent):
261
264
  self.logger.debug("Scaling layer %s.", layer_path)
262
265
  img = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
263
266
  img = cv2.resize(
264
- img,
267
+ img, # type: ignore
265
268
  (self.map.output_size, self.map.output_size),
266
269
  interpolation=cv2.INTER_NEAREST,
267
270
  )
@@ -384,12 +387,12 @@ class Texture(ImageComponent):
384
387
  self.logger.debug("First layer, creating new cumulative image.")
385
388
  cumulative_image = layer_image
386
389
 
387
- mask = cv2.bitwise_not(cumulative_image)
388
- self._draw_layer(layer, info_layer_data, layer_image)
390
+ mask = cv2.bitwise_not(cumulative_image) # type: ignore
391
+ self._draw_layer(layer, info_layer_data, layer_image) # type: ignore
389
392
  self._add_roads(layer, info_layer_data)
390
393
 
391
- output_image = cv2.bitwise_and(layer_image, mask)
392
- cumulative_image = cv2.bitwise_or(cumulative_image, output_image)
394
+ output_image = cv2.bitwise_and(layer_image, mask) # type: ignore
395
+ cumulative_image = cv2.bitwise_or(cumulative_image, output_image) # type: ignore
393
396
 
394
397
  cv2.imwrite(layer_path, output_image)
395
398
  self.logger.debug("Texture %s saved.", layer_path)
@@ -452,7 +455,11 @@ class Texture(ImageComponent):
452
455
  layer (Layer): Layer with textures and tags.
453
456
  info_layer_data (dict[list[list[int]]]): Dictionary to store info layer data.
454
457
  """
455
- if layer.info_layer == "roads":
458
+ linestring_infolayers = ["roads"]
459
+ if self.kwargs.get("info_layer_path", None):
460
+ linestring_infolayers.append("water")
461
+
462
+ if layer.info_layer in linestring_infolayers:
456
463
  for linestring in self.objects_generator(
457
464
  layer.tags, layer.width, layer.info_layer, yield_linestrings=True
458
465
  ):
@@ -464,6 +471,7 @@ class Texture(ImageComponent):
464
471
  linestring_entry = {
465
472
  "points": linestring,
466
473
  "tags": str(layer.tags),
474
+ "width": layer.width,
467
475
  }
468
476
  info_layer_data[f"{layer.info_layer}_polylines"].append(linestring_entry) # type: ignore
469
477
 
@@ -490,17 +498,17 @@ class Texture(ImageComponent):
490
498
  # Check if the image contains any non-zero values, otherwise continue.
491
499
  layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
492
500
 
493
- if not np.any(layer_image):
501
+ if not np.any(layer_image): # type: ignore
494
502
  self.logger.debug(
495
503
  "Layer %s does not contain any non-zero values, skipping.", layer.name
496
504
  )
497
505
  continue
498
506
 
499
507
  # Save the original image to use it for preview later, without combining the sublayers.
500
- cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy())
508
+ cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy()) # type: ignore
501
509
 
502
510
  # Get the coordinates of non-zero values.
503
- non_zero_coords = np.column_stack(np.where(layer_image > 0))
511
+ non_zero_coords = np.column_stack(np.where(layer_image > 0)) # type: ignore
504
512
 
505
513
  # Prepare sublayers.
506
514
  sublayers = [np.zeros_like(layer_image) for _ in range(layer.count)]
@@ -817,7 +825,7 @@ class Texture(ImageComponent):
817
825
 
818
826
  images = [
819
827
  cv2.resize(
820
- cv2.imread(layer.get_preview_or_path(self._weights_dir), cv2.IMREAD_UNCHANGED),
828
+ cv2.imread(layer.get_preview_or_path(self._weights_dir), cv2.IMREAD_UNCHANGED), # type: ignore
821
829
  preview_size,
822
830
  )
823
831
  for layer in active_layers
maps4fs/generator/map.py CHANGED
@@ -445,6 +445,7 @@ def fix_osm_file(input_file_path: str, output_file_path: str | None = None) -> t
445
445
  and the number of fixed errors.
446
446
  """
447
447
  broken_entries = ["relation", ".//*[@action='delete']"]
448
+ output_file_path = output_file_path or input_file_path
448
449
 
449
450
  tree = ET.parse(input_file_path)
450
451
  root = tree.getroot()
@@ -15,8 +15,10 @@ class Parameters:
15
15
  FIELDS = "fields"
16
16
  BUILDINGS = "buildings"
17
17
  TEXTURES = "textures"
18
+ BACKGROUND = "background"
18
19
  FOREST = "forest"
19
20
  ROADS_POLYLINES = "roads_polylines"
21
+ WATER_POLYLINES = "water_polylines"
20
22
  FARMYARDS = "farmyards"
21
23
 
22
24
  PREVIEW_MAXIMUM_SIZE = 2048
@@ -35,6 +37,8 @@ class Parameters:
35
37
  PLANTS_ISLAND_VERTEX_COUNT = 30
36
38
  PLANTS_ISLAND_ROUNDING_RADIUS = 15
37
39
 
40
+ WATER_ADD_WIDTH = 2
41
+
38
42
 
39
43
  class SharedSettings(BaseModel):
40
44
  """Represents the shared settings for all components."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.0.6
3
+ Version: 2.0.7
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: Apache License 2.0
@@ -2,26 +2,26 @@ maps4fs/__init__.py,sha256=Fy521EmVAWnhu6OvOInc97yrtJotFzcV0YfRB2b9O4s,314
2
2
  maps4fs/logger.py,sha256=WDfR14hxqy8b6xtwL6YIu2LGzFO1sbt0LxMgfsDTOkA,865
3
3
  maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
4
  maps4fs/generator/game.py,sha256=g8lMHuuRRmJLSDsQTAMv8p_-qntYMiZKnAqn7ru96i0,11645
5
- maps4fs/generator/map.py,sha256=CiCTxw-7nuEHVIvRUjHekxZQDdBppw6f1wn-shnqzBU,16298
5
+ maps4fs/generator/map.py,sha256=x_NjUwKkDnh2cC61QpTfX88mFHAkQG1sxVwZckoV0mE,16357
6
6
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
7
- maps4fs/generator/settings.py,sha256=8UbET3X1IbA7D4DX5HesZ7ijXLdzCCp7B3p3mFCZs8s,7429
7
+ maps4fs/generator/settings.py,sha256=CRXpUi_ftZP9N6FckVnnXPzQKfShP_5zaDy2mhKgaR4,7524
8
8
  maps4fs/generator/statistics.py,sha256=aynS3zbAtiwnU_YLKHPTiiaKW98_suvQUhy1SGBA6mc,2448
9
9
  maps4fs/generator/component/__init__.py,sha256=s01yVVVi8R2xxNvflu2D6wTd9I_g73AMM2x7vAC7GX4,490
10
- maps4fs/generator/component/background.py,sha256=9CyT5MoQWILFwEGZThZnn3l1UEDI-5Sg0-VUV9TT240,23722
10
+ maps4fs/generator/component/background.py,sha256=etQ3woLLfLA4_SYMtUcSRy6aGYKtJAROKFEXr9SHTcE,30019
11
11
  maps4fs/generator/component/config.py,sha256=IP530sapLofFskSnBEB96G0aUSd6Sno0G9ET3ca0ZOQ,3696
12
12
  maps4fs/generator/component/dem.py,sha256=SH_2Zu5O4dhWtZeOkCwzDF4RU04XhTdpGFYaRYJkdjc,11905
13
- maps4fs/generator/component/grle.py,sha256=5ftHSg8FwSkabVlStlfG_erGZSOrilU-RUvCobfT1Yw,19539
14
- maps4fs/generator/component/i3d.py,sha256=5703ntFt7EQKFpQWQN5ST_bM08akV3rpSwKZDvkFp6w,24462
13
+ maps4fs/generator/component/grle.py,sha256=8K32pC_ar9CR6p0EhCe2X--wEoIxFzJCPcN9ydHQ1LE,19747
14
+ maps4fs/generator/component/i3d.py,sha256=iGBDaCPC-EqoJ7C4PrKSel6mW4U1DPfgSp6pPNR-Xr8,23788
15
15
  maps4fs/generator/component/layer.py,sha256=-br4gAGcGeBNb3ldch9XFEK0lhXqb1IbArhFB5Owu54,6186
16
16
  maps4fs/generator/component/satellite.py,sha256=OsxoNOCgkUtRzL7Geuqubsf6uoKXAIN8jQvrJ7IFeAI,4958
17
- maps4fs/generator/component/texture.py,sha256=Nc_oOHX3b4vJm8FnNOn3W4EQGFkW0zW0rGzO_0nTJMM,33392
17
+ maps4fs/generator/component/texture.py,sha256=krtvOS0hH8BTzfxd2jsTo3bIJYRkIVbaz6FGhWY8L1o,33921
18
18
  maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
19
- maps4fs/generator/component/base/component.py,sha256=HeNDaToKrS6OLeJepKZA7iQzZQDYy-9QRtv1A73Ire0,22090
19
+ maps4fs/generator/component/base/component.py,sha256=Vgmdsn1ZC37EwWi4Va4uYVt0RnFLiARTtZ-R5GTSrrM,22877
20
20
  maps4fs/generator/component/base/component_image.py,sha256=2NYJgCU8deHl7O2FYFYk38WKZVJygFoc2gjBXwH6vjM,5970
21
- maps4fs/generator/component/base/component_mesh.py,sha256=7CfaEpfj_4P5LfAjFT4L76pTokqf6zmla9__sQNLUpA,8587
21
+ maps4fs/generator/component/base/component_mesh.py,sha256=lZuF-9hzDYodL4Ub4RgvFjsozxTPZJrOaLQmlNTrZ7I,9074
22
22
  maps4fs/generator/component/base/component_xml.py,sha256=V9pGUvHh6UF6BP0qFARqDq9vquoAgq1zJqhOgBoeS_Y,3983
23
- maps4fs-2.0.6.dist-info/licenses/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
24
- maps4fs-2.0.6.dist-info/METADATA,sha256=ONurcfsrzI0SqqGbmOY_EXwT7bbxI0_siBNAJO2_68I,44135
25
- maps4fs-2.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
- maps4fs-2.0.6.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
27
- maps4fs-2.0.6.dist-info/RECORD,,
23
+ maps4fs-2.0.7.dist-info/licenses/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
24
+ maps4fs-2.0.7.dist-info/METADATA,sha256=9jqulAtUcyYRpILh0AZOCt8ZsIhIhJ2Dln9N-E3p0Fc,44135
25
+ maps4fs-2.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
+ maps4fs-2.0.7.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
27
+ maps4fs-2.0.7.dist-info/RECORD,,