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/i3d.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import json
|
5
6
|
import os
|
6
7
|
from xml.etree import ElementTree as ET
|
7
8
|
|
@@ -10,16 +11,19 @@ from maps4fs.generator.component import Component
|
|
10
11
|
DEFAULT_HEIGHT_SCALE = 2000
|
11
12
|
DEFAULT_MAX_LOD_DISTANCE = 10000
|
12
13
|
DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
|
14
|
+
NODE_ID_STARTING_VALUE = 500
|
13
15
|
|
14
16
|
|
15
17
|
# pylint: disable=R0903
|
16
18
|
class I3d(Component):
|
17
19
|
"""Component for map i3d file settings and configuration.
|
18
20
|
|
19
|
-
|
21
|
+
Arguments:
|
22
|
+
game (Game): The game instance for which the map is generated.
|
20
23
|
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
21
|
-
|
22
|
-
|
24
|
+
map_size (int): The size of the map in pixels.
|
25
|
+
map_rotated_size (int): The size of the map in pixels after rotation.
|
26
|
+
rotation (int): The rotation angle of the map.
|
23
27
|
map_directory (str): The directory where the map files are stored.
|
24
28
|
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
25
29
|
info, warning. If not provided, default logging will be used.
|
@@ -30,6 +34,8 @@ class I3d(Component):
|
|
30
34
|
def preprocess(self) -> None:
|
31
35
|
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
32
36
|
attribute. If the game does not support I3D files, the attribute is set to None."""
|
37
|
+
self.auto_process = self.kwargs.get("auto_process", False)
|
38
|
+
|
33
39
|
try:
|
34
40
|
self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
|
35
41
|
self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
|
@@ -40,44 +46,46 @@ class I3d(Component):
|
|
40
46
|
def process(self) -> None:
|
41
47
|
"""Updates the map I3D file with the default settings."""
|
42
48
|
self._update_i3d_file()
|
49
|
+
self._add_fields()
|
43
50
|
|
44
|
-
def
|
45
|
-
"""
|
51
|
+
def _get_tree(self) -> ET.ElementTree | None:
|
52
|
+
"""Returns the ElementTree instance of the map I3D file."""
|
46
53
|
if not self._map_i3d_path:
|
47
54
|
self.logger.info("I3D is not obtained, skipping the update.")
|
48
|
-
return
|
55
|
+
return None
|
49
56
|
if not os.path.isfile(self._map_i3d_path):
|
50
57
|
self.logger.warning("I3D file not found: %s.", self._map_i3d_path)
|
51
|
-
return
|
58
|
+
return None
|
52
59
|
|
53
|
-
|
60
|
+
return ET.parse(self._map_i3d_path)
|
61
|
+
|
62
|
+
def _update_i3d_file(self) -> None:
|
63
|
+
"""Updates the map I3D file with the default settings."""
|
64
|
+
|
65
|
+
tree = self._get_tree()
|
66
|
+
if tree is None:
|
67
|
+
return
|
54
68
|
|
55
69
|
self.logger.debug("Map I3D file loaded from: %s.", self._map_i3d_path)
|
56
70
|
|
57
71
|
root = tree.getroot()
|
58
72
|
for map_elem in root.iter("Scene"):
|
59
73
|
for terrain_elem in map_elem.iter("TerrainTransformGroup"):
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
)
|
74
|
+
if self.auto_process:
|
75
|
+
terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
|
76
|
+
self.logger.debug(
|
77
|
+
"heightScale attribute set to %s in TerrainTransformGroup element.",
|
78
|
+
DEFAULT_HEIGHT_SCALE,
|
79
|
+
)
|
80
|
+
else:
|
81
|
+
self.logger.debug(
|
82
|
+
"Auto process is disabled, skipping the heightScale attribute update."
|
83
|
+
)
|
76
84
|
|
77
85
|
self.logger.debug("TerrainTransformGroup element updated in I3D file.")
|
78
86
|
|
79
|
-
tree.write(self._map_i3d_path)
|
80
|
-
self.logger.
|
87
|
+
tree.write(self._map_i3d_path) # type: ignore
|
88
|
+
self.logger.info("Map I3D file saved to: %s.", self._map_i3d_path)
|
81
89
|
|
82
90
|
def previews(self) -> list[str]:
|
83
91
|
"""Returns a list of paths to the preview images (empty list).
|
@@ -87,3 +95,198 @@ class I3d(Component):
|
|
87
95
|
list[str]: An empty list.
|
88
96
|
"""
|
89
97
|
return []
|
98
|
+
|
99
|
+
# pylint: disable=R0914, R0915
|
100
|
+
def _add_fields(self) -> None:
|
101
|
+
"""Adds fields to the map I3D file."""
|
102
|
+
tree = self._get_tree()
|
103
|
+
if tree is None:
|
104
|
+
return
|
105
|
+
|
106
|
+
textures_info_layer_path = self.get_infolayer_path("textures")
|
107
|
+
if not textures_info_layer_path:
|
108
|
+
return
|
109
|
+
|
110
|
+
with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
|
111
|
+
textures_info_layer = json.load(textures_info_layer_file)
|
112
|
+
|
113
|
+
fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
|
114
|
+
if not fields:
|
115
|
+
self.logger.warning("Fields data not found in textures info layer.")
|
116
|
+
return
|
117
|
+
|
118
|
+
self.logger.info("Found %s fields in textures info layer.", len(fields))
|
119
|
+
|
120
|
+
root = tree.getroot()
|
121
|
+
gameplay_node = root.find(".//TransformGroup[@name='gameplay']")
|
122
|
+
if gameplay_node is not None:
|
123
|
+
fields_node = gameplay_node.find(".//TransformGroup[@name='fields']")
|
124
|
+
user_attributes_node = root.find(".//UserAttributes")
|
125
|
+
|
126
|
+
if fields_node is not None:
|
127
|
+
node_id = NODE_ID_STARTING_VALUE
|
128
|
+
|
129
|
+
# Not using enumerate because in case of the error, we do not increment
|
130
|
+
# the field_id. So as a result we do not have a gap in the field IDs.
|
131
|
+
field_id = 1
|
132
|
+
|
133
|
+
for field in fields:
|
134
|
+
try:
|
135
|
+
fitted_field = self.fit_polygon_into_bounds(field, angle=self.rotation)
|
136
|
+
except ValueError as e:
|
137
|
+
self.logger.warning(
|
138
|
+
"Field %s could not be fitted into the map bounds with error: %s",
|
139
|
+
field_id,
|
140
|
+
e,
|
141
|
+
)
|
142
|
+
continue
|
143
|
+
|
144
|
+
field_ccs = [
|
145
|
+
self.top_left_coordinates_to_center(point) for point in fitted_field
|
146
|
+
]
|
147
|
+
|
148
|
+
try:
|
149
|
+
cx, cy = self.get_polygon_center(field_ccs)
|
150
|
+
except Exception as e: # pylint: disable=W0718
|
151
|
+
self.logger.warning(
|
152
|
+
"Field %s could not be fitted into the map bounds.", field_id
|
153
|
+
)
|
154
|
+
self.logger.debug("Error: %s", e)
|
155
|
+
continue
|
156
|
+
|
157
|
+
# Creating the main field node.
|
158
|
+
field_node = ET.Element("TransformGroup")
|
159
|
+
field_node.set("name", f"field{field_id}")
|
160
|
+
field_node.set("translation", f"{cx} 0 {cy}")
|
161
|
+
field_node.set("nodeId", str(node_id))
|
162
|
+
|
163
|
+
# Adding UserAttributes to the field node.
|
164
|
+
user_attribute_node = self.create_user_attribute_node(node_id)
|
165
|
+
user_attributes_node.append(user_attribute_node) # type: ignore
|
166
|
+
|
167
|
+
node_id += 1
|
168
|
+
|
169
|
+
# Creating the polygon points node, which contains the points of the field.
|
170
|
+
polygon_points_node = ET.Element("TransformGroup")
|
171
|
+
polygon_points_node.set("name", "polygonPoints")
|
172
|
+
polygon_points_node.set("nodeId", str(node_id))
|
173
|
+
node_id += 1
|
174
|
+
|
175
|
+
for point_id, point in enumerate(field_ccs, start=1):
|
176
|
+
rx, ry = self.absolute_to_relative(point, (cx, cy))
|
177
|
+
|
178
|
+
node_id += 1
|
179
|
+
point_node = ET.Element("TransformGroup")
|
180
|
+
point_node.set("name", f"point{point_id}")
|
181
|
+
point_node.set("translation", f"{rx} 0 {ry}")
|
182
|
+
point_node.set("nodeId", str(node_id))
|
183
|
+
|
184
|
+
polygon_points_node.append(point_node)
|
185
|
+
|
186
|
+
field_node.append(polygon_points_node)
|
187
|
+
|
188
|
+
# Adding the name indicator node to the field node.
|
189
|
+
name_indicator_node, node_id = self.get_name_indicator_node(node_id, field_id)
|
190
|
+
field_node.append(name_indicator_node)
|
191
|
+
|
192
|
+
# Adding the teleport indicator node to the field node.
|
193
|
+
teleport_indicator_node, node_id = self.get_teleport_indicator_node(node_id)
|
194
|
+
field_node.append(teleport_indicator_node)
|
195
|
+
|
196
|
+
# Adding the field node to the fields node.
|
197
|
+
fields_node.append(field_node)
|
198
|
+
self.logger.debug("Field %s added to the I3D file.", field_id)
|
199
|
+
|
200
|
+
node_id += 1
|
201
|
+
field_id += 1
|
202
|
+
|
203
|
+
tree.write(self._map_i3d_path) # type: ignore
|
204
|
+
self.logger.info("Map I3D file saved to: %s.", self._map_i3d_path)
|
205
|
+
|
206
|
+
def get_name_indicator_node(self, node_id: int, field_id: int) -> tuple[ET.Element, int]:
|
207
|
+
"""Creates a name indicator node with given node ID and field ID.
|
208
|
+
|
209
|
+
Arguments:
|
210
|
+
node_id (int): The node ID of the name indicator node.
|
211
|
+
field_id (int): The ID of the field.
|
212
|
+
|
213
|
+
Returns:
|
214
|
+
tuple[ET.Element, int]: The name indicator node and the updated node ID.
|
215
|
+
"""
|
216
|
+
node_id += 1
|
217
|
+
name_indicator_node = ET.Element("TransformGroup")
|
218
|
+
name_indicator_node.set("name", "nameIndicator")
|
219
|
+
name_indicator_node.set("nodeId", str(node_id))
|
220
|
+
|
221
|
+
node_id += 1
|
222
|
+
note_node = ET.Element("Note")
|
223
|
+
note_node.set("name", "Note")
|
224
|
+
note_node.set("nodeId", str(node_id))
|
225
|
+
note_node.set("text", f"field{field_id}
0.00 ha")
|
226
|
+
note_node.set("color", "4278190080")
|
227
|
+
note_node.set("fixedSize", "true")
|
228
|
+
|
229
|
+
name_indicator_node.append(note_node)
|
230
|
+
|
231
|
+
return name_indicator_node, node_id
|
232
|
+
|
233
|
+
def get_teleport_indicator_node(self, node_id: int) -> tuple[ET.Element, int]:
|
234
|
+
"""Creates a teleport indicator node with given node ID.
|
235
|
+
|
236
|
+
Arguments:
|
237
|
+
node_id (int): The node ID of the teleport indicator node.
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
tuple[ET.Element, int]: The teleport indicator node and the updated node ID.
|
241
|
+
"""
|
242
|
+
node_id += 1
|
243
|
+
teleport_indicator_node = ET.Element("TransformGroup")
|
244
|
+
teleport_indicator_node.set("name", "teleportIndicator")
|
245
|
+
teleport_indicator_node.set("nodeId", str(node_id))
|
246
|
+
|
247
|
+
return teleport_indicator_node, node_id
|
248
|
+
|
249
|
+
@staticmethod
|
250
|
+
def create_user_attribute_node(node_id: int) -> ET.Element:
|
251
|
+
"""Creates an XML user attribute node with given node ID.
|
252
|
+
|
253
|
+
Arguments:
|
254
|
+
node_id (int): The node ID of the user attribute node.
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
ET.Element: The created user attribute node.
|
258
|
+
"""
|
259
|
+
user_attribute_node = ET.Element("UserAttribute")
|
260
|
+
user_attribute_node.set("nodeId", str(node_id))
|
261
|
+
|
262
|
+
attributes = [
|
263
|
+
("angle", "integer", "0"),
|
264
|
+
("missionAllowed", "boolean", "true"),
|
265
|
+
("missionOnlyGrass", "boolean", "false"),
|
266
|
+
("nameIndicatorIndex", "string", "1"),
|
267
|
+
("polygonIndex", "string", "0"),
|
268
|
+
("teleportIndicatorIndex", "string", "2"),
|
269
|
+
]
|
270
|
+
|
271
|
+
for name, attr_type, value in attributes:
|
272
|
+
user_attribute_node.append(I3d.create_attribute_node(name, attr_type, value))
|
273
|
+
|
274
|
+
return user_attribute_node
|
275
|
+
|
276
|
+
@staticmethod
|
277
|
+
def create_attribute_node(name: str, attr_type: str, value: str) -> ET.Element:
|
278
|
+
"""Creates an XML attribute node with given name, type, and value.
|
279
|
+
|
280
|
+
Arguments:
|
281
|
+
name (str): The name of the attribute.
|
282
|
+
attr_type (str): The type of the attribute.
|
283
|
+
value (str): The value of the attribute.
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
ET.Element: The created attribute node.
|
287
|
+
"""
|
288
|
+
attribute_node = ET.Element("Attribute")
|
289
|
+
attribute_node.set("name", name)
|
290
|
+
attribute_node.set("type", attr_type)
|
291
|
+
attribute_node.set("value", value)
|
292
|
+
return attribute_node
|
maps4fs/generator/map.py
CHANGED
@@ -15,11 +15,10 @@ from maps4fs.logger import Logger
|
|
15
15
|
class Map:
|
16
16
|
"""Class used to generate map using all components.
|
17
17
|
|
18
|
-
|
18
|
+
Arguments:
|
19
19
|
game (Type[Game]): Game for which the map is generated.
|
20
20
|
coordinates (tuple[float, float]): Coordinates of the center of the map.
|
21
|
-
|
22
|
-
width (int): Width of the map in pixels.
|
21
|
+
size (int): Height and width of the map in pixels (it's a square).
|
23
22
|
map_directory (str): Path to the directory where map files will be stored.
|
24
23
|
logger (Any): Logger instance
|
25
24
|
"""
|
@@ -28,33 +27,41 @@ class Map:
|
|
28
27
|
self,
|
29
28
|
game: Game,
|
30
29
|
coordinates: tuple[float, float],
|
31
|
-
|
32
|
-
|
30
|
+
size: int,
|
31
|
+
rotation: int,
|
33
32
|
map_directory: str,
|
34
33
|
logger: Any = None,
|
35
34
|
**kwargs,
|
36
35
|
):
|
36
|
+
if not logger:
|
37
|
+
logger = Logger(to_stdout=True, to_file=False)
|
38
|
+
self.logger = logger
|
39
|
+
self.size = size
|
40
|
+
|
41
|
+
if rotation:
|
42
|
+
rotation_multiplier = 1.5
|
43
|
+
else:
|
44
|
+
rotation_multiplier = 1
|
45
|
+
|
46
|
+
self.rotation = rotation
|
47
|
+
self.rotated_size = int(size * rotation_multiplier)
|
48
|
+
|
37
49
|
self.game = game
|
38
50
|
self.components: list[Component] = []
|
39
51
|
self.coordinates = coordinates
|
40
|
-
self.height = height
|
41
|
-
self.width = width
|
42
52
|
self.map_directory = map_directory
|
43
53
|
|
44
|
-
|
45
|
-
logger = Logger(to_stdout=True, to_file=False)
|
46
|
-
self.logger = logger
|
47
|
-
self.logger.debug("Game was set to %s", game.code)
|
54
|
+
self.logger.info("Game was set to %s", game.code)
|
48
55
|
|
49
56
|
self.kwargs = kwargs
|
50
|
-
self.logger.
|
57
|
+
self.logger.info("Additional arguments: %s", kwargs)
|
51
58
|
|
52
59
|
os.makedirs(self.map_directory, exist_ok=True)
|
53
60
|
self.logger.debug("Map directory created: %s", self.map_directory)
|
54
61
|
|
55
62
|
try:
|
56
63
|
shutil.unpack_archive(game.template_path, self.map_directory)
|
57
|
-
self.logger.
|
64
|
+
self.logger.debug("Map template unpacked to %s", self.map_directory)
|
58
65
|
except Exception as e:
|
59
66
|
raise RuntimeError(f"Can not unpack map template due to error: {e}") from e
|
60
67
|
|
@@ -68,8 +75,9 @@ class Map:
|
|
68
75
|
component = game_component(
|
69
76
|
self.game,
|
70
77
|
self.coordinates,
|
71
|
-
self.
|
72
|
-
self.
|
78
|
+
self.size,
|
79
|
+
self.rotated_size,
|
80
|
+
self.rotation,
|
73
81
|
self.map_directory,
|
74
82
|
self.logger,
|
75
83
|
**self.kwargs,
|
@@ -116,15 +124,22 @@ class Map:
|
|
116
124
|
)
|
117
125
|
return previews
|
118
126
|
|
119
|
-
def pack(self,
|
127
|
+
def pack(self, archive_path: str, remove_source: bool = True) -> str:
|
120
128
|
"""Pack map directory to zip archive.
|
121
129
|
|
122
|
-
|
123
|
-
|
130
|
+
Arguments:
|
131
|
+
archive_path (str): Path to the archive.
|
132
|
+
remove_source (bool, optional): Remove source directory after packing.
|
124
133
|
|
125
134
|
Returns:
|
126
135
|
str: Path to the archive.
|
127
136
|
"""
|
128
|
-
archive_path = shutil.make_archive(
|
129
|
-
self.logger.info("Map packed to %s.zip",
|
137
|
+
archive_path = shutil.make_archive(archive_path, "zip", self.map_directory)
|
138
|
+
self.logger.info("Map packed to %s.zip", archive_path)
|
139
|
+
if remove_source:
|
140
|
+
try:
|
141
|
+
shutil.rmtree(self.map_directory)
|
142
|
+
self.logger.info("Map directory removed: %s", self.map_directory)
|
143
|
+
except Exception as e: # pylint: disable=W0718
|
144
|
+
self.logger.error("Error removing map directory %s: %s", self.map_directory, e)
|
130
145
|
return archive_path
|
maps4fs/generator/qgis.py
CHANGED
@@ -115,7 +115,7 @@ for layer in layers:
|
|
115
115
|
def _get_template(layers: list[tuple[str, float, float, float, float]], template: str) -> str:
|
116
116
|
"""Returns a template for creating layers in QGIS.
|
117
117
|
|
118
|
-
|
118
|
+
Arguments:
|
119
119
|
layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
|
120
120
|
layer name and the bounding box coordinates.
|
121
121
|
template (str): The template for creating layers in QGIS.
|
@@ -136,7 +136,7 @@ def _get_template(layers: list[tuple[str, float, float, float, float]], template
|
|
136
136
|
def get_bbox_template(layers: list[tuple[str, float, float, float, float]]) -> str:
|
137
137
|
"""Returns a template for creating bounding box layers in QGIS.
|
138
138
|
|
139
|
-
|
139
|
+
Arguments:
|
140
140
|
layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
|
141
141
|
layer name and the bounding box coordinates.
|
142
142
|
|
@@ -149,7 +149,7 @@ def get_bbox_template(layers: list[tuple[str, float, float, float, float]]) -> s
|
|
149
149
|
def get_point_template(layers: list[tuple[str, float, float, float, float]]) -> str:
|
150
150
|
"""Returns a template for creating point layers in QGIS.
|
151
151
|
|
152
|
-
|
152
|
+
Arguments:
|
153
153
|
layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
|
154
154
|
layer name and the bounding box coordinates.
|
155
155
|
|
@@ -162,7 +162,7 @@ def get_point_template(layers: list[tuple[str, float, float, float, float]]) ->
|
|
162
162
|
def get_rasterize_template(layers: list[tuple[str, float, float, float, float]]) -> str:
|
163
163
|
"""Returns a template for rasterizing bounding box layers in QGIS.
|
164
164
|
|
165
|
-
|
165
|
+
Arguments:
|
166
166
|
layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
|
167
167
|
layer name and the bounding box coordinates.
|
168
168
|
|
@@ -177,7 +177,7 @@ def save_scripts(
|
|
177
177
|
) -> None:
|
178
178
|
"""Saves QGIS scripts for creating bounding box, point, and raster layers.
|
179
179
|
|
180
|
-
|
180
|
+
Arguments:
|
181
181
|
layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
|
182
182
|
layer name and the bounding box coordinates.
|
183
183
|
save_dir (str): The directory to save the scripts.
|