maps4fs 1.8.185__py3-none-any.whl → 1.8.187__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.
@@ -13,9 +13,9 @@ import numpy as np
13
13
 
14
14
  from maps4fs.generator.component.base.component_image import ImageComponent
15
15
  from maps4fs.generator.component.base.component_mesh import MeshComponent
16
+ from maps4fs.generator.component.texture import Texture
16
17
  from maps4fs.generator.dem import DEM
17
18
  from maps4fs.generator.settings import Parameters
18
- from maps4fs.generator.texture import Texture
19
19
 
20
20
 
21
21
  class Background(MeshComponent, ImageComponent):
@@ -116,3 +116,25 @@ class ImageComponent(Component):
116
116
  )
117
117
 
118
118
  cv2.imwrite(save_path, image)
119
+
120
+ @staticmethod
121
+ def transfer_border(src_image: np.ndarray, dst_image: np.ndarray | None, border: int) -> None:
122
+ """Transfers the border of the source image to the destination image.
123
+
124
+ Arguments:
125
+ src_image (np.ndarray): The source image.
126
+ dst_image (np.ndarray, optional): The destination image.
127
+ border (int): The border size.
128
+ """
129
+ borders = [
130
+ (slice(None, border), slice(None)),
131
+ (slice(None), slice(-border, None)),
132
+ (slice(-border, None), slice(None)),
133
+ (slice(None), slice(None, border)),
134
+ ]
135
+
136
+ for row_slice, col_slice in borders:
137
+ border_slice = (row_slice, col_slice)
138
+ if dst_image is not None:
139
+ dst_image[border_slice][src_image[border_slice] != 0] = 255
140
+ src_image[border_slice] = 0
@@ -0,0 +1,162 @@
1
+ """This module contains the class representing a layer with textures and tags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+
8
+
9
+ class Layer:
10
+ """Class which represents a layer with textures and tags.
11
+ It's using to obtain data from OSM using tags and make changes into corresponding textures.
12
+
13
+ Arguments:
14
+ name (str): Name of the layer.
15
+ tags (dict[str, str | list[str]]): Dictionary of tags to search for.
16
+ width (int | None): Width of the polygon in meters (only for LineString).
17
+ color (tuple[int, int, int]): Color of the layer in BGR format.
18
+ exclude_weight (bool): Flag to exclude weight from the texture.
19
+ priority (int | None): Priority of the layer.
20
+ info_layer (str | None): Name of the corresnponding info layer.
21
+ usage (str | None): Usage of the layer.
22
+ background (bool): Flag to determine if the layer is a background.
23
+ invisible (bool): Flag to determine if the layer is invisible.
24
+
25
+ Attributes:
26
+ name (str): Name of the layer.
27
+ tags (dict[str, str | list[str]]): Dictionary of tags to search for.
28
+ width (int | None): Width of the polygon in meters (only for LineString).
29
+ """
30
+
31
+ # pylint: disable=R0913
32
+ def __init__( # pylint: disable=R0917
33
+ self,
34
+ name: str,
35
+ count: int,
36
+ tags: dict[str, str | list[str] | bool] | None = None,
37
+ width: int | None = None,
38
+ color: tuple[int, int, int] | list[int] | None = None,
39
+ exclude_weight: bool = False,
40
+ priority: int | None = None,
41
+ info_layer: str | None = None,
42
+ usage: str | None = None,
43
+ background: bool = False,
44
+ invisible: bool = False,
45
+ procedural: list[str] | None = None,
46
+ border: int | None = None,
47
+ ):
48
+ self.name = name
49
+ self.count = count
50
+ self.tags = tags
51
+ self.width = width
52
+ self.color = color if color else (255, 255, 255)
53
+ self.exclude_weight = exclude_weight
54
+ self.priority = priority
55
+ self.info_layer = info_layer
56
+ self.usage = usage
57
+ self.background = background
58
+ self.invisible = invisible
59
+ self.procedural = procedural
60
+ self.border = border
61
+
62
+ def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
63
+ """Returns dictionary with layer data.
64
+
65
+ Returns:
66
+ dict: Dictionary with layer data."""
67
+ data = {
68
+ "name": self.name,
69
+ "count": self.count,
70
+ "tags": self.tags,
71
+ "width": self.width,
72
+ "color": list(self.color),
73
+ "exclude_weight": self.exclude_weight,
74
+ "priority": self.priority,
75
+ "info_layer": self.info_layer,
76
+ "usage": self.usage,
77
+ "background": self.background,
78
+ "invisible": self.invisible,
79
+ "procedural": self.procedural,
80
+ "border": self.border,
81
+ }
82
+
83
+ data = {k: v for k, v in data.items() if v is not None}
84
+ return data # type: ignore
85
+
86
+ @classmethod
87
+ def from_json(cls, data: dict[str, str | list[str] | bool]) -> Layer:
88
+ """Creates a new instance of the class from dictionary.
89
+
90
+ Arguments:
91
+ data (dict[str, str | list[str] | bool]): Dictionary with layer data.
92
+
93
+ Returns:
94
+ Layer: New instance of the class.
95
+ """
96
+ return cls(**data) # type: ignore
97
+
98
+ def path(self, weights_directory: str) -> str:
99
+ """Returns path to the first texture of the layer.
100
+
101
+ Arguments:
102
+ weights_directory (str): Path to the directory with weights.
103
+
104
+ Returns:
105
+ str: Path to the texture.
106
+ """
107
+ idx = "01" if self.count > 0 else ""
108
+ weight_postfix = "_weight" if not self.exclude_weight else ""
109
+ return os.path.join(weights_directory, f"{self.name}{idx}{weight_postfix}.png")
110
+
111
+ def path_preview(self, weights_directory: str) -> str:
112
+ """Returns path to the preview of the first texture of the layer.
113
+
114
+ Arguments:
115
+ weights_directory (str): Path to the directory with weights.
116
+
117
+ Returns:
118
+ str: Path to the preview.
119
+ """
120
+ return self.path(weights_directory).replace(".png", "_preview.png")
121
+
122
+ def get_preview_or_path(self, weights_directory: str) -> str:
123
+ """Returns path to the preview of the first texture of the layer if it exists,
124
+ otherwise returns path to the texture.
125
+
126
+ Arguments:
127
+ weights_directory (str): Path to the directory with weights.
128
+
129
+ Returns:
130
+ str: Path to the preview or texture.
131
+ """
132
+ preview_path = self.path_preview(weights_directory)
133
+ return preview_path if os.path.isfile(preview_path) else self.path(weights_directory)
134
+
135
+ def paths(self, weights_directory: str) -> list[str]:
136
+ """Returns a list of paths to the textures of the layer.
137
+ NOTE: Works only after the textures are generated, since it just lists the directory.
138
+
139
+ Arguments:
140
+ weights_directory (str): Path to the directory with weights.
141
+
142
+ Returns:
143
+ list[str]: List of paths to the textures.
144
+ """
145
+ weight_files = os.listdir(weights_directory)
146
+
147
+ # Inconsistent names are the name of textures that are not following the pattern
148
+ # of texture_name{idx}_weight.png.
149
+ inconsistent_names = ["forestRockRoot", "waterPuddle"]
150
+
151
+ if self.name in inconsistent_names:
152
+ return [
153
+ os.path.join(weights_directory, weight_file)
154
+ for weight_file in weight_files
155
+ if weight_file.startswith(self.name)
156
+ ]
157
+
158
+ return [
159
+ os.path.join(weights_directory, weight_file)
160
+ for weight_file in weight_files
161
+ if re.match(rf"{self.name}\d{{2}}_weight.png", weight_file)
162
+ ]
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import os
7
- import re
8
7
  import shutil
9
8
  import warnings
10
9
  from collections import defaultdict
@@ -14,18 +13,18 @@ import cv2
14
13
  import numpy as np
15
14
  import osmnx as ox
16
15
  import pandas as pd
17
- import shapely.geometry # type: ignore
18
16
  from osmnx import settings as ox_settings
19
- from shapely.geometry.base import BaseGeometry # type: ignore
17
+ from shapely import LineString, Point, Polygon
18
+ from shapely.geometry.base import BaseGeometry
20
19
  from tqdm import tqdm
21
20
 
22
- from maps4fs.generator.component.base.component import Component
21
+ from maps4fs.generator.component.base.component_image import ImageComponent
22
+ from maps4fs.generator.component.layer import Layer
23
23
 
24
24
  PREVIEW_MAXIMUM_SIZE = 2048
25
25
 
26
26
 
27
- # pylint: disable=R0902, R0904
28
- class Texture(Component):
27
+ class Texture(ImageComponent):
29
28
  """Class which generates textures for the map using OSM data.
