maps4fs 2.2.6__tar.gz → 2.2.71__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 (36) hide show
  1. {maps4fs-2.2.6 → maps4fs-2.2.71}/PKG-INFO +1 -1
  2. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/__init__.py +1 -0
  3. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/background.py +1 -1
  4. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/grle.py +1 -1
  5. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/map.py +74 -258
  6. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/settings.py +60 -17
  7. maps4fs-2.2.71/maps4fs/generator/statistics.py +72 -0
  8. maps4fs-2.2.71/maps4fs/generator/utils.py +160 -0
  9. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs.egg-info/PKG-INFO +1 -1
  10. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs.egg-info/SOURCES.txt +1 -0
  11. {maps4fs-2.2.6 → maps4fs-2.2.71}/pyproject.toml +1 -1
  12. {maps4fs-2.2.6 → maps4fs-2.2.71}/tests/test_generator.py +13 -6
  13. maps4fs-2.2.6/maps4fs/generator/statistics.py +0 -85
  14. {maps4fs-2.2.6 → maps4fs-2.2.71}/LICENSE.md +0 -0
  15. {maps4fs-2.2.6 → maps4fs-2.2.71}/README.md +0 -0
  16. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/__init__.py +0 -0
  17. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/__init__.py +0 -0
  18. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/base/__init__.py +0 -0
  19. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/base/component.py +0 -0
  20. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/base/component_image.py +0 -0
  21. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/base/component_mesh.py +0 -0
  22. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/base/component_xml.py +0 -0
  23. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/config.py +0 -0
  24. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/dem.py +0 -0
  25. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/i3d.py +0 -0
  26. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/layer.py +0 -0
  27. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/satellite.py +0 -0
  28. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/component/texture.py +0 -0
  29. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/config.py +0 -0
  30. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/game.py +0 -0
  31. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/generator/qgis.py +0 -0
  32. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs/logger.py +0 -0
  33. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs.egg-info/dependency_links.txt +0 -0
  34. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs.egg-info/requires.txt +0 -0
  35. {maps4fs-2.2.6 → maps4fs-2.2.71}/maps4fs.egg-info/top_level.txt +0 -0
  36. {maps4fs-2.2.6 → maps4fs-2.2.71}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.2.6
3
+ Version: 2.2.71
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
@@ -6,4 +6,5 @@ import maps4fs.generator.component as component
6
6
  import maps4fs.generator.settings as settings
7
7
  from maps4fs.generator.game import Game
8
8
  from maps4fs.generator.map import Map
9
+ from maps4fs.generator.settings import GenerationSettings, MainSettings
9
10
  from maps4fs.logger import Logger
@@ -586,7 +586,7 @@ class Background(MeshComponent, ImageComponent):
586
586
  continue
587
587
 
588
588
  # Make Polygon a little bit bigger to hide under the terrain when creating water planes.
589
- polygon = polygon.buffer(Parameters.WATER_ADD_WIDTH, resolution=4)
589
+ polygon = polygon.buffer(Parameters.WATER_ADD_WIDTH, quad_segs=4)
590
590
 
591
591
  polygons.append(polygon)
592
592
 
@@ -472,7 +472,7 @@ class GRLE(ImageComponent, XMLComponent):
472
472
  for a, r in zip(random_angles, random_radii)
473
473
  ]
474
474
  polygon = Polygon(points)
475
- buffered_polygon = polygon.buffer(rounding_radius, resolution=16)
475
+ buffered_polygon = polygon.buffer(rounding_radius, quad_segs=16)
476
476
  rounded_polygon = list(buffered_polygon.exterior.coords)
477
477
  if not rounded_polygon:
478
478
  return None
@@ -5,30 +5,16 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import shutil
8
- from datetime import datetime
9
8
  from typing import Any, Generator
10
- from xml.etree import ElementTree as ET
11
9
 
12
- import osmnx as ox
13
- from geopy.geocoders import Nominatim
14
- from osmnx._errors import InsufficientResponseError
15
10
  from pydtmdl import DTMProvider
16
11
  from pydtmdl.base.dtm import DTMProviderSettings
17
12
 
18
13
  import maps4fs.generator.config as mfscfg
14
+ import maps4fs.generator.utils as mfsutils
19
15
  from maps4fs.generator.component import Background, Component, Layer, Texture
20
- from maps4fs.generator.game import FS25, Game
21
- from maps4fs.generator.settings import (
22
- BackgroundSettings,
23
- DEMSettings,
24
- GenerationSettings,
25
- GRLESettings,
26
- I3DSettings,
27
- MainSettings,
28
- SatelliteSettings,
29
- SharedSettings,
30
- TextureSettings,
31
- )
16
+ from maps4fs.generator.game import Game
17
+ from maps4fs.generator.settings import GenerationSettings, MainSettings, SharedSettings
32
18
  from maps4fs.generator.statistics import send_advanced_settings, send_main_settings
33
19
  from maps4fs.logger import Logger
34
20
 
@@ -55,174 +41,96 @@ class Map:
55
41
  map_directory: str | None = None,
56
42
  logger: Any = None,
57
43
  custom_osm: str | None = None,
58
- dem_settings: DEMSettings = DEMSettings(),
59
- background_settings: BackgroundSettings = BackgroundSettings(),
60
- grle_settings: GRLESettings = GRLESettings(),
61
- i3d_settings: I3DSettings = I3DSettings(),
62
- texture_settings: TextureSettings = TextureSettings(),
63
- satellite_settings: SatelliteSettings = SatelliteSettings(),
44
+ generation_settings: GenerationSettings = GenerationSettings(),
64
45
  **kwargs,
65
46
  ):
66
- if not logger:
67
- logger = Logger()
68
- self.logger = logger
69
- self.size = size
70
-
71
- if rotation:
72
- rotation_multiplier = 1.5
73
- else:
74
- rotation_multiplier = 1
75
-
76
- self.rotation = rotation
77
- self.rotated_size = int(size * rotation_multiplier)
78
- self.output_size = kwargs.get("output_size", None)
79
- self.size_scale = 1.0
80
- if self.output_size:
81
- self.size_scale = self.output_size / self.size
82
47
 
48
+ # region main properties
83
49
  self.game = game
84
50
  self.dtm_provider = dtm_provider
85
51
  self.dtm_provider_settings = dtm_provider_settings
86
- self.components: list[Component] = []
87
52
  self.coordinates = coordinates
88
53
  self.map_directory = map_directory or self.suggest_map_directory(
89
54
  coordinates=coordinates, game_code=game.code # type: ignore
90
55
  )
56
+ self.rotation = rotation
57
+ self.kwargs = kwargs
58
+ # endregion
91
59
 
92
- main_settings = MainSettings.from_json(
93
- {
94
- "game": game.code, # type: ignore
95
- "latitude": coordinates[0],
96
- "longitude": coordinates[1],
97
- "country": self.get_country_by_coordinates(),
98
- "size": size,
99
- "output_size": self.output_size,
100
- "rotation": rotation,
101
- "dtm_provider": dtm_provider.name(),
102
- "custom_osm": bool(custom_osm),
103
- "is_public": kwargs.get("is_public", False),
104
- "api_request": kwargs.get("api_request", False),
105
- "date": datetime.now().strftime("%Y-%m-%d"),
106
- "time": datetime.now().strftime("%H:%M:%S"),
107
- "version": mfscfg.PACKAGE_VERSION,
108
- "completed": False,
109
- "error": None,
110
- }
111
- )
112
- main_settings_json = main_settings.to_json()
113
-
114
- try:
115
- send_main_settings(main_settings_json)
116
- except Exception as e:
117
- self.logger.error("Error sending main settings: %s", e)
118
-
119
- self.main_settings_path = os.path.join(self.map_directory, "main_settings.json")
120
- with open(self.main_settings_path, "w", encoding="utf-8") as file:
121
- json.dump(main_settings_json, file, indent=4)
122
-
123
- log_entry = ""
124
- log_entry += f"Map instance created for Game: {game.code}. "
125
- log_entry += f"Coordinates: {coordinates}. Size: {size}. Rotation: {rotation}. "
126
- if self.output_size:
127
- log_entry += f"Output size: {self.output_size}. Scaling: {self.size_scale}. "
128
- log_entry += f"DTM provider is {dtm_provider.name()}. "
60
+ # region size properties
61
+ self.size = size
62
+ rotation_multiplier = 1.5 if rotation else 1
63
+ self.rotated_size = int(size * rotation_multiplier)
64
+ self.output_size = kwargs.get("output_size", None)
65
+ self.size_scale = 1.0 if not self.output_size else self.output_size / size
66
+ # endregion
129
67
 
68
+ # region custom OSM properties
130
69
  self.custom_osm = custom_osm
131
- log_entry += f"Custom OSM file: {custom_osm}. "
70
+ mfsutils.check_and_fix_osm(self.custom_osm, save_directory=self.map_directory)
71
+ # endregion
132
72
 
133
- if self.custom_osm:
134
- osm_is_valid = check_osm_file(self.custom_osm)
135
- if not osm_is_valid:
136
- self.logger.warning(
137
- "Custom OSM file %s is not valid. Attempting to fix it.", custom_osm
138
- )
139
- fixed, fixed_errors = fix_osm_file(self.custom_osm)
140
- if not fixed:
141
- raise ValueError(
142
- f"Custom OSM file {custom_osm} is not valid and cannot be fixed."
143
- )
144
- self.logger.info(
145
- "Custom OSM file %s fixed. Fixed errors: %d", custom_osm, fixed_errors
146
- )
147
-
148
- # Make a copy of a custom osm file to the map directory, so it will be
149
- # included in the output archive.
150
- if custom_osm:
151
- copy_path = os.path.join(self.map_directory, "custom_osm.osm")
152
- shutil.copyfile(custom_osm, copy_path)
153
- self.logger.debug("Custom OSM file copied to %s", copy_path)
154
-
155
- self.dem_settings = dem_settings
156
- log_entry += f"DEM settings: {dem_settings}. "
157
- if self.dem_settings.water_depth > 0:
158
- # Make sure that the plateau value is >= water_depth
159
- self.dem_settings.plateau = max(
160
- self.dem_settings.plateau, self.dem_settings.water_depth
161
- )
73
+ # region main settings
74
+ main_settings = MainSettings.from_map(self)
75
+ main_settings_json = main_settings.to_json()
76
+ self.main_settings_path = os.path.join(self.map_directory, "main_settings.json")
77
+ self._update_main_settings(main_settings_json)
78
+ # endregion
162
79
 
163
- self.background_settings = background_settings
164
- log_entry += f"Background settings: {background_settings}. "
165
- self.grle_settings = grle_settings
166
- log_entry += f"GRLE settings: {grle_settings}. "
167
- self.i3d_settings = i3d_settings
168
- log_entry += f"I3D settings: {i3d_settings}. "
169
- self.texture_settings = texture_settings
170
- log_entry += f"Texture settings: {texture_settings}. "
171
- self.satellite_settings = satellite_settings
172
-
173
- self.logger.info(log_entry)
174
- os.makedirs(self.map_directory, exist_ok=True)
175
- self.logger.debug("Map directory created: %s", self.map_directory)
80
+ # region generation settings
81
+ self.dem_settings = generation_settings.dem_settings
82
+ self.background_settings = generation_settings.background_settings
83
+ self.grle_settings = generation_settings.grle_settings
84
+ self.i3d_settings = generation_settings.i3d_settings
85
+ self.texture_settings = generation_settings.texture_settings
86
+ self.satellite_settings = generation_settings.satellite_settings
87
+ self.process_settings()
176
88
 
177
- settings_json = GenerationSettings(
178
- dem_settings=dem_settings,
179
- background_settings=background_settings,
180
- grle_settings=grle_settings,
181
- i3d_settings=i3d_settings,
182
- texture_settings=texture_settings,
183
- satellite_settings=satellite_settings,
184
- ).to_json()
89
+ self.logger = logger if logger else Logger()
90
+ generation_settings_json = generation_settings.to_json()
185
91
 
186
92
  try:
187
- send_advanced_settings(settings_json)
93
+ send_main_settings(main_settings_json)
94
+ send_advanced_settings(generation_settings_json)
95
+ self.logger.info("Settings sent successfully.")
188
96
  except Exception as e:
189
- self.logger.error("Error sending advanced settings: %s", e)
190
-
191
- save_path = os.path.join(self.map_directory, "generation_settings.json")
192
-
193
- with open(save_path, "w", encoding="utf-8") as file:
194
- json.dump(settings_json, file, indent=4)
195
-
196
- self.shared_settings = SharedSettings()
97
+ self.logger.warning("Error sending settings: %s", e)
98
+ # endregion
197
99
 
100
+ # region JSON data saving
101
+ os.makedirs(self.map_directory, exist_ok=True)
198
102
  self.texture_custom_schema = kwargs.get("texture_custom_schema", None)
199
- if self.texture_custom_schema:
200
- save_path = os.path.join(self.map_directory, "texture_custom_schema.json")
201
- with open(save_path, "w", encoding="utf-8") as file:
202
- json.dump(self.texture_custom_schema, file, indent=4)
203
- self.logger.debug("Texture custom schema saved to %s", save_path)
204
-
205
103
  self.tree_custom_schema = kwargs.get("tree_custom_schema", None)
206
- if self.tree_custom_schema:
207
- save_path = os.path.join(self.map_directory, "tree_custom_schema.json")
208
- with open(save_path, "w", encoding="utf-8") as file:
209
- json.dump(self.tree_custom_schema, file, indent=4)
210
- self.logger.debug("Tree custom schema saved to %s", save_path)
211
104
 
212
- self.custom_background_path = kwargs.get("custom_background_path", None)
213
- if self.custom_background_path:
214
- save_path = os.path.join(self.map_directory, "custom_background.png")
215
- shutil.copyfile(self.custom_background_path, save_path)
105
+ json_data = {
106
+ "generation_settings.json": generation_settings_json,
107
+ "texture_custom_schema.json": self.texture_custom_schema,
108
+ "tree_custom_schema.json": self.tree_custom_schema,
109
+ }
110
+
111
+ for filename, data in json_data.items():
112
+ mfsutils.dump_json(filename, self.map_directory, data)
113
+ # endregion
216
114
 
115
+ # region prepare map working directory
217
116
  try:
218
117
  shutil.unpack_archive(game.template_path, self.map_directory)
219
118
  self.logger.debug("Map template unpacked to %s", self.map_directory)
220
119
  except Exception as e:
221
120
  raise RuntimeError(f"Can not unpack map template due to error: {e}") from e
121
+ # endregion
222
122
 
223
- self.logger.debug(
224
- "MFS_DATA_DIR: %s. MFS_CACHE_DIR %s", mfscfg.MFS_DATA_DIR, mfscfg.MFS_CACHE_DIR
225
- )
123
+ self.shared_settings = SharedSettings()
124
+ self.components: list[Component] = []
125
+ self.custom_background_path = kwargs.get("custom_background_path", None)
126
+
127
+ def process_settings(self) -> None:
128
+ """Checks the settings by predefined rules and updates them accordingly."""
129
+ if self.dem_settings.water_depth > 0:
130
+ # Make sure that the plateau value is >= water_depth
131
+ self.dem_settings.plateau = max(
132
+ self.dem_settings.plateau, self.dem_settings.water_depth
133
+ )
226
134
 
227
135
  @staticmethod
228
136
  def suggest_map_directory(coordinates: tuple[float, float], game_code: str) -> str:
@@ -241,30 +149,9 @@ class Map:
241
149
  str: Directory name.
242
150
  """
243
151
  lat, lon = coordinates
244
- latr = Map.coordinate_to_string(lat)
245
- lonr = Map.coordinate_to_string(lon)
246
- return f"{Map.get_timestamp()}_{game_code}_{latr}_{lonr}".lower()
247
-
248
- @staticmethod
249
- def get_timestamp() -> str:
250
- """Get current underscore-separated timestamp.
251
-
252
- Returns:
253
- str: Current timestamp.
254
- """
255
- return datetime.now().strftime("%Y%m%d_%H%M%S")
256
-
257
- @staticmethod
258
- def coordinate_to_string(coordinate: float) -> str:
259
- """Convert coordinate to string with 3 decimal places.
260
-
261
- Arguments:
262
- coordinate (float): Coordinate value.
263
-
264
- Returns:
265
- str: Coordinate as string.
266
- """
267
- return f"{coordinate:.3f}".replace(".", "_")
152
+ latr = mfsutils.coordinate_to_string(lat)
153
+ lonr = mfsutils.coordinate_to_string(lon)
154
+ return f"{mfsutils.get_timestamp()}_{game_code}_{latr}_{lonr}".lower()
268
155
 
269
156
  @property
270
157
  def texture_schema(self) -> list[dict[str, Any]] | None:
@@ -332,14 +219,19 @@ class Map:
332
219
 
333
220
  def _update_main_settings(self, data: dict[str, Any]) -> None:
334
221
  """Update main settings with provided data.
222
+ If the main settings file exists, it will be updated with the new data.
223
+ If it does not exist, a new file will be created.
335
224
 
336
225
  Arguments:
337
226
  data (dict[str, Any]): Data to update main settings.
338
227
  """
339
- with open(self.main_settings_path, "r", encoding="utf-8") as file:
340
- main_settings_json = json.load(file)
228
+ if os.path.exists(self.main_settings_path):
229
+ with open(self.main_settings_path, "r", encoding="utf-8") as file:
230
+ main_settings_json = json.load(file)
341
231
 
342
- main_settings_json.update(data)
232
+ main_settings_json.update(data)
233
+ else:
234
+ main_settings_json = data
343
235
 
344
236
  with open(self.main_settings_path, "w", encoding="utf-8") as file:
345
237
  json.dump(main_settings_json, file, indent=4)
@@ -452,79 +344,3 @@ class Map:
452
344
  except Exception as e:
453
345
  self.logger.debug("Error removing map directory %s: %s", self.map_directory, e)
454
346
  return archive_path
455
-
456
- def get_country_by_coordinates(self) -> str:
457
- """Get country name by coordinates.
458
-
459
- Returns:
460
- str: Country name.
461
- """
462
- try:
463
- geolocator = Nominatim(user_agent="maps4fs")
464
- location = geolocator.reverse(self.coordinates, language="en")
465
- if location and "country" in location.raw["address"]:
466
- return location.raw["address"]["country"]
467
- except Exception as e:
468
- self.logger.error("Error getting country name by coordinates: %s", e)
469
- return "Unknown"
470
- return "Unknown"
471
-
472
-
473
- def check_osm_file(file_path: str) -> bool:
474
- """Tries to read the OSM file using OSMnx and returns True if the file is valid,
475
- False otherwise.
476
-
477
- Arguments:
478
- file_path (str): Path to the OSM file.
479
-
480
- Returns:
481
- bool: True if the file is valid, False otherwise.
482
- """
483
- with open(FS25().texture_schema, encoding="utf-8") as f:
484
- schema = json.load(f)
485
-
486
- tags = []
487
- for element in schema:
488
- element_tags = element.get("tags")
489
- if element_tags:
490
- tags.append(element_tags)
491
-
492
- for tag in tags:
493
- try:
494
- ox.features_from_xml(file_path, tags=tag)
495
- except InsufficientResponseError:
496
- continue
497
- except Exception: # pylint: disable=W0718
498
- return False
499
- return True
500
-
501
-
502
- def fix_osm_file(input_file_path: str, output_file_path: str | None = None) -> tuple[bool, int]:
503
- """Fixes the OSM file by removing all the <relation> nodes and all the nodes with
504
- action='delete'.
505
-
506
- Arguments:
507
- input_file_path (str): Path to the input OSM file.
508
- output_file_path (str | None): Path to the output OSM file. If None, the input file
509
- will be overwritten.
510
-
511
- Returns:
512
- tuple[bool, int]: A tuple containing the result of the check_osm_file function
513
- and the number of fixed errors.
514
- """
515
- broken_entries = ["relation", ".//*[@action='delete']"]
516
- output_file_path = output_file_path or input_file_path
517
-
518
- tree = ET.parse(input_file_path)
519
- root = tree.getroot()
520
-
521
- fixed_errors = 0
522
- for entry in broken_entries:
523
- for element in root.findall(entry):
524
- root.remove(element)
525
- fixed_errors += 1
526
-
527
- tree.write(output_file_path) # type: ignore
528
- result = check_osm_file(output_file_path) # type: ignore
529
-
530
- return result, fixed_errors
@@ -3,10 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
- from typing import Any, NamedTuple
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING, Any, NamedTuple
7
8
 
8
9
  from pydantic import BaseModel, ConfigDict
9
10
 
11
+ import maps4fs.generator.config as mfscfg
12
+
13
+ if TYPE_CHECKING:
14
+ from maps4fs.generator.map import Map
15
+
10
16
 
11
17
  class Parameters:
12
18
  """Simple class to store string constants for parameters."""
@@ -79,7 +85,7 @@ class SettingsModel(BaseModel):
79
85
 
80
86
  @classmethod
81
87
  def all_settings_from_json(
82
- cls, data: dict, flattening: bool = True
88
+ cls, data: dict, flattening: bool = True, from_snake: bool = False, safe: bool = False
83
89
  ) -> dict[str, SettingsModel]:
84
90
  """Create settings instances from JSON data.
85
91
 
@@ -87,13 +93,19 @@ class SettingsModel(BaseModel):
87
93
  data (dict): JSON data.
88
94
  flattening (bool): if set to True will flattet iterables to use the first element
89
95
  of it.
96
+ from_snake (bool): if set to True will convert snake_case keys to camelCase.
90
97
 
91
98
  Returns:
92
99
  dict[str, Type[SettingsModel]]: Dictionary with settings instances.
93
100
  """
94
101
  settings = {}
95
102
  for subclass in cls.__subclasses__():
96
- subclass_data = data[subclass.__name__]
103
+ if from_snake:
104
+ subclass_key = subclass.__name__.replace("Settings", "_settings").lower()
105
+ else:
106
+ subclass_key = subclass.__name__
107
+
108
+ subclass_data = data.get(subclass_key, {}) if safe else data[subclass_key]
97
109
  if flattening:
98
110
  for key, value in subclass_data.items():
99
111
  if isinstance(value, (list, tuple)):
@@ -257,12 +269,12 @@ class SatelliteSettings(SettingsModel):
257
269
  class GenerationSettings(BaseModel):
258
270
  """Represents the settings for the map generation process."""
259
271
 
260
- dem_settings: DEMSettings
261
- background_settings: BackgroundSettings
262
- grle_settings: GRLESettings
263
- i3d_settings: I3DSettings
264
- texture_settings: TextureSettings
265
- satellite_settings: SatelliteSettings
272
+ dem_settings: DEMSettings = DEMSettings()
273
+ background_settings: BackgroundSettings = BackgroundSettings()
274
+ grle_settings: GRLESettings = GRLESettings()
275
+ i3d_settings: I3DSettings = I3DSettings()
276
+ texture_settings: TextureSettings = TextureSettings()
277
+ satellite_settings: SatelliteSettings = SatelliteSettings()
266
278
 
267
279
  def to_json(self) -> dict[str, Any]:
268
280
  """Convert the GenerationSettings instance to JSON format.
@@ -280,23 +292,23 @@ class GenerationSettings(BaseModel):
280
292
  }
281
293
 
282
294
  @classmethod
283
- def from_json(cls, data: dict[str, Any]) -> GenerationSettings:
295
+ def from_json(
296
+ cls, data: dict[str, Any], from_snake: bool = False, safe: bool = False
297
+ ) -> GenerationSettings:
284
298
  """Create a GenerationSettings instance from JSON data.
285
299
 
286
300
  Arguments:
287
301
  data (dict[str, Any]): JSON data.
302
+ from_snake (bool): if set to True will convert snake_case keys to camelCase.
303
+ safe (bool): if set to True will ignore unknown keys.
288
304
 
289
305
  Returns:
290
306
  GenerationSettings: Instance of GenerationSettings.
291
307
  """
292
- return cls(
293
- dem_settings=DEMSettings(**data["DEMSettings"]),
294
- background_settings=BackgroundSettings(**data["BackgroundSettings"]),
295
- grle_settings=GRLESettings(**data["GRLESettings"]),
296
- i3d_settings=I3DSettings(**data["I3DSettings"]),
297
- texture_settings=TextureSettings(**data["TextureSettings"]),
298
- satellite_settings=SatelliteSettings(**data["SatelliteSettings"]),
308
+ all_settings = SettingsModel.all_settings_from_json(
309
+ data, flattening=False, from_snake=from_snake, safe=safe
299
310
  )
311
+ return cls(**all_settings) # type: ignore
300
312
 
301
313
 
302
314
  class MainSettings(NamedTuple):
@@ -355,3 +367,34 @@ class MainSettings(NamedTuple):
355
367
  "completed": self.completed,
356
368
  "error": self.error,
357
369
  }
