maps4fs 2.2.6__py3-none-any.whl → 2.2.71__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maps4fs/__init__.py +1 -0
- maps4fs/generator/component/background.py +1 -1
- maps4fs/generator/component/grle.py +1 -1
- maps4fs/generator/map.py +74 -258
- maps4fs/generator/settings.py +60 -17
- maps4fs/generator/statistics.py +20 -33
- maps4fs/generator/utils.py +160 -0
- {maps4fs-2.2.6.dist-info → maps4fs-2.2.71.dist-info}/METADATA +1 -1
- {maps4fs-2.2.6.dist-info → maps4fs-2.2.71.dist-info}/RECORD +12 -11
- {maps4fs-2.2.6.dist-info → maps4fs-2.2.71.dist-info}/WHEEL +0 -0
- {maps4fs-2.2.6.dist-info → maps4fs-2.2.71.dist-info}/licenses/LICENSE.md +0 -0
- {maps4fs-2.2.6.dist-info → maps4fs-2.2.71.dist-info}/top_level.txt +0 -0
maps4fs/__init__.py
CHANGED
@@ -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,
|
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,
|
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
|
maps4fs/generator/map.py
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
70
|
+
mfsutils.check_and_fix_osm(self.custom_osm, save_directory=self.map_directory)
|
71
|
+
# endregion
|
132
72
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
164
|
-
|
165
|
-
self.
|
166
|
-
|
167
|
-
self.i3d_settings = i3d_settings
|
168
|
-
|
169
|
-
self.
|
170
|
-
|
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
|
-
|
178
|
-
|
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
|
-
|
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.
|
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
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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.
|
224
|
-
|
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 =
|
245
|
-
lonr =
|
246
|
-
return f"{
|
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
|
-
|
340
|
-
|
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
|
-
|
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
|
maps4fs/generator/settings.py
CHANGED
@@ -3,10 +3,16 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import re
|
6
|
-
from
|
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
|
-
|
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(
|
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
|
-
|
293
|
-
|
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
|
+
)
|
maps4fs/generator/statistics.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Module for sending settings to the statistics server."""
|
2
2
|
|
3
3
|
import os
|
4
|
+
import threading
|
4
5
|
from typing import Any
|
5
6
|
|
6
7
|
import requests
|
@@ -24,27 +25,31 @@ if not API_TOKEN:
|
|
24
25
|
logger.debug("API_TOKEN not set in environment")
|
25
26
|
|
26
27
|
|
27
|
-
def post(endpoint: str, data: dict[str, Any]) ->
|
28
|
-
"""Make a POST request to the statistics server.
|
28
|
+
def post(endpoint: str, data: dict[str, Any]) -> None:
|
29
|
+
"""Make a POST request to the statistics server in a separate thread.
|
29
30
|
|
30
31
|
Arguments:
|
31
32
|
endpoint (str): The endpoint to send the request to.
|
32
33
|
data (dict[str, Any]): The data to send.
|
33
|
-
|
34
|
-
Returns:
|
35
|
-
dict[str, Any]: The response from the server.
|
36
34
|
"""
|
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
35
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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()
|
48
53
|
|
49
54
|
|
50
55
|
def send_main_settings(data: dict[str, Any]) -> None:
|
@@ -65,21 +70,3 @@ def send_advanced_settings(data: dict[str, Any]) -> None:
|
|
65
70
|
"""
|
66
71
|
endpoint = f"{STATS_HOST}/receive_advanced_settings"
|
67
72
|
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")
|
@@ -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,17 +1,18 @@
|
|
1
|
-
maps4fs/__init__.py,sha256=
|
1
|
+
maps4fs/__init__.py,sha256=5ixsCA5vgcIV0OrF9EJBm91Mmc_KfMiDRM-QyifMAvo,386
|
2
2
|
maps4fs/logger.py,sha256=6sem0aFKQqtVjQ_yNu9iGcc-hqzLQUhfxco05K6nqow,763
|
3
3
|
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
4
4
|
maps4fs/generator/config.py,sha256=H63HqgpZG6M2wt2DnS-m44cDPiy_hd3qJubX8WZz5Ys,3185
|
5
5
|
maps4fs/generator/game.py,sha256=nf6iuYNA5NJc-ir_WOgkw-MdJVgetVHeEtxbWJYt3Vo,14462
|
6
|
-
maps4fs/generator/map.py,sha256=
|
6
|
+
maps4fs/generator/map.py,sha256=pg0aaIBhf3V6pzlbsyfTJABNwXMBkihgsjvZJI47JYE,12580
|
7
7
|
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
8
|
-
maps4fs/generator/settings.py,sha256=
|
9
|
-
maps4fs/generator/statistics.py,sha256=
|
8
|
+
maps4fs/generator/settings.py,sha256=LX1Y4_8cisiel2fMvoRuxMypJlmysEHenM4SZ5oqw5o,13052
|
9
|
+
maps4fs/generator/statistics.py,sha256=Dp1-NS-DWv0l0UdmhOoXeQs_N-Hs7svYUnmziSW5Z9I,2098
|
10
|
+
maps4fs/generator/utils.py,sha256=ugdQ8C22NeiZLIlldLoEKCc7ioOefz4W-8qF2eOy9qU,4834
|
10
11
|
maps4fs/generator/component/__init__.py,sha256=s01yVVVi8R2xxNvflu2D6wTd9I_g73AMM2x7vAC7GX4,490
|
11
|
-
maps4fs/generator/component/background.py,sha256
|
12
|
+
maps4fs/generator/component/background.py,sha256=rUDTPduNCD7KKt_eoVWN4XVSVgjwGSoLNPbZOzpGx7E,34532
|
12
13
|
maps4fs/generator/component/config.py,sha256=uL76h9UwyhZKZmbxz0mBmWtEPN6qYay4epTEqqtej60,8601
|
13
14
|
maps4fs/generator/component/dem.py,sha256=mtsdTIcEHmR9mW1LMcCaX4F2OCch9BM_WXHkvJby9ZY,11930
|
14
|
-
maps4fs/generator/component/grle.py,sha256=
|
15
|
+
maps4fs/generator/component/grle.py,sha256=0PC1K829wjD4y4d9qfIbnU29ebjflIPBbwIZx8FXwc8,27242
|
15
16
|
maps4fs/generator/component/i3d.py,sha256=RvpiW9skkZ6McyahC-AeIdPuSQjpXiFs1l0xOioJAu4,26638
|
16
17
|
maps4fs/generator/component/layer.py,sha256=bdy1XGOODyPqYUM3b_wEY2H9Piz-AaHsCDecl-qRHiM,6627
|
17
18
|
maps4fs/generator/component/satellite.py,sha256=9nKwL8zQ-BB6WFMx2m8zduFn6RaxSNv6Vtpge1-QMYE,5052
|
@@ -21,8 +22,8 @@ maps4fs/generator/component/base/component.py,sha256=lf0V9CLUXMg88Nm2yI3rP5taVYY
|
|
21
22
|
maps4fs/generator/component/base/component_image.py,sha256=WTGC6v1KuS5sLNCC95Z48nCspvATKKNOuhTNYzTWXr4,8315
|
22
23
|
maps4fs/generator/component/base/component_mesh.py,sha256=3hC-qDT8Vde6SmRMqs9USAkrF-gL2dDTYW71ATpxUS4,9130
|
23
24
|
maps4fs/generator/component/base/component_xml.py,sha256=MT-VhU2dEckLFxAgmxg6V3gnv11di_94Qq6atfpOLdc,5342
|
24
|
-
maps4fs-2.2.
|
25
|
-
maps4fs-2.2.
|
26
|
-
maps4fs-2.2.
|
27
|
-
maps4fs-2.2.
|
28
|
-
maps4fs-2.2.
|
25
|
+
maps4fs-2.2.71.dist-info/licenses/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
|
26
|
+
maps4fs-2.2.71.dist-info/METADATA,sha256=MAvX0-LSUrhdWtR2fYPSDQ13uXAhF3lHt4Jd72CgqfI,46315
|
27
|
+
maps4fs-2.2.71.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
28
|
+
maps4fs-2.2.71.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
29
|
+
maps4fs-2.2.71.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|