maps4fs 1.8.11__py3-none-any.whl → 1.8.13__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/__init__.py +1 -0
 - maps4fs/generator/background.py +7 -11
 - maps4fs/generator/component/__init__.py +1 -0
 - maps4fs/generator/component/base/__init__.py +1 -0
 - maps4fs/generator/{component.py → component/base/component.py} +39 -23
 - maps4fs/generator/component/base/component_xml.py +95 -0
 - maps4fs/generator/{config.py → component/config.py} +15 -30
 - maps4fs/generator/component/i3d.py +545 -0
 - maps4fs/generator/dem.py +1 -10
 - maps4fs/generator/dtm/canada.py +37 -0
 - maps4fs/generator/game.py +33 -2
 - maps4fs/generator/grle.py +10 -16
 - maps4fs/generator/map.py +41 -1
 - maps4fs/generator/satellite.py +1 -2
 - maps4fs/generator/settings.py +11 -0
 - maps4fs/generator/texture.py +5 -7
 - maps4fs/toolbox/background.py +1 -3
 - {maps4fs-1.8.11.dist-info → maps4fs-1.8.13.dist-info}/METADATA +3 -1
 - maps4fs-1.8.13.dist-info/RECORD +40 -0
 - maps4fs/generator/i3d.py +0 -624
 - maps4fs-1.8.11.dist-info/RECORD +0 -36
 - {maps4fs-1.8.11.dist-info → maps4fs-1.8.13.dist-info}/LICENSE.md +0 -0
 - {maps4fs-1.8.11.dist-info → maps4fs-1.8.13.dist-info}/WHEEL +0 -0
 - {maps4fs-1.8.11.dist-info → maps4fs-1.8.13.dist-info}/top_level.txt +0 -0
 
| 
         @@ -0,0 +1,545 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            """This module contains the Config class for map settings and configuration."""
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            from __future__ import annotations
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            import json
         
     | 
| 
      
 6 
     | 
    
         
            +
            import os
         
     | 
| 
      
 7 
     | 
    
         
            +
            from random import choice, randint, uniform
         
     | 
| 
      
 8 
     | 
    
         
            +
            from typing import Generator
         
     | 
| 
      
 9 
     | 
    
         
            +
            from xml.etree import ElementTree as ET
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
            import cv2
         
     | 
| 
      
 12 
     | 
    
         
            +
            import numpy as np
         
     | 
| 
      
 13 
     | 
    
         
            +
            from tqdm import tqdm
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            from maps4fs.generator.component.base.component_xml import XMLComponent
         
     | 
| 
      
 16 
     | 
    
         
            +
            from maps4fs.generator.settings import Parameters
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            MAP_SIZE_LIMIT_FOR_DISPLACEMENT_LAYER = 4096
         
     | 
| 
      
 19 
     | 
    
         
            +
            DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
         
     | 
| 
      
 20 
     | 
    
         
            +
            NODE_ID_STARTING_VALUE = 2000
         
     | 
| 
      
 21 
     | 
    
         
            +
            SPLINES_NODE_ID_STARTING_VALUE = 5000
         
     | 
| 
      
 22 
     | 
    
         
            +
            TREE_NODE_ID_STARTING_VALUE = 10000
         
     | 
| 
      
 23 
     | 
    
         
            +
            TREES_DEFAULT_Z_VALUE = 400
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
            FIELDS_ATTRIBUTES = [
         
     | 
| 
      
 26 
     | 
    
         
            +
                ("angle", "integer", "0"),
         
     | 
| 
      
 27 
     | 
    
         
            +
                ("missionAllowed", "boolean", "true"),
         
     | 
| 
      
 28 
     | 
    
         
            +
                ("missionOnlyGrass", "boolean", "false"),
         
     | 
| 
      
 29 
     | 
    
         
            +
                ("nameIndicatorIndex", "string", "1"),
         
     | 
| 
      
 30 
     | 
    
         
            +
                ("polygonIndex", "string", "0"),
         
     | 
| 
      
 31 
     | 
    
         
            +
                ("teleportIndicatorIndex", "string", "2"),
         
     | 
| 
      
 32 
     | 
    
         
            +
            ]
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            class I3d(XMLComponent):
         
     | 
| 
      
 36 
     | 
    
         
            +
                """Component for map i3d file settings and configuration.
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                Arguments:
         
     | 
| 
      
 39 
     | 
    
         
            +
                    game (Game): The game instance for which the map is generated.
         
     | 
| 
      
 40 
     | 
    
         
            +
                    coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
         
     | 
| 
      
 41 
     | 
    
         
            +
                    map_size (int): The size of the map in pixels.
         
     | 
| 
      
 42 
     | 
    
         
            +
                    map_rotated_size (int): The size of the map in pixels after rotation.
         
     | 
| 
      
 43 
     | 
    
         
            +
                    rotation (int): The rotation angle of the map.
         
     | 
| 
      
 44 
     | 
    
         
            +
                    map_directory (str): The directory where the map files are stored.
         
     | 
| 
      
 45 
     | 
    
         
            +
                    logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
         
     | 
| 
      
 46 
     | 
    
         
            +
                        info, warning. If not provided, default logging will be used.
         
     | 
| 
      
 47 
     | 
    
         
            +
                """
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                def preprocess(self) -> None:
         
     | 
| 
      
 50 
     | 
    
         
            +
                    """Gets the path to the map I3D file from the game instance and saves it to the instance
         
     | 
| 
      
 51 
     | 
    
         
            +
                    attribute. If the game does not support I3D files, the attribute is set to None."""
         
     | 
| 
      
 52 
     | 
    
         
            +
                    self.xml_path = self.game.i3d_file_path(self.map_directory)
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                def process(self) -> None:
         
     | 
| 
      
 55 
     | 
    
         
            +
                    """Updates the map I3D file and creates splines in a separate I3D file."""
         
     | 
| 
      
 56 
     | 
    
         
            +
                    self.update_height_scale()
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    self._update_parameters()
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                    if self.game.i3d_processing:
         
     | 
| 
      
 61 
     | 
    
         
            +
                        self._add_fields()
         
     | 
| 
      
 62 
     | 
    
         
            +
                        self._add_forests()
         
     | 
| 
      
 63 
     | 
    
         
            +
                        self._add_splines()
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                def update_height_scale(self, value: int | None = None) -> None:
         
     | 
| 
      
 66 
     | 
    
         
            +
                    """Updates the height scale value in the map I3D file.
         
     | 
| 
      
 67 
     | 
    
         
            +
                    If the value is not provided, the method checks if the shared settings are set to change
         
     | 
| 
      
 68 
     | 
    
         
            +
                    the height scale and if the height scale value is set. If not, the method returns without
         
     | 
| 
      
 69 
     | 
    
         
            +
                    updating the height scale.
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                    Arguments:
         
     | 
| 
      
 72 
     | 
    
         
            +
                        value (int, optional): The height scale value.
         
     | 
| 
      
 73 
     | 
    
         
            +
                    """
         
     | 
| 
      
 74 
     | 
    
         
            +
                    if not value:
         
     | 
| 
      
 75 
     | 
    
         
            +
                        if (
         
     | 
| 
      
 76 
     | 
    
         
            +
                            self.map.shared_settings.change_height_scale
         
     | 
| 
      
 77 
     | 
    
         
            +
                            and self.map.shared_settings.height_scale_value
         
     | 
| 
      
 78 
     | 
    
         
            +
                        ):
         
     | 
| 
      
 79 
     | 
    
         
            +
                            value = int(self.map.shared_settings.height_scale_value)
         
     | 
| 
      
 80 
     | 
    
         
            +
                        else:
         
     | 
| 
      
 81 
     | 
    
         
            +
                            return
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                    tree = self.get_tree()
         
     | 
