maps4fs 1.2.3__py3-none-any.whl → 1.4.1__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.
Potentially problematic release.
This version of maps4fs might be problematic. Click here for more details.
- maps4fs/__init__.py +10 -1
- maps4fs/generator/background.py +196 -36
- maps4fs/generator/component.py +85 -23
- maps4fs/generator/config.py +1 -1
- maps4fs/generator/dem.py +11 -13
- maps4fs/generator/game.py +1 -1
- maps4fs/generator/grle.py +13 -14
- maps4fs/generator/i3d.py +169 -35
- maps4fs/generator/map.py +184 -7
- maps4fs/generator/texture.py +119 -33
- {maps4fs-1.2.3.dist-info → maps4fs-1.4.1.dist-info}/METADATA +44 -17
- maps4fs-1.4.1.dist-info/RECORD +21 -0
- maps4fs-1.2.3.dist-info/RECORD +0 -21
- {maps4fs-1.2.3.dist-info → maps4fs-1.4.1.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.2.3.dist-info → maps4fs-1.4.1.dist-info}/WHEEL +0 -0
- {maps4fs-1.2.3.dist-info → maps4fs-1.4.1.dist-info}/top_level.txt +0 -0
maps4fs/__init__.py
CHANGED
@@ -1,4 +1,13 @@
|
|
1
1
|
# pylint: disable=missing-module-docstring
|
2
2
|
from maps4fs.generator.game import Game
|
3
|
-
from maps4fs.generator.map import
|
3
|
+
from maps4fs.generator.map import (
|
4
|
+
BackgroundSettings,
|
5
|
+
DEMSettings,
|
6
|
+
GRLESettings,
|
7
|
+
I3DSettings,
|
8
|
+
Map,
|
9
|
+
SettingsModel,
|
10
|
+
SplineSettings,
|
11
|
+
TextureSettings,
|
12
|
+
)
|
4
13
|
from maps4fs.logger import Logger
|
maps4fs/generator/background.py
CHANGED
@@ -3,28 +3,26 @@ around the map."""
|
|
3
3
|
|
4
4
|
from __future__ import annotations
|
5
5
|
|
6
|
+
import json
|
6
7
|
import os
|
7
8
|
import shutil
|
9
|
+
from copy import deepcopy
|
8
10
|
|
9
11
|
import cv2
|
10
12
|
import numpy as np
|
11
13
|
import trimesh # type: ignore
|
12
14
|
|
13
15
|
from maps4fs.generator.component import Component
|
14
|
-
from maps4fs.generator.dem import
|
15
|
-
|
16
|
-
DEFAULT_MULTIPLIER,
|
17
|
-
DEFAULT_PLATEAU,
|
18
|
-
DEM,
|
19
|
-
)
|
16
|
+
from maps4fs.generator.dem import DEM
|
17
|
+
from maps4fs.generator.texture import Texture
|
20
18
|
|
21
19
|
DEFAULT_DISTANCE = 2048
|
22
|
-
RESIZE_FACTOR = 1 / 8
|
23
20
|
FULL_NAME = "FULL"
|
24
21
|
FULL_PREVIEW_NAME = "PREVIEW"
|
25
22
|
ELEMENTS = [FULL_NAME, FULL_PREVIEW_NAME]
|
26
23
|
|
27
24
|
|
25
|
+
# pylint: disable=R0902
|
28
26
|
class Background(Component):
|
29
27
|
"""Component for creating 3D obj files based on DEM data around the map.
|
30
28
|
|
@@ -42,8 +40,8 @@ class Background(Component):
|
|
42
40
|
# pylint: disable=R0801
|
43
41
|
def preprocess(self) -> None:
|
44
42
|
"""Registers the DEMs for the background terrain."""
|
45
|
-
self.light_version = self.kwargs.get("light_version", False)
|
46
43
|
self.stl_preview_path: str | None = None
|
44
|
+
self.water_resources_path: str | None = None
|
47
45
|
|
48
46
|
if self.rotation:
|
49
47
|
self.logger.debug("Rotation is enabled: %s.", self.rotation)
|
@@ -51,34 +49,41 @@ class Background(Component):
|
|
51
49
|
else:
|
52
50
|
output_size_multiplier = 1
|
53
51
|
|
54
|
-
background_size = self.map_size + DEFAULT_DISTANCE * 2
|
55
|
-
rotated_size = int(background_size * output_size_multiplier)
|
52
|
+
self.background_size = self.map_size + DEFAULT_DISTANCE * 2
|
53
|
+
self.rotated_size = int(self.background_size * output_size_multiplier)
|
56
54
|
|
57
55
|
self.background_directory = os.path.join(self.map_directory, "background")
|
56
|
+
self.water_directory = os.path.join(self.map_directory, "water")
|
58
57
|
os.makedirs(self.background_directory, exist_ok=True)
|
58
|
+
os.makedirs(self.water_directory, exist_ok=True)
|
59
|
+
|
60
|
+
autoprocesses = [self.map.dem_settings.auto_process, False]
|
61
|
+
self.output_paths = [
|
62
|
+
os.path.join(self.background_directory, f"{name}.png") for name in ELEMENTS
|
63
|
+
]
|
64
|
+
self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
|
65
|
+
self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
|
59
66
|
|
60
|
-
autoprocesses = [self.kwargs.get("auto_process", False), False]
|
61
67
|
dems = []
|
62
68
|
|
63
|
-
for name, autoprocess in zip(ELEMENTS, autoprocesses):
|
69
|
+
for name, autoprocess, output_path in zip(ELEMENTS, autoprocesses, self.output_paths):
|
64
70
|
dem = DEM(
|
65
71
|
self.game,
|
66
72
|
self.map,
|
67
73
|
self.coordinates,
|
68
|
-
background_size,
|
69
|
-
rotated_size,
|
74
|
+
self.background_size,
|
75
|
+
self.rotated_size,
|
70
76
|
self.rotation,
|
71
77
|
self.map_directory,
|
72
78
|
self.logger,
|
73
|
-
auto_process=autoprocess,
|
74
|
-
blur_radius=self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS),
|
75
|
-
multiplier=self.kwargs.get("multiplier", DEFAULT_MULTIPLIER),
|
76
|
-
plateau=self.kwargs.get("plateau", DEFAULT_PLATEAU),
|
77
79
|
)
|
78
80
|
dem.preprocess()
|
79
81
|
dem.is_preview = self.is_preview(name) # type: ignore
|
80
|
-
dem.
|
81
|
-
|
82
|
+
if dem.is_preview: # type: ignore
|
83
|
+
dem.multiplier = 1
|
84
|
+
dem.auto_process = autoprocess
|
85
|
+
dem.set_output_resolution((self.rotated_size, self.rotated_size))
|
86
|
+
dem.set_dem_path(output_path)
|
82
87
|
dems.append(dem)
|
83
88
|
|
84
89
|
self.dems = dems
|
@@ -98,17 +103,27 @@ class Background(Component):
|
|
98
103
|
"""Launches the component processing. Iterates over all tiles and processes them
|
99
104
|
as a result the DEM files will be saved, then based on them the obj files will be
|
100
105
|
generated."""
|
106
|
+
self.create_background_textures()
|
107
|
+
|
101
108
|
for dem in self.dems:
|
102
109
|
dem.process()
|
110
|
+
if not dem.is_preview: # type: ignore
|
111
|
+
shutil.copyfile(dem.dem_path, self.not_substracted_path)
|
112
|
+
self.cutout(dem.dem_path, save_path=self.not_resized_path)
|
113
|
+
|
114
|
+
if self.map.dem_settings.water_depth:
|
115
|
+
self.subtraction()
|
116
|
+
|
117
|
+
for dem in self.dems:
|
103
118
|
if not dem.is_preview: # type: ignore
|
104
119
|
cutted_dem_path = self.cutout(dem.dem_path)
|
105
120
|
if self.game.additional_dem_name is not None:
|
106
121
|
self.make_copy(cutted_dem_path, self.game.additional_dem_name)
|
107
122
|
|
108
|
-
if
|
123
|
+
if self.map.background_settings.generate_background:
|
109
124
|
self.generate_obj_files()
|
110
|
-
|
111
|
-
self.
|
125
|
+
if self.map.background_settings.generate_water:
|
126
|
+
self.generate_water_resources_obj()
|
112
127
|
|
113
128
|
def make_copy(self, dem_path: str, dem_name: str) -> None:
|
114
129
|
"""Copies DEM data to additional DEM file.
|
@@ -122,7 +137,7 @@ class Background(Component):
|
|
122
137
|
additional_dem_path = os.path.join(dem_directory, dem_name)
|
123
138
|
|
124
139
|
shutil.copyfile(dem_path, additional_dem_path)
|
125
|
-
self.logger.
|
140
|
+
self.logger.debug("Additional DEM data was copied to %s.", additional_dem_path)
|
126
141
|
|
127
142
|
def info_sequence(self) -> dict[str, str | float | int]:
|
128
143
|
"""Returns a dictionary with information about the background terrain.
|
@@ -184,11 +199,12 @@ class Background(Component):
|
|
184
199
|
self.plane_from_np(dem_data, save_path, is_preview=dem.is_preview) # type: ignore
|
185
200
|
|
186
201
|
# pylint: disable=too-many-locals
|
187
|
-
def cutout(self, dem_path: str) -> str:
|
202
|
+
def cutout(self, dem_path: str, save_path: str | None = None) -> str:
|
188
203
|
"""Cuts out the center of the DEM (the actual map) and saves it as a separate file.
|
189
204
|
|
190
205
|
Arguments:
|
191
206
|
dem_path (str): The path to the DEM file.
|
207
|
+
save_path (str, optional): The path where the cutout DEM file will be saved.
|
192
208
|
|
193
209
|
Returns:
|
194
210
|
str -- The path to the cutout DEM file.
|
@@ -203,6 +219,11 @@ class Background(Component):
|
|
203
219
|
y2 = center[1] + half_size
|
204
220
|
dem_data = dem_data[x1:x2, y1:y2]
|
205
221
|
|
222
|
+
if save_path:
|
223
|
+
cv2.imwrite(save_path, dem_data) # pylint: disable=no-member
|
224
|
+
self.logger.debug("Not resized DEM saved: %s", save_path)
|
225
|
+
return save_path
|
226
|
+
|
206
227
|
output_size = self.map_size + 1
|
207
228
|
|
208
229
|
main_dem_path = self.game.dem_file_path(self.map_directory)
|
@@ -223,19 +244,27 @@ class Background(Component):
|
|
223
244
|
return main_dem_path
|
224
245
|
|
225
246
|
# pylint: disable=too-many-locals
|
226
|
-
def plane_from_np(
|
247
|
+
def plane_from_np(
|
248
|
+
self,
|
249
|
+
dem_data: np.ndarray,
|
250
|
+
save_path: str,
|
251
|
+
is_preview: bool = False,
|
252
|
+
include_zeros: bool = True,
|
253
|
+
) -> None:
|
227
254
|
"""Generates a 3D obj file based on DEM data.
|
228
255
|
|
229
256
|
Arguments:
|
230
257
|
dem_data (np.ndarray) -- The DEM data as a numpy array.
|
231
258
|
save_path (str) -- The path where the obj file will be saved.
|
232
259
|
is_preview (bool, optional) -- If True, the preview mesh will be generated.
|
260
|
+
include_zeros (bool, optional) -- If True, the mesh will include the zero height values.
|
233
261
|
"""
|
262
|
+
resize_factor = 1 / self.map.background_settings.resize_factor
|
234
263
|
dem_data = cv2.resize( # pylint: disable=no-member
|
235
|
-
dem_data, (0, 0), fx=
|
264
|
+
dem_data, (0, 0), fx=resize_factor, fy=resize_factor
|
236
265
|
)
|
237
266
|
self.logger.debug(
|
238
|
-
"DEM data resized to shape: %s with factor: %s", dem_data.shape,
|
267
|
+
"DEM data resized to shape: %s with factor: %s", dem_data.shape, resize_factor
|
239
268
|
)
|
240
269
|
|
241
270
|
# Invert the height values.
|
@@ -247,6 +276,9 @@ class Background(Component):
|
|
247
276
|
x, y = np.meshgrid(x, y)
|
248
277
|
z = dem_data
|
249
278
|
|
279
|
+
ground = z.max()
|
280
|
+
self.logger.debug("Ground level: %s", ground)
|
281
|
+
|
250
282
|
self.logger.debug(
|
251
283
|
"Starting to generate a mesh for with shape: %s x %s. This may take a while...",
|
252
284
|
cols,
|
@@ -256,6 +288,8 @@ class Background(Component):
|
|
256
288
|
vertices = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
|
257
289
|
faces = []
|
258
290
|
|
291
|
+
skipped = 0
|
292
|
+
|
259
293
|
for i in range(rows - 1):
|
260
294
|
for j in range(cols - 1):
|
261
295
|
top_left = i * cols + j
|
@@ -263,9 +297,15 @@ class Background(Component):
|
|
263
297
|
bottom_left = top_left + cols
|
264
298
|
bottom_right = bottom_left + 1
|
265
299
|
|
300
|
+
if ground in [z[i, j], z[i, j + 1], z[i + 1, j], z[i + 1, j + 1]]:
|
301
|
+
skipped += 1
|
302
|
+
continue
|
303
|
+
|
266
304
|
faces.append([top_left, bottom_left, bottom_right])
|
267
305
|
faces.append([top_left, bottom_right, top_right])
|
268
306
|
|
307
|
+
self.logger.debug("Skipped faces: %s", skipped)
|
308
|
+
|
269
309
|
faces = np.array(faces) # type: ignore
|
270
310
|
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
|
271
311
|
|
@@ -283,13 +323,14 @@ class Background(Component):
|
|
283
323
|
mesh.apply_scale([0.5, 0.5, 0.5])
|
284
324
|
self.mesh_to_stl(mesh)
|
285
325
|
else:
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
326
|
+
if not include_zeros:
|
327
|
+
multiplier = self.map.dem_settings.multiplier
|
328
|
+
if multiplier != 1:
|
329
|
+
z_scaling_factor = 1 / multiplier
|
330
|
+
else:
|
331
|
+
z_scaling_factor = 1 / 2**5
|
332
|
+
self.logger.debug("Z scaling factor: %s", z_scaling_factor)
|
333
|
+
mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
|
293
334
|
|
294
335
|
mesh.export(save_path)
|
295
336
|
self.logger.debug("Obj file saved: %s", save_path)
|
@@ -305,7 +346,7 @@ class Background(Component):
|
|
305
346
|
preview_path = os.path.join(self.previews_directory, "background_dem.stl")
|
306
347
|
mesh.export(preview_path)
|
307
348
|
|
308
|
-
self.logger.
|
349
|
+
self.logger.debug("STL file saved: %s", preview_path)
|
309
350
|
|
310
351
|
self.stl_preview_path = preview_path # pylint: disable=attribute-defined-outside-init
|
311
352
|
|
@@ -413,3 +454,122 @@ class Background(Component):
|
|
413
454
|
|
414
455
|
cv2.imwrite(colored_dem_path, dem_data_colored)
|
415
456
|
return colored_dem_path
|
457
|
+
|
458
|
+
def create_background_textures(self) -> None:
|
459
|
+
"""Creates background textures for the map."""
|
460
|
+
if not os.path.isfile(self.game.texture_schema):
|
461
|
+
self.logger.warning("Texture schema file not found: %s", self.game.texture_schema)
|
462
|
+
return
|
463
|
+
|
464
|
+
with open(self.game.texture_schema, "r", encoding="utf-8") as f:
|
465
|
+
layers_schema = json.load(f)
|
466
|
+
|
467
|
+
background_layers = []
|
468
|
+
for layer in layers_schema:
|
469
|
+
if layer.get("background") is True:
|
470
|
+
layer_copy = deepcopy(layer)
|
471
|
+
layer_copy["count"] = 1
|
472
|
+
layer_copy["name"] = f"{layer['name']}_background"
|
473
|
+
background_layers.append(layer_copy)
|
474
|
+
|
475
|
+
if not background_layers:
|
476
|
+
return
|
477
|
+
|
478
|
+
self.background_texture = Texture( # pylint: disable=W0201
|
479
|
+
self.game,
|
480
|
+
self.map,
|
481
|
+
self.coordinates,
|
482
|
+
self.background_size,
|
483
|
+
self.rotated_size,
|
484
|
+
rotation=self.rotation,
|
485
|
+
map_directory=self.map_directory,
|
486
|
+
logger=self.logger,
|
487
|
+
texture_custom_schema=background_layers, # type: ignore
|
488
|
+
)
|
489
|
+
|
490
|
+
self.background_texture.preprocess()
|
491
|
+
self.background_texture.process()
|
492
|
+
|
493
|
+
processed_layers = self.background_texture.get_background_layers()
|
494
|
+
weights_directory = self.game.weights_dir_path(self.map_directory)
|
495
|
+
background_paths = [layer.path(weights_directory) for layer in processed_layers]
|
496
|
+
self.logger.debug("Found %s background textures.", len(background_paths))
|
497
|
+
|
498
|
+
if not background_paths:
|
499
|
+
self.logger.warning("No background textures found.")
|
500
|
+
return
|
501
|
+
|
502
|
+
# Merge all images into one.
|
503
|
+
background_image = np.zeros((self.background_size, self.background_size), dtype=np.uint8)
|
504
|
+
for path in background_paths:
|
505
|
+
layer = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
|
506
|
+
background_image = cv2.add(background_image, layer) # type: ignore
|
507
|
+
|
508
|
+
background_save_path = os.path.join(self.water_directory, "water_resources.png")
|
509
|
+
cv2.imwrite(background_save_path, background_image)
|
510
|
+
self.logger.debug("Background texture saved: %s", background_save_path)
|
511
|
+
self.water_resources_path = background_save_path # pylint: disable=W0201
|
512
|
+
|
513
|
+
def subtraction(self) -> None:
|
514
|
+
"""Subtracts the water depth from the DEM data where the water resources are located."""
|
515
|
+
if not self.water_resources_path:
|
516
|
+
self.logger.warning("Water resources texture not found.")
|
517
|
+
return
|
518
|
+
|
519
|
+
# Single channeled 8 bit image, where the water have values of 255, and the rest 0.
|
520
|
+
water_resources_image = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
|
521
|
+
mask = water_resources_image == 255
|
522
|
+
|
523
|
+
# Make mask a little bit smaller (1 pixel).
|
524
|
+
mask = cv2.erode(mask.astype(np.uint8), np.ones((3, 3), np.uint8), iterations=1).astype(
|
525
|
+
bool
|
526
|
+
)
|
527
|
+
|
528
|
+
for output_path in self.output_paths:
|
529
|
+
if FULL_PREVIEW_NAME in output_path:
|
530
|
+
continue
|
531
|
+
dem_image = cv2.imread(output_path, cv2.IMREAD_UNCHANGED)
|
532
|
+
|
533
|
+
# Create a mask where water_resources_image is 255 (or not 0)
|
534
|
+
# Subtract water_depth from dem_image where mask is True
|
535
|
+
dem_image[mask] = dem_image[mask] - self.map.dem_settings.water_depth
|
536
|
+
|
537
|
+
# Save the modified dem_image back to the output path
|
538
|
+
cv2.imwrite(output_path, dem_image)
|
539
|
+
self.logger.debug("Water depth subtracted from DEM data: %s", output_path)
|
540
|
+
|
541
|
+
def generate_water_resources_obj(self) -> None:
|
542
|
+
"""Generates 3D obj files based on water resources data."""
|
543
|
+
if not self.water_resources_path:
|
544
|
+
self.logger.warning("Water resources texture not found.")
|
545
|
+
return
|
546
|
+
|
547
|
+
# Single channeled 8 bit image, where the water have values of 255, and the rest 0.
|
548
|
+
plane_water = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
|
549
|
+
dilated_plane_water = cv2.dilate(
|
550
|
+
plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
|
551
|
+
).astype(np.uint8)
|
552
|
+
plane_save_path = os.path.join(self.water_directory, "plane_water.obj")
|
553
|
+
self.plane_from_np(
|
554
|
+
dilated_plane_water, plane_save_path, is_preview=False, include_zeros=False
|
555
|
+
)
|
556
|
+
|
557
|
+
# Single channeled 16 bit DEM image of terrain.
|
558
|
+
background_dem = cv2.imread(self.not_substracted_path, cv2.IMREAD_UNCHANGED)
|
559
|
+
|
560
|
+
# Remove all the values from the background dem where the plane_water is 0.
|
561
|
+
background_dem[plane_water == 0] = 0
|
562
|
+
|
563
|
+
# Dilate the background dem to make the water more smooth.
|
564
|
+
elevated_water = cv2.dilate(background_dem, np.ones((3, 3), np.uint16), iterations=10)
|
565
|
+
|
566
|
+
# Use the background dem as a mask to prevent the original values from being overwritten.
|
567
|
+
mask = background_dem > 0
|
568
|
+
|
569
|
+
# Combine the dilated background dem with non-dilated background dem.
|
570
|
+
elevated_water = np.where(mask, background_dem, elevated_water)
|
571
|
+
elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
|
572
|
+
|
573
|
+
self.plane_from_np(
|
574
|
+
elevated_water, elevated_save_path, is_preview=False, include_zeros=False
|
575
|
+
)
|
maps4fs/generator/component.py
CHANGED
@@ -7,11 +7,11 @@ import os
|
|
7
7
|
from copy import deepcopy
|
8
8
|
from typing import TYPE_CHECKING, Any
|
9
9
|
|
10
|
-
import cv2
|
10
|
+
import cv2 # type: ignore
|
11
11
|
import osmnx as ox # type: ignore
|
12
12
|
from pyproj import Transformer
|
13
13
|
from shapely.affinity import rotate, translate # type: ignore
|
14
|
-
from shapely.geometry import Polygon, box # type: ignore
|
14
|
+
from shapely.geometry import LineString, Polygon, box # type: ignore
|
15
15
|
|
16
16
|
from maps4fs.generator.qgis import save_scripts
|
17
17
|
|
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
|
20
20
|
from maps4fs.generator.map import Map
|
21
21
|
|
22
22
|
|
23
|
-
# pylint: disable=R0801, R0903, R0902, R0904
|
23
|
+
# pylint: disable=R0801, R0903, R0902, R0904, R0913, R0917
|
24
24
|
class Component:
|
25
25
|
"""Base class for all map generation components.
|
26
26
|
|
@@ -46,7 +46,7 @@ class Component:
|
|
46
46
|
rotation: int,
|
47
47
|
map_directory: str,
|
48
48
|
logger: Any = None,
|
49
|
-
**kwargs
|
49
|
+
**kwargs: dict[str, Any],
|
50
50
|
):
|
51
51
|
self.game = game
|
52
52
|
self.map = map
|
@@ -58,6 +58,13 @@ class Component:
|
|
58
58
|
self.logger = logger
|
59
59
|
self.kwargs = kwargs
|
60
60
|
|
61
|
+
self.logger.info(
|
62
|
+
"Component %s initialized. Map size: %s, map rotated size: %s", # type: ignore
|
63
|
+
self.__class__.__name__,
|
64
|
+
self.map_size,
|
65
|
+
self.map_rotated_size,
|
66
|
+
)
|
67
|
+
|
61
68
|
os.makedirs(self.previews_directory, exist_ok=True)
|
62
69
|
os.makedirs(self.scripts_directory, exist_ok=True)
|
63
70
|
os.makedirs(self.info_layers_directory, exist_ok=True)
|
@@ -330,57 +337,80 @@ class Component:
|
|
330
337
|
|
331
338
|
return cs_x, cs_y
|
332
339
|
|
333
|
-
|
334
|
-
|
340
|
+
# pylint: disable=R0914
|
341
|
+
def fit_object_into_bounds(
|
342
|
+
self,
|
343
|
+
polygon_points: list[tuple[int, int]] | None = None,
|
344
|
+
linestring_points: list[tuple[int, int]] | None = None,
|
345
|
+
margin: int = 0,
|
346
|
+
angle: int = 0,
|
335
347
|
) -> list[tuple[int, int]]:
|
336
348
|
"""Fits a polygon into the bounds of the map.
|
337
349
|
|
338
350
|
Arguments:
|
339
351
|
polygon_points (list[tuple[int, int]]): The points of the polygon.
|
352
|
+
linestring_points (list[tuple[int, int]]): The points of the linestring.
|
340
353
|
margin (int, optional): The margin to add to the polygon. Defaults to 0.
|
341
354
|
angle (int, optional): The angle to rotate the polygon by. Defaults to 0.
|
342
355
|
|
343
356
|
Returns:
|
344
357
|
list[tuple[int, int]]: The points of the polygon fitted into the map bounds.
|
345
358
|
"""
|
359
|
+
if polygon_points is None and linestring_points is None:
|
360
|
+
raise ValueError("Either polygon or linestring points must be provided.")
|
361
|
+
|
346
362
|
min_x = min_y = 0
|
347
363
|
max_x = max_y = self.map_size
|
348
364
|
|
349
|
-
|
365
|
+
object_type = Polygon if polygon_points else LineString
|
366
|
+
|
367
|
+
# polygon = Polygon(polygon_points)
|
368
|
+
osm_object = object_type(polygon_points or linestring_points)
|
350
369
|
|
351
370
|
if angle:
|
352
371
|
center_x = center_y = self.map_rotated_size // 2
|
353
372
|
self.logger.debug(
|
354
|
-
"Rotating the
|
373
|
+
"Rotating the osm_object by %s degrees with center at %sx%s",
|
355
374
|
angle,
|
356
375
|
center_x,
|
357
376
|
center_y,
|
358
377
|
)
|
359
|
-
|
378
|
+
osm_object = rotate(osm_object, -angle, origin=(center_x, center_y))
|
360
379
|
offset = (self.map_size / 2) - (self.map_rotated_size / 2)
|
361
|
-
self.logger.debug("Translating the
|
362
|
-
|
363
|
-
self.logger.debug("Rotated and translated
|
380
|
+
self.logger.debug("Translating the osm_object by %s", offset)
|
381
|
+
osm_object = translate(osm_object, xoff=offset, yoff=offset)
|
382
|
+
self.logger.debug("Rotated and translated the osm_object.")
|
364
383
|
|
365
|
-
if margin:
|
366
|
-
|
367
|
-
if
|
368
|
-
raise ValueError("The
|
384
|
+
if margin and object_type is Polygon:
|
385
|
+
osm_object = osm_object.buffer(margin, join_style="mitre")
|
386
|
+
if osm_object.is_empty:
|
387
|
+
raise ValueError("The osm_object is empty after adding the margin.")
|
369
388
|
|
370
389
|
# Create a bounding box for the map bounds
|
371
390
|
bounds = box(min_x, min_y, max_x, max_y)
|
372
391
|
|
373
|
-
# Intersect the
|
374
|
-
|
375
|
-
|
392
|
+
# Intersect the osm_object with the bounds to fit it within the map
|
393
|
+
try:
|
394
|
+
fitted_osm_object = osm_object.intersection(bounds)
|
395
|
+
self.logger.debug("Fitted the osm_object into the bounds: %s", bounds)
|
396
|
+
except Exception as e:
|
397
|
+
raise ValueError( # pylint: disable=W0707
|
398
|
+
f"Could not fit the osm_object into the bounds: {e}"
|
399
|
+
)
|
376
400
|
|
377
|
-
if not isinstance(
|
378
|
-
raise ValueError("The fitted
|
401
|
+
if not isinstance(fitted_osm_object, object_type):
|
402
|
+
raise ValueError("The fitted osm_object is not valid (probably splitted into parts).")
|
379
403
|
|
380
404
|
# Return the fitted polygon points
|
381
|
-
|
405
|
+
if object_type is Polygon:
|
406
|
+
as_list = list(fitted_osm_object.exterior.coords)
|
407
|
+
elif object_type is LineString:
|
408
|
+
as_list = list(fitted_osm_object.coords)
|
409
|
+
else:
|
410
|
+
raise ValueError("The object type is not supported.")
|
411
|
+
|
382
412
|
if not as_list:
|
383
|
-
raise ValueError("The fitted
|
413
|
+
raise ValueError("The fitted osm_object has no points.")
|
384
414
|
return as_list
|
385
415
|
|
386
416
|
def get_infolayer_path(self, layer_name: str) -> str | None:
|
@@ -463,3 +493,35 @@ class Component:
|
|
463
493
|
self.logger.debug("Shape of the cropped image: %s", cropped.shape)
|
464
494
|
|
465
495
|
cv2.imwrite(output_path, cropped)
|
496
|
+
|
497
|
+
@staticmethod
|
498
|
+
def interpolate_points(
|
499
|
+
polyline: list[tuple[int, int]], num_points: int = 4
|
500
|
+
) -> list[tuple[int, int]]:
|
501
|
+
"""Receives a list of tuples, which represents a polyline. Add additional points
|
502
|
+
between the existing points to make the polyline smoother.
|
503
|
+
|
504
|
+
Arguments:
|
505
|
+
polyline (list[tuple[int, int]]): The list of points to interpolate.
|
506
|
+
num_points (int): The number of additional points to add between each pair of points.
|
507
|
+
|
508
|
+
Returns:
|
509
|
+
list[tuple[int, int]]: The list of points with additional points.
|
510
|
+
"""
|
511
|
+
if not polyline or num_points < 1:
|
512
|
+
return polyline
|
513
|
+
|
514
|
+
interpolated_polyline = []
|
515
|
+
for i in range(len(polyline) - 1):
|
516
|
+
p1 = polyline[i]
|
517
|
+
p2 = polyline[i + 1]
|
518
|
+
interpolated_polyline.append(p1)
|
519
|
+
for j in range(1, num_points + 1):
|
520
|
+
new_point = (
|
521
|
+
p1[0] + (p2[0] - p1[0]) * j / (num_points + 1),
|
522
|
+
p1[1] + (p2[1] - p1[1]) * j / (num_points + 1),
|
523
|
+
)
|
524
|
+
interpolated_polyline.append((int(new_point[0]), int(new_point[1])))
|
525
|
+
interpolated_polyline.append(polyline[-1])
|
526
|
+
|
527
|
+
return interpolated_polyline
|
maps4fs/generator/config.py
CHANGED
@@ -38,7 +38,7 @@ class Config(Component):
|
|
38
38
|
self.logger.warning("Map XML file not found: %s.", self._map_xml_path)
|
39
39
|
return
|
40
40
|
tree = ET.parse(self._map_xml_path)
|
41
|
-
self.logger.
|
41
|
+
self.logger.debug("Map XML file loaded from: %s.", self._map_xml_path)
|
42
42
|
root = tree.getroot()
|
43
43
|
for map_elem in root.iter("map"):
|
44
44
|
map_elem.set("width", str(self.map_size))
|
maps4fs/generator/dem.py
CHANGED
@@ -14,9 +14,6 @@ from pympler import asizeof # type: ignore
|
|
14
14
|
from maps4fs.generator.component import Component
|
15
15
|
|
16
16
|
SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
|
17
|
-
DEFAULT_MULTIPLIER = 1
|
18
|
-
DEFAULT_BLUR_RADIUS = 35
|
19
|
-
DEFAULT_PLATEAU = 0
|
20
17
|
|
21
18
|
|
22
19
|
# pylint: disable=R0903, R0902
|
@@ -50,20 +47,21 @@ class DEM(Component):
|
|
50
47
|
self.output_resolution = self.get_output_resolution()
|
51
48
|
self.logger.debug("Output resolution for DEM data: %s.", self.output_resolution)
|
52
49
|
|
53
|
-
|
54
|
-
blur_radius = self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS)
|
50
|
+
blur_radius = self.map.dem_settings.blur_radius
|
55
51
|
if blur_radius is None or blur_radius <= 0:
|
56
52
|
# We'll disable blur if the radius is 0 or negative.
|
57
53
|
blur_radius = 0
|
58
54
|
elif blur_radius % 2 == 0:
|
59
55
|
blur_radius += 1
|
60
56
|
self.blur_radius = blur_radius
|
57
|
+
self.multiplier = self.map.dem_settings.multiplier
|
61
58
|
self.logger.debug(
|
62
|
-
"DEM value multiplier is %s, blur radius is %s.",
|
59
|
+
"DEM value multiplier is %s, blur radius is %s.",
|
60
|
+
self.multiplier,
|
61
|
+
self.blur_radius,
|
63
62
|
)
|
64
63
|
|
65
|
-
self.auto_process = self.
|
66
|
-
self.plateau = self.kwargs.get("plateau", False)
|
64
|
+
self.auto_process = self.map.dem_settings.auto_process
|
67
65
|
|
68
66
|
@property
|
69
67
|
def dem_path(self) -> str:
|
@@ -240,24 +238,24 @@ class DEM(Component):
|
|
240
238
|
resampled_data.max(),
|
241
239
|
)
|
242
240
|
|
243
|
-
if self.plateau:
|
241
|
+
if self.map.dem_settings.plateau:
|
244
242
|
# Plateau is a flat area with a constant height.
|
245
243
|
# So we just add this value to each pixel of the DEM.
|
246
244
|
# And also need to ensure that there will be no values with height greater than
|
247
245
|
# it's allowed in 16-bit unsigned integer.
|
248
246
|
|
249
|
-
resampled_data += self.plateau
|
247
|
+
resampled_data += self.map.dem_settings.plateau
|
250
248
|
resampled_data = np.clip(resampled_data, 0, 65535)
|
251
249
|
|
252
250
|
self.logger.debug(
|
253
251
|
"Plateau with height %s was added to DEM data. Min: %s, max: %s.",
|
254
|
-
self.plateau,
|
252
|
+
self.map.dem_settings.plateau,
|
255
253
|
resampled_data.min(),
|
256
254
|
resampled_data.max(),
|
257
255
|
)
|
258
256
|
|
259
257
|
cv2.imwrite(self._dem_path, resampled_data)
|
260
|
-
self.logger.
|
258
|
+
self.logger.debug("DEM data was saved to %s.", self._dem_path)
|
261
259
|
|
262
260
|
if self.rotation:
|
263
261
|
self.rotate_dem()
|
@@ -403,7 +401,7 @@ class DEM(Component):
|
|
403
401
|
|
404
402
|
scaling_factor = self._get_scaling_factor(max_dev)
|
405
403
|
adjusted_max_height = int(65535 * scaling_factor)
|
406
|
-
self.logger.
|
404
|
+
self.logger.debug(
|
407
405
|
"Maximum deviation: %s. Scaling factor: %s. Adjusted max height: %s.",
|
408
406
|
max_dev,
|
409
407
|
scaling_factor,
|
maps4fs/generator/game.py
CHANGED
@@ -39,7 +39,7 @@ class Game:
|
|
39
39
|
_tree_schema: str | None = None
|
40
40
|
|
41
41
|
# Order matters! Some components depend on others.
|
42
|
-
components = [Texture,
|
42
|
+
components = [Texture, GRLE, Background, I3d, Config]
|
43
43
|
|
44
44
|
def __init__(self, map_template_path: str | None = None):
|
45
45
|
if map_template_path:
|