30
29
 
31
30
  Attributes:
@@ -36,167 +35,43 @@ class Texture(Component):
36
35
  color (tuple[int, int, int]): Color of the layer in BGR format.
37
36
  """
38
37
 
39
- # pylint: disable=R0903
40
- class Layer:
41
- """Class which represents a layer with textures and tags.
42
- It's using to obtain data from OSM using tags and make changes into corresponding textures.
38
+ def preprocess(self) -> None:
39
+ """Preprocesses the data before the generation."""
40
+ self.read_layers(self.get_schema())
41
+
42
+ self._weights_dir = self.game.weights_dir_path(self.map_directory)
43
+ self.procedural_dir = os.path.join(self._weights_dir, "masks")
44
+ os.makedirs(self.procedural_dir, exist_ok=True)
45
+
46
+ self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
47
+ self.info_layer_path = os.path.join(self.info_layers_directory, "textures.json")
48
+
49
+ def read_layers(self, layers_schema: list[dict[str, Any]]) -> None:
50
+ """Reads layers from the schema.
43
51
 
44
52
  Arguments:
45
- name (str): Name of the layer.
46
- tags (dict[str, str | list[str]]): Dictionary of tags to search for.
47
- width (int | None): Width of the polygon in meters (only for LineString).
48
- color (tuple[int, int, int]): Color of the layer in BGR format.
49
- exclude_weight (bool): Flag to exclude weight from the texture.
50
- priority (int | None): Priority of the layer.
51
- info_layer (str | None): Name of the corresnponding info layer.
52
- usage (str | None): Usage of the layer.
53
- background (bool): Flag to determine if the layer is a background.
54
- invisible (bool): Flag to determine if the layer is invisible.
55
-
56
- Attributes:
57
- name (str): Name of the layer.
58
- tags (dict[str, str | list[str]]): Dictionary of tags to search for.
59
- width (int | None): Width of the polygon in meters (only for LineString).
53
+ layers_schema (list[dict[str, Any]]): Schema with layers for textures.
60
54
  """
55
+ try:
56
+ self.layers = [Layer.from_json(layer) for layer in layers_schema]
57
+ self.logger.debug("Loaded %s layers.", len(self.layers))
58
+ except Exception as e:
59
+ raise ValueError(f"Error loading texture layers: {e}") from e
61
60
 
