maps4fs 1.0.9__py3-none-any.whl → 1.1.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.
- maps4fs/generator/background.py +209 -149
- maps4fs/generator/component.py +108 -30
- maps4fs/generator/config.py +13 -9
- maps4fs/generator/dem.py +70 -72
- maps4fs/generator/game.py +1 -2
- maps4fs/generator/grle.py +31 -10
- maps4fs/generator/i3d.py +24 -8
- maps4fs/generator/map.py +19 -11
- maps4fs/generator/texture.py +47 -15
- {maps4fs-1.0.9.dist-info → maps4fs-1.1.1.dist-info}/METADATA +6 -8
- maps4fs-1.1.1.dist-info/RECORD +21 -0
- maps4fs/generator/path_steps.py +0 -97
- maps4fs/generator/tile.py +0 -51
- maps4fs-1.0.9.dist-info/RECORD +0 -23
- {maps4fs-1.0.9.dist-info → maps4fs-1.1.1.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.0.9.dist-info → maps4fs-1.1.1.dist-info}/WHEEL +0 -0
- {maps4fs-1.0.9.dist-info → maps4fs-1.1.1.dist-info}/top_level.txt +0 -0
maps4fs/generator/background.py
CHANGED
@@ -15,87 +15,92 @@ from maps4fs.generator.dem import (
|
|
15
15
|
DEFAULT_BLUR_RADIUS,
|
16
16
|
DEFAULT_MULTIPLIER,
|
17
17
|
DEFAULT_PLATEAU,
|
18
|
+
DEM,
|
18
19
|
)
|
19
|
-
from maps4fs.generator.path_steps import (
|
20
|
-
PATH_FULL_NAME,
|
21
|
-
PATH_FULL_PREVIEW_NAME,
|
22
|
-
get_steps,
|
23
|
-
)
|
24
|
-
from maps4fs.generator.tile import Tile
|
25
20
|
|
26
|
-
|
27
|
-
|
21
|
+
DEFAULT_DISTANCE = 2048
|
22
|
+
RESIZE_FACTOR = 1 / 8
|
23
|
+
FULL_NAME = "FULL"
|
24
|
+
FULL_PREVIEW_NAME = "PREVIEW"
|
25
|
+
ELEMENTS = [FULL_NAME, FULL_PREVIEW_NAME]
|
28
26
|
|
29
27
|
|
30
28
|
class Background(Component):
|
31
29
|
"""Component for creating 3D obj files based on DEM data around the map.
|
32
30
|
|
33
31
|
Arguments:
|
32
|
+
game (Game): The game instance for which the map is generated.
|
34
33
|
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
35
|
-
|
36
|
-
|
34
|
+
map_size (int): The size of the map in pixels (it's a square).
|
35
|
+
rotated_map_size (int): The size of the map in pixels after rotation.
|
36
|
+
rotation (int): The rotation angle of the map.
|
37
37
|
map_directory (str): The directory where the map files are stored.
|
38
38
|
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
39
39
|
info, warning. If not provided, default logging will be used.
|
40
40
|
"""
|
41
41
|
|
42
|
+
# pylint: disable=R0801
|
42
43
|
def preprocess(self) -> None:
|
43
|
-
"""
|
44
|
-
clockwise from North, then clockwise."""
|
45
|
-
self.tiles: list[Tile] = []
|
46
|
-
origin = self.coordinates
|
47
|
-
|
48
|
-
only_full_tiles = self.kwargs.get("only_full_tiles", True)
|
44
|
+
"""Registers the DEMs for the background terrain."""
|
49
45
|
self.light_version = self.kwargs.get("light_version", False)
|
46
|
+
self.stl_preview_path: str | None = None
|
50
47
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
coordinates
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
auto_process=
|
48
|
+
if self.rotation:
|
49
|
+
self.logger.debug("Rotation is enabled: %s.", self.rotation)
|
50
|
+
output_size_multiplier = 1.5
|
51
|
+
else:
|
52
|
+
output_size_multiplier = 1
|
53
|
+
|
54
|
+
background_size = self.map_size + DEFAULT_DISTANCE * 2
|
55
|
+
rotated_size = int(background_size * output_size_multiplier)
|
56
|
+
|
57
|
+
self.background_directory = os.path.join(self.map_directory, "background")
|
58
|
+
os.makedirs(self.background_directory, exist_ok=True)
|
59
|
+
|
60
|
+
autoprocesses = [self.kwargs.get("auto_process", False), False]
|
61
|
+
dems = []
|
62
|
+
|
63
|
+
for name, autoprocess in zip(ELEMENTS, autoprocesses):
|
64
|
+
dem = DEM(
|
65
|
+
self.game,
|
66
|
+
self.coordinates,
|
67
|
+
background_size,
|
68
|
+
rotated_size,
|
69
|
+
self.rotation,
|
70
|
+
self.map_directory,
|
71
|
+
self.logger,
|
72
|
+
auto_process=autoprocess,
|
76
73
|
blur_radius=self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS),
|
77
74
|
multiplier=self.kwargs.get("multiplier", DEFAULT_MULTIPLIER),
|
78
75
|
plateau=self.kwargs.get("plateau", DEFAULT_PLATEAU),
|
79
76
|
)
|
77
|
+
dem.preprocess()
|
78
|
+
dem.is_preview = self.is_preview(name) # type: ignore
|
79
|
+
dem.set_output_resolution((rotated_size, rotated_size))
|
80
|
+
dem.set_dem_path(os.path.join(self.background_directory, f"{name}.png"))
|
81
|
+
dems.append(dem)
|
80
82
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
83
|
+
self.dems = dems
|
84
|
+
|
85
|
+
def is_preview(self, name: str) -> bool:
|
86
|
+
"""Checks if the DEM is a preview.
|
87
|
+
|
88
|
+
Arguments:
|
89
|
+
name (str): The name of the DEM.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
bool: True if the DEM is a preview, False otherwise.
|
93
|
+
"""
|
94
|
+
return name == FULL_PREVIEW_NAME
|
90
95
|
|
91
96
|
def process(self) -> None:
|
92
97
|
"""Launches the component processing. Iterates over all tiles and processes them
|
93
98
|
as a result the DEM files will be saved, then based on them the obj files will be
|
94
99
|
generated."""
|
95
|
-
for
|
96
|
-
|
97
|
-
if
|
98
|
-
cutted_dem_path = self.cutout(
|
100
|
+
for dem in self.dems:
|
101
|
+
dem.process()
|
102
|
+
if not dem.is_preview: # type: ignore
|
103
|
+
cutted_dem_path = self.cutout(dem.dem_path)
|
99
104
|
if self.game.additional_dem_name is not None:
|
100
105
|
self.make_copy(cutted_dem_path, self.game.additional_dem_name)
|
101
106
|
|
@@ -118,71 +123,64 @@ class Background(Component):
|
|
118
123
|
shutil.copyfile(dem_path, additional_dem_path)
|
119
124
|
self.logger.info("Additional DEM data was copied to %s.", additional_dem_path)
|
120
125
|
|
121
|
-
def info_sequence(self) -> dict[str,
|
122
|
-
"""Returns a dictionary with information about the
|
126
|
+
def info_sequence(self) -> dict[str, str | float | int]:
|
127
|
+
"""Returns a dictionary with information about the background terrain.
|
123
128
|
Adds the EPSG:3857 string to the data for convenient usage in QGIS.
|
124
129
|
|
125
130
|
Returns:
|
126
|
-
dict[str,
|
131
|
+
dict[str, str, float | int] -- A dictionary with information about the background
|
132
|
+
terrain.
|
127
133
|
"""
|
128
|
-
data = {}
|
129
134
|
self.qgis_sequence()
|
130
135
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
data[tile.code] = tile_entry
|
150
|
-
|
136
|
+
dem = self.dems[0]
|
137
|
+
|
138
|
+
north, south, east, west = dem.bbox
|
139
|
+
epsg3857_string = dem.get_epsg3857_string()
|
140
|
+
epsg3857_string_with_margin = dem.get_epsg3857_string(add_margin=True)
|
141
|
+
|
142
|
+
data = {
|
143
|
+
"center_latitude": dem.coordinates[0],
|
144
|
+
"center_longitude": dem.coordinates[1],
|
145
|
+
"epsg3857_string": epsg3857_string,
|
146
|
+
"epsg3857_string_with_margin": epsg3857_string_with_margin,
|
147
|
+
"height": dem.map_size,
|
148
|
+
"width": dem.map_size,
|
149
|
+
"north": north,
|
150
|
+
"south": south,
|
151
|
+
"east": east,
|
152
|
+
"west": west,
|
153
|
+
}
|
151
154
|
return data # type: ignore
|
152
155
|
|
153
156
|
def qgis_sequence(self) -> None:
|
154
157
|
"""Generates QGIS scripts for creating bounding box layers and rasterizing them."""
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
]
|
162
|
-
|
163
|
-
layers = qgis_layers + qgis_layers_with_margin
|
164
|
-
|
165
|
-
self.create_qgis_scripts(layers)
|
158
|
+
qgis_layer = (f"Background_{FULL_NAME}", *self.dems[0].get_espg3857_bbox())
|
159
|
+
qgis_layer_with_margin = (
|
160
|
+
f"Background_{FULL_NAME}_margin",
|
161
|
+
*self.dems[0].get_espg3857_bbox(add_margin=True),
|
162
|
+
)
|
163
|
+
self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
|
166
164
|
|
167
165
|
def generate_obj_files(self) -> None:
|
168
|
-
"""Iterates over all
|
166
|
+
"""Iterates over all dems and generates 3D obj files based on DEM data.
|
169
167
|
If at least one DEM file is missing, the generation will be stopped at all.
|
170
168
|
"""
|
171
|
-
for
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
169
|
+
for dem in self.dems:
|
170
|
+
if not os.path.isfile(dem.dem_path):
|
171
|
+
self.logger.warning(
|
172
|
+
"DEM file not found, generation will be stopped: %s", dem.dem_path
|
173
|
+
)
|
176
174
|
return
|
177
175
|
|
178
|
-
self.logger.debug("DEM file for
|
176
|
+
self.logger.debug("DEM file for found: %s", dem.dem_path)
|
179
177
|
|
180
|
-
|
181
|
-
save_path = os.path.join(
|
182
|
-
self.logger.debug("Generating obj file
|
178
|
+
filename = os.path.splitext(os.path.basename(dem.dem_path))[0]
|
179
|
+
save_path = os.path.join(self.background_directory, f"{filename}.obj")
|
180
|
+
self.logger.debug("Generating obj file in path: %s", save_path)
|
183
181
|
|
184
|
-
dem_data = cv2.imread(
|
185
|
-
self.plane_from_np(
|
182
|
+
dem_data = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
183
|
+
self.plane_from_np(dem_data, save_path, is_preview=dem.is_preview) # type: ignore
|
186
184
|
|
187
185
|
# pylint: disable=too-many-locals
|
188
186
|
def cutout(self, dem_path: str) -> str:
|
@@ -197,17 +195,16 @@ class Background(Component):
|
|
197
195
|
dem_data = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
198
196
|
|
199
197
|
center = (dem_data.shape[0] // 2, dem_data.shape[1] // 2)
|
200
|
-
half_size = self.
|
198
|
+
half_size = self.map_size // 2
|
201
199
|
x1 = center[0] - half_size
|
202
200
|
x2 = center[0] + half_size
|
203
201
|
y1 = center[1] - half_size
|
204
202
|
y2 = center[1] + half_size
|
205
203
|
dem_data = dem_data[x1:x2, y1:y2]
|
206
204
|
|
207
|
-
output_size = self.
|
205
|
+
output_size = self.map_size + 1
|
208
206
|
|
209
207
|
main_dem_path = self.game.dem_file_path(self.map_directory)
|
210
|
-
dem_directory = os.path.dirname(main_dem_path)
|
211
208
|
|
212
209
|
try:
|
213
210
|
os.remove(main_dem_path)
|
@@ -219,46 +216,25 @@ class Background(Component):
|
|
219
216
|
dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR
|
220
217
|
)
|
221
218
|
|
222
|
-
|
223
|
-
|
224
|
-
# the DEM image should be 8193x8193, but it does not work, so we need to
|
225
|
-
# resize the DEM to 4097x4097.
|
226
|
-
if self.map_height > 4096:
|
227
|
-
correct_dem_path = os.path.join(dem_directory, "correct_dem.png")
|
228
|
-
save_path = correct_dem_path
|
229
|
-
|
230
|
-
output_size = self.map_height // 2 + 1
|
231
|
-
bugged_dem_data = cv2.resize(
|
232
|
-
dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR
|
233
|
-
)
|
234
|
-
# pylint: disable=no-member
|
235
|
-
cv2.imwrite(main_dem_path, bugged_dem_data)
|
236
|
-
else:
|
237
|
-
save_path = main_dem_path
|
238
|
-
|
239
|
-
cv2.imwrite(save_path, resized_dem_data) # pylint: disable=no-member
|
240
|
-
self.logger.info("DEM cutout saved: %s", save_path)
|
219
|
+
cv2.imwrite(main_dem_path, resized_dem_data) # pylint: disable=no-member
|
220
|
+
self.logger.info("DEM cutout saved: %s", main_dem_path)
|
241
221
|
|
242
|
-
return
|
222
|
+
return main_dem_path
|
243
223
|
|
244
224
|
# pylint: disable=too-many-locals
|
245
|
-
def plane_from_np(self,
|
225
|
+
def plane_from_np(self, dem_data: np.ndarray, save_path: str, is_preview: bool = False) -> None:
|
246
226
|
"""Generates a 3D obj file based on DEM data.
|
247
227
|
|
248
228
|
Arguments:
|
249
|
-
tile_code (str) -- The code of the tile.
|
250
229
|
dem_data (np.ndarray) -- The DEM data as a numpy array.
|
251
230
|
save_path (str) -- The path where the obj file will be saved.
|
231
|
+
is_preview (bool, optional) -- If True, the preview mesh will be generated.
|
252
232
|
"""
|
253
|
-
if tile_code == PATH_FULL_NAME:
|
254
|
-
resize_factor = FULL_RESIZE_FACTOR
|
255
|
-
else:
|
256
|
-
resize_factor = RESIZE_FACTOR
|
257
233
|
dem_data = cv2.resize( # pylint: disable=no-member
|
258
|
-
dem_data, (0, 0), fx=
|
234
|
+
dem_data, (0, 0), fx=RESIZE_FACTOR, fy=RESIZE_FACTOR
|
259
235
|
)
|
260
236
|
self.logger.debug(
|
261
|
-
"DEM data resized to shape: %s with factor: %s", dem_data.shape,
|
237
|
+
"DEM data resized to shape: %s with factor: %s", dem_data.shape, RESIZE_FACTOR
|
262
238
|
)
|
263
239
|
|
264
240
|
# Invert the height values.
|
@@ -271,9 +247,7 @@ class Background(Component):
|
|
271
247
|
z = dem_data
|
272
248
|
|
273
249
|
self.logger.debug(
|
274
|
-
"Starting to generate a mesh for
|
275
|
-
"This may take a while...",
|
276
|
-
tile_code,
|
250
|
+
"Starting to generate a mesh for with shape: %s x %s. This may take a while...",
|
277
251
|
cols,
|
278
252
|
rows,
|
279
253
|
)
|
@@ -300,7 +274,7 @@ class Background(Component):
|
|
300
274
|
mesh.apply_transform(rotation_matrix_y)
|
301
275
|
mesh.apply_transform(rotation_matrix_z)
|
302
276
|
|
303
|
-
if
|
277
|
+
if is_preview:
|
304
278
|
# Simplify the preview mesh to reduce the size of the file.
|
305
279
|
mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
|
306
280
|
|
@@ -314,7 +288,7 @@ class Background(Component):
|
|
314
288
|
else:
|
315
289
|
z_scaling_factor = 1 / 2**5
|
316
290
|
self.logger.debug("Z scaling factor: %s", z_scaling_factor)
|
317
|
-
mesh.apply_scale([1 /
|
291
|
+
mesh.apply_scale([1 / RESIZE_FACTOR, 1 / RESIZE_FACTOR, z_scaling_factor])
|
318
292
|
|
319
293
|
mesh.export(save_path)
|
320
294
|
self.logger.debug("Obj file saved: %s", save_path)
|
@@ -336,19 +310,105 @@ class Background(Component):
|
|
336
310
|
|
337
311
|
# pylint: disable=no-member
|
338
312
|
def previews(self) -> list[str]:
|
339
|
-
"""Returns the path to the image
|
313
|
+
"""Returns the path to the image previews paths and the path to the STL preview file.
|
340
314
|
|
341
315
|
Returns:
|
342
316
|
list[str] -- A list of paths to the previews.
|
343
317
|
"""
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
318
|
+
preview_paths = self.dem_previews(self.game.dem_file_path(self.map_directory))
|
319
|
+
for dem in self.dems:
|
320
|
+
if dem.is_preview: # type: ignore
|
321
|
+
background_dem_preview_path = os.path.join(
|
322
|
+
self.previews_directory, "background_dem.png"
|
323
|
+
)
|
324
|
+
background_dem_preview_image = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED)
|
325
|
+
|
326
|
+
background_dem_preview_image = cv2.resize(
|
327
|
+
background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
|
328
|
+
)
|
329
|
+
background_dem_preview_image = cv2.normalize( # type: ignore
|
330
|
+
background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
|
331
|
+
)
|
332
|
+
background_dem_preview_image = cv2.cvtColor(
|
333
|
+
background_dem_preview_image, cv2.COLOR_GRAY2BGR
|
334
|
+
)
|
335
|
+
|
336
|
+
cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
|
337
|
+
preview_paths.append(background_dem_preview_path)
|
338
|
+
|
339
|
+
if self.stl_preview_path:
|
340
|
+
preview_paths.append(self.stl_preview_path)
|
341
|
+
|
342
|
+
return preview_paths
|
343
|
+
|
344
|
+
def dem_previews(self, image_path: str) -> list[str]:
|
345
|
+
"""Get list of preview images.
|
346
|
+
|
347
|
+
Arguments:
|
348
|
+
image_path (str): Path to the DEM file.
|
349
|
+
|
350
|
+
Returns:
|
351
|
+
list[str]: List of preview images.
|
352
|
+
"""
|
353
|
+
self.logger.debug("Starting DEM previews generation.")
|
354
|
+
return [self.grayscale_preview(image_path), self.colored_preview(image_path)]
|
355
|
+
|
356
|
+
def grayscale_preview(self, image_path: str) -> str:
|
357
|
+
"""Converts DEM image to grayscale RGB image and saves it to the map directory.
|
358
|
+
Returns path to the preview image.
|
359
|
+
|
360
|
+
Arguments:
|
361
|
+
image_path (str): Path to the DEM file.
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
str: Path to the preview image.
|
365
|
+
"""
|
366
|
+
grayscale_dem_path = os.path.join(self.previews_directory, "dem_grayscale.png")
|
367
|
+
|
368
|
+
self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
|
369
|
+
|
370
|
+
dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
371
|
+
dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
|
372
|
+
cv2.imwrite(grayscale_dem_path, dem_data_rgb)
|
373
|
+
return grayscale_dem_path
|
374
|
+
|
375
|
+
def colored_preview(self, image_path: str) -> str:
|
376
|
+
"""Converts DEM image to colored RGB image and saves it to the map directory.
|
377
|
+
Returns path to the preview image.
|
378
|
+
|
379
|
+
Arguments:
|
380
|
+
image_path (str): Path to the DEM file.
|
381
|
+
|
382
|
+
Returns:
|
383
|
+
list[str]: List with a single path to the DEM file
|
384
|
+
"""
|
385
|
+
colored_dem_path = os.path.join(self.previews_directory, "dem_colored.png")
|
386
|
+
|
387
|
+
self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
|
388
|
+
|
389
|
+
dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
390
|
+
|
391
|
+
self.logger.debug(
|
392
|
+
"DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
393
|
+
dem_data.shape,
|
394
|
+
dem_data.dtype,
|
395
|
+
dem_data.min(),
|
396
|
+
dem_data.max(),
|
397
|
+
)
|
398
|
+
|
399
|
+
# Create an empty array with the same shape and type as dem_data.
|
400
|
+
dem_data_normalized = np.empty_like(dem_data)
|
401
|
+
|
402
|
+
# Normalize the DEM data to the range [0, 255]
|
403
|
+
cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
|
404
|
+
self.logger.debug(
|
405
|
+
"DEM data after normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
406
|
+
dem_data_normalized.shape,
|
407
|
+
dem_data_normalized.dtype,
|
408
|
+
dem_data_normalized.min(),
|
409
|
+
dem_data_normalized.max(),
|
410
|
+
)
|
411
|
+
dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
|
412
|
+
|
413
|
+
cv2.imwrite(colored_dem_path, dem_data_colored)
|
414
|
+
return colored_dem_path
|