| 
      
 84 
     | 
    
         
            +
                    root = tree.getroot()
         
     | 
| 
      
 85 
     | 
    
         
            +
                    path = ".//Scene/TerrainTransformGroup"
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                    data = {"heightScale": str(value)}
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                    self.get_and_update_element(root, path, data)
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                def _update_parameters(self) -> None:
         
     | 
| 
      
 92 
     | 
    
         
            +
                    """Updates the map I3D file with the  sun bounding box and displacement layer size."""
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                    tree = self.get_tree()
         
     | 
| 
      
 95 
     | 
    
         
            +
                    root = tree.getroot()
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                    sun_element_path = ".//Scene/Light[@name='sun']"
         
     | 
| 
      
 98 
     | 
    
         
            +
                    distance = self.map_size // 2
         
     | 
| 
      
 99 
     | 
    
         
            +
                    data = {
         
     | 
| 
      
 100 
     | 
    
         
            +
                        "lastShadowMapSplitBboxMin": f"-{distance},-128,-{distance}",
         
     | 
| 
      
 101 
     | 
    
         
            +
                        "lastShadowMapSplitBboxMax": f"{distance},148,{distance}",
         
     | 
| 
      
 102 
     | 
    
         
            +
                    }
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                    self.get_and_update_element(root, sun_element_path, data)
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                    if self.map_size > MAP_SIZE_LIMIT_FOR_DISPLACEMENT_LAYER:
         
     | 
| 
      
 107 
     | 
    
         
            +
                        displacement_layer_path = ".//Scene/TerrainTransformGroup/DisplacementLayer"
         
     | 
| 
      
 108 
     | 
    
         
            +
                        data = {"size": str(DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS)}
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                        self.get_and_update_element(root, displacement_layer_path, data)
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                    self.save_tree(tree)
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
                def _add_splines(self) -> None:
         
     | 
| 
      
 115 
     | 
    
         
            +
                    """Adds splines to the map I3D file."""
         
     | 
| 
      
 116 
     | 
    
         
            +
                    splines_i3d_path = self.game.splines_file_path(self.map_directory)
         
     | 
| 
      
 117 
     | 
    
         
            +
                    if not os.path.isfile(splines_i3d_path):
         
     | 
| 
      
 118 
     | 
    
         
            +
                        self.logger.warning("Splines I3D file not found: %s.", splines_i3d_path)
         
     | 
| 
      
 119 
     | 
    
         
            +
                        return
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                    tree = self.get_tree(splines_i3d_path)
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                    roads_polylines = self.get_infolayer_data(Parameters.TEXTURES, Parameters.ROADS_POLYLINES)
         
     | 
| 
      
 124 
     | 
    
         
            +
                    if not roads_polylines:
         
     | 
| 
      
 125 
     | 
    
         
            +
                        self.logger.warning("Roads polylines data not found in textures info layer.")
         
     | 
| 
      
 126 
     | 
    
         
            +
                        return
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                    root = tree.getroot()
         
     | 
| 
      
 129 
     | 
    
         
            +
                    # Find <Shapes> element in the I3D file.
         
     | 
| 
      
 130 
     | 
    
         
            +
                    shapes_node = root.find(".//Shapes")
         
     | 
| 
      
 131 
     | 
    
         
            +
                    # Find <Scene> element in the I3D file.
         
     | 
| 
      
 132 
     | 
    
         
            +
                    scene_node = root.find(".//Scene")
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                    if shapes_node is None or scene_node is None:
         
     | 
| 
      
 135 
     | 
    
         
            +
                        self.logger.warning("Shapes or Scene node not found in I3D file.")
         
     | 
| 
      
 136 
     | 
    
         
            +
                        return
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                    # Read the not resized DEM to obtain Z values for spline points.
         
     | 
| 
      
 139 
     | 
    
         
            +
                    background_component = self.map.get_background_component()
         
     | 
| 
      
 140 
     | 
    
         
            +
                    if not background_component:
         
     | 
| 
      
 141 
     | 
    
         
            +
                        self.logger.warning("Background component not found.")
         
     | 
| 
      
 142 
     | 
    
         
            +
                        return
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                    not_resized_dem = cv2.imread(background_component.not_resized_path, cv2.IMREAD_UNCHANGED)
         
     | 
| 
      
 145 
     | 
    
         
            +
                    if not_resized_dem is None:
         
     | 
| 
      
 146 
     | 
    
         
            +
                        self.logger.warning("Not resized DEM not found.")
         
     | 
| 
      
 147 
     | 
    
         
            +
                        return
         
     | 
| 
      
 148 
     | 
    
         
            +
                    dem_x_size, dem_y_size = not_resized_dem.shape
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                    user_attributes_node = root.find(".//UserAttributes")
         
     | 
| 
      
 151 
     | 
    
         
            +
                    if user_attributes_node is None:
         
     | 
| 
      
 152 
     | 
    
         
            +
                        self.logger.warning("UserAttributes node not found in I3D file.")
         
     | 
| 
      
 153 
     | 
    
         
            +
                        return
         
     | 
| 
      
 154 
     | 
    
         
            +
             
     | 
| 
      
 155 
     | 
    
         
            +
                    node_id = SPLINES_NODE_ID_STARTING_VALUE
         
     | 
| 
      
 156 
     | 
    
         
            +
                    for road_id, road in enumerate(roads_polylines, start=1):
         
     | 
| 
      
 157 
     | 
    
         
            +
                        # Add to scene node
         
     | 
| 
      
 158 
     | 
    
         
            +
                        # <Shape name="spline01_CSV" translation="0 0 0" nodeId="11" shapeId="11"/>
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 161 
     | 
    
         
            +
                            fitted_road = self.fit_object_into_bounds(
         
     | 
| 
      
 162 
     | 
    
         
            +
                                linestring_points=road, angle=self.rotation
         
     | 
| 
      
 163 
     | 
    
         
            +
                            )
         
     | 
| 
      
 164 
     | 
    
         
            +
                        except ValueError as e:
         
     | 
| 
      
 165 
     | 
    
         
            +
                            self.logger.debug(
         
     | 
| 
      
 166 
     | 
    
         
            +
                                "Road %s could not be fitted into the map bounds with error: %s",
         
     | 
| 
      
 167 
     | 
    
         
            +
                                road_id,
         
     | 
| 
      
 168 
     | 
    
         
            +
                                e,
         
     | 
| 
      
 169 
     | 
    
         
            +
                            )
         
     | 
| 
      
 170 
     | 
    
         
            +
                            continue
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
                        fitted_road = self.interpolate_points(
         
     | 
| 
      
 173 
     | 
    
         
            +
                            fitted_road, num_points=self.map.spline_settings.spline_density
         
     | 
| 
      
 174 
     | 
    
         
            +
                        )
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
                        spline_name = f"spline{road_id}"
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
                        data = {
         
     | 
| 
      
 179 
     | 
    
         
            +
                            "name": spline_name,
         
     | 
| 
      
 180 
     | 
    
         
            +
                            "translation": "0 0 0",
         
     | 
| 
      
 181 
     | 
    
         
            +
                            "nodeId": str(node_id),
         
     | 
| 
      
 182 
     | 
    
         
            +
                            "shapeId": str(node_id),
         
     | 
| 
      
 183 
     | 
    
         
            +
                        }
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
                        scene_node.append(self.create_element("Shape", data))
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
                        road_ccs = [self.top_left_coordinates_to_center(point) for point in fitted_road]
         
     | 
| 
      
 188 
     | 
    
         
            +
             
     | 
