maps4fs 0.7.9__py3-none-any.whl → 0.8.3__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.
@@ -0,0 +1,89 @@
1
+ """This module contains the Config class for map settings and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from xml.etree import ElementTree as ET
7
+
8
+ from maps4fs.generator.component import Component
9
+
10
+ DEFAULT_HEIGHT_SCALE = 2000
11
+ DEFAULT_MAX_LOD_DISTANCE = 10000
12
+ DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
13
+
14
+
15
+ # pylint: disable=R0903
16
+ class I3d(Component):
17
+ """Component for map i3d file settings and configuration.
18
+
19
+ Args:
20
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
21
+ map_height (int): The height of the map in pixels.
22
+ map_width (int): The width of the map in pixels.
23
+ map_directory (str): The directory where the map files are stored.
24
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
25
+ info, warning. If not provided, default logging will be used.
26
+ """
27
+
28
+ _map_i3d_path: str | None = None
29
+
30
+ def preprocess(self) -> None:
31
+ """Gets the path to the map I3D file from the game instance and saves it to the instance
32
+ attribute. If the game does not support I3D files, the attribute is set to None."""
33
+ try:
34
+ self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
35
+ self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
36
+ except NotImplementedError:
37
+ self.logger.info("I3D file processing is not implemented for this game.")
38
+ self._map_i3d_path = None
39
+
40
+ def process(self) -> None:
41
+ """Updates the map I3D file with the default settings."""
42
+ self._update_i3d_file()
43
+
44
+ def _update_i3d_file(self) -> None:
45
+ """Updates the map I3D file with the default settings."""
46
+ if not self._map_i3d_path:
47
+ self.logger.info("I3D is not obtained, skipping the update.")
48
+ return
49
+ if not os.path.isfile(self._map_i3d_path):
50
+ self.logger.warning("I3D file not found: %s.", self._map_i3d_path)
51
+ return
52
+
53
+ tree = ET.parse(self._map_i3d_path)
54
+
55
+ self.logger.debug("Map I3D file loaded from: %s.", self._map_i3d_path)
56
+
57
+ root = tree.getroot()
58
+ for map_elem in root.iter("Scene"):
59
+ for terrain_elem in map_elem.iter("TerrainTransformGroup"):
60
+ terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
61
+ self.logger.debug(
62
+ "heightScale attribute set to %s in TerrainTransformGroup element.",
63
+ DEFAULT_HEIGHT_SCALE,
64
+ )
65
+ terrain_elem.set("maxLODDistance", str(DEFAULT_MAX_LOD_DISTANCE))
66
+ self.logger.debug(
67
+ "maxLODDistance attribute set to %s in TerrainTransformGroup element.",
68
+ DEFAULT_MAX_LOD_DISTANCE,
69
+ )
70
+
71
+ terrain_elem.set("occMaxLODDistance", str(DEFAULT_MAX_LOD_OCCLUDER_DISTANCE))
72
+ self.logger.debug(
73
+ "occMaxLODDistance attribute set to %s in TerrainTransformGroup element.",
74
+ DEFAULT_MAX_LOD_OCCLUDER_DISTANCE,
75
+ )
76
+
77
+ self.logger.debug("TerrainTransformGroup element updated in I3D file.")
78
+
79
+ tree.write(self._map_i3d_path)
80
+ self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
81
+
82
+ def previews(self) -> list[str]:
83
+ """Returns a list of paths to the preview images (empty list).
84
+ The component does not generate any preview images so it returns an empty list.
85
+
86
+ Returns:
87
+ list[str]: An empty list.
88
+ """
89
+ return []
maps4fs/generator/map.py CHANGED
@@ -1,11 +1,12 @@
1
1
  """This module contains Map class, which is used to generate map using all components."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import os
4
6
  import shutil
5
- from typing import Any
6
-
7
- from tqdm import tqdm
7
+ from typing import Any, Generator
8
8
 
9
+ # from maps4fs.generator.background import Background
9
10
  from maps4fs.generator.component import Component
10
11
  from maps4fs.generator.game import Game
11
12
  from maps4fs.logger import Logger
@@ -47,6 +48,7 @@ class Map:
47
48
  self.logger.debug("Game was set to %s", game.code)
48
49
 
49
50
  self.kwargs = kwargs
51
+ self.logger.debug("Additional arguments: %s", kwargs)
50
52
 
51
53
  os.makedirs(self.map_directory, exist_ok=True)
52
54
  self.logger.debug("Map directory created: %s", self.map_directory)
@@ -57,31 +59,45 @@ class Map:
57
59
  except Exception as e:
58
60
  raise RuntimeError(f"Can not unpack map template due to error: {e}") from e
59
61
 
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,
62
+ def generate(self) -> Generator[str, None, None]:
63
+ """Launch map generation using all components. Yield component names during the process.
64
+
65
+ Yields:
66
+ Generator[str, None, None]: Component names.
67
+ """
68
+ for game_component in self.game.components:
69
+ component = game_component(
70
+ self.game,
71
+ self.coordinates,
72
+ self.height,
73
+ self.width,
74
+ self.map_directory,
75
+ self.logger,
76
+ **self.kwargs,
77
+ )
78
+
79
+ yield component.__class__.__name__
80
+
81
+ try:
82
+ component.process()
83
+ except Exception as e: # pylint: disable=W0718
84
+ self.logger.error(
85
+ "Error processing component %s: %s",
86
+ component.__class__.__name__,
87
+ e,
88
+ )
89
+ raise e
90
+
91
+ try:
92
+ component.commit_generation_info()
93
+ except Exception as e: # pylint: disable=W0718
94
+ self.logger.error(
95
+ "Error committing generation info for component %s: %s",
96
+ component.__class__.__name__,
97
+ e,
72
98
  )
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)
99
+ raise e
100
+ self.components.append(component)
85
101
 
86
102
  def previews(self) -> list[str]:
87
103
  """Get list of preview images.
@@ -0,0 +1,72 @@
1
+ """This module contains functions and clas for generating path steps."""
2
+
3
+ from typing import NamedTuple
4
+
5
+ from geopy.distance import distance # type: ignore
6
+
7
+ DEFAULT_DISTANCE = 2048
8
+
9
+
10
+ class PathStep(NamedTuple):
11
+ """Represents parameters of one step in the path.
12
+
13
+ Attributes:
14
+ code {str} -- Tile code (N, NE, E, SE, S, SW, W, NW).
15
+ angle {int} -- Angle in degrees (for example 0 for North, 90 for East).
16
+ distance {int} -- Distance in meters from previous step.
17
+ size {tuple[int, int]} -- Size of the tile in pixels (width, height).
18
+ """
19
+
20
+ code: str
21
+ angle: int
22
+ distance: int
23
+ size: tuple[int, int]
24
+
25
+ def get_destination(self, origin: tuple[float, float]) -> tuple[float, float]:
26
+ """Calculate destination coordinates based on origin and step parameters.
27
+
28
+ Arguments:
29
+ origin {tuple[float, float]} -- Origin coordinates (latitude, longitude)
30
+
31
+ Returns:
32
+ tuple[float, float] -- Destination coordinates (latitude, longitude)
33
+ """
34
+ destination = distance(meters=self.distance).destination(origin, self.angle)
35
+ return destination.latitude, destination.longitude
36
+
37
+
38
+ def get_steps(map_height: int, map_width: int) -> list[PathStep]:
39
+ """Return a list of PathStep objects for each tile, which represent a step in the path.
40
+ Moving from the center of the map to North, then clockwise.
41
+
42
+ Arguments:
43
+ map_height {int} -- Height of the map in pixels
44
+ map_width {int} -- Width of the map in pixels
45
+
46
+ Returns:
47
+ list[PathStep] -- List of PathStep objects
48
+ """
49
+ # Move clockwise from N and calculate coordinates and sizes for each tile.
50
+ half_width = int(map_width / 2)
51
+ half_height = int(map_height / 2)
52
+
53
+ half_default_distance = int(DEFAULT_DISTANCE / 2)
54
+
55
+ return [
56
+ PathStep("N", 0, half_height + half_default_distance, (map_width, DEFAULT_DISTANCE)),
57
+ PathStep(
58
+ "NE", 90, half_width + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
59
+ ),
60
+ PathStep("E", 180, half_height + half_default_distance, (DEFAULT_DISTANCE, map_height)),
61
+ PathStep(
62
+ "SE", 180, half_height + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
63
+ ),
64
+ PathStep("S", 270, half_width + half_default_distance, (map_width, DEFAULT_DISTANCE)),
65
+ PathStep(
66
+ "SW", 270, half_width + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
67
+ ),
68
+ PathStep("W", 0, half_height + half_default_distance, (DEFAULT_DISTANCE, map_height)),
69
+ PathStep(
70
+ "NW", 0, half_height + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
71
+ ),
72
+ ]
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import warnings
8
- from typing import Callable, Generator, Optional
8
+ from typing import Any, Callable, Generator, Optional
9
9
 
10
10
  import cv2
11
11
  import numpy as np
@@ -57,6 +57,7 @@ class Texture(Component):
57
57
  width: int | None = None,
58
58
  color: tuple[int, int, int] | list[int] | None = None,
59
59
  exclude_weight: bool = False,
60
+ priority: int | None = None,
60
61
  ):
61
62
  self.name = name
62
63
  self.count = count
@@ -64,6 +65,7 @@ class Texture(Component):
64
65
  self.width = width
65
66
  self.color = color if color else (255, 255, 255)
66
67
  self.exclude_weight = exclude_weight
68
+ self.priority = priority
67
69
 
68
70
  def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
69
71
  """Returns dictionary with layer data.
