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/dem.py CHANGED
@@ -9,11 +9,12 @@ import cv2
9
9
  import numpy as np
10
10
  import rasterio # type: ignore
11
11
  import requests
12
+ from pympler import asizeof # type: ignore
12
13
 
13
14
  from maps4fs.generator.component import Component
14
15
 
15
16
  SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
16
- DEFAULT_MULTIPLIER = 1.0
17
+ DEFAULT_MULTIPLIER = 1
17
18
  DEFAULT_BLUR_RADIUS = 35
18
19
  DEFAULT_PLATEAU = 0
19
20
 
@@ -22,10 +23,12 @@ DEFAULT_PLATEAU = 0
22
23
  class DEM(Component):
23
24
  """Component for processing Digital Elevation Model data.
24
25
 
25
- Args:
26
+ Arguments:
27
+ game (Game): The game instance for which the map is generated.
26
28
  coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
27
- map_height (int): The height of the map in pixels.
28
- map_width (int): The width of the map in pixels.
29
+ map_size (int): The size of the map in pixels.
30
+ map_rotated_size (int): The size of the map in pixels after rotation.
31
+ rotation (int): The rotation angle of the map.
29
32
  map_directory (str): The directory where the map files are stored.
30
33
  logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
31
34
  info, warning. If not provided, default logging will be used.
@@ -39,6 +42,14 @@ class DEM(Component):
39
42
  os.makedirs(self.hgt_dir, exist_ok=True)
40
43
  os.makedirs(self.gz_dir, exist_ok=True)
41
44
 
45
+ self.logger.debug("Map size: %s x %s.", self.map_size, self.map_size)
46
+ self.logger.debug(
47
+ "Map rotated size: %s x %s.", self.map_rotated_size, self.map_rotated_size
48
+ )
49
+
50
+ self.output_resolution = self.get_output_resolution()
51
+ self.logger.debug("Output resolution for DEM data: %s.", self.output_resolution)
52
+
42
53
  self.multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
43
54
  blur_radius = self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS)
44
55
  if blur_radius is None or blur_radius <= 0:
@@ -63,21 +74,61 @@ class DEM(Component):
63
74
  """
64
75
  return self._dem_path
65
76
 
66
- def get_output_resolution(self) -> tuple[int, int]:
77
+ # pylint: disable=W0201
78
+ def set_dem_path(self, dem_path: str) -> None:
79
+ """Set path to the DEM file.
80
+
81
+ Arguments:
82
+ dem_path (str): Path to the DEM file.
83
+ """
84
+ self._dem_path = dem_path
85
+
86
+ # pylint: disable=W0201
87
+ def set_output_resolution(self, output_resolution: tuple[int, int]) -> None:
88
+ """Set output resolution for DEM data (width, height).
89
+
90
+ Arguments:
91
+ output_resolution (tuple[int, int]): Output resolution for DEM data.
92
+ """
93
+ self.output_resolution = output_resolution
94
+
95
+ def get_output_resolution(self, use_original: bool = False) -> tuple[int, int]:
67
96
  """Get output resolution for DEM data.
68
97
 
98
+ Arguments:
99
+ use_original (bool, optional): If True, will use original map size. Defaults to False.
100
+
69
101
  Returns:
70
102
  tuple[int, int]: Output resolution for DEM data.
71
103
  """