| 
      
 189 
     | 
    
         
            +
                        data = {
         
     | 
| 
      
 190 
     | 
    
         
            +
                            "name": spline_name,
         
     | 
| 
      
 191 
     | 
    
         
            +
                            "shapeId": str(node_id),
         
     | 
| 
      
 192 
     | 
    
         
            +
                            "degree": "3",
         
     | 
| 
      
 193 
     | 
    
         
            +
                            "form": "open",
         
     | 
| 
      
 194 
     | 
    
         
            +
                        }
         
     | 
| 
      
 195 
     | 
    
         
            +
                        nurbs_curve_node = self.create_element("NurbsCurve", data)
         
     | 
| 
      
 196 
     | 
    
         
            +
             
     | 
| 
      
 197 
     | 
    
         
            +
                        for point_ccs, point in zip(road_ccs, fitted_road):
         
     | 
| 
      
 198 
     | 
    
         
            +
                            cx, cy = point_ccs
         
     | 
| 
      
 199 
     | 
    
         
            +
                            x, y = point
         
     | 
| 
      
 200 
     | 
    
         
            +
             
     | 
| 
      
 201 
     | 
    
         
            +
                            x = max(0, min(int(x), dem_x_size - 1))
         
     | 
| 
      
 202 
     | 
    
         
            +
                            y = max(0, min(int(y), dem_y_size - 1))
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
                            z = not_resized_dem[y, x]
         
     | 
| 
      
 205 
     | 
    
         
            +
                            z *= self.get_z_scaling_factor()
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
                            nurbs_curve_node.append(self.create_element("cv", {"c": f"{cx}, {z}, {cy}"}))
         
     | 
| 
      
 208 
     | 
    
         
            +
             
     | 
| 
      
 209 
     | 
    
         
            +
                        shapes_node.append(nurbs_curve_node)
         
     | 
| 
      
 210 
     | 
    
         
            +
             
     | 
| 
      
 211 
     | 
    
         
            +
                        user_attribute_node = self.get_user_attribute_node(
         
     | 
| 
      
 212 
     | 
    
         
            +
                            node_id,
         
     | 
| 
      
 213 
     | 
    
         
            +
                            attributes=[
         
     | 
| 
      
 214 
     | 
    
         
            +
                                ("maxSpeedScale", "integer", "1"),
         
     | 
| 
      
 215 
     | 
    
         
            +
                                ("speedLimit", "integer", "100"),
         
     | 
| 
      
 216 
     | 
    
         
            +
                            ],
         
     | 
| 
      
 217 
     | 
    
         
            +
                        )
         
     | 
| 
      
 218 
     | 
    
         
            +
             
     | 
| 
      
 219 
     | 
    
         
            +
                        user_attributes_node.append(user_attribute_node)
         
     | 
| 
      
 220 
     | 
    
         
            +
                        node_id += 1
         
     | 
| 
      
 221 
     | 
    
         
            +
             
     | 
| 
      
 222 
     | 
    
         
            +
                    tree.write(splines_i3d_path)  # type: ignore
         
     | 
| 
      
 223 
     | 
    
         
            +
                    self.logger.debug("Splines I3D file saved to: %s.", splines_i3d_path)
         
     | 
| 
      
 224 
     | 
    
         
            +
             
     | 
| 
      
 225 
     | 
    
         
            +
                def _add_fields(self) -> None:
         
     | 
| 
      
 226 
     | 
    
         
            +
                    """Adds fields to the map I3D file."""
         
     | 
| 
      
 227 
     | 
    
         
            +
                    tree = self.get_tree()
         
     | 
| 
      
 228 
     | 
    
         
            +
             
     | 
| 
      
 229 
     | 
    
         
            +
                    border = 0
         
     | 
| 
      
 230 
     | 
    
         
            +
                    fields_layer = self.map.get_texture_layer(by_usage=Parameters.FIELD)
         
     | 
| 
      
 231 
     | 
    
         
            +
                    if fields_layer and fields_layer.border:
         
     | 
| 
      
 232 
     | 
    
         
            +
                        border = fields_layer.border
         
     | 
| 
      
 233 
     | 
    
         
            +
             
     | 
| 
      
 234 
     | 
    
         
            +
                    fields = self.get_infolayer_data(Parameters.TEXTURES, Parameters.FIELDS)
         
     | 
| 
      
 235 
     | 
    
         
            +
                    if not fields:
         
     | 
| 
      
 236 
     | 
    
         
            +
                        self.logger.warning("Fields data not found in textures info layer.")
         
     | 
| 
      
 237 
     | 
    
         
            +
                        return
         
     | 
| 
      
 238 
     | 
    
         
            +
             
     | 
| 
      
 239 
     | 
    
         
            +
                    self.logger.debug("Found %s fields in textures info layer.", len(fields))
         
     | 
| 
      
 240 
     | 
    
         
            +
                    self.logger.debug("Starging to add fields to the I3D file.")
         
     | 
| 
      
 241 
     | 
    
         
            +
             
     | 
| 
      
 242 
     | 
    
         
            +
                    root = tree.getroot()
         
     | 
| 
      
 243 
     | 
    
         
            +
                    gameplay_node = root.find(".//TransformGroup[@name='gameplay']")
         
     | 
| 
      
 244 
     | 
    
         
            +
             
     | 
| 
      
 245 
     | 
    
         
            +
                    if gameplay_node is None:
         
     | 
| 
      
 246 
     | 
    
         
            +
                        return
         
     | 
| 
      
 247 
     | 
    
         
            +
                    fields_node = gameplay_node.find(".//TransformGroup[@name='fields']")
         
     | 
| 
      
 248 
     | 
    
         
            +
                    user_attributes_node = root.find(".//UserAttributes")
         
     | 
| 
      
 249 
     | 
    
         
            +
             
     | 
| 
      
 250 
     | 
    
         
            +
                    if fields_node is None or user_attributes_node is None:
         
     | 
| 
      
 251 
     | 
    
         
            +
                        return
         
     | 
| 
      
 252 
     | 
    
         
            +
             
     | 
| 
      
 253 
     | 
    
         
            +
                    node_id = NODE_ID_STARTING_VALUE
         
     | 
| 
      
 254 
     | 
    
         
            +
                    field_id = 1
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
                    for field in tqdm(fields, desc="Adding fields", unit="field"):
         
     | 
| 
      
 257 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 258 
     | 
    
         
            +
                            fitted_field = self.fit_object_into_bounds(
         
     | 
| 
      
 259 
     | 
    
         
            +
                                polygon_points=field, angle=self.rotation, border=border
         
     | 
| 
      
 260 
     | 
    
         
            +
                            )
         
     | 
| 
      
 261 
     | 
    
         
            +
                        except ValueError as e:
         
     | 
| 
      
 262 
     | 
    
         
            +
                            self.logger.debug(
         
     | 
| 
      
 263 
     | 
    
         
            +
                                "Field %s could not be fitted into the map bounds with error: %s",
         
     | 
| 
      
 264 
     | 
    
         
            +
                                field_id,
         
     | 
| 
      
 265 
     | 
    
         
            +
                                e,
         
     | 
| 
      
 266 
     | 
    
         
            +
                            )
         
     | 
| 
      
 267 
     | 
    
         
            +
                            continue
         
     | 
| 
      
 268 
     | 
    
         
            +
             
     | 
| 
      
 269 
     | 
    
         
            +
                        field_ccs = [self.top_left_coordinates_to_center(point) for point in fitted_field]
         
     | 
| 
      
 270 
     | 
    
         
            +
             
     | 
| 
      
 271 
     | 
    
         
            +
                        field_node, updated_node_id = self._get_field_xml_entry(field_id, field_ccs, node_id)
         
     | 
| 
      
 272 
     | 
    
         
            +
                        if field_node is None:
         
     | 
| 
      
 273 
     | 
    
         
            +
                            continue
         
     | 
