maps4fs 0.9.8__py3-none-any.whl → 1.1.0__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 +277 -227
- maps4fs/generator/component.py +215 -32
- maps4fs/generator/config.py +15 -11
- maps4fs/generator/dem.py +118 -100
- maps4fs/generator/game.py +18 -2
- maps4fs/generator/grle.py +175 -0
- maps4fs/generator/i3d.py +229 -26
- maps4fs/generator/map.py +35 -20
- 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.0.dist-info/LICENSE.md +190 -0
- {maps4fs-0.9.8.dist-info → maps4fs-1.1.0.dist-info}/METADATA +93 -50
- maps4fs-1.1.0.dist-info/RECORD +21 -0
- maps4fs/generator/path_steps.py +0 -83
- maps4fs/generator/tile.py +0 -55
- maps4fs-0.9.8.dist-info/LICENSE.md +0 -21
- maps4fs-0.9.8.dist-info/RECORD +0 -21
- {maps4fs-0.9.8.dist-info → maps4fs-1.1.0.dist-info}/WHEEL +0 -0
- {maps4fs-0.9.8.dist-info → maps4fs-1.1.0.dist-info}/top_level.txt +0 -0
maps4fs/generator/dem.py
CHANGED
@@ -9,11 +9,12 @@ import cv2
|
|
9
9
|
import numpy as np
|
10
10
|
import rasterio # type: ignore
|
11
11
|
import requests
|
12
|
+
from pympler import asizeof # type: ignore
|
12
13
|
|
13
14
|
from maps4fs.generator.component import Component
|
14
15
|
|
15
16
|
SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
|
16
|
-
DEFAULT_MULTIPLIER = 1
|
17
|
+
DEFAULT_MULTIPLIER = 1
|
17
18
|
DEFAULT_BLUR_RADIUS = 35
|
18
19
|
DEFAULT_PLATEAU = 0
|
19
20
|
|
@@ -22,10 +23,12 @@ DEFAULT_PLATEAU = 0
|
|
22
23
|
class DEM(Component):
|
23
24
|
"""Component for processing Digital Elevation Model data.
|
24
25
|
|
25
|
-
|
26
|
+
Arguments:
|
27
|
+
game (Game): The game instance for which the map is generated.
|
26
28
|
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
27
|
-
|
28
|
-
|
29
|
+
map_size (int): The size of the map in pixels.
|
30
|
+
map_rotated_size (int): The size of the map in pixels after rotation.
|
31
|
+
rotation (int): The rotation angle of the map.
|
29
32
|
map_directory (str): The directory where the map files are stored.
|
30
33
|
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
31
34
|
info, warning. If not provided, default logging will be used.
|
@@ -39,6 +42,14 @@ class DEM(Component):
|
|
39
42
|
os.makedirs(self.hgt_dir, exist_ok=True)
|
40
43
|
os.makedirs(self.gz_dir, exist_ok=True)
|
41
44
|
|
45
|
+
self.logger.debug("Map size: %s x %s.", self.map_size, self.map_size)
|
46
|
+
self.logger.debug(
|
47
|
+
"Map rotated size: %s x %s.", self.map_rotated_size, self.map_rotated_size
|
48
|
+
)
|
49
|
+
|
50
|
+
self.output_resolution = self.get_output_resolution()
|
51
|
+
self.logger.debug("Output resolution for DEM data: %s.", self.output_resolution)
|
52
|
+
|
42
53
|
self.multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
|
43
54
|
blur_radius = self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS)
|
44
55
|
if blur_radius is None or blur_radius <= 0:
|
@@ -63,21 +74,61 @@ class DEM(Component):
|
|
63
74
|
"""
|
64
75
|
return self._dem_path
|
65
76
|
|
66
|
-
|
77
|
+
# pylint: disable=W0201
|
78
|
+
def set_dem_path(self, dem_path: str) -> None:
|
79
|
+
"""Set path to the DEM file.
|
80
|
+
|
81
|
+
Arguments:
|
82
|
+
dem_path (str): Path to the DEM file.
|
83
|
+
"""
|
84
|
+
self._dem_path = dem_path
|
85
|
+
|
86
|
+
# pylint: disable=W0201
|
87
|
+
def set_output_resolution(self, output_resolution: tuple[int, int]) -> None:
|
88
|
+
"""Set output resolution for DEM data (width, height).
|
89
|
+
|
90
|
+
Arguments:
|
91
|
+
output_resolution (tuple[int, int]): Output resolution for DEM data.
|
92
|
+
"""
|
93
|
+
self.output_resolution = output_resolution
|
94
|
+
|
95
|
+
def get_output_resolution(self, use_original: bool = False) -> tuple[int, int]:
|
67
96
|
"""Get output resolution for DEM data.
|
68
97
|
|
98
|
+
Arguments:
|
99
|
+
use_original (bool, optional): If True, will use original map size. Defaults to False.
|
100
|
+
|
69
101
|
Returns:
|
70
102
|
tuple[int, int]: Output resolution for DEM data.
|
71
103
|
"""
|
72
|
-
|
73
|
-
|
104
|
+
map_size = self.map_size if use_original else self.map_rotated_size
|
105
|
+
|
106
|
+
dem_size = int((map_size / 2) * self.game.dem_multipliyer)
|
107
|
+
|
74
108
|
self.logger.debug(
|
75
|
-
"DEM size multiplier is %s, DEM
|
109
|
+
"DEM size multiplier is %s, DEM size: %sx%s, use original: %s.",
|
76
110
|
self.game.dem_multipliyer,
|
77
|
-
|
78
|
-
|
111
|
+
dem_size,
|
112
|
+
dem_size,
|
113
|
+
use_original,
|
114
|
+
)
|
115
|
+
return dem_size, dem_size
|
116
|
+
|
117
|
+
def to_ground(self, data: np.ndarray) -> np.ndarray:
|
118
|
+
"""Receives the signed 16-bit integer array and converts it to the ground level.
|
119
|
+
If the min value is negative, it will become zero value and the rest of the values
|
120
|
+
will be shifted accordingly.
|
121
|
+
"""
|
122
|
+
# For examlem, min value was -50, it will become 0 and for all values we'll +50.
|
123
|
+
|
124
|
+
if data.min() < 0:
|
125
|
+
self.logger.debug("Array contains negative values, will be shifted to the ground.")
|
126
|
+
data = data + abs(data.min())
|
127
|
+
|
128
|
+
self.logger.debug(
|
129
|
+
"Array was shifted to the ground. Min: %s, max: %s.", data.min(), data.max()
|
79
130
|
)
|
80
|
-
return
|
131
|
+
return data
|
81
132
|
|
82
133
|
# pylint: disable=no-member
|
83
134
|
def process(self) -> None:
|
@@ -85,7 +136,7 @@ class DEM(Component):
|
|
85
136
|
saves to map directory."""
|
86
137
|
north, south, east, west = self.bbox
|
87
138
|
|
88
|
-
dem_output_resolution = self.
|
139
|
+
dem_output_resolution = self.output_resolution
|
89
140
|
self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
|
90
141
|
|
91
142
|
tile_path = self._srtm_tile()
|
@@ -119,14 +170,20 @@ class DEM(Component):
|
|
119
170
|
data.max(),
|
120
171
|
)
|
121
172
|
|
173
|
+
data = self.to_ground(data)
|
174
|
+
|
122
175
|
resampled_data = cv2.resize(
|
123
176
|
data, dem_output_resolution, interpolation=cv2.INTER_LINEAR
|
124
177
|
).astype("uint16")
|
125
178
|
|
179
|
+
size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
|
180
|
+
self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
|
181
|
+
|
126
182
|
self.logger.debug(
|
127
|
-
"Maximum value in resampled data: %s, minimum value: %s.",
|
183
|
+
"Maximum value in resampled data: %s, minimum value: %s. Data type: %s.",
|
128
184
|
resampled_data.max(),
|
129
185
|
resampled_data.min(),
|
186
|
+
resampled_data.dtype,
|
130
187
|
)
|
131
188
|
|
132
189
|
if self.auto_process:
|
@@ -135,6 +192,18 @@ class DEM(Component):
|
|
135
192
|
else:
|
136
193
|
self.logger.debug("Auto processing is disabled, DEM data will not be normalized.")
|
137
194
|
resampled_data = resampled_data * self.multiplier
|
195
|
+
|
196
|
+
self.logger.debug(
|
197
|
+
"DEM data was multiplied by %s. Min: %s, max: %s. Data type: %s.",
|
198
|
+
self.multiplier,
|
199
|
+
resampled_data.min(),
|
200
|
+
resampled_data.max(),
|
201
|
+
resampled_data.dtype,
|
202
|
+
)
|
203
|
+
|
204
|
+
size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
|
205
|
+
self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
|
206
|
+
|
138
207
|
# Clip values to 16-bit unsigned integer range.
|
139
208
|
resampled_data = np.clip(resampled_data, 0, 65535)
|
140
209
|
resampled_data = resampled_data.astype("uint16")
|
@@ -188,28 +257,31 @@ class DEM(Component):
|
|
188
257
|
)
|
189
258
|
|
190
259
|
cv2.imwrite(self._dem_path, resampled_data)
|
191
|
-
self.logger.
|
260
|
+
self.logger.info("DEM data was saved to %s.", self._dem_path)
|
192
261
|
|
193
|
-
if self.
|
194
|
-
self.
|
262
|
+
if self.rotation:
|
263
|
+
self.rotate_dem()
|
195
264
|
|
196
|
-
def
|
197
|
-
"""
|
265
|
+
def rotate_dem(self) -> None:
|
266
|
+
"""Rotate DEM image."""
|
267
|
+
self.logger.debug("Rotating DEM image by %s degrees.", self.rotation)
|
268
|
+
output_width, output_height = self.get_output_resolution(use_original=True)
|
198
269
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
dem_directory = os.path.dirname(self._dem_path)
|
203
|
-
|
204
|
-
additional_dem_path = os.path.join(dem_directory, dem_name)
|
270
|
+
self.logger.debug(
|
271
|
+
"Output resolution for rotated DEM: %s x %s.", output_width, output_height
|
272
|
+
)
|
205
273
|
|
206
|
-
|
207
|
-
|
274
|
+
self.rotate_image(
|
275
|
+
self._dem_path,
|
276
|
+
self.rotation,
|
277
|
+
output_height=output_height,
|
278
|
+
output_width=output_width,
|
279
|
+
)
|
208
280
|
|
209
281
|
def _tile_info(self, lat: float, lon: float) -> tuple[str, str]:
|
210
282
|
"""Returns latitude band and tile name for SRTM tile from coordinates.
|
211
283
|
|
212
|
-
|
284
|
+
Arguments:
|
213
285
|
lat (float): Latitude.
|
214
286
|
lon (float): Longitude.
|
215
287
|
|
@@ -265,7 +337,7 @@ class DEM(Component):
|
|
265
337
|
|
266
338
|
decompressed_file_path = os.path.join(self.hgt_dir, f"{tile_name}.hgt")
|
267
339
|
if os.path.isfile(decompressed_file_path):
|
268
|
-
self.logger.
|
340
|
+
self.logger.debug(
|
269
341
|
"Decompressed tile already exists: %s, skipping download.",
|
270
342
|
decompressed_file_path,
|
271
343
|
)
|
@@ -284,80 +356,22 @@ class DEM(Component):
|
|
284
356
|
def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
|
285
357
|
"""Saves empty DEM file filled with zeros."""
|
286
358
|
dem_data = np.zeros(dem_output_resolution, dtype="uint16")
|
287
|
-
cv2.imwrite(self._dem_path, dem_data)
|
359
|
+
cv2.imwrite(self._dem_path, dem_data)
|
288
360
|
self.logger.warning("DEM data filled with zeros and saved to %s.", self._dem_path)
|
289
361
|
|
290
|
-
def
|
291
|
-
"""
|
292
|
-
Returns path to the preview image.
|
362
|
+
def previews(self) -> list:
|
363
|
+
"""This component does not have previews, returns empty list.
|
293
364
|
|
294
365
|
Returns:
|
295
|
-
|
366
|
+
list: Empty list.
|
296
367
|
"""
|
297
|
-
|
298
|
-
grayscale_dem_path = os.path.join(self.previews_directory, "dem_grayscale.png")
|
299
|
-
|
300
|
-
self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
|
301
|
-
|
302
|
-
dem_data = cv2.imread(self._dem_path, cv2.IMREAD_GRAYSCALE)
|
303
|
-
dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
|
304
|
-
cv2.imwrite(grayscale_dem_path, dem_data_rgb)
|
305
|
-
return grayscale_dem_path
|
306
|
-
|
307
|
-
def colored_preview(self) -> str:
|
308
|
-
"""Converts DEM image to colored RGB image and saves it to the map directory.
|
309
|
-
Returns path to the preview image.
|
310
|
-
|
311
|
-
Returns:
|
312
|
-
list[str]: List with a single path to the DEM file
|
313
|
-
"""
|
314
|
-
|
315
|
-
# colored_dem_path = self._dem_path.replace(".png", "_colored.png")
|
316
|
-
colored_dem_path = os.path.join(self.previews_directory, "dem_colored.png")
|
317
|
-
|
318
|
-
self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
|
319
|
-
|
320
|
-
dem_data = cv2.imread(self._dem_path, cv2.IMREAD_GRAYSCALE)
|
321
|
-
|
322
|
-
self.logger.debug(
|
323
|
-
"DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
324
|
-
dem_data.shape,
|
325
|
-
dem_data.dtype,
|
326
|
-
dem_data.min(),
|
327
|
-
dem_data.max(),
|
328
|
-
)
|
329
|
-
|
330
|
-
# Create an empty array with the same shape and type as dem_data.
|
331
|
-
dem_data_normalized = np.empty_like(dem_data)
|
332
|
-
|
333
|
-
# Normalize the DEM data to the range [0, 255]
|
334
|
-
cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
|
335
|
-
self.logger.debug(
|
336
|
-
"DEM data after normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
337
|
-
dem_data_normalized.shape,
|
338
|
-
dem_data_normalized.dtype,
|
339
|
-
dem_data_normalized.min(),
|
340
|
-
dem_data_normalized.max(),
|
341
|
-
)
|
342
|
-
dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
|
343
|
-
|
344
|
-
cv2.imwrite(colored_dem_path, dem_data_colored)
|
345
|
-
return colored_dem_path
|
346
|
-
|
347
|
-
def previews(self) -> list[str]:
|
348
|
-
"""Get list of preview images.
|
349
|
-
|
350
|
-
Returns:
|
351
|
-
list[str]: List of preview images.
|
352
|
-
"""
|
353
|
-
self.logger.debug("Starting DEM previews generation.")
|
354
|
-
return [self.grayscale_preview(), self.colored_preview()]
|
368
|
+
return []
|
355
369
|
|
356
370
|
def _get_scaling_factor(self, maximum_deviation: int) -> float:
|
357
371
|
"""Calculate scaling factor for DEM data normalization.
|
358
372
|
NOTE: Needs reconsideration for the implementation.
|
359
373
|
|
360
|
-
|
374
|
+
Arguments:
|
361
375
|
maximum_deviation (int): Maximum deviation in DEM data.
|
362
376
|
|
363
377
|
Returns:
|
@@ -369,7 +383,7 @@ class DEM(Component):
|
|
369
383
|
|
370
384
|
def _normalize_dem(self, data: np.ndarray) -> np.ndarray:
|
371
385
|
"""Normalize DEM data to 16-bit unsigned integer using max height from settings.
|
372
|
-
|
386
|
+
Arguments:
|
373
387
|
data (np.ndarray): DEM data from SRTM file after cropping.
|
374
388
|
Returns:
|
375
389
|
np.ndarray: Normalized DEM data.
|
@@ -377,9 +391,9 @@ class DEM(Component):
|
|
377
391
|
self.logger.debug("Starting DEM data normalization.")
|
378
392
|
# Calculate the difference between the maximum and minimum values in the DEM data.
|
379
393
|
|
380
|
-
max_height = data.max()
|
381
|
-
min_height = data.min()
|
382
|
-
max_dev = max_height - min_height
|
394
|
+
max_height = data.max()
|
395
|
+
min_height = data.min()
|
396
|
+
max_dev = max_height - min_height
|
383
397
|
self.logger.debug(
|
384
398
|
"Maximum deviation: %s with maximum at %s and minimum at %s.",
|
385
399
|
max_dev,
|
@@ -389,14 +403,18 @@ class DEM(Component):
|
|
389
403
|
|
390
404
|
scaling_factor = self._get_scaling_factor(max_dev)
|
391
405
|
adjusted_max_height = int(65535 * scaling_factor)
|
392
|
-
self.logger.
|
393
|
-
|
394
|
-
|
406
|
+
self.logger.info(
|
407
|
+
"Maximum deviation: %s. Scaling factor: %s. Adjusted max height: %s.",
|
408
|
+
max_dev,
|
409
|
+
scaling_factor,
|
410
|
+
adjusted_max_height,
|
395
411
|
)
|
396
412
|
normalized_data = (
|
397
413
|
(data - data.min()) / (data.max() - data.min()) * adjusted_max_height
|
398
414
|
).astype("uint16")
|
399
415
|
self.logger.debug(
|
400
|
-
|
416
|
+
"DEM data was normalized to %s - %s.",
|
417
|
+
normalized_data.min(),
|
418
|
+
normalized_data.max(),
|
401
419
|
)
|
402
420
|
return normalized_data
|
maps4fs/generator/game.py
CHANGED
@@ -8,7 +8,7 @@ import os
|
|
8
8
|
|
9
9
|
from maps4fs.generator.background import Background
|
10
10
|
from maps4fs.generator.config import Config
|
11
|
-
from maps4fs.generator.
|
11
|
+
from maps4fs.generator.grle import GRLE
|
12
12
|
from maps4fs.generator.i3d import I3d
|
13
13
|
from maps4fs.generator.texture import Texture
|
14
14
|
|
@@ -35,8 +35,10 @@ class Game:
|
|
35
35
|
_additional_dem_name: str | None = None
|
36
36
|
_map_template_path: str | None = None
|
37
37
|
_texture_schema: str | None = None
|
38
|
+
_grle_schema: str | None = None
|
38
39
|
|
39
|
-
|
40
|
+
# Order matters! Some components depend on others.
|
41
|
+
components = [Texture, I3d, GRLE, Background, Config]
|
40
42
|
|
41
43
|
def __init__(self, map_template_path: str | None = None):
|
42
44
|
if map_template_path:
|
@@ -94,6 +96,19 @@ class Game:
|
|
94
96
|
raise ValueError("Texture layers schema path not set.")
|
95
97
|
return self._texture_schema
|
96
98
|
|
99
|
+
@property
|
100
|
+
def grle_schema(self) -> str:
|
101
|
+
"""Returns the path to the GRLE layers schema file.
|
102
|
+
|
103
|
+
Raises:
|
104
|
+
ValueError: If the GRLE layers schema path is not set.
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
str: The path to the GRLE layers schema file."""
|
108
|
+
if not self._grle_schema:
|
109
|
+
raise ValueError("GRLE layers schema path not set.")
|
110
|
+
return self._grle_schema
|
111
|
+
|
97
112
|
def dem_file_path(self, map_directory: str) -> str:
|
98
113
|
"""Returns the path to the DEM file.
|
99
114
|
|
@@ -171,6 +186,7 @@ class FS25(Game):
|
|
171
186
|
_additional_dem_name = "unprocessedHeightMap.png"
|
172
187
|
_map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip")
|
173
188
|
_texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json")
|
189
|
+
_grle_schema = os.path.join(working_directory, "data", "fs25-grle-schema.json")
|
174
190
|
|
175
191
|
def dem_file_path(self, map_directory: str) -> str:
|
176
192
|
"""Returns the path to the DEM file.
|
@@ -0,0 +1,175 @@
|
|
1
|
+
"""This module contains the GRLE class for generating InfoLayer PNG files based on GRLE schema."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
from xml.etree import ElementTree as ET
|
6
|
+
|
7
|
+
import cv2
|
8
|
+
import numpy as np
|
9
|
+
|
10
|
+
from maps4fs.generator.component import Component
|
11
|
+
|
12
|
+
|
13
|
+
# pylint: disable=W0223
|
14
|
+
class GRLE(Component):
|
15
|
+
"""Component for to generate InfoLayer PNG files based on GRLE schema.
|
16
|
+
|
17
|
+
Arguments:
|
18
|
+
game (Game): The game instance for which the map is generated.
|
19
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
20
|
+
map_size (int): The size of the map in pixels.
|
21
|
+
map_rotated_size (int): The size of the map in pixels after rotation.
|
22
|
+
rotation (int): The rotation angle of the map.
|
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
|
+
_grle_schema: dict[str, float | int | 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
|
+
|
34
|
+
self.farmland_margin = self.kwargs.get("farmland_margin", 0)
|
35
|
+
|
36
|
+
try:
|
37
|
+
grle_schema_path = self.game.grle_schema
|
38
|
+
except ValueError:
|
39
|
+
self.logger.info("GRLE schema processing is not implemented for this game.")
|
40
|
+
return
|
41
|
+
|
42
|
+
try:
|
43
|
+
with open(grle_schema_path, "r", encoding="utf-8") as file:
|
44
|
+
self._grle_schema = json.load(file)
|
45
|
+
self.logger.debug("GRLE schema loaded from: %s.", grle_schema_path)
|
46
|
+
except (json.JSONDecodeError, FileNotFoundError) as error:
|
47
|
+
self.logger.error("Error loading GRLE schema from %s: %s.", grle_schema_path, error)
|
48
|
+
self._grle_schema = None
|
49
|
+
|
50
|
+
def process(self) -> None:
|
51
|
+
"""Generates InfoLayer PNG files based on the GRLE schema."""
|
52
|
+
if not self._grle_schema:
|
53
|
+
self.logger.info("GRLE schema is not obtained, skipping the processing.")
|
54
|
+
return
|
55
|
+
|
56
|
+
for info_layer in self._grle_schema:
|
57
|
+
if isinstance(info_layer, dict):
|
58
|
+
file_path = os.path.join(
|
59
|
+
self.game.weights_dir_path(self.map_directory), info_layer["name"]
|
60
|
+
)
|
61
|
+
|
62
|
+
height = int(self.map_size * info_layer["height_multiplier"])
|
63
|
+
width = int(self.map_size * info_layer["width_multiplier"])
|
64
|
+
channels = info_layer["channels"]
|
65
|
+
data_type = info_layer["data_type"]
|
66
|
+
|
67
|
+
# Create the InfoLayer PNG file with zeros.
|
68
|
+
if channels == 1:
|
69
|
+
info_layer_data = np.zeros((height, width), dtype=data_type)
|
70
|
+
else:
|
71
|
+
info_layer_data = np.zeros((height, width, channels), dtype=data_type)
|
72
|
+
self.logger.debug("Shape of %s: %s.", info_layer["name"], info_layer_data.shape)
|
73
|
+
cv2.imwrite(file_path, info_layer_data) # pylint: disable=no-member
|
74
|
+
self.logger.debug("InfoLayer PNG file %s created.", file_path)
|
75
|
+
else:
|
76
|
+
self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
|
77
|
+
|
78
|
+
self._add_farmlands()
|
79
|
+
|
80
|
+
def previews(self) -> list[str]:
|
81
|
+
"""Returns a list of paths to the preview images (empty list).
|
82
|
+
The component does not generate any preview images so it returns an empty list.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
list[str]: An empty list.
|
86
|
+
"""
|
87
|
+
return []
|
88
|
+
|
89
|
+
# pylint: disable=R0801, R0914
|
90
|
+
def _add_farmlands(self) -> None:
|
91
|
+
"""Adds farmlands to the InfoLayer PNG file."""
|
92
|
+
|
93
|
+
textures_info_layer_path = self.get_infolayer_path("textures")
|
94
|
+
if not textures_info_layer_path:
|
95
|
+
return
|
96
|
+
|
97
|
+
with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
|
98
|
+
textures_info_layer = json.load(textures_info_layer_file)
|
99
|
+
|
100
|
+
fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
|
101
|
+
if not fields:
|
102
|
+
self.logger.warning("Fields data not found in textures info layer.")
|
103
|
+
return
|
104
|
+
|
105
|
+
self.logger.info("Found %s fields in textures info layer.", len(fields))
|
106
|
+
|
107
|
+
info_layer_farmlands_path = os.path.join(
|
108
|
+
self.game.weights_dir_path(self.map_directory), "infoLayer_farmlands.png"
|
109
|
+
)
|
110
|
+
|
111
|
+
if not os.path.isfile(info_layer_farmlands_path):
|
112
|
+
self.logger.warning("InfoLayer PNG file %s not found.", info_layer_farmlands_path)
|
113
|
+
return
|
114
|
+
|
115
|
+
# pylint: disable=no-member
|
116
|
+
image = cv2.imread(info_layer_farmlands_path, cv2.IMREAD_UNCHANGED)
|
117
|
+
farmlands_xml_path = os.path.join(self.map_directory, "map/config/farmlands.xml")
|
118
|
+
if not os.path.isfile(farmlands_xml_path):
|
119
|
+
self.logger.warning("Farmlands XML file %s not found.", farmlands_xml_path)
|
120
|
+
return
|
121
|
+
|
122
|
+
tree = ET.parse(farmlands_xml_path)
|
123
|
+
farmlands_xml = tree.find("farmlands")
|
124
|
+
|
125
|
+
# Not using enumerate because in case of the error, we do not increment
|
126
|
+
# the farmland_id. So as a result we do not have a gap in the farmland IDs.
|
127
|
+
farmland_id = 1
|
128
|
+
|
129
|
+
for field in fields:
|
130
|
+
try:
|
131
|
+
fitted_field = self.fit_polygon_into_bounds(
|
132
|
+
field, self.farmland_margin, angle=self.rotation
|
133
|
+
)
|
134
|
+
except ValueError as e:
|
135
|
+
self.logger.warning(
|
136
|
+
"Farmland %s could not be fitted into the map bounds with error: %s",
|
137
|
+
farmland_id,
|
138
|
+
e,
|
139
|
+
)
|
140
|
+
continue
|
141
|
+
|
142
|
+
field_np = np.array(fitted_field, np.int32)
|
143
|
+
field_np = field_np.reshape((-1, 1, 2))
|
144
|
+
|
145
|
+
# Infolayer image is 1/2 of the size of the map image, that's why we need to divide
|
146
|
+
# the coordinates by 2.
|
147
|
+
field_np = field_np // 2
|
148
|
+
|
149
|
+
# pylint: disable=no-member
|
150
|
+
try:
|
151
|
+
cv2.fillPoly(image, [field_np], farmland_id) # type: ignore
|
152
|
+
except Exception as e: # pylint: disable=W0718
|
153
|
+
self.logger.warning(
|
154
|
+
"Farmland %s could not be added to the InfoLayer PNG file with error: %s",
|
155
|
+
farmland_id,
|
156
|
+
e,
|
157
|
+
)
|
158
|
+
continue
|
159
|
+
|
160
|
+
# Add the field to the farmlands XML.
|
161
|
+
farmland = ET.SubElement(farmlands_xml, "farmland") # type: ignore
|
162
|
+
farmland.set("id", str(farmland_id))
|
163
|
+
farmland.set("priceScale", "1")
|
164
|
+
farmland.set("npcName", "FORESTER")
|
165
|
+
|
166
|
+
farmland_id += 1
|
167
|
+
|
168
|
+
tree.write(farmlands_xml_path)
|
169
|
+
|
170
|
+
self.logger.info("Farmlands added to the farmlands XML file: %s.", farmlands_xml_path)
|
171
|
+
|
172
|
+
cv2.imwrite(info_layer_farmlands_path, image) # pylint: disable=no-member
|
173
|
+
self.logger.info(
|
174
|
+
"Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
175
|
+
)
|