maps4fs 1.5.0__py3-none-any.whl → 1.6.91__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 +5 -2
- maps4fs/generator/background.py +161 -113
- maps4fs/generator/component.py +34 -1
- maps4fs/generator/dem.py +33 -154
- maps4fs/generator/dtm/__init__.py +0 -0
- maps4fs/generator/dtm/dtm.py +321 -0
- maps4fs/generator/dtm/srtm.py +226 -0
- maps4fs/generator/dtm/usgs.py +351 -0
- maps4fs/generator/game.py +2 -1
- maps4fs/generator/grle.py +94 -28
- maps4fs/generator/i3d.py +17 -21
- maps4fs/generator/map.py +38 -135
- maps4fs/generator/satellite.py +92 -0
- maps4fs/generator/settings.py +187 -0
- maps4fs/generator/texture.py +65 -12
- {maps4fs-1.5.0.dist-info → maps4fs-1.6.91.dist-info}/METADATA +55 -11
- maps4fs-1.6.91.dist-info/RECORD +27 -0
- {maps4fs-1.5.0.dist-info → maps4fs-1.6.91.dist-info}/WHEEL +1 -1
- maps4fs-1.5.0.dist-info/RECORD +0 -21
- {maps4fs-1.5.0.dist-info → maps4fs-1.6.91.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.5.0.dist-info → maps4fs-1.6.91.dist-info}/top_level.txt +0 -0
maps4fs/generator/grle.py
CHANGED
@@ -10,13 +10,26 @@ import numpy as np
|
|
10
10
|
from shapely.geometry import Polygon # type: ignore
|
11
11
|
|
12
12
|
from maps4fs.generator.component import Component
|
13
|
-
from maps4fs.generator.texture import Texture
|
13
|
+
from maps4fs.generator.texture import PREVIEW_MAXIMUM_SIZE, Texture
|
14
14
|
|
15
|
-
ISLAND_SIZE_MIN = 10
|
16
|
-
ISLAND_SIZE_MAX = 200
|
17
15
|
ISLAND_DISTORTION = 0.3
|
18
|
-
|
19
|
-
|
16
|
+
|
17
|
+
|
18
|
+
def plant_to_pixel_value(plant_name: str) -> int | None:
|
19
|
+
"""Returns the pixel value representation of the plant.
|
20
|
+
If not found, returns None.
|
21
|
+
|
22
|
+
Arguments:
|
23
|
+
plant_name (str): name of the plant
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
int | None: pixel value of the plant or None if not found.
|
27
|
+
"""
|
28
|
+
plants = {
|
29
|
+
"smallDenseMix": 33,
|
30
|
+
"meadow": 131,
|
31
|
+
}
|
32
|
+
return plants.get(plant_name)
|
20
33
|
|
21
34
|
|
22
35
|
# pylint: disable=W0223
|
@@ -39,6 +52,7 @@ class GRLE(Component):
|
|
39
52
|
def preprocess(self) -> None:
|
40
53
|
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
41
54
|
attribute. If the game does not support I3D files, the attribute is set to None."""
|
55
|
+
self.preview_paths: dict[str, str] = {}
|
42
56
|
|
43
57
|
try:
|
44
58
|
grle_schema_path = self.game.grle_schema
|
@@ -89,6 +103,7 @@ class GRLE(Component):
|
|
89
103
|
else:
|
90
104
|
self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)
|
91
105
|
|
106
|
+
# pylint: disable=no-member
|
92
107
|
def previews(self) -> list[str]:
|
93
108
|
"""Returns a list of paths to the preview images (empty list).
|
94
109
|
The component does not generate any preview images so it returns an empty list.
|
@@ -96,7 +111,57 @@ class GRLE(Component):
|
|
96
111
|
Returns:
|
97
112
|
list[str]: An empty list.
|
98
113
|
"""
|
99
|
-
|
114
|
+
preview_paths = []
|
115
|
+
for preview_name, preview_path in self.preview_paths.items():
|
116
|
+
save_path = os.path.join(self.previews_directory, f"{preview_name}.png")
|
117
|
+
# Resize the preview image to the maximum size allowed for previews.
|
118
|
+
image = cv2.imread(preview_path, cv2.IMREAD_GRAYSCALE)
|
119
|
+
if image.shape[0] > PREVIEW_MAXIMUM_SIZE or image.shape[1] > PREVIEW_MAXIMUM_SIZE:
|
120
|
+
image = cv2.resize(image, (PREVIEW_MAXIMUM_SIZE, PREVIEW_MAXIMUM_SIZE))
|
121
|
+
image_normalized = np.empty_like(image)
|
122
|
+
cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX)
|
123
|
+
image_colored = cv2.applyColorMap(image_normalized, cv2.COLORMAP_JET)
|
124
|
+
cv2.imwrite(save_path, image_colored)
|
125
|
+
preview_paths.append(save_path)
|
126
|
+
|
127
|
+
with_fields_save_path = os.path.join(
|
128
|
+
self.previews_directory, f"{preview_name}_with_fields.png"
|
129
|
+
)
|
130
|
+
image_with_fields = self.overlay_fields(image_colored)
|
131
|
+
if image_with_fields is None:
|
132
|
+
continue
|
133
|
+
cv2.imwrite(with_fields_save_path, image_with_fields) # pylint: disable=no-member
|
134
|
+
preview_paths.append(with_fields_save_path)
|
135
|
+
|
136
|
+
return preview_paths
|
137
|
+
|
138
|
+
def overlay_fields(self, farmlands_np: np.ndarray) -> np.ndarray | None:
|
139
|
+
"""Overlay fields on the farmlands preview image.
|
140
|
+
|
141
|
+
Arguments:
|
142
|
+
farmlands_np (np.ndarray): The farmlands preview image.
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
np.ndarray | None: The farmlands preview image with fields overlayed on top of it.
|
146
|
+
"""
|
147
|
+
texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
|
148
|
+
if not texture_component:
|
149
|
+
self.logger.warning("Texture component not found in the map.")
|
150
|
+
return None
|
151
|
+
|
152
|
+
fields_layer = texture_component.get_layer_by_usage("field")
|
153
|
+
fields_layer_path = fields_layer.get_preview_or_path( # type: ignore
|
154
|
+
self.game.weights_dir_path(self.map_directory)
|
155
|
+
)
|
156
|
+
if not fields_layer_path or not os.path.isfile(fields_layer_path):
|
157
|
+
self.logger.warning("Fields layer not found in the texture component.")
|
158
|
+
return None
|
159
|
+
fields_np = cv2.imread(fields_layer_path)
|
160
|
+
# Resize fields_np to the same size as farmlands_np.
|
161
|
+
fields_np = cv2.resize(fields_np, (farmlands_np.shape[1], farmlands_np.shape[0]))
|
162
|
+
|
163
|
+
# use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
|
164
|
+
return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
|
100
165
|
|
101
166
|
# pylint: disable=R0801, R0914
|
102
167
|
def _add_farmlands(self) -> None:
|
@@ -114,12 +179,12 @@ class GRLE(Component):
|
|
114
179
|
self.logger.warning("Fields data not found in textures info layer.")
|
115
180
|
return
|
116
181
|
|
117
|
-
self.logger.
|
182
|
+
self.logger.debug("Found %s fields in textures info layer.", len(fields))
|
118
183
|
|
119
184
|
farmyards: list[list[tuple[int, int]]] | None = textures_info_layer.get("farmyards")
|
120
185
|
if farmyards and self.map.grle_settings.add_farmyards:
|
121
186
|
fields.extend(farmyards)
|
122
|
-
self.logger.
|
187
|
+
self.logger.debug("Found %s farmyards in textures info layer.", len(farmyards))
|
123
188
|
|
124
189
|
info_layer_farmlands_path = os.path.join(
|
125
190
|
self.game.weights_dir_path(self.map_directory), "infoLayer_farmlands.png"
|
@@ -155,7 +220,7 @@ class GRLE(Component):
|
|
155
220
|
angle=self.rotation,
|
156
221
|
)
|
157
222
|
except ValueError as e:
|
158
|
-
self.logger.
|
223
|
+
self.logger.debug(
|
159
224
|
"Farmland %s could not be fitted into the map bounds with error: %s",
|
160
225
|
farmland_id,
|
161
226
|
e,
|
@@ -180,7 +245,7 @@ class GRLE(Component):
|
|
180
245
|
try:
|
181
246
|
cv2.fillPoly(image, [field_np], farmland_id) # type: ignore
|
182
247
|
except Exception as e: # pylint: disable=W0718
|
183
|
-
self.logger.
|
248
|
+
self.logger.debug(
|
184
249
|
"Farmland %s could not be added to the InfoLayer PNG file with error: %s",
|
185
250
|
farmland_id,
|
186
251
|
e,
|
@@ -204,6 +269,8 @@ class GRLE(Component):
|
|
204
269
|
"Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
205
270
|
)
|
206
271
|
|
272
|
+
self.preview_paths["farmlands"] = info_layer_farmlands_path # type: ignore
|
273
|
+
|
207
274
|
# pylint: disable=R0915
|
208
275
|
def _add_plants(self) -> None:
|
209
276
|
"""Adds plants to the InfoLayer PNG file."""
|
@@ -270,10 +337,13 @@ class GRLE(Component):
|
|
270
337
|
grass_image[forest_image != 0] = 255
|
271
338
|
|
272
339
|
# B and G channels remain the same (zeros), while we change the R channel.
|
273
|
-
possible_R_values = [
|
340
|
+
possible_R_values = [65, 97, 129, 161, 193, 225] # pylint: disable=C0103
|
274
341
|
|
275
|
-
|
276
|
-
|
342
|
+
base_layer_pixel_value = plant_to_pixel_value(
|
343
|
+
self.map.grle_settings.base_grass # type:ignore
|
344
|
+
)
|
345
|
+
if not base_layer_pixel_value:
|
346
|
+
base_layer_pixel_value = 131
|
277
347
|
|
278
348
|
# pylint: disable=no-member
|
279
349
|
def create_island_of_plants(image: np.ndarray, count: int) -> np.ndarray:
|
@@ -290,23 +360,20 @@ class GRLE(Component):
|
|
290
360
|
# Randomly choose the value for the island.
|
291
361
|
plant_value = choice(possible_R_values)
|
292
362
|
# Randomly choose the size of the island.
|
293
|
-
island_size = randint(
|
363
|
+
island_size = randint(
|
364
|
+
self.map.grle_settings.plants_island_minimum_size, # type:ignore
|
365
|
+
self.map.grle_settings.plants_island_maximum_size, # type:ignore
|
366
|
+
)
|
294
367
|
# Randomly choose the position of the island.
|
295
|
-
# x = np.random.randint(0, image.shape[1] - island_size)
|
296
|
-
# y = np.random.randint(0, image.shape[0] - island_size)
|
297
368
|
x = randint(0, image.shape[1] - island_size)
|
298
369
|
y = randint(0, image.shape[0] - island_size)
|
299
370
|
|
300
|
-
# Randomly choose the shape of the island.
|
301
|
-
# shapes = ["circle", "ellipse", "polygon"]
|
302
|
-
# shape = choice(shapes)
|
303
|
-
|
304
371
|
try:
|
305
372
|
polygon_points = get_rounded_polygon(
|
306
|
-
num_vertices=
|
373
|
+
num_vertices=self.map.grle_settings.plants_island_vertex_count,
|
307
374
|
center=(x + island_size // 2, y + island_size // 2),
|
308
375
|
radius=island_size // 2,
|
309
|
-
rounding_radius=
|
376
|
+
rounding_radius=self.map.grle_settings.plants_island_rounding_radius,
|
310
377
|
)
|
311
378
|
if not polygon_points:
|
312
379
|
continue
|
@@ -355,16 +422,16 @@ class GRLE(Component):
|
|
355
422
|
grass_image_copy = grass_image.copy()
|
356
423
|
if forest_image is not None:
|
357
424
|
# Add the forest layer to the base image, to merge the masks.
|
358
|
-
grass_image_copy[forest_image != 0] =
|
359
|
-
|
360
|
-
grass_image_copy[grass_image != 0] =
|
425
|
+
grass_image_copy[forest_image != 0] = base_layer_pixel_value
|
426
|
+
|
427
|
+
grass_image_copy[grass_image != 0] = base_layer_pixel_value
|
361
428
|
|
362
429
|
# Add islands of plants to the base image.
|
363
|
-
island_count = self.map_size
|
430
|
+
island_count = int(self.map_size * self.map.grle_settings.plants_island_percent // 100)
|
364
431
|
self.logger.debug("Adding %s islands of plants to the base image.", island_count)
|
365
432
|
if self.map.grle_settings.random_plants:
|
366
433
|
grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
|
367
|
-
|
434
|
+
self.logger.info("Added %s islands of plants to the base image.", island_count)
|
368
435
|
|
369
436
|
# Sligtly reduce the size of the grass_image, that we'll use as mask.
|
370
437
|
kernel = np.ones((3, 3), np.uint8)
|
@@ -380,7 +447,6 @@ class GRLE(Component):
|
|
380
447
|
grass_image_copy[:, 0] = 0 # Left side
|
381
448
|
grass_image_copy[:, -1] = 0 # Right side
|
382
449
|
|
383
|
-
# Value of 33 represents the base grass plant.
|
384
450
|
# After painting it with base grass, we'll create multiple islands of different plants.
|
385
451
|
# On the final step, we'll remove all the values which in pixels
|
386
452
|
# where zerons in the original base image (so we don't paint grass where it should not be).
|
maps4fs/generator/i3d.py
CHANGED
@@ -14,14 +14,10 @@ import numpy as np
|
|
14
14
|
from maps4fs.generator.component import Component
|
15
15
|
from maps4fs.generator.texture import Texture
|
16
16
|
|
17
|
-
DEFAULT_HEIGHT_SCALE = 2000
|
18
17
|
DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
|
19
|
-
DEFAULT_MAX_LOD_DISTANCE = 10000
|
20
|
-
DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
|
21
18
|
NODE_ID_STARTING_VALUE = 2000
|
22
19
|
SPLINES_NODE_ID_STARTING_VALUE = 5000
|
23
20
|
TREE_NODE_ID_STARTING_VALUE = 10000
|
24
|
-
DEFAULT_FOREST_DENSITY = 10
|
25
21
|
|
26
22
|
|
27
23
|
# pylint: disable=R0903
|
@@ -82,19 +78,19 @@ class I3d(Component):
|
|
82
78
|
root = tree.getroot()
|
83
79
|
for map_elem in root.iter("Scene"):
|
84
80
|
for terrain_elem in map_elem.iter("TerrainTransformGroup"):
|
85
|
-
if self.map.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
81
|
+
if self.map.shared_settings.change_height_scale:
|
82
|
+
suggested_height_scale = self.map.shared_settings.height_scale_value
|
83
|
+
if suggested_height_scale is not None and suggested_height_scale > 255:
|
84
|
+
new_height_scale = int(
|
85
|
+
self.map.shared_settings.height_scale_value # type: ignore
|
86
|
+
)
|
87
|
+
terrain_elem.set("heightScale", str(new_height_scale))
|
88
|
+
self.logger.info(
|
89
|
+
"heightScale attribute set to %s in TerrainTransformGroup element.",
|
90
|
+
new_height_scale,
|
91
|
+
)
|
95
92
|
|
96
93
|
self.logger.debug("TerrainTransformGroup element updated in I3D file.")
|
97
|
-
|
98
94
|
sun_elem = map_elem.find(".//Light[@name='sun']")
|
99
95
|
|
100
96
|
if sun_elem is not None:
|
@@ -156,7 +152,7 @@ class I3d(Component):
|
|
156
152
|
self.logger.warning("Roads polylines data not found in textures info layer.")
|
157
153
|
return
|
158
154
|
|
159
|
-
self.logger.
|
155
|
+
self.logger.debug("Found %s roads polylines in textures info layer.", len(roads_polylines))
|
160
156
|
self.logger.debug("Starging to add roads polylines to the I3D file.")
|
161
157
|
|
162
158
|
root = tree.getroot()
|
@@ -198,7 +194,7 @@ class I3d(Component):
|
|
198
194
|
linestring_points=road, angle=self.rotation
|
199
195
|
)
|
200
196
|
except ValueError as e:
|
201
|
-
self.logger.
|
197
|
+
self.logger.debug(
|
202
198
|
"Road %s could not be fitted into the map bounds with error: %s",
|
203
199
|
road_id,
|
204
200
|
e,
|
@@ -249,7 +245,7 @@ class I3d(Component):
|
|
249
245
|
y = max(0, min(y, dem_y_size - 1))
|
250
246
|
|
251
247
|
z = not_resized_dem[y, x]
|
252
|
-
z
|
248
|
+
z *= self.get_z_scaling_factor() # type: ignore
|
253
249
|
|
254
250
|
cv_node = ET.Element("cv")
|
255
251
|
cv_node.set("c", f"{cx}, {z}, {cy}")
|
@@ -301,7 +297,7 @@ class I3d(Component):
|
|
301
297
|
self.logger.warning("Fields data not found in textures info layer.")
|
302
298
|
return
|
303
299
|
|
304
|
-
self.logger.
|
300
|
+
self.logger.debug("Found %s fields in textures info layer.", len(fields))
|
305
301
|
self.logger.debug("Starging to add fields to the I3D file.")
|
306
302
|
|
307
303
|
root = tree.getroot()
|
@@ -323,7 +319,7 @@ class I3d(Component):
|
|
323
319
|
polygon_points=field, angle=self.rotation
|
324
320
|
)
|
325
321
|
except ValueError as e:
|
326
|
-
self.logger.
|
322
|
+
self.logger.debug(
|
327
323
|
"Field %s could not be fitted into the map bounds with error: %s",
|
328
324
|
field_id,
|
329
325
|
e,
|
@@ -337,7 +333,7 @@ class I3d(Component):
|
|
337
333
|
try:
|
338
334
|
cx, cy = self.get_polygon_center(field_ccs)
|
339
335
|
except Exception as e: # pylint: disable=W0718
|
340
|
-
self.logger.
|
336
|
+
self.logger.debug(
|
341
337
|
"Field %s could not be fitted into the map bounds.", field_id
|
342
338
|
)
|
343
339
|
self.logger.debug("Error: %s", e)
|
maps4fs/generator/map.py
CHANGED
@@ -7,145 +7,22 @@ import os
|
|
7
7
|
import shutil
|
8
8
|
from typing import Any, Generator
|
9
9
|
|
10
|
-
from pydantic import BaseModel
|
11
|
-
|
12
10
|
from maps4fs.generator.component import Component
|
11
|
+
from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
|
13
12
|
from maps4fs.generator.game import Game
|
13
|
+
from maps4fs.generator.settings import (
|
14
|
+
BackgroundSettings,
|
15
|
+
DEMSettings,
|
16
|
+
GRLESettings,
|
17
|
+
I3DSettings,
|
18
|
+
SatelliteSettings,
|
19
|
+
SharedSettings,
|
20
|
+
SplineSettings,
|
21
|
+
TextureSettings,
|
22
|
+
)
|
14
23
|
from maps4fs.logger import Logger
|
15
24
|
|
16
25
|
|
17
|
-
class SettingsModel(BaseModel):
|
18
|
-
"""Base class for settings models. It provides methods to convert settings to and from JSON."""
|
19
|
-
|
20
|
-
@classmethod
|
21
|
-
def all_settings_to_json(cls) -> dict[str, dict[str, Any]]:
|
22
|
-
"""Get all settings of the current class and its subclasses as a dictionary.
|
23
|
-
|
24
|
-
Returns:
|
25
|
-
dict[str, dict[str, Any]]: Dictionary with settings of the current class and its
|
26
|
-
subclasses.
|
27
|
-
"""
|
28
|
-
all_settings = {}
|
29
|
-
for subclass in cls.__subclasses__():
|
30
|
-
all_settings[subclass.__name__] = subclass().model_dump()
|
31
|
-
|
32
|
-
return all_settings
|
33
|
-
|
34
|
-
@classmethod
|
35
|
-
def all_settings_from_json(cls, data: dict) -> dict[str, SettingsModel]:
|
36
|
-
"""Create settings instances from JSON data.
|
37
|
-
|
38
|
-
Arguments:
|
39
|
-
data (dict): JSON data.
|
40
|
-
|
41
|
-
Returns:
|
42
|
-
dict[str, Type[SettingsModel]]: Dictionary with settings instances.
|
43
|
-
"""
|
44
|
-
settings = {}
|
45
|
-
for subclass in cls.__subclasses__():
|
46
|
-
settings[subclass.__name__] = subclass(**data[subclass.__name__])
|
47
|
-
|
48
|
-
return settings
|
49
|
-
|
50
|
-
@classmethod
|
51
|
-
def all_settings(cls) -> list[SettingsModel]:
|
52
|
-
"""Get all settings of the current class and its subclasses.
|
53
|
-
|
54
|
-
Returns:
|
55
|
-
list[SettingsModel]: List with settings of the current class and its subclasses.
|
56
|
-
"""
|
57
|
-
settings = []
|
58
|
-
for subclass in cls.__subclasses__():
|
59
|
-
settings.append(subclass())
|
60
|
-
|
61
|
-
return settings
|
62
|
-
|
63
|
-
|
64
|
-
class DEMSettings(SettingsModel):
|
65
|
-
"""Represents the advanced settings for DEM component.
|
66
|
-
|
67
|
-
Attributes:
|
68
|
-
auto_process (bool): use the auto preset to change the multiplier.
|
69
|
-
multiplier (int): multiplier for the heightmap, every pixel will be multiplied by this
|
70
|
-
value.
|
71
|
-
blur_radius (int): radius of the blur filter.
|
72
|
-
plateau (int): plateau height, will be added to each pixel.
|
73
|
-
water_depth (int): water depth, will be subtracted from each pixel where the water
|
74
|
-
is present.
|
75
|
-
"""
|
76
|
-
|
77
|
-
auto_process: bool = True
|
78
|
-
multiplier: int = 1
|
79
|
-
blur_radius: int = 35
|
80
|
-
plateau: int = 0
|
81
|
-
water_depth: int = 0
|
82
|
-
|
83
|
-
|
84
|
-
class BackgroundSettings(SettingsModel):
|
85
|
-
"""Represents the advanced settings for background component.
|
86
|
-
|
87
|
-
Attributes:
|
88
|
-
generate_background (bool): generate obj files for the background terrain.
|
89
|
-
generate_water (bool): generate obj files for the water.
|
90
|
-
resize_factor (int): resize factor for the background terrain and water.
|
91
|
-
It will be used as 1 / resize_factor of the original size.
|
92
|
-
"""
|
93
|
-
|
94
|
-
generate_background: bool = True
|
95
|
-
generate_water: bool = True
|
96
|
-
resize_factor: int = 8
|
97
|
-
|
98
|
-
|
99
|
-
class GRLESettings(SettingsModel):
|
100
|
-
"""Represents the advanced settings for GRLE component.
|
101
|
-
|
102
|
-
Attributes:
|
103
|
-
farmland_margin (int): margin around the farmland.
|
104
|
-
random_plants (bool): generate random plants on the map or use the default one.
|
105
|
-
add_farmyards (bool): If True, regions of frarmyards will be added to the map
|
106
|
-
without corresponding fields.
|
107
|
-
"""
|
108
|
-
|
109
|
-
farmland_margin: int = 0
|
110
|
-
random_plants: bool = True
|
111
|
-
add_farmyards: bool = False
|
112
|
-
|
113
|
-
|
114
|
-
class I3DSettings(SettingsModel):
|
115
|
-
"""Represents the advanced settings for I3D component.
|
116
|
-
|
117
|
-
Attributes:
|
118
|
-
forest_density (int): density of the forest (distance between trees).
|
119
|
-
"""
|
120
|
-
|
121
|
-
forest_density: int = 10
|
122
|
-
|
123
|
-
|
124
|
-
class TextureSettings(SettingsModel):
|
125
|
-
"""Represents the advanced settings for texture component.
|
126
|
-
|
127
|
-
Attributes:
|
128
|
-
dissolve (bool): dissolve the texture into several images.
|
129
|
-
fields_padding (int): padding around the fields.
|
130
|
-
skip_drains (bool): skip drains generation.
|
131
|
-
"""
|
132
|
-
|
133
|
-
dissolve: bool = False
|
134
|
-
fields_padding: int = 0
|
135
|
-
skip_drains: bool = False
|
136
|
-
|
137
|
-
|
138
|
-
class SplineSettings(SettingsModel):
|
139
|
-
"""Represents the advanced settings for spline component.
|
140
|
-
|
141
|
-
Attributes:
|
142
|
-
spline_density (int): the number of extra points that will be added between each two
|
143
|
-
existing points.
|
144
|
-
"""
|
145
|
-
|
146
|
-
spline_density: int = 2
|
147
|
-
|
148
|
-
|
149
26
|
# pylint: disable=R0913, R0902, R0914
|
150
27
|
class Map:
|
151
28
|
"""Class used to generate map using all components.
|
@@ -161,6 +38,8 @@ class Map:
|
|
161
38
|
def __init__( # pylint: disable=R0917, R0915
|
162
39
|
self,
|
163
40
|
game: Game,
|
41
|
+
dtm_provider: DTMProvider,
|
42
|
+
dtm_provider_settings: DTMProviderSettings,
|
164
43
|
coordinates: tuple[float, float],
|
165
44
|
size: int,
|
166
45
|
rotation: int,
|
@@ -173,6 +52,7 @@ class Map:
|
|
173
52
|
i3d_settings: I3DSettings = I3DSettings(),
|
174
53
|
texture_settings: TextureSettings = TextureSettings(),
|
175
54
|
spline_settings: SplineSettings = SplineSettings(),
|
55
|
+
satellite_settings: SatelliteSettings = SatelliteSettings(),
|
176
56
|
**kwargs,
|
177
57
|
):
|
178
58
|
if not logger:
|
@@ -189,6 +69,8 @@ class Map:
|
|
189
69
|
self.rotated_size = int(size * rotation_multiplier)
|
190
70
|
|
191
71
|
self.game = game
|
72
|
+
self.dtm_provider = dtm_provider
|
73
|
+
self.dtm_provider_settings = dtm_provider_settings
|
192
74
|
self.components: list[Component] = []
|
193
75
|
self.coordinates = coordinates
|
194
76
|
self.map_directory = map_directory
|
@@ -207,6 +89,17 @@ class Map:
|
|
207
89
|
|
208
90
|
self.dem_settings = dem_settings
|
209
91
|
self.logger.info("DEM settings: %s", dem_settings)
|
92
|
+
if self.dem_settings.water_depth > 0:
|
93
|
+
# Make sure that the plateau value is >= water_depth
|
94
|
+
self.dem_settings.plateau = max(
|
95
|
+
self.dem_settings.plateau, self.dem_settings.water_depth
|
96
|
+
)
|
97
|
+
self.logger.info(
|
98
|
+
"Plateau value was set to %s to be >= water_depth value %s",
|
99
|
+
self.dem_settings.plateau,
|
100
|
+
self.dem_settings.water_depth,
|
101
|
+
)
|
102
|
+
|
210
103
|
self.background_settings = background_settings
|
211
104
|
self.logger.info("Background settings: %s", background_settings)
|
212
105
|
self.grle_settings = grle_settings
|
@@ -217,6 +110,7 @@ class Map:
|
|
217
110
|
self.logger.info("Texture settings: %s", texture_settings)
|
218
111
|
self.spline_settings = spline_settings
|
219
112
|
self.logger.info("Spline settings: %s", spline_settings)
|
113
|
+
self.satellite_settings = satellite_settings
|
220
114
|
|
221
115
|
os.makedirs(self.map_directory, exist_ok=True)
|
222
116
|
self.logger.debug("Map directory created: %s", self.map_directory)
|
@@ -228,6 +122,7 @@ class Map:
|
|
228
122
|
i3d_settings,
|
229
123
|
texture_settings,
|
230
124
|
spline_settings,
|
125
|
+
satellite_settings,
|
231
126
|
]
|
232
127
|
|
233
128
|
settings_json = {}
|
@@ -240,6 +135,8 @@ class Map:
|
|
240
135
|
with open(save_path, "w", encoding="utf-8") as file:
|
241
136
|
json.dump(settings_json, file, indent=4)
|
242
137
|
|
138
|
+
self.shared_settings = SharedSettings()
|
139
|
+
|
243
140
|
self.texture_custom_schema = kwargs.get("texture_custom_schema", None)
|
244
141
|
if self.texture_custom_schema:
|
245
142
|
save_path = os.path.join(self.map_directory, "texture_custom_schema.json")
|
@@ -249,11 +146,17 @@ class Map:
|
|
249
146
|
|
250
147
|
self.tree_custom_schema = kwargs.get("tree_custom_schema", None)
|
251
148
|
if self.tree_custom_schema:
|
149
|
+
self.logger.info("Custom tree schema contains %s trees", len(self.tree_custom_schema))
|
252
150
|
save_path = os.path.join(self.map_directory, "tree_custom_schema.json")
|
253
151
|
with open(save_path, "w", encoding="utf-8") as file:
|
254
152
|
json.dump(self.tree_custom_schema, file, indent=4)
|
255
153
|
self.logger.debug("Tree custom schema saved to %s", save_path)
|
256
154
|
|
155
|
+
self.custom_background_path = kwargs.get("custom_background_path", None)
|
156
|
+
if self.custom_background_path:
|
157
|
+
save_path = os.path.join(self.map_directory, "custom_background.png")
|
158
|
+
shutil.copyfile(self.custom_background_path, save_path)
|
159
|
+
|
257
160
|
try:
|
258
161
|
shutil.unpack_archive(game.template_path, self.map_directory)
|
259
162
|
self.logger.debug("Map template unpacked to %s", self.map_directory)
|
@@ -362,7 +265,7 @@ class Map:
|
|
362
265
|
str: Path to the archive.
|
363
266
|
"""
|
364
267
|
archive_path = shutil.make_archive(archive_path, "zip", self.map_directory)
|
365
|
-
self.logger.
|
268
|
+
self.logger.debug("Map packed to %s.zip", archive_path)
|
366
269
|
if remove_source:
|
367
270
|
try:
|
368
271
|
shutil.rmtree(self.map_directory)
|
@@ -0,0 +1,92 @@
|
|
1
|
+
"""This module contains the Satellite class for the maps4fs package to download satellite images
|
2
|
+
for the map."""
|
3
|
+
|
4
|
+
import os
|
5
|
+
|
6
|
+
import cv2
|
7
|
+
from pygmdl import save_image # type: ignore
|
8
|
+
|
9
|
+
from maps4fs.generator.background import DEFAULT_DISTANCE
|
10
|
+
from maps4fs.generator.component import Component
|
11
|
+
from maps4fs.generator.texture import PREVIEW_MAXIMUM_SIZE
|
12
|
+
|
13
|
+
|
14
|
+
# pylint: disable=W0223
|
15
|
+
class Satellite(Component):
|
16
|
+
"""Component for to download satellite images for the map.
|
17
|
+
|
18
|
+
Arguments:
|
19
|
+
game (Game): The game instance for which the map is generated.
|
20
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
21
|
+
map_size (int): The size of the map in pixels.
|
22
|
+
map_rotated_size (int): The size of the map in pixels after rotation.
|
23
|
+
rotation (int): The rotation angle of the map.
|
24
|
+
map_directory (str): The directory where the map files are stored.
|
25
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
26
|
+
info, warning. If not provided, default logging will be used.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def preprocess(self) -> None:
|
30
|
+
"""This component does not require any preprocessing."""
|
31
|
+
return
|
32
|
+
|
33
|
+
def process(self) -> None:
|
34
|
+
"""Downloads the satellite images for the map."""
|
35
|
+
self.image_paths = [] # pylint: disable=W0201
|
36
|
+
if not self.map.satellite_settings.download_images:
|
37
|
+
self.logger.debug("Satellite images download is disabled.")
|
38
|
+
return
|
39
|
+
|
40
|
+
margin = self.map.satellite_settings.satellite_margin
|
41
|
+
overview_size = (self.map_size + margin) * 2
|
42
|
+
overwiew_path = os.path.join(self.satellite_directory, "satellite_overview.png")
|
43
|
+
|
44
|
+
background_size = self.map_size + (DEFAULT_DISTANCE + margin) * 2
|
45
|
+
background_path = os.path.join(self.satellite_directory, "satellite_background.png")
|
46
|
+
|
47
|
+
sizes = [overview_size, background_size]
|
48
|
+
self.image_paths = [overwiew_path, background_path] # pylint: disable=W0201
|
49
|
+
|
50
|
+
for size, path in zip(sizes, self.image_paths):
|
51
|
+
try:
|
52
|
+
lat, lon = self.coordinates
|
53
|
+
zoom = self.map.satellite_settings.zoom_level
|
54
|
+
save_image(
|
55
|
+
lat,
|
56
|
+
lon,
|
57
|
+
size,
|
58
|
+
output_path=path,
|
59
|
+
rotation=self.rotation,
|
60
|
+
zoom=zoom,
|
61
|
+
from_center=True,
|
62
|
+
logger=self.logger,
|
63
|
+
)
|
64
|
+
except Exception as e: # pylint: disable=W0718
|
65
|
+
self.logger.error(f"Failed to download satellite image: {e}")
|
66
|
+
continue
|
67
|
+
|
68
|
+
# pylint: disable=no-member
|
69
|
+
def previews(self) -> list[str]:
|
70
|
+
"""Returns the paths to the preview images.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
list[str]: List of paths to the preview images.
|
74
|
+
"""
|
75
|
+
previews = []
|
76
|
+
for image_path in self.image_paths:
|
77
|
+
if not os.path.isfile(image_path):
|
78
|
+
self.logger.warning(f"File {image_path} does not exist.")
|
79
|
+
continue
|
80
|
+
image = cv2.imread(image_path)
|
81
|
+
if image is None:
|
82
|
+
self.logger.warning(f"Failed to read image from {image_path}")
|
83
|
+
continue
|
84
|
+
|
85
|
+
if image.shape[0] > PREVIEW_MAXIMUM_SIZE or image.shape[1] > PREVIEW_MAXIMUM_SIZE:
|
86
|
+
image = cv2.resize(image, (PREVIEW_MAXIMUM_SIZE, PREVIEW_MAXIMUM_SIZE))
|
87
|
+
|
88
|
+
preview_path = os.path.join(self.previews_directory, os.path.basename(image_path))
|
89
|
+
cv2.imwrite(preview_path, image)
|
90
|
+
previews.append(preview_path)
|
91
|
+
|
92
|
+
return previews
|