maps4fs 2.8.4__py3-none-any.whl → 2.8.6__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.

Potentially problematic release.


This version of maps4fs might be problematic. Click here for more details.

@@ -5,11 +5,22 @@ from __future__ import annotations
5
5
  import os
6
6
 
7
7
  import cv2
8
+ import numpy as np
8
9
 
10
+ import maps4fs.generator.utils as mfsutils
9
11
  from maps4fs.generator.component.base.component_image import ImageComponent
10
12
  from maps4fs.generator.component.base.component_xml import XMLComponent
11
13
  from maps4fs.generator.settings import Parameters
12
14
 
15
+ # Defines coordinates for country block on the license plate texture.
16
+ COUNTRY_CODE_TOP = 169
17
+ COUNTRY_CODE_BOTTOM = 252
18
+ COUNTRY_CODE_LEFT = 74
19
+ COUNTRY_CODE_RIGHT = 140
20
+
21
+ LICENSE_PLATES_XML_FILENAME = "licensePlatesPL.xml"
22
+ LICENSE_PLATES_I3D_FILENAME = "licensePlatesPL.i3d"
23
+
13
24
 
14
25
  # pylint: disable=R0903
15
26
  class Config(XMLComponent, ImageComponent):
@@ -40,6 +51,8 @@ class Config(XMLComponent, ImageComponent):
40
51
 
41
52
  self._set_overview()
42
53
 
54
+ self.update_license_plates()
55
+
43
56
  def _set_map_size(self) -> None:
44
57
  """Edits map.xml file to set correct map size."""
45
58
  tree = self.get_tree()
@@ -276,3 +289,520 @@ class Config(XMLComponent, ImageComponent):
276
289
 
277
290
  self.convert_png_to_dds(resized_overview_path, overview_image_path)
278
291
  self.logger.info("Overview image converted and saved to: %s", overview_image_path)
