maps4fs 1.8.13__py3-none-any.whl → 1.8.14__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ """Base class for all components that primarily used to work with images."""
2
+
3
+ import numpy as np
4
+
5
+ from maps4fs.generator.component.base.component import Component
6
+
7
+
8
+ class ImageComponent(Component):
9
+ """Base class for all components that primarily used to work with images."""
10
+
11
+ @staticmethod
12
+ def polygon_points_to_np(
13
+ polygon_points: list[tuple[int, int]], divide: int | None = None
14
+ ) -> np.ndarray:
15
+ """Converts the polygon points to a NumPy array.
16
+
17
+ Arguments:
18
+ polygon_points (list[tuple[int, int]]): The polygon points.
19
+ divide (int, optional): The number to divide the points by. Defaults to None.
20
+
21
+ Returns:
22
+ np.array: The NumPy array of the polygon points.
23
+ """
24
+ array = np.array(polygon_points, dtype=np.int32).reshape((-1, 1, 2))
25
+ if divide:
26
+ return array // divide
27
+ return array
@@ -93,3 +93,16 @@ class XMLComponent(Component):
93
93
  for key, value in data.items():
94
94
  element.set(key, value)
95
95
  return element
96
+
97
+ def create_subelement(
98
+ self, parent: ET.Element, element_name: str, data: dict[str, str]
99
+ ) -> None:
100
+ """Creates a subelement under the parent element with the provided data.
101
+
102
+ Arguments:
103
+ parent (ET.Element): The parent element.
104
+ element_name (str): The name of the subelement.
105
+ data (dict[str, str]): The data to set the subelement attributes to.
106
+ """
107
+ element = ET.SubElement(parent, element_name)
108
+ self.update_element(element, data)
@@ -3,17 +3,15 @@
3
3
  import json
4
4
  import os
5
5
  from random import choice, randint
6
- from xml.etree import ElementTree as ET
7
6
 
8
7
  import cv2
9
8
  import numpy as np
10
- from shapely.geometry import Polygon # type: ignore
9
+ from shapely.geometry import Polygon
11
10
  from tqdm import tqdm
12
11
 
13
- from maps4fs.generator.component.base.component import Component
14
- from maps4fs.generator.texture import PREVIEW_MAXIMUM_SIZE, Texture
15
-
16
- ISLAND_DISTORTION = 0.3
12
+ from maps4fs.generator.component.base.component_image import ImageComponent
13
+ from maps4fs.generator.component.base.component_xml import XMLComponent
14
+ from maps4fs.generator.settings import Parameters
17
15
 
18
16
 
19
17
  def plant_to_pixel_value(plant_name: str) -> int | None:
@@ -33,8 +31,7 @@ def plant_to_pixel_value(plant_name: str) -> int | None:
33
31
  return plants.get(plant_name)
34
32
 
35
33
 
36
- # pylint: disable=W0223
37
- class GRLE(Component):
34
+ class GRLE(ImageComponent, XMLComponent):
38
35
  """Component for to generate InfoLayer PNG files based on GRLE schema.
39
36
 
40
37
  Arguments:
@@ -48,34 +45,41 @@ class GRLE(Component):
48
45
  info, warning. If not provided, default logging will be used.
49
46
  """
50
47
 
51
- _grle_schema: dict[str, float | int | str] | None = None
52
-
53
48
  def preprocess(self) -> None:
54
49
  """Gets the path to the map I3D file from the game instance and saves it to the instance
55
50
  attribute. If the game does not support I3D files, the attribute is set to None."""
56
51
  self.preview_paths: dict[str, str] = {}
52
+ try:
53
+ self.xml_path = self.game.get_farmlands_xml_path(self.map_directory)
54
+ except NotImplementedError:
55
+ self.logger.warning("Farmlands XML file processing is not implemented for this game.")
56
+ self.xml_path = None
57
57
 
58
+ def _read_grle_schema(self) -> dict[str, float | int | str] | None:
58
59
  try:
59
60
  grle_schema_path = self.game.grle_schema
60
61
  except ValueError:
61
62
  self.logger.warning("GRLE schema processing is not implemented for this game.")
62
- return
63
+ return None
63
64
 
64
65
  try:
65
66
  with open(grle_schema_path, "r", encoding="utf-8") as file:
66
- self._grle_schema = json.load(file)
67
+ grle_schema = json.load(file)
67
68
  self.logger.debug("GRLE schema loaded from: %s.", grle_schema_path)
68
69
  except (json.JSONDecodeError, FileNotFoundError) as error:
69
70
  self.logger.error("Error loading GRLE schema from %s: %s.", grle_schema_path, error)
70
- self._grle_schema = None
71
+ grle_schema = None
72
+
73
+ return grle_schema
71
74
 
72
75
  def process(self) -> None:
73
76
  """Generates InfoLayer PNG files based on the GRLE schema."""
74
- if not self._grle_schema:
77
+ grle_schema = self._read_grle_schema()
78
+ if not grle_schema:
75
79
  self.logger.debug("GRLE schema is not obtained, skipping the processing.")
76
80
  return
77
81
 