| 
      
 274 
     | 
    
         
            +
                        user_attributes_node.append(
         
     | 
| 
      
 275 
     | 
    
         
            +
                            self.get_user_attribute_node(node_id, attributes=FIELDS_ATTRIBUTES)
         
     | 
| 
      
 276 
     | 
    
         
            +
                        )
         
     | 
| 
      
 277 
     | 
    
         
            +
                        node_id = updated_node_id
         
     | 
| 
      
 278 
     | 
    
         
            +
             
     | 
| 
      
 279 
     | 
    
         
            +
                        # Adding the field node to the fields node.
         
     | 
| 
      
 280 
     | 
    
         
            +
                        fields_node.append(field_node)
         
     | 
| 
      
 281 
     | 
    
         
            +
                        self.logger.debug("Field %s added to the I3D file.", field_id)
         
     | 
| 
      
 282 
     | 
    
         
            +
             
     | 
| 
      
 283 
     | 
    
         
            +
                        node_id += 1
         
     | 
| 
      
 284 
     | 
    
         
            +
                        field_id += 1
         
     | 
| 
      
 285 
     | 
    
         
            +
             
     | 
| 
      
 286 
     | 
    
         
            +
                    self.save_tree(tree)
         
     | 
| 
      
 287 
     | 
    
         
            +
             
     | 
| 
      
 288 
     | 
    
         
            +
                def _get_field_xml_entry(
         
     | 
| 
      
 289 
     | 
    
         
            +
                    self, field_id: int, field_ccs: list[tuple[int, int]], node_id: int
         
     | 
| 
      
 290 
     | 
    
         
            +
                ) -> tuple[ET.Element, int] | tuple[None, int]:
         
     | 
| 
      
 291 
     | 
    
         
            +
                    """Creates an XML entry for the field with given field ID and field coordinates.
         
     | 
| 
      
 292 
     | 
    
         
            +
             
     | 
| 
      
 293 
     | 
    
         
            +
                    Arguments:
         
     | 
| 
      
 294 
     | 
    
         
            +
                        field_id (int): The ID of the field.
         
     | 
| 
      
 295 
     | 
    
         
            +
                        field_ccs (list[tuple[int, int]]): The coordinates of the field polygon points
         
     | 
| 
      
 296 
     | 
    
         
            +
                            in the center coordinate system.
         
     | 
| 
      
 297 
     | 
    
         
            +
                        node_id (int): The node ID of the field node.
         
     | 
| 
      
 298 
     | 
    
         
            +
             
     | 
| 
      
 299 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 300 
     | 
    
         
            +
                        tuple[ET.Element, int] | tuple[None, int]: The field node and the updated node ID or
         
     | 
| 
      
 301 
     | 
    
         
            +
                            None and the node ID.
         
     | 
| 
      
 302 
     | 
    
         
            +
                    """
         
     | 
| 
      
 303 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 304 
     | 
    
         
            +
                        cx, cy = self.get_polygon_center(field_ccs)
         
     | 
| 
      
 305 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 306 
     | 
    
         
            +
                        self.logger.debug("Field %s could not be fitted into the map bounds.", field_id)
         
     | 
| 
      
 307 
     | 
    
         
            +
                        self.logger.debug("Error: %s", e)
         
     | 
| 
      
 308 
     | 
    
         
            +
                        return None, node_id
         
     | 
| 
      
 309 
     | 
    
         
            +
             
     | 
| 
      
 310 
     | 
    
         
            +
                    # Creating the main field node.
         
     | 
| 
      
 311 
     | 
    
         
            +
                    data = {
         
     | 
| 
      
 312 
     | 
    
         
            +
                        "name": f"field{field_id}",
         
     | 
| 
      
 313 
     | 
    
         
            +
                        "translation": f"{cx} 0 {cy}",
         
     | 
| 
      
 314 
     | 
    
         
            +
                        "nodeId": str(node_id),
         
     | 
| 
      
 315 
     | 
    
         
            +
                    }
         
     | 
| 
      
 316 
     | 
    
         
            +
                    field_node = self.create_element("TransformGroup", data)
         
     | 
| 
      
 317 
     | 
    
         
            +
                    node_id += 1
         
     | 
| 
      
 318 
     | 
    
         
            +
             
     | 
| 
      
 319 
     | 
    
         
            +
                    # Creating the polygon points node, which contains the points of the field.
         
     | 
| 
      
 320 
     | 
    
         
            +
                    polygon_points_node = self.create_element(
         
     | 
| 
      
 321 
     | 
    
         
            +
                        "TransformGroup", {"name": "polygonPoints", "nodeId": str(node_id)}
         
     | 
| 
      
 322 
     | 
    
         
            +
                    )
         
     | 
| 
      
 323 
     | 
    
         
            +
                    node_id += 1
         
     | 
| 
      
 324 
     | 
    
         
            +
             
     | 
| 
      
 325 
     | 
    
         
            +
                    for point_id, point in enumerate(field_ccs, start=1):
         
     | 
| 
      
 326 
     | 
    
         
            +
                        rx, ry = self.absolute_to_relative(point, (cx, cy))
         
     | 
| 
      
 327 
     | 
    
         
            +
             
     | 
| 
      
 328 
     | 
    
         
            +
                        node_id += 1
         
     | 
| 
      
 329 
     | 
    
         
            +
                        point_node = self.create_element(
         
     | 
| 
      
 330 
     | 
    
         
            +
                            "TransformGroup",
         
     | 
| 
      
 331 
     | 
    
         
            +
                            {
         
     | 
| 
      
 332 
     | 
    
         
            +
                                "name": f"point{point_id}",
         
     | 
| 
      
 333 
     | 
    
         
            +
                                "translation": f"{rx} 0 {ry}",
         
     | 
| 
      
 334 
     | 
    
         
            +
                                "nodeId": str(node_id),
         
     | 
| 
      
 335 
     | 
    
         
            +
                            },
         
     | 
| 
      
 336 
     | 
    
         
            +
                        )
         
     | 
| 
      
 337 
     | 
    
         
            +
             
     | 
| 
      
 338 
     | 
    
         
            +
                        polygon_points_node.append(point_node)
         
     | 
| 
      
 339 
     | 
    
         
            +
             
     | 
| 
      
 340 
     | 
    
         
            +
                    field_node.append(polygon_points_node)
         
     | 
| 
      
 341 
     | 
    
         
            +
             
     | 
| 
      
 342 
     | 
    
         
            +
                    # Adding the name indicator node to the field node.
         
     | 
| 
      
 343 
     | 
    
         
            +
                    name_indicator_node, node_id = self._get_name_indicator_node(node_id, field_id)
         
     | 
| 
      
 344 
     | 
    
         
            +
                    field_node.append(name_indicator_node)
         
     | 
| 
      
 345 
     | 
    
         
            +
             
     | 
| 
      
 346 
     | 
    
         
            +
                    node_id += 1
         
     | 
| 
      
 347 
     | 
    
         
            +
                    field_node.append(
         
     | 
| 
      
 348 
     | 
    
         
            +
                        self.create_element(
         
     | 
| 
      
 349 
     | 
    
         
            +
                            "TransformGroup", {"name": "teleportIndicator", "nodeId": str(node_id)}
         
     | 
| 
      
 350 
     | 
    
         
            +
                        )
         
     | 
| 
      
 351 
     | 
    
         
            +
                    )
         
     | 
| 
      
 352 
     | 
    
         
            +
             
     | 
| 
      
 353 
     | 
    
         
            +
                    return field_node, node_id
         
     | 
| 
      
 354 
     | 
    
         
            +
             
     | 
| 
      
 355 
     | 
    
         
            +
                def _get_name_indicator_node(self, node_id: int, field_id: int) -> tuple[ET.Element, int]:
         
     | 