292
+
293
+ @property
294
+ def supported_countries(self) -> dict[str, str]:
295
+ """Returns a dictionary of supported countries and their corresponding country codes.
296
+
297
+ Returns:
298
+ dict[str, str]: Supported countries and their country codes.
299
+ """
300
+ return {
301
+ "germany": "DE",
302
+ "austria": "A",
303
+ "switzerland": "CH",
304
+ "france": "F",
305
+ "italy": "I",
306
+ "spain": "E",
307
+ "portugal": "P",
308
+ "netherlands": "NL",
309
+ "belgium": "B",
310
+ "luxembourg": "L",
311
+ "poland": "PL",
312
+ "czech republic": "CZ",
313
+ "slovakia": "SK",
314
+ "hungary": "H",
315
+ "slovenia": "SLO",
316
+ "croatia": "HR",
317
+ "bosnia and herzegovina": "BIH",
318
+ "serbia": "SRB",
319
+ "north macedonia": "MKD",
320
+ "greece": "GR",
321
+ "turkey": "TR",
322
+ }
323
+
324
+ @property
325
+ def eu_countries(self) -> set[str]:
326
+ """Returns a set of country codes that are in the European Union.
327
+
328
+ Returns:
329
+ set[str]: Set of country codes in the EU.
330
+ """
331
+ return {
332
+ "DE",
333
+ "A",
334
+ "CH",
335
+ "F",
336
+ "I",
337
+ "E",
338
+ "P",
339
+ "NL",
340
+ "B",
341
+ "L",
342
+ "PL",
343
+ "CZ",
344
+ "SK",
345
+ "H",
346
+ "SLO",
347
+ "HR",
348
+ }
349
+
350
+ def update_license_plates(self):
351
+ """Updates license plates for the specified country."""
352
+ try:
353
+ license_plates_directory = self.game.license_plates_dir_path(self.map_directory)
354
+ except NotImplementedError:
355
+ self.logger.warning("Game does not support license plates processing.")
356
+ return
357
+
358
+ country_name = mfsutils.get_country_by_coordinates(self.map.coordinates).lower()
359
+ if country_name not in self.supported_countries:
360
+ self.logger.warning(
361
+ "License plates processing is not supported for country: %s.", country_name
362
+ )
363
+ return
364
+
365
+ # Get license plate country code and EU format.
366
+ country_code = self.supported_countries[country_name]
367
+ eu_format = country_code in self.eu_countries
368
+
369
+ self.logger.info(
370
+ "Updating license plates for country: %s, EU format: %s",
371
+ )
372
+
373
+ license_plates_prefix = self.map.i3d_settings.license_plate_prefix
374
+ if len(license_plates_prefix) < 1 or len(license_plates_prefix) > 3:
375
+ self.logger.error(
376
+ "Invalid license plate prefix: %s. It must be 1 to 3 characters long.",
377
+ license_plates_prefix,
378
+ )
379
+ return
380
+
381
+ try:
382
+ # 1. Update licensePlatesPL.xml with license plate prefix.
383
+ self._update_license_plates_xml(license_plates_directory, license_plates_prefix)
384
+
385
+ # 2. Update licensePlatesPL.i3d texture reference depending on EU format.
386
+ self._update_license_plates_i3d(license_plates_directory, eu_format)
387
+
388
+ # 3. Generate texture with country code.
389
+ self._generate_license_plate_texture(
390
+ license_plates_directory,
391
+ country_code,
392
+ eu_format,
393
+ COUNTRY_CODE_LEFT,
394
+ COUNTRY_CODE_TOP,
395
+ COUNTRY_CODE_RIGHT,
396
+ COUNTRY_CODE_BOTTOM,
397
+ )
398
+
399
+ self.logger.info("License plates updated successfully")
400
+ except Exception as e:
401
+ self.logger.error("Failed to update license plates: %s", e)
402
+ return
403
+
404
+ # Edit the map.xml only if all previous steps succeeded.
405
+ self._update_map_xml_license_plates()
406
+
407
+ def _update_map_xml_license_plates(self) -> None:
408
+ """Update map.xml to reference PL license plates.
409
+
410
+ Raises:
411
+ FileNotFoundError: If the map XML file is not found.
412
+ ValueError: If the map XML root element is None.
413
+ """
414
+ tree = self.get_tree()
415
+ if not tree:
416
+ raise FileNotFoundError(f"Map XML file not found: {self.xml_path}")
417
+
418
+ root = tree.getroot()
419
+
420
+ if root is None:
421
+ raise ValueError("Map XML root element is None.")
422
+
423
+ # Find or create licensePlates element
424
+ license_plates_element = root.find(".//licensePlates")
425
+ if license_plates_element is not None:
426
+ license_plates_element.set("filename", "map/licensePlates/licensePlatesPL.xml")
427
+ else:
428
+ # Create new licensePlates element if it doesn't exist
429
+ license_plates_element = root.makeelement(
430
+ "licensePlates", {"filename": "map/licensePlates/licensePlatesPL.xml"}
431
+ )
432
+ root.append(license_plates_element)
433
+
434
+ self.save_tree(tree)
435
+ self.logger.debug("Updated map.xml to use PL license plates")
436
+
437
+ def _update_license_plates_xml(
438
+ self, license_plates_directory: str, license_plate_prefix: str
439
+ ) -> None:
440
+ """Update licensePlatesPL.xml with license plate prefix.
441
+
442
+ Arguments:
443
+ license_plates_directory (str): Directory where license plates XML is located.
444
+ license_plate_prefix (str): The prefix to set on the license plates.
445
+
446
+ Raises:
447
+ FileNotFoundError: If the license plates XML file is not found.
448
+ ValueError: If required XML elements are not found.
449
+ """
450
+ xml_path = os.path.join(license_plates_directory, LICENSE_PLATES_XML_FILENAME)
451
+ if not os.path.isfile(xml_path):
452
+ raise FileNotFoundError(f"License plates XML file not found: {xml_path}.")
453
+
454
+ tree = self.get_tree(xml_path=xml_path)
455
+ root = tree.getroot()
456
+ if root is None:
457
+ raise ValueError("License plates XML root element is None.")
458
+
459
+ # Find licensePlate with node="0"
460
+ license_plate = None
461
+ for plate in root.findall(".//licensePlate"):
462
+ if plate.get("node") == "0":
463
+ license_plate = plate
464
+ break
465
+
466
+ if license_plate is None:
467
+ raise ValueError("Could not find licensePlate element with node='0'")
468
+
469
+ # Find first variations/variation element
470
+ variations = license_plate.find("variations")
471
+ if variations is None:
472
+ raise ValueError("Could not find variations element")
473
+
474
+ variation = variations.find("variation")
475
+ if variation is None:
476
+ raise ValueError("Could not find first variation element")
477
+
478
+ # 1. Update license plate prefix to ensure max 3 letters, uppercase.
479
+ license_plate_prefix = license_plate_prefix.upper()[:3]
480
+
481
+ # 2. Position X values for the letters.
482
+ pos_x_values = ["-0.1712", "-0.1172", "-0.0632"] # ? DO WE REALLY NEED THEM?
483
+
484
+ # 3. Update only the first 3 values (prefix letters), leave others intact.
485
+ # Find and update nodes 0|0, 0|1, 0|2 specifically.
486
+ for i, letter in enumerate(license_plate_prefix):
487
+ target_node = f"0|{i}"
488
+ # Find existing value with this node ID.
489
+ existing_value = None
490
+ for value in variation.findall("value"):
491
+ if value.get("node") == target_node:
492
+ existing_value = value
493
+ break
494
+
495
+ if existing_value is not None:
496
+ # Update existing value.
497
+ existing_value.set("character", letter)
498
+ existing_value.set("posX", pos_x_values[i])
499
+ existing_value.set("numerical", "false")
500
+ existing_value.set("alphabetical", "true")
501
+ else:
502
+ # Create new value if it doesn't exist.
503
+ value_elem = root.makeelement(
504
+ "value",
505
+ {
506
+ "node": target_node,
507
+ "character": letter,
508
+ "posX": pos_x_values[i],
509
+ "numerical": "false",
510
+ "alphabetical": "true",
511
+ },
512
+ )
513
+ # Insert at the beginning to maintain order.
514
+ variation.insert(i, value_elem)
515
+
516
+ # 5. Save the updated XML.
517
+ self.save_tree(tree, xml_path=xml_path)
518
+ self.logger.debug(
519
+ "Updated licensePlatesPL.xml with license plate prefix: %s", license_plate_prefix
520
+ )
521
+
522
+ def _update_license_plates_i3d(self, license_plates_directory: str, eu_format: bool) -> None:
523
+ """Update licensePlatesPL.i3d texture reference.
524
+
525
+ Arguments:
526
+ license_plates_directory (str): Directory where license plates i3d is located.
527
+ eu_format (bool): Whether to use EU format texture.
528
+
529
+ Raises:
530
+ FileNotFoundError: If the license plates i3d file is not found.
531
+ ValueError: If required XML elements are not found.
532
+ """
533
+ i3d_path = os.path.join(license_plates_directory, LICENSE_PLATES_I3D_FILENAME)
534
+ if not os.path.isfile(i3d_path):
535
+ raise FileNotFoundError(f"License plates i3d file not found: {i3d_path}")
536
+
537
+ # 1. Load the i3d XML.
538
+ tree = self.get_tree(xml_path=i3d_path)
539
+ root = tree.getroot()
540
+
541
+ if root is None:
542
+ raise ValueError("License plates i3d XML root element is None.")
543
+
544
+ # 2. Find File element with fileId="12"
545
+ file_element = None
546
+ for file_elem in root.findall(".//File"):
547
+ if file_elem.get("fileId") == "12":
548
+ file_element = file_elem
549
+ break
550
+
551
+ if file_element is None:
552
+ raise ValueError("Could not find File element with fileId='12'")
553
+
554
+ # 3. Update filename to point to local map directory (relative path).
555
+ if eu_format:
556
+ filename = "licensePlates_diffuseEU.png"
557
+ else:
558
+ filename = "licensePlates_diffuse.png"
559
+
560
+ file_element.set("filename", filename)
561
+
562
+ # 4. Save the updated i3d XML.
563
+ self.save_tree(tree, xml_path=i3d_path)
564
+ self.logger.debug("Updated licensePlatesPL.i3d texture reference to: %s", filename)
565
+
566
+ def _generate_license_plate_texture(
567
+ self,
568
+ license_plates_directory: str,
569
+ country_code: str,
570
+ eu_format: bool,
571
+ left: int,
572
+ top: int,
573
+ right: int,
574
+ bottom: int,
575
+ ) -> None:
576
+ """Generate license plate texture with country code.
577
+
578
+ Arguments:
579
+ license_plates_directory (str): Directory where license plates textures are located.
580
+ country_code (str): The country code to render on the license plate.
581
+ eu_format (bool): Whether to use EU format texture.
582
+ left (int): Left coordinate of the country code box.
583
+ top (int): Top coordinate of the country code box.
584
+ right (int): Right coordinate of the country code box.
585
+ bottom (int): Bottom coordinate of the country code box.
586
+
587
+ Raises:
588
+ FileNotFoundError: If the base texture file is not found.
589
+ ValueError: If there is an error generating the texture.
590
+ """
591
+ # 1. Define the path to the base texture depending on EU format.
592
+ if eu_format:
593
+ texture_filename = "licensePlates_diffuseEU.png"
594
+ else:
595
+ texture_filename = "licensePlates_diffuse.png"
596
+
597
+ # 2. Check if the base texture file exists.
598
+ texture_path = os.path.join(license_plates_directory, texture_filename)
599
+ if not os.path.isfile(texture_path):
600
+ self.logger.warning("Base texture file not found: %s.", texture_path)
601
+ raise FileNotFoundError(f"Base texture file not found: {texture_path}")
602
+
603
+ # 3. Load the base texture.
604
+ texture = cv2.imread(texture_path, cv2.IMREAD_UNCHANGED)
605
+ if texture is None:
606
+ raise ValueError(f"Could not load base texture: {texture_path}")
607
+
608
+ # 4. Calculate text box dimensions.
609
+ box_width = right - left
610
+ box_height = bottom - top
611
+
612
+ # 5. Define font and fit text in rotated box.
613
+ font = cv2.FONT_HERSHEY_SIMPLEX
614
+ thickness = 3
615
+ font_scale, large_canvas_size = self.fit_text_in_rotated_box(
616
+ country_code,
617
+ box_width,
618
+ box_height,
619
+ font,
620
+ thickness,
621
+ )
622
+
623
+ # 6. Create the actual text image (black background for white text).
624
+ text_img = np.zeros((large_canvas_size, large_canvas_size, 3), dtype=np.uint8)
625
+ text_size = cv2.getTextSize(country_code, font, font_scale, thickness)[0]
626
+
627
+ # 7. Center text on canvas.
628
+ text_x = (large_canvas_size - text_size[0]) // 2
629
+ text_y = (large_canvas_size + text_size[1]) // 2
630
+
631
+ # 8. Use white text on black background with anti-aliasing.
632
+ cv2.putText(
633
+ text_img,
634
+ country_code,
635
+ (text_x, text_y),
636
+ font,
637
+ font_scale,
638
+ (255, 255, 255),
639
+ thickness,
640
+ lineType=cv2.LINE_AA,
641
+ )
642
+
643
+ # 9. Rotate the text image 90 degrees clockwise.
644
+ rotated_text_rgb = cv2.rotate(text_img, cv2.ROTATE_90_CLOCKWISE)
645
+
646
+ # 10. Find bounding box and crop to content (looking for white pixels).
647
+ gray = cv2.cvtColor(rotated_text_rgb, cv2.COLOR_BGR2GRAY)
648
+ coords = np.column_stack(np.where(gray > 0))
649
+
650
+ # 11. Crop the rotated text image to the bounding box of the text.
651
+ if len(coords) > 0:
652
+ y_min, x_min = coords.min(axis=0)
653
+ y_max, x_max = coords.max(axis=0)
654
+
655
+ # Crop to content with small padding
656
+ padding = 2
657
+ y_min = max(0, y_min - padding)
658
+ x_min = max(0, x_min - padding)
659
+ y_max = min(rotated_text_rgb.shape[0], y_max + padding)
660
+ x_max = min(rotated_text_rgb.shape[1], x_max + padding)
661
+
662
+ cropped_text_rgb = rotated_text_rgb[y_min:y_max, x_min:x_max]
663
+
664
+ # Convert to RGBA with proper alpha
665
+ cropped_height, cropped_width = cropped_text_rgb.shape[:2]
666
+ rotated_text = np.zeros((cropped_height, cropped_width, 4), dtype=np.uint8)
667
+ rotated_text[:, :, :3] = cropped_text_rgb
668
+
669
+ # Set alpha: opaque for text (white pixels), transparent for background
670
+ text_mask = np.any(cropped_text_rgb > 0, axis=2)
671
+ rotated_text[text_mask, 3] = 255 # Opaque text
672
+ rotated_text[~text_mask, 3] = 0 # Transparent background
673
+ else:
674
+ raise ValueError("No text found in the generated image.")
675
+
676
+ # 12. Ensure the texture is RGBA.
677
+ if texture.shape[2] == 3:
678
+ # Convert RGB to RGBA
679
+ texture_rgba = np.zeros((texture.shape[0], texture.shape[1], 4), dtype=np.uint8)
680
+ texture_rgba[:, :, :3] = texture
681
+ texture_rgba[:, :, 3] = 255 # Fully opaque
682
+ texture = texture_rgba
683
+
684
+ # 13. Place the rotated text in the texture.
685
+ h, w = rotated_text.shape[:2]
686
+
687
+ # 14. Center the text within the target box.
688
+ center_x = left + (box_width - w) // 2
689
+ center_y = top + (box_height - h) // 2
690
+
691
+ # 15. Ensure the centered text fits within texture bounds.
692
+ if (
693
+ center_y >= 0
694
+ and center_x >= 0
695
+ and center_y + h <= texture.shape[0]
696
+ and center_x + w <= texture.shape[1]
697
+ ):
698
+ # Extract the region where text will be placed
699
+ texture_region = texture[center_y : center_y + h, center_x : center_x + w]
700
+
701
+ # Alpha blend the text onto the texture
702
+ alpha = rotated_text[:, :, 3:4] / 255.0
703
+
704
+ # Blend only the RGB channels
705
+ texture[center_y : center_y + h, center_x : center_x + w, :3] = (
706
+ rotated_text[:, :, :3] * alpha + texture_region[:, :, :3] * (1 - alpha)
707
+ ).astype(np.uint8)
708
+
709
+ # Update alpha channel where text is present
710
+ texture[center_y : center_y + h, center_x : center_x + w, 3] = np.maximum(
711
+ texture_region[:, :, 3], rotated_text[:, :, 3]
712
+ )
713
+
714
+ self.logger.debug(
715
+ "Text placed at centered position: (%s,%s) size: %sx%s",
716
+ center_x,
717
+ center_y,
718
+ w,
719
+ h,
720
+ )
721
+ else:
722
+ self.logger.warning(
723
+ "Centered text position (%s,%s) with size %sx%s would exceed texture bounds",
724
+ center_x,
725
+ center_y,
726
+ w,
727
+ h,
728
+ )
729
+
730
+ # 16. Save the modified texture.
731
+ cv2.imwrite(texture_path, texture)
732
+ self.logger.debug(
733
+ "Generated license plate texture with country code %s at: %s",
734
+ country_code,
735
+ texture_path,
736
+ )
737
+
738
+ def fit_text_in_rotated_box(
739
+ self,
740
+ text: str,
741
+ box_width: int,
742
+ box_height: int,
743
+ font: int,
744
+ thickness: int,
745
+ ) -> tuple[float, int]:
746
+ """Fits text into a rotated box by adjusting font size.
747
+
748
+ Arguments:
749
+ text (str): The text to fit.
750
+ box_width (int): The width of the box.
751
+ box_height (int): The height of the box.
752
+ font (int): The OpenCV font to use.
753
+ thickness (int): The thickness of the text.
754
+
755
+ Returns:
756
+ tuple[float, int]: The calculated font scale and the size of the large canvas used.
757
+ """
758
+ font_scale = min(box_height / (len(text) * 10), box_width / 22)
759
+
760
+ # Create a large canvas to render text without clipping
761
+ large_canvas_size = max(box_width, box_height) * 2
762
+
763
+ # Iteratively reduce font size until rotated text fits
764
+ for _ in range(15): # More iterations for better fitting
765
+ # Test on a large canvas first (black background for white text)
766
+ test_img = np.zeros((large_canvas_size, large_canvas_size, 3), dtype=np.uint8)
767
+ text_size = cv2.getTextSize(text, font, font_scale, thickness)[0]
768
+
769
+ # Center text on large canvas
770
+ text_x = (large_canvas_size - text_size[0]) // 2
771
+ text_y = (large_canvas_size + text_size[1]) // 2
772
+
773
+ # Use white text for testing with anti-aliasing
774
+ cv2.putText(
775
+ test_img,
776
+ text,
777
+ (text_x, text_y),
778
+ font,
779
+ font_scale,
780
+ (255, 255, 255),
781
+ thickness,
782
+ lineType=cv2.LINE_AA, # Anti-aliasing for smooth edges
783
+ )
784
+
785
+ # Rotate the test image
786
+ rotated_test = cv2.rotate(test_img, cv2.ROTATE_90_CLOCKWISE)
787
+
788
+ # Find the bounding box of non-black pixels
789
+ gray = cv2.cvtColor(rotated_test, cv2.COLOR_BGR2GRAY)
790
+ coords = np.column_stack(np.where(gray > 0))
791
+
792
+ if len(coords) > 0:
793
+ y_min, x_min = coords.min(axis=0)
794
+ y_max, x_max = coords.max(axis=0)
795
+ rotated_width = x_max - x_min + 1
796
+ rotated_height = y_max - y_min + 1
797
+
798
+ # Check if the rotated text fits in our target box with good margin
799
+ if rotated_width <= box_width * 0.90 and rotated_height <= box_height * 0.90:
800
+ break
801
+
802
+ font_scale *= 0.93 # Reduce by 7% each iteration
803
+
804
+ self.logger.debug("Final font scale: %s, Original text size: %s", font_scale, text_size)
805
+ if len(coords) > 0:
806
+ self.logger.debug("Rotated text dimensions: %sx%s", rotated_width, rotated_height)
807
+
808
+ return font_scale, large_canvas_size
@@ -7,6 +7,7 @@ import os
7
7
  import shutil