78
- for info_layer in tqdm(self._grle_schema, desc="Preparing GRLE files", unit="layer"):
82
+ for info_layer in tqdm(grle_schema, desc="Preparing GRLE files", unit="layer"):
79
83
  if isinstance(info_layer, dict):
80
84
  file_path = os.path.join(
81
85
  self.game.weights_dir_path(self.map_directory), info_layer["name"]
@@ -98,8 +102,7 @@ class GRLE(Component):
98
102
  self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
99
103
 
100
104
  self._add_farmlands()
101
- if self.game.code == "FS25":
102
- self.logger.debug("Game is %s, plants will be added.", self.game.code)
105
+ if self.game.plants_processing:
103
106
  self._add_plants()
104
107
  else:
105
108
  self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)
@@ -116,8 +119,13 @@ class GRLE(Component):
116
119
  save_path = os.path.join(self.previews_directory, f"{preview_name}.png")
117
120
  # Resize the preview image to the maximum size allowed for previews.
118
121
  image = cv2.imread(preview_path, cv2.IMREAD_GRAYSCALE)
119
- if image.shape[0] > PREVIEW_MAXIMUM_SIZE or image.shape[1] > PREVIEW_MAXIMUM_SIZE:
120
- image = cv2.resize(image, (PREVIEW_MAXIMUM_SIZE, PREVIEW_MAXIMUM_SIZE))
122
+ if (
123
+ image.shape[0] > Parameters.PREVIEW_MAXIMUM_SIZE
124
+ or image.shape[1] > Parameters.PREVIEW_MAXIMUM_SIZE
125
+ ):
126
+ image = cv2.resize(
127
+ image, (Parameters.PREVIEW_MAXIMUM_SIZE, Parameters.PREVIEW_MAXIMUM_SIZE)
128
+ )
121
129
  image_normalized = np.empty_like(image)
122
130
  cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX)
123
131
  image_colored = cv2.applyColorMap(image_normalized, cv2.COLORMAP_JET)
@@ -144,13 +152,12 @@ class GRLE(Component):
144
152
  Returns:
145
153
  np.ndarray | None: The farmlands preview image with fields overlayed on top of it.