| 
      
 356 
     | 
    
         
            +
                    """Creates a name indicator node with given node ID and field ID.
         
     | 
| 
      
 357 
     | 
    
         
            +
             
     | 
| 
      
 358 
     | 
    
         
            +
                    Arguments:
         
     | 
| 
      
 359 
     | 
    
         
            +
                        node_id (int): The node ID of the name indicator node.
         
     | 
| 
      
 360 
     | 
    
         
            +
                        field_id (int): The ID of the field.
         
     | 
| 
      
 361 
     | 
    
         
            +
             
     | 
| 
      
 362 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 363 
     | 
    
         
            +
                        tuple[ET.Element, int]: The name indicator node and the updated node ID.
         
     | 
| 
      
 364 
     | 
    
         
            +
                    """
         
     | 
| 
      
 365 
     | 
    
         
            +
                    node_id += 1
         
     | 
| 
      
 366 
     | 
    
         
            +
                    name_indicator_node = self.create_element(
         
     | 
| 
      
 367 
     | 
    
         
            +
                        "TransformGroup", {"name": "nameIndicator", "nodeId": str(node_id)}
         
     | 
| 
      
 368 
     | 
    
         
            +
                    )
         
     | 
| 
      
 369 
     | 
    
         
            +
             
     | 
| 
      
 370 
     | 
    
         
            +
                    node_id += 1
         
     | 
| 
      
 371 
     | 
    
         
            +
                    data = {
         
     | 
| 
      
 372 
     | 
    
         
            +
                        "name": "Note",
         
     | 
| 
      
 373 
     | 
    
         
            +
                        "nodeId": str(node_id),
         
     | 
| 
      
 374 
     | 
    
         
            +
                        "text": f"field{field_id}
0.00 ha",
         
     | 
| 
      
 375 
     | 
    
         
            +
                        "color": "4278190080",
         
     | 
| 
      
 376 
     | 
    
         
            +
                        "fixedSize": "true",
         
     | 
| 
      
 377 
     | 
    
         
            +
                    }
         
     | 
| 
      
 378 
     | 
    
         
            +
                    note_node = self.create_element("Note", data)
         
     | 
| 
      
 379 
     | 
    
         
            +
                    name_indicator_node.append(note_node)
         
     | 
| 
      
 380 
     | 
    
         
            +
             
     | 
| 
      
 381 
     | 
    
         
            +
                    return name_indicator_node, node_id
         
     | 
| 
      
 382 
     | 
    
         
            +
             
     | 
| 
      
 383 
     | 
    
         
            +
                def get_user_attribute_node(
         
     | 
| 
      
 384 
     | 
    
         
            +
                    self, node_id: int, attributes: list[tuple[str, str, str]]
         
     | 
| 
      
 385 
     | 
    
         
            +
                ) -> ET.Element:
         
     | 
| 
      
 386 
     | 
    
         
            +
                    """Creates an XML user attribute node with given node ID.
         
     | 
| 
      
 387 
     | 
    
         
            +
             
     | 
| 
      
 388 
     | 
    
         
            +
                    Arguments:
         
     | 
| 
      
 389 
     | 
    
         
            +
                        node_id (int): The node ID of the user attribute node.
         
     | 
| 
      
 390 
     | 
    
         
            +
                        attributes (list[tuple[str, str, str]]): The list of attributes to add to the node.
         
     | 
| 
      
 391 
     | 
    
         
            +
             
     | 
| 
      
 392 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 393 
     | 
    
         
            +
                        ET.Element: The created user attribute node.
         
     | 
| 
      
 394 
     | 
    
         
            +
                    """
         
     | 
| 
      
 395 
     | 
    
         
            +
                    user_attribute_node = ET.Element("UserAttribute")
         
     | 
| 
      
 396 
     | 
    
         
            +
                    user_attribute_node.set("nodeId", str(node_id))
         
     | 
| 
      
 397 
     | 
    
         
            +
             
     | 
| 
      
 398 
     | 
    
         
            +
                    for name, attr_type, value in attributes:
         
     | 
| 
      
 399 
     | 
    
         
            +
                        data = {
         
     | 
| 
      
 400 
     | 
    
         
            +
                            "name": name,
         
     | 
| 
      
 401 
     | 
    
         
            +
                            "type": attr_type,
         
     | 
| 
      
 402 
     | 
    
         
            +
                            "value": value,
         
     | 
| 
      
 403 
     | 
    
         
            +
                        }
         
     | 
| 
      
 404 
     | 
    
         
            +
                        user_attribute_node.append(self.create_element("Attribute", data))
         
     | 
| 
      
 405 
     | 
    
         
            +
             
     | 
| 
      
 406 
     | 
    
         
            +
                    return user_attribute_node
         
     | 
| 
      
 407 
     | 
    
         
            +
             
     | 
| 
      
 408 
     | 
    
         
            +
                def _read_tree_schema(self) -> list[dict[str, str]] | None:
         
     | 
| 
      
 409 
     | 
    
         
            +
                    """Reads the tree schema from the game instance or from the custom schema.
         
     | 
| 
      
 410 
     | 
    
         
            +
             
     | 
| 
      
 411 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 412 
     | 
    
         
            +
                        list[dict[str, int | str]] | None: The tree schema or None if the schema could not be
         
     | 
| 
      
 413 
     | 
    
         
            +
                            read.
         
     | 
| 
      
 414 
     | 
    
         
            +
                    """
         
     | 
| 
      
 415 
     | 
    
         
            +
                    custom_schema = self.kwargs.get("tree_custom_schema")
         
     | 
| 
      
 416 
     | 
    
         
            +
                    if custom_schema:
         
     | 
| 
      
 417 
     | 
    
         
            +
                        tree_schema = custom_schema
         
     | 
| 
      
 418 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 419 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 420 
     | 
    
         
            +
                            tree_schema_path = self.game.tree_schema
         
     | 
| 
      
 421 
     | 
    
         
            +
                        except ValueError:
         
     | 
| 
      
 422 
     | 
    
         
            +
                            self.logger.warning("Tree schema path not set for the Game %s.", self.game.code)
         
     | 
| 
      
 423 
     | 
    
         
            +
                            return None
         
     | 
| 
      
 424 
     | 
    
         
            +
             
     | 
| 
      
 425 
     | 
    
         
            +
                        if not os.path.isfile(tree_schema_path):
         
     | 
| 
      
 426 
     | 
    
         
            +
                            self.logger.warning("Tree schema file was not found: %s.", tree_schema_path)
         
     | 
| 
      
 427 
     | 
    
         
            +
                            return None
         
     | 
| 
      
 428 
     | 
    
         
            +
             
     | 
| 
      
 429 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 430 
     | 
    
         
            +
                            with open(tree_schema_path, "r", encoding="utf-8") as tree_schema_file:
         
     | 
| 
      
 431 
     | 
    
         
            +
                                tree_schema = json.load(tree_schema_file)  # type: ignore
         
     | 
| 
      
 432 
     | 
    
         
            +
                        except json.JSONDecodeError as e:
         
     | 
| 
      
 433 
     | 
    
         
            +
                            self.logger.warning(
         
     | 
| 
      
 434 
     | 
    
         
            +
                                "Could not load tree schema from %s with error: %s", tree_schema_path, e
         
     | 
| 
      
 435 
     | 
    
         
            +
                            )
         
     | 
| 
      
 436 
     | 
    
         
            +
                            return None
         
     | 
| 
      
 437 
     | 
    
         
            +
             
     | 
| 
      
 438 
     | 
    
         
            +
                    return tree_schema  # type: ignore
         
     | 
| 
      
 439 
     | 
    
         
            +
             
     | 
