maps4fs 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,275 @@
1
+ """This module contains Map class, which is used to generate map using all components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ from typing import Any, Generator
9
+
10
+ from maps4fs.generator.component import Component
11
+ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
12
+ from maps4fs.generator.game import Game
13
+ from maps4fs.generator.settings import (
14
+ BackgroundSettings,
15
+ DEMSettings,
16
+ GRLESettings,
17
+ I3DSettings,
18
+ SatelliteSettings,
19
+ SharedSettings,
20
+ SplineSettings,
21
+ TextureSettings,
22
+ )
23
+ from maps4fs.logger import Logger
24
+
25
+
26
+ # pylint: disable=R0913, R0902, R0914
27
+ class Map:
28
+ """Class used to generate map using all components.
29
+
30
+ Arguments:
31
+ game (Type[Game]): Game for which the map is generated.
32
+ coordinates (tuple[float, float]): Coordinates of the center of the map.
33
+ size (int): Height and width of the map in pixels (it's a square).
34
+ map_directory (str): Path to the directory where map files will be stored.
35
+ logger (Any): Logger instance
36
+ """
37
+
38
+ def __init__( # pylint: disable=R0917, R0915
39
+ self,
40
+ game: Game,
41
+ dtm_provider: DTMProvider,
42
+ dtm_provider_settings: DTMProviderSettings,
43
+ coordinates: tuple[float, float],
44
+ size: int,
45
+ rotation: int,
46
+ map_directory: str,
47
+ logger: Any = None,
48
+ custom_osm: str | None = None,
49
+ dem_settings: DEMSettings = DEMSettings(),
50
+ background_settings: BackgroundSettings = BackgroundSettings(),
51
+ grle_settings: GRLESettings = GRLESettings(),
52
+ i3d_settings: I3DSettings = I3DSettings(),
53
+ texture_settings: TextureSettings = TextureSettings(),
54
+ spline_settings: SplineSettings = SplineSettings(),
55
+ satellite_settings: SatelliteSettings = SatelliteSettings(),
56
+ **kwargs,
57
+ ):
58
+ if not logger:
59
+ logger = Logger(to_stdout=True, to_file=False)
60
+ self.logger = logger
61
+ self.size = size
62
+
63
+ if rotation:
64
+ rotation_multiplier = 1.5
65
+ else:
66
+ rotation_multiplier = 1
67
+
68
+ self.rotation = rotation
69
+ self.rotated_size = int(size * rotation_multiplier)
70
+
71
+ self.game = game
72
+ self.dtm_provider = dtm_provider
73
+ self.dtm_provider_settings = dtm_provider_settings
74
+ self.components: list[Component] = []
75
+ self.coordinates = coordinates
76
+ self.map_directory = map_directory
77
+
78
+ self.logger.info("Game was set to %s", game.code)
79
+
80
+ self.custom_osm = custom_osm
81
+ self.logger.info("Custom OSM file: %s", custom_osm)
82
+
83
+ # Make a copy of a custom osm file to the map directory, so it will be
84
+ # included in the output archive.
85
+ if custom_osm:
86
+ copy_path = os.path.join(self.map_directory, "custom_osm.osm")
87
+ shutil.copyfile(custom_osm, copy_path)
88
+ self.logger.debug("Custom OSM file copied to %s", copy_path)
89
+
90
+ self.dem_settings = dem_settings
91
+ self.logger.info("DEM settings: %s", dem_settings)
92
+ if self.dem_settings.water_depth > 0:
93
+ # Make sure that the plateau value is >= water_depth
94
+ self.dem_settings.plateau = max(
95
+ self.dem_settings.plateau, self.dem_settings.water_depth
96
+ )
97
+ self.logger.info(
98
+ "Plateau value was set to %s to be >= water_depth value %s",
99
+ self.dem_settings.plateau,
100
+ self.dem_settings.water_depth,
101
+ )
102
+
103
+ self.background_settings = background_settings
104
+ self.logger.info("Background settings: %s", background_settings)
105
+ self.grle_settings = grle_settings
106
+ self.logger.info("GRLE settings: %s", grle_settings)
107
+ self.i3d_settings = i3d_settings
108
+ self.logger.info("I3D settings: %s", i3d_settings)
109
+ self.texture_settings = texture_settings
110
+ self.logger.info("Texture settings: %s", texture_settings)
111
+ self.spline_settings = spline_settings
112
+ self.logger.info("Spline settings: %s", spline_settings)
113
+ self.satellite_settings = satellite_settings
114
+
115
+ os.makedirs(self.map_directory, exist_ok=True)
116
+ self.logger.debug("Map directory created: %s", self.map_directory)
117
+
118
+ settings = [
119
+ dem_settings,
120
+ background_settings,
121
+ grle_settings,
122
+ i3d_settings,
123
+ texture_settings,
124
+ spline_settings,
125
+ satellite_settings,
126
+ ]
127
+
128
+ settings_json = {}
129
+
130
+ for setting in settings:
131
+ settings_json[setting.__class__.__name__] = setting.model_dump()
132
+
133
+ save_path = os.path.join(self.map_directory, "generation_settings.json")
134
+
135
+ with open(save_path, "w", encoding="utf-8") as file:
136
+ json.dump(settings_json, file, indent=4)
137
+
138
+ self.shared_settings = SharedSettings()
139
+
140
+ self.texture_custom_schema = kwargs.get("texture_custom_schema", None)
141
+ if self.texture_custom_schema:
142
+ save_path = os.path.join(self.map_directory, "texture_custom_schema.json")
143
+ with open(save_path, "w", encoding="utf-8") as file:
144
+ json.dump(self.texture_custom_schema, file, indent=4)
145
+ self.logger.debug("Texture custom schema saved to %s", save_path)
146
+
147
+ self.tree_custom_schema = kwargs.get("tree_custom_schema", None)
148
+ if self.tree_custom_schema:
149
+ self.logger.info("Custom tree schema contains %s trees", len(self.tree_custom_schema))
150
+ save_path = os.path.join(self.map_directory, "tree_custom_schema.json")
151
+ with open(save_path, "w", encoding="utf-8") as file:
152
+ json.dump(self.tree_custom_schema, file, indent=4)
153
+ self.logger.debug("Tree custom schema saved to %s", save_path)
154
+
155
+ self.custom_background_path = kwargs.get("custom_background_path", None)
156
+ if self.custom_background_path:
157
+ save_path = os.path.join(self.map_directory, "custom_background.png")
158
+ shutil.copyfile(self.custom_background_path, save_path)
159
+
160
+ try:
161
+ shutil.unpack_archive(game.template_path, self.map_directory)
162
+ self.logger.debug("Map template unpacked to %s", self.map_directory)
163
+ except Exception as e:
164
+ raise RuntimeError(f"Can not unpack map template due to error: {e}") from e
165
+
166
+ def generate(self) -> Generator[str, None, None]:
167
+ """Launch map generation using all components. Yield component names during the process.
168
+
169
+ Yields:
170
+ Generator[str, None, None]: Component names.
171
+ """
172
+ self.logger.info(
173
+ "Starting map generation. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
174
+ self.game.code,
175
+ self.coordinates,
176
+ self.size,
177
+ self.rotation,
178
+ )
179
+
180
+ for game_component in self.game.components:
181
+ component = game_component(
182
+ self.game,
183
+ self,
184
+ self.coordinates,
185
+ self.size,
186
+ self.rotated_size,
187
+ self.rotation,
188
+ self.map_directory,
189
+ self.logger,
190
+ texture_custom_schema=self.texture_custom_schema,
191
+ tree_custom_schema=self.tree_custom_schema,
192
+ )
193
+ self.components.append(component)
194
+
195
+ yield component.__class__.__name__
196
+
197
+ try:
198
+ component.process()
199
+ except Exception as e: # pylint: disable=W0718
200
+ self.logger.error(
201
+ "Error processing component %s: %s",
202
+ component.__class__.__name__,
203
+ e,
204
+ )
205
+ raise e
206
+
207
+ try:
208
+ component.commit_generation_info()
209
+ except Exception as e: # pylint: disable=W0718
210
+ self.logger.error(
211
+ "Error committing generation info for component %s: %s",
212
+ component.__class__.__name__,
213
+ e,
214
+ )
215
+ raise e
216
+
217
+ self.logger.info(
218
+ "Map generation completed. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
219
+ self.game.code,
220
+ self.coordinates,
221
+ self.size,
222
+ self.rotation,
223
+ )
224
+
225
+ def get_component(self, component_name: str) -> Component | None:
226
+ """Get component by name.
227
+
228
+ Arguments:
229
+ component_name (str): Name of the component.
230
+
231
+ Returns:
232
+ Component | None: Component instance or None if not found.
233
+ """
234
+ for component in self.components:
235
+ if component.__class__.__name__ == component_name:
236
+ return component
237
+ return None
238
+
239
+ def previews(self) -> list[str]:
240
+ """Get list of preview images.
241
+
242
+ Returns:
243
+ list[str]: List of preview images.
244
+ """
245
+ previews = []
246
+ for component in self.components:
247
+ try:
248
+ previews.extend(component.previews())
249
+ except Exception as e: # pylint: disable=W0718
250
+ self.logger.error(
251
+ "Error getting previews for component %s: %s",
252
+ component.__class__.__name__,
253
+ e,
254
+ )
255
+ return previews
256
+
257
+ def pack(self, archive_path: str, remove_source: bool = True) -> str:
258
+ """Pack map directory to zip archive.
259
+
260
+ Arguments:
261
+ archive_path (str): Path to the archive.
262
+ remove_source (bool, optional): Remove source directory after packing.
263
+
264
+ Returns:
265
+ str: Path to the archive.
266
+ """
267
+ archive_path = shutil.make_archive(archive_path, "zip", self.map_directory)
268
+ self.logger.debug("Map packed to %s.zip", archive_path)
269
+ if remove_source:
270
+ try:
271
+ shutil.rmtree(self.map_directory)
272
+ self.logger.debug("Map directory removed: %s", self.map_directory)
273
+ except Exception as e: # pylint: disable=W0718
274
+ self.logger.debug("Error removing map directory %s: %s", self.map_directory, e)
275
+ return archive_path
@@ -0,0 +1,196 @@
1
+ """This module contains templates for generating QGIS scripts."""
2
+
3
+ import os
4
+
5
+ BBOX_TEMPLATE = """
6
+ layers = [
7
+ {layers}
8
+ ]
9
+ for layer in layers:
10
+ name = "Bounding_Box_" + layer[0]
11
+ north, south, east, west = layer[1:]
12
+
13
+ # Create a rectangle geometry from the bounding box.
14
+ rect = QgsRectangle(north, east, south, west)
15
+
16
+ # Create a new memory layer to hold the bounding box.
17
+ layer = QgsVectorLayer("Polygon?crs=EPSG:3857", name, "memory")
18
+ provider = layer.dataProvider()
19
+
20
+ # Add the rectangle as a feature to the layer.
21
+ feature = QgsFeature()
22
+ feature.setGeometry(QgsGeometry.fromRect(rect))
23
+ provider.addFeatures([feature])
24
+
25
+ # Add the layer to the map.
26
+ QgsProject.instance().addMapLayer(layer)
27
+
28
+ # Set the fill opacity.
29
+ symbol = layer.renderer().symbol()
30
+ symbol_layer = symbol.symbolLayer(0)
31
+
32
+ # Set the stroke color and width.
33
+ symbol_layer.setStrokeColor(QColor(0, 255, 0))
34
+ symbol_layer.setStrokeWidth(0.2)
35
+ symbol_layer.setFillColor(QColor(0, 0, 255, 0))
36
+ layer.triggerRepaint()
37
+ """
38
+
39
+ POINT_TEMPLATE = """
40
+ layers = [
41
+ {layers}
42
+ ]
43
+ for layer in layers:
44
+ name = "Points_" + layer[0]
45
+ north, south, east, west = layer[1:]
46
+
47
+ top_left = QgsPointXY(north, west)
48
+ top_right = QgsPointXY(north, east)
49
+ bottom_right = QgsPointXY(south, east)
50
+ bottom_left = QgsPointXY(south, west)
51
+
52
+ points = [top_left, top_right, bottom_right, bottom_left, top_left]
53
+
54
+ # Create a new layer
55
+ layer = QgsVectorLayer('Point?crs=EPSG:3857', name, 'memory')
56
+ provider = layer.dataProvider()
57
+
58
+ # Add fields
59
+ provider.addAttributes([QgsField("id", QVariant.Int)])
60
+ layer.updateFields()
61
+
62
+ # Create and add features for each point
63
+ for i, point in enumerate(points):
64
+ feature = QgsFeature()
65
+ feature.setGeometry(QgsGeometry.fromPointXY(point))
66
+ feature.setAttributes([i + 1])
67
+ provider.addFeature(feature)
68
+
69
+ layer.updateExtents()
70
+
71
+ # Add the layer to the project
72
+ QgsProject.instance().addMapLayer(layer)
73
+ """
74
+
75
+ RASTERIZE_TEMPLATE = """
76
+ import processing
77
+
78
+ ############################################################
79
+ ####### ADD THE DIRECTORY FOR THE FILES TO SAVE HERE #######
80
+ ############################################################
81
+ ############### IT MUST END WITH A SLASH (/) ###############
82
+ ############################################################
83
+
84
+ SAVE_DIR = "C:/Users/iwatk/OneDrive/Desktop/"
85
+
86
+ ############################################################
87
+
88
+ layers = [
89
+ {layers}
90
+ ]
91
+
92
+ for layer in layers:
93
+ name = layer[0]
94
+ north, south, east, west = layer[1:]
95
+
96
+ epsg3857_string = str(north) + "," + str(south) + "," + str(east) + "," + str(west) + " [EPSG:3857]"
97
+ file_path = SAVE_DIR + name + ".tif"
98
+
99
+ processing.run(
100
+ "native:rasterize",
101
+ {{
102
+ "EXTENT": epsg3857_string,
103
+ "EXTENT_BUFFER": 0,
104
+ "TILE_SIZE": 64,
105
+ "MAP_UNITS_PER_PIXEL": 1,
106
+ "MAKE_BACKGROUND_TRANSPARENT": False,
107
+ "MAP_THEME": None,
108
+ "LAYERS": None,
109
+ "OUTPUT": file_path,
110
+ }},
111
+ )
112
+ """
113
+
114
+
115
+ def _get_template(layers: list[tuple[str, float, float, float, float]], template: str) -> str:
116
+ """Returns a template for creating layers in QGIS.
117
+
118
+ Arguments:
119
+ layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
120
+ layer name and the bounding box coordinates.
121
+ template (str): The template for creating layers in QGIS.
122
+
123
+ Returns:
124
+ str: The template for creating layers in QGIS.
125
+ """
126
+ return template.format(
127
+ layers=",\n ".join(
128
+ [
129
+ f'("{name}", {north}, {south}, {east}, {west})'
130
+ for name, north, south, east, west in layers
131
+ ]
132
+ )
133
+ )
134
+
135
+
136
+ def get_bbox_template(layers: list[tuple[str, float, float, float, float]]) -> str:
137
+ """Returns a template for creating bounding box layers in QGIS.
138
+
139
+ Arguments:
140
+ layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
141
+ layer name and the bounding box coordinates.
142
+
143
+ Returns:
144
+ str: The template for creating bounding box layers in QGIS.
145
+ """
146
+ return _get_template(layers, BBOX_TEMPLATE)
147
+
148
+
149
+ def get_point_template(layers: list[tuple[str, float, float, float, float]]) -> str:
150
+ """Returns a template for creating point layers in QGIS.
151
+
152
+ Arguments:
153
+ layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
154
+ layer name and the bounding box coordinates.
155
+
156
+ Returns:
157
+ str: The template for creating point layers in QGIS.
158
+ """
159
+ return _get_template(layers, POINT_TEMPLATE)
160
+
161
+
162
+ def get_rasterize_template(layers: list[tuple[str, float, float, float, float]]) -> str:
163
+ """Returns a template for rasterizing bounding box layers in QGIS.
164
+
165
+ Arguments:
166
+ layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
167
+ layer name and the bounding box coordinates.
168
+
169
+ Returns:
170
+ str: The template for rasterizing bounding box layers in QGIS.
171
+ """
172
+ return _get_template(layers, RASTERIZE_TEMPLATE)
173
+
174
+
175
+ def save_scripts(
176
+ layers: list[tuple[str, float, float, float, float]], file_prefix: str, save_directory: str
177
+ ) -> None:
178
+ """Saves QGIS scripts for creating bounding box, point, and raster layers.
179
+
180
+ Arguments:
181
+ layers (list[tuple[str, float, float, float, float]]): A list of tuples containing the
182
+ layer name and the bounding box coordinates.
183
+ save_dir (str): The directory to save the scripts.
184
+ """
185
+ script_files = [
186
+ (f"{file_prefix}_bbox.py", get_bbox_template),
187
+ (f"{file_prefix}_rasterize.py", get_rasterize_template),
188
+ (f"{file_prefix}_point.py", get_point_template),
189
+ ]
190
+
191
+ for script_file, process_function in script_files:
192
+ script_path = os.path.join(save_directory, script_file)
193
+ script_content = process_function(layers) # type: ignore
194
+
195
+ with open(script_path, "w", encoding="utf-8") as file:
196
+ file.write(script_content)
@@ -0,0 +1,92 @@
1
+ """This module contains the Satellite class for the maps4fs package to download satellite images
2
+ for the map."""
3
+
4
+ import os
5
+
6
+ import cv2
7
+ from pygmdl import save_image # type: ignore
8
+
9
+ from maps4fs.generator.background import DEFAULT_DISTANCE
10
+ from maps4fs.generator.component import Component
11
+ from maps4fs.generator.texture import PREVIEW_MAXIMUM_SIZE
12
+
13
+
14
+ # pylint: disable=W0223
15
+ class Satellite(Component):
16
+ """Component for to download satellite images for the map.
17
+
18
+ Arguments:
19
+ game (Game): The game instance for which the map is generated.
20
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
21
+ map_size (int): The size of the map in pixels.
22
+ map_rotated_size (int): The size of the map in pixels after rotation.
23
+ rotation (int): The rotation angle of the map.
24
+ map_directory (str): The directory where the map files are stored.
25
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
26
+ info, warning. If not provided, default logging will be used.
27
+ """
28
+
29
+ def preprocess(self) -> None:
30
+ """This component does not require any preprocessing."""
31
+ return
32
+
33
+ def process(self) -> None:
34
+ """Downloads the satellite images for the map."""
35
+ self.image_paths = [] # pylint: disable=W0201
36
+ if not self.map.satellite_settings.download_images:
37
+ self.logger.debug("Satellite images download is disabled.")
38
+ return
39
+
40
+ margin = self.map.satellite_settings.satellite_margin
41
+ overview_size = (self.map_size + margin) * 2
42
+ overwiew_path = os.path.join(self.satellite_directory, "satellite_overview.png")
43
+
44
+ background_size = self.map_size + (DEFAULT_DISTANCE + margin) * 2
45
+ background_path = os.path.join(self.satellite_directory, "satellite_background.png")
46
+
47
+ sizes = [overview_size, background_size]
48
+ self.image_paths = [overwiew_path, background_path] # pylint: disable=W0201
49
+
50
+ for size, path in zip(sizes, self.image_paths):
51
+ try:
52
+ lat, lon = self.coordinates
53
+ zoom = self.map.satellite_settings.zoom_level
54
+ save_image(
55
+ lat,
56
+ lon,
57
+ size,
58
+ output_path=path,
59
+ rotation=self.rotation,
60
+ zoom=zoom,
61
+ from_center=True,
62
+ logger=self.logger,
63
+ )
64
+ except Exception as e: # pylint: disable=W0718
65
+ self.logger.error(f"Failed to download satellite image: {e}")
66
+ continue
67
+
68
+ # pylint: disable=no-member
69
+ def previews(self) -> list[str]:
70
+ """Returns the paths to the preview images.
71
+
72
+ Returns:
73
+ list[str]: List of paths to the preview images.
74
+ """
75
+ previews = []
76
+ for image_path in self.image_paths:
77
+ if not os.path.isfile(image_path):
78
+ self.logger.warning(f"File {image_path} does not exist.")
79
+ continue
80
+ image = cv2.imread(image_path)
81
+ if image is None:
82
+ self.logger.warning(f"Failed to read image from {image_path}")
83
+ continue
84
+
85
+ if image.shape[0] > PREVIEW_MAXIMUM_SIZE or image.shape[1] > PREVIEW_MAXIMUM_SIZE:
86
+ image = cv2.resize(image, (PREVIEW_MAXIMUM_SIZE, PREVIEW_MAXIMUM_SIZE))
87
+
88
+ preview_path = os.path.join(self.previews_directory, os.path.basename(image_path))
89
+ cv2.imwrite(preview_path, image)
90
+ previews.append(preview_path)
91
+
92
+ return previews