maps4fs 1.8.1__py3-none-any.whl → 1.8.12__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- maps4fs/generator/background.py +7 -11
- maps4fs/generator/component/__init__.py +1 -0
- maps4fs/generator/component/base/__init__.py +1 -0
- maps4fs/generator/{component.py → component/base/component.py} +39 -23
- maps4fs/generator/component/base/component_xml.py +95 -0
- maps4fs/generator/{config.py → component/config.py} +15 -30
- maps4fs/generator/component/i3d.py +545 -0
- maps4fs/generator/dem.py +1 -10
- maps4fs/generator/dtm/srtm.py +0 -2
- maps4fs/generator/game.py +33 -2
- maps4fs/generator/grle.py +10 -16
- maps4fs/generator/map.py +41 -1
- maps4fs/generator/satellite.py +1 -2
- maps4fs/generator/settings.py +11 -0
- maps4fs/generator/texture.py +5 -7
- maps4fs/toolbox/background.py +1 -3
- {maps4fs-1.8.1.dist-info → maps4fs-1.8.12.dist-info}/METADATA +5 -1
- maps4fs-1.8.12.dist-info/RECORD +39 -0
- maps4fs/generator/i3d.py +0 -624
- maps4fs-1.8.1.dist-info/RECORD +0 -36
- {maps4fs-1.8.1.dist-info → maps4fs-1.8.12.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.8.1.dist-info → maps4fs-1.8.12.dist-info}/WHEEL +0 -0
- {maps4fs-1.8.1.dist-info → maps4fs-1.8.12.dist-info}/top_level.txt +0 -0
maps4fs/generator/i3d.py
DELETED
@@ -1,624 +0,0 @@
|
|
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
|
maps4fs-1.8.1.dist-info/RECORD
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
maps4fs/__init__.py,sha256=_KfKq_ppTfHH6N7yLHTPjYc0QP2AG1HT8BfYTR9SMJE,853
|
2
|
-
maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
|
3
|
-
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
4
|
-
maps4fs/generator/background.py,sha256=wLiJQroR4IjxpCekVnm030qnrOR9utoKqOVN5n-UwSQ,25023
|
5
|
-
maps4fs/generator/component.py,sha256=p2bn37VlTLWu_pqH96GCV7LgKzj5Y0d3yATpXGegFDI,20950
|
6
|
-
maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
|
7
|
-
maps4fs/generator/dem.py,sha256=20gx0dzX0LyO6ipvDitst-BwGfcKogFqgQf9Q2qMH5U,10933
|
8
|
-
maps4fs/generator/game.py,sha256=GmCl_KQ9D-UwKao4HFEb0PRAm829ThtSZfkgzK3Oh2g,8143
|
9
|
-
maps4fs/generator/grle.py,sha256=wWYzi7tqYy5f1CVbYyk2T92nJJntahtV7wSsVnrEgqM,21181
|
10
|
-
maps4fs/generator/i3d.py,sha256=o90zZU1dEzOaBzgrt2C6ymlYIsI8i2Oicqafz7TC1mo,25148
|
11
|
-
maps4fs/generator/map.py,sha256=a50KQEr1XZKjS_WKXywGwh4OC3gyjY6M8FTc0eNcxpg,10183
|
12
|
-
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
13
|
-
maps4fs/generator/satellite.py,sha256=_7RcuNmR1mjxEJWMDsjnzKUIqWxnGUn50XtjB7HmSPg,3661
|
14
|
-
maps4fs/generator/settings.py,sha256=9vbXISQrE-aDY7ATpvZ7LVJMqjfwa3-gNl-huI8XLO0,5666
|
15
|
-
maps4fs/generator/texture.py,sha256=tH7cbX10WL8WbwmMh_7ndIBNZptNgr_CMGbpGFL3HHM,36584
|
16
|
-
maps4fs/generator/dtm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
-
maps4fs/generator/dtm/bavaria.py,sha256=7njrEvSCYAC8ZVyvS-_84iXHhWA0oHKrEqSzxdnZuGs,4293
|
18
|
-
maps4fs/generator/dtm/dtm.py,sha256=tbYPlpDbFS374ywHrYIJwRflRRzGzi7M5mmcVqwCYBw,22880
|
19
|
-
maps4fs/generator/dtm/england.py,sha256=YyCYwnNUJuBeeMNUozfKIj_yNjHpGeuH1Mz0NiAJL-U,1122
|
20
|
-
maps4fs/generator/dtm/hessen.py,sha256=dYu7TUXICaFMI1sc2l1i3ZDdM5fecXkj5jcz5ZuXIOw,964
|
21
|
-
maps4fs/generator/dtm/niedersachsen.py,sha256=rHScUGrI8JYseBb3xeSN6mdZ7b28YgdlPVCMlluaAMM,1297
|
22
|
-
maps4fs/generator/dtm/nrw.py,sha256=1zMa10O0NdQbiTwOa7XXGrx3NhdHUqRXI4yBn_Scb2M,958
|
23
|
-
maps4fs/generator/dtm/srtm.py,sha256=EYGU20a8c8DnIxzoDAP3rrHYSdgXunwgiJ7R6-FIsuE,4448
|
24
|
-
maps4fs/generator/dtm/usgs.py,sha256=LYtbtecpluAsfS3vhJ2xpVKPbBWerTIJ2uqaM2UmBEw,3228
|
25
|
-
maps4fs/generator/dtm/utils.py,sha256=I-wUSA_J85Xbt8sZCZAVKHSIcrMj5Ng-0adtPVhVmk0,2315
|
26
|
-
maps4fs/generator/dtm/base/wcs.py,sha256=3Irnk9eJPsuRXe1GNpnEK4sWlbpPSJIJUT1V2W-qpKo,2278
|
27
|
-
maps4fs/generator/dtm/base/wms.py,sha256=tevNGOf9VgGYBtc1TTd_oXWHLjU-nLowTIG8QShWDA0,2197
|
28
|
-
maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
29
|
-
maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
|
30
|
-
maps4fs/toolbox/custom_osm.py,sha256=X6ZlPqiOhNjkmdD_qVroIfdOl9Rb90cDwVSLDVYgx80,1892
|
31
|
-
maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
|
32
|
-
maps4fs-1.8.1.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
|
33
|
-
maps4fs-1.8.1.dist-info/METADATA,sha256=ByakHrv8vLLlLLtBcBKd74GOhIm8XjjbKd7YcH4MbvI,41661
|
34
|
-
maps4fs-1.8.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
35
|
-
maps4fs-1.8.1.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
36
|
-
maps4fs-1.8.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|