370
+
371
+ @classmethod
372
+ def from_map(cls, map: Map) -> MainSettings:
373
+ """Create a MainSettings instance from a Map instance.
374
+
375
+ Arguments:
376
+ map (Map): Instance of Map.
377
+
378
+ Returns:
379
+ MainSettings: Instance of MainSettings.
380
+ """
381
+ from maps4fs.generator.utils import get_country_by_coordinates
382
+
383
+ return cls(
384
+ game=map.game.code, # type: ignore
385
+ latitude=map.coordinates[0],
386
+ longitude=map.coordinates[1],
387
+ country=get_country_by_coordinates(map.coordinates),
388
+ size=map.size,
389
+ output_size=map.output_size,
390
+ rotation=map.rotation,
391
+ dtm_provider=map.dtm_provider.name(),
392
+ custom_osm=bool(map.custom_osm),
393
+ is_public=map.kwargs.get("is_public", False),
394
+ api_request=map.kwargs.get("api_request", False),
395
+ date=datetime.now().strftime("%Y-%m-%d"),
396
+ time=datetime.now().strftime("%H:%M:%S"),
397
+ version=mfscfg.PACKAGE_VERSION,
398
+ completed=False,
399
+ error=None,
400
+ )
@@ -0,0 +1,72 @@
1
+ """Module for sending settings to the statistics server."""
2
+
3
+ import os
4
+ import threading
5
+ from typing import Any
6
+
7
+ import requests
8
+
9
+ from maps4fs.logger import Logger
10
+
11
+ logger = Logger()
12
+
13
+ try:
14
+ from dotenv import load_dotenv
15
+
16
+ load_dotenv("local.env")
17
+ except Exception:
18
+ pass
19
+
20
+ STATS_HOST = os.getenv("STATS_HOST")
21
+ if not STATS_HOST:
22
+ logger.debug("STATS_HOST not set in environment")
23
+ API_TOKEN = os.getenv("API_TOKEN")
24
+ if not API_TOKEN:
25
+ logger.debug("API_TOKEN not set in environment")
26
+
27
+
28
+ def post(endpoint: str, data: dict[str, Any]) -> None:
29
+ """Make a POST request to the statistics server in a separate thread.
30
+
31
+ Arguments:
32
+ endpoint (str): The endpoint to send the request to.
33
+ data (dict[str, Any]): The data to send.
34
+ """
35
+
36
+ def _post_thread():
37
+ try:
38
+ if not STATS_HOST or not API_TOKEN:
39
+ logger.debug("STATS_HOST or API_TOKEN not set in environment, can't send settings.")
40
+ return
41
+
42
+ headers = {"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"}
43
+ response = requests.post(endpoint, headers=headers, json=data, timeout=10)
44
+ if response.status_code != 200:
45
+ logger.warning("Failed to send settings: %s", response.text)
46
+ return
47
+ logger.debug("Settings sent successfully")
48
+ except Exception as e:
49
+ logger.warning("Error while trying to send settings: %s", e)
50
+
51
+ thread = threading.Thread(target=_post_thread, daemon=True)
52
+ thread.start()
53
+
54
+
55
+ def send_main_settings(data: dict[str, Any]) -> None:
56
+ """Send main settings to the statistics server.
57
+
58
+ Arguments:
59
+ data (dict[str, Any]): The main settings to send.
60
+ """
61
+ endpoint = f"{STATS_HOST}/receive_main_settings"
62
+ post(endpoint, data)
63
+
64
+
65
+ def send_advanced_settings(data: dict[str, Any]) -> None:
66
+ """Send advanced settings to the statistics server.
67
+
68
+ Arguments:
69
+ data (dict[str, Any]): The advanced settings to send.
70
+ """
71
+ endpoint = f"{STATS_HOST}/receive_advanced_settings"
72
+ post(endpoint, data)
@@ -0,0 +1,160 @@
1
+ """This module contains utility functions for working with maps4fs."""
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ from datetime import datetime
7
+ from typing import Any
8
+ from xml.etree import ElementTree as ET
9
+
10
+ import osmnx as ox
11
+ from geopy.geocoders import Nominatim
12
+ from osmnx._errors import InsufficientResponseError
13
+
14
+
15
+ def check_osm_file(file_path: str) -> bool:
16
+ """Tries to read the OSM file using OSMnx and returns True if the file is valid,
17
+ False otherwise.
18
+
19
+ Arguments:
20
+ file_path (str): Path to the OSM file.
21
+
22
+ Returns:
23
+ bool: True if the file is valid, False otherwise.
24
+ """
25
+ from maps4fs.generator.game import FS25
26
+
27
+ with open(FS25().texture_schema, encoding="utf-8") as f:
28
+ schema = json.load(f)
29
+
30
+ tags = []
31
+ for element in schema:
32
+ element_tags = element.get("tags")
33
+ if element_tags:
34
+ tags.append(element_tags)
35
+
36
+ for tag in tags:
37
+ try:
38
+ ox.features_from_xml(file_path, tags=tag)
39
+ except InsufficientResponseError:
40
+ continue
41
+ except Exception: # pylint: disable=W0718
42
+ return False
43
+ return True
44
+
45
+
46
+ def fix_osm_file(input_file_path: str, output_file_path: str | None = None) -> tuple[bool, int]:
47
+ """Fixes the OSM file by removing all the <relation> nodes and all the nodes with
48
+ action='delete'.
49
+
50
+ Arguments:
51
+ input_file_path (str): Path to the input OSM file.
52
+ output_file_path (str | None): Path to the output OSM file. If None, the input file
53
+ will be overwritten.
54
+
55
+ Returns:
56
+ tuple[bool, int]: A tuple containing the result of the check_osm_file function
57
+ and the number of fixed errors.
58
+ """
59
+ broken_entries = ["relation", ".//*[@action='delete']"]
60
+ output_file_path = output_file_path or input_file_path
61
+
62
+ tree = ET.parse(input_file_path)
63
+ root = tree.getroot()
64
+
65
+ fixed_errors = 0
66
+ for entry in broken_entries:
67
+ for element in root.findall(entry):
68
+ root.remove(element)
69
+ fixed_errors += 1
70
+
71
+ tree.write(output_file_path) # type: ignore
72
+ result = check_osm_file(output_file_path) # type: ignore
73
+
74
+ return result, fixed_errors
75
+
76
+
77
+ def check_and_fix_osm(
78
+ custom_osm: str | None, save_directory: str | None = None, output_name: str = "custom_osm.osm"
79
+ ) -> None:
80
+ """Check and fix custom OSM file if necessary.
81
+
82
+ Arguments:
83
+ custom_osm (str | None): Path to the custom OSM file.
84
+ save_directory (str | None): Directory to save the fixed OSM file.
85
+ output_name (str): Name of the output OSM file.
86
+
87
+ Raises:
88
+ FileNotFoundError: If the custom OSM file does not exist.
89
+ ValueError: If the custom OSM file is not valid and cannot be fixed.
90
+ """
91
+ if not custom_osm:
92
+ return None
93
+ if not os.path.isfile(custom_osm):
94
+ raise FileNotFoundError(f"Custom OSM file {custom_osm} does not exist.")
95
+
96
+ osm_is_valid = check_osm_file(custom_osm)
97
+ if not osm_is_valid:
98
+ fixed, _ = fix_osm_file(custom_osm)
99
+ if not fixed:
100
+ raise ValueError(f"Custom OSM file {custom_osm} is not valid and cannot be fixed.")
101
+
102
+ if save_directory:
103
+ output_path = os.path.join(save_directory, output_name)
104
+ shutil.copyfile(custom_osm, output_path)
105
+
106
+ return None
107
+
108
+
109
+ def get_country_by_coordinates(coordinates: tuple[float, float]) -> str:
110
+ """Get country name by coordinates.
111
+
112
+ Returns:
113
+ str: Country name.
114
+ """
115
+ try:
116
+ geolocator = Nominatim(user_agent="maps4fs")
117
+ location = geolocator.reverse(coordinates, language="en")
118
+ if location and "country" in location.raw["address"]:
119
+ return location.raw["address"]["country"]
120
+ except Exception:
121
+ return "Unknown"
122
+ return "Unknown"
123
+
124
+
125
+ def get_timestamp() -> str:
126
+ """Get current underscore-separated timestamp.
127
+
128
+ Returns:
129
+ str: Current timestamp.
130
+ """
131
+ return datetime.now().strftime("%Y%m%d_%H%M%S")
132
+
133
+
134
+ def coordinate_to_string(coordinate: float) -> str:
135
+ """Convert coordinate to string with 3 decimal places.
136
+
137
+ Arguments:
138
+ coordinate (float): Coordinate value.
139
+
140
+ Returns:
141
+ str: Coordinate as string.
142
+ """
143
+ return f"{coordinate:.3f}".replace(".", "_")
144
+
145
+
146
+ def dump_json(filename: str, directory: str, data: dict[Any, Any] | Any | None) -> None:
147
+ """Dump data to a JSON file.
148
+
149
+ Arguments:
150
+ filename (str): Name of the JSON file.
151
+ directory (str): Directory to save the JSON file.
152
+ data (dict[Any, Any] | Any | None): Data to dump.
153
+ """
154
+ if not data:
155
+ return
156
+ if not isinstance(data, (dict, list)):
157
+ raise TypeError("Data must be a dictionary or a list.")
158
+ save_path = os.path.join(directory, filename)
159
+ with open(save_path, "w", encoding="utf-8") as file:
160
+ json.dump(data, file, indent=4)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.2.6
3
+ Version: 2.2.71
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
@@ -15,6 +15,7 @@ maps4fs/generator/map.py
15
15
  maps4fs/generator/qgis.py