8
8
  import warnings
9
9
  from collections import defaultdict
10
+ from time import perf_counter
10
11
  from typing import Any, Callable, Generator, Optional
11
12
 
12
13
  import cv2
@@ -796,7 +797,13 @@ class Texture(ImageComponent):
796
797
  warnings.simplefilter("ignore", FutureWarning)
797
798
  objects = ox.features_from_xml(self.map.custom_osm, tags=tags)
798
799
  else:
800
+ before_fetch = perf_counter()
799
801
  objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
802
+ after_fetch = perf_counter()
803
+ fetched_in = after_fetch - before_fetch
804
+ self.logger.info(
805
+ "Fetched OSMNX objects for tags: %s in %.2f seconds.", tags, fetched_in
806
+ )
800
807
  except Exception as e:
801
808
  self.logger.debug("Error fetching objects for tags: %s. Error: %s.", tags, e)
802
809
  return
maps4fs/generator/game.py CHANGED
@@ -180,6 +180,16 @@ class Game:
180
180
  str: The path to the weights directory."""
181
181
  raise NotImplementedError
182
182
 
183
+ def license_plates_dir_path(self, map_directory: str) -> str:
184
+ """Returns the path to the license plates directory.
185
+
186
+ Arguments:
187
+ map_directory (str): The path to the map directory.
188
+
189
+ Returns:
190
+ str: The path to the license plates directory."""
191
+ raise NotImplementedError
192
+
183
193
  def get_density_map_fruits_path(self, map_directory: str) -> str:
184
194
  """Returns the path to the density map fruits file.