146
154
  """
147
- texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
148
- if not texture_component:
149
- self.logger.warning("Texture component not found in the map.")
155
+ fields_layer = self.map.get_texture_layer(by_usage="field")
156
+ if not fields_layer:
157
+ self.logger.warning("Fields layer not found in the texture component.")
150
158
  return None
151
159
 
152
- fields_layer = texture_component.get_layer_by_usage("field")
153
- fields_layer_path = fields_layer.get_preview_or_path( # type: ignore
160
+ fields_layer_path = fields_layer.get_preview_or_path(
154
161
  self.game.weights_dir_path(self.map_directory)
155
162
  )
156
163
  if not fields_layer_path or not os.path.isfile(fields_layer_path):
@@ -163,24 +170,15 @@ class GRLE(Component):
163
170
  # use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
164
171
  return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
165
172
 
166
- # pylint: disable=R0801, R0914, R0915
167
173
  def _add_farmlands(self) -> None:
168
174
  """Adds farmlands to the InfoLayer PNG file."""
169
-
170
- textures_info_layer_path = self.get_infolayer_path("textures")
171
- if not textures_info_layer_path:
172
- return
173
-
174
- with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
175
- textures_info_layer = json.load(textures_info_layer_file)
176
-
177
175
  farmlands = []
178
- farmyards: list[list[tuple[int, int]]] | None = textures_info_layer.get("farmyards")
176
+ farmyards = self.get_infolayer_data(Parameters.TEXTURES, Parameters.FARMYARDS)
179
177
  if farmyards and self.map.grle_settings.add_farmyards:
180
178
  farmlands.extend(farmyards)
181
179
  self.logger.debug("Found %s farmyards in textures info layer.", len(farmyards))
182
180
 
183
- fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
181
+ fields = self.get_infolayer_data(Parameters.TEXTURES, Parameters.FIELDS)
184
182
  if not fields:
185
183
  self.logger.warning("Fields data not found in textures info layer.")
186
184
  return
@@ -188,9 +186,7 @@ class GRLE(Component):
188
186
 
189
187
  self.logger.debug("Found %s fields in textures info layer.", len(fields))
190
188
 
191
- info_layer_farmlands_path = os.path.join(
192
- self.game.weights_dir_path(self.map_directory), "infoLayer_farmlands.png"
193
- )
189
+ info_layer_farmlands_path = self.game.get_farmlands_path(self.map_directory)
194
190
 
195
191
  self.logger.debug(
196
192
  "Adding farmlands to the InfoLayer PNG file: %s.", info_layer_farmlands_path
@@ -201,22 +197,21 @@ class GRLE(Component):
201
197
  return
202
198
 
203
199
  image = cv2.imread(info_layer_farmlands_path, cv2.IMREAD_UNCHANGED)
204
- farmlands_xml_path = os.path.join(self.map_directory, "map/config/farmlands.xml")
205
- if not os.path.isfile(farmlands_xml_path):
206
- self.logger.warning("Farmlands XML file %s not found.", farmlands_xml_path)
207
- return
208
200
 
209
- tree = ET.parse(farmlands_xml_path)
210
- farmlands_xml = tree.find("farmlands")
201
+ tree = self.get_tree()
202
+ root = tree.getroot()
203
+ farmlands_node = root.find("farmlands")
204
+ if farmlands_node is None:
205
+ raise ValueError("Farmlands XML element not found in the farmlands XML file.")
206
+
207
+ self.update_element(farmlands_node, {"pricePerHa": str(self.map.grle_settings.base_price)})
211
208
 
212
- # Not using enumerate because in case of the error, we do not increment
213
- # the farmland_id. So as a result we do not have a gap in the farmland IDs.
214
209
  farmland_id = 1
215
210
 
216
- for farmland_data in tqdm(farmlands, desc="Adding farmlands", unit="farmland"):
211
+ for farmland in tqdm(farmlands, desc="Adding farmlands", unit="farmland"):
217
212
  try:
218
- fitted_field = self.fit_object_into_bounds(
219
- polygon_points=farmland_data,
213
+ fitted_farmland = self.fit_object_into_bounds(
214
+ polygon_points=farmland,
220
215
  margin=self.map.grle_settings.farmland_margin,
221
216
  angle=self.rotation,
222
217
  )
@@ -228,23 +223,11 @@ class GRLE(Component):
228
223
  )
229
224
  continue
230
225
 
231
- self.logger.debug("Fitted field %s contains %s points.", farmland_id, len(fitted_field))
232
-
233
- field_np = np.array(fitted_field, np.int32)
234
- field_np = field_np.reshape((-1, 1, 2))
235
-
236
- self.logger.debug(
237
- "Created a numpy array and reshaped it. Number of points: %s", len(field_np)
238
- )
239
-
240
- # Infolayer image is 1/2 of the size of the map image, that's why we need to divide
241
- # the coordinates by 2.
242
- field_np = field_np // 2
243
- self.logger.debug("Divided the coordinates by 2.")
226
+ farmland_np = self.polygon_points_to_np(fitted_farmland, divide=2)
244
227
 
245
228
  try:
246
- cv2.fillPoly(image, [field_np], farmland_id) # type: ignore
247
- except Exception as e: # pylint: disable=W0718
229
+ cv2.fillPoly(image, [farmland_np], (float(farmland_id),))
230
+ except Exception as e:
248
231
  self.logger.debug(
249
232
  "Farmland %s could not be added to the InfoLayer PNG file with error: %s",
250
233
  farmland_id,
@@ -252,37 +235,24 @@ class GRLE(Component):
252
235
  )
253
236
  continue
254
237
 
255
- # Add the field to the farmlands XML.
256
- farmland = ET.SubElement(farmlands_xml, "farmland") # type: ignore
257
- farmland.set("id", str(farmland_id))
258
- farmland.set("priceScale", "1")
259
- farmland.set("npcName", "FORESTER")
238
+ data = {
239
+ "id": str(farmland_id),
240
+ "priceScale": "1",
241
+ "npcName": "FORESTER",
242
+ }
243
+ self.create_subelement(farmlands_node, "farmland", data)
260
244
 
261
245
  farmland_id += 1
262
246
 
263
- tree.write(farmlands_xml_path)
264
-
265
- self.logger.debug("Farmlands added to the farmlands XML file: %s.", farmlands_xml_path)
247
+ self.save_tree(tree)
266
248
 
267
249
  cv2.imwrite(info_layer_farmlands_path, image)
268
- self.logger.debug(
269
- "Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
270
- )
271
250
 
272
- self.preview_paths["farmlands"] = info_layer_farmlands_path # type: ignore
251
+ self.preview_paths["farmlands"] = info_layer_farmlands_path
273
252
 
274
- # pylint: disable=R0915
275
253
  def _add_plants(self) -> None:
276
254
  """Adds plants to the InfoLayer PNG file."""
277
- # 1. Get the path to the densityMap_fruits.png.
278
- # 2. Get the path to the base layer (grass).
279
- # 3. Detect non-zero areas in the base layer (it's where the plants will be placed).
280
- texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
281
- if not texture_component:
282
- self.logger.warning("Texture component not found in the map.")
283
- return
284
-
285
- grass_layer = texture_component.get_layer_by_usage("grass")
255
+ grass_layer = self.map.get_texture_layer(by_usage="grass")
286
256
  if not grass_layer:
287
257
  self.logger.warning("Grass layer not found in the texture component.")
288
258
  return
@@ -291,7 +261,7 @@ class GRLE(Component):
291
261
  grass_image_path = grass_layer.get_preview_or_path(weights_directory)
292
262
  self.logger.debug("Grass image path: %s.", grass_image_path)
293
263
 
294
- forest_layer = texture_component.get_layer_by_usage("forest")
264
+ forest_layer = self.map.get_texture_layer(by_usage="forest")
295
265
  forest_image = None
296
266
  if forest_layer:
297
267
  forest_image_path = forest_layer.get_preview_or_path(weights_directory)
@@ -304,9 +274,7 @@ class GRLE(Component):
304
274
  self.logger.warning("Base image not found in %s.", grass_image_path)
305
275
  return
306
276
 
307
- density_map_fruit_path = os.path.join(
308
- self.game.weights_dir_path(self.map_directory), "densityMap_fruits.png"
309
- )
277
+ density_map_fruit_path = self.game.get_density_map_fruits_path(self.map_directory)
310
278
 
311
279
  self.logger.debug("Density map for fruits path: %s.", density_map_fruit_path)
312
280
 
@@ -334,88 +302,14 @@ class GRLE(Component):
334
302
  # Add non zero values from the forest image to the grass image.
335
303
  grass_image[forest_image != 0] = 255
336
304
 
337
- # B and G channels remain the same (zeros), while we change the R channel.
338
- possible_R_values = [65, 97, 129, 161, 193, 225] # pylint: disable=C0103
305
+ base_grass = self.map.grle_settings.base_grass
306
+ if isinstance(base_grass, tuple):
307
+ base_grass = base_grass[0]
339
308
 
340
- base_layer_pixel_value = plant_to_pixel_value(
341
- self.map.grle_settings.base_grass # type:ignore
342
- )
309
+ base_layer_pixel_value = plant_to_pixel_value(str(base_grass))
343
310
  if not base_layer_pixel_value:
344
311
  base_layer_pixel_value = 131
345
312
 
346
- def create_island_of_plants(image: np.ndarray, count: int) -> np.ndarray:
347
- """Create an island of plants in the image.
348
-
349
- Arguments:
350
- image (np.ndarray): The image where the island of plants will be created.
351
- count (int): The number of islands of plants to create.
352
-
353
- Returns:
354
- np.ndarray: The image with the islands of plants.
355
- """
356
- for _ in tqdm(range(count), desc="Adding islands of plants", unit="island"):
357
- # Randomly choose the value for the island.
358
- plant_value = choice(possible_R_values)
359
- # Randomly choose the size of the island.
360
- island_size = randint(
361
- self.map.grle_settings.plants_island_minimum_size, # type:ignore
362
- self.map.grle_settings.plants_island_maximum_size, # type:ignore
363
- )
364
- # Randomly choose the position of the island.
365
- x = randint(0, image.shape[1] - island_size)
366
- y = randint(0, image.shape[0] - island_size)
367
-
368
- try:
369
- polygon_points = get_rounded_polygon(
370
- num_vertices=self.map.grle_settings.plants_island_vertex_count,
371
- center=(x + island_size // 2, y + island_size // 2),
372
- radius=island_size // 2,
373
- rounding_radius=self.map.grle_settings.plants_island_rounding_radius,
374
- )
375
- if not polygon_points:
376
- continue
377
-
378
- nodes = np.array(polygon_points, np.int32) # type: ignore
379
- cv2.fillPoly(image, [nodes], plant_value) # type: ignore
380
- except Exception: # pylint: disable=W0703
381
- continue
382
-
383
- return image
384
-
385
- def get_rounded_polygon(
386
- num_vertices: int, center: tuple[int, int], radius: int, rounding_radius: int
387
- ) -> list[tuple[int, int]] | None:
388
- """Get a randomly rounded polygon.
389
-
390
- Arguments:
391
- num_vertices (int): The number of vertices of the polygon.
392
- center (tuple[int, int]): The center of the polygon.
393
- radius (int): The radius of the polygon.
394
- rounding_radius (int): The rounding radius of the polygon.
395
-
396
- Returns:
397
- list[tuple[int, int]] | None: The rounded polygon.
398
- """
399
- angle_offset = np.pi / num_vertices
400
- angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False) + angle_offset
401
- random_angles = angles + np.random.uniform(
402
- -ISLAND_DISTORTION, ISLAND_DISTORTION, num_vertices
403
- ) # Add randomness to angles
404
- random_radii = radius + np.random.uniform(
405
- -radius * ISLAND_DISTORTION, radius * ISLAND_DISTORTION, num_vertices
406
- ) # Add randomness to radii
407
-
408
- points = [
409
- (center[0] + np.cos(a) * r, center[1] + np.sin(a) * r)
410
- for a, r in zip(random_angles, random_radii)
411
- ]
412
- polygon = Polygon(points)
413
- buffered_polygon = polygon.buffer(rounding_radius, resolution=16)
414
- rounded_polygon = list(buffered_polygon.exterior.coords)
415
- if not rounded_polygon:
416
- return None
417
- return rounded_polygon
418
-
419
313
  grass_image_copy = grass_image.copy()
420
314
  if forest_image is not None:
421
315
  # Add the forest layer to the base image, to merge the masks.
@@ -427,7 +321,7 @@ class GRLE(Component):
427
321
  island_count = int(self.map_size * self.map.grle_settings.plants_island_percent // 100)
428
322
  self.logger.debug("Adding %s islands of plants to the base image.", island_count)
429
323
  if self.map.grle_settings.random_plants:
430
- grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
324
+ grass_image_copy = self.create_island_of_plants(grass_image_copy, island_count)
431
325
  self.logger.debug("Added %s islands of plants to the base image.", island_count)
432
326
 
433
327
  # Sligtly reduce the size of the grass_image, that we'll use as mask.
@@ -438,15 +332,7 @@ class GRLE(Component):
438
332
  grass_image_copy[grass_image == 0] = 0
439
333
  self.logger.debug("Removed the values where the base image has zeros.")
440
334
 
441
- # Set zeros on all sides of the image
442
- grass_image_copy[0, :] = 0 # Top side
443
- grass_image_copy[-1, :] = 0 # Bottom side
444
- grass_image_copy[:, 0] = 0 # Left side
445
- grass_image_copy[:, -1] = 0 # Right side
446
-
447
- # After painting it with base grass, we'll create multiple islands of different plants.
448
- # On the final step, we'll remove all the values which in pixels
449
- # where zerons in the original base image (so we don't paint grass where it should not be).
335
+ grass_image_copy = self.remove_edge_pixel_values(grass_image_copy)
450
336
 
451
337
  # Three channeled 8-bit image, where non-zero values are the
452
338
  # different types of plants (only in the R channel).
@@ -462,3 +348,99 @@ class GRLE(Component):
462
348
  density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB)
463
349
  cv2.imwrite(density_map_fruit_path, density_map_fruits)
464
350
  self.logger.debug("Updated density map for fruits saved in %s.", density_map_fruit_path)
351
+
352
+ def create_island_of_plants(self, image: np.ndarray, count: int) -> np.ndarray:
353
+ """Create an island of plants in the image.
354
+
355
+ Arguments:
356
+ image (np.ndarray): The image where the island of plants will be created.
357
+ count (int): The number of islands of plants to create.
358
+
359
+ Returns:
360
+ np.ndarray: The image with the islands of plants.
361
+ """
362
+ # B and G channels remain the same (zeros), while we change the R channel.
363
+ possible_r_values = [65, 97, 129, 161, 193, 225]
364
+
365
+ for _ in tqdm(range(count), desc="Adding islands of plants", unit="island"):
366
+ # Randomly choose the value for the island.
367
+ plant_value = choice(possible_r_values)
368
+ # Randomly choose the size of the island.
369
+ island_size = randint(
370
+ self.map.grle_settings.plants_island_minimum_size,
371
+ self.map.grle_settings.plants_island_maximum_size,
372
+ )
373
+ # Randomly choose the position of the island.
374
+ x = randint(0, image.shape[1] - island_size)
375
+ y = randint(0, image.shape[0] - island_size)
376
+
377
+ try:
378
+ polygon_points = self.get_rounded_polygon(
379
+ num_vertices=self.map.grle_settings.plants_island_vertex_count,
380
+ center=(x + island_size // 2, y + island_size // 2),
381
+ radius=island_size // 2,
382
+ rounding_radius=self.map.grle_settings.plants_island_rounding_radius,
383
+ )
384
+ if not polygon_points:
385
+ continue
386
+
387
+ nodes = np.array(polygon_points, np.int32)
388
+ cv2.fillPoly(image, [nodes], (float(plant_value),))
389
+ except Exception:
390
+ continue
391
+
392
+ return image
393
+
394
+ @staticmethod
395
+ def get_rounded_polygon(
396
+ num_vertices: int, center: tuple[int, int], radius: int, rounding_radius: int
397
+ ) -> list[tuple[int, int]] | None:
398
+ """Get a randomly rounded polygon.
399
+
400
+ Arguments:
401
+ num_vertices (int): The number of vertices of the polygon.
402
+ center (tuple[int, int]): The center of the polygon.
403
+ radius (int): The radius of the polygon.
404
+ rounding_radius (int): The rounding radius of the polygon.
405
+
406
+ Returns:
407
+ list[tuple[int, int]] | None: The rounded polygon.
408
+ """
409
+ island_distortion = 0.3
410
+
411
+ angle_offset = np.pi / num_vertices
412
+ angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False) + angle_offset
413
+ random_angles = angles + np.random.uniform(
414
+ -island_distortion, island_distortion, num_vertices
415
+ ) # Add randomness to angles
416
+ random_radii = radius + np.random.uniform(
417
+ -radius * island_distortion, radius * island_distortion, num_vertices
418
+ ) # Add randomness to radii
419
+
420
+ points = [
421
+ (center[0] + np.cos(a) * r, center[1] + np.sin(a) * r)
422
+ for a, r in zip(random_angles, random_radii)
423
+ ]
424
+ polygon = Polygon(points)
425
+ buffered_polygon = polygon.buffer(rounding_radius, resolution=16)
426
+ rounded_polygon = list(buffered_polygon.exterior.coords)
427
+ if not rounded_polygon:
428
+ return None
429
+ return rounded_polygon
430
+
431
+ @staticmethod
432
+ def remove_edge_pixel_values(image_np: np.ndarray) -> np.ndarray:
433
+ """Remove the edge pixel values from the image.
434
+
435
+ Arguments:
436
+ image_np (np.ndarray): The image to remove the edge pixel values from.
437
+
438
+ Returns:
439
+ np.ndarray: The image with the edge pixel values removed.
440
+ """
441
+ # Set zeros on all sides of the image
442
+ image_np[0, :] = 0 # Top side
443
+ image_np[-1, :] = 0 # Bottom side
444
+ image_np[:, 0] = 0 # Left side
445
+ image_np[:, -1] = 0 # Right side
446
+ return image_np
@@ -297,8 +297,6 @@ class DTMProvider(ABC):
297
297
  with rasterio.open(tile) as src:
298
298
  crs = src.crs
299
299
  if crs != "EPSG:4326":
300
- print("crs:", crs)
301
- print("reprojecting to EPSG:4326")
302
300
  self.logger.debug(f"Reprojecting GeoTIFF from {crs} to EPSG:4326...")
303
301
  tile = self.reproject_geotiff(tile)
304
302
 
@@ -499,12 +497,10 @@ class DTMProvider(ABC):
499
497
  # Open all input GeoTIFF files as datasets
500
498
  self.logger.debug("Merging tiff files...")
501
499
  datasets = [rasterio.open(file) for file in input_files]
502
- print("datasets:", datasets)
503
500
 
504
501
  # Merge datasets
505
502
  crs = datasets[0].crs
506
503
  mosaic, out_transform = merge(datasets, nodata=0)
507
- print("mosaic:", mosaic)
508
504
 
509
505
  # Get metadata from the first file and update it for the output
510
506
  out_meta = datasets[0].meta.copy()
maps4fs/generator/game.py CHANGED
@@ -8,8 +8,8 @@ import os
8
8
 
9
9
  from maps4fs.generator.background import Background
10
10
  from maps4fs.generator.component.config import Config
11
+ from maps4fs.generator.component.grle import GRLE
11
12
  from maps4fs.generator.component.i3d import I3d
12
- from maps4fs.generator.grle import GRLE
13
13
  from maps4fs.generator.satellite import Satellite
14
14
  from maps4fs.generator.texture import Texture
15
15
 
@@ -39,6 +39,7 @@ class Game:
39
39
  _grle_schema: str | None = None
40
40
  _tree_schema: str | None = None
41
41
  _i3d_processing: bool = True
42
+ _plants_processing: bool = True
42
43
 
43
44
  # Order matters! Some components depend on others.
44
45
  components = [Texture, Background, GRLE, I3d, Config, Satellite]
@@ -147,6 +148,38 @@ class Game:
147
148
  str: The path to the weights directory."""