| 
      
 440 
     | 
    
         
            +
                def _add_forests(self) -> None:
         
     | 
| 
      
 441 
     | 
    
         
            +
                    """Adds forests to the map I3D file."""
         
     | 
| 
      
 442 
     | 
    
         
            +
                    tree_schema = self._read_tree_schema()
         
     | 
| 
      
 443 
     | 
    
         
            +
                    if not tree_schema:
         
     | 
| 
      
 444 
     | 
    
         
            +
                        return
         
     | 
| 
      
 445 
     | 
    
         
            +
             
     | 
| 
      
 446 
     | 
    
         
            +
                    forest_layer = self.map.get_texture_layer(by_usage=Parameters.FOREST)
         
     | 
| 
      
 447 
     | 
    
         
            +
                    if not forest_layer:
         
     | 
| 
      
 448 
     | 
    
         
            +
                        self.logger.warning("Forest layer not found.")
         
     | 
| 
      
 449 
     | 
    
         
            +
                        return
         
     | 
| 
      
 450 
     | 
    
         
            +
             
     | 
| 
      
 451 
     | 
    
         
            +
                    weights_directory = self.game.weights_dir_path(self.map_directory)
         
     | 
| 
      
 452 
     | 
    
         
            +
                    forest_image_path = forest_layer.get_preview_or_path(weights_directory)
         
     | 
| 
      
 453 
     | 
    
         
            +
             
     | 
| 
      
 454 
     | 
    
         
            +
                    if not forest_image_path or not os.path.isfile(forest_image_path):
         
     | 
| 
      
 455 
     | 
    
         
            +
                        self.logger.warning("Forest image not found.")
         
     | 
| 
      
 456 
     | 
    
         
            +
                        return
         
     | 
| 
      
 457 
     | 
    
         
            +
             
     | 
| 
      
 458 
     | 
    
         
            +
                    tree = self.get_tree()
         
     | 
| 
      
 459 
     | 
    
         
            +
                    root = tree.getroot()
         
     | 
| 
      
 460 
     | 
    
         
            +
                    scene_node = root.find(".//Scene")
         
     | 
| 
      
 461 
     | 
    
         
            +
                    if scene_node is None:
         
     | 
| 
      
 462 
     | 
    
         
            +
                        self.logger.warning("Scene element not found in I3D file.")
         
     | 
| 
      
 463 
     | 
    
         
            +
                        return
         
     | 
| 
      
 464 
     | 
    
         
            +
             
     | 
| 
      
 465 
     | 
    
         
            +
                    node_id = TREE_NODE_ID_STARTING_VALUE
         
     | 
| 
      
 466 
     | 
    
         
            +
             
     | 
| 
      
 467 
     | 
    
         
            +
                    trees_node = self.create_element(
         
     | 
| 
      
 468 
     | 
    
         
            +
                        "TransformGroup",
         
     | 
| 
      
 469 
     | 
    
         
            +
                        {
         
     | 
| 
      
 470 
     | 
    
         
            +
                            "name": "trees",
         
     | 
| 
      
 471 
     | 
    
         
            +
                            "translation": f"0 {TREES_DEFAULT_Z_VALUE} 0",
         
     | 
| 
      
 472 
     | 
    
         
            +
                            "nodeId": str(node_id),
         
     | 
| 
      
 473 
     | 
    
         
            +
                        },
         
     | 
| 
      
 474 
     | 
    
         
            +
                    )
         
     | 
| 
      
 475 
     | 
    
         
            +
                    node_id += 1
         
     | 
| 
      
 476 
     | 
    
         
            +
             
     | 
| 
      
 477 
     | 
    
         
            +
                    forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
         
     | 
| 
      
 478 
     | 
    
         
            +
                    for x, y in self.non_empty_pixels(forest_image, step=self.map.i3d_settings.forest_density):
         
     | 
| 
      
 479 
     | 
    
         
            +
                        xcs, ycs = self.top_left_coordinates_to_center((x, y))
         
     | 
| 
      
 480 
     | 
    
         
            +
                        node_id += 1
         
     | 
| 
      
 481 
     | 
    
         
            +
             
     | 
| 
      
 482 
     | 
    
         
            +
                        rotation = randint(-180, 180)
         
     | 
| 
      
 483 
     | 
    
         
            +
                        shifted_xcs, shifted_ycs = self.randomize_coordinates(
         
     | 
| 
      
 484 
     | 
    
         
            +
                            (xcs, ycs),
         
     | 
| 
      
 485 
     | 
    
         
            +
                            self.map.i3d_settings.forest_density,
         
     | 
| 
      
 486 
     | 
    
         
            +
                            self.map.i3d_settings.trees_relative_shift,
         
     | 
| 
      
 487 
     | 
    
         
            +
                        )
         
     | 
| 
      
 488 
     | 
    
         
            +
             
     | 
| 
      
 489 
     | 
    
         
            +
                        random_tree = choice(tree_schema)
         
     | 
| 
      
 490 
     | 
    
         
            +
                        tree_name = random_tree["name"]
         
     | 
| 
      
 491 
     | 
    
         
            +
                        tree_id = random_tree["reference_id"]
         
     | 
| 
      
 492 
     | 
    
         
            +
             
     | 
| 
      
 493 
     | 
    
         
            +
                        data = {
         
     | 
| 
      
 494 
     | 
    
         
            +
                            "name": tree_name,
         
     | 
| 
      
 495 
     | 
    
         
            +
                            "translation": f"{shifted_xcs} 0 {shifted_ycs}",
         
     | 
| 
      
 496 
     | 
    
         
            +
                            "rotation": f"0 {rotation} 0",
         
     | 
| 
      
 497 
     | 
    
         
            +
                            "referenceId": str(tree_id),
         
     | 
| 
      
 498 
     | 
    
         
            +
                            "nodeId": str(node_id),
         
     | 
| 
      
 499 
     | 
    
         
            +
                        }
         
     | 
| 
      
 500 
     | 
    
         
            +
                        trees_node.append(self.create_element("ReferenceNode", data))
         
     | 
| 
      
 501 
     | 
    
         
            +
             
     | 
| 
      
 502 
     | 
    
         
            +
                    scene_node.append(trees_node)
         
     | 
| 
      
 503 
     | 
    
         
            +
                    self.save_tree(tree)
         
     | 
| 
      
 504 
     | 
    
         
            +
             
     | 
| 
      
 505 
     | 
    
         
            +
                @staticmethod
         
     | 
| 
      
 506 
     | 
    
         
            +
                def randomize_coordinates(
         
     | 
| 
      
 507 
     | 
    
         
            +
                    coordinates: tuple[int, int], density: int, shift_percent: int
         
     | 
| 
      
 508 
     | 
    
         
            +
                ) -> tuple[float, float]:
         
     | 
| 
      
 509 
     | 
    
         
            +
                    """Randomizes the coordinates of the point with the given density.
         
     | 
| 
      
 510 
     | 
    
         
            +
             
     | 
| 
      
 511 
     | 
    
         
            +
                    Arguments:
         
     | 
| 
      
 512 
     | 
    
         
            +
                        coordinates (tuple[int, int]): The coordinates of the point.
         
     | 
| 
      
 513 
     | 
    
         
            +
                        density (int): The density of the randomization.
         
     | 
| 
      
 514 
     | 
    
         
            +
                        shift_percent (int): Maximum relative shift in percent.
         
     | 
| 
      
 515 
     | 
    
         
            +
             
     | 
| 
      
 516 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 517 
     | 
    
         
            +
                        tuple[float, float]: The randomized coordinates of the point.
         
     | 
| 
      
 518 
     | 
    
         
            +
                    """
         
     | 