185
195
 
@@ -418,6 +428,16 @@ class FS25(Game):
418
428
  str: The path to the weights directory."""
419
429
  return os.path.join(map_directory, "map", "data")
420
430
 
431
+ def license_plates_dir_path(self, map_directory: str) -> str:
432
+ """Returns the path to the license plates directory.
433
+
434
+ Arguments:
435
+ map_directory (str): The path to the map directory.
436
+
437
+ Returns:
438
+ str: The path to the license plates directory."""
439
+ return os.path.join(map_directory, "map", "licensePlates")
440
+
421
441
  def i3d_file_path(self, map_directory: str) -> str:
422
442
  """Returns the path to the i3d file.
423
443
 
@@ -236,6 +236,7 @@ class I3DSettings(SettingsModel):
236
236
  existing points.
237
237
  add_reversed_splines (bool): if True, reversed splines will be added to the map.
238
238
  field_splines (bool): if True, splines will be added to the fields.
239
+ license_plate_prefix (str): prefix for the license plates.
239
240
  """
240
241
 
241
242
  add_trees: bool = True
@@ -247,6 +248,8 @@ class I3DSettings(SettingsModel):
247
248
  add_reversed_splines: bool = False
248
249
  field_splines: bool = False
249
250
 
251
+ license_plate_prefix: str = "M4S"
252
+
250
253
 
251
254
  class TextureSettings(SettingsModel):
252
255
  """Represents the advanced settings for texture component.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.8.4