72
- dem_height = int((self.map_height / 2) * self.game.dem_multipliyer + 1)
73
- dem_width = int((self.map_width / 2) * self.game.dem_multipliyer + 1)
104
+ map_size = self.map_size if use_original else self.map_rotated_size
105
+
106
+ dem_size = int((map_size / 2) * self.game.dem_multipliyer)
107
+
74
108
  self.logger.debug(
75
- "DEM size multiplier is %s, DEM height is %s, DEM width is %s.",
109
+ "DEM size multiplier is %s, DEM size: %sx%s, use original: %s.",
76
110
  self.game.dem_multipliyer,
77
- dem_height,
78
- dem_width,
111
+ dem_size,
112
+ dem_size,
113
+ use_original,
114
+ )
115
+ return dem_size, dem_size
116
+
117
+ def to_ground(self, data: np.ndarray) -> np.ndarray:
118
+ """Receives the signed 16-bit integer array and converts it to the ground level.
119
+ If the min value is negative, it will become zero value and the rest of the values
120
+ will be shifted accordingly.
121
+ """
122
+ # For examlem, min value was -50, it will become 0 and for all values we'll +50.
123
+
124
+ if data.min() < 0:
125
+ self.logger.debug("Array contains negative values, will be shifted to the ground.")
126
+ data = data + abs(data.min())
127
+
128
+ self.logger.debug(
129
+ "Array was shifted to the ground. Min: %s, max: %s.", data.min(), data.max()
79
130
  )
80
- return dem_width, dem_height
131
+ return data
81
132
 
82
133
  # pylint: disable=no-member
83
134
  def process(self) -> None:
@@ -85,7 +136,7 @@ class DEM(Component):
85
136
  saves to map directory."""
86
137
  north, south, east, west = self.bbox
87
138
 
88
- dem_output_resolution = self.get_output_resolution()
139
+ dem_output_resolution = self.output_resolution
89
140
  self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
90
141
 
91
142
  tile_path = self._srtm_tile()
@@ -119,14 +170,20 @@ class DEM(Component):
119
170
  data.max(),
120
171
  )
121
172
 
173
+ data = self.to_ground(data)
174
+
122
175
  resampled_data = cv2.resize(
123
176
  data, dem_output_resolution, interpolation=cv2.INTER_LINEAR
124
177
  ).astype("uint16")
125
178
 
179
+ size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
180
+ self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
181
+
126
182
  self.logger.debug(
127
- "Maximum value in resampled data: %s, minimum value: %s.",
183
+ "Maximum value in resampled data: %s, minimum value: %s. Data type: %s.",
128
184
  resampled_data.max(),
129
185
  resampled_data.min(),
186
+ resampled_data.dtype,
130
187
  )
131
188
 
132
189
  if self.auto_process:
@@ -135,6 +192,18 @@ class DEM(Component):
135
192
  else:
136
193
  self.logger.debug("Auto processing is disabled, DEM data will not be normalized.")
137
194
  resampled_data = resampled_data * self.multiplier
195
+
196
+ self.logger.debug(
197
+ "DEM data was multiplied by %s. Min: %s, max: %s. Data type: %s.",
198
+ self.multiplier,
199
+ resampled_data.min(),
200
+ resampled_data.max(),
201
+ resampled_data.dtype,
202
+ )
203
+
204
+ size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
205
+ self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
206
+
138
207
  # Clip values to 16-bit unsigned integer range.
139
208
  resampled_data = np.clip(resampled_data, 0, 65535)
140
209
  resampled_data = resampled_data.astype("uint16")
@@ -188,28 +257,31 @@ class DEM(Component):
188
257
  )
189
258
 
190
259
  cv2.imwrite(self._dem_path, resampled_data)
191
- self.logger.debug("DEM data was saved to %s.", self._dem_path)
260
+ self.logger.info("DEM data was saved to %s.", self._dem_path)
192
261
 
193
- if self.game.additional_dem_name is not None:
194
- self.make_copy(self.game.additional_dem_name)
262
+ if self.rotation:
263
+ self.rotate_dem()
195
264
 
196
- def make_copy(self, dem_name: str) -> None:
197
- """Copies DEM data to additional DEM file.
265
+ def rotate_dem(self) -> None:
266
+ """Rotate DEM image."""
267
+ self.logger.debug("Rotating DEM image by %s degrees.", self.rotation)
268
+ output_width, output_height = self.get_output_resolution(use_original=True)
198
269
 