16
16
  maps4fs/generator/settings.py
17
17
  maps4fs/generator/statistics.py
18
+ maps4fs/generator/utils.py
18
19
  maps4fs/generator/component/__init__.py
19
20
  maps4fs/generator/component/background.py
20
21
  maps4fs/generator/component/config.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maps4fs"
7
- version = "2.2.6"
7
+ version = "2.2.71"
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"}
@@ -6,7 +6,7 @@ from time import time
6
6
 
7
7
  import cv2
8
8
 
9
- from maps4fs import DTMProvider, Map
9
+ from maps4fs import DTMProvider, GenerationSettings, Map
10
10
  from maps4fs.generator.game import Game
11
11
  from maps4fs.generator.settings import (
12
12
  BackgroundSettings,
@@ -41,6 +41,9 @@ background_settings = BackgroundSettings(
41
41
  generate_water=True,
42
42
  remove_center=False,
43
43
  )
44
+ generation_settings = GenerationSettings(
45
+ background_settings=background_settings,
46
+ )
44
47
 
45
48
 
46
49
  def get_random_size() -> tuple[int, int]:
@@ -95,11 +98,11 @@ def test_map():
95
98
  game=game,
96
99
  dtm_provider=dtm_provider,
97
100
  dtm_provider_settings=None,
98
- background_settings=background_settings,
99
101
  coordinates=coordinates,
100
102
  size=height,
101
103
  rotation=0,
102
104
  map_directory=directory,
105
+ generation_settings=generation_settings,
103
106
  )
