maps4fs 2.0.9__tar.gz → 2.1.1__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 (34) hide show
  1. {maps4fs-2.0.9 → maps4fs-2.1.1}/PKG-INFO +3 -1
  2. {maps4fs-2.0.9 → maps4fs-2.1.1}/README.md +2 -0
  3. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/base/component_mesh.py +1 -1
  4. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/base/component_xml.py +37 -0
  5. maps4fs-2.1.1/maps4fs/generator/component/config.py +215 -0
  6. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/i3d.py +91 -11
  7. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/game.py +30 -0
  8. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/settings.py +4 -0
  9. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs.egg-info/PKG-INFO +3 -1
  10. {maps4fs-2.0.9 → maps4fs-2.1.1}/pyproject.toml +1 -1
  11. maps4fs-2.0.9/maps4fs/generator/component/config.py +0 -94
  12. {maps4fs-2.0.9 → maps4fs-2.1.1}/LICENSE.md +0 -0
  13. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/__init__.py +0 -0
  14. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/__init__.py +0 -0
  15. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/__init__.py +0 -0
  16. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/background.py +0 -0
  17. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/base/__init__.py +0 -0
  18. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/base/component.py +0 -0
  19. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/base/component_image.py +0 -0
  20. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/dem.py +0 -0
  21. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/grle.py +0 -0
  22. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/layer.py +0 -0
  23. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/satellite.py +0 -0
  24. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/component/texture.py +0 -0
  25. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/map.py +0 -0
  26. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/qgis.py +0 -0
  27. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/generator/statistics.py +0 -0
  28. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs/logger.py +0 -0
  29. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs.egg-info/SOURCES.txt +0 -0
  30. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs.egg-info/dependency_links.txt +0 -0
  31. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs.egg-info/requires.txt +0 -0
  32. {maps4fs-2.0.9 → maps4fs-2.1.1}/maps4fs.egg-info/top_level.txt +0 -0
  33. {maps4fs-2.0.9 → maps4fs-2.1.1}/setup.cfg +0 -0
  34. {maps4fs-2.0.9 → maps4fs-2.1.1}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.0.9
3
+ Version: 2.1.1
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
@@ -650,6 +650,8 @@ You can also apply some advanced settings to the map generation process.<br>
650
650
 
651
651
  - Forest density - the density of the forest in meters. The lower the value, the lower the distance between the trees, which makes the forest denser. Note, that low values will lead to enormous number of trees, which may cause the Giants Editor to crash or lead to performance issues. By default, it's set to 10.
652
652
 
653
+ - Tree limit - this value will be used to adjust the forest density value. For example, if it's possible to place 100000 trees from OSM data, and the forest density is set to 10, the expected number of trees on map will be 10000. At the same time, if you set the tree limit to 5000, the forest density will be adjusted to 20, which means that the distance between the trees will be doubled. This value is useful to prevent the Giants Editor from crashing due to too many trees on the map. By default, it's set to 0, which means that it's disabled and will use the forest density value as is. Note, that it will not lead to the exact number of trees, but will adjust the forest density value to fit the tree limit so the resulting number of trees will be more or less equal to the tree limit value.
654
+
653
655
  - Trees relative shift - represents the maximum possible shift of the tree from it's original position in percents of the forest density value. The higher the value, the more the trees will be shifted from their original positions. Warning: higher values can lead to overlapping trees.
654
656
 
655
657
  - Splines density - number of points, which will be added (interpolate) between each pair of existing points. The higher the value, the denser the spline will be. It can smooth the splines, but high values can in opposite make the splines look unnatural.
@@ -619,6 +619,8 @@ You can also apply some advanced settings to the map generation process.<br>
619
619
 
620
620
  - Forest density - the density of the forest in meters. The lower the value, the lower the distance between the trees, which makes the forest denser. Note, that low values will lead to enormous number of trees, which may cause the Giants Editor to crash or lead to performance issues. By default, it's set to 10.
621
621
 
622
+ - Tree limit - this value will be used to adjust the forest density value. For example, if it's possible to place 100000 trees from OSM data, and the forest density is set to 10, the expected number of trees on map will be 10000. At the same time, if you set the tree limit to 5000, the forest density will be adjusted to 20, which means that the distance between the trees will be doubled. This value is useful to prevent the Giants Editor from crashing due to too many trees on the map. By default, it's set to 0, which means that it's disabled and will use the forest density value as is. Note, that it will not lead to the exact number of trees, but will adjust the forest density value to fit the tree limit so the resulting number of trees will be more or less equal to the tree limit value.
623
+
622
624
  - Trees relative shift - represents the maximum possible shift of the tree from it's original position in percents of the forest density value. The higher the value, the more the trees will be shifted from their original positions. Warning: higher values can lead to overlapping trees.
623
625
 
624
626
  - Splines density - number of points, which will be added (interpolate) between each pair of existing points. The higher the value, the denser the spline will be. It can smooth the splines, but high values can in opposite make the splines look unnatural.
@@ -265,5 +265,5 @@ class MeshComponent(Component):
265
265
  trimesh.Trimesh: A new mesh with inverted faces.
266
266
  """
267
267
  mesh_copy = mesh.copy()
268
- mesh_copy.faces = mesh_copy.faces[:, ::-1]
268
+ mesh_copy.faces = mesh_copy.faces[:, ::-1] # type: ignore
269
269
  return mesh_copy
@@ -4,6 +4,7 @@ import os
4
4
  from xml.etree import ElementTree as ET
5
5
 
6
6
  from maps4fs.generator.component.base.component import Component
7
+ from maps4fs.generator.settings import Parameters
7
8
 
8
9
 
9
10
  class XMLComponent(Component):
@@ -57,6 +58,20 @@ class XMLComponent(Component):
57
58
 
58
59
  tree.write(xml_path, encoding="utf-8", xml_declaration=True)
59
60
 
61
+ def get_element_from_tree(self, path: str, xml_path: str | None = None) -> ET.Element | None:
62
+ """Finds an element in the XML tree by the path.
63
+
64
+ Arguments:
65
+ path (str): The path to the element.
66
+ xml_path (str, optional): The path to the XML file. Defaults to None.
67
+
68
+ Returns:
69
+ ET.Element | None: The found element or None if not found.
70
+ """
71
+ tree = self.get_tree(xml_path)
72
+ root = tree.getroot()
73
+ return root.find(path) # type: ignore
74
+
60
75
  def get_and_update_element(self, root: ET.Element, path: str, data: dict[str, str]) -> None:
61
76
  """Finds the element by the path and updates it with the provided data.
62
77
 
@@ -106,3 +121,25 @@ class XMLComponent(Component):
106
121
  """
107
122
  element = ET.SubElement(parent, element_name)
108
123
  self.update_element(element, data)
124
+
125
+ def get_height_scale(self) -> int:
126
+ """Returns the height scale from the I3D file.
127
+
128
+ Returns:
129
+ int: The height scale value.
130
+
131
+ Raises:
132
+ ValueError: If the height scale element is not found in the I3D file.
133
+ """
134
+ height_scale_element = self.get_element_from_tree(
135
+ path=".//Scene/TerrainTransformGroup",
136
+ xml_path=self.game.i3d_file_path(self.map_directory),
137
+ )
138
+ if height_scale_element is None:
139
+ raise ValueError("Height scale element not found in the I3D file.")
140
+
141
+ height_scale = height_scale_element.get(Parameters.HEIGHT_SCALE)
142
+ if height_scale is None:
143
+ raise ValueError("Height scale not found in the I3D file.")
144
+
145
+ return int(height_scale)
@@ -0,0 +1,215 @@
1
+ """This module contains the Config class for map settings and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ import cv2
8
+
9
+ from maps4fs.generator.component.base.component_xml import XMLComponent
10
+
11
+
12
+ # pylint: disable=R0903
13
+ class Config(XMLComponent):
14
+ """Component for map settings and configuration.
15
+
16
+ Arguments:
17
+ game (Game): The game instance for which the map is generated.
18
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
19
+ map_size (int): The size of the map in pixels (it's a square).
20
+ map_rotated_size (int): The size of the map in pixels after rotation.
21
+ rotation (int): The rotation angle of the map.
22
+ map_directory (str): The directory where the map files are stored.
23
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
24
+ info, warning. If not provided, default logging will be used.
25
+ """
26
+
27
+ def preprocess(self) -> None:
28
+ """Gets the path to the map XML file and saves it to the instance variable."""
29
+ self.xml_path = self.game.map_xml_path(self.map_directory)
30
+ self.fog_parameters: dict[str, int] = {}
31
+
32
+ def process(self) -> None:
33
+ """Sets the map size in the map.xml file."""
34
+ self._set_map_size()
35
+
36
+ if self.game.fog_processing:
37
+ self._adjust_fog()
38
+
39
+ def _set_map_size(self) -> None:
40
+ """Edits map.xml file to set correct map size."""
41
+ tree = self.get_tree()
42
+ if not tree:
43
+ raise FileNotFoundError(f"Map XML file not found: {self.xml_path}")
44
+
45
+ root = tree.getroot()
46
+ data = {
47
+ "width": str(self.scaled_size),
48
+ "height": str(self.scaled_size),
49
+ }
50
+
51
+ for element in root.iter("map"): # type: ignore
52
+ self.update_element(element, data)
53
+ break
54
+ self.save_tree(tree)
55
+
56
+ def info_sequence(self) -> dict[str, dict[str, str | float | int]]:
57
+ """Returns information about the component.
58
+ Overview section is needed to create the overview file (in-game map).
59
+
60
+ Returns:
61
+ dict[str, dict[str, str | float | int]]: Information about the component.
62
+ """
63
+ # The overview file is exactly 2X bigger than the map size, does not matter
64
+ # if the map is 2048x2048 or 4096x4096, the overview will be 4096x4096
65
+ # and the map will be in the center of the overview.
66
+ # That's why the distance is set to the map height not as a half of it.
67
+ bbox = self.get_bbox(distance=self.map_size)
68
+ south, west, north, east = bbox
69
+ epsg3857_string = self.get_epsg3857_string(bbox=bbox)
70
+ epsg3857_string_with_margin = self.get_epsg3857_string(bbox=bbox, add_margin=True)
71
+
72
+ self.qgis_sequence()
73
+
74
+ overview_data = {
75
+ "epsg3857_string": epsg3857_string,
76
+ "epsg3857_string_with_margin": epsg3857_string_with_margin,
77
+ "south": south,
78
+ "west": west,
79
+ "north": north,
80
+ "east": east,
81
+ "height": self.map_size * 2,
82
+ "width": self.map_size * 2,
83
+ }
84
+
85
+ data = {
86
+ "Overview": overview_data,
87
+ }
88
+ if self.fog_parameters:
89
+ data["Fog"] = self.fog_parameters # type: ignore
90
+
91
+ return data # type: ignore
92
+
93
+ def qgis_sequence(self) -> None:
94
+ """Generates QGIS scripts for creating bounding box layers and rasterizing them."""
95
+ bbox = self.get_bbox(distance=self.map_size)
96
+ espg3857_bbox = self.get_espg3857_bbox(bbox=bbox)
97
+ espg3857_bbox_with_margin = self.get_espg3857_bbox(bbox=bbox, add_margin=True)
98
+
99
+ qgis_layers = [("Overview_bbox", *espg3857_bbox)]
100
+ qgis_layers_with_margin = [("Overview_bbox_with_margin", *espg3857_bbox_with_margin)]
101
+
102
+ layers = qgis_layers + qgis_layers_with_margin
103
+
104
+ self.create_qgis_scripts(layers)
105
+
106
+ def _adjust_fog(self) -> None:
107
+ """Adjusts the fog settings in the environment XML file based on the DEM and height scale."""
108
+ self.logger.debug("Adjusting fog settings based on DEM and height scale...")
109
+ try:
110
+ environment_xml_path = self.game.get_environment_xml_path(self.map_directory)
111
+ except NotImplementedError:
112
+ self.logger.warning(
113
+ "Game does not support environment XML file, fog adjustment will not be applied."
114
+ )
115
+ return
116
+
117
+ if not environment_xml_path or not os.path.isfile(environment_xml_path):
118
+ self.logger.warning(
119
+ "Environment XML file not found, fog adjustment will not be applied."
120
+ )
121
+ return
122
+
123
+ self.logger.debug("Will work with environment XML file: %s", environment_xml_path)
124
+
125
+ dem_params = self._get_dem_meter_params()
126
+ if not dem_params:
127
+ return
128
+ maximum_height, minimum_height = dem_params
129
+
130
+ tree = self.get_tree(xml_path=environment_xml_path)
131
+ root = tree.getroot()
132
+
133
+ # Find the <latitude>40.6</latitude> element in the XML file.
134
+ latitude_element = root.find("./latitude") # type: ignore
135
+ if latitude_element is not None:
136
+ map_latitude = round(self.map.coordinates[0], 1)
137
+ latitude_element.text = str(map_latitude)
138
+ self.logger.debug(
139
+ "Found latitude element and set it to: %s",
140
+ latitude_element.text,
141
+ )
142
+
143
+ # The XML file contains 4 <fog> entries in different sections of <weather> representing
144
+ # different seasons, such as <season name="spring">, <season name="summer">, etc.
145
+ # We need to find them all and adjust the parameters accordingly.
146
+ for season in root.findall(".//weather/season"): # type: ignore
147
+ # Example of the <heightFog> element:
148
+ # <heightFog>
149
+ # <groundLevelDensity min="0.05" max="0.2" />
150
+ # <maxHeight min="420" max="600" />
151
+ # </heightFog>
152
+ # We need to adjust the maxheight min and max attributes.
153
+ max_height_element = season.find("./fog/heightFog/maxHeight")
154
+ data = {
155
+ "min": str(minimum_height),
156
+ "max": str(maximum_height),
157
+ }
158
+ self.update_element(max_height_element, data) # type: ignore
159
+ self.logger.debug(
160
+ "Adjusted fog settings for season '%s': min=%s, max=%s",
161
+ season.get("name", "unknown"),
162
+ minimum_height,
163
+ maximum_height,
164
+ )
165
+
166
+ self.logger.debug("Fog adjusted and file will be saved to %s", environment_xml_path)
167
+ self.save_tree(tree, xml_path=environment_xml_path)
168
+
169
+ self.fog_parameters = {
170
+ "minimum_height": minimum_height,
171
+ "maximum_height": maximum_height,
172
+ }
173
+
174
+ def _get_dem_meter_params(self) -> tuple[int, int] | None:
175
+ """Reads the DEM file and returns the maximum and minimum height in meters.
176
+
177
+ Returns:
178
+ tuple[int, int] | None: Maximum and minimum height in meters or None if the DEM file
179
+ is not found or cannot be read.
180
+ """
181
+ self.logger.debug("Reading DEM meter parameters...")
182
+ dem_path = self.game.dem_file_path(self.map_directory)
183
+ if not dem_path or not os.path.isfile(dem_path):
184
+ self.logger.warning("DEM file not found, fog adjustment will not be applied.")
185
+ return None
186
+
187
+ dem_image = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED)
188
+ if dem_image is None:
189
+ self.logger.warning("Failed to read DEM image, fog adjustment will not be applied.")
190
+ return None
191
+ dem_maximum_pixel = dem_image.max()
192
+ dem_minimum_pixel = dem_image.min()
193
+
194
+ self.logger.debug(
195
+ "DEM read successfully. Max pixel: %d, Min pixel: %d",
196
+ dem_maximum_pixel,
197
+ dem_minimum_pixel,
198
+ )
199
+
200
+ try:
201
+ height_scale = self.get_height_scale()
202
+ except ValueError as e:
203
+ self.logger.warning("Error getting height scale from I3D file: %s", e)
204
+ return None
205
+ self.logger.debug("Height scale from I3D file: %d", height_scale)
206
+
207
+ dem_maximum_meter = int(dem_maximum_pixel / height_scale)
208
+ dem_minimum_meter = int(dem_minimum_pixel / height_scale)
209
+ self.logger.debug(
210
+ "DEM maximum height in meters: %d, minimum height in meters: %d",
211
+ dem_maximum_meter,
212
+ dem_minimum_meter,
213
+ )
214
+
215
+ return dem_maximum_meter, dem_minimum_meter
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  from random import choice, randint, uniform
8
- from typing import Generator
8
+ from typing import Any, Generator
9
9
  from xml.etree import ElementTree as ET