148
149
  raise NotImplementedError
149
150
 
151
+ def get_density_map_fruits_path(self, map_directory: str) -> str:
152
+ """Returns the path to the density map fruits file.
153
+
154
+ Arguments:
155
+ map_directory (str): The path to the map directory.
156
+
157
+ Returns:
158
+ str: The path to the density map fruits file."""
159
+ weights_dir = self.weights_dir_path(map_directory)
160
+ return os.path.join(weights_dir, "densityMap_fruits.png")
161
+
162
+ def get_farmlands_path(self, map_directory: str) -> str:
163
+ """Returns the path to the farmlands file.
164
+
165
+ Arguments:
166
+ map_directory (str): The path to the map directory.
167
+
168
+ Returns:
169
+ str: The path to the farmlands file."""
170
+ weights_dir = self.weights_dir_path(map_directory)
171
+ return os.path.join(weights_dir, "infoLayer_farmlands.png")
172
+
173
+ def get_farmlands_xml_path(self, map_directory: str) -> str:
174
+ """Returns the path to the farmlands xml file.
175
+
176
+ Arguments:
177
+ map_directory (str): The path to the map directory.
178
+
179
+ Returns:
180
+ str: The path to the farmlands xml file."""
181
+ raise NotImplementedError
182
+
150
183
  def i3d_file_path(self, map_directory: str) -> str:
151
184
  """Returns the path to the i3d file.
152
185
 
@@ -165,6 +198,14 @@ class Game:
165
198
  bool: True if the i3d file should be processed, False otherwise."""
166
199
  return self._i3d_processing
167
200
 
201
+ @property
202
+ def plants_processing(self) -> bool:
203
+ """Returns whether the plants should be processed.
204
+
205
+ Returns:
206
+ bool: True if the plants should be processed, False otherwise."""
207
+ return self._plants_processing
208
+
168
209
  @property
169
210
  def additional_dem_name(self) -> str | None:
170
211
  """Returns the name of the additional DEM file.
@@ -193,6 +234,7 @@ class FS22(Game):
193
234
  _map_template_path = os.path.join(working_directory, "data", "fs22-map-template.zip")
194
235
  _texture_schema = os.path.join(working_directory, "data", "fs22-texture-schema.json")
195
236
  _i3d_processing = False
237
+ _plants_processing = False
196
238
 
197
239
  def dem_file_path(self, map_directory: str) -> str:
198
240
  """Returns the path to the DEM file.
@@ -276,3 +318,13 @@ class FS25(Game):
276
318
  Returns:
277
319
  str: The path to the i3d file."""
278
320
  return os.path.join(map_directory, "map", "map.i3d")
321
+
322
+ def get_farmlands_xml_path(self, map_directory: str) -> str:
323
+ """Returns the path to the farmlands xml file.
324
+
325
+ Arguments:
326
+ map_directory (str): The path to the map directory.
327
+
328
+ Returns:
329
+ str: The path to the farmlands xml file."""
330
+ return os.path.join(map_directory, "map", "config", "farmlands.xml")
@@ -15,6 +15,9 @@ class Parameters:
15
15
  TEXTURES = "textures"
16
16
  FOREST = "forest"
17
17
  ROADS_POLYLINES = "roads_polylines"
18
+ FARMYARDS = "farmyards"
19
+
20
+ PREVIEW_MAXIMUM_SIZE = 2048
18
21
 
19
22
 
20
23
  class SharedSettings(BaseModel):
@@ -141,6 +144,8 @@ class GRLESettings(SettingsModel):
141
144
  farmland_margin: int = 0
142
145
  random_plants: bool = True
143
146
  add_farmyards: bool = False
147
+ base_price: int = 60000
148
+ price_scale: int = 100
144
149
  base_grass: tuple | str = ("smallDenseMix", "meadow")
145
150
  plants_island_minimum_size: int = 10
146
151
  plants_island_maximum_size: int = 200
@@ -172,6 +177,7 @@ class TextureSettings(SettingsModel):
172
177
  dissolve: bool = False
173
178
  fields_padding: int = 0
174
179
  skip_drains: bool = False
180
+ use_cache: bool = True
175
181
 
176
182
 
177
183
  class SplineSettings(SettingsModel):
@@ -12,9 +12,10 @@ from typing import Any, Callable, Generator, Optional
12
12
 
13
13
  import cv2
14
14
  import numpy as np
15
- import osmnx as ox # type: ignore
15
+ import osmnx as ox
16
16
  import pandas as pd
17
17
  import shapely.geometry # type: ignore
18
+ from osmnx import settings as ox_settings
18
19
  from shapely.geometry.base import BaseGeometry # type: ignore
19
20
  from tqdm import tqdm
20
21
 
@@ -764,6 +765,10 @@ class Texture(Component):
764
765
  Numpy array of polygon points or list of point coordinates.
765
766
  """