62
- # pylint: disable=R0913
63
- def __init__( # pylint: disable=R0917
64
- self,
65
- name: str,
66
- count: int,
67
- tags: dict[str, str | list[str] | bool] | None = None,
68
- width: int | None = None,
69
- color: tuple[int, int, int] | list[int] | None = None,
70
- exclude_weight: bool = False,
71
- priority: int | None = None,
72
- info_layer: str | None = None,
73
- usage: str | None = None,
74
- background: bool = False,
75
- invisible: bool = False,
76
- procedural: list[str] | None = None,
77
- border: int | None = None,
78
- ):
79
- self.name = name
80
- self.count = count
81
- self.tags = tags
82
- self.width = width
83
- self.color = color if color else (255, 255, 255)
84
- self.exclude_weight = exclude_weight
85
- self.priority = priority
86
- self.info_layer = info_layer
87
- self.usage = usage
88
- self.background = background
89
- self.invisible = invisible
90
- self.procedural = procedural
91
- self.border = border
92
-
93
- def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
94
- """Returns dictionary with layer data.
95
-
96
- Returns:
97
- dict: Dictionary with layer data."""
98
- data = {
99
- "name": self.name,
100
- "count": self.count,
101
- "tags": self.tags,
102
- "width": self.width,
103
- "color": list(self.color),
104
- "exclude_weight": self.exclude_weight,
105
- "priority": self.priority,
106
- "info_layer": self.info_layer,
107
- "usage": self.usage,
108
- "background": self.background,
109
- "invisible": self.invisible,
110
- "procedural": self.procedural,
111
- "border": self.border,
112
- }
113
-
114
- data = {k: v for k, v in data.items() if v is not None}
115
- return data # type: ignore
116
-
117
- @classmethod
118
- def from_json(cls, data: dict[str, str | list[str] | bool]) -> Texture.Layer:
119
- """Creates a new instance of the class from dictionary.
120
-
121
- Arguments:
122
- data (dict[str, str | list[str] | bool]): Dictionary with layer data.
123
-
124
- Returns:
125
- Layer: New instance of the class.
126
- """
127
- return cls(**data) # type: ignore
128
-
129
- def path(self, weights_directory: str) -> str:
130
- """Returns path to the first texture of the layer.
131
-
132
- Arguments:
133
- weights_directory (str): Path to the directory with weights.
134
-
135
- Returns:
136
- str: Path to the texture.
137
- """
138
- idx = "01" if self.count > 0 else ""
139
- weight_postfix = "_weight" if not self.exclude_weight else ""
140
- return os.path.join(weights_directory, f"{self.name}{idx}{weight_postfix}.png")
141
-
142
- def path_preview(self, weights_directory: str) -> str:
143
- """Returns path to the preview of the first texture of the layer.
144
-
145
- Arguments:
146
- weights_directory (str): Path to the directory with weights.
147
-
148
- Returns:
149
- str: Path to the preview.
150
- """
151
- return self.path(weights_directory).replace(".png", "_preview.png")
152
-
153
- def get_preview_or_path(self, weights_directory: str) -> str:
154
- """Returns path to the preview of the first texture of the layer if it exists,
155
- otherwise returns path to the texture.
156
-
157
- Arguments:
158
- weights_directory (str): Path to the directory with weights.
159
-
160
- Returns:
161
- str: Path to the preview or texture.
162
- """
163
- preview_path = self.path_preview(weights_directory)
164
- return preview_path if os.path.isfile(preview_path) else self.path(weights_directory)
165
-
166
- def paths(self, weights_directory: str) -> list[str]:
167
- """Returns a list of paths to the textures of the layer.
168
- NOTE: Works only after the textures are generated, since it just lists the directory.
169
-
170
- Arguments:
171
- weights_directory (str): Path to the directory with weights.
172
-
173
- Returns:
174
- list[str]: List of paths to the textures.
175
- """
176
- weight_files = os.listdir(weights_directory)
177
-
178
- # Inconsistent names are the name of textures that are not following the pattern
179
- # of texture_name{idx}_weight.png.
180
- inconsistent_names = ["forestRockRoot", "waterPuddle"]
181
-
182
- if self.name in inconsistent_names:
183
- return [
184
- os.path.join(weights_directory, weight_file)
185
- for weight_file in weight_files
186
- if weight_file.startswith(self.name)
187
- ]
188
-
189
- return [
190
- os.path.join(weights_directory, weight_file)
191
- for weight_file in weight_files
192
- if re.match(rf"{self.name}\d{{2}}_weight.png", weight_file)
193
- ]
61
+ def get_schema(self) -> list[dict[str, Any]]:
62
+ """Returns schema with layers for textures.
194
63
 
195
- def preprocess(self) -> None:
196
- """Preprocesses the data before the generation."""
64
+ Raises:
65
+ FileNotFoundError: If the schema file is not found.
66
+ ValueError: If there is an error loading the schema.
67
+ ValueError: If the schema is not a list of dictionaries.
68
+
69
+ Returns:
70
+ dict[str, Any]: Schema with layers for textures.
71
+ """
197
72
  custom_schema = self.kwargs.get("texture_custom_schema")
198
73
  if custom_schema:
199
- layers_schema = custom_schema # type: ignore
74
+ layers_schema = custom_schema
200
75
  self.logger.debug("Custom schema loaded with %s layers.", len(layers_schema))
201
76
  else:
202
77
  if not os.path.isfile(self.game.texture_schema):
@@ -210,27 +85,10 @@ class Texture(Component):
210
85
  except json.JSONDecodeError as e:
211
86
  raise ValueError(f"Error loading texture layers schema: {e}") from e
212
87
 
213
- try:
214
- self.layers = [self.Layer.from_json(layer) for layer in layers_schema] # type: ignore
215
- self.logger.debug("Loaded %s layers.", len(self.layers))
216
- except Exception as e: # pylint: disable=W0703
217
- raise ValueError(f"Error loading texture layers: {e}") from e
218
-
219
- base_layer = self.get_base_layer()
220
- if base_layer:
221
- self.logger.debug("Base layer found: %s.", base_layer.name)
88
+ if not isinstance(layers_schema, list):
89
+ raise ValueError("Texture layers schema must be a list of dictionaries.")
222
90
 