10
10
 
11
11
  import cv2
@@ -48,6 +48,9 @@ class I3d(XMLComponent):
48
48
  attribute. If the game does not support I3D files, the attribute is set to None."""
49
49
  self.xml_path = self.game.i3d_file_path(self.map_directory)
50
50
 
51
+ self.forest_info: dict[str, Any] = {}
52
+ self.field_info: dict[str, Any] = {}
53
+
51
54
  def process(self) -> None:
52
55
  """Updates the map I3D file and creates splines in a separate I3D file."""
53
56
  self.update_height_scale()
@@ -83,7 +86,7 @@ class I3d(XMLComponent):
83
86
  root = tree.getroot()
84
87
  path = ".//Scene/TerrainTransformGroup"
85
88
 
86
- data = {"heightScale": str(value)}
89
+ data = {Parameters.HEIGHT_SCALE: str(value)}
87
90
 
88
91
  self.get_and_update_element(root, path, data) # type: ignore
89
92
  self.save_tree(tree) # type: ignore
@@ -267,6 +270,8 @@ class I3d(XMLComponent):
267
270
 
268
271
  node_id = NODE_ID_STARTING_VALUE
269
272
  field_id = 1
273
+ added_fields = skipped_fields = 0
274
+ skipped_field_ids: list[int] = []
270
275
 
271
276
  for field in tqdm(fields, desc="Adding fields", unit="field"):
272
277
  try:
@@ -279,6 +284,7 @@ class I3d(XMLComponent):
279
284
  field_id,
280
285
  e,
281
286
  )
287
+ skipped_fields += 1
282
288
  continue
283
289
 
284
290
  field_ccs = [self.top_left_coordinates_to_center(point) for point in fitted_field]
@@ -297,6 +303,11 @@ class I3d(XMLComponent):
297
303
 
298
304
  node_id += 1
299
305
  field_id += 1
306
+ added_fields += 1
307
+
308
+ self.field_info["added_fields"] = added_fields
309
+ self.field_info["skipped_fields"] = skipped_fields
310
+ self.field_info["skipped_field_ids"] = skipped_field_ids
300
311
 
301
312
  self.save_tree(tree)
302
313
 
@@ -504,6 +515,8 @@ class I3d(XMLComponent):
504
515
 
505
516
  node_id = TREE_NODE_ID_STARTING_VALUE
506
517
 
518
+ tree_count = 0
519
+
507
520
  for forest_layer in forest_layers:
508
521
  weights_directory = self.game.weights_dir_path(self.map_directory)
509
522
  forest_image_path = forest_layer.get_preview_or_path(weights_directory)
@@ -542,12 +555,17 @@ class I3d(XMLComponent):
542
555
  )
543
556
 
544
557
  forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
545
- for x, y in self.non_empty_pixels(
546
- forest_image, step=self.map.i3d_settings.forest_density # type: ignore
547
- ):
558
+
559
+ step = self.get_step_by_limit(
560
+ forest_image, # type: ignore
561
+ self.map.i3d_settings.tree_limit,
562
+ self.map.i3d_settings.forest_density,
563
+ )
564
+
565
+ for x, y in self.non_empty_pixels(forest_image, step=step): # type: ignore
548
566
  shifted_x, shifted_y = self.randomize_coordinates(
549
567
  (x, y),
550
- self.map.i3d_settings.forest_density,
568
+ step,
551
569
  self.map.i3d_settings.trees_relative_shift,
552
570
  )
553
571
 
@@ -573,9 +591,16 @@ class I3d(XMLComponent):
573
591
  }
574
592
  trees_node.append(self.create_element("ReferenceNode", data))
575
593
 
594
+ tree_count += 1
595
+
576
596
  scene_node.append(trees_node)
577
597
  self.save_tree(tree)
578
598
 
599
+ self.forest_info["tree_count"] = tree_count
600
+ self.forest_info["tree_limit"] = self.map.i3d_settings.tree_limit
601
+ self.forest_info["initial_step"] = self.map.i3d_settings.forest_density
602
+ self.forest_info["actual_step"] = step
603
+
579
604
  self.assets.forests = self.xml_path
580
605
 
581
606
  @staticmethod
@@ -606,19 +631,61 @@ class I3d(XMLComponent):
606
631
  image: np.ndarray, step: int = 1
607
632
  ) -> Generator[tuple[int, int], None, None]:
608
633
  """Receives numpy array, which represents single-channeled image of uint8 type.