766
767
  is_fieds = info_layer == "fields"
768
+
769
+ ox_settings.use_cache = self.map.texture_settings.use_cache
770
+ ox_settings.requests_timeout = 30
771
+
767
772
  try:
768
773
  if self.map.custom_osm is not None:
769
774
  with warnings.catch_warnings():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: maps4fs
3
- Version: 1.8.13
3
+ Version: 1.8.14
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: MIT License
@@ -595,6 +595,10 @@ You can also apply some advanced settings to the map generation process.<br>
595
595
 
596
596
  - Add Farmyards - if enabled, the tool will create farmlands from the regions that are marked as farmyards in the OSM data. Those farmlands will not have fields and also will not be drawn on textures. By default, it's turned off.
597
597
 
598
+ - Base price - the base price of the farmland. It's used to calculate the price of the farmland in the game. In default in-game maps this value equals to 60000.
599
+
600
+ - Price scale - is a value in percent which will be applied to all farmnlands. The price per Ha will be calculated as `base_price * price_scale / 100`. By default, it's set to 100%.
601
+
598
602
  - Base grass - you can select which plant will be used as a base grass on the map.
599
603
 
600
604
  - Plants island minimum size - when random plants are enabled, the generator will add islands of differents plants to the map and choose the random size of those island between the minimum and maximum values. This one is the minimum size of the island in meters.