199
- Args:
200
- dem_name (str): Name of the additional DEM file.
201
- """
202
- dem_directory = os.path.dirname(self._dem_path)
203
-
204
- additional_dem_path = os.path.join(dem_directory, dem_name)
270
+ self.logger.debug(
271
+ "Output resolution for rotated DEM: %s x %s.", output_width, output_height
272
+ )
205
273
 
206
- shutil.copyfile(self._dem_path, additional_dem_path)
207
- self.logger.debug("Additional DEM data was copied to %s.", additional_dem_path)
274
+ self.rotate_image(
275
+ self._dem_path,
276
+ self.rotation,
277
+ output_height=output_height,
278
+ output_width=output_width,
279
+ )
208
280
 
209
281
  def _tile_info(self, lat: float, lon: float) -> tuple[str, str]:
210
282
  """Returns latitude band and tile name for SRTM tile from coordinates.
211
283
 
212
- Args:
284
+ Arguments:
213
285
  lat (float): Latitude.
214
286
  lon (float): Longitude.
215
287
 
@@ -265,7 +337,7 @@ class DEM(Component):
265
337
 
266
338
  decompressed_file_path = os.path.join(self.hgt_dir, f"{tile_name}.hgt")
267
339
  if os.path.isfile(decompressed_file_path):
