maps4fs 0.7.8__py3-none-any.whl → 0.9.93__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/generator/background.py +362 -0
- maps4fs/generator/component.py +176 -4
- maps4fs/generator/config.py +52 -2
- maps4fs/generator/dem.py +181 -35
- maps4fs/generator/game.py +27 -4
- maps4fs/generator/i3d.py +89 -0
- maps4fs/generator/map.py +51 -32
- maps4fs/generator/path_steps.py +83 -0
- maps4fs/generator/qgis.py +196 -0
- maps4fs/generator/texture.py +88 -45
- maps4fs/generator/tile.py +55 -0
- maps4fs/logger.py +27 -3
- maps4fs/toolbox/__init__.py +1 -0
- maps4fs/toolbox/dem.py +112 -0
- maps4fs-0.9.93.dist-info/METADATA +451 -0
- maps4fs-0.9.93.dist-info/RECORD +21 -0
- maps4fs-0.7.8.dist-info/METADATA +0 -218
- maps4fs-0.7.8.dist-info/RECORD +0 -14
- {maps4fs-0.7.8.dist-info → maps4fs-0.9.93.dist-info}/LICENSE.md +0 -0
- {maps4fs-0.7.8.dist-info → maps4fs-0.9.93.dist-info}/WHEEL +0 -0
- {maps4fs-0.7.8.dist-info → maps4fs-0.9.93.dist-info}/top_level.txt +0 -0
maps4fs/generator/dem.py
CHANGED
@@ -9,17 +9,19 @@ import cv2
|
|
9
9
|
import numpy as np
|
10
10
|
import rasterio # type: ignore
|
11
11
|
import requests
|
12
|
+
from pympler import asizeof # type: ignore
|
12
13
|
|
13
14
|
from maps4fs.generator.component import Component
|
14
15
|
|
15
16
|
SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
|
16
17
|
DEFAULT_MULTIPLIER = 1
|
17
|
-
DEFAULT_BLUR_RADIUS =
|
18
|
+
DEFAULT_BLUR_RADIUS = 35
|
19
|
+
DEFAULT_PLATEAU = 0
|
18
20
|
|
19
21
|
|
20
|
-
# pylint: disable=R0903
|
22
|
+
# pylint: disable=R0903, R0902
|
21
23
|
class DEM(Component):
|
22
|
-
"""Component for
|
24
|
+
"""Component for processing Digital Elevation Model data.
|
23
25
|
|
24
26
|
Args:
|
25
27
|
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
@@ -40,19 +42,34 @@ class DEM(Component):
|
|
40
42
|
|
41
43
|
self.multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
|
42
44
|
blur_radius = self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS)
|
43
|
-
if blur_radius
|
45
|
+
if blur_radius is None or blur_radius <= 0:
|
46
|
+
# We'll disable blur if the radius is 0 or negative.
|
47
|
+
blur_radius = 0
|
48
|
+
elif blur_radius % 2 == 0:
|
44
49
|
blur_radius += 1
|
45
50
|
self.blur_radius = blur_radius
|
46
51
|
self.logger.debug(
|
47
52
|
"DEM value multiplier is %s, blur radius is %s.", self.multiplier, self.blur_radius
|
48
53
|
)
|
49
54
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
+
self.auto_process = self.kwargs.get("auto_process", False)
|
56
|
+
self.plateau = self.kwargs.get("plateau", False)
|
57
|
+
|
58
|
+
@property
|
59
|
+
def dem_path(self) -> str:
|
60
|
+
"""Returns path to the DEM file.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
str: Path to the DEM file.
|
64
|
+
"""
|
65
|
+
return self._dem_path
|
66
|
+
|
67
|
+
def get_output_resolution(self) -> tuple[int, int]:
|
68
|
+
"""Get output resolution for DEM data.
|
55
69
|
|
70
|
+
Returns:
|
71
|
+
tuple[int, int]: Output resolution for DEM data.
|
72
|
+
"""
|
56
73
|
dem_height = int((self.map_height / 2) * self.game.dem_multipliyer + 1)
|
57
74
|
dem_width = int((self.map_width / 2) * self.game.dem_multipliyer + 1)
|
58
75
|
self.logger.debug(
|
@@ -61,7 +78,31 @@ class DEM(Component):
|
|
61
78
|
dem_height,
|
62
79
|
dem_width,
|
63
80
|
)
|
64
|
-
|
81
|
+
return dem_width, dem_height
|
82
|
+
|
83
|
+
def to_ground(self, data: np.ndarray) -> np.ndarray:
|
84
|
+
"""Receives the signed 16-bit integer array and converts it to the ground level.
|
85
|
+
If the min value is negative, it will become zero value and the rest of the values
|
86
|
+
will be shifted accordingly.
|
87
|
+
"""
|
88
|
+
# For examlem, min value was -50, it will become 0 and for all values we'll +50.
|
89
|
+
|
90
|
+
if data.min() < 0:
|
91
|
+
self.logger.debug("Array contains negative values, will be shifted to the ground.")
|
92
|
+
data = data + abs(data.min())
|
93
|
+
|
94
|
+
self.logger.debug(
|
95
|
+
"Array was shifted to the ground. Min: %s, max: %s.", data.min(), data.max()
|
96
|
+
)
|
97
|
+
return data
|
98
|
+
|
99
|
+
# pylint: disable=no-member
|
100
|
+
def process(self) -> None:
|
101
|
+
"""Reads SRTM file, crops it to map size, normalizes and blurs it,
|
102
|
+
saves to map directory."""
|
103
|
+
north, south, east, west = self.bbox
|
104
|
+
|
105
|
+
dem_output_resolution = self.get_output_resolution()
|
65
106
|
self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
|
66
107
|
|
67
108
|
tile_path = self._srtm_tile()
|
@@ -95,26 +136,51 @@ class DEM(Component):
|
|
95
136
|
data.max(),
|
96
137
|
)
|
97
138
|
|
139
|
+
data = self.to_ground(data)
|
140
|
+
|
98
141
|
resampled_data = cv2.resize(
|
99
142
|
data, dem_output_resolution, interpolation=cv2.INTER_LINEAR
|
100
143
|
).astype("uint16")
|
101
144
|
|
145
|
+
size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
|
146
|
+
self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
|
147
|
+
|
102
148
|
self.logger.debug(
|
103
|
-
"Maximum value in resampled data: %s, minimum value: %s.",
|
149
|
+
"Maximum value in resampled data: %s, minimum value: %s. Data type: %s.",
|
104
150
|
resampled_data.max(),
|
105
151
|
resampled_data.min(),
|
106
|
-
)
|
107
|
-
|
108
|
-
resampled_data = resampled_data * self.multiplier
|
109
|
-
self.logger.debug(
|
110
|
-
"DEM data multiplied by %s. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
111
|
-
self.multiplier,
|
112
|
-
resampled_data.shape,
|
113
152
|
resampled_data.dtype,
|
114
|
-
resampled_data.min(),
|
115
|
-
resampled_data.max(),
|
116
153
|
)
|
117
154
|
|
155
|
+
if self.auto_process:
|
156
|
+
self.logger.debug("Auto processing is enabled, will normalize DEM data.")
|
157
|
+
resampled_data = self._normalize_dem(resampled_data)
|
158
|
+
else:
|
159
|
+
self.logger.debug("Auto processing is disabled, DEM data will not be normalized.")
|
160
|
+
resampled_data = resampled_data * self.multiplier
|
161
|
+
|
162
|
+
self.logger.debug(
|
163
|
+
"DEM data was multiplied by %s. Min: %s, max: %s. Data type: %s.",
|
164
|
+
self.multiplier,
|
165
|
+
resampled_data.min(),
|
166
|
+
resampled_data.max(),
|
167
|
+
resampled_data.dtype,
|
168
|
+
)
|
169
|
+
|
170
|
+
size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
|
171
|
+
self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
|
172
|
+
|
173
|
+
# Clip values to 16-bit unsigned integer range.
|
174
|
+
resampled_data = np.clip(resampled_data, 0, 65535)
|
175
|
+
resampled_data = resampled_data.astype("uint16")
|
176
|
+
self.logger.debug(
|
177
|
+
"DEM data was multiplied by %s and clipped to 16-bit unsigned integer range. "
|
178
|
+
"Min: %s, max: %s.",
|
179
|
+
self.multiplier,
|
180
|
+
resampled_data.min(),
|
181
|
+
resampled_data.max(),
|
182
|
+
)
|
183
|
+
|
118
184
|
self.logger.debug(
|
119
185
|
"DEM data was resampled. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
120
186
|
resampled_data.shape,
|
@@ -123,11 +189,15 @@ class DEM(Component):
|
|
123
189
|
resampled_data.max(),
|
124
190
|
)
|
125
191
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
192
|
+
if self.blur_radius > 0:
|
193
|
+
resampled_data = cv2.GaussianBlur(
|
194
|
+
resampled_data, (self.blur_radius, self.blur_radius), sigmaX=40, sigmaY=40
|
195
|
+
)
|
196
|
+
self.logger.debug(
|
197
|
+
"Gaussion blur applied to DEM data with kernel size %s.",
|
198
|
+
self.blur_radius,
|
199
|
+
)
|
200
|
+
|
131
201
|
self.logger.debug(
|
132
202
|
"DEM data was blurred. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
133
203
|
resampled_data.shape,
|
@@ -136,14 +206,40 @@ class DEM(Component):
|
|
136
206
|
resampled_data.max(),
|
137
207
|
)
|
138
208
|
|
209
|
+
if self.plateau:
|
210
|
+
# Plateau is a flat area with a constant height.
|
211
|
+
# So we just add this value to each pixel of the DEM.
|
212
|
+
# And also need to ensure that there will be no values with height greater than
|
213
|
+
# it's allowed in 16-bit unsigned integer.
|
214
|
+
|
215
|
+
resampled_data += self.plateau
|
216
|
+
resampled_data = np.clip(resampled_data, 0, 65535)
|
217
|
+
|
218
|
+
self.logger.debug(
|
219
|
+
"Plateau with height %s was added to DEM data. Min: %s, max: %s.",
|
220
|
+
self.plateau,
|
221
|
+
resampled_data.min(),
|
222
|
+
resampled_data.max(),
|
223
|
+
)
|
224
|
+
|
139
225
|
cv2.imwrite(self._dem_path, resampled_data)
|
140
226
|
self.logger.debug("DEM data was saved to %s.", self._dem_path)
|
141
227
|
|
142
228
|
if self.game.additional_dem_name is not None:
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
229
|
+
self.make_copy(self.game.additional_dem_name)
|
230
|
+
|
231
|
+
def make_copy(self, dem_name: str) -> None:
|
232
|
+
"""Copies DEM data to additional DEM file.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
dem_name (str): Name of the additional DEM file.
|
236
|
+
"""
|
237
|
+
dem_directory = os.path.dirname(self._dem_path)
|
238
|
+
|
239
|
+
additional_dem_path = os.path.join(dem_directory, dem_name)
|
240
|
+
|
241
|
+
shutil.copyfile(self._dem_path, additional_dem_path)
|
242
|
+
self.logger.debug("Additional DEM data was copied to %s.", additional_dem_path)
|
147
243
|
|
148
244
|
def _tile_info(self, lat: float, lon: float) -> tuple[str, str]:
|
149
245
|
"""Returns latitude band and tile name for SRTM tile from coordinates.
|
@@ -233,14 +329,15 @@ class DEM(Component):
|
|
233
329
|
Returns:
|
234
330
|
str: Path to the preview image.
|
235
331
|
"""
|
236
|
-
rgb_dem_path = self._dem_path.replace(".png", "_grayscale.png")
|
332
|
+
# rgb_dem_path = self._dem_path.replace(".png", "_grayscale.png")
|
333
|
+
grayscale_dem_path = os.path.join(self.previews_directory, "dem_grayscale.png")
|
237
334
|
|
238
|
-
self.logger.debug("Creating grayscale preview of DEM data in %s.",
|
335
|
+
self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
|
239
336
|
|
240
337
|
dem_data = cv2.imread(self._dem_path, cv2.IMREAD_GRAYSCALE)
|
241
338
|
dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
|
242
|
-
cv2.imwrite(
|
243
|
-
return
|
339
|
+
cv2.imwrite(grayscale_dem_path, dem_data_rgb)
|
340
|
+
return grayscale_dem_path
|
244
341
|
|
245
342
|
def colored_preview(self) -> str:
|
246
343
|
"""Converts DEM image to colored RGB image and saves it to the map directory.
|
@@ -250,11 +347,12 @@ class DEM(Component):
|
|
250
347
|
list[str]: List with a single path to the DEM file
|
251
348
|
"""
|
252
349
|
|
253
|
-
colored_dem_path = self._dem_path.replace(".png", "_colored.png")
|
350
|
+
# colored_dem_path = self._dem_path.replace(".png", "_colored.png")
|
351
|
+
colored_dem_path = os.path.join(self.previews_directory, "dem_colored.png")
|
254
352
|
|
255
353
|
self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
|
256
354
|
|
257
|
-
dem_data = cv2.imread(self._dem_path, cv2.
|
355
|
+
dem_data = cv2.imread(self._dem_path, cv2.IMREAD_GRAYSCALE)
|
258
356
|
|
259
357
|
self.logger.debug(
|
260
358
|
"DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
@@ -289,3 +387,51 @@ class DEM(Component):
|
|
289
387
|
"""
|
290
388
|
self.logger.debug("Starting DEM previews generation.")
|
291
389
|
return [self.grayscale_preview(), self.colored_preview()]
|
390
|
+
|
391
|
+
def _get_scaling_factor(self, maximum_deviation: int) -> float:
|
392
|
+
"""Calculate scaling factor for DEM data normalization.
|
393
|
+
NOTE: Needs reconsideration for the implementation.
|
394
|
+
|
395
|
+
Args:
|
396
|
+
maximum_deviation (int): Maximum deviation in DEM data.
|
397
|
+
|
398
|
+
Returns:
|
399
|
+
float: Scaling factor for DEM data normalization.
|
400
|
+
"""
|
401
|
+
ESTIMATED_MAXIMUM_DEVIATION = 1000 # pylint: disable=C0103
|
402
|
+
scaling_factor = maximum_deviation / ESTIMATED_MAXIMUM_DEVIATION
|
403
|
+
return scaling_factor if scaling_factor < 1 else 1
|
404
|
+
|
405
|
+
def _normalize_dem(self, data: np.ndarray) -> np.ndarray:
|
406
|
+
"""Normalize DEM data to 16-bit unsigned integer using max height from settings.
|
407
|
+
Args:
|
408
|
+
data (np.ndarray): DEM data from SRTM file after cropping.
|
409
|
+
Returns:
|
410
|
+
np.ndarray: Normalized DEM data.
|
411
|
+
"""
|
412
|
+
self.logger.debug("Starting DEM data normalization.")
|
413
|
+
# Calculate the difference between the maximum and minimum values in the DEM data.
|
414
|
+
|
415
|
+
max_height = data.max() # 1800
|
416
|
+
min_height = data.min() # 1700
|
417
|
+
max_dev = max_height - min_height # 100
|
418
|
+
self.logger.debug(
|
419
|
+
"Maximum deviation: %s with maximum at %s and minimum at %s.",
|
420
|
+
max_dev,
|
421
|
+
max_height,
|
422
|
+
min_height,
|
423
|
+
)
|
424
|
+
|
425
|
+
scaling_factor = self._get_scaling_factor(max_dev)
|
426
|
+
adjusted_max_height = int(65535 * scaling_factor)
|
427
|
+
self.logger.debug(
|
428
|
+
f"Maximum deviation: {max_dev}. Scaling factor: {scaling_factor}. "
|
429
|
+
f"Adjusted max height: {adjusted_max_height}."
|
430
|
+
)
|
431
|
+
normalized_data = (
|
432
|
+
(data - data.min()) / (data.max() - data.min()) * adjusted_max_height
|
433
|
+
).astype("uint16")
|
434
|
+
self.logger.debug(
|
435
|
+
f"DEM data was normalized to {normalized_data.min()} - {normalized_data.max()}."
|
436
|
+
)
|
437
|
+
return normalized_data
|
maps4fs/generator/game.py
CHANGED
@@ -6,8 +6,10 @@ from __future__ import annotations
|
|
6
6
|
|
7
7
|
import os
|
8
8
|
|
9
|
+
from maps4fs.generator.background import Background
|
9
10
|
from maps4fs.generator.config import Config
|
10
11
|
from maps4fs.generator.dem import DEM
|
12
|
+
from maps4fs.generator.i3d import I3d
|
11
13
|
from maps4fs.generator.texture import Texture
|
12
14
|
|
13
15
|
working_directory = os.getcwd()
|
@@ -34,7 +36,7 @@ class Game:
|
|
34
36
|
_map_template_path: str | None = None
|
35
37
|
_texture_schema: str | None = None
|
36
38
|
|
37
|
-
components = [Config, Texture, DEM]
|
39
|
+
components = [Config, Texture, DEM, I3d, Background]
|
38
40
|
|
39
41
|
def __init__(self, map_template_path: str | None = None):
|
40
42
|
if map_template_path:
|
@@ -113,6 +115,16 @@ class Game:
|
|
113
115
|
str: The path to the weights directory."""
|
114
116
|
raise NotImplementedError
|
115
117
|
|
118
|
+
def i3d_file_path(self, map_directory: str) -> str:
|
119
|
+
"""Returns the path to the i3d file.
|
120
|
+
|
121
|
+
Arguments:
|
122
|
+
map_directory (str): The path to the map directory.
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
str: The path to the i3d file."""
|
126
|
+
raise NotImplementedError
|
127
|
+
|
116
128
|
@property
|
117
129
|
def additional_dem_name(self) -> str | None:
|
118
130
|
"""Returns the name of the additional DEM file.
|
@@ -122,6 +134,7 @@ class Game:
|
|
122
134
|
return self._additional_dem_name
|
123
135
|
|
124
136
|
|
137
|
+
# pylint: disable=W0223
|
125
138
|
class FS22(Game):
|
126
139
|
"""Class used to define the game version FS22."""
|
127
140
|
|
@@ -167,7 +180,7 @@ class FS25(Game):
|
|
167
180
|
|
168
181
|
Returns:
|
169
182
|
str: The path to the DEM file."""
|
170
|
-
return os.path.join(map_directory, "
|
183
|
+
return os.path.join(map_directory, "map", "data", "dem.png")
|
171
184
|
|
172
185
|
def map_xml_path(self, map_directory: str) -> str:
|
173
186
|
"""Returns the path to the map.xml file.
|
@@ -178,7 +191,7 @@ class FS25(Game):
|
|
178
191
|
Returns:
|
179
192
|
str: The path to the map.xml file.
|
180
193
|
"""
|
181
|
-
return os.path.join(map_directory, "
|
194
|
+
return os.path.join(map_directory, "map", "map.xml")
|
182
195
|
|
183
196
|
def weights_dir_path(self, map_directory: str) -> str:
|
184
197
|
"""Returns the path to the weights directory.
|
@@ -188,4 +201,14 @@ class FS25(Game):
|
|
188
201
|
|
189
202
|
Returns:
|
190
203
|
str: The path to the weights directory."""
|
191
|
-
return os.path.join(map_directory, "
|
204
|
+
return os.path.join(map_directory, "map", "data")
|
205
|
+
|
206
|
+
def i3d_file_path(self, map_directory: str) -> str:
|
207
|
+
"""Returns the path to the i3d file.
|
208
|
+
|
209
|
+
Arguments:
|
210
|
+
map_directory (str): The path to the map directory.
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
str: The path to the i3d file."""
|
214
|
+
return os.path.join(map_directory, "map", "map.i3d")
|
maps4fs/generator/i3d.py
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
"""This module contains the Config class for map settings and configuration."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import os
|
6
|
+
from xml.etree import ElementTree as ET
|
7
|
+
|
8
|
+
from maps4fs.generator.component import Component
|
9
|
+
|
10
|
+
DEFAULT_HEIGHT_SCALE = 2000
|
11
|
+
DEFAULT_MAX_LOD_DISTANCE = 10000
|
12
|
+
DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
|
13
|
+
|
14
|
+
|
15
|
+
# pylint: disable=R0903
|
16
|
+
class I3d(Component):
|
17
|
+
"""Component for map i3d file settings and configuration.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
21
|
+
map_height (int): The height of the map in pixels.
|
22
|
+
map_width (int): The width of the map in pixels.
|
23
|
+
map_directory (str): The directory where the map files are stored.
|
24
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
25
|
+
info, warning. If not provided, default logging will be used.
|
26
|
+
"""
|
27
|
+
|
28
|
+
_map_i3d_path: str | None = None
|
29
|
+
|
30
|
+
def preprocess(self) -> None:
|
31
|
+
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
32
|
+
attribute. If the game does not support I3D files, the attribute is set to None."""
|
33
|
+
try:
|
34
|
+
self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
|
35
|
+
self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
|
36
|
+
except NotImplementedError:
|
37
|
+
self.logger.info("I3D file processing is not implemented for this game.")
|
38
|
+
self._map_i3d_path = None
|
39
|
+
|
40
|
+
def process(self) -> None:
|
41
|
+
"""Updates the map I3D file with the default settings."""
|
42
|
+
self._update_i3d_file()
|
43
|
+
|
44
|
+
def _update_i3d_file(self) -> None:
|
45
|
+
"""Updates the map I3D file with the default settings."""
|
46
|
+
if not self._map_i3d_path:
|
47
|
+
self.logger.info("I3D is not obtained, skipping the update.")
|
48
|
+
return
|
49
|
+
if not os.path.isfile(self._map_i3d_path):
|
50
|
+
self.logger.warning("I3D file not found: %s.", self._map_i3d_path)
|
51
|
+
return
|
52
|
+
|
53
|
+
tree = ET.parse(self._map_i3d_path)
|
54
|
+
|
55
|
+
self.logger.debug("Map I3D file loaded from: %s.", self._map_i3d_path)
|
56
|
+
|
57
|
+
root = tree.getroot()
|
58
|
+
for map_elem in root.iter("Scene"):
|
59
|
+
for terrain_elem in map_elem.iter("TerrainTransformGroup"):
|
60
|
+
terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
|
61
|
+
self.logger.debug(
|
62
|
+
"heightScale attribute set to %s in TerrainTransformGroup element.",
|
63
|
+
DEFAULT_HEIGHT_SCALE,
|
64
|
+
)
|
65
|
+
terrain_elem.set("maxLODDistance", str(DEFAULT_MAX_LOD_DISTANCE))
|
66
|
+
self.logger.debug(
|
67
|
+
"maxLODDistance attribute set to %s in TerrainTransformGroup element.",
|
68
|
+
DEFAULT_MAX_LOD_DISTANCE,
|
69
|
+
)
|
70
|
+
|
71
|
+
terrain_elem.set("occMaxLODDistance", str(DEFAULT_MAX_LOD_OCCLUDER_DISTANCE))
|
72
|
+
self.logger.debug(
|
73
|
+
"occMaxLODDistance attribute set to %s in TerrainTransformGroup element.",
|
74
|
+
DEFAULT_MAX_LOD_OCCLUDER_DISTANCE,
|
75
|
+
)
|
76
|
+
|
77
|
+
self.logger.debug("TerrainTransformGroup element updated in I3D file.")
|
78
|
+
|
79
|
+
tree.write(self._map_i3d_path)
|
80
|
+
self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
|
81
|
+
|
82
|
+
def previews(self) -> list[str]:
|
83
|
+
"""Returns a list of paths to the preview images (empty list).
|
84
|
+
The component does not generate any preview images so it returns an empty list.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
list[str]: An empty list.
|
88
|
+
"""
|
89
|
+
return []
|
maps4fs/generator/map.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
"""This module contains Map class, which is used to generate map using all components."""
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
|
3
5
|
import os
|
4
6
|
import shutil
|
5
|
-
from typing import Any
|
6
|
-
|
7
|
-
from tqdm import tqdm
|
7
|
+
from typing import Any, Generator
|
8
8
|
|
9
9
|
from maps4fs.generator.component import Component
|
10
10
|
from maps4fs.generator.game import Game
|
@@ -42,11 +42,12 @@ class Map:
|
|
42
42
|
self.map_directory = map_directory
|
43
43
|
|
44
44
|
if not logger:
|
45
|
-
logger = Logger(
|
45
|
+
logger = Logger(to_stdout=True, to_file=False)
|
46
46
|
self.logger = logger
|
47
47
|
self.logger.debug("Game was set to %s", game.code)
|
48
48
|
|
49
49
|
self.kwargs = kwargs
|
50
|
+
self.logger.debug("Additional arguments: %s", kwargs)
|
50
51
|
|
51
52
|
os.makedirs(self.map_directory, exist_ok=True)
|
52
53
|
self.logger.debug("Map directory created: %s", self.map_directory)
|
@@ -57,31 +58,45 @@ class Map:
|
|
57
58
|
except Exception as e:
|
58
59
|
raise RuntimeError(f"Can not unpack map template due to error: {e}") from e
|
59
60
|
|
60
|
-
def generate(self) -> None:
|
61
|
-
"""Launch map generation using all components.
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
61
|
+
def generate(self) -> Generator[str, None, None]:
|
62
|
+
"""Launch map generation using all components. Yield component names during the process.
|
63
|
+
|
64
|
+
Yields:
|
65
|
+
Generator[str, None, None]: Component names.
|
66
|
+
"""
|
67
|
+
for game_component in self.game.components:
|
68
|
+
component = game_component(
|
69
|
+
self.game,
|
70
|
+
self.coordinates,
|
71
|
+
self.height,
|
72
|
+
self.width,
|
73
|
+
self.map_directory,
|
74
|
+
self.logger,
|
75
|
+
**self.kwargs,
|
76
|
+
)
|
77
|
+
|
78
|
+
yield component.__class__.__name__
|
79
|
+
|
80
|
+
try:
|
81
|
+
component.process()
|
82
|
+
except Exception as e: # pylint: disable=W0718
|
83
|
+
self.logger.error(
|
84
|
+
"Error processing component %s: %s",
|
85
|
+
component.__class__.__name__,
|
86
|
+
e,
|
87
|
+
)
|
88
|
+
raise e
|
89
|
+
|
90
|
+
try:
|
91
|
+
component.commit_generation_info()
|
92
|
+
except Exception as e: # pylint: disable=W0718
|
93
|
+
self.logger.error(
|
94
|
+
"Error committing generation info for component %s: %s",
|
95
|
+
component.__class__.__name__,
|
96
|
+
e,
|
72
97
|
)
|
73
|
-
|
74
|
-
|
75
|
-
except Exception as e: # pylint: disable=W0718
|
76
|
-
self.logger.error(
|
77
|
-
"Error processing component %s: %s",
|
78
|
-
component.__class__.__name__,
|
79
|
-
e,
|
80
|
-
)
|
81
|
-
raise e
|
82
|
-
self.components.append(component)
|
83
|
-
|
84
|
-
pbar.update(1)
|
98
|
+
raise e
|
99
|
+
self.components.append(component)
|
85
100
|
|
86
101
|
def previews(self) -> list[str]:
|
87
102
|
"""Get list of preview images.
|
@@ -101,15 +116,19 @@ class Map:
|
|
101
116
|
)
|
102
117
|
return previews
|
103
118
|
|
104
|
-
def pack(self,
|
119
|
+
def pack(self, archive_path: str, remove_source: bool = True) -> str:
|
105
120
|
"""Pack map directory to zip archive.
|
106
121
|
|
107
122
|
Args:
|
108
|
-
|
123
|
+
archive_path (str): Path to the archive.
|
124
|
+
remove_source (bool, optional): Remove source directory after packing.
|
109
125
|
|
110
126
|
Returns:
|
111
127
|
str: Path to the archive.
|
112
128
|
"""
|
113
|
-
archive_path = shutil.make_archive(
|
114
|
-
self.logger.info("Map packed to %s.zip",
|
129
|
+
archive_path = shutil.make_archive(archive_path, "zip", self.map_directory)
|
130
|
+
self.logger.info("Map packed to %s.zip", archive_path)
|
131
|
+
if remove_source:
|
132
|
+
shutil.rmtree(self.map_directory)
|
133
|
+
self.logger.info("Map directory removed: %s", self.map_directory)
|
115
134
|
return archive_path
|
@@ -0,0 +1,83 @@
|
|
1
|
+
"""This module contains functions and clas for generating path steps."""
|
2
|
+
|
3
|
+
from typing import NamedTuple
|
4
|
+
|
5
|
+
from geopy.distance import distance # type: ignore
|
6
|
+
|
7
|
+
DEFAULT_DISTANCE = 2048
|
8
|
+
PATH_FULL_NAME = "FULL"
|
9
|
+
|
10
|
+
|
11
|
+
class PathStep(NamedTuple):
|
12
|
+
"""Represents parameters of one step in the path.
|
13
|
+
|
14
|
+
Attributes:
|
15
|
+
code {str} -- Tile code (N, NE, E, SE, S, SW, W, NW).
|
16
|
+
angle {int} -- Angle in degrees (for example 0 for North, 90 for East).
|
17
|
+
If None, the step is a full map with a center at the same coordinates as the
|
18
|
+
map itself.
|
19
|
+
distance {int} -- Distance in meters from previous step.
|
20
|
+
If None, the step is a full map with a center at the same coordinates as the
|
21
|
+
map itself.
|
22
|
+
size {tuple[int, int]} -- Size of the tile in pixels (width, height).
|
23
|
+
"""
|
24
|
+
|
25
|
+
code: str
|
26
|
+
angle: int | None
|
27
|
+
distance: int | None
|
28
|
+
size: tuple[int, int]
|
29
|
+
|
30
|
+
def get_destination(self, origin: tuple[float, float]) -> tuple[float, float]:
|
31
|
+
"""Calculate destination coordinates based on origin and step parameters.
|
32
|
+
|
33
|
+
Arguments:
|
34
|
+
origin {tuple[float, float]} -- Origin coordinates (latitude, longitude)
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
tuple[float, float] -- Destination coordinates (latitude, longitude)
|
38
|
+
"""
|
39
|
+
destination = distance(meters=self.distance).destination(origin, self.angle)
|
40
|
+
return destination.latitude, destination.longitude
|
41
|
+
|
42
|
+
|
43
|
+
def get_steps(map_height: int, map_width: int) -> list[PathStep]:
|
44
|
+
"""Return a list of PathStep objects for each tile, which represent a step in the path.
|
45
|
+
Moving from the center of the map to North, then clockwise.
|
46
|
+
|
47
|
+
Arguments:
|
48
|
+
map_height {int} -- Height of the map in pixels
|
49
|
+
map_width {int} -- Width of the map in pixels
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
list[PathStep] -- List of PathStep objects
|
53
|
+
"""
|
54
|
+
# Move clockwise from N and calculate coordinates and sizes for each tile.
|
55
|
+
half_width = int(map_width / 2)
|
56
|
+
half_height = int(map_height / 2)
|
57
|
+
|
58
|
+
half_default_distance = int(DEFAULT_DISTANCE / 2)
|
59
|
+
|
60
|
+
return [
|
61
|
+
PathStep("N", 0, half_height + half_default_distance, (map_width, DEFAULT_DISTANCE)),
|
62
|
+
PathStep(
|
63
|
+
"NE", 90, half_width + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
|
64
|
+
),
|
65
|
+
PathStep("E", 180, half_height + half_default_distance, (DEFAULT_DISTANCE, map_height)),
|
66
|
+
PathStep(
|
67
|
+
"SE", 180, half_height + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
|
68
|
+
),
|
69
|
+
PathStep("S", 270, half_width + half_default_distance, (map_width, DEFAULT_DISTANCE)),
|
70
|
+
PathStep(
|
71
|
+
"SW", 270, half_width + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
|
72
|
+
),
|
73
|
+
PathStep("W", 0, half_height + half_default_distance, (DEFAULT_DISTANCE, map_height)),
|
74
|
+
PathStep(
|
75
|
+
"NW", 0, half_height + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
|
76
|
+
),
|
77
|
+
PathStep(
|
78
|
+
PATH_FULL_NAME,
|
79
|
+
None,
|
80
|
+
None,
|
81
|
+
(map_width + DEFAULT_DISTANCE * 2, map_height + DEFAULT_DISTANCE * 2),
|
82
|
+
),
|
83
|
+
]
|