maps4fs 0.7.8__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 +4 -0
- maps4fs/generator/__init__.py +1 -0
- maps4fs/generator/component.py +101 -0
- maps4fs/generator/config.py +55 -0
- maps4fs/generator/dem.py +291 -0
- maps4fs/generator/game.py +191 -0
- maps4fs/generator/map.py +115 -0
- maps4fs/generator/texture.py +434 -0
- maps4fs/logger.py +46 -0
- maps4fs-0.7.8.dist-info/LICENSE.md +21 -0
- maps4fs-0.7.8.dist-info/METADATA +218 -0
- maps4fs-0.7.8.dist-info/RECORD +14 -0
- maps4fs-0.7.8.dist-info/WHEEL +5 -0
- maps4fs-0.7.8.dist-info/top_level.txt +1 -0
maps4fs/generator/map.py
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
"""This module contains Map class, which is used to generate map using all components."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import shutil
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from tqdm import tqdm
|
8
|
+
|
9
|
+
from maps4fs.generator.component import Component
|
10
|
+
from maps4fs.generator.game import Game
|
11
|
+
from maps4fs.logger import Logger
|
12
|
+
|
13
|
+
|
14
|
+
# pylint: disable=R0913, R0902
|
15
|
+
class Map:
|
16
|
+
"""Class used to generate map using all components.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
game (Type[Game]): Game for which the map is generated.
|
20
|
+
coordinates (tuple[float, float]): Coordinates of the center of the map.
|
21
|
+
height (int): Height of the map in pixels.
|
22
|
+
width (int): Width of the map in pixels.
|
23
|
+
map_directory (str): Path to the directory where map files will be stored.
|
24
|
+
logger (Any): Logger instance
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__( # pylint: disable=R0917
|
28
|
+
self,
|
29
|
+
game: Game,
|
30
|
+
coordinates: tuple[float, float],
|
31
|
+
height: int,
|
32
|
+
width: int,
|
33
|
+
map_directory: str,
|
34
|
+
logger: Any = None,
|
35
|
+
**kwargs,
|
36
|
+
):
|
37
|
+
self.game = game
|
38
|
+
self.components: list[Component] = []
|
39
|
+
self.coordinates = coordinates
|
40
|
+
self.height = height
|
41
|
+
self.width = width
|
42
|
+
self.map_directory = map_directory
|
43
|
+
|
44
|
+
if not logger:
|
45
|
+
logger = Logger(__name__, to_stdout=True, to_file=False)
|
46
|
+
self.logger = logger
|
47
|
+
self.logger.debug("Game was set to %s", game.code)
|
48
|
+
|
49
|
+
self.kwargs = kwargs
|
50
|
+
|
51
|
+
os.makedirs(self.map_directory, exist_ok=True)
|
52
|
+
self.logger.debug("Map directory created: %s", self.map_directory)
|
53
|
+
|
54
|
+
try:
|
55
|
+
shutil.unpack_archive(game.template_path, self.map_directory)
|
56
|
+
self.logger.info("Map template unpacked to %s", self.map_directory)
|
57
|
+
except Exception as e:
|
58
|
+
raise RuntimeError(f"Can not unpack map template due to error: {e}") from e
|
59
|
+
|
60
|
+
def generate(self) -> None:
|
61
|
+
"""Launch map generation using all components."""
|
62
|
+
with tqdm(total=len(self.game.components), desc="Generating map...") as pbar:
|
63
|
+
for game_component in self.game.components:
|
64
|
+
component = game_component(
|
65
|
+
self.game,
|
66
|
+
self.coordinates,
|
67
|
+
self.height,
|
68
|
+
self.width,
|
69
|
+
self.map_directory,
|
70
|
+
self.logger,
|
71
|
+
**self.kwargs,
|
72
|
+
)
|
73
|
+
try:
|
74
|
+
component.process()
|
75
|
+
except Exception as e: # pylint: disable=W0718
|
76
|
+
self.logger.error(
|
77
|
+
"Error processing component %s: %s",
|
78
|
+
component.__class__.__name__,
|
79
|
+
e,
|
80
|
+
)
|
81
|
+
raise e
|
82
|
+
self.components.append(component)
|
83
|
+
|
84
|
+
pbar.update(1)
|
85
|
+
|
86
|
+
def previews(self) -> list[str]:
|
87
|
+
"""Get list of preview images.
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
list[str]: List of preview images.
|
91
|
+
"""
|
92
|
+
previews = []
|
93
|
+
for component in self.components:
|
94
|
+
try:
|
95
|
+
previews.extend(component.previews())
|
96
|
+
except Exception as e: # pylint: disable=W0718
|
97
|
+
self.logger.error(
|
98
|
+
"Error getting previews for component %s: %s",
|
99
|
+
component.__class__.__name__,
|
100
|
+
e,
|
101
|
+
)
|
102
|
+
return previews
|
103
|
+
|
104
|
+
def pack(self, archive_name: str) -> str:
|
105
|
+
"""Pack map directory to zip archive.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
archive_name (str): Name of the archive.
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
str: Path to the archive.
|
112
|
+
"""
|
113
|
+
archive_path = shutil.make_archive(archive_name, "zip", self.map_directory)
|
114
|
+
self.logger.info("Map packed to %s.zip", archive_name)
|
115
|
+
return archive_path
|
@@ -0,0 +1,434 @@
|
|
1
|
+
"""Module with Texture class for generating textures for the map using OSM data."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import json
|
6
|
+
import os
|
7
|
+
import warnings
|
8
|
+
from typing import Callable, Generator, Optional
|
9
|
+
|
10
|
+
import cv2
|
11
|
+
import numpy as np
|
12
|
+
import osmnx as ox # type: ignore
|
13
|
+
import pandas as pd
|
14
|
+
import shapely.geometry # type: ignore
|
15
|
+
from shapely.geometry.base import BaseGeometry # type: ignore
|
16
|
+
|
17
|
+
from maps4fs.generator.component import Component
|
18
|
+
|
19
|
+
PREVIEW_MAXIMUM_SIZE = 2048
|
20
|
+
|
21
|
+
|
22
|
+
# pylint: disable=R0902
|
23
|
+
class Texture(Component):
|
24
|
+
"""Class which generates textures for the map using OSM data.
|
25
|
+
|
26
|
+
Attributes:
|
27
|
+
weights_dir (str): Path to the directory with weights.
|
28
|
+
name (str): Name of the texture.
|
29
|
+
tags (dict[str, str | list[str] | bool]): Dictionary of tags to search for.
|
30
|
+
width (int | None): Width of the polygon in meters (only for LineString).
|
31
|
+
color (tuple[int, int, int]): Color of the layer in BGR format.
|
32
|
+
"""
|
33
|
+
|
34
|
+
# pylint: disable=R0903
|
35
|
+
class Layer:
|
36
|
+
"""Class which represents a layer with textures and tags.
|
37
|
+
It's using to obtain data from OSM using tags and make changes into corresponding textures.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
name (str): Name of the layer.
|
41
|
+
tags (dict[str, str | list[str]]): Dictionary of tags to search for.
|
42
|
+
width (int | None): Width of the polygon in meters (only for LineString).
|
43
|
+
color (tuple[int, int, int]): Color of the layer in BGR format.
|
44
|
+
|
45
|
+
Attributes:
|
46
|
+
name (str): Name of the layer.
|
47
|
+
tags (dict[str, str | list[str]]): Dictionary of tags to search for.
|
48
|
+
width (int | None): Width of the polygon in meters (only for LineString).
|
49
|
+
"""
|
50
|
+
|
51
|
+
# pylint: disable=R0913
|
52
|
+
def __init__( # pylint: disable=R0917
|
53
|
+
self,
|
54
|
+
name: str,
|
55
|
+
count: int,
|
56
|
+
tags: dict[str, str | list[str] | bool] | None = None,
|
57
|
+
width: int | None = None,
|
58
|
+
color: tuple[int, int, int] | list[int] | None = None,
|
59
|
+
exclude_weight: bool = False,
|
60
|
+
):
|
61
|
+
self.name = name
|
62
|
+
self.count = count
|
63
|
+
self.tags = tags
|
64
|
+
self.width = width
|
65
|
+
self.color = color if color else (255, 255, 255)
|
66
|
+
self.exclude_weight = exclude_weight
|
67
|
+
|
68
|
+
def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
|
69
|
+
"""Returns dictionary with layer data.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
dict: Dictionary with layer data."""
|
73
|
+
data = {
|
74
|
+
"name": self.name,
|
75
|
+
"count": self.count,
|
76
|
+
"tags": self.tags,
|
77
|
+
"width": self.width,
|
78
|
+
"color": list(self.color),
|
79
|
+
"exclude_weight": self.exclude_weight,
|
80
|
+
}
|
81
|
+
|
82
|
+
data = {k: v for k, v in data.items() if v is not None}
|
83
|
+
return data # type: ignore
|
84
|
+
|
85
|
+
@classmethod
|
86
|
+
def from_json(cls, data: dict[str, str | list[str] | bool]) -> Texture.Layer:
|
87
|
+
"""Creates a new instance of the class from dictionary.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
data (dict[str, str | list[str] | bool]): Dictionary with layer data.
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
Layer: New instance of the class.
|
94
|
+
"""
|
95
|
+
return cls(**data) # type: ignore
|
96
|
+
|
97
|
+
def path(self, weights_directory: str) -> str:
|
98
|
+
"""Returns path to the first texture of the layer.
|
99
|
+
|
100
|
+
Arguments:
|
101
|
+
weights_directory (str): Path to the directory with weights.
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
str: Path to the texture.
|
105
|
+
"""
|
106
|
+
idx = "01" if self.count > 0 else ""
|
107
|
+
weight_postfix = "_weight" if not self.exclude_weight else ""
|
108
|
+
return os.path.join(weights_directory, f"{self.name}{idx}{weight_postfix}.png")
|
109
|
+
|
110
|
+
def preprocess(self) -> None:
|
111
|
+
if not os.path.isfile(self.game.texture_schema):
|
112
|
+
raise FileNotFoundError(f"Texture layers schema not found: {self.game.texture_schema}")
|
113
|
+
|
114
|
+
try:
|
115
|
+
with open(self.game.texture_schema, "r", encoding="utf-8") as f:
|
116
|
+
layers_schema = json.load(f)
|
117
|
+
except json.JSONDecodeError as e:
|
118
|
+
raise ValueError(f"Error loading texture layers schema: {e}") from e
|
119
|
+
|
120
|
+
self.layers = [self.Layer.from_json(layer) for layer in layers_schema]
|
121
|
+
self.logger.info("Loaded %s layers.", len(self.layers))
|
122
|
+
|
123
|
+
self._weights_dir = self.game.weights_dir_path(self.map_directory)
|
124
|
+
self.logger.debug("Weights directory: %s.", self._weights_dir)
|
125
|
+
self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
|
126
|
+
self.logger.debug("Generation info save path: %s.", self.info_save_path)
|
127
|
+
|
128
|
+
def process(self):
|
129
|
+
self._prepare_weights()
|
130
|
+
self._read_parameters()
|
131
|
+
self.draw()
|
132
|
+
self.info_sequence()
|
133
|
+
|
134
|
+
# pylint: disable=W0201
|
135
|
+
def _read_parameters(self) -> None:
|
136
|
+
"""Reads map parameters from OSM data, such as:
|
137
|
+
- minimum and maximum coordinates in UTM format
|
138
|
+
- map dimensions in meters
|
139
|
+
- map coefficients (meters per pixel)
|
140
|
+
"""
|
141
|
+
north, south, east, west = self.get_bbox(project_utm=True)
|
142
|
+
|
143
|
+
# Parameters of the map in UTM format (meters).
|
144
|
+
self.minimum_x = min(west, east)
|
145
|
+
self.minimum_y = min(south, north)
|
146
|
+
self.maximum_x = max(west, east)
|
147
|
+
self.maximum_y = max(south, north)
|
148
|
+
self.logger.debug("Map minimum coordinates (XxY): %s x %s.", self.minimum_x, self.minimum_y)
|
149
|
+
self.logger.debug("Map maximum coordinates (XxY): %s x %s.", self.maximum_x, self.maximum_y)
|
150
|
+
|
151
|
+
self.height = abs(north - south)
|
152
|
+
self.width = abs(east - west)
|
153
|
+
self.logger.info("Map dimensions (HxW): %s x %s.", self.height, self.width)
|
154
|
+
|
155
|
+
self.height_coef = self.height / self.map_height
|
156
|
+
self.width_coef = self.width / self.map_width
|
157
|
+
self.logger.debug("Map coefficients (HxW): %s x %s.", self.height_coef, self.width_coef)
|
158
|
+
|
159
|
+
def info_sequence(self) -> None:
|
160
|
+
"""Saves generation info to JSON file "generation_info.json".
|
161
|
+
|
162
|
+
Info sequence contains following attributes:
|
163
|
+
- coordinates
|
164
|
+
- bbox
|
165
|
+
- map_height
|
166
|
+
- map_width
|
167
|
+
- minimum_x
|
168
|
+
- minimum_y
|
169
|
+
- maximum_x
|
170
|
+
- maximum_y
|
171
|
+
- height
|
172
|
+
- width
|
173
|
+
- height_coef
|
174
|
+
- width_coef
|
175
|
+
"""
|
176
|
+
useful_attributes = [
|
177
|
+
"coordinates",
|
178
|
+
"bbox",
|
179
|
+
"map_height",
|
180
|
+
"map_width",
|
181
|
+
"minimum_x",
|
182
|
+
"minimum_y",
|
183
|
+
"maximum_x",
|
184
|
+
"maximum_y",
|
185
|
+
"height",
|
186
|
+
"width",
|
187
|
+
"height_coef",
|
188
|
+
"width_coef",
|
189
|
+
]
|
190
|
+
info_sequence = {attr: getattr(self, attr, None) for attr in useful_attributes}
|
191
|
+
|
192
|
+
with open(self.info_save_path, "w") as f: # pylint: disable=W1514
|
193
|
+
json.dump(info_sequence, f, indent=4)
|
194
|
+
self.logger.info("Generation info saved to %s.", self.info_save_path)
|
195
|
+
|
196
|
+
def _prepare_weights(self):
|
197
|
+
self.logger.debug("Starting preparing weights from %s layers.", len(self.layers))
|
198
|
+
|
199
|
+
for layer in self.layers:
|
200
|
+
self._generate_weights(layer)
|
201
|
+
self.logger.debug("Prepared weights for %s layers.", len(self.layers))
|
202
|
+
|
203
|
+
def _generate_weights(self, layer: Layer) -> None:
|
204
|
+
"""Generates weight files for textures. Each file is a numpy array of zeros and
|
205
|
+
dtype uint8 (0-255).
|
206
|
+
|
207
|
+
Args:
|
208
|
+
layer (Layer): Layer with textures and tags.
|
209
|
+
"""
|
210
|
+
size = (self.map_height, self.map_width)
|
211
|
+
postfix = "_weight.png" if not layer.exclude_weight else ".png"
|
212
|
+
if layer.count == 0:
|
213
|
+
filepaths = [os.path.join(self._weights_dir, layer.name + postfix)]
|
214
|
+
else:
|
215
|
+
filepaths = [
|
216
|
+
os.path.join(self._weights_dir, layer.name + str(i).zfill(2) + postfix)
|
217
|
+
for i in range(1, layer.count + 1)
|
218
|
+
]
|
219
|
+
|
220
|
+
for filepath in filepaths:
|
221
|
+
img = np.zeros(size, dtype=np.uint8)
|
222
|
+
cv2.imwrite(filepath, img) # pylint: disable=no-member
|
223
|
+
|
224
|
+
@property
|
225
|
+
def layers(self) -> list[Layer]:
|
226
|
+
"""Returns list of layers with textures and tags from textures.json.
|
227
|
+
|
228
|
+
Returns:
|
229
|
+
list[Layer]: List of layers.
|
230
|
+
"""
|
231
|
+
return self._layers
|
232
|
+
|
233
|
+
@layers.setter
|
234
|
+
def layers(self, layers: list[Layer]) -> None:
|
235
|
+
"""Sets list of layers with textures and tags.
|
236
|
+
|
237
|
+
Args:
|
238
|
+
layers (list[Layer]): List of layers.
|
239
|
+
"""
|
240
|
+
self._layers = layers
|
241
|
+
|
242
|
+
# pylint: disable=no-member
|
243
|
+
def draw(self) -> None:
|
244
|
+
"""Iterates over layers and fills them with polygons from OSM data."""
|
245
|
+
for layer in self.layers:
|
246
|
+
if not layer.tags:
|
247
|
+
self.logger.debug("Layer %s has no tags, there's nothing to draw.", layer.name)
|
248
|
+
continue
|
249
|
+
layer_path = layer.path(self._weights_dir)
|
250
|
+
self.logger.debug("Drawing layer %s.", layer_path)
|
251
|
+
|
252
|
+
img = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
|
253
|
+
for polygon in self.polygons(layer.tags, layer.width): # type: ignore
|
254
|
+
cv2.fillPoly(img, [polygon], color=255) # type: ignore
|
255
|
+
cv2.imwrite(layer_path, img)
|
256
|
+
self.logger.debug("Texture %s saved.", layer_path)
|
257
|
+
|
258
|
+
def get_relative_x(self, x: float) -> int:
|
259
|
+
"""Converts UTM X coordinate to relative X coordinate in map image.
|
260
|
+
|
261
|
+
Args:
|
262
|
+
x (float): UTM X coordinate.
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
int: Relative X coordinate in map image.
|
266
|
+
"""
|
267
|
+
raw_x = x - self.minimum_x
|
268
|
+
return int(raw_x * self.height_coef)
|
269
|
+
|
270
|
+
def get_relative_y(self, y: float) -> int:
|
271
|
+
"""Converts UTM Y coordinate to relative Y coordinate in map image.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
y (float): UTM Y coordinate.
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
int: Relative Y coordinate in map image.
|
278
|
+
"""
|
279
|
+
raw_y = y - self.minimum_y
|
280
|
+
return self.height - int(raw_y * self.width_coef)
|
281
|
+
|
282
|
+
# pylint: disable=W0613
|
283
|
+
def _to_np(self, geometry: shapely.geometry.polygon.Polygon, *args) -> np.ndarray:
|
284
|
+
"""Converts Polygon geometry to numpy array of polygon points.
|
285
|
+
|
286
|
+
Args:
|
287
|
+
geometry (shapely.geometry.polygon.Polygon): Polygon geometry.
|
288
|
+
*args: Additional arguments:
|
289
|
+
- width (int | None): Width of the polygon in meters.
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
np.ndarray: Numpy array of polygon points.
|
293
|
+
"""
|
294
|
+
xs, ys = geometry.exterior.coords.xy
|
295
|
+
xs = [int(self.get_relative_x(x)) for x in xs.tolist()]
|
296
|
+
ys = [int(self.get_relative_y(y)) for y in ys.tolist()]
|
297
|
+
pairs = list(zip(xs, ys))
|
298
|
+
return np.array(pairs, dtype=np.int32).reshape((-1, 1, 2))
|
299
|
+
|
300
|
+
def _to_polygon(self, obj: pd.core.series.Series, width: int | None) -> np.ndarray | None:
|
301
|
+
"""Converts OSM object to numpy array of polygon points.
|
302
|
+
|
303
|
+
Args:
|
304
|
+
obj (pd.core.series.Series): OSM object.
|
305
|
+
width (int | None): Width of the polygon in meters.
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
np.ndarray | None: Numpy array of polygon points.
|
309
|
+
"""
|
310
|
+
geometry = obj["geometry"]
|
311
|
+
geometry_type = geometry.geom_type
|
312
|
+
converter = self._converters(geometry_type)
|
313
|
+
if not converter:
|
314
|
+
self.logger.warning("Geometry type %s not supported.", geometry_type)
|
315
|
+
return None
|
316
|
+
return converter(geometry, width)
|
317
|
+
|
318
|
+
def _sequence(
|
319
|
+
self,
|
320
|
+
geometry: shapely.geometry.linestring.LineString | shapely.geometry.point.Point,
|
321
|
+
width: int | None,
|
322
|
+
) -> np.ndarray:
|
323
|
+
"""Converts LineString or Point geometry to numpy array of polygon points.
|
324
|
+
|
325
|
+
Args:
|
326
|
+
geometry (shapely.geometry.linestring.LineString | shapely.geometry.point.Point):
|
327
|
+
LineString or Point geometry.
|
328
|
+
width (int | None): Width of the polygon in meters.
|
329
|
+
|
330
|
+
Returns:
|
331
|
+
np.ndarray: Numpy array of polygon points.
|
332
|
+
"""
|
333
|
+
polygon = geometry.buffer(width)
|
334
|
+
return self._to_np(polygon)
|
335
|
+
|
336
|
+
def _converters(
|
337
|
+
self, geom_type: str
|
338
|
+
) -> Optional[Callable[[BaseGeometry, Optional[int]], np.ndarray]]:
|
339
|
+
"""Returns a converter function for a given geometry type.
|
340
|
+
|
341
|
+
Args:
|
342
|
+
geom_type (str): Geometry type.
|
343
|
+
|
344
|
+
Returns:
|
345
|
+
Callable[[shapely.geometry, int | None], np.ndarray]: Converter function.
|
346
|
+
"""
|
347
|
+
converters = {"Polygon": self._to_np, "LineString": self._sequence, "Point": self._sequence}
|
348
|
+
return converters.get(geom_type) # type: ignore
|
349
|
+
|
350
|
+
def polygons(
|
351
|
+
self, tags: dict[str, str | list[str] | bool], width: int | None
|
352
|
+
) -> Generator[np.ndarray, None, None]:
|
353
|
+
"""Generator which yields numpy arrays of polygons from OSM data.
|
354
|
+
|
355
|
+
Args:
|
356
|
+
tags (dict[str, str | list[str]]): Dictionary of tags to search for.
|
357
|
+
width (int | None): Width of the polygon in meters (only for LineString).
|
358
|
+
|
359
|
+
Yields:
|
360
|
+
Generator[np.ndarray, None, None]: Numpy array of polygon points.
|
361
|
+
"""
|
362
|
+
try:
|
363
|
+
with warnings.catch_warnings():
|
364
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
365
|
+
objects = ox.features_from_bbox(bbox=self.bbox, tags=tags)
|
366
|
+
except Exception as e: # pylint: disable=W0718
|
367
|
+
self.logger.warning("Error fetching objects for tags: %s.", tags)
|
368
|
+
self.logger.warning(e)
|
369
|
+
return
|
370
|
+
objects_utm = ox.project_gdf(objects, to_latlong=False)
|
371
|
+
self.logger.debug("Fetched %s elements for tags: %s.", len(objects_utm), tags)
|
372
|
+
|
373
|
+
for _, obj in objects_utm.iterrows():
|
374
|
+
polygon = self._to_polygon(obj, width)
|
375
|
+
if polygon is None:
|
376
|
+
continue
|
377
|
+
yield polygon
|
378
|
+
|
379
|
+
def previews(self) -> list[str]:
|
380
|
+
"""Invokes methods to generate previews. Returns list of paths to previews.
|
381
|
+
|
382
|
+
Returns:
|
383
|
+
list[str]: List of paths to previews.
|
384
|
+
"""
|
385
|
+
preview_paths = []
|
386
|
+
preview_paths.append(self._osm_preview())
|
387
|
+
return preview_paths
|
388
|
+
|
389
|
+
# pylint: disable=no-member
|
390
|
+
def _osm_preview(self) -> str:
|
391
|
+
"""Merges layers into one image and saves it into the png file.
|
392
|
+
|
393
|
+
Returns:
|
394
|
+
str: Path to the preview.
|
395
|
+
"""
|
396
|
+
scaling_factor = min(
|
397
|
+
PREVIEW_MAXIMUM_SIZE / self.map_width, PREVIEW_MAXIMUM_SIZE / self.map_height
|
398
|
+
)
|
399
|
+
|
400
|
+
preview_size = (
|
401
|
+
int(self.map_width * scaling_factor),
|
402
|
+
int(self.map_height * scaling_factor),
|
403
|
+
)
|
404
|
+
self.logger.debug(
|
405
|
+
"Scaling factor: %s. Preview size: %s.",
|
406
|
+
scaling_factor,
|
407
|
+
preview_size,
|
408
|
+
)
|
409
|
+
|
410
|
+
active_layers = [layer for layer in self.layers if layer.tags is not None]
|
411
|
+
self.logger.debug("Following layers have tag textures: %s.", len(active_layers))
|
412
|
+
|
413
|
+
images = [
|
414
|
+
cv2.resize(
|
415
|
+
cv2.imread(layer.path(self._weights_dir), cv2.IMREAD_UNCHANGED), preview_size
|
416
|
+
)
|
417
|
+
for layer in active_layers
|
418
|
+
]
|
419
|
+
colors = [layer.color for layer in active_layers]
|
420
|
+
color_images = []
|
421
|
+
for img, color in zip(images, colors):
|
422
|
+
color_img = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)
|
423
|
+
color_img[img > 0] = color
|
424
|
+
color_images.append(color_img)
|
425
|
+
merged = np.sum(color_images, axis=0, dtype=np.uint8)
|
426
|
+
self.logger.debug(
|
427
|
+
"Merged layers into one image. Shape: %s, dtype: %s.",
|
428
|
+
merged.shape,
|
429
|
+
merged.dtype,
|
430
|
+
)
|
431
|
+
preview_path = os.path.join(self.map_directory, "preview_osm.png")
|
432
|
+
cv2.imwrite(preview_path, merged) # pylint: disable=no-member
|
433
|
+
self.logger.info("Preview saved to %s.", preview_path)
|
434
|
+
return preview_path
|
maps4fs/logger.py
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
"""This module contains the Logger class for logging to the file and stdout."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
from datetime import datetime
|
7
|
+
from typing import Literal
|
8
|
+
|
9
|
+
log_directory = os.path.join(os.getcwd(), "logs")
|
10
|
+
os.makedirs(log_directory, exist_ok=True)
|
11
|
+
|
12
|
+
|
13
|
+
class Logger(logging.Logger):
|
14
|
+
"""Handles logging to the file and stroudt with timestamps."""
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
name: str,
|
19
|
+
level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "ERROR",
|
20
|
+
to_stdout: bool = True,
|
21
|
+
to_file: bool = True,
|
22
|
+
):
|
23
|
+
super().__init__(name)
|
24
|
+
self.setLevel(level)
|
25
|
+
self.stdout_handler = logging.StreamHandler(sys.stdout)
|
26
|
+
self.file_handler = logging.FileHandler(
|
27
|
+
filename=self.log_file(), mode="a", encoding="utf-8"
|
28
|
+
)
|
29
|
+
formatter = "%(name)s | %(levelname)s | %(asctime)s | %(message)s"
|
30
|
+
self.fmt = formatter
|
31
|
+
self.stdout_handler.setFormatter(logging.Formatter(formatter))
|
32
|
+
self.file_handler.setFormatter(logging.Formatter(formatter))
|
33
|
+
if to_stdout:
|
34
|
+
self.addHandler(self.stdout_handler)
|
35
|
+
if to_file:
|
36
|
+
self.addHandler(self.file_handler)
|
37
|
+
|
38
|
+
def log_file(self) -> str:
|
39
|
+
"""Returns the path to the log file.
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
str: The path to the log file.
|
43
|
+
"""
|
44
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
45
|
+
log_file = os.path.join(log_directory, f"{today}.txt")
|
46
|
+
return log_file
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright © [2024] [iwatkot]
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|