maps4fs 1.8.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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