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.
- maps4fs/__init__.py +22 -0
- maps4fs/generator/__init__.py +1 -0
- maps4fs/generator/background.py +625 -0
- maps4fs/generator/component.py +553 -0
- maps4fs/generator/config.py +109 -0
- maps4fs/generator/dem.py +297 -0
- maps4fs/generator/dtm/__init__.py +0 -0
- maps4fs/generator/dtm/base/wcs.py +71 -0
- maps4fs/generator/dtm/base/wms.py +70 -0
- maps4fs/generator/dtm/bavaria.py +113 -0
- maps4fs/generator/dtm/dtm.py +637 -0
- maps4fs/generator/dtm/england.py +31 -0
- maps4fs/generator/dtm/hessen.py +31 -0
- maps4fs/generator/dtm/niedersachsen.py +39 -0
- maps4fs/generator/dtm/nrw.py +30 -0
- maps4fs/generator/dtm/srtm.py +127 -0
- maps4fs/generator/dtm/usgs.py +87 -0
- maps4fs/generator/dtm/utils.py +61 -0
- maps4fs/generator/game.py +247 -0
- maps4fs/generator/grle.py +470 -0
- maps4fs/generator/i3d.py +624 -0
- maps4fs/generator/map.py +275 -0
- maps4fs/generator/qgis.py +196 -0
- maps4fs/generator/satellite.py +92 -0
- maps4fs/generator/settings.py +187 -0
- maps4fs/generator/texture.py +893 -0
- maps4fs/logger.py +46 -0
- maps4fs/toolbox/__init__.py +1 -0
- maps4fs/toolbox/background.py +63 -0
- maps4fs/toolbox/custom_osm.py +67 -0
- maps4fs/toolbox/dem.py +112 -0
- maps4fs-1.8.0.dist-info/LICENSE.md +190 -0
- maps4fs-1.8.0.dist-info/METADATA +693 -0
- maps4fs-1.8.0.dist-info/RECORD +36 -0
- maps4fs-1.8.0.dist-info/WHEEL +5 -0
- maps4fs-1.8.0.dist-info/top_level.txt +1 -0
maps4fs/generator/map.py
ADDED
@@ -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
|