| 
      
 519 
     | 
    
         
            +
                    shift_range = density * shift_percent / 100
         
     | 
| 
      
 520 
     | 
    
         
            +
             
     | 
| 
      
 521 
     | 
    
         
            +
                    x_shift = uniform(-shift_range, shift_range)
         
     | 
| 
      
 522 
     | 
    
         
            +
                    y_shift = uniform(-shift_range, shift_range)
         
     | 
| 
      
 523 
     | 
    
         
            +
             
     | 
| 
      
 524 
     | 
    
         
            +
                    x, y = coordinates
         
     | 
| 
      
 525 
     | 
    
         
            +
             
     | 
| 
      
 526 
     | 
    
         
            +
                    return x + x_shift, y + y_shift
         
     | 
| 
      
 527 
     | 
    
         
            +
             
     | 
| 
      
 528 
     | 
    
         
            +
                @staticmethod
         
     | 
| 
      
 529 
     | 
    
         
            +
                def non_empty_pixels(
         
     | 
| 
      
 530 
     | 
    
         
            +
                    image: np.ndarray, step: int = 1
         
     | 
| 
      
 531 
     | 
    
         
            +
                ) -> Generator[tuple[int, int], None, None]:
         
     | 
| 
      
 532 
     | 
    
         
            +
                    """Receives numpy array, which represents single-channeled image of uint8 type.
         
     | 
| 
      
 533 
     | 
    
         
            +
                    Yield coordinates of non-empty pixels (pixels with value greater than 0).
         
     | 
| 
      
 534 
     | 
    
         
            +
             
     | 
| 
      
 535 
     | 
    
         
            +
                    Arguments:
         
     | 
| 
      
 536 
     | 
    
         
            +
                        image (np.ndarray): The image to get non-empty pixels from.
         
     | 
| 
      
 537 
     | 
    
         
            +
                        step (int, optional): The step to iterate through the image. Defaults to 1.
         
     | 
| 
      
 538 
     | 
    
         
            +
             
     | 
| 
      
 539 
     | 
    
         
            +
                    Yields:
         
     | 
| 
      
 540 
     | 
    
         
            +
                        tuple[int, int]: The coordinates of non-empty pixels.
         
     | 
| 
      
 541 
     | 
    
         
            +
                    """
         
     | 
| 
      
 542 
     | 
    
         
            +
                    for y, row in enumerate(image[::step]):
         
     | 
| 
      
 543 
     | 
    
         
            +
                        for x, value in enumerate(row[::step]):
         
     | 
| 
      
 544 
     | 
    
         
            +
                            if value > 0:
         
     | 
| 
      
 545 
     | 
    
         
            +
                                yield x * step, y * step
         
     | 
    
        maps4fs/generator/dem.py
    CHANGED
    
    | 
         @@ -9,7 +9,7 @@ import numpy as np 
     | 
|
| 
       9 
9 
     | 
    
         
             
            # import rasterio  # type: ignore
         
     | 
| 
       10 
10 
     | 
    
         
             
            from pympler import asizeof  # type: ignore
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
            from maps4fs.generator.component import Component
         
     | 
| 
      
 12 
     | 
    
         
            +
            from maps4fs.generator.component.base.component import Component
         
     | 
| 
       13 
13 
     | 
    
         
             
            from maps4fs.generator.dtm.dtm import DTMProvider
         
     | 
| 
       14 
14 
     | 
    
         | 
| 
       15 
15 
     | 
    
         | 
| 
         @@ -132,7 +132,6 @@ class DEM(Component): 
     | 
|
| 
       132 
132 
     | 
    
         
             
                    )
         
     | 
| 
       133 
133 
     | 
    
         
             
                    return data
         
     | 
| 
       134 
134 
     | 
    
         | 
| 
       135 
     | 
    
         
            -
                # pylint: disable=no-member
         
     | 
| 
       136 
135 
     | 
    
         
             
                def process(self) -> None:
         
     | 
| 
       137 
136 
     | 
    
         
             
                    """Reads SRTM file, crops it to map size, normalizes and blurs it,
         
     | 
| 
       138 
137 
     | 
    
         
             
                    saves to map directory."""
         
     | 
| 
         @@ -275,14 +274,6 @@ class DEM(Component): 
     | 
|
| 
       275 
274 
     | 
    
         
             
                    cv2.imwrite(self._dem_path, dem_data)
         
     | 
| 
       276 
275 
     | 
    
         
             
                    self.logger.warning("DEM data filled with zeros and saved to %s.", self._dem_path)
         
     | 
| 
       277 
276 
     | 
    
         | 
| 
       278 
     | 
    
         
            -
                def previews(self) -> list:
         
     | 
| 
       279 
     | 
    
         
            -
                    """This component does not have previews, returns empty list.
         
     | 
| 
       280 
     | 
    
         
            -
             
     | 
| 
       281 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       282 
     | 
    
         
            -
                        list: Empty list.
         
     | 
| 
       283 
     | 
    
         
            -
                    """
         
     | 
| 
       284 
     | 
    
         
            -
                    return []
         
     | 
| 
       285 
     | 
    
         
            -
             
     | 
| 
       286 
277 
     | 
    
         
             
                def info_sequence(self) -> dict[Any, Any] | None:  # type: ignore
         
     | 
| 
       287 
278 
     | 
    
         
             
                    """Returns the information sequence for the component. Must be implemented in the child
         
     | 
| 
       288 
279 
     | 
    
         
             
                    class. If the component does not have an information sequence, an empty dictionary must be
         
     | 
| 
         @@ -0,0 +1,37 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            """This module contains provider of Canada data."""
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            from maps4fs.generator.dtm.base.wcs import WCSProvider
         
     | 
| 
      
 4 
     | 
    
         
            +
            from maps4fs.generator.dtm.dtm import DTMProvider
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            class CanadaProvider(WCSProvider, DTMProvider):
         
     | 
| 
      
 8 
     | 
    
         
            +
                """Provider of Canada data."""
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                _code = "canada"
         
     | 
| 
      
 11 
     | 
    
         
            +
                _name = "Canada HRDEM"
         
     | 
| 
      
 12 
     | 
    
         
            +
                _region = "CN"
         
     | 
| 
      
 13 
     | 
    
         
            +
                _icon = "🇨🇦"
         
     | 
| 
      
 14 
     | 
    
         
            +
                _resolution = 1
         
     | 
| 
      
 15 
     | 
    
         
            +
                _author = "[kbrandwijk](https://github.com/kbrandwijk)"
         
     | 
| 
      
 16 
     | 
    
         
            +
                _is_community = True
         
     | 
| 
      
 17 
     | 
    
         
            +
                _is_base = False
         
     | 
| 
      
 18 
     | 
    
         
            +
                _extents = (76.49491845750764, 33.66564101989275, -26.69697497450798, -157.7322455868316)
         
     | 
| 
      
 19 
     | 
    
         
            +
                _instructions = (
         
     | 
| 
      
 20 
     | 
    
         
            +
                    "HRDEM coverage for Canada is limited. Make sure to check the "
         
     | 
| 
      
 21 
     | 
    
         
            +
                    "[coverage map](https://geo.ca/imagery/high-resolution-digital"
         
     | 
| 
      
 22 
     | 
    
         
            +
                    "-elevation-model-hrdem-canelevation-series/)."
         
     | 
| 
      
 23 
     | 
    
         
            +
                )
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                _url = "https://datacube.services.geo.ca/ows/elevation"
         
     | 
| 
      
 26 
     | 
    
         
            +
                _wcs_version = "1.1.1"
         
     | 
| 
      
 27 
     | 
    
         
            +
                _source_crs = "EPSG:3979"
         
     | 
| 
      
 28 
     | 
    
         
            +
                _tile_size = 1000
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                def get_wcs_parameters(self, tile: tuple[float, float, float, float]) -> dict:
         
     | 
| 
      
 31 
     | 
    
         
            +
                    return {
         
     | 
| 
      
 32 
     | 
    
         
            +
                        "identifier": "dtm",
         
     | 
| 
      
 33 
     | 
    
         
            +
                        "gridbasecrs": "urn:ogc:def:crs:EPSG::3979",
         
     | 
| 
      
 34 
     | 
    
         
            +
                        "boundingbox": f"{tile[1]},{tile[0]},{tile[3]},{tile[2]},urn:ogc:def:crs:EPSG::3979",
         
     | 
| 
      
 35 
     | 
    
         
            +
                        "format": "image/geotiff",
         
     | 
| 
      
 36 
     | 
    
         
            +
                        "timeout": 600,
         
     | 
| 
      
 37 
     | 
    
         
            +
                    }
         
     | 
    
        maps4fs/generator/game.py
    CHANGED
    
    | 
         @@ -7,9 +7,9 @@ from __future__ import annotations 
     | 
|
| 
       7 
7 
     | 
    
         
             
            import os
         
     | 
| 
       8 
8 
     | 
    
         | 
| 
       9 
9 
     | 
    
         
             
            from maps4fs.generator.background import Background
         
     | 
| 
       10 
     | 
    
         
            -
            from maps4fs.generator.config import Config
         
     | 
| 
      
 10 
     | 
    
         
            +
            from maps4fs.generator.component.config import Config
         
     | 
| 
      
 11 
     | 
    
         
            +
            from maps4fs.generator.component.i3d import I3d
         
     | 
| 
       11 
12 
     | 
    
         
             
            from maps4fs.generator.grle import GRLE
         
     | 
| 
       12 
     | 
    
         
            -
            from maps4fs.generator.i3d import I3d
         
     | 
| 
       13 
13 
     | 
    
         
             
            from maps4fs.generator.satellite import Satellite
         
     | 
| 
       14 
14 
     | 
    
         
             
            from maps4fs.generator.texture import Texture
         
     | 
| 
       15 
15 
     | 
    
         | 
| 
         @@ -38,6 +38,7 @@ class Game: 
     | 
|
| 
       38 
38 
     | 
    
         
             
                _texture_schema: str | None = None
         
     | 
| 
       39 
39 
     | 
    
         
             
                _grle_schema: str | None = None
         
     | 
| 
       40 
40 
     | 
    
         
             
                _tree_schema: str | None = None
         
     | 
| 
      
 41 
     | 
    
         
            +
                _i3d_processing: bool = True
         
     | 
| 
       41 
42 
     | 
    
         | 
| 
       42 
43 
     | 
    
         
             
                # Order matters! Some components depend on others.
         
     | 
| 
       43 
44 
     | 
    
         
             
                components = [Texture, Background, GRLE, I3d, Config, Satellite]
         
     | 
| 
         @@ -156,6 +157,14 @@ class Game: 
     | 
|
| 
       156 
157 
     | 
    
         
             
                        str: The path to the i3d file."""
         
     | 
| 
       157 
158 
     | 
    
         
             
                    raise NotImplementedError
         
     | 
| 
       158 
159 
     | 
    
         | 
| 
      
 160 
     | 
    
         
            +
                @property
         
     | 
| 
      
 161 
     | 
    
         
            +
                def i3d_processing(self) -> bool:
         
     | 
| 
      
 162 
     | 
    
         
            +
                    """Returns whether the i3d file should be processed.
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 165 
     | 
    
         
            +
                        bool: True if the i3d file should be processed, False otherwise."""
         
     | 
| 
      
 166 
     | 
    
         
            +
                    return self._i3d_processing
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
       159 
168 
     | 
    
         
             
                @property
         
     | 
| 
       160 
169 
     | 
    
         
             
                def additional_dem_name(self) -> str | None:
         
     | 
| 
       161 
170 
     | 
    
         
             
                    """Returns the name of the additional DEM file.
         
     | 
| 
         @@ -164,6 +173,17 @@ class Game: 
     | 
|
| 
       164 
173 
     | 
    
         
             
                        str | None: The name of the additional DEM file."""
         
     | 
| 
       165 
174 
     | 
    
         
             
                    return self._additional_dem_name
         
     | 
| 
       166 
175 
     | 
    
         | 
| 
      
 176 
     | 
    
         
            +
                def splines_file_path(self, map_directory: str) -> str:
         
     | 
| 
      
 177 
     | 
    
         
            +
                    """Returns the path to the splines file.
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
      
 179 
     | 
    
         
            +
                    Arguments:
         
     | 
| 
      
 180 
     | 
    
         
            +
                        map_directory (str): The path to the map directory.
         
     | 
| 
      
 181 
     | 
    
         
            +
             
     | 
| 
      
 182 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 183 
     | 
    
         
            +
                        str: The path to the splines file."""
         
     | 
| 
      
 184 
     | 
    
         
            +
                    i3d_base_directory = os.path.dirname(self.i3d_file_path(map_directory))
         
     | 
| 
      
 185 
     | 
    
         
            +
                    return os.path.join(i3d_base_directory, "splines.i3d")
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
       167 
187 
     | 
    
         | 
| 
       168 
188 
     | 
    
         
             
            # pylint: disable=W0223
         
     | 
| 
       169 
189 
     | 
    
         
             
            class FS22(Game):
         
     | 
| 
         @@ -172,6 +192,7 @@ class FS22(Game): 
     | 
|
| 
       172 
192 
     | 
    
         
             
                code = "FS22"
         
     | 
| 
       173 
193 
     | 
    
         
             
                _map_template_path = os.path.join(working_directory, "data", "fs22-map-template.zip")
         
     | 
| 
       174 
194 
     | 
    
         
             
                _texture_schema = os.path.join(working_directory, "data", "fs22-texture-schema.json")
         
     | 
| 
      
 195 
     | 
    
         
            +
                _i3d_processing = False
         
     | 
| 
       175 
196 
     | 
    
         | 
| 
       176 
197 
     | 
    
         
             
                def dem_file_path(self, map_directory: str) -> str:
         
     | 
| 
       177 
198 
     | 
    
         
             
                    """Returns the path to the DEM file.
         
     | 
| 
         @@ -193,6 +214,16 @@ class FS22(Game): 
     | 
|
| 
       193 
214 
     | 
    
         
             
                        str: The path to the weights directory."""
         
     | 
| 
       194 
215 
     | 
    
         
             
                    return os.path.join(map_directory, "maps", "map", "data")
         
     | 
| 
       195 
216 
     | 
    
         | 
| 
      
 217 
     | 
    
         
            +
                def i3d_file_path(self, map_directory: str) -> str:
         
     | 
| 
      
 218 
     | 
    
         
            +
                    """Returns the path to the i3d file.
         
     | 
| 
      
 219 
     | 
    
         
            +
             
     | 
| 
      
 220 
     | 
    
         
            +
                    Arguments:
         
     | 
| 
      
 221 
     | 
    
         
            +
                        map_directory (str): The path to the map directory.
         
     | 
| 
      
 222 
     | 
    
         
            +
             
     | 
| 
      
 223 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 224 
     | 
    
         
            +
                        str: The path to the i3d file."""
         
     | 
| 
      
 225 
     | 
    
         
            +
                    return os.path.join(map_directory, "maps", "map", "map.i3d")
         
     | 
| 
      
 226 
     | 
    
         
            +
             
     | 
| 
       196 
227 
     | 
    
         | 
| 
       197 
228 
     | 
    
         
             
            class FS25(Game):
         
     | 
| 
       198 
229 
     | 
    
         
             
                """Class used to define the game version FS25."""
         
     |