268
- self.logger.info(
340
+ self.logger.debug(
269
341
  "Decompressed tile already exists: %s, skipping download.",
270
342
  decompressed_file_path,
271
343
  )
@@ -284,80 +356,22 @@ class DEM(Component):
284
356
  def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
285
357
  """Saves empty DEM file filled with zeros."""
286
358
  dem_data = np.zeros(dem_output_resolution, dtype="uint16")
287
- cv2.imwrite(self._dem_path, dem_data) # pylint: disable=no-member
359
+ cv2.imwrite(self._dem_path, dem_data)
288
360
  self.logger.warning("DEM data filled with zeros and saved to %s.", self._dem_path)
289
361
 
290
- def grayscale_preview(self) -> str:
291
- """Converts DEM image to grayscale RGB image and saves it to the map directory.
292
- Returns path to the preview image.
362
+ def previews(self) -> list:
363
+ """This component does not have previews, returns empty list.
293
364
 
294
365
  Returns:
295
- str: Path to the preview image.
366
+ list: Empty list.
296
367
  """
297
- # rgb_dem_path = self._dem_path.replace(".png", "_grayscale.png")
298
- grayscale_dem_path = os.path.join(self.previews_directory, "dem_grayscale.png")
299
-
300
- self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
301
-
302
- dem_data = cv2.imread(self._dem_path, cv2.IMREAD_GRAYSCALE)
303
- dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
304
- cv2.imwrite(grayscale_dem_path, dem_data_rgb)
305
- return grayscale_dem_path
306
-
307
- def colored_preview(self) -> str:
308
- """Converts DEM image to colored RGB image and saves it to the map directory.
309
- Returns path to the preview image.
310
-
311
- Returns:
312
- list[str]: List with a single path to the DEM file
313
- """
314
-
315
- # colored_dem_path = self._dem_path.replace(".png", "_colored.png")
316
- colored_dem_path = os.path.join(self.previews_directory, "dem_colored.png")
317
-
318
- self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
319
-
320
- dem_data = cv2.imread(self._dem_path, cv2.IMREAD_GRAYSCALE)
321
-
322
- self.logger.debug(
323
- "DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
324
- dem_data.shape,
325
- dem_data.dtype,
326
- dem_data.min(),
327
- dem_data.max(),
328
- )
329
-
330
- # Create an empty array with the same shape and type as dem_data.
331
- dem_data_normalized = np.empty_like(dem_data)
332
-
333
- # Normalize the DEM data to the range [0, 255]
334
- cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
335
- self.logger.debug(
336
- "DEM data after normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
337
- dem_data_normalized.shape,
338
- dem_data_normalized.dtype,
339
- dem_data_normalized.min(),
340
- dem_data_normalized.max(),
341
- )
342
- dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
343
-
344
- cv2.imwrite(colored_dem_path, dem_data_colored)
345
- return colored_dem_path
346
-
347
- def previews(self) -> list[str]:
348
- """Get list of preview images.
349
-
350
- Returns:
351
- list[str]: List of preview images.
352
- """
353
- self.logger.debug("Starting DEM previews generation.")
354
- return [self.grayscale_preview(), self.colored_preview()]
368
+ return []
355
369
 
356
370
  def _get_scaling_factor(self, maximum_deviation: int) -> float:
357
371
  """Calculate scaling factor for DEM data normalization.
358
372
  NOTE: Needs reconsideration for the implementation.
359
373
 
360
- Args:
374
+ Arguments:
361
375
  maximum_deviation (int): Maximum deviation in DEM data.
362
376
 
363
377
  Returns:
@@ -369,7 +383,7 @@ class DEM(Component):
369
383
 
370
384
  def _normalize_dem(self, data: np.ndarray) -> np.ndarray:
371
385
  """Normalize DEM data to 16-bit unsigned integer using max height from settings.
372
- Args:
386
+ Arguments:
373
387
  data (np.ndarray): DEM data from SRTM file after cropping.
374
388
  Returns:
375
389
  np.ndarray: Normalized DEM data.
@@ -377,9 +391,9 @@ class DEM(Component):
377
391
  self.logger.debug("Starting DEM data normalization.")
378
392
  # Calculate the difference between the maximum and minimum values in the DEM data.
379
393
 
380
- max_height = data.max() # 1800
381
- min_height = data.min() # 1700
382
- max_dev = max_height - min_height # 100
394
+ max_height = data.max()
395
+ min_height = data.min()
396
+ max_dev = max_height - min_height
383
397
  self.logger.debug(
384
398
  "Maximum deviation: %s with maximum at %s and minimum at %s.",
385
399
  max_dev,
@@ -389,14 +403,18 @@ class DEM(Component):
389
403
 
390
404
  scaling_factor = self._get_scaling_factor(max_dev)
391
405
  adjusted_max_height = int(65535 * scaling_factor)
392
- self.logger.debug(
393
- f"Maximum deviation: {max_dev}. Scaling factor: {scaling_factor}. "
394
- f"Adjusted max height: {adjusted_max_height}."
406
+ self.logger.info(
407
+ "Maximum deviation: %s. Scaling factor: %s. Adjusted max height: %s.",
408
+ max_dev,
409
+ scaling_factor,
410
+ adjusted_max_height,
395
411
  )
396
412
  normalized_data = (
397
413
  (data - data.min()) / (data.max() - data.min()) * adjusted_max_height
398
414
  ).astype("uint16")
399
415
  self.logger.debug(
400
- f"DEM data was normalized to {normalized_data.min()} - {normalized_data.max()}."
416
+ "DEM data was normalized to %s - %s.",
417
+ normalized_data.min(),
418
+ normalized_data.max(),
401
419
  )
402
420
  return normalized_data
maps4fs/generator/game.py CHANGED
@@ -8,7 +8,7 @@ import os
8
8
 
9
9
  from maps4fs.generator.background import Background
10
10
  from maps4fs.generator.config import Config
11
- from maps4fs.generator.dem import DEM
11
+ from maps4fs.generator.grle import GRLE
12
12
  from maps4fs.generator.i3d import I3d
13
13
  from maps4fs.generator.texture import Texture
14
14
 
@@ -35,8 +35,10 @@ class Game:
35
35
  _additional_dem_name: str | None = None
36
36
  _map_template_path: str | None = None
37
37
  _texture_schema: str | None = None
38
+ _grle_schema: str | None = None
38
39
 
39
- components = [Config, Texture, DEM, I3d, Background]
40
+ # Order matters! Some components depend on others.
41
+ components = [Texture, I3d, GRLE, Background, Config]
40
42
 
41
43
  def __init__(self, map_template_path: str | None = None):
42
44
  if map_template_path:
@@ -94,6 +96,19 @@ class Game:
94
96
  raise ValueError("Texture layers schema path not set.")
95
97
  return self._texture_schema
96
98
 
99
+ @property
100
+ def grle_schema(self) -> str:
101
+ """Returns the path to the GRLE layers schema file.
102
+
103
+ Raises:
104
+ ValueError: If the GRLE layers schema path is not set.
105
+
106
+ Returns:
107
+ str: The path to the GRLE layers schema file."""
108
+ if not self._grle_schema:
109
+ raise ValueError("GRLE layers schema path not set.")
110
+ return self._grle_schema
111
+
97
112
  def dem_file_path(self, map_directory: str) -> str:
98
113
  """Returns the path to the DEM file.
99
114
 
@@ -171,6 +186,7 @@ class FS25(Game):
171
186
  _additional_dem_name = "unprocessedHeightMap.png"
172
187
  _map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip")
173
188
  _texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json")
189
+ _grle_schema = os.path.join(working_directory, "data", "fs25-grle-schema.json")
174
190
 
175
191
  def dem_file_path(self, map_directory: str) -> str:
176
192
  """Returns the path to the DEM file.
@@ -0,0 +1,175 @@
1
+ """This module contains the GRLE class for generating InfoLayer PNG files based on GRLE schema."""
2
+
3
+ import json
4
+ import os
5
+ from xml.etree import ElementTree as ET
6
+
7
+ import cv2
8
+ import numpy as np
9
+
10
+ from maps4fs.generator.component import Component
11
+
12
+
13
+ # pylint: disable=W0223
14
+ class GRLE(Component):
15
+ """Component for to generate InfoLayer PNG files based on GRLE schema.
16
+
17
+ Arguments:
18
+ game (Game): The game instance for which the map is generated.
19
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
20
+ map_size (int): The size of the map in pixels.
21
+ map_rotated_size (int): The size of the map in pixels after rotation.
22
+ rotation (int): The rotation angle of the map.
23
+ map_directory (str): The directory where the map files are stored.
24
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
25
+ info, warning. If not provided, default logging will be used.
26
+ """
27
+
28
+ _grle_schema: dict[str, float | int | str] | None = None
29
+
30
+ def preprocess(self) -> None:
31
+ """Gets the path to the map I3D file from the game instance and saves it to the instance
32
+ attribute. If the game does not support I3D files, the attribute is set to None."""
33
+
34
+ self.farmland_margin = self.kwargs.get("farmland_margin", 0)
35
+
36
+ try:
37
+ grle_schema_path = self.game.grle_schema
38
+ except ValueError:
39
+ self.logger.info("GRLE schema processing is not implemented for this game.")
40
+ return
41
+
42
+ try:
43
+ with open(grle_schema_path, "r", encoding="utf-8") as file:
44
+ self._grle_schema = json.load(file)
45
+ self.logger.debug("GRLE schema loaded from: %s.", grle_schema_path)
46
+ except (json.JSONDecodeError, FileNotFoundError) as error:
47
+ self.logger.error("Error loading GRLE schema from %s: %s.", grle_schema_path, error)
48
+ self._grle_schema = None
49
+
50
+ def process(self) -> None:
51
+ """Generates InfoLayer PNG files based on the GRLE schema."""
52
+ if not self._grle_schema:
53
+ self.logger.info("GRLE schema is not obtained, skipping the processing.")
54
+ return
55
+
56
+ for info_layer in self._grle_schema:
57
+ if isinstance(info_layer, dict):
58
+ file_path = os.path.join(
59
+ self.game.weights_dir_path(self.map_directory), info_layer["name"]
60
+ )
61
+
62
+ height = int(self.map_size * info_layer["height_multiplier"])
63
+ width = int(self.map_size * info_layer["width_multiplier"])
64
+ channels = info_layer["channels"]
65
+ data_type = info_layer["data_type"]
66
+
67
+ # Create the InfoLayer PNG file with zeros.
68
+ if channels == 1:
69
+ info_layer_data = np.zeros((height, width), dtype=data_type)
70
+ else:
71
+ info_layer_data = np.zeros((height, width, channels), dtype=data_type)
72
+ self.logger.debug("Shape of %s: %s.", info_layer["name"], info_layer_data.shape)
73
+ cv2.imwrite(file_path, info_layer_data) # pylint: disable=no-member
74
+ self.logger.debug("InfoLayer PNG file %s created.", file_path)
75
+ else:
76
+ self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
77
+
78
+ self._add_farmlands()
79
+
80
+ def previews(self) -> list[str]:
81
+ """Returns a list of paths to the preview images (empty list).
82
+ The component does not generate any preview images so it returns an empty list.
83
+
84
+ Returns:
85
+ list[str]: An empty list.
86
+ """
87
+ return []
88
+
89
+ # pylint: disable=R0801, R0914
90
+ def _add_farmlands(self) -> None:
91
+ """Adds farmlands to the InfoLayer PNG file."""
92
+
93
+ textures_info_layer_path = self.get_infolayer_path("textures")
94
+ if not textures_info_layer_path:
95
+ return
96
+
97
+ with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
98
+ textures_info_layer = json.load(textures_info_layer_file)
99
+
100
+ fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
101
+ if not fields:
102
+ self.logger.warning("Fields data not found in textures info layer.")
103
+ return
104
+
105
+ self.logger.info("Found %s fields in textures info layer.", len(fields))
106
+
107
+ info_layer_farmlands_path = os.path.join(
108
+ self.game.weights_dir_path(self.map_directory), "infoLayer_farmlands.png"
109
+ )
110
+
111
+ if not os.path.isfile(info_layer_farmlands_path):
112
+ self.logger.warning("InfoLayer PNG file %s not found.", info_layer_farmlands_path)
113
+ return
114
+
115
+ # pylint: disable=no-member
116
+ image = cv2.imread(info_layer_farmlands_path, cv2.IMREAD_UNCHANGED)
117
+ farmlands_xml_path = os.path.join(self.map_directory, "map/config/farmlands.xml")
118
+ if not os.path.isfile(farmlands_xml_path):
119
+ self.logger.warning("Farmlands XML file %s not found.", farmlands_xml_path)
120
+ return
121
+
122
+ tree = ET.parse(farmlands_xml_path)
123
+ farmlands_xml = tree.find("farmlands")
124
+
125
+ # Not using enumerate because in case of the error, we do not increment
126
+ # the farmland_id. So as a result we do not have a gap in the farmland IDs.
127
+ farmland_id = 1
128
+
129
+ for field in fields:
130
+ try:
131
+ fitted_field = self.fit_polygon_into_bounds(
132
+ field, self.farmland_margin, angle=self.rotation
133
+ )
134
+ except ValueError as e:
135
+ self.logger.warning(
136
+ "Farmland %s could not be fitted into the map bounds with error: %s",
137
+ farmland_id,
138
+ e,
139
+ )
140
+ continue
141
+
142
+ field_np = np.array(fitted_field, np.int32)
143
+ field_np = field_np.reshape((-1, 1, 2))
144
+
145
+ # Infolayer image is 1/2 of the size of the map image, that's why we need to divide
146
+ # the coordinates by 2.
147
+ field_np = field_np // 2
148
+
149
+ # pylint: disable=no-member
150
+ try:
151
+ cv2.fillPoly(image, [field_np], farmland_id) # type: ignore
152
+ except Exception as e: # pylint: disable=W0718
153
+ self.logger.warning(
154
+ "Farmland %s could not be added to the InfoLayer PNG file with error: %s",
155
+ farmland_id,
156
+ e,
157
+ )
158
+ continue
159
+
160
+ # Add the field to the farmlands XML.
161
+ farmland = ET.SubElement(farmlands_xml, "farmland") # type: ignore
162
+ farmland.set("id", str(farmland_id))
163
+ farmland.set("priceScale", "1")
164
+ farmland.set("npcName", "FORESTER")
165
+
166
+ farmland_id += 1
167
+
168
+ tree.write(farmlands_xml_path)
169
+
170
+ self.logger.info("Farmlands added to the farmlands XML file: %s.", farmlands_xml_path)
171
+
172
+ cv2.imwrite(info_layer_farmlands_path, image) # pylint: disable=no-member
173
+ self.logger.info(
174
+ "Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
175
+ )