maps4fs 1.8.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- maps4fs/__init__.py +22 -0
- maps4fs/generator/__init__.py +1 -0
- maps4fs/generator/background.py +625 -0
- maps4fs/generator/component.py +553 -0
- maps4fs/generator/config.py +109 -0
- maps4fs/generator/dem.py +297 -0
- maps4fs/generator/dtm/__init__.py +0 -0
- maps4fs/generator/dtm/base/wcs.py +71 -0
- maps4fs/generator/dtm/base/wms.py +70 -0
- maps4fs/generator/dtm/bavaria.py +113 -0
- maps4fs/generator/dtm/dtm.py +637 -0
- maps4fs/generator/dtm/england.py +31 -0
- maps4fs/generator/dtm/hessen.py +31 -0
- maps4fs/generator/dtm/niedersachsen.py +39 -0
- maps4fs/generator/dtm/nrw.py +30 -0
- maps4fs/generator/dtm/srtm.py +127 -0
- maps4fs/generator/dtm/usgs.py +87 -0
- maps4fs/generator/dtm/utils.py +61 -0
- maps4fs/generator/game.py +247 -0
- maps4fs/generator/grle.py +470 -0
- maps4fs/generator/i3d.py +624 -0
- maps4fs/generator/map.py +275 -0
- maps4fs/generator/qgis.py +196 -0
- maps4fs/generator/satellite.py +92 -0
- maps4fs/generator/settings.py +187 -0
- maps4fs/generator/texture.py +893 -0
- maps4fs/logger.py +46 -0
- maps4fs/toolbox/__init__.py +1 -0
- maps4fs/toolbox/background.py +63 -0
- maps4fs/toolbox/custom_osm.py +67 -0
- maps4fs/toolbox/dem.py +112 -0
- maps4fs-1.8.0.dist-info/LICENSE.md +190 -0
- maps4fs-1.8.0.dist-info/METADATA +693 -0
- maps4fs-1.8.0.dist-info/RECORD +36 -0
- maps4fs-1.8.0.dist-info/WHEEL +5 -0
- maps4fs-1.8.0.dist-info/top_level.txt +1 -0
maps4fs/generator/i3d.py
ADDED
@@ -0,0 +1,624 @@
|
|
1
|
+
"""This module contains the Config class for map settings and configuration."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import json
|
6
|
+
import os
|
7
|
+
from random import choice, randint, uniform
|
8
|
+
from typing import Generator
|
9
|
+
from xml.etree import ElementTree as ET
|
10
|
+
|
11
|
+
import cv2
|
12
|
+
import numpy as np
|
13
|
+
from tqdm import tqdm
|
14
|
+
|
15
|
+
from maps4fs.generator.component import Component
|
16
|
+
from maps4fs.generator.texture import Texture
|
17
|
+
|
18
|
+
DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
|
19
|
+
NODE_ID_STARTING_VALUE = 2000
|
20
|
+
SPLINES_NODE_ID_STARTING_VALUE = 5000
|
21
|
+
TREE_NODE_ID_STARTING_VALUE = 10000
|
22
|
+
|
23
|
+
|
24
|
+
# pylint: disable=R0903
|
25
|
+
class I3d(Component):
|
26
|
+
"""Component for map i3d file settings and configuration.
|
27
|
+
|
28
|
+
Arguments:
|
29
|
+
game (Game): The game instance for which the map is generated.
|
30
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
31
|
+
map_size (int): The size of the map in pixels.
|
32
|
+
map_rotated_size (int): The size of the map in pixels after rotation.
|
33
|
+
rotation (int): The rotation angle of the map.
|
34
|
+
map_directory (str): The directory where the map files are stored.
|
35
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
36
|
+
info, warning. If not provided, default logging will be used.
|
37
|
+
"""
|
38
|
+
|
39
|
+
_map_i3d_path: str | None = None
|
40
|
+
|
41
|
+
def preprocess(self) -> None:
|
42
|
+
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
43
|
+
attribute. If the game does not support I3D files, the attribute is set to None."""
|
44
|
+
try:
|
45
|
+
self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
|
46
|
+
self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
|
47
|
+
except NotImplementedError:
|
48
|
+
self.logger.warning("I3D file processing is not implemented for this game.")
|
49
|
+
self._map_i3d_path = None
|
50
|
+
|
51
|
+
def process(self) -> None:
|
52
|
+
"""Updates the map I3D file with the default settings."""
|
53
|
+
self._update_i3d_file()
|
54
|
+
self._add_fields()
|
55
|
+
if self.game.code == "FS25":
|
56
|
+
self._add_forests()
|
57
|
+
self._add_splines()
|
58
|
+
|
59
|
+
def _get_tree(self) -> ET.ElementTree | None:
|
60
|
+
"""Returns the ElementTree instance of the map I3D file."""
|
61
|
+
if not self._map_i3d_path:
|
62
|
+
self.logger.debug("I3D is not obtained, skipping the update.")
|
63
|
+
return None
|
64
|
+
if not os.path.isfile(self._map_i3d_path):
|
65
|
+
self.logger.warning("I3D file not found: %s.", self._map_i3d_path)
|
66
|
+
return None
|
67
|
+
|
68
|
+
return ET.parse(self._map_i3d_path)
|
69
|
+
|
70
|
+
def _update_i3d_file(self) -> None:
|
71
|
+
"""Updates the map I3D file with the default settings."""
|
72
|
+
|
73
|
+
tree = self._get_tree()
|
74
|
+
if tree is None:
|
75
|
+
return
|
76
|
+
|
77
|
+
self.logger.debug("Map I3D file loaded from: %s.", self._map_i3d_path)
|
78
|
+
|
79
|
+
root = tree.getroot()
|
80
|
+
for map_elem in root.iter("Scene"):
|
81
|
+
for terrain_elem in map_elem.iter("TerrainTransformGroup"):
|
82
|
+
if self.map.shared_settings.change_height_scale:
|
83
|
+
suggested_height_scale = self.map.shared_settings.height_scale_value
|
84
|
+
if suggested_height_scale is not None and suggested_height_scale > 255:
|
85
|
+
new_height_scale = int(
|
86
|
+
self.map.shared_settings.height_scale_value # type: ignore
|
87
|
+
)
|
88
|
+
terrain_elem.set("heightScale", str(new_height_scale))
|
89
|
+
self.logger.info(
|
90
|
+
"heightScale attribute set to %s in TerrainTransformGroup element.",
|
91
|
+
new_height_scale,
|
92
|
+
)
|
93
|
+
|
94
|
+
self.logger.debug("TerrainTransformGroup element updated in I3D file.")
|
95
|
+
sun_elem = map_elem.find(".//Light[@name='sun']")
|
96
|
+
|
97
|
+
if sun_elem is not None:
|
98
|
+
self.logger.debug("Sun element found in I3D file.")
|
99
|
+
|
100
|
+
distance = self.map_size // 2
|
101
|
+
|
102
|
+
sun_elem.set("lastShadowMapSplitBboxMin", f"-{distance},-128,-{distance}")
|
103
|
+
sun_elem.set("lastShadowMapSplitBboxMax", f"{distance},148,{distance}")
|
104
|
+
|
105
|
+
self.logger.debug(
|
106
|
+
"Sun BBOX updated with half of the map size: %s.",
|
107
|
+
distance,
|
108
|
+
)
|
109
|
+
|
110
|
+
if self.map_size > 4096:
|
111
|
+
displacement_layer = terrain_elem.find(".//DisplacementLayer") # pylint: disable=W0631
|
112
|
+
|
113
|
+
if displacement_layer is not None:
|
114
|
+
displacement_layer.set("size", str(DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS))
|
115
|
+
self.logger.debug(
|
116
|
+
"Map size is greater than 4096, DisplacementLayer size set to %s.",
|
117
|
+
)
|
118
|
+
|
119
|
+
tree.write(self._map_i3d_path) # type: ignore
|
120
|
+
self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
|
121
|
+
|
122
|
+
def previews(self) -> list[str]:
|
123
|
+
"""Returns a list of paths to the preview images (empty list).
|
124
|
+
The component does not generate any preview images so it returns an empty list.
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
list[str]: An empty list.
|
128
|
+
"""
|
129
|
+
return []
|
130
|
+
|
131
|
+
# pylint: disable=R0914, R0915
|
132
|
+
def _add_splines(self) -> None:
|
133
|
+
"""Adds splines to the map I3D file."""
|
134
|
+
splines_i3d_path = os.path.join(self.map_directory, "map", "splines.i3d")
|
135
|
+
if not os.path.isfile(splines_i3d_path):
|
136
|
+
self.logger.warning("Splines I3D file not found: %s.", splines_i3d_path)
|
137
|
+
return
|
138
|
+
|
139
|
+
tree = ET.parse(splines_i3d_path)
|
140
|
+
|
141
|
+
textures_info_layer_path = self.get_infolayer_path("textures")
|
142
|
+
if not textures_info_layer_path:
|
143
|
+
return
|
144
|
+
|
145
|
+
with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
|
146
|
+
textures_info_layer = json.load(textures_info_layer_file)
|
147
|
+
|
148
|
+
roads_polylines: list[list[tuple[int, int]]] | None = textures_info_layer.get(
|
149
|
+
"roads_polylines"
|
150
|
+
)
|
151
|
+
|
152
|
+
if not roads_polylines:
|
153
|
+
self.logger.warning("Roads polylines data not found in textures info layer.")
|
154
|
+
return
|
155
|
+
|
156
|
+
self.logger.debug("Found %s roads polylines in textures info layer.", len(roads_polylines))
|
157
|
+
self.logger.debug("Starging to add roads polylines to the I3D file.")
|
158
|
+
|
159
|
+
root = tree.getroot()
|
160
|
+
# Find <Shapes> element in the I3D file.
|
161
|
+
shapes_node = root.find(".//Shapes")
|
162
|
+
# Find <Scene> element in the I3D file.
|
163
|
+
scene_node = root.find(".//Scene")
|
164
|
+
|
165
|
+
# Read the not resized DEM to obtain Z values for spline points.
|
166
|
+
background_component = self.map.get_component("Background")
|
167
|
+
if not background_component:
|
168
|
+
self.logger.warning("Background component not found.")
|
169
|
+
return
|
170
|
+
|
171
|
+
# pylint: disable=no-member
|
172
|
+
not_resized_dem = cv2.imread(
|
173
|
+
background_component.not_resized_path, cv2.IMREAD_UNCHANGED # type: ignore
|
174
|
+
)
|
175
|
+
self.logger.debug(
|
176
|
+
"Not resized DEM loaded from: %s. Shape: %s.",
|
177
|
+
background_component.not_resized_path, # type: ignore
|
178
|
+
not_resized_dem.shape,
|
179
|
+
)
|
180
|
+
dem_x_size, dem_y_size = not_resized_dem.shape
|
181
|
+
|
182
|
+
if shapes_node is not None and scene_node is not None:
|
183
|
+
node_id = SPLINES_NODE_ID_STARTING_VALUE
|
184
|
+
user_attributes_node = root.find(".//UserAttributes")
|
185
|
+
if user_attributes_node is None:
|
186
|
+
self.logger.warning("UserAttributes node not found in I3D file.")
|
187
|
+
return
|
188
|
+
|
189
|
+
for road_id, road in enumerate(roads_polylines, start=1):
|
190
|
+
# Add to scene node
|
191
|
+
# <Shape name="spline01_CSV" translation="0 0 0" nodeId="11" shapeId="11"/>
|
192
|
+
|
193
|
+
try:
|
194
|
+
fitted_road = self.fit_object_into_bounds(
|
195
|
+
linestring_points=road, angle=self.rotation
|
196
|
+
)
|
197
|
+
except ValueError as e:
|
198
|
+
self.logger.debug(
|
199
|
+
"Road %s could not be fitted into the map bounds with error: %s",
|
200
|
+
road_id,
|
201
|
+
e,
|
202
|
+
)
|
203
|
+
continue
|
204
|
+
|
205
|
+
self.logger.debug("Road %s has %s points.", road_id, len(fitted_road))
|
206
|
+
fitted_road = self.interpolate_points(
|
207
|
+
fitted_road, num_points=self.map.spline_settings.spline_density
|
208
|
+
)
|
209
|
+
self.logger.debug(
|
210
|
+
"Road %s has %s points after interpolation.", road_id, len(fitted_road)
|
211
|
+
)
|
212
|
+
|
213
|
+
spline_name = f"spline{road_id}"
|
214
|
+
|
215
|
+
shape_node = ET.Element("Shape")
|
216
|
+
shape_node.set("name", spline_name)
|
217
|
+
shape_node.set("translation", "0 0 0")
|
218
|
+
shape_node.set("nodeId", str(node_id))
|
219
|
+
shape_node.set("shapeId", str(node_id))
|
220
|
+
|
221
|
+
scene_node.append(shape_node)
|
222
|
+
|
223
|
+
road_ccs = [self.top_left_coordinates_to_center(point) for point in fitted_road]
|
224
|
+
|
225
|
+
# Add to shapes node
|
226
|
+
# <NurbsCurve name="spline01_CSV" shapeId="11" degree="3" form="open">
|
227
|
+
|
228
|
+
nurbs_curve_node = ET.Element("NurbsCurve")
|
229
|
+
nurbs_curve_node.set("name", spline_name)
|
230
|
+
nurbs_curve_node.set("shapeId", str(node_id))
|
231
|
+
nurbs_curve_node.set("degree", "3")
|
232
|
+
nurbs_curve_node.set("form", "open")
|
233
|
+
|
234
|
+
# Now for each point in the road add the following entry to nurbs_curve_node
|
235
|
+
# <cv c="-224.548401, 427.297546, -2047.570312" />
|
236
|
+
# The second coordinate (Z) will be 0 at the moment.
|
237
|
+
|
238
|
+
for point_ccs, point in zip(road_ccs, fitted_road):
|
239
|
+
cx, cy = point_ccs
|
240
|
+
x, y = point
|
241
|
+
|
242
|
+
x = int(x)
|
243
|
+
y = int(y)
|
244
|
+
|
245
|
+
x = max(0, min(x, dem_x_size - 1))
|
246
|
+
y = max(0, min(y, dem_y_size - 1))
|
247
|
+
|
248
|
+
z = not_resized_dem[y, x]
|
249
|
+
z *= self.get_z_scaling_factor() # type: ignore
|
250
|
+
|
251
|
+
cv_node = ET.Element("cv")
|
252
|
+
cv_node.set("c", f"{cx}, {z}, {cy}")
|
253
|
+
|
254
|
+
nurbs_curve_node.append(cv_node)
|
255
|
+
|
256
|
+
shapes_node.append(nurbs_curve_node)
|
257
|
+
|
258
|
+
# Add UserAttributes to the shape node.
|
259
|
+
# <UserAttribute nodeId="5000">
|
260
|
+
# <Attribute name="maxSpeedScale" type="integer" value="1"/>
|
261
|
+
# <Attribute name="speedLimit" type="integer" value="100"/>
|
262
|
+
# </UserAttribute>
|
263
|
+
|
264
|
+
user_attribute_node = ET.Element("UserAttribute")
|
265
|
+
user_attribute_node.set("nodeId", str(node_id))
|
266
|
+
|
267
|
+
attributes = [
|
268
|
+
("maxSpeedScale", "integer", "1"),
|
269
|
+
("speedLimit", "integer", "100"),
|
270
|
+
]
|
271
|
+
|
272
|
+
for name, attr_type, value in attributes:
|
273
|
+
user_attribute_node.append(I3d.create_attribute_node(name, attr_type, value))
|
274
|
+
|
275
|
+
user_attributes_node.append(user_attribute_node) # type: ignore
|
276
|
+
|
277
|
+
node_id += 1
|
278
|
+
|
279
|
+
tree.write(splines_i3d_path) # type: ignore
|
280
|
+
self.logger.debug("Splines I3D file saved to: %s.", splines_i3d_path)
|
281
|
+
|
282
|
+
# pylint: disable=R0914, R0915
|
283
|
+
def _add_fields(self) -> None:
|
284
|
+
"""Adds fields to the map I3D file."""
|
285
|
+
tree = self._get_tree()
|
286
|
+
if tree is None:
|
287
|
+
return
|
288
|
+
|
289
|
+
textures_info_layer_path = self.get_infolayer_path("textures")
|
290
|
+
if not textures_info_layer_path:
|
291
|
+
return
|
292
|
+
|
293
|
+
border = 0
|
294
|
+
textures_layer: Texture | None = self.map.get_component("Texture") # type: ignore
|
295
|
+
if textures_layer:
|
296
|
+
border = textures_layer.get_layer_by_usage("field").border # type: ignore
|
297
|
+
|
298
|
+
with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
|
299
|
+
textures_info_layer = json.load(textures_info_layer_file)
|
300
|
+
|
301
|
+
fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
|
302
|
+
if not fields:
|
303
|
+
self.logger.warning("Fields data not found in textures info layer.")
|
304
|
+
return
|
305
|
+
|
306
|
+
self.logger.debug("Found %s fields in textures info layer.", len(fields))
|
307
|
+
self.logger.debug("Starging to add fields to the I3D file.")
|
308
|
+
|
309
|
+
root = tree.getroot()
|
310
|
+
gameplay_node = root.find(".//TransformGroup[@name='gameplay']")
|
311
|
+
if gameplay_node is not None:
|
312
|
+
fields_node = gameplay_node.find(".//TransformGroup[@name='fields']")
|
313
|
+
user_attributes_node = root.find(".//UserAttributes")
|
314
|
+
|
315
|
+
if fields_node is not None:
|
316
|
+
node_id = NODE_ID_STARTING_VALUE
|
317
|
+
|
318
|
+
# Not using enumerate because in case of the error, we do not increment
|
319
|
+
# the field_id. So as a result we do not have a gap in the field IDs.
|
320
|
+
field_id = 1
|
321
|
+
|
322
|
+
for field in tqdm(fields, desc="Adding fields", unit="field"):
|
323
|
+
try:
|
324
|
+
fitted_field = self.fit_object_into_bounds(
|
325
|
+
polygon_points=field, angle=self.rotation, border=border
|
326
|
+
)
|
327
|
+
except ValueError as e:
|
328
|
+
self.logger.debug(
|
329
|
+
"Field %s could not be fitted into the map bounds with error: %s",
|
330
|
+
field_id,
|
331
|
+
e,
|
332
|
+
)
|
333
|
+
continue
|
334
|
+
|
335
|
+
field_ccs = [
|
336
|
+
self.top_left_coordinates_to_center(point) for point in fitted_field
|
337
|
+
]
|
338
|
+
|
339
|
+
try:
|
340
|
+
cx, cy = self.get_polygon_center(field_ccs)
|
341
|
+
except Exception as e: # pylint: disable=W0718
|
342
|
+
self.logger.debug(
|
343
|
+
"Field %s could not be fitted into the map bounds.", field_id
|
344
|
+
)
|
345
|
+
self.logger.debug("Error: %s", e)
|
346
|
+
continue
|
347
|
+
|
348
|
+
# Creating the main field node.
|
349
|
+
field_node = ET.Element("TransformGroup")
|
350
|
+
field_node.set("name", f"field{field_id}")
|
351
|
+
field_node.set("translation", f"{cx} 0 {cy}")
|
352
|
+
field_node.set("nodeId", str(node_id))
|
353
|
+
|
354
|
+
# Adding UserAttributes to the field node.
|
355
|
+
user_attribute_node = self.create_user_attribute_node(node_id)
|
356
|
+
user_attributes_node.append(user_attribute_node) # type: ignore
|
357
|
+
|
358
|
+
node_id += 1
|
359
|
+
|
360
|
+
# Creating the polygon points node, which contains the points of the field.
|
361
|
+
polygon_points_node = ET.Element("TransformGroup")
|
362
|
+
polygon_points_node.set("name", "polygonPoints")
|
363
|
+
polygon_points_node.set("nodeId", str(node_id))
|
364
|
+
node_id += 1
|
365
|
+
|
366
|
+
for point_id, point in enumerate(field_ccs, start=1):
|
367
|
+
rx, ry = self.absolute_to_relative(point, (cx, cy))
|
368
|
+
|
369
|
+
node_id += 1
|
370
|
+
point_node = ET.Element("TransformGroup")
|
371
|
+
point_node.set("name", f"point{point_id}")
|
372
|
+
point_node.set("translation", f"{rx} 0 {ry}")
|
373
|
+
point_node.set("nodeId", str(node_id))
|
374
|
+
|
375
|
+
polygon_points_node.append(point_node)
|
376
|
+
|
377
|
+
field_node.append(polygon_points_node)
|
378
|
+
|
379
|
+
# Adding the name indicator node to the field node.
|
380
|
+
name_indicator_node, node_id = self.get_name_indicator_node(node_id, field_id)
|
381
|
+
field_node.append(name_indicator_node)
|
382
|
+
|
383
|
+
# Adding the teleport indicator node to the field node.
|
384
|
+
teleport_indicator_node, node_id = self.get_teleport_indicator_node(node_id)
|
385
|
+
field_node.append(teleport_indicator_node)
|
386
|
+
|
387
|
+
# Adding the field node to the fields node.
|
388
|
+
fields_node.append(field_node)
|
389
|
+
self.logger.debug("Field %s added to the I3D file.", field_id)
|
390
|
+
|
391
|
+
node_id += 1
|
392
|
+
field_id += 1
|
393
|
+
|
394
|
+
tree.write(self._map_i3d_path) # type: ignore
|
395
|
+
self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
|
396
|
+
|
397
|
+
def get_name_indicator_node(self, node_id: int, field_id: int) -> tuple[ET.Element, int]:
|
398
|
+
"""Creates a name indicator node with given node ID and field ID.
|
399
|
+
|
400
|
+
Arguments:
|
401
|
+
node_id (int): The node ID of the name indicator node.
|
402
|
+
field_id (int): The ID of the field.
|
403
|
+
|
404
|
+
Returns:
|
405
|
+
tuple[ET.Element, int]: The name indicator node and the updated node ID.
|
406
|
+
"""
|
407
|
+
node_id += 1
|
408
|
+
name_indicator_node = ET.Element("TransformGroup")
|
409
|
+
name_indicator_node.set("name", "nameIndicator")
|
410
|
+
name_indicator_node.set("nodeId", str(node_id))
|
411
|
+
|
412
|
+
node_id += 1
|
413
|
+
note_node = ET.Element("Note")
|
414
|
+
note_node.set("name", "Note")
|
415
|
+
note_node.set("nodeId", str(node_id))
|
416
|
+
note_node.set("text", f"field{field_id}
0.00 ha")
|
417
|
+
note_node.set("color", "4278190080")
|
418
|
+
note_node.set("fixedSize", "true")
|
419
|
+
|
420
|
+
name_indicator_node.append(note_node)
|
421
|
+
|
422
|
+
return name_indicator_node, node_id
|
423
|
+
|
424
|
+
def get_teleport_indicator_node(self, node_id: int) -> tuple[ET.Element, int]:
|
425
|
+
"""Creates a teleport indicator node with given node ID.
|
426
|
+
|
427
|
+
Arguments:
|
428
|
+
node_id (int): The node ID of the teleport indicator node.
|
429
|
+
|
430
|
+
Returns:
|
431
|
+
tuple[ET.Element, int]: The teleport indicator node and the updated node ID.
|
432
|
+
"""
|
433
|
+
node_id += 1
|
434
|
+
teleport_indicator_node = ET.Element("TransformGroup")
|
435
|
+
teleport_indicator_node.set("name", "teleportIndicator")
|
436
|
+
teleport_indicator_node.set("nodeId", str(node_id))
|
437
|
+
|
438
|
+
return teleport_indicator_node, node_id
|
439
|
+
|
440
|
+
@staticmethod
|
441
|
+
def create_user_attribute_node(node_id: int) -> ET.Element:
|
442
|
+
"""Creates an XML user attribute node with given node ID.
|
443
|
+
|
444
|
+
Arguments:
|
445
|
+
node_id (int): The node ID of the user attribute node.
|
446
|
+
|
447
|
+
Returns:
|
448
|
+
ET.Element: The created user attribute node.
|
449
|
+
"""
|
450
|
+
user_attribute_node = ET.Element("UserAttribute")
|
451
|
+
user_attribute_node.set("nodeId", str(node_id))
|
452
|
+
|
453
|
+
attributes = [
|
454
|
+
("angle", "integer", "0"),
|
455
|
+
("missionAllowed", "boolean", "true"),
|
456
|
+
("missionOnlyGrass", "boolean", "false"),
|
457
|
+
("nameIndicatorIndex", "string", "1"),
|
458
|
+
("polygonIndex", "string", "0"),
|
459
|
+
("teleportIndicatorIndex", "string", "2"),
|
460
|
+
]
|
461
|
+
|
462
|
+
for name, attr_type, value in attributes:
|
463
|
+
user_attribute_node.append(I3d.create_attribute_node(name, attr_type, value))
|
464
|
+
|
465
|
+
return user_attribute_node
|
466
|
+
|
467
|
+
@staticmethod
|
468
|
+
def create_attribute_node(name: str, attr_type: str, value: str) -> ET.Element:
|
469
|
+
"""Creates an XML attribute node with given name, type, and value.
|
470
|
+
|
471
|
+
Arguments:
|
472
|
+
name (str): The name of the attribute.
|
473
|
+
attr_type (str): The type of the attribute.
|
474
|
+
value (str): The value of the attribute.
|
475
|
+
|
476
|
+
Returns:
|
477
|
+
ET.Element: The created attribute node.
|
478
|
+
"""
|
479
|
+
attribute_node = ET.Element("Attribute")
|
480
|
+
attribute_node.set("name", name)
|
481
|
+
attribute_node.set("type", attr_type)
|
482
|
+
attribute_node.set("value", value)
|
483
|
+
return attribute_node
|
484
|
+
|
485
|
+
# pylint: disable=R0911
|
486
|
+
def _add_forests(self) -> None:
|
487
|
+
"""Adds forests to the map I3D file."""
|
488
|
+
custom_schema = self.kwargs.get("tree_custom_schema")
|
489
|
+
if custom_schema:
|
490
|
+
tree_schema = custom_schema
|
491
|
+
else:
|
492
|
+
try:
|
493
|
+
tree_schema_path = self.game.tree_schema
|
494
|
+
except ValueError:
|
495
|
+
self.logger.warning("Tree schema path not set for the Game %s.", self.game.code)
|
496
|
+
return
|
497
|
+
|
498
|
+
if not os.path.isfile(tree_schema_path):
|
499
|
+
self.logger.warning("Tree schema file was not found: %s.", tree_schema_path)
|
500
|
+
return
|
501
|
+
|
502
|
+
try:
|
503
|
+
with open(tree_schema_path, "r", encoding="utf-8") as tree_schema_file:
|
504
|
+
tree_schema = json.load(tree_schema_file) # type: ignore
|
505
|
+
except json.JSONDecodeError as e:
|
506
|
+
self.logger.warning(
|
507
|
+
"Could not load tree schema from %s with error: %s", tree_schema_path, e
|
508
|
+
)
|
509
|
+
return
|
510
|
+
|
511
|
+
texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
|
512
|
+
if not texture_component:
|
513
|
+
self.logger.warning("Texture component not found.")
|
514
|
+
return
|
515
|
+
|
516
|
+
forest_layer = texture_component.get_layer_by_usage("forest")
|
517
|
+
|
518
|
+
if not forest_layer:
|
519
|
+
self.logger.warning("Forest layer not found.")
|
520
|
+
return
|
521
|
+
|
522
|
+
weights_directory = self.game.weights_dir_path(self.map_directory)
|
523
|
+
forest_image_path = forest_layer.get_preview_or_path(weights_directory)
|
524
|
+
|
525
|
+
if not forest_image_path or not os.path.isfile(forest_image_path):
|
526
|
+
self.logger.warning("Forest image not found.")
|
527
|
+
return
|
528
|
+
|
529
|
+
tree = self._get_tree()
|
530
|
+
if tree is None:
|
531
|
+
return
|
532
|
+
|
533
|
+
# Find the <Scene> element in the I3D file.
|
534
|
+
root = tree.getroot()
|
535
|
+
scene_node = root.find(".//Scene")
|
536
|
+
if scene_node is None:
|
537
|
+
self.logger.warning("Scene element not found in I3D file.")
|
538
|
+
return
|
539
|
+
|
540
|
+
self.logger.debug("Scene element found in I3D file, starting to add forests.")
|
541
|
+
|
542
|
+
node_id = TREE_NODE_ID_STARTING_VALUE
|
543
|
+
|
544
|
+
# Create <TransformGroup name="trees" translation="0 400 0" nodeId="{node_id}"> element.
|
545
|
+
trees_node = ET.Element("TransformGroup")
|
546
|
+
trees_node.set("name", "trees")
|
547
|
+
trees_node.set("translation", "0 400 0")
|
548
|
+
trees_node.set("nodeId", str(node_id))
|
549
|
+
node_id += 1
|
550
|
+
|
551
|
+
# pylint: disable=no-member
|
552
|
+
forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
|
553
|
+
|
554
|
+
tree_count = 0
|
555
|
+
for x, y in self.non_empty_pixels(forest_image, step=self.map.i3d_settings.forest_density):
|
556
|
+
xcs, ycs = self.top_left_coordinates_to_center((x, y))
|
557
|
+
node_id += 1
|
558
|
+
|
559
|
+
rotation = randint(-180, 180)
|
560
|
+
xcs, ycs = self.randomize_coordinates( # type: ignore
|
561
|
+
(xcs, ycs), self.map.i3d_settings.forest_density
|
562
|
+
)
|
563
|
+
|
564
|
+
random_tree = choice(tree_schema) # type: ignore
|
565
|
+
tree_name = random_tree["name"]
|
566
|
+
tree_id = random_tree["reference_id"]
|
567
|
+
|
568
|
+
reference_node = ET.Element("ReferenceNode")
|
569
|
+
reference_node.set("name", tree_name) # type: ignore
|
570
|
+
reference_node.set("translation", f"{xcs} 0 {ycs}")
|
571
|
+
reference_node.set("rotation", f"0 {rotation} 0")
|
572
|
+
reference_node.set("referenceId", str(tree_id))
|
573
|
+
reference_node.set("nodeId", str(node_id))
|
574
|
+
|
575
|
+
trees_node.append(reference_node)
|
576
|
+
tree_count += 1
|
577
|
+
|
578
|
+
scene_node.append(trees_node)
|
579
|
+
self.logger.debug("Added %s trees to the I3D file.", tree_count)
|
580
|
+
|
581
|
+
tree.write(self._map_i3d_path) # type: ignore
|
582
|
+
self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
|
583
|
+
|
584
|
+
@staticmethod
|
585
|
+
def randomize_coordinates(coordinates: tuple[int, int], density: int) -> tuple[float, float]:
|
586
|
+
"""Randomizes the coordinates of the point with the given density.
|
587
|
+
|
588
|
+
Arguments:
|
589
|
+
coordinates (tuple[int, int]): The coordinates of the point.
|
590
|
+
density (int): The density of the randomization.
|
591
|
+
|
592
|
+
Returns:
|
593
|
+
tuple[float, float]: The randomized coordinates of the point.
|
594
|
+
"""
|
595
|
+
MAXIMUM_RELATIVE_SHIFT = 0.2 # pylint: disable=C0103
|
596
|
+
shift_range = density * MAXIMUM_RELATIVE_SHIFT
|
597
|
+
|
598
|
+
x_shift = uniform(-shift_range, shift_range)
|
599
|
+
y_shift = uniform(-shift_range, shift_range)
|
600
|
+
|
601
|
+
x, y = coordinates
|
602
|
+
x += x_shift # type: ignore
|
603
|
+
y += y_shift # type: ignore
|
604
|
+
|
605
|
+
return x, y
|
606
|
+
|
607
|
+
@staticmethod
|
608
|
+
def non_empty_pixels(
|
609
|
+
image: np.ndarray, step: int = 1
|
610
|
+
) -> Generator[tuple[int, int], None, None]:
|
611
|
+
"""Receives numpy array, which represents single-channeled image of uint8 type.
|
612
|
+
Yield coordinates of non-empty pixels (pixels with value greater than 0).
|
613
|
+
|
614
|
+
Arguments:
|
615
|
+
image (np.ndarray): The image to get non-empty pixels from.
|
616
|
+
step (int, optional): The step to iterate through the image. Defaults to 1.
|
617
|
+
|
618
|
+
Yields:
|
619
|
+
tuple[int, int]: The coordinates of non-empty pixels.
|
620
|
+
"""
|
621
|
+
for y, row in enumerate(image[::step]):
|
622
|
+
for x, value in enumerate(row[::step]):
|
623
|
+
if value > 0:
|
624
|
+
yield x * step, y * step
|