609
- Yield coordinates of non-empty pixels (pixels with value greater than 0).
634
+ Yield coordinates of non-empty pixels (pixels with value greater than 0), sampling about 1/step of them.
610
635
 
611
636
  Arguments:
612
637
  image (np.ndarray): The image to get non-empty pixels from.
613
- step (int, optional): The step to iterate through the image. Defaults to 1.
638
+ step (int, optional): The step to sample non-empty pixels. Defaults to 1.
614
639
 
615
640
  Yields:
616
641
  tuple[int, int]: The coordinates of non-empty pixels.
617
642
  """
618
- for y, row in enumerate(image[::step]):
619
- for x, value in enumerate(row[::step]):
643
+ count = 0
644
+ for y, row in enumerate(image):
645
+ for x, value in enumerate(row):
620
646
  if value > 0:
621
- yield x * step, y * step
647
+ if count % step == 0:
648
+ yield x, y
649
+ count += 1
650
+
651
+ @staticmethod
652
+ def non_empty_pixels_count(image: np.ndarray) -> int:
653
+ """Counts the number of non-empty pixels in the image.
654
+
655
+ Arguments:
656
+ image (np.ndarray): The image to count non-empty pixels in.
657
+
658
+ Returns:
659
+ int: The number of non-empty pixels in the image.
660
+ """
661
+ result = np.count_nonzero(image > 0)
662
+ return result
663
+
664
+ def get_step_by_limit(
665
+ self, image: np.ndarray, limit: int, current_step: int | None = None
666
+ ) -> int:
667
+ """Calculates the step size for iterating through the image based on the limit based
668
+ on the number of non-empty pixels in the image.
669
+
670
+ Arguments:
671
+ image (np.ndarray): The image to calculate the step size for.
672
+ limit (int): The maximum number of non-empty pixels to process.
673
+ current_step (int | None, optional): The current step size. If provided, the method
674
+ will return the maximum of the recommended step and the current step.
675
+
676
+ Returns:
677
+ int: The recommended step size for iterating through the image.
678
+ """
679
+ available_tree_count = self.non_empty_pixels_count(image)
680
+ self.forest_info["available_tree_count"] = available_tree_count
681
+ if limit <= 0 or available_tree_count <= limit:
682
+ recommended_step = 1
683
+ else:
684
+ recommended_step = int(available_tree_count / limit)
685
+
686
+ self.forest_info["step_by_limit"] = recommended_step
687
+
688
+ return recommended_step if not current_step else max(recommended_step, current_step)
622
689
 
623
690
  def get_not_resized_dem(self) -> np.ndarray | None:
624
691
  """Reads the not resized DEM image from the background component.