3
+ Version: 2.8.6
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: GNU Affero General Public License v3.0
@@ -2,28 +2,28 @@ maps4fs/__init__.py,sha256=5ixsCA5vgcIV0OrF9EJBm91Mmc_KfMiDRM-QyifMAvo,386
2
2
  maps4fs/logger.py,sha256=6sem0aFKQqtVjQ_yNu9iGcc-hqzLQUhfxco05K6nqow,763
3
3
  maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
4
  maps4fs/generator/config.py,sha256=KwYYWM5wrMB_tKn3Fls6WpEurZJxmrBH5AgAXPGHTJE,7122
5
- maps4fs/generator/game.py,sha256=axi-h2wmLZHMu5xdzFq4BEYjofJHmMNiGjmw5AA36JA,15421
5
+ maps4fs/generator/game.py,sha256=_LNiH__7FeSGsPKsuvAGiktt5GcJQVqcQYtsFZNWGyM,16106
6
6
  maps4fs/generator/map.py,sha256=ZZRU8x0feGbgeJgxc0D3N-mfiasyFXxj6gbGyl-WRzE,14528
7
7
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
8
- maps4fs/generator/settings.py,sha256=OSwnkvaF9O2nh0khQ9u9RS4pomXsywTUGyqZOUcUk2Y,13299
8
+ maps4fs/generator/settings.py,sha256=_QJL4ikQYLFOIB1zWqXjYvyLfoh3cr2RYb2IzsunMJg,13405
9
9
  maps4fs/generator/statistics.py,sha256=Dp1-NS-DWv0l0UdmhOoXeQs_N-Hs7svYUnmziSW5Z9I,2098