104
107
 
105
108
  for _ in map.generate():
@@ -158,11 +161,11 @@ def test_map_preview():
158
161
  game=game,
159
162
  dtm_provider=dtm_provider,
160
163
  dtm_provider_settings=None,
161
- background_settings=background_settings,
162
164
  coordinates=case,
163
165
  size=height,
164
166
  rotation=0,
165
167
  map_directory=directory,
168
+ generation_settings=generation_settings,
166
169
  )
167
170
  for _ in map.generate():
168
171
  pass
@@ -188,6 +191,12 @@ def test_map_pack():
188
191
 
189
192
  satellite_settings = SatelliteSettings(download_images=True, zoom_level=14)
190
193
 
194
+ pack_generation_settings = GenerationSettings(
195
+ dem_settings=dem_settings,
196
+ background_settings=background_settings,
197
+ satellite_settings=satellite_settings,
198
+ )
199
+
191
200
  directory = map_directory(game_code)
192
201
  map = Map(
193
202
  game=game,
@@ -197,9 +206,7 @@ def test_map_pack():
197
206
  size=height,
198
207
  rotation=30,
199
208
  map_directory=directory,
200
- dem_settings=dem_settings,
201
- background_settings=background_settings,
202
- satellite_settings=satellite_settings,
209
+ generation_settings=pack_generation_settings,
203
210
  )
204
211
  for _ in map.generate():
205
212
  pass
@@ -1,85 +0,0 @@
1
- """Module for sending settings to the statistics server."""
2
-
3
- import os
4
- from typing import Any
5
-
6
- import requests
7
-
8
- from maps4fs.logger import Logger
9
-
10
- logger = Logger()
11
-
12
- try:
13
- from dotenv import load_dotenv
14
-
15
- load_dotenv("local.env")
16
- except Exception:
17
- pass
18
-
19
- STATS_HOST = os.getenv("STATS_HOST")
20
- if not STATS_HOST:
21
- logger.debug("STATS_HOST not set in environment")
22
- API_TOKEN = os.getenv("API_TOKEN")
23
- if not API_TOKEN:
24
- logger.debug("API_TOKEN not set in environment")
25
-
26
-
27
- def post(endpoint: str, data: dict[str, Any]) -> dict[str, Any] | None:
28
- """Make a POST request to the statistics server.
29
-
30
- Arguments:
31
- endpoint (str): The endpoint to send the request to.
32
- data (dict[str, Any]): The data to send.
33
-
34
- Returns:
35
- dict[str, Any]: The response from the server.
36
- """
37
- if not STATS_HOST or not API_TOKEN:
38
- logger.info("STATS_HOST or API_TOKEN not set in environment, can't send settings.")
39
- return None
40
-
41
- headers = {"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"}
42
- response = requests.post(endpoint, headers=headers, json=data, timeout=10)
43
- if response.status_code != 200:
44
- logger.error("Failed to send settings: %s", response.text)
45
- return None
46
- logger.info("Settings sent successfully")
47
- return response.json()
48
-
49
-
50
- def send_main_settings(data: dict[str, Any]) -> None:
51
- """Send main settings to the statistics server.
52
-
53
- Arguments:
54
- data (dict[str, Any]): The main settings to send.
55
- """
56
- endpoint = f"{STATS_HOST}/receive_main_settings"
57
- post(endpoint, data)
58
-
59
-
60
- def send_advanced_settings(data: dict[str, Any]) -> None:
61
- """Send advanced settings to the statistics server.
62
-
63
- Arguments:
64
- data (dict[str, Any]): The advanced settings to send.
65
- """
66
- endpoint = f"{STATS_HOST}/receive_advanced_settings"
67
- post(endpoint, data)
68
-
69
-
70
- def get_main_settings(fields: list[str], limit: int | None = None) -> list[dict[str, Any]] | None:
71
- """Get main settings from the statistics server.
72
-
73
- Arguments:
74
- fields (list[str]): The fields to get.
75
- limit (int | None): The maximum number of settings to get.
76
-
77
- Returns:
78
- list[dict[str, Any]]: The settings from the server.
79
- """
80
- endpoint = f"{STATS_HOST}/get_main_settings"
81
- data = {"fields": fields, "limit": limit}
82
- result = post(endpoint, data)
83
- if not result:
84
- return None
85
- return result.get("settings")
File without changes
File without changes
File without changes
File without changes