maps4fs 2.0.9__tar.gz → 2.1.0__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.0}/PKG-INFO +1 -1
  2. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/base/component_xml.py +37 -0
  3. maps4fs-2.1.0/maps4fs/generator/component/config.py +215 -0
  4. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/i3d.py +1 -1
  5. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/game.py +30 -0
  6. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/settings.py +2 -0
  7. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs.egg-info/PKG-INFO +1 -1
  8. {maps4fs-2.0.9 → maps4fs-2.1.0}/pyproject.toml +1 -1
  9. maps4fs-2.0.9/maps4fs/generator/component/config.py +0 -94
  10. {maps4fs-2.0.9 → maps4fs-2.1.0}/LICENSE.md +0 -0
  11. {maps4fs-2.0.9 → maps4fs-2.1.0}/README.md +0 -0
  12. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/__init__.py +0 -0
  13. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/__init__.py +0 -0
  14. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/__init__.py +0 -0
  15. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/background.py +0 -0
  16. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/base/__init__.py +0 -0
  17. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/base/component.py +0 -0
  18. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/base/component_image.py +0 -0
  19. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/base/component_mesh.py +0 -0
  20. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/dem.py +0 -0
  21. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/grle.py +0 -0
  22. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/layer.py +0 -0
  23. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/satellite.py +0 -0
  24. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/component/texture.py +0 -0
  25. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/map.py +0 -0
  26. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/qgis.py +0 -0
  27. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/generator/statistics.py +0 -0
  28. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs/logger.py +0 -0
  29. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs.egg-info/SOURCES.txt +0 -0
  30. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs.egg-info/dependency_links.txt +0 -0
  31. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs.egg-info/requires.txt +0 -0
  32. {maps4fs-2.0.9 → maps4fs-2.1.0}/maps4fs.egg-info/top_level.txt +0 -0
  33. {maps4fs-2.0.9 → maps4fs-2.1.0}/setup.cfg +0 -0
  34. {maps4fs-2.0.9 → maps4fs-2.1.0}/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.0
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,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
@@ -83,7 +83,7 @@ class I3d(XMLComponent):
83
83
  root = tree.getroot()
84
84
  path = ".//Scene/TerrainTransformGroup"
85
85
 
86
- data = {"heightScale": str(value)}
86
+ data = {Parameters.HEIGHT_SCALE: str(value)}
87
87
 
88
88
  self.get_and_update_element(root, path, data) # type: ignore
89
89
  self.save_tree(tree) # type: ignore
@@ -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."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.0.9
3
+ Version: 2.1.0
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.9"
7
+ version = "2.1.0"
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
File without changes