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/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
- Args:
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
- map_height (int): The height of the map in pixels.
22
- map_width (int): The width of the map in pixels.
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 _update_i3d_file(self) -> None:
45
- """Updates the map I3D file with the default settings."""
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
- tree = ET.parse(self._map_i3d_path)
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
- terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
61
- self.logger.debug(
62
- "heightScale attribute set to %s in TerrainTransformGroup element.",
63
- DEFAULT_HEIGHT_SCALE,
64
- )
65
- terrain_elem.set("maxLODDistance", str(DEFAULT_MAX_LOD_DISTANCE))
66
- self.logger.debug(
67
- "maxLODDistance attribute set to %s in TerrainTransformGroup element.",
68
- DEFAULT_MAX_LOD_DISTANCE,
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.debug("Map I3D file saved to: %s.", self._map_i3d_path)
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
- Args:
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
- height (int): Height of the map in pixels.
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
- height: int,
32
- width: int,
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
- if not logger:
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.debug("Additional arguments: %s", kwargs)
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.info("Map template unpacked to %s", self.map_directory)
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.height,
72
- self.width,
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, archive_name: str) -> str:
127
+ def pack(self, archive_path: str, remove_source: bool = True) -> str:
120
128
  """Pack map directory to zip archive.
121
129
 
122
- Args:
123
- archive_name (str): Name of the archive.
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(archive_name, "zip", self.map_directory)
129
- self.logger.info("Map packed to %s.zip", archive_name)
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
- Args:
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
- Args:
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
- Args:
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
- Args:
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
- Args:
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.