@@ -638,3 +705,16 @@ class I3d(XMLComponent):
638
705
  not_resized_dem = cv2.imread(background_component.not_resized_path, cv2.IMREAD_UNCHANGED)
639
706
 
640
707
  return not_resized_dem
708
+
709
+ def info_sequence(self) -> dict[str, dict[str, str | float | int]]:
710
+ """Returns information about the component.
711
+
712
+ Returns:
713
+ dict[str, dict[str, str | float | int]]: Information about the component.
714
+ """
715
+ data = {
716
+ "Forests": self.forest_info,
717
+ "Fields": self.field_info,
718
+ }
719
+
720
+ return data
@@ -40,6 +40,7 @@ class Game:
40
40
  _tree_schema: str | None = None
41
41
  _i3d_processing: bool = True
42
42
  _plants_processing: bool = True
43
+ _fog_processing: bool = True
43
44
  _dissolve: bool = True
44
45
 
45
46
  # Order matters! Some components depend on others.
@@ -189,6 +190,16 @@ class Game:
189
190
  str: The path to the farmlands xml file."""
190
191
  raise NotImplementedError
191
192
 
193
+ def get_environment_xml_path(self, map_directory: str) -> str:
194
+ """Returns the path to the environment xml file.
195
+
196
+ Arguments:
197
+ map_directory (str): The path to the map directory.
198
+
199
+ Returns:
200
+ str: The path to the environment xml file."""
201
+ raise NotImplementedError
202
+
192
203
  def i3d_file_path(self, map_directory: str) -> str:
193
204
  """Returns the path to the i3d file.