@@ -77,6 +79,7 @@ class Texture(Component):
77
79
  "width": self.width,
78
80
  "color": list(self.color),
79
81
  "exclude_weight": self.exclude_weight,
82
+ "priority": self.priority,
80
83
  }
81
84
 
82
85
  data = {k: v for k, v in data.items() if v is not None}
@@ -120,16 +123,32 @@ class Texture(Component):
120
123
  self.layers = [self.Layer.from_json(layer) for layer in layers_schema]
121
124
  self.logger.info("Loaded %s layers.", len(self.layers))
122
125
 
126
+ base_layer = self.get_base_layer()
127
+ if base_layer:
128
+ self.logger.info("Base layer found: %s.", base_layer.name)
129
+ else:
130
+ self.logger.warning("No base layer found.")
131
+
123
132
  self._weights_dir = self.game.weights_dir_path(self.map_directory)
124
133
  self.logger.debug("Weights directory: %s.", self._weights_dir)
125
134
  self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
126
135
  self.logger.debug("Generation info save path: %s.", self.info_save_path)
127
136
 
137
+ def get_base_layer(self) -> Layer | None:
138
+ """Returns base layer.
139
+
140
+ Returns:
141
+ Layer | None: Base layer.
142
+ """
143
+ for layer in self.layers:
144
+ if layer.priority == 0:
145
+ return layer
146
+ return None
147
+
128
148
  def process(self):
129
149
  self._prepare_weights()
130
150
  self._read_parameters()
131
151
  self.draw()
132
- self.info_sequence()
133
152
 
134
153
  # pylint: disable=W0201
135
154
  def _read_parameters(self) -> None:
@@ -156,23 +175,8 @@ class Texture(Component):
156
175
  self.width_coef = self.width / self.map_width
157
176
  self.logger.debug("Map coefficients (HxW): %s x %s.", self.height_coef, self.width_coef)
158
177
 
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
- """
178
+ def info_sequence(self) -> dict[str, Any]:
179
+ """Returns the JSON representation of the generation info for textures."""
176
180
  useful_attributes = [
177
181
  "coordinates",
178
182
  "bbox",
@@ -187,11 +191,7 @@ class Texture(Component):
187
191
  "height_coef",
188
192
  "width_coef",
189
193
  ]
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)
194
+ return {attr: getattr(self, attr, None) for attr in useful_attributes}
195
195
 
196
196
  def _prepare_weights(self):
197
197
  self.logger.debug("Starting preparing weights from %s layers.", len(self.layers))
@@ -239,22 +239,79 @@ class Texture(Component):
239
239
  """
240
240
  self._layers = layers
241
241
 
242
+ def layers_by_priority(self) -> list[Layer]:
243
+ """Returns list of layers sorted by priority: None priority layers are first,
244
+ then layers are sorted by priority (descending).
245
+
246
+ Returns:
247
+ list[Layer]: List of layers sorted by priority.
248
+ """
249
+ return sorted(
250
+ self.layers,
251
+ key=lambda _layer: (
252
+ _layer.priority is not None,
253
+ -_layer.priority if _layer.priority is not None else float("inf"),
254
+ ),
255
+ )
256
+
242
257
  # pylint: disable=no-member
243
258
  def draw(self) -> None:
244
259
  """Iterates over layers and fills them with polygons from OSM data."""
245
- for layer in self.layers:
260
+ layers = self.layers_by_priority()
261
+
262
+ self.logger.debug(
263
+ "Sorted layers by priority: %s.", [(layer.name, layer.priority) for layer in layers]
264
+ )
265
+
266
+ cumulative_image = None
267
+
268
+ for layer in layers:
246
269
  if not layer.tags:
247
270
  self.logger.debug("Layer %s has no tags, there's nothing to draw.", layer.name)
248
271
  continue
272
+ if layer.priority == 0:
273
+ self.logger.debug(
274
+ "Found base layer %s. Postponing that to be the last layer drawn.", layer.name
275
+ )
276
+ continue
249
277
  layer_path = layer.path(self._weights_dir)
