maps4fs 0.9.93__py3-none-any.whl → 1.1.6__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 +279 -227
- maps4fs/generator/component.py +220 -32
- maps4fs/generator/config.py +15 -11
- maps4fs/generator/dem.py +81 -98
- maps4fs/generator/game.py +19 -3
- maps4fs/generator/grle.py +186 -0
- maps4fs/generator/i3d.py +239 -25
- maps4fs/generator/map.py +29 -18
- maps4fs/generator/qgis.py +5 -5
- maps4fs/generator/texture.py +233 -38
- maps4fs/logger.py +1 -25
- maps4fs/toolbox/background.py +63 -0
- maps4fs/toolbox/dem.py +3 -3
- maps4fs-1.1.6.dist-info/LICENSE.md +190 -0
- {maps4fs-0.9.93.dist-info → maps4fs-1.1.6.dist-info}/METADATA +111 -58
- maps4fs-1.1.6.dist-info/RECORD +21 -0
- maps4fs/generator/path_steps.py +0 -83
- maps4fs/generator/tile.py +0 -55
- maps4fs-0.9.93.dist-info/LICENSE.md +0 -21
- maps4fs-0.9.93.dist-info/RECORD +0 -21
- {maps4fs-0.9.93.dist-info → maps4fs-1.1.6.dist-info}/WHEEL +0 -0
- {maps4fs-0.9.93.dist-info → maps4fs-1.1.6.dist-info}/top_level.txt +0 -0
maps4fs/generator/texture.py
CHANGED
@@ -4,7 +4,8 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import json
|
6
6
|
import os
|
7
|
-
import
|
7
|
+
import re
|
8
|
+
from collections import defaultdict
|
8
9
|
from typing import Any, Callable, Generator, Optional
|
9
10
|
|
10
11
|
import cv2
|
@@ -36,11 +37,14 @@ class Texture(Component):
|
|
36
37
|
"""Class which represents a layer with textures and tags.
|
37
38
|
It's using to obtain data from OSM using tags and make changes into corresponding textures.
|
38
39
|
|
39
|
-
|
40
|
+
Arguments:
|
40
41
|
name (str): Name of the layer.
|
41
42
|
tags (dict[str, str | list[str]]): Dictionary of tags to search for.
|
42
43
|
width (int | None): Width of the polygon in meters (only for LineString).
|
43
44
|
color (tuple[int, int, int]): Color of the layer in BGR format.
|
45
|
+
exclude_weight (bool): Flag to exclude weight from the texture.
|
46
|
+
priority (int | None): Priority of the layer.
|
47
|
+
info_layer (str | None): Name of the corresnponding info layer.
|
44
48
|
|
45
49
|
Attributes:
|
46
50
|
name (str): Name of the layer.
|
@@ -58,6 +62,7 @@ class Texture(Component):
|
|
58
62
|
color: tuple[int, int, int] | list[int] | None = None,
|
59
63
|
exclude_weight: bool = False,
|
60
64
|
priority: int | None = None,
|
65
|
+
info_layer: str | None = None,
|
61
66
|
):
|
62
67
|
self.name = name
|
63
68
|
self.count = count
|
@@ -66,6 +71,7 @@ class Texture(Component):
|
|
66
71
|
self.color = color if color else (255, 255, 255)
|
67
72
|
self.exclude_weight = exclude_weight
|
68
73
|
self.priority = priority
|
74
|
+
self.info_layer = info_layer
|
69
75
|
|
70
76
|
def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
|
71
77
|
"""Returns dictionary with layer data.
|
@@ -80,6 +86,7 @@ class Texture(Component):
|
|
80
86
|
"color": list(self.color),
|
81
87
|
"exclude_weight": self.exclude_weight,
|
82
88
|
"priority": self.priority,
|
89
|
+
"info_layer": self.info_layer,
|
83
90
|
}
|
84
91
|
|
85
92
|
data = {k: v for k, v in data.items() if v is not None}
|
@@ -89,7 +96,7 @@ class Texture(Component):
|
|
89
96
|
def from_json(cls, data: dict[str, str | list[str] | bool]) -> Texture.Layer:
|
90
97
|
"""Creates a new instance of the class from dictionary.
|
91
98
|
|
92
|
-
|
99
|
+
Arguments:
|
93
100
|
data (dict[str, str | list[str] | bool]): Dictionary with layer data.
|
94
101
|
|
95
102
|
Returns:
|
@@ -110,7 +117,64 @@ class Texture(Component):
|
|
110
117
|
weight_postfix = "_weight" if not self.exclude_weight else ""
|
111
118
|
return os.path.join(weights_directory, f"{self.name}{idx}{weight_postfix}.png")
|
112
119
|
|
120
|
+
def path_preview(self, previews_directory: str) -> str:
|
121
|
+
"""Returns path to the preview of the first texture of the layer.
|
122
|
+
|
123
|
+
Arguments:
|
124
|
+
previews_directory (str): Path to the directory with previews.
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
str: Path to the preview.
|
128
|
+
"""
|
129
|
+
return self.path(previews_directory).replace(".png", "_preview.png")
|
130
|
+
|
131
|
+
def get_preview_or_path(self, previews_directory: str) -> str:
|
132
|
+
"""Returns path to the preview of the first texture of the layer if it exists,
|
133
|
+
otherwise returns path to the texture.
|
134
|
+
|
135
|
+
Arguments:
|
136
|
+
previews_directory (str): Path to the directory with previews.
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
str: Path to the preview or texture.
|
140
|
+
"""
|
141
|
+
preview_path = self.path_preview(previews_directory)
|
142
|
+
return preview_path if os.path.isfile(preview_path) else self.path(previews_directory)
|
143
|
+
|
144
|
+
def paths(self, weights_directory: str) -> list[str]:
|
145
|
+
"""Returns a list of paths to the textures of the layer.
|
146
|
+
NOTE: Works only after the textures are generated, since it just lists the directory.
|
147
|
+
|
148
|
+
Arguments:
|
149
|
+
weights_directory (str): Path to the directory with weights.
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
list[str]: List of paths to the textures.
|
153
|
+
"""
|
154
|
+
weight_files = os.listdir(weights_directory)
|
155
|
+
|
156
|
+
# Inconsistent names are the name of textures that are not following the pattern
|
157
|
+
# of texture_name{idx}_weight.png.
|
158
|
+
inconsistent_names = ["forestRockRoot", "waterPuddle"]
|
159
|
+
|
160
|
+
if self.name in inconsistent_names:
|
161
|
+
return [
|
162
|
+
os.path.join(weights_directory, weight_file)
|
163
|
+
for weight_file in weight_files
|
164
|
+
if weight_file.startswith(self.name)
|
165
|
+
]
|
166
|
+
|
167
|
+
return [
|
168
|
+
os.path.join(weights_directory, weight_file)
|
169
|
+
for weight_file in weight_files
|
170
|
+
if re.match(rf"{self.name}\d{{2}}_weight.png", weight_file)
|
171
|
+
]
|
172
|
+
|
113
173
|
def preprocess(self) -> None:
|
174
|
+
self.light_version = self.kwargs.get("light_version", False)
|
175
|
+
self.fields_padding = self.kwargs.get("fields_padding", 0)
|
176
|
+
self.logger.debug("Light version: %s.", self.light_version)
|
177
|
+
|
114
178
|
if not os.path.isfile(self.game.texture_schema):
|
115
179
|
raise FileNotFoundError(f"Texture layers schema not found: {self.game.texture_schema}")
|
116
180
|
|
@@ -134,6 +198,9 @@ class Texture(Component):
|
|
134
198
|
self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
|
135
199
|
self.logger.debug("Generation info save path: %s.", self.info_save_path)
|
136
200
|
|
201
|
+
self.info_layer_path = os.path.join(self.info_layers_directory, "textures.json")
|
202
|
+
self.logger.debug("Info layer path: %s.", self.info_layer_path)
|
203
|
+
|
137
204
|
def get_base_layer(self) -> Layer | None:
|
138
205
|
"""Returns base layer.
|
139
206
|
|
@@ -149,6 +216,31 @@ class Texture(Component):
|
|
149
216
|
self._prepare_weights()
|
150
217
|
self._read_parameters()
|
151
218
|
self.draw()
|
219
|
+
self.rotate_textures()
|
220
|
+
|
221
|
+
def rotate_textures(self) -> None:
|
222
|
+
"""Rotates textures of the layers which have tags."""
|
223
|
+
if self.rotation:
|
224
|
+
# Iterate over the layers which have tags and rotate them.
|
225
|
+
for layer in self.layers:
|
226
|
+
if layer.tags:
|
227
|
+
self.logger.debug("Rotating layer %s.", layer.name)
|
228
|
+
layer_paths = layer.paths(self._weights_dir)
|
229
|
+
layer_paths += [layer.path_preview(self._weights_dir)]
|
230
|
+
for layer_path in layer_paths:
|
231
|
+
if os.path.isfile(layer_path):
|
232
|
+
self.rotate_image(
|
233
|
+
layer_path,
|
234
|
+
self.rotation,
|
235
|
+
output_height=self.map_size,
|
236
|
+
output_width=self.map_size,
|
237
|
+
)
|
238
|
+
else:
|
239
|
+
self.logger.warning("Layer path %s not found.", layer_path)
|
240
|
+
else:
|
241
|
+
self.logger.debug(
|
242
|
+
"Skipping rotation of layer %s because it has no tags.", layer.name
|
243
|
+
)
|
152
244
|
|
153
245
|
# pylint: disable=W0201
|
154
246
|
def _read_parameters(self) -> None:
|
@@ -186,16 +278,16 @@ class Texture(Component):
|
|
186
278
|
|
187
279
|
for layer in self.layers:
|
188
280
|
self._generate_weights(layer)
|
189
|
-
self.logger.
|
281
|
+
self.logger.info("Prepared weights for %s layers.", len(self.layers))
|
190
282
|
|
191
283
|
def _generate_weights(self, layer: Layer) -> None:
|
192
284
|
"""Generates weight files for textures. Each file is a numpy array of zeros and
|
193
285
|
dtype uint8 (0-255).
|
194
286
|
|
195
|
-
|
287
|
+
Arguments:
|
196
288
|
layer (Layer): Layer with textures and tags.
|
197
289
|
"""
|
198
|
-
size = (self.
|
290
|
+
size = (self.map_rotated_size, self.map_rotated_size)
|
199
291
|
postfix = "_weight.png" if not layer.exclude_weight else ".png"
|
200
292
|
if layer.count == 0:
|
201
293
|
filepaths = [os.path.join(self._weights_dir, layer.name + postfix)]
|
@@ -222,7 +314,7 @@ class Texture(Component):
|
|
222
314
|
def layers(self, layers: list[Layer]) -> None:
|
223
315
|
"""Sets list of layers with textures and tags.
|
224
316
|
|
225
|
-
|
317
|
+
Arguments:
|
226
318
|
layers (list[Layer]): List of layers.
|
227
319
|
"""
|
228
320
|
self._layers = layers
|
@@ -253,6 +345,10 @@ class Texture(Component):
|
|
253
345
|
|
254
346
|
cumulative_image = None
|
255
347
|
|
348
|
+
# Dictionary to store info layer data.
|
349
|
+
# Key is a layer.info_layer, value is a list of polygon points as tuples (x, y).
|
350
|
+
info_layer_data = defaultdict(list)
|
351
|
+
|
256
352
|
for layer in layers:
|
257
353
|
if not layer.tags:
|
258
354
|
self.logger.debug("Layer %s has no tags, there's nothing to draw.", layer.name)
|
@@ -273,6 +369,8 @@ class Texture(Component):
|
|
273
369
|
mask = cv2.bitwise_not(cumulative_image)
|
274
370
|
|
275
371
|
for polygon in self.polygons(layer.tags, layer.width): # type: ignore
|
372
|
+
if layer.info_layer:
|
373
|
+
info_layer_data[layer.info_layer].append(self.np_to_polygon_points(polygon))
|
276
374
|
cv2.fillPoly(layer_image, [polygon], color=255) # type: ignore
|
277
375
|
|
278
376
|
output_image = cv2.bitwise_and(layer_image, mask)
|
@@ -280,16 +378,76 @@ class Texture(Component):
|
|
280
378
|
cumulative_image = cv2.bitwise_or(cumulative_image, output_image)
|
281
379
|
|
282
380
|
cv2.imwrite(layer_path, output_image)
|
283
|
-
self.logger.
|
381
|
+
self.logger.info("Texture %s saved.", layer_path)
|
382
|
+
|
383
|
+
# Save info layer data.
|
384
|
+
with open(self.info_layer_path, "w", encoding="utf-8") as f:
|
385
|
+
json.dump(info_layer_data, f, ensure_ascii=False, indent=4)
|
284
386
|
|
285
387
|
if cumulative_image is not None:
|
286
388
|
self.draw_base_layer(cumulative_image)
|
287
389
|
|
390
|
+
if not self.light_version:
|
391
|
+
self.dissolve()
|
392
|
+
else:
|
393
|
+
self.logger.debug("Skipping dissolve in light version of the map.")
|
394
|
+
|
395
|
+
def dissolve(self) -> None:
|
396
|
+
"""Dissolves textures of the layers with tags into sublayers for them to look more
|
397
|
+
natural in the game.
|
398
|
+
Iterates over all layers with tags and reads the first texture, checks if the file
|
399
|
+
contains any non-zero values (255), splits those non-values between different weight
|
400
|
+
files of the corresponding layer and saves the changes to the files.
|
401
|
+
"""
|
402
|
+
for layer in self.layers:
|
403
|
+
if not layer.tags:
|
404
|
+
self.logger.debug("Layer %s has no tags, there's nothing to dissolve.", layer.name)
|
405
|
+
continue
|
406
|
+
layer_path = layer.path(self._weights_dir)
|
407
|
+
layer_paths = layer.paths(self._weights_dir)
|
408
|
+
|
409
|
+
if len(layer_paths) < 2:
|
410
|
+
self.logger.debug("Layer %s has only one texture, skipping.", layer.name)
|
411
|
+
continue
|
412
|
+
|
413
|
+
self.logger.debug("Dissolving layer from %s to %s.", layer_path, layer_paths)
|
414
|
+
|
415
|
+
# Check if the image contains any non-zero values, otherwise continue.
|
416
|
+
layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
|
417
|
+
|
418
|
+
if not np.any(layer_image):
|
419
|
+
self.logger.debug(
|
420
|
+
"Layer %s does not contain any non-zero values, skipping.", layer.name
|
421
|
+
)
|
422
|
+
continue
|
423
|
+
|
424
|
+
# Save the original image to use it for preview later, without combining the sublayers.
|
425
|
+
cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy())
|
426
|
+
|
427
|
+
# Get the coordinates of non-zero values.
|
428
|
+
non_zero_coords = np.column_stack(np.where(layer_image > 0))
|
429
|
+
|
430
|
+
# Prepare sublayers.
|
431
|
+
sublayers = [np.zeros_like(layer_image) for _ in range(layer.count)]
|
432
|
+
|
433
|
+
# Randomly assign non-zero values to sublayers.
|
434
|
+
for coord in non_zero_coords:
|
435
|
+
sublayers[np.random.randint(0, layer.count)][coord[0], coord[1]] = 255
|
436
|
+
|
437
|
+
# Save the sublayers.
|
438
|
+
for sublayer, sublayer_path in zip(sublayers, layer_paths):
|
439
|
+
cv2.imwrite(sublayer_path, sublayer)
|
440
|
+
self.logger.debug("Sublayer %s saved.", sublayer_path)
|
441
|
+
|
442
|
+
self.logger.info("Dissolved layer %s.", layer.name)
|
443
|
+
|
444
|
+
self.logger.info("Dissolving finished.")
|
445
|
+
|
288
446
|
def draw_base_layer(self, cumulative_image: np.ndarray) -> None:
|
289
447
|
"""Draws base layer and saves it into the png file.
|
290
448
|
Base layer is the last layer to be drawn, it fills the remaining area of the map.
|
291
449
|
|
292
|
-
|
450
|
+
Arguments:
|
293
451
|
cumulative_image (np.ndarray): Cumulative image with all layers.
|
294
452
|
"""
|
295
453
|
base_layer = self.get_base_layer()
|
@@ -298,37 +456,50 @@ class Texture(Component):
|
|
298
456
|
self.logger.debug("Drawing base layer %s.", layer_path)
|
299
457
|
img = cv2.bitwise_not(cumulative_image)
|
300
458
|
cv2.imwrite(layer_path, img)
|
301
|
-
self.logger.
|
459
|
+
self.logger.info("Base texture %s saved.", layer_path)
|
302
460
|
|
303
461
|
def get_relative_x(self, x: float) -> int:
|
304
462
|
"""Converts UTM X coordinate to relative X coordinate in map image.
|
305
463
|
|
306
|
-
|
464
|
+
Arguments:
|
307
465
|
x (float): UTM X coordinate.
|
308
466
|
|
309
467
|
Returns:
|
310
468
|
int: Relative X coordinate in map image.
|
311
469
|
"""
|
312
|
-
return int(self.
|
470
|
+
return int(self.map_rotated_size * (x - self.minimum_x) / (self.maximum_x - self.minimum_x))
|
313
471
|
|
314
472
|
def get_relative_y(self, y: float) -> int:
|
315
473
|
"""Converts UTM Y coordinate to relative Y coordinate in map image.
|
316
474
|
|
317
|
-
|
475
|
+
Arguments:
|
318
476
|
y (float): UTM Y coordinate.
|
319
477
|
|
320
478
|
Returns:
|
321
479
|
int: Relative Y coordinate in map image.
|
322
480
|
"""
|
323
|
-
return int(
|
481
|
+
return int(
|
482
|
+
self.map_rotated_size * (1 - (y - self.minimum_y) / (self.maximum_y - self.minimum_y))
|
483
|
+
)
|
484
|
+
|
485
|
+
def np_to_polygon_points(self, np_array: np.ndarray) -> list[tuple[int, int]]:
|
486
|
+
"""Converts numpy array of polygon points to list of tuples.
|
487
|
+
|
488
|
+
Arguments:
|
489
|
+
np_array (np.ndarray): Numpy array of polygon points.
|
490
|
+
|
491
|
+
Returns:
|
492
|
+
list[tuple[int, int]]: List of tuples.
|
493
|
+
"""
|
494
|
+
return [(int(x), int(y)) for x, y in np_array.reshape(-1, 2)]
|
324
495
|
|
325
496
|
# pylint: disable=W0613
|
326
497
|
def _to_np(self, geometry: shapely.geometry.polygon.Polygon, *args) -> np.ndarray:
|
327
498
|
"""Converts Polygon geometry to numpy array of polygon points.
|
328
499
|
|
329
|
-
|
500
|
+
Arguments:
|
330
501
|
geometry (shapely.geometry.polygon.Polygon): Polygon geometry.
|
331
|
-
*
|
502
|
+
*Arguments: Additional arguments:
|
332
503
|
- width (int | None): Width of the polygon in meters.
|
333
504
|
|
334
505
|
Returns:
|
@@ -340,15 +511,17 @@ class Texture(Component):
|
|
340
511
|
pairs = list(zip(xs, ys))
|
341
512
|
return np.array(pairs, dtype=np.int32).reshape((-1, 1, 2))
|
342
513
|
|
343
|
-
def _to_polygon(
|
514
|
+
def _to_polygon(
|
515
|
+
self, obj: pd.core.series.Series, width: int | None
|
516
|
+
) -> shapely.geometry.polygon.Polygon:
|
344
517
|
"""Converts OSM object to numpy array of polygon points.
|
345
518
|
|
346
|
-
|
519
|
+
Arguments:
|
347
520
|
obj (pd.core.series.Series): OSM object.
|
348
521
|
width (int | None): Width of the polygon in meters.
|
349
522
|
|
350
523
|
Returns:
|
351
|
-
|
524
|
+
shapely.geometry.polygon.Polygon: Polygon geometry.
|
352
525
|
"""
|
353
526
|
geometry = obj["geometry"]
|
354
527
|
geometry_type = geometry.geom_type
|
@@ -362,32 +535,45 @@ class Texture(Component):
|
|
362
535
|
self,
|
363
536
|
geometry: shapely.geometry.linestring.LineString | shapely.geometry.point.Point,
|
364
537
|
width: int | None,
|
365
|
-
) ->
|
538
|
+
) -> shapely.geometry.polygon.Polygon:
|
366
539
|
"""Converts LineString or Point geometry to numpy array of polygon points.
|
367
540
|
|
368
|
-
|
541
|
+
Arguments:
|
369
542
|
geometry (shapely.geometry.linestring.LineString | shapely.geometry.point.Point):
|
370
543
|
LineString or Point geometry.
|
371
544
|
width (int | None): Width of the polygon in meters.
|
372
545
|
|
373
546
|
Returns:
|
374
|
-
|
547
|
+
shapely.geometry.polygon.Polygon: Polygon geometry.
|
375
548
|
"""
|
376
549
|
polygon = geometry.buffer(width)
|
377
|
-
return
|
550
|
+
return polygon
|
551
|
+
|
552
|
+
def _skip(
|
553
|
+
self, geometry: shapely.geometry.polygon.Polygon, *args, **kwargs
|
554
|
+
) -> shapely.geometry.polygon.Polygon:
|
555
|
+
"""Returns the same geometry.
|
556
|
+
|
557
|
+
Arguments:
|
558
|
+
geometry (shapely.geometry.polygon.Polygon): Polygon geometry.
|
559
|
+
|
560
|
+
Returns:
|
561
|
+
shapely.geometry.polygon.Polygon: Polygon geometry.
|
562
|
+
"""
|
563
|
+
return geometry
|
378
564
|
|
379
565
|
def _converters(
|
380
566
|
self, geom_type: str
|
381
567
|
) -> Optional[Callable[[BaseGeometry, Optional[int]], np.ndarray]]:
|
382
568
|
"""Returns a converter function for a given geometry type.
|
383
569
|
|
384
|
-
|
570
|
+
Arguments:
|
385
571
|
geom_type (str): Geometry type.
|
386
572
|
|
387
573
|
Returns:
|
388
574
|
Callable[[shapely.geometry, int | None], np.ndarray]: Converter function.
|
389
575
|
"""
|
390
|
-
converters = {"Polygon": self.
|
576
|
+
converters = {"Polygon": self._skip, "LineString": self._sequence, "Point": self._sequence}
|
391
577
|
return converters.get(geom_type) # type: ignore
|
392
578
|
|
393
579
|
def polygons(
|
@@ -395,29 +581,38 @@ class Texture(Component):
|
|
395
581
|
) -> Generator[np.ndarray, None, None]:
|
396
582
|
"""Generator which yields numpy arrays of polygons from OSM data.
|
397
583
|
|
398
|
-
|
584
|
+
Arguments:
|
399
585
|
tags (dict[str, str | list[str]]): Dictionary of tags to search for.
|
400
586
|
width (int | None): Width of the polygon in meters (only for LineString).
|
401
587
|
|
402
588
|
Yields:
|
403
589
|
Generator[np.ndarray, None, None]: Numpy array of polygon points.
|
404
590
|
"""
|
591
|
+
is_fieds = "farmland" in tags.values()
|
405
592
|
try:
|
406
|
-
|
407
|
-
warnings.simplefilter("ignore", DeprecationWarning)
|
408
|
-
objects = ox.features_from_bbox(bbox=self.bbox, tags=tags)
|
593
|
+
objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
|
409
594
|
except Exception as e: # pylint: disable=W0718
|
410
595
|
self.logger.warning("Error fetching objects for tags: %s.", tags)
|
411
596
|
self.logger.warning(e)
|
412
597
|
return
|
413
|
-
objects_utm = ox.project_gdf(objects, to_latlong=False)
|
598
|
+
objects_utm = ox.projection.project_gdf(objects, to_latlong=False)
|
414
599
|
self.logger.debug("Fetched %s elements for tags: %s.", len(objects_utm), tags)
|
415
600
|
|
416
601
|
for _, obj in objects_utm.iterrows():
|
417
602
|
polygon = self._to_polygon(obj, width)
|
418
603
|
if polygon is None:
|
419
604
|
continue
|
420
|
-
|
605
|
+
|
606
|
+
if is_fieds and self.fields_padding > 0:
|
607
|
+
padded_polygon = polygon.buffer(-self.fields_padding)
|
608
|
+
|
609
|
+
if not isinstance(padded_polygon, shapely.geometry.polygon.Polygon):
|
610
|
+
self.logger.warning("The padding value is too high, field will not padded.")
|
611
|
+
else:
|
612
|
+
polygon = padded_polygon
|
613
|
+
|
614
|
+
polygon_np = self._to_np(polygon)
|
615
|
+
yield polygon_np
|
421
616
|
|
422
617
|
def previews(self) -> list[str]:
|
423
618
|
"""Invokes methods to generate previews. Returns list of paths to previews.
|
@@ -436,13 +631,11 @@ class Texture(Component):
|
|
436
631
|
Returns:
|
437
632
|
str: Path to the preview.
|
438
633
|
"""
|
439
|
-
scaling_factor =
|
440
|
-
PREVIEW_MAXIMUM_SIZE / self.map_width, PREVIEW_MAXIMUM_SIZE / self.map_height
|
441
|
-
)
|
634
|
+
scaling_factor = PREVIEW_MAXIMUM_SIZE / self.map_size
|
442
635
|
|
443
636
|
preview_size = (
|
444
|
-
int(self.
|
445
|
-
int(self.
|
637
|
+
int(self.map_size * scaling_factor),
|
638
|
+
int(self.map_size * scaling_factor),
|
446
639
|
)
|
447
640
|
self.logger.debug(
|
448
641
|
"Scaling factor: %s. Preview size: %s.",
|
@@ -455,7 +648,8 @@ class Texture(Component):
|
|
455
648
|
|
456
649
|
images = [
|
457
650
|
cv2.resize(
|
458
|
-
cv2.imread(layer.
|
651
|
+
cv2.imread(layer.get_preview_or_path(self._weights_dir), cv2.IMREAD_UNCHANGED),
|
652
|
+
preview_size,
|
459
653
|
)
|
460
654
|
for layer in active_layers
|
461
655
|
]
|
@@ -472,6 +666,7 @@ class Texture(Component):
|
|
472
666
|
merged.dtype,
|
473
667
|
)
|
474
668
|
preview_path = os.path.join(self.previews_directory, "textures_osm.png")
|
475
|
-
|
669
|
+
|
670
|
+
cv2.imwrite(preview_path, merged) # type: ignore
|
476
671
|
self.logger.info("Preview saved to %s.", preview_path)
|
477
672
|
return preview_path
|
maps4fs/logger.py
CHANGED
@@ -4,9 +4,7 @@ import logging
|
|
4
4
|
import os
|
5
5
|
import sys
|
6
6
|
from datetime import datetime
|
7
|
-
from
|
8
|
-
from time import perf_counter
|
9
|
-
from typing import Any, Callable, Literal
|
7
|
+
from typing import Literal
|
10
8
|
|
11
9
|
LOGGER_NAME = "maps4fs"
|
12
10
|
log_directory = os.path.join(os.getcwd(), "logs")
|
@@ -46,25 +44,3 @@ class Logger(logging.Logger):
|
|
46
44
|
today = datetime.now().strftime("%Y-%m-%d")
|
47
45
|
log_file = os.path.join(log_directory, f"{today}.txt")
|
48
46
|
return log_file
|
49
|
-
|
50
|
-
|
51
|
-
def timeit(func: Callable[..., Any]) -> Callable[..., Any]:
|
52
|
-
"""Decorator to log the time taken by a function to execute.
|
53
|
-
|
54
|
-
Args:
|
55
|
-
func (function): The function to be timed.
|
56
|
-
|
57
|
-
Returns:
|
58
|
-
function: The timed function.
|
59
|
-
"""
|
60
|
-
|
61
|
-
def timed(*args, **kwargs):
|
62
|
-
logger = getLogger("maps4fs")
|
63
|
-
start = perf_counter()
|
64
|
-
result = func(*args, **kwargs)
|
65
|
-
end = perf_counter()
|
66
|
-
if logger is not None:
|
67
|
-
logger.info("Function %s took %s seconds to execute", func.__name__, end - start)
|
68
|
-
return result
|
69
|
-
|
70
|
-
return timed
|
@@ -0,0 +1,63 @@
|
|
1
|
+
"""This module contains functions to work with the background terrain of the map."""
|
2
|
+
|
3
|
+
import cv2
|
4
|
+
import numpy as np
|
5
|
+
import trimesh # type: ignore
|
6
|
+
|
7
|
+
|
8
|
+
# pylint: disable=R0801, R0914
|
9
|
+
def plane_from_np(
|
10
|
+
dem_data: np.ndarray,
|
11
|
+
resize_factor: float,
|
12
|
+
simplify_factor: int,
|
13
|
+
save_path: str,
|
14
|
+
) -> None:
|
15
|
+
"""Generates a 3D obj file based on DEM data.
|
16
|
+
|
17
|
+
Arguments:
|
18
|
+
dem_data (np.ndarray) -- The DEM data as a numpy array.
|
19
|
+
resize_factor (float) -- The factor by which the DEM data will be resized. Bigger values
|
20
|
+
will result in a bigger mesh.
|
21
|
+
simplify_factor (int) -- The factor by which the mesh will be simplified. Bigger values
|
22
|
+
will result in a simpler mesh.
|
23
|
+
save_path (str) -- The path to save the obj file.
|
24
|
+
"""
|
25
|
+
dem_data = cv2.resize( # pylint: disable=no-member
|
26
|
+
dem_data, (0, 0), fx=resize_factor, fy=resize_factor
|
27
|
+
)
|
28
|
+
|
29
|
+
# Invert the height values.
|
30
|
+
dem_data = dem_data.max() - dem_data
|
31
|
+
|
32
|
+
rows, cols = dem_data.shape
|
33
|
+
x = np.linspace(0, cols - 1, cols)
|
34
|
+
y = np.linspace(0, rows - 1, rows)
|
35
|
+
x, y = np.meshgrid(x, y)
|
36
|
+
z = dem_data
|
37
|
+
|
38
|
+
vertices = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
|
39
|
+
faces = []
|
40
|
+
|
41
|
+
for i in range(rows - 1):
|
42
|
+
for j in range(cols - 1):
|
43
|
+
top_left = i * cols + j
|
44
|
+
top_right = top_left + 1
|
45
|
+
bottom_left = top_left + cols
|
46
|
+
bottom_right = bottom_left + 1
|
47
|
+
|
48
|
+
faces.append([top_left, bottom_left, bottom_right])
|
49
|
+
faces.append([top_left, bottom_right, top_right])
|
50
|
+
|
51
|
+
faces = np.array(faces) # type: ignore
|
52
|
+
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
|
53
|
+
|
54
|
+
# Apply rotation: 180 degrees around Y-axis and Z-axis
|
55
|
+
rotation_matrix_y = trimesh.transformations.rotation_matrix(np.pi, [0, 1, 0])
|
56
|
+
rotation_matrix_z = trimesh.transformations.rotation_matrix(np.pi, [0, 0, 1])
|
57
|
+
mesh.apply_transform(rotation_matrix_y)
|
58
|
+
mesh.apply_transform(rotation_matrix_z)
|
59
|
+
|
60
|
+
# Simplify the mesh to reduce the number of faces.
|
61
|
+
mesh = mesh.simplify_quadric_decimation(face_count=len(faces) // simplify_factor)
|
62
|
+
|
63
|
+
mesh.export(save_path)
|
maps4fs/toolbox/dem.py
CHANGED
@@ -11,7 +11,7 @@ from rasterio.windows import from_bounds # type: ignore
|
|
11
11
|
def read_geo_tiff(file_path: str) -> DatasetReader:
|
12
12
|
"""Read a GeoTIFF file and return the DatasetReader object.
|
13
13
|
|
14
|
-
|
14
|
+
Arguments:
|
15
15
|
file_path (str): The path to the GeoTIFF file.
|
16
16
|
|
17
17
|
Raises:
|
@@ -43,7 +43,7 @@ def get_geo_tiff_bbox(
|
|
43
43
|
) -> tuple[float, float, float, float]:
|
44
44
|
"""Return the bounding box of a GeoTIFF file in the destination CRS.
|
45
45
|
|
46
|
-
|
46
|
+
Arguments:
|
47
47
|
src (DatasetReader): The DatasetReader object for the GeoTIFF file.
|
48
48
|
dst_crs (str, optional): The destination CRS. Defaults to "EPSG:4326".
|
49
49
|
|
@@ -65,7 +65,7 @@ def get_geo_tiff_bbox(
|
|
65
65
|
def extract_roi(file_path: str, bbox: tuple[float, float, float, float]) -> str:
|
66
66
|
"""Extract a region of interest (ROI) from a GeoTIFF file and save it as a new file.
|
67
67
|
|
68
|
-
|
68
|
+
Arguments:
|
69
69
|
file_path (str): The path to the GeoTIFF file.
|
70
70
|
bbox (tuple[float, float, float, float]): The bounding box of the region of interest
|
71
71
|
as (north, south, east, west).
|