@@ -621,6 +625,8 @@ You can also apply some advanced settings to the map generation process.<br>
621
625
 
622
626
  - Skip drains - if enabled, the tool will not generate the drains and ditches on the map. By default, it's set to False. Use this if you don't need the drains on the map.
623
627
 
628
+ - Use cache - if enabled, the tool will use the cached OSM data for generating the map. It's useful when you're generating the same map multiple times and don't want to download the OSM data each time. But if you've made some changes to the OSM data, you should disable this option to get the updated data. By default, it's set to True. This option has no effect when you're using the custom OSM file.
629
+
624
630
  ### Splines Advanced settings
625
631
 
626
632
  - Splines density - number of points, which will be added (interpolate) between each pair of existing points. The higher the value, the denser the spline will be. It can smooth the splines, but high values can in opposite make the splines look unnatural.
@@ -3,23 +3,24 @@ maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
3
3
  maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
4
  maps4fs/generator/background.py,sha256=p6rciVwGXgmx7o_LoeugjqUY6JLqVdaID9vt33W9Seg,24774
5
5
  maps4fs/generator/dem.py,sha256=RsIaL3LabYnZzow2fzmccsnzBo0m1YjcVVxUkJJGJMU,10735
6
- maps4fs/generator/game.py,sha256=zpS6PDd4Nej_VgpncVBeIhEFkAAyXYAD4Pgj-b_sLbQ,9196
7
- maps4fs/generator/grle.py,sha256=5bWTTVSYHr2wKbuynYdz6IGYZFD-dlbZBErDPCZ8NQ8,20726
6
+ maps4fs/generator/game.py,sha256=qLFVUegp6qcLJm1kRNmKVJNDri-yXAXF9TXDThrNmpE,11005
8
7
  maps4fs/generator/map.py,sha256=ataDN-Hsx3XlHdisrLBZbP7q1aKFDaSXG939MEGsiE4,11513