223
- self._weights_dir = self.game.weights_dir_path(self.map_directory)
224
- self.logger.debug("Weights directory: %s.", self._weights_dir)
225
- self.procedural_dir = os.path.join(self._weights_dir, "masks")
226
- os.makedirs(self.procedural_dir, exist_ok=True)
227
- self.logger.debug("Procedural directory: %s.", self.procedural_dir)
228
-
229
- self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
230
- self.logger.debug("Generation info save path: %s.", self.info_save_path)
231
-
232
- self.info_layer_path = os.path.join(self.info_layers_directory, "textures.json")
233
- self.logger.debug("Info layer path: %s.", self.info_layer_path)
91
+ return layers_schema
234
92
 
235
93
  def get_base_layer(self) -> Layer | None:
236
94
  """Returns base layer.
@@ -272,9 +130,7 @@ class Texture(Component):
272
130
  self.draw()
273
131
  self.rotate_textures()
274
132
  self.add_borders()
275
- if self.map.texture_settings.dissolve and self.game.code != "FS22":
276
- # FS22 has textures splitted into 4 sublayers, which leads to a very
277
- # long processing time when dissolving them.
133
+ if self.map.texture_settings.dissolve and self.game.dissolve:
278
134
  self.dissolve()
279
135
  self.copy_procedural()
280
136
 
@@ -297,31 +153,16 @@ class Texture(Component):
297
153
  # And set it to 0 in the current layer image.
298
154
  layer_image = cv2.imread(layer.path(self._weights_dir), cv2.IMREAD_UNCHANGED)
299
155
  border = layer.border
300
- if border == 0:
156
+ if not border:
301
157
  continue
302
158
 
303
- top = layer_image[:border, :] # type: ignore
304
- right = layer_image[:, -border:] # type: ignore
305
- bottom = layer_image[-border:, :] # type: ignore
306
- left = layer_image[:, :border] # type: ignore
307
-
308
- if base_layer_image is not None:
309
- base_layer_image[:border, :][top != 0] = 255 # type: ignore
310
- base_layer_image[:, -border:][right != 0] = 255 # type: ignore
311
- base_layer_image[-border:, :][bottom != 0] = 255 # type: ignore
312
- base_layer_image[:, :border][left != 0] = 255 # type: ignore
313
-
314
- layer_image[:border, :] = 0 # type: ignore
315
- layer_image[:, -border:] = 0 # type: ignore
316
- layer_image[-border:, :] = 0 # type: ignore
317
- layer_image[:, :border] = 0 # type: ignore
159
+ self.transfer_border(layer_image, base_layer_image, border)
318
160
 
319
161
  cv2.imwrite(layer.path(self._weights_dir), layer_image)
320
162
  self.logger.debug("Borders added to layer %s.", layer.name)
321
163
 
322
164
  if base_layer_image is not None:
323
165
  cv2.imwrite(base_layer.path(self._weights_dir), base_layer_image) # type: ignore
324
- self.logger.debug("Borders added to base layer %s.", base_layer.name) # type: ignore
325
166
 
326
167
  def copy_procedural(self) -> None:
327
168
  """Copies some of the textures to use them as mask for procedural generation.
@@ -350,7 +191,6 @@ class Texture(Component):
350
191
  # If there are more than one texture, merge them.
351
192
  merged_texture = np.zeros((self.map_size, self.map_size), dtype=np.uint8)
352
193
  for texture_path in texture_paths:
353
- # pylint: disable=E1101
354
194
  texture = cv2.imread(texture_path, cv2.IMREAD_UNCHANGED)
355
195
  merged_texture[texture == 255] = 255
356
196
  cv2.imwrite(procedural_save_path, merged_texture)
@@ -390,12 +230,9 @@ class Texture(Component):
390
230
  "Skipping rotation of layer %s because it has no tags.", layer.name
391
231
  )
392
232
 
393
- # pylint: disable=W0201
394
233
  def _read_parameters(self) -> None:
395
234
  """Reads map parameters from OSM data, such as:
396
235
  - minimum and maximum coordinates
397
- - map dimensions in meters
398
- - map coefficients (meters per pixel)
399
236
  """
400
237
  bbox = ox.utils_geo.bbox_from_point(self.coordinates, dist=self.map_rotated_size / 2)
401
238
  self.minimum_x, self.minimum_y, self.maximum_x, self.maximum_y = bbox
@@ -480,20 +317,16 @@ class Texture(Component):
480
317
  ),
481
318
  )
482
319
 
483
- # pylint: disable = R0912
484
320
  def draw(self) -> None:
485
321
  """Iterates over layers and fills them with polygons from OSM data."""
486
322
  layers = self.layers_by_priority()
487
-
488
- self.logger.debug(
489
- "Sorted layers by priority: %s.", [(layer.name, layer.priority) for layer in layers]
490
- )
323
+ layers = [layer for layer in layers if layer.tags is not None]
491
324
 
492
325
  cumulative_image = None
493
326
 
494
327
  # Dictionary to store info layer data.
495
328
  # Key is a layer.info_layer, value is a list of polygon points as tuples (x, y).
496
- info_layer_data = defaultdict(list)
329
+ info_layer_data: dict[str, list[list[int]]] = defaultdict(list)
497
330
 