194
205
 
@@ -207,6 +218,14 @@ class Game:
207
218
  bool: True if the i3d file should be processed, False otherwise."""
208
219
  return self._i3d_processing
209
220
 
221
+ @property
222
+ def fog_processing(self) -> bool:
223
+ """Returns whether the fog should be processed.
224
+
225
+ Returns:
226
+ bool: True if the fog should be processed, False otherwise."""
227
+ return self._fog_processing
228
+
210
229
  @property
211
230
  def plants_processing(self) -> bool:
212
231
  """Returns whether the plants should be processed.
@@ -250,6 +269,7 @@ class FS22(Game):
250
269
  _map_template_path = os.path.join(working_directory, "data", "fs22-map-template.zip")
251
270
  _texture_schema = os.path.join(working_directory, "data", "fs22-texture-schema.json")
252
271
  _i3d_processing = False
272
+ _fog_processing = False
253
273
  _plants_processing = False
254
274
  _dissolve = False
255
275
 
@@ -345,3 +365,13 @@ class FS25(Game):
345
365
  Returns:
346
366
  str: The path to the farmlands xml file."""
347
367
  return os.path.join(map_directory, "map", "config", "farmlands.xml")
368
+
369
+ def get_environment_xml_path(self, map_directory: str) -> str:
370
+ """Returns the path to the environment xml file.
371
+
372
+ Arguments:
373
+ map_directory (str): The path to the map directory.
374
+
375
+ Returns:
376
+ str: The path to the environment xml file."""
377
+ return os.path.join(map_directory, "map", "config", "environment.xml")
@@ -40,6 +40,8 @@ class Parameters:
40
40
 
41
41
  WATER_ADD_WIDTH = 2
42
42
 
43
+ HEIGHT_SCALE = "heightScale"
44
+
43
45
 
44
46
  class SharedSettings(BaseModel):
45
47
  """Represents the shared settings for all components."""
@@ -201,6 +203,7 @@ class I3DSettings(SettingsModel):
201
203
  Attributes:
202
204
  add_trees (bool): add trees to the map.
203
205
  forest_density (int): density of the forest (distance between trees).
206
+ tree_limit (int): maximum number of trees to be added to the map.
204
207
  trees_relative_shift (int): relative shift of the trees.
205
208
  spline_density (int): the number of extra points that will be added between each two
206
209
  existing points.
@@ -210,6 +213,7 @@ class I3DSettings(SettingsModel):
210
213
 
211
214
  add_trees: bool = True
212
215
  forest_density: int = 10
216
+ tree_limit: int = 0
213
217
  trees_relative_shift: int = 20
214
218
 
215
219
  spline_density: int = 2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.0.9
3
+ Version: 2.1.1
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
@@ -650,6 +650,8 @@ You can also apply some advanced settings to the map generation process.<br>
650
650
 
651
651
  - Forest density - the density of the forest in meters. The lower the value, the lower the distance between the trees, which makes the forest denser. Note, that low values will lead to enormous number of trees, which may cause the Giants Editor to crash or lead to performance issues. By default, it's set to 10.
652
652
 
653
+ - Tree limit - this value will be used to adjust the forest density value. For example, if it's possible to place 100000 trees from OSM data, and the forest density is set to 10, the expected number of trees on map will be 10000. At the same time, if you set the tree limit to 5000, the forest density will be adjusted to 20, which means that the distance between the trees will be doubled. This value is useful to prevent the Giants Editor from crashing due to too many trees on the map. By default, it's set to 0, which means that it's disabled and will use the forest density value as is. Note, that it will not lead to the exact number of trees, but will adjust the forest density value to fit the tree limit so the resulting number of trees will be more or less equal to the tree limit value.
654
+
653
655
  - Trees relative shift - represents the maximum possible shift of the tree from it's original position in percents of the forest density value. The higher the value, the more the trees will be shifted from their original positions. Warning: higher values can lead to overlapping trees.
654
656
 
655
657
  - Splines density - number of points, which will be added (interpolate) between each pair of existing points. The higher the value, the denser the spline will be. It can smooth the splines, but high values can in opposite make the splines look unnatural.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maps4fs"
7
- version = "2.0.9"
7
+ version = "2.1.1"
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"}
@@ -1,94 +0,0 @@
1
- """This module contains the Config class for map settings and configuration."""
2
-
3
- from __future__ import annotations
4
-
5
- from maps4fs.generator.component.base.component_xml import XMLComponent
6
-
7
-
8
- # pylint: disable=R0903
9
- class Config(XMLComponent):
10
- """Component for map settings and configuration.
11
-
12
- Arguments:
13
- game (Game): The game instance for which the map is generated.
14
- coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
15
- map_size (int): The size of the map in pixels (it's a square).
16
- map_rotated_size (int): The size of the map in pixels after rotation.
17
- rotation (int): The rotation angle of the map.
18
- map_directory (str): The directory where the map files are stored.
19
- logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
20
- info, warning. If not provided, default logging will be used.
21
- """
22
-
23
- def preprocess(self) -> None:
24
- """Gets the path to the map XML file and saves it to the instance variable."""
25
- self.xml_path = self.game.map_xml_path(self.map_directory)
26
-
27
- def process(self) -> None:
28
- """Sets the map size in the map.xml file."""
29
- self._set_map_size()
30
-
31
- def _set_map_size(self) -> None:
32
- """Edits map.xml file to set correct map size."""
33
- tree = self.get_tree()
34
- if not tree:
35
- raise FileNotFoundError(f"Map XML file not found: {self.xml_path}")
36
-
37
- root = tree.getroot()
38
- data = {
39
- "width": str(self.scaled_size),
40
- "height": str(self.scaled_size),
41
- }
42
-
43
- for element in root.iter("map"): # type: ignore
44
- self.update_element(element, data)
45
- break
46
- self.save_tree(tree)
47
-
48
- def info_sequence(self) -> dict[str, dict[str, str | float | int]]:
49
- """Returns information about the component.
50
- Overview section is needed to create the overview file (in-game map).
51
-
52
- Returns:
53
- dict[str, dict[str, str | float | int]]: Information about the component.
54
- """
55
- # The overview file is exactly 2X bigger than the map size, does not matter
56
- # if the map is 2048x2048 or 4096x4096, the overview will be 4096x4096
57
- # and the map will be in the center of the overview.
58
- # That's why the distance is set to the map height not as a half of it.
59
- bbox = self.get_bbox(distance=self.map_size)
60
- south, west, north, east = bbox
61
- epsg3857_string = self.get_epsg3857_string(bbox=bbox)
62
- epsg3857_string_with_margin = self.get_epsg3857_string(bbox=bbox, add_margin=True)
63
-
64
- self.qgis_sequence()
65
-
66
- overview_data = {
67
- "epsg3857_string": epsg3857_string,
68
- "epsg3857_string_with_margin": epsg3857_string_with_margin,
69
- "south": south,
70
- "west": west,
71
- "north": north,
72
- "east": east,
73
- "height": self.map_size * 2,
74
- "width": self.map_size * 2,
75
- }
76
-
77
- data = {
78
- "Overview": overview_data,
79
- }
80
-
81
- return data # type: ignore
82
-
83
- def qgis_sequence(self) -> None:
84
- """Generates QGIS scripts for creating bounding box layers and rasterizing them."""
85
- bbox = self.get_bbox(distance=self.map_size)
86
- espg3857_bbox = self.get_espg3857_bbox(bbox=bbox)
87
- espg3857_bbox_with_margin = self.get_espg3857_bbox(bbox=bbox, add_margin=True)
88
-
89
- qgis_layers = [("Overview_bbox", *espg3857_bbox)]
90
- qgis_layers_with_margin = [("Overview_bbox_with_margin", *espg3857_bbox_with_margin)]
91
-
92
- layers = qgis_layers + qgis_layers_with_margin
93
-
94
- self.create_qgis_scripts(layers)
File without changes
File without changes
File without changes
File without changes
File without changes