9
8
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
10
9
  maps4fs/generator/satellite.py,sha256=jeKZ1IC5kReewDBDIxGzQc11UaGceysyh_XAOxw6JO0,3644
11
- maps4fs/generator/settings.py,sha256=rzXOJi6d6GY7GMWZ5SYwKQoih_qcZKkYAuVwiyPMyjo,5917
12
- maps4fs/generator/texture.py,sha256=VEmbjTpv-asnQs7yo2pSjvE-jJI8zOFisnNGRbAsQVk,36432
10
+ maps4fs/generator/settings.py,sha256=ujxXzRLj_XYGxtULmv69a_QtQ06mSAL9dZHk1UgdUlU,6060
11
+ maps4fs/generator/texture.py,sha256=_IfqIuxH4934VJXKtdABHa6ToPWk3T0xknvlu-rZ5Uc,36570
13
12
  maps4fs/generator/component/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
14
13
  maps4fs/generator/component/config.py,sha256=RitKgFDZPzjA1fi8GcEi1na75qqaueUvpcITHjBvCXc,3674
14
+ maps4fs/generator/component/grle.py,sha256=aKMjVJNuKHJJ2gsXaH00bz10kWaIbbZXU_JbP-ZAGw4,18990
15
15
  maps4fs/generator/component/i3d.py,sha256=3x38yL-kSJ8ylBwICBb6wPYzRSky4gVj8XCk2jzYSeo,19861
16
16
  maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
17
17
  maps4fs/generator/component/base/component.py,sha256=U3XJae0GUvHN08psv2j24Y4PBAAytSkSK3AmD-VjjXE,21404
18
- maps4fs/generator/component/base/component_xml.py,sha256=DLSUobR0GtVCdBWOCwZ9VLJv5vY9da5l4fzp86W8QhE,3446
18
+ maps4fs/generator/component/base/component_image.py,sha256=Wg9V4XMWtkJgnj5_q5yK63rmsWwUMxeeiKlJoWmuh9k,904
19
+ maps4fs/generator/component/base/component_xml.py,sha256=6OO1dKoceO1ACk7-k1oGtnkfNud8ZN3u3ZNjdNMpTqw,3967
19
20
  maps4fs/generator/dtm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
21
  maps4fs/generator/dtm/bavaria.py,sha256=7njrEvSCYAC8ZVyvS-_84iXHhWA0oHKrEqSzxdnZuGs,4293
21
22
  maps4fs/generator/dtm/canada.py,sha256=lYONwm6aNX5cjVggR3AiZZF9dlCDAWg0M8RMaObog8s,1288
22
- maps4fs/generator/dtm/dtm.py,sha256=tbYPlpDbFS374ywHrYIJwRflRRzGzi7M5mmcVqwCYBw,22880
23
+ maps4fs/generator/dtm/dtm.py,sha256=Ox4uo16L8T1JdDjsceNn74gvoL_o9qX7_YZ5pLHg3Dg,22732
23
24
  maps4fs/generator/dtm/england.py,sha256=YyCYwnNUJuBeeMNUozfKIj_yNjHpGeuH1Mz0NiAJL-U,1122
24
25
  maps4fs/generator/dtm/hessen.py,sha256=dYu7TUXICaFMI1sc2l1i3ZDdM5fecXkj5jcz5ZuXIOw,964
25
26
  maps4fs/generator/dtm/niedersachsen.py,sha256=rHScUGrI8JYseBb3xeSN6mdZ7b28YgdlPVCMlluaAMM,1297
@@ -33,8 +34,8 @@ maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,4
33
34
  maps4fs/toolbox/background.py,sha256=ym9a6TjZpRC2ButAUaQ_rwhOEuAo2ScwUQsXdJrV_Hs,2111
34
35
  maps4fs/toolbox/custom_osm.py,sha256=X6ZlPqiOhNjkmdD_qVroIfdOl9Rb90cDwVSLDVYgx80,1892
35
36
  maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
36
- maps4fs-1.8.13.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
37
- maps4fs-1.8.13.dist-info/METADATA,sha256=1LYrpLhWYaCMzExdg4c8VvuiLH1qHKiwp0FvEGqgeCk,42156
38
- maps4fs-1.8.13.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
39
- maps4fs-1.8.13.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
40
- maps4fs-1.8.13.dist-info/RECORD,,
37
+ maps4fs-1.8.14.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
38
+ maps4fs-1.8.14.dist-info/METADATA,sha256=NfXYsB5Q4ndNxdmzVOgeGj9iXVM2NX7ABwHWvJkMpSk,42904
39
+ maps4fs-1.8.14.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
40
+ maps4fs-1.8.14.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
41
+ maps4fs-1.8.14.dist-info/RECORD,,