maps4fs 1.8.242__py3-none-any.whl → 1.8.244__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.
@@ -291,7 +291,7 @@ class Background(MeshComponent, ImageComponent):
291
291
  mesh = self.mesh_from_np(
292
292
  dem_data,
293
293
  include_zeros=include_zeros,
294
- z_scaling_factor=self.get_z_scaling_factor(),
294
+ z_scaling_factor=self.get_z_scaling_factor(ignore_height_scale_multiplier=True),
295
295
  resize_factor=self.map.background_settings.resize_factor,
296
296
  apply_decimation=self.map.background_settings.apply_decimation,
297
297
  decimation_percent=self.map.background_settings.decimation_percent,
@@ -430,80 +430,118 @@ class I3d(XMLComponent):
430
430
 
431
431
  return tree_schema # type: ignore
432
432
 
433
+ def _get_random_tree(
434
+ self, tree_schema: list[dict[str, str]], leaf_type: str | None = None
435
+ ) -> dict[str, str]:
436
+ """Gets a random tree from the tree schema.
437
+ If the leaf type is provided, the method tries to get a tree with the same leaf type.
438
+
439
+ Arguments:
440
+ tree_schema (list[dict[str, str]]): The tree schema.
441
+ leaf_type (str, optional): The leaf type of the tree. Defaults to None.
442
+
443
+ Returns:
444
+ dict[str, str]: The random tree from the schema
445
+ """
446
+ if not leaf_type:
447
+ return choice(tree_schema)
448
+
449
+ try:
450
+ leaf_type = leaf_type.split("_")[0]
451
+ except IndexError:
452
+ return choice(tree_schema)
453
+
454
+ if leaf_type == "mixed":
455
+ trees_with_leaf_type = [tree for tree in tree_schema if tree.get("leaf_type")]
456
+ return choice(trees_with_leaf_type)
457
+
458
+ trees_by_leaf_type = [tree for tree in tree_schema if tree.get("leaf_type") == leaf_type]
459
+ if not trees_by_leaf_type:
460
+ return choice(tree_schema)
461
+
462
+ return choice(trees_by_leaf_type)
463
+
433
464
  def _add_forests(self) -> None:
434
465
  """Adds forests to the map I3D file."""
435
466
  tree_schema = self._read_tree_schema()
436
467
  if not tree_schema:
437
468
  return
438
469
 
439
- forest_layer = self.map.get_texture_layer(by_usage=Parameters.FOREST)
440
- if not forest_layer:
470
+ if self.map.texture_settings.use_precise_tags:
471
+ forest_layers = self.map.get_texture_layers(by_usage=Parameters.FOREST)
472
+ else:
473
+ layer = self.map.get_texture_layer(by_usage=Parameters.FOREST)
474
+ forest_layers = [layer] if layer else []
475
+ if not forest_layers:
441
476
  self.logger.warning("Forest layer not found.")
442
477
  return
443
478
 
444
- weights_directory = self.game.weights_dir_path(self.map_directory)
445
- forest_image_path = forest_layer.get_preview_or_path(weights_directory)
479
+ node_id = TREE_NODE_ID_STARTING_VALUE
446
480
 
447
- if not forest_image_path or not os.path.isfile(forest_image_path):
448
- self.logger.warning("Forest image not found.")
449
- return
481
+ for forest_layer in forest_layers:
482
+ weights_directory = self.game.weights_dir_path(self.map_directory)
483
+ forest_image_path = forest_layer.get_preview_or_path(weights_directory)
450
484
 
451
- tree = self.get_tree()
452
- root = tree.getroot()
453
- scene_node = root.find(".//Scene")
454
- if scene_node is None:
455
- self.logger.warning("Scene element not found in I3D file.")
456
- return
485
+ if not forest_image_path or not os.path.isfile(forest_image_path):
486
+ self.logger.warning("Forest image not found.")
487
+ continue
457
488
 
458
- node_id = TREE_NODE_ID_STARTING_VALUE
489
+ tree = self.get_tree()
490
+ root = tree.getroot()
491
+ scene_node = root.find(".//Scene")
492
+ if scene_node is None:
493
+ self.logger.warning("Scene element not found in I3D file.")
494
+ return
459
495
 
460
- trees_node = self.create_element(
461
- "TransformGroup",
462
- {
463
- "name": "trees",
464
- "translation": "0 0 0",
465
- "nodeId": str(node_id),
466
- },
467
- )
468
- node_id += 1
496
+ trees_node = self.create_element(
497
+ "TransformGroup",
498
+ {
499
+ "name": "trees",
500
+ "translation": "0 0 0",
501
+ "nodeId": str(node_id),
502
+ },
503
+ )
504
+ node_id += 1
469
505
 
470
- not_resized_dem = self.get_not_resized_dem()
471
- if not_resized_dem is None:
472
- self.logger.warning("Not resized DEM not found.")
473
- return
506
+ not_resized_dem = self.get_not_resized_dem()
507
+ if not_resized_dem is None:
508
+ self.logger.warning("Not resized DEM not found.")
509
+ return
474
510
 
475
- forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
476
- for x, y in self.non_empty_pixels(forest_image, step=self.map.i3d_settings.forest_density):
477
- shifted_x, shifted_y = self.randomize_coordinates(
478
- (x, y),
479
- self.map.i3d_settings.forest_density,
480
- self.map.i3d_settings.trees_relative_shift,
481
- )
511
+ forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
512
+ for x, y in self.non_empty_pixels(
513
+ forest_image, step=self.map.i3d_settings.forest_density
514
+ ):
515
+ shifted_x, shifted_y = self.randomize_coordinates(
516
+ (x, y),
517
+ self.map.i3d_settings.forest_density,
518
+ self.map.i3d_settings.trees_relative_shift,
519
+ )
482
520
 
483
- shifted_x, shifted_y = int(shifted_x), int(shifted_y)
521
+ shifted_x, shifted_y = int(shifted_x), int(shifted_y)
484
522
 
485
- z = self.get_z_coordinate_from_dem(not_resized_dem, shifted_x, shifted_y)
523
+ z = self.get_z_coordinate_from_dem(not_resized_dem, shifted_x, shifted_y)
486
524
 
487
- xcs, ycs = self.top_left_coordinates_to_center((shifted_x, shifted_y))
488
- node_id += 1
525
+ xcs, ycs = self.top_left_coordinates_to_center((shifted_x, shifted_y))
526
+ node_id += 1
489
527
 
490
- rotation = randint(-180, 180)
528
+ rotation = randint(-180, 180)
491
529
 
492
- random_tree = choice(tree_schema)
493
- tree_name = random_tree["name"]
494
- tree_id = random_tree["reference_id"]
530
+ random_tree = self._get_random_tree(tree_schema, forest_layer.precise_usage)
531
+ tree_name = random_tree["name"]
532
+ tree_id = random_tree["reference_id"]
495
533
 
496
- data = {
497
- "name": tree_name,
498
- "translation": f"{xcs} {z} {ycs}",
499
- "rotation": f"0 {rotation} 0",
500
- "referenceId": str(tree_id),
501
- "nodeId": str(node_id),
502
- }
503
- trees_node.append(self.create_element("ReferenceNode", data))
534
+ data = {
535
+ "name": tree_name,
536
+ "translation": f"{xcs} {z} {ycs}",
537
+ "rotation": f"0 {rotation} 0",
538
+ "referenceId": str(tree_id),
539
+ "nodeId": str(node_id),
540
+ }
541
+ trees_node.append(self.create_element("ReferenceNode", data))
504
542
 
505
- scene_node.append(trees_node)
506
- self.save_tree(tree)
543
+ scene_node.append(trees_node)
544
+ self.save_tree(tree)
507
545
 
508
546
  @staticmethod
509
547
  def randomize_coordinates(
@@ -44,6 +44,8 @@ class Layer:
44
44
  invisible: bool = False,
45
45
  procedural: list[str] | None = None,
46
46
  border: int | None = None,
47
+ precise_tags: dict[str, str | list[str] | bool] | None = None,
48
+ precise_usage: str | None = None,
47
49
  ):
48
50
  self.name = name
49
51
  self.count = count
@@ -58,6 +60,8 @@ class Layer:
58
60
  self.invisible = invisible
59
61
  self.procedural = procedural
60
62
  self.border = border
63
+ self.precise_tags = precise_tags
64
+ self.precise_usage = precise_usage
61
65
 
62
66
  def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
63
67
  """Returns dictionary with layer data.
@@ -78,6 +82,8 @@ class Layer:
78
82
  "invisible": self.invisible,
79
83
  "procedural": self.procedural,
80
84
  "border": self.border,
85
+ "precise_tags": self.precise_tags,
86
+ "precise_usage": self.precise_usage,
81
87
  }
82
88
 
83
89
  data = {k: v for k, v in data.items() if v is not None}
@@ -123,6 +123,17 @@ class Texture(ImageComponent):
123
123
  return layer
124
124
  return None
125
125
 
126
+ def get_layers_by_usage(self, usage: str) -> list[Layer]:
127
+ """Returns layer by usage.
128
+
129
+ Arguments:
130
+ usage (str): Usage of the layer.
131
+
132
+ Returns:
133
+ list[Layer]: List of layers.
134
+ """
135
+ return [layer for layer in self.layers if layer.usage == usage]
136
+
126
137
  def process(self) -> None:
127
138
  """Processes the data to generate textures."""
128
139
  self._prepare_weights()
@@ -211,7 +222,7 @@ class Texture(ImageComponent):
211
222
  if self.rotation:
212
223
  # Iterate over the layers which have tags and rotate them.
213
224
  for layer in tqdm(self.layers, desc="Rotating textures", unit="layer"):
214
- if layer.tags:
225
+ if layer.tags or layer.precise_tags:
215
226
  self.logger.debug("Rotating layer %s.", layer.name)
216
227
  layer_paths = layer.paths(self._weights_dir)
217
228
  layer_paths += [layer.path_preview(self._weights_dir)]
@@ -263,7 +274,7 @@ class Texture(ImageComponent):
263
274
  Arguments:
264
275
  layer (Layer): Layer with textures and tags.
265
276
  """
266
- if layer.tags is None:
277
+ if layer.tags is None and layer.precise_tags is None:
267
278
  size = (self.map_size, self.map_size)
268
279
  else:
269
280
  size = (self.map_rotated_size, self.map_rotated_size)
@@ -316,7 +327,9 @@ class Texture(ImageComponent):
316
327
  def draw(self) -> None:
317
328
  """Iterates over layers and fills them with polygons from OSM data."""
318
329
  layers = self.layers_by_priority()
319
- layers = [layer for layer in layers if layer.tags is not None]
330
+ layers = [
331
+ layer for layer in layers if layer.tags is not None or layer.precise_tags is not None
332
+ ]
320
333
 
321
334
  cumulative_image = None
322
335
 
@@ -376,7 +389,18 @@ class Texture(ImageComponent):
376
389
  info_layer_data (dict[list[list[int]]]): Dictionary to store info layer data.
377
390
  layer_image (np.ndarray): Layer image.
378
391
  """
379
- for polygon in self.objects_generator(layer.tags, layer.width, layer.info_layer):
392
+ tags = layer.tags
393
+ if self.map.texture_settings.use_precise_tags:
394
+ if layer.precise_tags:
395
+ self.logger.debug(
396
+ "Using precise tags: %s for layer %s.", layer.precise_tags, layer.name
397
+ )
398
+ tags = layer.precise_tags
399
+
400
+ if tags is None:
401
+ return
402
+
403
+ for polygon in self.objects_generator(tags, layer.width, layer.info_layer):
380
404
  if not len(polygon) > 2:
381
405
  self.logger.debug("Skipping polygon with less than 3 points.")
382
406
  continue
@@ -746,7 +770,11 @@ class Texture(ImageComponent):
746
770
  preview_size,
747
771
  )
748
772
 
749
- active_layers = [layer for layer in self.layers if layer.tags is not None]
773
+ active_layers = [
774
+ layer
775
+ for layer in self.layers
776
+ if layer.tags is not None or layer.precise_tags is not None
777
+ ]
750
778
  self.logger.debug("Following layers have tag textures: %s.", len(active_layers))
751
779
 
752
780
  images = [
maps4fs/generator/dem.py CHANGED
@@ -144,15 +144,24 @@ class DEM(Component):
144
144
  data = self.dtm_provider.get_numpy()
145
145
  except Exception as e: # pylint: disable=W0718
146
146
  self.logger.error("Failed to get DEM data from DTM provider: %s.", e)
147
- raise e
147
+ raise ValueError(
148
+ f"Failed to get DEM data from DTM provider: {e}. "
149
+ "Try using different DTM provider."
150
+ )
148
151
 
149
152
  if len(data.shape) != 2:
150
153
  self.logger.error("DTM provider returned incorrect data: more than 1 channel.")
151
- raise ValueError("DTM provider returned incorrect data: more than 1 channel.")
154
+ raise ValueError(
155
+ "DTM provider returned incorrect data: more than 1 channel. "
156
+ "Try using different DTM provider."
157
+ )
152
158
 
153
159
  if data.dtype not in ["int16", "uint16", "float", "float32"]:
154
160
  self.logger.error("DTM provider returned incorrect data type: %s.", data.dtype)
155
- raise ValueError(f"DTM provider returned incorrect data type: {data.dtype}.")
161
+ raise ValueError(
162
+ f"DTM provider returned incorrect data type: {data.dtype}. "
163
+ "Try using different DTM provider."
164
+ )
156
165
 
157
166
  self.logger.debug(
158
167
  "DEM data was retrieved from DTM provider. Shape: %s, dtype: %s. Min: %s, max: %s.",
@@ -167,7 +176,7 @@ class DEM(Component):
167
176
  # Check if the data contains any non zero values, otherwise raise an error.
168
177
  if not np.any(data):
169
178
  self.logger.error("DTM provider returned empty data.")
170
- raise ValueError("DTM provider returned empty data.")
179
+ raise ValueError("DTM provider returned empty data. Try using different DTM provider.")
171
180
 
172
181
  # 1. Resize DEM data to the output resolution.
173
182
  resampled_data = self.resize_to_output(data)
maps4fs/generator/map.py CHANGED
@@ -307,6 +307,25 @@ class Map:
307
307
  return texture_component.get_layer_by_usage(by_usage)
308
308
  return None
309
309
 
310
+ def get_texture_layers(
311
+ self,
312
+ by_usage: str | None = None,
313
+ ) -> None | list[Layer]:
314
+ """Get texture layers by usage.
315
+
316
+ Arguments:
317
+ by_usage (str, optional): Texture usage.
318
+
319
+ Returns:
320
+ None | list[Layer]: List of texture layers.
321
+ """
322
+ texture_component = self.get_texture_component()
323
+ if not texture_component:
324
+ return None
325
+ if by_usage:
326
+ return texture_component.get_layers_by_usage(by_usage)
327
+ return None
328
+
310
329
  def previews(self) -> list[str]:
311
330
  """Get list of preview images.
312
331
 
@@ -206,6 +206,7 @@ class TextureSettings(SettingsModel):
206
206
  fields_padding: int = 0
207
207
  skip_drains: bool = False
208
208
  use_cache: bool = True
209
+ use_precise_tags: bool = False
209
210
 
210
211
 
211
212
  class SplineSettings(SettingsModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: maps4fs
3
- Version: 1.8.242
3
+ Version: 1.8.244
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
@@ -523,6 +523,8 @@ Let's have a closer look at the fields:
523
523
  - `invisible` - set it to True for the textures, which should not be drawn in the files, but only to save the data in the JSON file (related to the previous field).
524
524
  - `procedural` - is a list of corresponding files, that will be used for a procedural generation. For example: `"procedural": ["PG_meadow", "PG_acres"]` - means that the texture will be used for two procedural generation files: `masks/PG_meadow.png` and `masks/PG_acres.png`. Note, that the one procuderal name can be applied to multiple textures, in this case they will be merged into one mask.
525
525
  - `border` - this value defines the border between the texture and the edge of the map. It's used to prevent the texture from being drawn on the edge of the map. The value is in pixels.
526
+ - `precise_tags` - can be used for more specific tags, for example instead of `"natural": "wood"` you can use `"leaf_type": "broadleaved"` to draw only broadleaved trees.
527
+ - `precise_usage` - the same as `usage`, but being used with `precise_tags`.
526
528
 
527
529
  ## Background terrain
528
530
 
@@ -655,6 +657,8 @@ You can also apply some advanced settings to the map generation process.<br>
655
657
 
656
658
  - 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.
657
659
 
660
+ - Use precise tags - if enabled, the tool will use the precise tags from the texture schema and will ignore basic tags specified for the texture. In the default schema being used for specific types of forests: broadleaved, needleleaved, mixed, and so on. Note, that if it's enabled and the object does not have the precise tag, it will not be drawn on the map. By default, it's set to False.
661
+
658
662
  ### Splines Advanced settings
659
663
 
660
664
  - 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.
@@ -1,20 +1,20 @@
1
1
  maps4fs/__init__.py,sha256=EGvLVoRpSt4jITswsGbe1ISVmGAZAMQJcBmTwtyuVxI,335
2
2
  maps4fs/logger.py,sha256=HQrDyj72mUjVYo25aR_-_SxVn2rfFjDCNbj-JKJdSnE,1488
3
3
  maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
- maps4fs/generator/dem.py,sha256=a9B3tatj7pzvvdLIyLw7BA3JoDTibFczpqiXJnx054U,12864
4
+ maps4fs/generator/dem.py,sha256=CDfdDGCWhFn2_QYY5TtxgsXm3lgJ4W4qD-46UU3u-ck,13207
5
5
  maps4fs/generator/game.py,sha256=NZaxj5z7WzMiHzAvQyr-TvVjGoHgqGldM6ZsItuYyzA,11292
6
- maps4fs/generator/map.py,sha256=9tQTEMYvh4RSC5ACp7fE7RhVDGa7Mc-P5W95h-Zineg,13069
6
+ maps4fs/generator/map.py,sha256=64uIHCwfpFyFXZJSayB0tuRFn0LVuTwf0wrAi4KMv00,13600
7
7
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
8
- maps4fs/generator/settings.py,sha256=KFyzqtJ2BwjuESRzqSWyNuFlHS7W_wc8K_1tkLRKHwQ,6902
8
+ maps4fs/generator/settings.py,sha256=285LjC5mUuzpHNPubBqd928MU7Yd_ZlLEnkBY3RbiHs,6937
9
9
  maps4fs/generator/statistics.py,sha256=aynS3zbAtiwnU_YLKHPTiiaKW98_suvQUhy1SGBA6mc,2448
10
10
  maps4fs/generator/component/__init__.py,sha256=s01yVVVi8R2xxNvflu2D6wTd9I_g73AMM2x7vAC7GX4,490
11
- maps4fs/generator/component/background.py,sha256=Pr2dKNghC4ds9Gm8pj7LPWdAX-twYGy1Nfj7QY7pXX4,21012
11
+ maps4fs/generator/component/background.py,sha256=QwyTaltQOTo3lD1uxewY99zw5Wd91CBWRQ35cC67WuE,21047
12
12
  maps4fs/generator/component/config.py,sha256=RitKgFDZPzjA1fi8GcEi1na75qqaueUvpcITHjBvCXc,3674
13
13
  maps4fs/generator/component/grle.py,sha256=KlNbbYY4oyFfB_0qTInrweBTyLUW0koSSpqclkRw-Lk,19381
14
- maps4fs/generator/component/i3d.py,sha256=9OhGHFrgf8i9ZV9sKKVPLGMAUcSGZjNOxwSC38zRa9k,21382
15
- maps4fs/generator/component/layer.py,sha256=QPcEzTv_8N9wYvHAZy8OezfATaVLG-YetSfCXf2lnFI,5892
14
+ maps4fs/generator/component/i3d.py,sha256=QsqtyOLyc_Y10UthV03EjZO60mHBubPvbEvLIsxzank,23017
15
+ maps4fs/generator/component/layer.py,sha256=hbTBMLamKOWv_MXbnk-LjLyNV91jPsF33UEc17rBZCM,6185
16
16
  maps4fs/generator/component/satellite.py,sha256=oZBHjP_QY0ik1-Vk7JqMS__zIG8ffw2voeozB7-HUQc,4946
17
- maps4fs/generator/component/texture.py,sha256=GWOE7w2maydB2LDku-YEC1w1oc8kcnnZ1SPTL6b82H8,30966
17
+ maps4fs/generator/component/texture.py,sha256=ELU5QwysZaMPH9QQkE5jzIHAOzZ_Y8khcT9ruZuxQpQ,31803
18
18
  maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
19
19
  maps4fs/generator/component/base/component.py,sha256=CIR4Fjn9YfLU4y7nB_jOaz7PjGFvBcDnHlvXxeahpdM,21499
20
20
  maps4fs/generator/component/base/component_image.py,sha256=2QnJ9xm0D54v4whg7bc1s-kwRVjZHhOo1OR5jHr1Qp0,4786
@@ -53,8 +53,8 @@ maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,4
53
53
  maps4fs/toolbox/background.py,sha256=RclEqxEWLbMxuEkkegQP8jybzugwQ1_R3rdfDe0s21U,2104
54
54
  maps4fs/toolbox/custom_osm.py,sha256=X6ZlPqiOhNjkmdD_qVroIfdOl9Rb90cDwVSLDVYgx80,1892
55
55
  maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
56
- maps4fs-1.8.242.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
57
- maps4fs-1.8.242.dist-info/METADATA,sha256=0foJVSbmrPUAJZ7L7AHhiN4u5qCw0XjSqb8a9UONleI,46452
58
- maps4fs-1.8.242.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
59
- maps4fs-1.8.242.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
60
- maps4fs-1.8.242.dist-info/RECORD,,
56
+ maps4fs-1.8.244.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
57
+ maps4fs-1.8.244.dist-info/METADATA,sha256=P1k1UFEjijon_156MSDJV1TQq3LXmyrUg5XXhQ-F2SQ,47093
58
+ maps4fs-1.8.244.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
59
+ maps4fs-1.8.244.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
60
+ maps4fs-1.8.244.dist-info/RECORD,,