498
331
  for layer in tqdm(
499
332
  layers, desc="Drawing textures", unit="layer", disable=self.map.is_public
@@ -501,9 +334,6 @@ class Texture(Component):
501
334
  if self.map.texture_settings.skip_drains and layer.usage == "drain":
502
335
  self.logger.debug("Skipping layer %s because of the usage.", layer.name)
503
336
  continue
504
- if not layer.tags:
505
- self.logger.debug("Layer %s has no tags, there's nothing to draw.", layer.name)
506
- continue
507
337
  if layer.priority == 0:
508
338
  self.logger.debug(
509
339
  "Found base layer %s. Postponing that to be the last layer drawn.", layer.name
@@ -518,34 +348,10 @@ class Texture(Component):
518
348
  cumulative_image = layer_image
519
349
 
520
350
  mask = cv2.bitwise_not(cumulative_image)
521
-
522
- for polygon in self.objects_generator( # type: ignore
523
- layer.tags, layer.width, layer.info_layer
524
- ):
525
- if not len(polygon) > 2:
526
- self.logger.debug("Skipping polygon with less than 3 points.")
527
- continue
528
- if layer.info_layer:
529
- info_layer_data[layer.info_layer].append(
530
- self.np_to_polygon_points(polygon) # type: ignore
531
- )
532
- if not layer.invisible:
533
- try:
534
- cv2.fillPoly(layer_image, [polygon], color=255) # type: ignore
535
- except Exception as e: # pylint: disable=W0718
536
- self.logger.warning("Error drawing polygon: %s.", repr(e))
537
- continue
538
-
539
- if layer.info_layer == "roads":
540
- for linestring in self.objects_generator(
541
- layer.tags, layer.width, layer.info_layer, yield_linestrings=True
542
- ):
543
- info_layer_data[f"{layer.info_layer}_polylines"].append(
544
- linestring # type: ignore
545
- )
351
+ self._draw_layer(layer, info_layer_data, layer_image)
352
+ self._add_roads(layer, info_layer_data)
546
353
 
547
354
  output_image = cv2.bitwise_and(layer_image, mask)
548
-
549
355
  cumulative_image = cv2.bitwise_or(cumulative_image, output_image)
550
356
 
551
357
  cv2.imwrite(layer_path, output_image)
@@ -566,6 +372,44 @@ class Texture(Component):
566
372
  if cumulative_image is not None:
567
373
  self.draw_base_layer(cumulative_image)
568
374
 
375
+ def _draw_layer(
376
+ self, layer: Layer, info_layer_data: dict[str, list[list[int]]], layer_image: np.ndarray
377
+ ) -> None:
378
+ """Draws polygons from OSM data on the layer image and updates the info layer data.
379
+
380
+ Arguments:
381
+ layer (Layer): Layer with textures and tags.
382
+ info_layer_data (dict[list[list[int]]]): Dictionary to store info layer data.
383
+ layer_image (np.ndarray): Layer image.
384
+ """
385
+ for polygon in self.objects_generator(layer.tags, layer.width, layer.info_layer):
386
+ if not len(polygon) > 2:
387
+ self.logger.debug("Skipping polygon with less than 3 points.")
388
+ continue
389
+ if layer.info_layer:
390
+ info_layer_data[layer.info_layer].append(
391
+ self.np_to_polygon_points(polygon) # type: ignore
392
+ )
393
+ if not layer.invisible:
394
+ try:
395
+ cv2.fillPoly(layer_image, [polygon], color=255) # type: ignore
396
+ except Exception as e:
397
+ self.logger.warning("Error drawing polygon: %s.", repr(e))
398
+ continue
399
+
400
+ def _add_roads(self, layer: Layer, info_layer_data: dict[str, list[list[int]]]) -> None:
401
+ """Adds roads to the info layer data.
402
+
403
+ Arguments:
404
+ layer (Layer): Layer with textures and tags.
405
+ info_layer_data (dict[list[list[int]]]): Dictionary to store info layer data.
406
+ """
407
+ if layer.info_layer == "roads":
408
+ for linestring in self.objects_generator(
409
+ layer.tags, layer.width, layer.info_layer, yield_linestrings=True
410
+ ):
411
+ info_layer_data[f"{layer.info_layer}_polylines"].append(linestring) # type: ignore
412
+
569
413
  def dissolve(self) -> None:
570
414
  """Dissolves textures of the layers with tags into sublayers for them to look more
571
415
  natural in the game.
@@ -657,12 +501,11 @@ class Texture(Component):
657
501
  """
658
502
  return [(int(x), int(y)) for x, y in np_array.reshape(-1, 2)]
659
503
 
660
- # pylint: disable=W0613
661
- def _to_np(self, geometry: shapely.geometry.polygon.Polygon, *args) -> np.ndarray:
504
+ def _to_np(self, geometry: Polygon, *args) -> np.ndarray:
662
505
  """Converts Polygon geometry to numpy array of polygon points.
663
506
 
664
507
  Arguments:
665
- geometry (shapely.geometry.polygon.Polygon): Polygon geometry.
508
+ geometry (Polygon): Polygon geometry.
666
509
  *Arguments: Additional arguments:
667
510
  - width (int | None): Width of the polygon in meters.
668
511
 
@@ -670,24 +513,19 @@ class Texture(Component):
670
513
  np.ndarray: Numpy array of polygon points.
671
514
  """
672
515
  coords = list(geometry.exterior.coords)
673
- pts = np.array(
674
- [self.latlon_to_pixel(coord[1], coord[0]) for coord in coords],
675
- np.int32,
676
- )
516
+ pts = np.array(coords, np.int32)
677
517
  pts = pts.reshape((-1, 1, 2))
678
518
  return pts
679
519
 
680
- def _to_polygon(
681
- self, obj: pd.core.series.Series, width: int | None
682
- ) -> shapely.geometry.polygon.Polygon:
683
- """Converts OSM object to numpy array of polygon points.
520
+ def _to_polygon(self, obj: pd.core.series.Series, width: int | None) -> Polygon:
521
+ """Converts OSM object to numpy array of polygon points and converts coordinates to pixels.
684
522
 
685
523
  Arguments:
686
524
  obj (pd.core.series.Series): OSM object.
687
525
  width (int | None): Width of the polygon in meters.
688
526
 
689
527
  Returns:
690
- shapely.geometry.polygon.Polygon: Polygon geometry.
528
+ Polygon: Polygon geometry with pixel coordinates.
691
529
  """
692
530
  geometry = obj["geometry"]
693
531
  geometry_type = geometry.geom_type
@@ -697,47 +535,80 @@ class Texture(Component):
697
535
  return None
698
536
  return converter(geometry, width)
699
537
 
700
- def _sequence(
701
- self,
702
- geometry: shapely.geometry.linestring.LineString | shapely.geometry.point.Point,
703
- width: int | None,
704
- ) -> shapely.geometry.polygon.Polygon:
705
- """Converts LineString or Point geometry to numpy array of polygon points.
538
+ def polygon_to_pixel_coordinates(self, polygon: Polygon) -> Polygon:
539
+ """Converts polygon coordinates from lat lon to pixel coordinates.
706
540
 
707
541
  Arguments:
708
- geometry (shapely.geometry.linestring.LineString | shapely.geometry.point.Point):
709
- LineString or Point geometry.
710
- width (int | None): Width of the polygon in meters.
542
+ polygon (Polygon): Polygon geometry.
711
543
 
712
544
  Returns:
713
- shapely.geometry.polygon.Polygon: Polygon geometry.
545
+ Polygon: Polygon geometry.
714
546
  """
715
- polygon = geometry.buffer(self.meters_to_degrees(width) if width else 0)
716
- return polygon
547
+ coords_pixel = [
548
+ self.latlon_to_pixel(lat, lon) for lon, lat in list(polygon.exterior.coords)
549
+ ]
550
+ return Polygon(coords_pixel)
717
551
 
718
- def meters_to_degrees(self, meters: int) -> float:
719
- """Converts meters to degrees.
552
+ def linestring_to_pixel_coordinates(self, linestring: LineString) -> LineString:
553
+ """Converts LineString coordinates from lat lon to pixel coordinates.
720
554
 
721
555
  Arguments:
722
- meters (int): Meters.
556
+ linestring (LineString): LineString geometry.
723
557
 
724
558
  Returns:
725
- float: Degrees.
559
+ LineString: LineString geometry.
726
560
  """
727
- return meters / 111320
561
+ coords_pixel = [self.latlon_to_pixel(lat, lon) for lon, lat in list(linestring.coords)]
562
+ return LineString(coords_pixel)
728
563
 
729
- def _skip(
730
- self, geometry: shapely.geometry.polygon.Polygon, *args, **kwargs
731
- ) -> shapely.geometry.polygon.Polygon:
732
- """Returns the same geometry.
564
+ def point_to_pixel_coordinates(self, point: Point) -> Point:
565
+ """Converts Point coordinates from lat lon to pixel coordinates.
733
566
 
734
567
  Arguments:
735
- geometry (shapely.geometry.polygon.Polygon): Polygon geometry.
568
+ point (Point): Point geometry.
736
569
 
737
570
  Returns:
738
- shapely.geometry.polygon.Polygon: Polygon geometry.
571
+ Point: Point geometry.
739
572
  """
740
- return geometry
573
+ x, y = self.latlon_to_pixel(point.y, point.x)
574
+ return Point(x, y)
575
+
576
+ def _to_pixel(self, geometry: Polygon, *args, **kwargs) -> Polygon:
577
+ """Returns the same geometry with pixel coordinates.
578
+
579
+ Arguments:
580
+ geometry (Polygon): Polygon geometry.
581
+
582
+ Returns:
583
+ Polygon: Polygon geometry with pixel coordinates.
584
+ """
585
+ return self.polygon_to_pixel_coordinates(geometry)
586
+
587
+ def _sequence_to_pixel(
588
+ self,
589
+ geometry: LineString | Point,
590
+ width: int | None,
591
+ ) -> Polygon:
592
+ """Converts LineString or Point geometry to numpy array of polygon points.
593
+
594
+ Arguments:
595
+ geometry (LineString | Point): LineString or Point geometry.
596
+ width (int | None): Width of the polygon in meters.
597
+
598
+ Raises:
599
+ ValueError: If the geometry type is not supported
600
+
601
+ Returns:
602
+ Polygon: Polygon geometry.
603
+ """
604
+ if isinstance(geometry, LineString):
605
+ geometry = self.linestring_to_pixel_coordinates(geometry)
606
+ elif isinstance(geometry, Point):
607
+ geometry = self.point_to_pixel_coordinates(geometry)
608
+ else:
609
+ raise ValueError(f"Geometry type {type(geometry)} not supported.")
610
+
611
+ return geometry.buffer(width if width else 0)
741
612
 
742
613
  def _converters(
743
614
  self, geom_type: str
@@ -750,12 +621,16 @@ class Texture(Component):
750
621
  Returns:
751
622
  Callable[[shapely.geometry, int | None], np.ndarray]: Converter function.
752
623
  """
753
- converters = {"Polygon": self._skip, "LineString": self._sequence, "Point": self._sequence}
624
+ converters = {
625
+ "Polygon": self._to_pixel,
626
+ "LineString": self._sequence_to_pixel,
627
+ "Point": self._sequence_to_pixel,
628
+ }
754
629
  return converters.get(geom_type) # type: ignore
755
630
 
756
631
  def objects_generator(
757
632
  self,
758
- tags: dict[str, str | list[str] | bool],
633
+ tags: dict[str, str | list[str] | bool] | None,
759
634
  width: int | None,
760
635
  info_layer: str | None = None,
761
636
  yield_linestrings: bool = False,
@@ -772,6 +647,8 @@ class Texture(Component):
772
647
  Generator[np.ndarray, None, None] | Generator[list[tuple[int, int]], None, None]:
773
648
  Numpy array of polygon points or list of point coordinates.
774
649
  """
650
+ if tags is None:
651
+ return
775
652
  is_fieds = info_layer == "fields"
776
653
 
777
654
  ox_settings.use_cache = self.map.texture_settings.use_cache
@@ -784,7 +661,7 @@ class Texture(Component):
784
661
  objects = ox.features_from_xml(self.map.custom_osm, tags=tags)
785
662
  else:
786
663
  objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
787
- except Exception as e: # pylint: disable=W0718
664
+ except Exception as e:
788
665
  self.logger.debug("Error fetching objects for tags: %s. Error: %s.", tags, e)
789
666
  return
790
667
  self.logger.debug("Fetched %s elements for tags: %s.", len(objects), tags)
@@ -806,7 +683,7 @@ class Texture(Component):
806
683
  """
807
684
  for _, obj in objects.iterrows():
808
685
  geometry = obj["geometry"]
809
- if isinstance(geometry, shapely.geometry.linestring.LineString):
686
+ if isinstance(geometry, LineString):
810
687
  points = [self.latlon_to_pixel(x, y) for y, x in geometry.coords]
811
688
  yield points
812
689
 
@@ -826,20 +703,18 @@ class Texture(Component):
826
703
  for _, obj in objects.iterrows():
827
704
  try:
828
705
  polygon = self._to_polygon(obj, width)
829
- except Exception as e: # pylint: disable=W0703
706
+ except Exception as e:
830
707
  self.logger.warning("Error converting object to polygon: %s.", e)
831
708
  continue
832
709
  if polygon is None:
833
710
  continue
834
711
 
835
712
  if is_fieds and self.map.texture_settings.fields_padding > 0:
836
- padded_polygon = polygon.buffer(
837
- -self.meters_to_degrees(self.map.texture_settings.fields_padding)
838
- )
713
+ padded_polygon = polygon.buffer(-self.map.texture_settings.fields_padding)
839
714
 
840
- if not isinstance(padded_polygon, shapely.geometry.polygon.Polygon):
841
- self.logger.debug("The padding value is too high, field will not padded.")
842
- elif not list(padded_polygon.exterior.coords):
715
+ if not isinstance(padded_polygon, Polygon) or not list(
716
+ padded_polygon.exterior.coords
717
+ ):
843
718
  self.logger.debug("The padding value is too high, field will not padded.")
844
719
  else:
845
720
  polygon = padded_polygon
maps4fs/generator/game.py CHANGED
@@ -11,7 +11,7 @@ from maps4fs.generator.component.config import Config
11
11
  from maps4fs.generator.component.grle import GRLE
12
12
  from maps4fs.generator.component.i3d import I3d
13
13
  from maps4fs.generator.component.satellite import Satellite
14
- from maps4fs.generator.texture import Texture
14
+ from maps4fs.generator.component.texture import Texture
15
15
 
16
16
  working_directory = os.getcwd()
17
17
 
@@ -40,6 +40,7 @@ class Game:
40
40
  _tree_schema: str | None = None
41
41
  _i3d_processing: bool = True
42
42
  _plants_processing: bool = True
43
+ _dissolve: bool = True
43
44
 
44
45
  # Order matters! Some components depend on others.
45
46
  components = [Texture, Background, GRLE, I3d, Config, Satellite]
@@ -225,8 +226,15 @@ class Game:
225
226
  i3d_base_directory = os.path.dirname(self.i3d_file_path(map_directory))
226
227
  return os.path.join(i3d_base_directory, "splines.i3d")
227
228
 
229
+ @property
230
+ def dissolve(self) -> bool:
231
+ """Returns whether the dissolve should be applied.
232
+
233
+ Returns:
234
+ bool: True if the dissolve should be applied, False otherwise."""
235
+ return self._dissolve
236
+
228
237
 
229
- # pylint: disable=W0223
230
238
  class FS22(Game):
231
239
  """Class used to define the game version FS22."""
232
240
 
@@ -235,6 +243,7 @@ class FS22(Game):
235
243
  _texture_schema = os.path.join(working_directory, "data", "fs22-texture-schema.json")
236
244
  _i3d_processing = False
237
245
  _plants_processing = False
246
+ _dissolve = False
238
247
 
239
248
  def dem_file_path(self, map_directory: str) -> str:
240
249
  """Returns the path to the DEM file.
maps4fs/generator/map.py CHANGED
@@ -9,6 +9,8 @@ from typing import Any, Generator
9
9
 
10
10
  from maps4fs.generator.component.background import Background
11
11
  from maps4fs.generator.component.base.component import Component
12
+ from maps4fs.generator.component.layer import Layer
13
+ from maps4fs.generator.component.texture import Texture
12
14
  from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
13
15
  from maps4fs.generator.game import Game
14
16
  from maps4fs.generator.settings import (
@@ -21,7 +23,6 @@ from maps4fs.generator.settings import (
21
23
  SplineSettings,
22
24
  TextureSettings,
23
25
  )
24
- from maps4fs.generator.texture import Texture
25
26
  from maps4fs.logger import Logger
26
27
 
27
28
 
@@ -260,14 +261,14 @@ class Map:
260
261
  return None
261
262
  return component
262
263
 
263
- def get_texture_layer(self, by_usage: str | None = None) -> Texture.Layer | None:
264
+ def get_texture_layer(self, by_usage: str | None = None) -> Layer | None:
264
265
  """Get texture layer by usage.
265
266
 
266
267
  Arguments:
267
268
  by_usage (str, optional): Texture usage.
268
269
 
269
270
  Returns:
270
- Texture.Layer | None: Texture layer instance or None if not found.
271
+ Layer | None: Texture layer instance or None if not found.
271
272
  """
272
273
  texture_component = self.get_texture_component()
273
274
  if not texture_component:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: maps4fs
3
- Version: 1.8.185
3
+ Version: 1.8.187
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: MIT License
@@ -2,20 +2,21 @@ maps4fs/__init__.py,sha256=nKKMY2PGVAluIcIdLp5sgspSDCBDriF3iE8Pd6xyKWI,1563
2
2
  maps4fs/logger.py,sha256=HQrDyj72mUjVYo25aR_-_SxVn2rfFjDCNbj-JKJdSnE,1488
3
3
  maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
4
  maps4fs/generator/dem.py,sha256=dyzCFfDL6W_nCjwi9uF3-PCiL36rfOh3jGXlDuwiJYg,11795
5
- maps4fs/generator/game.py,sha256=FFAyckuTW6Ix5aRACXOj2eiA72xd3OMCcOARrMhS164,11025
6
- maps4fs/generator/map.py,sha256=QrsZfOcyuLrQuGUjA0nyB7FTcxwj-LUe-rjLiq5P4ME,11467
5
+ maps4fs/generator/game.py,sha256=NZaxj5z7WzMiHzAvQyr-TvVjGoHgqGldM6ZsItuYyzA,11292
6
+ maps4fs/generator/map.py,sha256=BV7OTrv3zHud6DbTGpm3TU5JNWJlKgRhZUGfs_wxntw,11513
7
7
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
8
8
  maps4fs/generator/settings.py,sha256=cFlN-gK8QcySqyPtcGm-2fLnxQnlmC3Y9kQufJxwI3Y,6270
9
- maps4fs/generator/texture.py,sha256=kCbTHoHhJPRDLWNhqBF0ElC2bQBPztvaj2qaFrS9vnY,36774
10
9
  maps4fs/generator/component/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
11
- maps4fs/generator/component/background.py,sha256=Q1GCrCZxAHOI8eNabx_KpgVOYv-3B_WTWOKk4Gn7-o4,18790
10
+ maps4fs/generator/component/background.py,sha256=IF56eQ2eHM37kdK1kz2vEFt-s88Esn9XKex_TbWtUZY,18800
12
11
  maps4fs/generator/component/config.py,sha256=RitKgFDZPzjA1fi8GcEi1na75qqaueUvpcITHjBvCXc,3674
13
12
  maps4fs/generator/component/grle.py,sha256=nDA6vjjfWFL0Hkz6aso1aLIwuksbxzZw9syNax1bD04,19134
14
13
  maps4fs/generator/component/i3d.py,sha256=kJ3Th1mSdBC9j8cyWBwIyYm0fKzYJtocI0jYWkVX3AU,19713
14
+ maps4fs/generator/component/layer.py,sha256=QPcEzTv_8N9wYvHAZy8OezfATaVLG-YetSfCXf2lnFI,5892
15
15
  maps4fs/generator/component/satellite.py,sha256=xzxqHp-G-jRgyI38-XdaMPdGWiC3PdhVJAjBnZL9wL8,5004
16
+ maps4fs/generator/component/texture.py,sha256=VqxUmK7HHw-G_C_qS1rb5mSpkzGwI0dAnW3-o5HCGgU,31026
16
17
  maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
17
18
  maps4fs/generator/component/base/component.py,sha256=apGuQ7TcwqL0neJZiciNLGO22wZwYyqoDZM7aI1RHw8,21273
18
- maps4fs/generator/component/base/component_image.py,sha256=fFgY6k2CbLIg3HS7KeiHi9iFQPJXCAxmBueYh2ExlFU,3918
19
+ maps4fs/generator/component/base/component_image.py,sha256=2QnJ9xm0D54v4whg7bc1s-kwRVjZHhOo1OR5jHr1Qp0,4786
19
20
  maps4fs/generator/component/base/component_mesh.py,sha256=43JY8X0ugIWAJq5y11vTJM9UfbL7SSugj8LkdPmni10,8871
20
21
  maps4fs/generator/component/base/component_xml.py,sha256=6OO1dKoceO1ACk7-k1oGtnkfNud8ZN3u3ZNjdNMpTqw,3967
21
22
  maps4fs/generator/dtm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -46,8 +47,8 @@ maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,4
46
47
  maps4fs/toolbox/background.py,sha256=RclEqxEWLbMxuEkkegQP8jybzugwQ1_R3rdfDe0s21U,2104
47
48
  maps4fs/toolbox/custom_osm.py,sha256=X6ZlPqiOhNjkmdD_qVroIfdOl9Rb90cDwVSLDVYgx80,1892
48
49
  maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
49
- maps4fs-1.8.185.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
50
- maps4fs-1.8.185.dist-info/METADATA,sha256=YmO48up_xnl4GkR49Mpe5zbpdFxXn0TTWu_FaKbMvyo,44584
51
- maps4fs-1.8.185.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
52
- maps4fs-1.8.185.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
53
- maps4fs-1.8.185.dist-info/RECORD,,
50
+ maps4fs-1.8.187.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
51
+ maps4fs-1.8.187.dist-info/METADATA,sha256=qAJxxbv2n4e87XZeC61IXX6RgOnsYuys4dxFptebSPo,44584
52
+ maps4fs-1.8.187.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
53
+ maps4fs-1.8.187.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
54
+ maps4fs-1.8.187.dist-info/RECORD,,