10
10
  maps4fs/generator/utils.py,sha256=ugdQ8C22NeiZLIlldLoEKCc7ioOefz4W-8qF2eOy9qU,4834
11
11
  maps4fs/generator/component/__init__.py,sha256=s01yVVVi8R2xxNvflu2D6wTd9I_g73AMM2x7vAC7GX4,490
12
12
  maps4fs/generator/component/background.py,sha256=tFWdYASUUZsJ8QiQrxxba-6aMECD3hvpd0pMm_UdrH8,46151
13
- maps4fs/generator/component/config.py,sha256=yujW_FF-0mbJ_5PuSeFC45hHb5MOoPAJYdISPQvmtRQ,11118
13
+ maps4fs/generator/component/config.py,sha256=oDA0UWrf2ZPxjJgbZHFJuBTp2DZJq2IROj7lzT9ePwE,31078
14
14
  maps4fs/generator/component/dem.py,sha256=FPqcXmFQg5MPaGuy4g5kxzvY1wbhozeCf-aNMCj5eaU,11687
15
15
  maps4fs/generator/component/grle.py,sha256=0PC1K829wjD4y4d9qfIbnU29ebjflIPBbwIZx8FXwc8,27242
16
16
  maps4fs/generator/component/i3d.py,sha256=RvpiW9skkZ6McyahC-AeIdPuSQjpXiFs1l0xOioJAu4,26638