250
278
  self.logger.debug("Drawing layer %s.", layer_path)
279
+ layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
280
+
281
+ if cumulative_image is None:
282
+ self.logger.debug("First layer, creating new cumulative image.")
283
+ cumulative_image = layer_image
284
+
285
+ mask = cv2.bitwise_not(cumulative_image)
251
286
 
252
- img = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
253
287
  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)
288
+ cv2.fillPoly(layer_image, [polygon], color=255) # type: ignore
289
+
290
+ output_image = cv2.bitwise_and(layer_image, mask)
291
+
292
+ cumulative_image = cv2.bitwise_or(cumulative_image, output_image)
293
+
294
+ cv2.imwrite(layer_path, output_image)
256
295
  self.logger.debug("Texture %s saved.", layer_path)
257
296
 
297
+ if cumulative_image is not None:
298
+ self.draw_base_layer(cumulative_image)
299
+
300
+ def draw_base_layer(self, cumulative_image: np.ndarray) -> None:
301
+ """Draws base layer and saves it into the png file.
302
+ Base layer is the last layer to be drawn, it fills the remaining area of the map.
303
+
304
+ Args:
305
+ cumulative_image (np.ndarray): Cumulative image with all layers.
306
+ """
307
+ base_layer = self.get_base_layer()
308
+ if base_layer is not None:
309
+ layer_path = base_layer.path(self._weights_dir)
310
+ self.logger.debug("Drawing base layer %s.", layer_path)
311
+ img = cv2.bitwise_not(cumulative_image)
312
+ cv2.imwrite(layer_path, img)
313
+ self.logger.debug("Base texture %s saved.", layer_path)
314
+
258
315
  def get_relative_x(self, x: float) -> int:
259
316
  """Converts UTM X coordinate to relative X coordinate in map image.
260
317
 
@@ -428,7 +485,7 @@ class Texture(Component):
428
485
  merged.shape,
429
486
  merged.dtype,
430
487
  )
431
- preview_path = os.path.join(self.map_directory, "preview_osm.png")
488
+ preview_path = os.path.join(self.previews_directory, "textures_osm.png")
432
489
  cv2.imwrite(preview_path, merged) # pylint: disable=no-member
433
490
  self.logger.info("Preview saved to %s.", preview_path)
434
491
  return preview_path
@@ -0,0 +1,55 @@
1
+ """This module contains the Tile component, which is used to generate a tile of DEM data around
2
+ the map."""
3
+
4
+ import os
5
+
6
+ from maps4fs.generator.dem import DEM
7
+
8
+
9
+ class Tile(DEM):
10
+ """Component for creating a tile of DEM data around the map.
11
+
12
+ Arguments:
13
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
14
+ map_height (int): The height of the map in pixels.
15
+ map_width (int): The width of the map in pixels.
16
+ map_directory (str): The directory where the map files are stored.
17
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
18
+ info, warning. If not provided, default logging will be used.
19
+
20
+ Keyword Arguments:
21
+ tile_code (str): The code of the tile (N, NE, E, SE, S, SW, W, NW).
22
+
23
+ Public Methods:
24
+ get_output_resolution: Return the resolution of the output image.
25
+ process: Launch the component processing.
26
+ make_copy: Override the method to prevent copying the tile.
27
+ """
28
+
29
+ def preprocess(self) -> None:
30
+ """Prepares the component for processing. Reads the tile code from the kwargs and sets the
31
+ DEM path for the tile."""
32
+ super().preprocess()
33
+ self.code = self.kwargs.get("tile_code")
34
+ if not self.code:
35
+ raise ValueError("Tile code was not provided")
36
+
37
+ self.logger.debug(f"Generating tile {self.code}")
38
+
39
+ tiles_directory = os.path.join(self.map_directory, "objects", "tiles")
40
+ os.makedirs(tiles_directory, exist_ok=True)
41
+
42
+ self._dem_path = os.path.join(tiles_directory, f"{self.code}.png")
43
+ self.logger.debug(f"DEM path for tile {self.code} is {self._dem_path}")
44
+
45
+ def get_output_resolution(self) -> tuple[int, int]:
46
+ """Return the resolution of the output image.
47
+
48
+ Returns:
49
+ tuple[int, int]: The width and height of the output image.
50
+ """
51
+ return self.map_width, self.map_height
52
+
53
+ def make_copy(self, *args, **kwargs) -> None:
54
+ """Override the method to prevent copying the tile."""
55
+ pass # pylint: disable=unnecessary-pass