maps4fs 2.0.6__tar.gz → 2.0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {maps4fs-2.0.6 → maps4fs-2.0.8}/PKG-INFO +1 -1
  2. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/background.py +148 -18
  3. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/component.py +26 -1
  4. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/component_mesh.py +15 -0
  5. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/grle.py +13 -13
  6. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/i3d.py +1 -22
  7. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/texture.py +21 -13
  8. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/map.py +1 -0
  9. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/settings.py +5 -0
  10. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/PKG-INFO +1 -1
  11. {maps4fs-2.0.6 → maps4fs-2.0.8}/pyproject.toml +1 -1
  12. {maps4fs-2.0.6 → maps4fs-2.0.8}/LICENSE.md +0 -0
  13. {maps4fs-2.0.6 → maps4fs-2.0.8}/README.md +0 -0
  14. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/__init__.py +0 -0
  15. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/__init__.py +0 -0
  16. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/__init__.py +0 -0
  17. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/__init__.py +0 -0
  18. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/component_image.py +0 -0
  19. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/component_xml.py +0 -0
  20. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/config.py +0 -0
  21. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/dem.py +0 -0
  22. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/layer.py +0 -0
  23. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/satellite.py +0 -0
  24. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/game.py +0 -0
  25. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/qgis.py +0 -0
  26. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/statistics.py +0 -0
  27. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/logger.py +0 -0
  28. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/SOURCES.txt +0 -0
  29. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/dependency_links.txt +0 -0
  30. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/requires.txt +0 -0
  31. {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/top_level.txt +0 -0
  32. {maps4fs-2.0.6 → maps4fs-2.0.8}/setup.cfg +0 -0
  33. {maps4fs-2.0.6 → maps4fs-2.0.8}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.0.6
3
+ Version: 2.0.8
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
@@ -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,135 @@ 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
+ water_polygons = self.get_infolayer_data(Parameters.BACKGROUND, Parameters.WATER)
546
+ self.logger.debug(
547
+ "Found %s water polygons in background info layer.", len(water_polygons) # type: ignore
548
+ )
549
+ if not water_polygons:
550
+ self.logger.warning("No water polygons found in background info layer.")
551
+ return
552
+
553
+ polygons: list[shapely.Polygon] = []
554
+ for polygon_points in water_polygons:
555
+ if not polygon_points or len(polygon_points) < 2:
556
+ self.logger.warning("Skipping polygon with insufficient points...")
557
+ continue
558
+
559
+ polygon = shapely.Polygon(polygon_points)
560
+
561
+ if polygon.is_empty or not polygon.is_valid:
562
+ self.logger.warning("Skipping empty or invalid polygon...")
563
+ continue
564
+
565
+ # Make Polygon a little bit bigger to hide under the terrain when creating water planes.
566
+ polygon = polygon.buffer(Parameters.WATER_ADD_WIDTH, resolution=4)
567
+
568
+ polygons.append(polygon)
569
+
570
+ fitted_polygons = []
571
+ for polygon in polygons:
572
+ try:
573
+ fitted_polygon_points = self.fit_object_into_bounds(
574
+ polygon_points=polygon.exterior.coords,
575
+ angle=self.rotation,
576
+ canvas_size=self.background_size,
577
+ )
578
+ fitted_polygon = shapely.Polygon(fitted_polygon_points)
579
+ fitted_polygons.append(fitted_polygon)
580
+ except Exception as e:
581
+ self.logger.warning(
582
+ "Could not fit polygon into bounds with error: %s, polygon: %s", e, polygon
583
+ )
584
+ continue
585
+
586
+ if not fitted_polygons:
587
+ self.logger.warning("No valid water polygons created from polylines.")
588
+ return
589
+
590
+ # Create a mesh from the 3D polygons
591
+ mesh = self.mesh_from_3d_polygons(fitted_polygons)
592
+ if mesh is None:
593
+ self.logger.warning("No mesh could be created from the water polygons.")
594
+ return
595
+ self.logger.debug("Created mesh from %s water polygons.", len(fitted_polygons))
596
+
597
+ mesh = self.rotate_mesh(mesh)
598
+ mesh = self.invert_faces(mesh)
599
+
600
+ line_based_save_path = os.path.join(self.water_directory, "line_based_water.obj")
601
+ mesh.export(line_based_save_path)
602
+ self.logger.debug("Line-based water mesh saved to %s", line_based_save_path)
603
+
604
+ def mesh_from_3d_polygons(self, polygons: list[shapely.Polygon]) -> Trimesh | None:
605
+ """Create a simple mesh from a list of 3D shapely Polygons.
606
+ Each polygon must be flat (all Z the same or nearly the same for each polygon).
607
+ Returns a single Trimesh mesh.
608
+
609
+ Arguments:
610
+ polygons (list[shapely.Polygon]): List of 3D shapely Polygons to create the mesh from.
611
+
612
+ Returns:
613
+ Trimesh: A single Trimesh object containing the mesh created from the polygons.
614
+ """
615
+
616
+ all_vertices = []
617
+ all_faces = []
618
+ vertex_offset = 0
619
+
620
+ not_resized_dem = cv2.imread(self.not_resized_path, cv2.IMREAD_UNCHANGED)
621
+
622
+ for polygon in polygons:
623
+ # Get exterior 3D coordinates
624
+ exterior_coords = np.array(polygon.exterior.coords)
625
+ # Project to 2D for triangulation
626
+ exterior_2d = exterior_coords[:, :2]
627
+ poly_2d = shapely.geometry.Polygon(
628
+ exterior_2d, [np.array(ring.coords)[:, :2] for ring in polygon.interiors]
629
+ )
630
+
631
+ # Triangulate in 2D
632
+ vertices_2d, faces = trimesh.creation.triangulate_polygon(poly_2d)
633
+ # tris.vertices is 2D, tris.faces are indices
634
+
635
+ # Map 2D triangulated vertices back to 3D by matching to original 3D coords
636
+ vertices_3d = []
637
+ for v in vertices_2d:
638
+ # Find closest original 2D point to get Z
639
+ dists = np.linalg.norm(exterior_2d - v[:2], axis=1)
640
+ idx = np.argmin(dists)
641
+ # z = exterior_coords[idx, 2]
642
+ z = self.get_z_coordinate_from_dem(
643
+ not_resized_dem, exterior_coords[idx, 0], exterior_coords[idx, 1] # type: ignore
644
+ )
645
+ vertices_3d.append([v[0], v[1], z])
646
+ vertices_3d = np.array(vertices_3d) # type: ignore
647
+
648
+ faces = faces + vertex_offset
649
+ all_vertices.append(vertices_3d)
650
+ all_faces.append(faces)
651
+ vertex_offset += len(vertices_3d)
652
+
653
+ if not all_vertices:
654
+ return None
655
+
656
+ vertices = np.vstack(all_vertices)
657
+ faces = np.vstack(all_faces)
658
+ mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
659
+ return mesh
660
+
537
661
  def generate_water_resources_obj(self) -> None:
538
662
  """Generates 3D obj files based on water resources data."""
663
+ self.logger.debug("Starting water resources generation...")
664
+ try:
665
+ self.generate_linebased_water()
666
+ except Exception as e:
667
+ self.logger.error("Error during line-based water generation: %s", e)
668
+
539
669
  if not os.path.isfile(self.water_resources_path):
540
670
  self.logger.warning("Water resources texture not found.")
541
671
  return
@@ -544,12 +674,12 @@ class Background(MeshComponent, ImageComponent):
544
674
  plane_water = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
545
675
 
546
676
  # Check if the image contains non-zero values.
547
- if not np.any(plane_water):
677
+ if not np.any(plane_water): # type: ignore
548
678
  self.logger.debug("Water resources image is empty, skipping water generation.")
549
679
  return
550
680
 
551
681
  dilated_plane_water = cv2.dilate(
552
- plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
682
+ plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5 # type: ignore
553
683
  ).astype(np.uint8)
554
684
  plane_save_path = os.path.join(self.water_directory, "plane_water.obj")
555
685
  self.plane_from_np(dilated_plane_water, plane_save_path, include_zeros=False)
@@ -560,12 +690,12 @@ class Background(MeshComponent, ImageComponent):
560
690
  if self.map.output_size is not None:
561
691
  scaled_background_size = int(self.background_size * self.map.size_scale)
562
692
  plane_water = cv2.resize(
563
- plane_water,
693
+ plane_water, # type: ignore
564
694
  (scaled_background_size, scaled_background_size),
565
695
  interpolation=cv2.INTER_NEAREST,
566
696
  )
567
697
  background_dem = cv2.resize(
568
- background_dem,
698
+ background_dem, # type: ignore
569
699
  (scaled_background_size, scaled_background_size),
570
700
  interpolation=cv2.INTER_NEAREST,
571
701
  )
@@ -574,20 +704,20 @@ class Background(MeshComponent, ImageComponent):
574
704
  # Apply Gaussian blur to the background dem.
575
705
  blur_power = self._get_blur_power()
576
706
  background_dem = cv2.GaussianBlur(
577
- background_dem, (blur_power, blur_power), sigmaX=blur_power, sigmaY=blur_power
707
+ background_dem, (blur_power, blur_power), sigmaX=blur_power, sigmaY=blur_power # type: ignore
578
708
  )
579
709
 
580
710
  # Remove all the values from the background dem where the plane_water is 0.
581
- background_dem[plane_water == 0] = 0
711
+ background_dem[plane_water == 0] = 0 # type: ignore
582
712
 
583
713
  # 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)
714
+ elevated_water = cv2.dilate(background_dem, np.ones((3, 3), np.uint16), iterations=10) # type: ignore
585
715
 
586
716
  # Use the background dem as a mask to prevent the original values from being overwritten.
587
- mask = background_dem > 0
717
+ mask = background_dem > 0 # type: ignore
588
718
 
589
719
  # Combine the dilated background dem with non-dilated background dem.
590
- elevated_water = np.where(mask, background_dem, elevated_water)
720
+ elevated_water = np.where(mask, background_dem, elevated_water) # type: ignore
591
721
  elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
592
722
 
593
723
  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
@@ -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,11 @@ 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"
22
+ WATER = "water"
20
23
  FARMYARDS = "farmyards"
21
24
 
22
25
  PREVIEW_MAXIMUM_SIZE = 2048
@@ -35,6 +38,8 @@ class Parameters:
35
38
  PLANTS_ISLAND_VERTEX_COUNT = 30
36
39
  PLANTS_ISLAND_ROUNDING_RADIUS = 15
37
40
 
41
+ WATER_ADD_WIDTH = 2
42
+
38
43
 
39
44
  class SharedSettings(BaseModel):
40
45
  """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.8
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maps4fs"
7
- version = "2.0.6"
7
+ version = "2.0.8"
8
8
  description = "Generate map templates for Farming Simulator from real places."
9
9
  authors = [{name = "iwatkot", email = "iwatkot@gmail.com"}]
10
10
  license = {text = "Apache License 2.0"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes