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/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# pylint: disable=missing-module-docstring
|
@@ -0,0 +1,101 @@
|
|
1
|
+
"""This module contains the base class for all map generation components."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, Any
|
6
|
+
|
7
|
+
import osmnx as ox # type: ignore
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from maps4fs.generator.game import Game
|
11
|
+
|
12
|
+
|
13
|
+
# pylint: disable=R0801, R0903, R0902
|
14
|
+
class Component:
|
15
|
+
"""Base class for all map generation components.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
19
|
+
map_height (int): The height of the map in pixels.
|
20
|
+
map_width (int): The width of the map in pixels.
|
21
|
+
map_directory (str): The directory where the map files are stored.
|
22
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
23
|
+
info, warning. If not provided, default logging will be used.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
game: Game,
|
29
|
+
coordinates: tuple[float, float],
|
30
|
+
map_height: int,
|
31
|
+
map_width: int,
|
32
|
+
map_directory: str,
|
33
|
+
logger: Any = None,
|
34
|
+
**kwargs, # pylint: disable=W0613, R0913, R0917
|
35
|
+
):
|
36
|
+
self.game = game
|
37
|
+
self.coordinates = coordinates
|
38
|
+
self.map_height = map_height
|
39
|
+
self.map_width = map_width
|
40
|
+
self.map_directory = map_directory
|
41
|
+
self.logger = logger
|
42
|
+
self.kwargs = kwargs
|
43
|
+
|
44
|
+
self.save_bbox()
|
45
|
+
self.preprocess()
|
46
|
+
|
47
|
+
def preprocess(self) -> None:
|
48
|
+
"""Prepares the component for processing. Must be implemented in the child class.
|
49
|
+
|
50
|
+
Raises:
|
51
|
+
NotImplementedError: If the method is not implemented in the child class.
|
52
|
+
"""
|
53
|
+
raise NotImplementedError
|
54
|
+
|
55
|
+
def process(self) -> None:
|
56
|
+
"""Launches the component processing. Must be implemented in the child class.
|
57
|
+
|
58
|
+
Raises:
|
59
|
+
NotImplementedError: If the method is not implemented in the child class.
|
60
|
+
"""
|
61
|
+
raise NotImplementedError
|
62
|
+
|
63
|
+
def previews(self) -> list[str]:
|
64
|
+
"""Returns a list of paths to the preview images. Must be implemented in the child class.
|
65
|
+
|
66
|
+
Raises:
|
67
|
+
NotImplementedError: If the method is not implemented in the child class.
|
68
|
+
"""
|
69
|
+
raise NotImplementedError
|
70
|
+
|
71
|
+
def get_bbox(self, project_utm: bool = False) -> tuple[int, int, int, int]:
|
72
|
+
"""Calculates the bounding box of the map from the coordinates and the height and
|
73
|
+
width of the map.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
project_utm (bool, optional): Whether to project the bounding box to UTM.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
tuple[int, int, int, int]: The bounding box of the map.
|
80
|
+
"""
|
81
|
+
north, south, _, _ = ox.utils_geo.bbox_from_point(
|
82
|
+
self.coordinates, dist=self.map_height / 2, project_utm=project_utm
|
83
|
+
)
|
84
|
+
_, _, east, west = ox.utils_geo.bbox_from_point(
|
85
|
+
self.coordinates, dist=self.map_width / 2, project_utm=project_utm
|
86
|
+
)
|
87
|
+
bbox = north, south, east, west
|
88
|
+
self.logger.debug(
|
89
|
+
"Calculated bounding box for component: %s: %s, project_utm: %s",
|
90
|
+
self.__class__.__name__,
|
91
|
+
bbox,
|
92
|
+
project_utm,
|
93
|
+
)
|
94
|
+
return bbox
|
95
|
+
|
96
|
+
def save_bbox(self) -> None:
|
97
|
+
"""Saves the bounding box of the map to the component instance from the coordinates and the
|
98
|
+
height and width of the map.
|
99
|
+
"""
|
100
|
+
self.bbox = self.get_bbox(project_utm=False)
|
101
|
+
self.logger.debug("Saved bounding box: %s", self.bbox)
|
@@ -0,0 +1,55 @@
|
|
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
|
+
|
11
|
+
# pylint: disable=R0903
|
12
|
+
class Config(Component):
|
13
|
+
"""Component for map settings and configuration.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
17
|
+
map_height (int): The height of the map in pixels.
|
18
|
+
map_width (int): The width of the map in pixels.
|
19
|
+
map_directory (str): The directory where the map files are stored.
|
20
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
21
|
+
info, warning. If not provided, default logging will be used.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def preprocess(self) -> None:
|
25
|
+
self._map_xml_path = self.game.map_xml_path(self.map_directory)
|
26
|
+
self.logger.debug("Map XML path: %s.", self._map_xml_path)
|
27
|
+
|
28
|
+
def process(self):
|
29
|
+
self._set_map_size()
|
30
|
+
|
31
|
+
def _set_map_size(self):
|
32
|
+
"""Edits map.xml file to set correct map size."""
|
33
|
+
if not os.path.isfile(self._map_xml_path):
|
34
|
+
self.logger.warning("Map XML file not found: %s.", self._map_xml_path)
|
35
|
+
return
|
36
|
+
tree = ET.parse(self._map_xml_path)
|
37
|
+
self.logger.debug("Map XML file loaded from: %s.", self._map_xml_path)
|
38
|
+
root = tree.getroot()
|
39
|
+
for map_elem in root.iter("map"):
|
40
|
+
map_elem.set("width", str(self.map_width))
|
41
|
+
map_elem.set("height", str(self.map_height))
|
42
|
+
self.logger.debug(
|
43
|
+
"Map size set to %sx%s in Map XML file.", self.map_width, self.map_height
|
44
|
+
)
|
45
|
+
tree.write(self._map_xml_path)
|
46
|
+
self.logger.debug("Map XML file saved to: %s.", self._map_xml_path)
|
47
|
+
|
48
|
+
def previews(self) -> list[str]:
|
49
|
+
"""Returns a list of paths to the preview images (empty list).
|
50
|
+
The component does not generate any preview images so it returns an empty list.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
list[str]: An empty list.
|
54
|
+
"""
|
55
|
+
return []
|
maps4fs/generator/dem.py
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
"""This module contains DEM class for processing Digital Elevation Model data."""
|
2
|
+
|
3
|
+
import gzip
|
4
|
+
import math
|
5
|
+
import os
|
6
|
+
import shutil
|
7
|
+
|
8
|
+
import cv2
|
9
|
+
import numpy as np
|
10
|
+
import rasterio # type: ignore
|
11
|
+
import requests
|
12
|
+
|
13
|
+
from maps4fs.generator.component import Component
|
14
|
+
|
15
|
+
SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
|
16
|
+
DEFAULT_MULTIPLIER = 1
|
17
|
+
DEFAULT_BLUR_RADIUS = 21
|
18
|
+
|
19
|
+
|
20
|
+
# pylint: disable=R0903
|
21
|
+
class DEM(Component):
|
22
|
+
"""Component for map settings and configuration.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
26
|
+
map_height (int): The height of the map in pixels.
|
27
|
+
map_width (int): The width of the map in pixels.
|
28
|
+
map_directory (str): The directory where the map files are stored.
|
29
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
30
|
+
info, warning. If not provided, default logging will be used.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def preprocess(self) -> None:
|
34
|
+
self._dem_path = self.game.dem_file_path(self.map_directory)
|
35
|
+
self.temp_dir = "temp"
|
36
|
+
self.hgt_dir = os.path.join(self.temp_dir, "hgt")
|
37
|
+
self.gz_dir = os.path.join(self.temp_dir, "gz")
|
38
|
+
os.makedirs(self.hgt_dir, exist_ok=True)
|
39
|
+
os.makedirs(self.gz_dir, exist_ok=True)
|
40
|
+
|
41
|
+
self.multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
|
42
|
+
blur_radius = self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS)
|
43
|
+
if blur_radius % 2 == 0:
|
44
|
+
blur_radius += 1
|
45
|
+
self.blur_radius = blur_radius
|
46
|
+
self.logger.debug(
|
47
|
+
"DEM value multiplier is %s, blur radius is %s.", self.multiplier, self.blur_radius
|
48
|
+
)
|
49
|
+
|
50
|
+
# pylint: disable=no-member
|
51
|
+
def process(self) -> None:
|
52
|
+
"""Reads SRTM file, crops it to map size, normalizes and blurs it,
|
53
|
+
saves to map directory."""
|
54
|
+
north, south, east, west = self.bbox
|
55
|
+
|
56
|
+
dem_height = int((self.map_height / 2) * self.game.dem_multipliyer + 1)
|
57
|
+
dem_width = int((self.map_width / 2) * self.game.dem_multipliyer + 1)
|
58
|
+
self.logger.debug(
|
59
|
+
"DEM size multiplier is %s, DEM height is %s, DEM width is %s.",
|
60
|
+
self.game.dem_multipliyer,
|
61
|
+
dem_height,
|
62
|
+
dem_width,
|
63
|
+
)
|
64
|
+
dem_output_resolution = (dem_width, dem_height)
|
65
|
+
self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
|
66
|
+
|
67
|
+
tile_path = self._srtm_tile()
|
68
|
+
if not tile_path:
|
69
|
+
self.logger.warning("Tile was not downloaded, DEM file will be filled with zeros.")
|
70
|
+
self._save_empty_dem(dem_output_resolution)
|
71
|
+
return
|
72
|
+
|
73
|
+
with rasterio.open(tile_path) as src:
|
74
|
+
self.logger.debug("Opened tile, shape: %s, dtype: %s.", src.shape, src.dtypes[0])
|
75
|
+
window = rasterio.windows.from_bounds(west, south, east, north, src.transform)
|
76
|
+
self.logger.debug(
|
77
|
+
"Window parameters. Column offset: %s, row offset: %s, width: %s, height: %s.",
|
78
|
+
window.col_off,
|
79
|
+
window.row_off,
|
80
|
+
window.width,
|
81
|
+
window.height,
|
82
|
+
)
|
83
|
+
data = src.read(1, window=window)
|
84
|
+
|
85
|
+
if not data.size > 0:
|
86
|
+
self.logger.warning("DEM data is empty, DEM file will be filled with zeros.")
|
87
|
+
self._save_empty_dem(dem_output_resolution)
|
88
|
+
return
|
89
|
+
|
90
|
+
self.logger.debug(
|
91
|
+
"DEM data was read from SRTM file. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
92
|
+
data.shape,
|
93
|
+
data.dtype,
|
94
|
+
data.min(),
|
95
|
+
data.max(),
|
96
|
+
)
|
97
|
+
|
98
|
+
resampled_data = cv2.resize(
|
99
|
+
data, dem_output_resolution, interpolation=cv2.INTER_LINEAR
|
100
|
+
).astype("uint16")
|
101
|
+
|
102
|
+
self.logger.debug(
|
103
|
+
"Maximum value in resampled data: %s, minimum value: %s.",
|
104
|
+
resampled_data.max(),
|
105
|
+
resampled_data.min(),
|
106
|
+
)
|
107
|
+
|
108
|
+
resampled_data = resampled_data * self.multiplier
|
109
|
+
self.logger.debug(
|
110
|
+
"DEM data multiplied by %s. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
111
|
+
self.multiplier,
|
112
|
+
resampled_data.shape,
|
113
|
+
resampled_data.dtype,
|
114
|
+
resampled_data.min(),
|
115
|
+
resampled_data.max(),
|
116
|
+
)
|
117
|
+
|
118
|
+
self.logger.debug(
|
119
|
+
"DEM data was resampled. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
120
|
+
resampled_data.shape,
|
121
|
+
resampled_data.dtype,
|
122
|
+
resampled_data.min(),
|
123
|
+
resampled_data.max(),
|
124
|
+
)
|
125
|
+
|
126
|
+
resampled_data = cv2.GaussianBlur(resampled_data, (self.blur_radius, self.blur_radius), 0)
|
127
|
+
self.logger.debug(
|
128
|
+
"Gaussion blur applied to DEM data with kernel size %s.",
|
129
|
+
self.blur_radius,
|
130
|
+
)
|
131
|
+
self.logger.debug(
|
132
|
+
"DEM data was blurred. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
133
|
+
resampled_data.shape,
|
134
|
+
resampled_data.dtype,
|
135
|
+
resampled_data.min(),
|
136
|
+
resampled_data.max(),
|
137
|
+
)
|
138
|
+
|
139
|
+
cv2.imwrite(self._dem_path, resampled_data)
|
140
|
+
self.logger.debug("DEM data was saved to %s.", self._dem_path)
|
141
|
+
|
142
|
+
if self.game.additional_dem_name is not None:
|
143
|
+
dem_directory = os.path.dirname(self._dem_path)
|
144
|
+
additional_dem_path = os.path.join(dem_directory, self.game.additional_dem_name)
|
145
|
+
shutil.copyfile(self._dem_path, additional_dem_path)
|
146
|
+
self.logger.debug("Additional DEM data was copied to %s.", additional_dem_path)
|
147
|
+
|
148
|
+
def _tile_info(self, lat: float, lon: float) -> tuple[str, str]:
|
149
|
+
"""Returns latitude band and tile name for SRTM tile from coordinates.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
lat (float): Latitude.
|
153
|
+
lon (float): Longitude.
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
tuple[str, str]: Latitude band and tile name.
|
157
|
+
"""
|
158
|
+
tile_latitude = math.floor(lat)
|
159
|
+
tile_longitude = math.floor(lon)
|
160
|
+
|
161
|
+
latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
|
162
|
+
if lon < 0:
|
163
|
+
tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
|
164
|
+
else:
|
165
|
+
tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
|
166
|
+
|
167
|
+
self.logger.debug(
|
168
|
+
"Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
|
169
|
+
)
|
170
|
+
return latitude_band, tile_name
|
171
|
+
|
172
|
+
def _download_tile(self) -> str | None:
|
173
|
+
"""Downloads SRTM tile from Amazon S3 using coordinates.
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
str: Path to compressed tile or None if download failed.
|
177
|
+
"""
|
178
|
+
latitude_band, tile_name = self._tile_info(*self.coordinates)
|
179
|
+
compressed_file_path = os.path.join(self.gz_dir, f"{tile_name}.hgt.gz")
|
180
|
+
url = SRTM.format(latitude_band=latitude_band, tile_name=tile_name)
|
181
|
+
self.logger.debug("Trying to get response from %s...", url)
|
182
|
+
response = requests.get(url, stream=True, timeout=10)
|
183
|
+
|
184
|
+
if response.status_code == 200:
|
185
|
+
self.logger.debug("Response received. Saving to %s...", compressed_file_path)
|
186
|
+
with open(compressed_file_path, "wb") as f:
|
187
|
+
for chunk in response.iter_content(chunk_size=8192):
|
188
|
+
f.write(chunk)
|
189
|
+
self.logger.debug("Compressed tile successfully downloaded.")
|
190
|
+
else:
|
191
|
+
self.logger.error("Response was failed with status code %s.", response.status_code)
|
192
|
+
return None
|
193
|
+
|
194
|
+
return compressed_file_path
|
195
|
+
|
196
|
+
def _srtm_tile(self) -> str | None:
|
197
|
+
"""Determines SRTM tile name from coordinates downloads it if necessary, and decompresses.
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
str: Path to decompressed tile or None if download failed.
|
201
|
+
"""
|
202
|
+
latitude_band, tile_name = self._tile_info(*self.coordinates)
|
203
|
+
self.logger.debug("SRTM tile name %s from latitude band %s.", tile_name, latitude_band)
|
204
|
+
|
205
|
+
decompressed_file_path = os.path.join(self.hgt_dir, f"{tile_name}.hgt")
|
206
|
+
if os.path.isfile(decompressed_file_path):
|
207
|
+
self.logger.info(
|
208
|
+
"Decompressed tile already exists: %s, skipping download.",
|
209
|
+
decompressed_file_path,
|
210
|
+
)
|
211
|
+
return decompressed_file_path
|
212
|
+
|
213
|
+
compressed_file_path = self._download_tile()
|
214
|
+
if not compressed_file_path:
|
215
|
+
self.logger.error("Download from SRTM failed, DEM file will be filled with zeros.")
|
216
|
+
return None
|
217
|
+
with gzip.open(compressed_file_path, "rb") as f_in:
|
218
|
+
with open(decompressed_file_path, "wb") as f_out:
|
219
|
+
shutil.copyfileobj(f_in, f_out)
|
220
|
+
self.logger.debug("Tile decompressed to %s.", decompressed_file_path)
|
221
|
+
return decompressed_file_path
|
222
|
+
|
223
|
+
def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
|
224
|
+
"""Saves empty DEM file filled with zeros."""
|
225
|
+
dem_data = np.zeros(dem_output_resolution, dtype="uint16")
|
226
|
+
cv2.imwrite(self._dem_path, dem_data) # pylint: disable=no-member
|
227
|
+
self.logger.warning("DEM data filled with zeros and saved to %s.", self._dem_path)
|
228
|
+
|
229
|
+
def grayscale_preview(self) -> str:
|
230
|
+
"""Converts DEM image to grayscale RGB image and saves it to the map directory.
|
231
|
+
Returns path to the preview image.
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
str: Path to the preview image.
|
235
|
+
"""
|
236
|
+
rgb_dem_path = self._dem_path.replace(".png", "_grayscale.png")
|
237
|
+
|
238
|
+
self.logger.debug("Creating grayscale preview of DEM data in %s.", rgb_dem_path)
|
239
|
+
|
240
|
+
dem_data = cv2.imread(self._dem_path, cv2.IMREAD_GRAYSCALE)
|
241
|
+
dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
|
242
|
+
cv2.imwrite(rgb_dem_path, dem_data_rgb)
|
243
|
+
return rgb_dem_path
|
244
|
+
|
245
|
+
def colored_preview(self) -> str:
|
246
|
+
"""Converts DEM image to colored RGB image and saves it to the map directory.
|
247
|
+
Returns path to the preview image.
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
list[str]: List with a single path to the DEM file
|
251
|
+
"""
|
252
|
+
|
253
|
+
colored_dem_path = self._dem_path.replace(".png", "_colored.png")
|
254
|
+
|
255
|
+
self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
|
256
|
+
|
257
|
+
dem_data = cv2.imread(self._dem_path, cv2.IMREAD_UNCHANGED)
|
258
|
+
|
259
|
+
self.logger.debug(
|
260
|
+
"DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
261
|
+
dem_data.shape,
|
262
|
+
dem_data.dtype,
|
263
|
+
dem_data.min(),
|
264
|
+
dem_data.max(),
|
265
|
+
)
|
266
|
+
|
267
|
+
# Create an empty array with the same shape and type as dem_data.
|
268
|
+
dem_data_normalized = np.empty_like(dem_data)
|
269
|
+
|
270
|
+
# Normalize the DEM data to the range [0, 255]
|
271
|
+
cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
|
272
|
+
self.logger.debug(
|
273
|
+
"DEM data after normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
274
|
+
dem_data_normalized.shape,
|
275
|
+
dem_data_normalized.dtype,
|
276
|
+
dem_data_normalized.min(),
|
277
|
+
dem_data_normalized.max(),
|
278
|
+
)
|
279
|
+
dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
|
280
|
+
|
281
|
+
cv2.imwrite(colored_dem_path, dem_data_colored)
|
282
|
+
return colored_dem_path
|
283
|
+
|
284
|
+
def previews(self) -> list[str]:
|
285
|
+
"""Get list of preview images.
|
286
|
+
|
287
|
+
Returns:
|
288
|
+
list[str]: List of preview images.
|
289
|
+
"""
|
290
|
+
self.logger.debug("Starting DEM previews generation.")
|
291
|
+
return [self.grayscale_preview(), self.colored_preview()]
|
@@ -0,0 +1,191 @@
|
|
1
|
+
"""This module contains the Game class and its subclasses. Game class is used to define
|
2
|
+
different versions of the game for which the map is generated. Each game has its own map
|
3
|
+
template file and specific settings for map generation."""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import os
|
8
|
+
|
9
|
+
from maps4fs.generator.config import Config
|
10
|
+
from maps4fs.generator.dem import DEM
|
11
|
+
from maps4fs.generator.texture import Texture
|
12
|
+
|
13
|
+
working_directory = os.getcwd()
|
14
|
+
|
15
|
+
|
16
|
+
class Game:
|
17
|
+
"""Class used to define different versions of the game for which the map is generated.
|
18
|
+
|
19
|
+
Arguments:
|
20
|
+
map_template_path (str, optional): Path to the map template file. Defaults to None.
|
21
|
+
|
22
|
+
Attributes and Properties:
|
23
|
+
code (str): The code of the game.
|
24
|
+
components (list[Type[Component]]): List of components used for map generation.
|
25
|
+
map_template_path (str): Path to the map template file.
|
26
|
+
|
27
|
+
Public Methods:
|
28
|
+
from_code(cls, code: str) -> Game: Returns the game instance based on the game code.
|
29
|
+
"""
|
30
|
+
|
31
|
+
code: str | None = None
|
32
|
+
dem_multipliyer: int = 1
|
33
|
+
_additional_dem_name: str | None = None
|
34
|
+
_map_template_path: str | None = None
|
35
|
+
_texture_schema: str | None = None
|
36
|
+
|
37
|
+
components = [Config, Texture, DEM]
|
38
|
+
|
39
|
+
def __init__(self, map_template_path: str | None = None):
|
40
|
+
if map_template_path:
|
41
|
+
self._map_template_path = map_template_path
|
42
|
+
|
43
|
+
def map_xml_path(self, map_directory: str) -> str:
|
44
|
+
"""Returns the path to the map.xml file.
|
45
|
+
|
46
|
+
Arguments:
|
47
|
+
map_directory (str): The path to the map directory.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
str: The path to the map.xml file.
|
51
|
+
"""
|
52
|
+
return os.path.join(map_directory, "maps", "map", "map.xml")
|
53
|
+
|
54
|
+
@classmethod
|
55
|
+
def from_code(cls, code: str) -> Game:
|
56
|
+
"""Returns the game instance based on the game code.
|
57
|
+
|
58
|
+
Arguments:
|
59
|
+
code (str): The code of the game.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
Game: The game instance.
|
63
|
+
"""
|
64
|
+
for game in cls.__subclasses__():
|
65
|
+
if game.code and game.code.lower() == code.lower():
|
66
|
+
return game()
|
67
|
+
raise ValueError(f"Game with code {code} not found.")
|
68
|
+
|
69
|
+
@property
|
70
|
+
def template_path(self) -> str:
|
71
|
+
"""Returns the path to the map template file.
|
72
|
+
|
73
|
+
Raises:
|
74
|
+
ValueError: If the map template path is not set.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
str: The path to the map template file."""
|
78
|
+
if not self._map_template_path:
|
79
|
+
raise ValueError("Map template path not set.")
|
80
|
+
return self._map_template_path
|
81
|
+
|
82
|
+
@property
|
83
|
+
def texture_schema(self) -> str:
|
84
|
+
"""Returns the path to the texture layers schema file.
|
85
|
+
|
86
|
+
Raises:
|
87
|
+
ValueError: If the texture layers schema path is not set.
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
str: The path to the texture layers schema file."""
|
91
|
+
if not self._texture_schema:
|
92
|
+
raise ValueError("Texture layers schema path not set.")
|
93
|
+
return self._texture_schema
|
94
|
+
|
95
|
+
def dem_file_path(self, map_directory: str) -> str:
|
96
|
+
"""Returns the path to the DEM file.
|
97
|
+
|
98
|
+
Arguments:
|
99
|
+
map_directory (str): The path to the map directory.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
str: The path to the DEM file.
|
103
|
+
"""
|
104
|
+
raise NotImplementedError
|
105
|
+
|
106
|
+
def weights_dir_path(self, map_directory: str) -> str:
|
107
|
+
"""Returns the path to the weights directory.
|
108
|
+
|
109
|
+
Arguments:
|
110
|
+
map_directory (str): The path to the map directory.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
str: The path to the weights directory."""
|
114
|
+
raise NotImplementedError
|
115
|
+
|
116
|
+
@property
|
117
|
+
def additional_dem_name(self) -> str | None:
|
118
|
+
"""Returns the name of the additional DEM file.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
str | None: The name of the additional DEM file."""
|
122
|
+
return self._additional_dem_name
|
123
|
+
|
124
|
+
|
125
|
+
class FS22(Game):
|
126
|
+
"""Class used to define the game version FS22."""
|
127
|
+
|
128
|
+
code = "FS22"
|
129
|
+
_map_template_path = os.path.join(working_directory, "data", "fs22-map-template.zip")
|
130
|
+
_texture_schema = os.path.join(working_directory, "data", "fs22-texture-schema.json")
|
131
|
+
|
132
|
+
def dem_file_path(self, map_directory: str) -> str:
|
133
|
+
"""Returns the path to the DEM file.
|
134
|
+
|
135
|
+
Arguments:
|
136
|
+
map_directory (str): The path to the map directory.
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
str: The path to the DEM file."""
|
140
|
+
return os.path.join(map_directory, "maps", "map", "data", "map_dem.png")
|
141
|
+
|
142
|
+
def weights_dir_path(self, map_directory: str) -> str:
|
143
|
+
"""Returns the path to the weights directory.
|
144
|
+
|
145
|
+
Arguments:
|
146
|
+
map_directory (str): The path to the map directory.
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
str: The path to the weights directory."""
|
150
|
+
return os.path.join(map_directory, "maps", "map", "data")
|
151
|
+
|
152
|
+
|
153
|
+
class FS25(Game):
|
154
|
+
"""Class used to define the game version FS25."""
|
155
|
+
|
156
|
+
code = "FS25"
|
157
|
+
dem_multipliyer: int = 2
|
158
|
+
_additional_dem_name = "unprocessedHeightMap.png"
|
159
|
+
_map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip")
|
160
|
+
_texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json")
|
161
|
+
|
162
|
+
def dem_file_path(self, map_directory: str) -> str:
|
163
|
+
"""Returns the path to the DEM file.
|
164
|
+
|
165
|
+
Arguments:
|
166
|
+
map_directory (str): The path to the map directory.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
str: The path to the DEM file."""
|
170
|
+
return os.path.join(map_directory, "mapUS", "data", "dem.png")
|
171
|
+
|
172
|
+
def map_xml_path(self, map_directory: str) -> str:
|
173
|
+
"""Returns the path to the map.xml file.
|
174
|
+
|
175
|
+
Arguments:
|
176
|
+
map_directory (str): The path to the map directory.
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
str: The path to the map.xml file.
|
180
|
+
"""
|
181
|
+
return os.path.join(map_directory, "mapUS", "mapUS.xml")
|
182
|
+
|
183
|
+
def weights_dir_path(self, map_directory: str) -> str:
|
184
|
+
"""Returns the path to the weights directory.
|
185
|
+
|
186
|
+
Arguments:
|
187
|
+
map_directory (str): The path to the map directory.
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
str: The path to the weights directory."""
|
191
|
+
return os.path.join(map_directory, "mapUS", "data")
|