17
17
  maps4fs/generator/component/layer.py,sha256=bdy1XGOODyPqYUM3b_wEY2H9Piz-AaHsCDecl-qRHiM,6627
18
18
  maps4fs/generator/component/satellite.py,sha256=9nKwL8zQ-BB6WFMx2m8zduFn6RaxSNv6Vtpge1-QMYE,5052
19
- maps4fs/generator/component/texture.py,sha256=gXZgr73ehT3_qjuIku0j7N7exloywtmmEQKBJ-MDcco,36988
19
+ maps4fs/generator/component/texture.py,sha256=AjN1RsnVLtL7F13B_va6psKTTPc_0JCEznjT5mb1vKo,37309
20
20
  maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
21
21
  maps4fs/generator/component/base/component.py,sha256=-7H3donrH19f0_rivNyI3fgLsiZkntXfGywEx4tOnM4,23924
22
22
  maps4fs/generator/component/base/component_image.py,sha256=GXFkEFARNRkWkDiGSjvU4WX6f_8s6R1t2ZYqZflv1jk,9626
23
23
  maps4fs/generator/component/base/component_mesh.py,sha256=S5M_SU-FZz17-LgzTIM935ms1Vc4O06UQNTEN4e0INU,24729
24
24
  maps4fs/generator/component/base/component_xml.py,sha256=MT-VhU2dEckLFxAgmxg6V3gnv11di_94Qq6atfpOLdc,5342
25
- maps4fs-2.8.4.dist-info/licenses/LICENSE.md,sha256=Ptw8AkqJ60c4tRts6yuqGP_8B0dxwOGmJsp6YJ8dKqM,34328
26
- maps4fs-2.8.4.dist-info/METADATA,sha256=YQl9Cryl9G4MFtHX-Lt_FQSLzx5eGnbggY7QvISISdg,10042
27
- maps4fs-2.8.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- maps4fs-2.8.4.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
29
- maps4fs-2.8.4.dist-info/RECORD,,
25
+ maps4fs-2.8.6.dist-info/licenses/LICENSE.md,sha256=Ptw8AkqJ60c4tRts6yuqGP_8B0dxwOGmJsp6YJ8dKqM,34328
26
+ maps4fs-2.8.6.dist-info/METADATA,sha256=E3EgIq4ULTVlG6A8Dp-PgLEsFL_N88sjaex4UYV9p8s,10042
27
+ maps4fs-2.8.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ maps4fs-2.8.6.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
29
+ maps4fs-2.8.6.dist-info/RECORD,,