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,553 @@
1
+ """This module contains the base class for all map generation components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from copy import deepcopy
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ import cv2 # type: ignore
11
+ import osmnx as ox # type: ignore
12
+ from pyproj import Transformer
13
+ from shapely.affinity import rotate, translate # type: ignore
14
+ from shapely.geometry import LineString, Polygon, box # type: ignore
15
+
16
+ from maps4fs.generator.qgis import save_scripts
17
+
18
+ if TYPE_CHECKING:
19
+ from maps4fs.generator.game import Game
20
+ from maps4fs.generator.map import Map
21
+
22
+
23
+ # pylint: disable=R0801, R0903, R0902, R0904, R0913, R0917
24
+ class Component:
25
+ """Base class for all map generation components.
26
+
27
+ Arguments:
28
+ game (Game): The game instance for which the map is generated.
29
+ map (Map): The map instance for which the component is generated.
30
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
31
+ map_size (int): The size of the map in pixels.
32
+ map_rotated_size (int): The size of the map in pixels after rotation.
33
+ rotation (int): The rotation angle of the map.
34
+ map_directory (str): The directory where the map files are stored.
35
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
36
+ info, warning. If not provided, default logging will be used.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ game: Game,
42
+ map: Map, # pylint: disable=W0622
43
+ coordinates: tuple[float, float],
44
+ map_size: int,
45
+ map_rotated_size: int,
46
+ rotation: int,
47
+ map_directory: str,
48
+ logger: Any = None,
49
+ **kwargs: dict[str, Any],
50
+ ):
51
+ self.game = game
52
+ self.map = map
53
+ self.coordinates = coordinates
54
+ self.map_size = map_size
55
+ self.map_rotated_size = map_rotated_size
56
+ self.rotation = rotation
57
+ self.map_directory = map_directory
58
+ self.logger = logger
59
+ self.kwargs = kwargs
60
+
61
+ self.logger.debug(
62
+ "Component %s initialized. Map size: %s, map rotated size: %s", # type: ignore
63
+ self.__class__.__name__,
64
+ self.map_size,
65
+ self.map_rotated_size,
66
+ )
67
+
68
+ os.makedirs(self.previews_directory, exist_ok=True)
69
+ os.makedirs(self.scripts_directory, exist_ok=True)
70
+ os.makedirs(self.info_layers_directory, exist_ok=True)
71
+ os.makedirs(self.satellite_directory, exist_ok=True)
72
+
73
+ self.save_bbox()
74
+ self.preprocess()
75
+
76
+ def preprocess(self) -> None:
77
+ """Prepares the component for processing. Must be implemented in the child class.
78
+
79
+ Raises:
80
+ NotImplementedError: If the method is not implemented in the child class.
81
+ """
82
+ raise NotImplementedError
83
+
84
+ def process(self) -> None:
85
+ """Launches the component processing. Must be implemented in the child class.
86
+
87
+ Raises:
88
+ NotImplementedError: If the method is not implemented in the child class.
89
+ """
90
+ raise NotImplementedError
91
+
92
+ def previews(self) -> list[str]:
93
+ """Returns a list of paths to the preview images. Must be implemented in the child class.
94
+
95
+ Raises:
96
+ NotImplementedError: If the method is not implemented in the child class.
97
+ """
98
+ raise NotImplementedError
99
+
100
+ @property
101
+ def previews_directory(self) -> str:
102
+ """The directory where the preview images are stored.
103
+
104
+ Returns:
105
+ str: The directory where the preview images are stored.
106
+ """
107
+ return os.path.join(self.map_directory, "previews")
108
+
109
+ @property
110
+ def info_layers_directory(self) -> str:
111
+ """The directory where the info layers are stored.
112
+
113
+ Returns:
114
+ str: The directory where the info layers are stored.
115
+ """
116
+ return os.path.join(self.map_directory, "info_layers")
117
+
118
+ @property
119
+ def scripts_directory(self) -> str:
120
+ """The directory where the scripts are stored.
121
+
122
+ Returns:
123
+ str: The directory where the scripts are stored.
124
+ """
125
+ return os.path.join(self.map_directory, "scripts")
126
+
127
+ @property
128
+ def satellite_directory(self) -> str:
129
+ """The directory where the satellite images are stored.
130
+
131
+ Returns:
132
+ str: The directory where the satellite images are stored.
133
+ """
134
+ return os.path.join(self.map_directory, "satellite")
135
+
136
+ @property
137
+ def generation_info_path(self) -> str:
138
+ """The path to the generation info JSON file.
139
+
140
+ Returns:
141
+ str: The path to the generation info JSON file.
142
+ """
143
+ return os.path.join(self.map_directory, "generation_info.json")
144
+
145
+ def info_sequence(self) -> dict[Any, Any]:
146
+ """Returns the information sequence for the component. Must be implemented in the child
147
+ class. If the component does not have an information sequence, an empty dictionary must be
148
+ returned.
149
+
150
+ Returns:
151
+ dict[Any, Any]: The information sequence for the component.
152
+ """
153
+ return {}
154
+
155
+ def commit_generation_info(self) -> None:
156
+ """Commits the generation info to the generation info JSON file."""
157
+ self.update_generation_info(self.info_sequence())
158
+
159
+ def update_generation_info(self, data: dict[Any, Any]) -> None:
160
+ """Updates the generation info with the provided data.
161
+ If the generation info file does not exist, it will be created.
162
+
163
+ Arguments:
164
+ data (dict[Any, Any]): The data to update the generation info with.
165
+ """
166
+ if os.path.isfile(self.generation_info_path):
167
+ with open(self.generation_info_path, "r", encoding="utf-8") as file:
168
+ generation_info = json.load(file)
169
+ self.logger.debug("Loaded generation info from %s", self.generation_info_path)
170
+ else:
171
+ self.logger.debug(
172
+ "Generation info file does not exist, creating a new one in %s",
173
+ self.generation_info_path,
174
+ )
175
+ generation_info = {}
176
+
177
+ updated_generation_info = deepcopy(generation_info)
178
+ updated_generation_info[self.__class__.__name__] = data
179
+
180
+ self.logger.debug("Updated generation info, now contains %s fields", len(data))
181
+
182
+ with open(self.generation_info_path, "w", encoding="utf-8") as file:
183
+ json.dump(updated_generation_info, file, indent=4)
184
+
185
+ self.logger.debug("Saved updated generation info to %s", self.generation_info_path)
186
+
187
+ def get_bbox(
188
+ self,
189
+ coordinates: tuple[float, float] | None = None,
190
+ distance: int | None = None,
191
+ ) -> tuple[float, float, float, float]:
192
+ """Calculates the bounding box of the map from the coordinates and the height and
193
+ width of the map.
194
+ If coordinates and distance are not provided, the instance variables are used.
195
+
196
+ Arguments:
197
+ coordinates (tuple[float, float], optional): The latitude and longitude of the center
198
+ of the map. Defaults to None.
199
+ distance (int, optional): The distance from the center of the map to the edge of the
200
+ map in all directions. Defaults to None.
201
+
202
+ Returns:
203
+ tuple[float, float, float, float]: The bounding box of the map.
204
+ """
205
+ coordinates = coordinates or self.coordinates
206
+ distance = distance or int(self.map_rotated_size / 2)
207
+
208
+ west, south, east, north = ox.utils_geo.bbox_from_point( # type: ignore
209
+ coordinates,
210
+ dist=distance,
211
+ )
212
+
213
+ bbox = north, south, east, west
214
+ self.logger.debug(
215
+ "Calculated bounding box for component: %s: %s, distance: %s",
216
+ self.__class__.__name__,
217
+ bbox,
218
+ distance,
219
+ )
220
+ return bbox
221
+
222
+ def save_bbox(self) -> None:
223
+ """Saves the bounding box of the map to the component instance from the coordinates and the
224
+ height and width of the map.
225
+ """
226
+ self.bbox = self.get_bbox()
227
+ self.logger.debug("Saved bounding box: %s", self.bbox)
228
+
229
+ @property
230
+ def new_bbox(self) -> tuple[float, float, float, float]:
231
+ """This property is used for a new version of osmnx library, where the order of coordinates
232
+ has been changed to (left, bottom, right, top).
233
+
234
+ Returns:
235
+ tuple[float, float, float, float]: The bounding box of the map in the new order:
236
+ (left, bottom, right, top).
237
+ """
238
+ # FutureWarning: The expected order of coordinates in `bbox`
239
+ # will change in the v2.0.0 release to `(left, bottom, right, top)`.
240
+ north, south, east, west = self.bbox
241
+ return west, south, east, north
242
+
243
+ def get_espg3857_bbox(
244
+ self, bbox: tuple[float, float, float, float] | None = None, add_margin: bool = False
245
+ ) -> tuple[float, float, float, float]:
246
+ """Converts the bounding box to EPSG:3857.
247
+ If the bounding box is not provided, the instance variable is used.
248
+
249
+ Arguments:
250
+ bbox (tuple[float, float, float, float], optional): The bounding box to convert.
251
+ add_margin (bool, optional): Whether to add a margin to the bounding box.
252
+
253
+ Returns:
254
+ tuple[float, float, float, float]: The bounding box in EPSG:3857.
255
+ """
256
+ bbox = bbox or self.bbox
257
+ north, south, east, west = bbox
258
+ transformer = Transformer.from_crs("epsg:4326", "epsg:3857")
259
+ epsg3857_north, epsg3857_west = transformer.transform(north, west)
260
+ epsg3857_south, epsg3857_east = transformer.transform(south, east)
261
+
262
+ if add_margin:
263
+ MARGIN = 500 # pylint: disable=C0103
264
+ epsg3857_north = int(epsg3857_north - MARGIN)
265
+ epsg3857_south = int(epsg3857_south + MARGIN)
266
+ epsg3857_east = int(epsg3857_east - MARGIN)
267
+ epsg3857_west = int(epsg3857_west + MARGIN)
268
+
269
+ return epsg3857_north, epsg3857_south, epsg3857_east, epsg3857_west
270
+
271
+ def get_epsg3857_string(
272
+ self, bbox: tuple[float, float, float, float] | None = None, add_margin: bool = False
273
+ ) -> str:
274
+ """Converts the bounding box to EPSG:3857 string.
275
+ If the bounding box is not provided, the instance variable is used.
276
+
277
+ Arguments:
278
+ bbox (tuple[float, float, float, float], optional): The bounding box to convert.
279
+ add_margin (bool, optional): Whether to add a margin to the bounding box.
280
+
281
+ Returns:
282
+ str: The bounding box in EPSG:3857 string.
283
+ """
284
+ north, south, east, west = self.get_espg3857_bbox(bbox, add_margin=add_margin)
285
+ return f"{north},{south},{east},{west} [EPSG:3857]"
286
+
287
+ def create_qgis_scripts(
288
+ self, qgis_layers: list[tuple[str, float, float, float, float]]
289
+ ) -> None:
290
+ """Creates QGIS scripts from the given layers.
291
+ Each layer is a tuple where the first element is a name of the layer and the rest are the
292
+ bounding box coordinates in EPSG:3857.
293
+ For filenames, the class name is used as a prefix.
294
+
295
+ Arguments:
296
+ qgis_layers (list[tuple[str, float, float, float, float]]): The list of layers to
297
+ create scripts for.
298
+ """
299
+ class_name = self.__class__.__name__.lower()
300
+ save_scripts(qgis_layers, class_name, self.scripts_directory)
301
+
302
+ def get_polygon_center(self, polygon_points: list[tuple[int, int]]) -> tuple[int, int]:
303
+ """Calculates the center of a polygon defined by a list of points.
304
+
305
+ Arguments:
306
+ polygon_points (list[tuple[int, int]]): The points of the polygon.
307
+
308
+ Returns:
309
+ tuple[int, int]: The center of the polygon.
310
+ """
311
+ polygon = Polygon(polygon_points)
312
+ center = polygon.centroid
313
+ return int(center.x), int(center.y)
314
+
315
+ def absolute_to_relative(
316
+ self, point: tuple[int, int], center: tuple[int, int]
317
+ ) -> tuple[int, int]:
318
+ """Converts a pair of absolute coordinates to relative coordinates.
319
+
320
+ Arguments:
321
+ point (tuple[int, int]): The absolute coordinates.
322
+ center (tuple[int, int]): The center coordinates.
323
+
324
+ Returns:
325
+ tuple[int, int]: The relative coordinates.
326
+ """
327
+ cx, cy = center
328
+ x, y = point
329
+ return x - cx, y - cy
330
+
331
+ def top_left_coordinates_to_center(self, top_left: tuple[int, int]) -> tuple[int, int]:
332
+ """Converts a pair of coordinates from the top-left system to the center system.
333
+ In top-left system, the origin (0, 0) is in the top-left corner of the map, while in the
334
+ center system, the origin is in the center of the map.
335
+
336
+ Arguments:
337
+ top_left (tuple[int, int]): The coordinates in the top-left system.
338
+
339
+ Returns:
340
+ tuple[int, int]: The coordinates in the center system.
341
+ """
342
+ x, y = top_left
343
+ cs_x = x - self.map_size // 2
344
+ cs_y = y - self.map_size // 2
345
+
346
+ return cs_x, cs_y
347
+
348
+ # pylint: disable=R0914
349
+ def fit_object_into_bounds(
350
+ self,
351
+ polygon_points: list[tuple[int, int]] | None = None,
352
+ linestring_points: list[tuple[int, int]] | None = None,
353
+ margin: int = 0,
354
+ angle: int = 0,
355
+ border: int = 0,
356
+ ) -> list[tuple[int, int]]:
357
+ """Fits a polygon into the bounds of the map.
358
+
359
+ Arguments:
360
+ polygon_points (list[tuple[int, int]]): The points of the polygon.
361
+ linestring_points (list[tuple[int, int]]): The points of the linestring.
362
+ margin (int, optional): The margin to add to the polygon. Defaults to 0.
363
+ angle (int, optional): The angle to rotate the polygon by. Defaults to 0.
364
+ border (int, optional): The border to add to the bounds. Defaults to 0.
365
+
366
+ Returns:
367
+ list[tuple[int, int]]: The points of the polygon fitted into the map bounds.
368
+ """
369
+ if polygon_points is None and linestring_points is None:
370
+ raise ValueError("Either polygon or linestring points must be provided.")
371
+
372
+ min_x = min_y = 0 + border
373
+ max_x = max_y = self.map_size - border
374
+
375
+ object_type = Polygon if polygon_points else LineString
376
+
377
+ # polygon = Polygon(polygon_points)
378
+ osm_object = object_type(polygon_points or linestring_points)
379
+
380
+ if angle:
381
+ center_x = center_y = self.map_rotated_size // 2
382
+ self.logger.debug(
383
+ "Rotating the osm_object by %s degrees with center at %sx%s",
384
+ angle,
385
+ center_x,
386
+ center_y,
387
+ )
388
+ osm_object = rotate(osm_object, -angle, origin=(center_x, center_y))
389
+ offset = (self.map_size / 2) - (self.map_rotated_size / 2)
390
+ self.logger.debug("Translating the osm_object by %s", offset)
391
+ osm_object = translate(osm_object, xoff=offset, yoff=offset)
392
+ self.logger.debug("Rotated and translated the osm_object.")
393
+
394
+ if margin and object_type is Polygon:
395
+ osm_object = osm_object.buffer(margin, join_style="mitre")
396
+ if osm_object.is_empty:
397
+ raise ValueError("The osm_object is empty after adding the margin.")
398
+
399
+ # Create a bounding box for the map bounds
400
+ bounds = box(min_x, min_y, max_x, max_y)
401
+
402
+ # Intersect the osm_object with the bounds to fit it within the map
403
+ try:
404
+ fitted_osm_object = osm_object.intersection(bounds)
405
+ self.logger.debug("Fitted the osm_object into the bounds: %s", bounds)
406
+ except Exception as e:
407
+ raise ValueError( # pylint: disable=W0707
408
+ f"Could not fit the osm_object into the bounds: {e}"
409
+ )
410
+
411
+ if not isinstance(fitted_osm_object, object_type):
412
+ raise ValueError("The fitted osm_object is not valid (probably splitted into parts).")
413
+
414
+ # Return the fitted polygon points
415
+ if object_type is Polygon:
416
+ as_list = list(fitted_osm_object.exterior.coords)
417
+ elif object_type is LineString:
418
+ as_list = list(fitted_osm_object.coords)
419
+ else:
420
+ raise ValueError("The object type is not supported.")
421
+
422
+ if not as_list:
423
+ raise ValueError("The fitted osm_object has no points.")
424
+ return as_list
425
+
426
+ def get_infolayer_path(self, layer_name: str) -> str | None:
427
+ """Returns the path to the info layer file.
428
+
429
+ Arguments:
430
+ layer_name (str): The name of the layer.
431
+
432
+ Returns:
433
+ str | None: The path to the info layer file or None if the layer does not exist.
434
+ """
435
+ info_layer_path = os.path.join(self.info_layers_directory, f"{layer_name}.json")
436
+ if not os.path.isfile(info_layer_path):
437
+ self.logger.warning("Info layer %s does not exist", info_layer_path)
438
+ return None
439
+ return info_layer_path
440
+
441
+ # pylint: disable=R0913, R0917, R0914
442
+ def rotate_image(
443
+ self,
444
+ image_path: str,
445
+ angle: int,
446
+ output_height: int,
447
+ output_width: int,
448
+ output_path: str | None = None,
449
+ ) -> None:
450
+ """Rotates an image by a given angle around its center and cuts out the center to match
451
+ the output size.
452
+
453
+ Arguments:
454
+ image_path (str): The path to the image to rotate.
455
+ angle (int): The angle to rotate the image by.
456
+ output_height (int): The height of the output image.
457
+ output_width (int): The width of the output image.
458
+ """
459
+ if not os.path.isfile(image_path):
460
+ self.logger.warning("Image %s does not exist", image_path)
461
+ return
462
+
463
+ # pylint: disable=no-member
464
+ image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
465
+ if image is None:
466
+ self.logger.warning("Image %s could not be read", image_path)
467
+ return
468
+
469
+ self.logger.debug("Read image from %s with shape: %s", image_path, image.shape)
470
+
471
+ if not output_path:
472
+ output_path = image_path
473
+
474
+ height, width = image.shape[:2]
475
+ center = (width // 2, height // 2)
476
+
477
+ self.logger.debug(
478
+ "Rotating the image... Angle: %s, center: %s, height: %s, width: %s",
479
+ angle,
480
+ center,
481
+ height,
482
+ width,
483
+ )
484
+
485
+ rotation_matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
486
+ rotated = cv2.warpAffine(image, rotation_matrix, (width, height))
487
+
488
+ start_x = center[0] - output_width // 2
489
+ start_y = center[1] - output_height // 2
490
+ end_x = start_x + output_width
491
+ end_y = start_y + output_height
492
+
493
+ self.logger.debug(
494
+ "Cropping the rotated image: start_x: %s, start_y: %s, end_x: %s, end_y: %s",
495
+ start_x,
496
+ start_y,
497
+ end_x,
498
+ end_y,
499
+ )
500
+
501
+ cropped = rotated[start_y:end_y, start_x:end_x]
502
+
503
+ self.logger.debug("Shape of the cropped image: %s", cropped.shape)
504
+
505
+ cv2.imwrite(output_path, cropped)
506
+
507
+ @staticmethod
508
+ def interpolate_points(
509
+ polyline: list[tuple[int, int]], num_points: int = 4
510
+ ) -> list[tuple[int, int]]:
511
+ """Receives a list of tuples, which represents a polyline. Add additional points
512
+ between the existing points to make the polyline smoother.
513
+
514
+ Arguments:
515
+ polyline (list[tuple[int, int]]): The list of points to interpolate.
516
+ num_points (int): The number of additional points to add between each pair of points.
517
+
518
+ Returns:
519
+ list[tuple[int, int]]: The list of points with additional points.
520
+ """
521
+ if not polyline or num_points < 1:
522
+ return polyline
523
+
524
+ interpolated_polyline = []
525
+ for i in range(len(polyline) - 1):
526
+ p1 = polyline[i]
527
+ p2 = polyline[i + 1]
528
+ interpolated_polyline.append(p1)
529
+ for j in range(1, num_points + 1):
530
+ new_point = (
531
+ p1[0] + (p2[0] - p1[0]) * j / (num_points + 1),
532
+ p1[1] + (p2[1] - p1[1]) * j / (num_points + 1),
533
+ )
534
+ interpolated_polyline.append((int(new_point[0]), int(new_point[1])))
535
+ interpolated_polyline.append(polyline[-1])
536
+
537
+ return interpolated_polyline
538
+
539
+ def get_z_scaling_factor(self) -> float:
540
+ """Calculates the scaling factor for the Z axis based on the map settings.
541
+
542
+ Returns:
543
+ float -- The scaling factor for the Z axis.
544
+ """
545
+
546
+ scaling_factor = 1 / self.map.dem_settings.multiplier
547
+
548
+ if self.map.shared_settings.height_scale_multiplier:
549
+ scaling_factor *= self.map.shared_settings.height_scale_multiplier
550
+ if self.map.shared_settings.mesh_z_scaling_factor:
551
+ scaling_factor *= 1 / self.map.shared_settings.mesh_z_scaling_factor
552
+
553
+ return scaling_factor
@@ -0,0 +1,109 @@
1
+ """This module contains the Config class for map settings and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from xml.etree import ElementTree as ET
7
+
8
+ from maps4fs.generator.component import Component
9
+
10
+
11
+ # pylint: disable=R0903
12
+ class Config(Component):
13
+ """Component for map settings and configuration.
14
+
15
+ Arguments:
16
+ game (Game): The game instance for which the map is generated.
17
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
18
+ map_size (int): The size of the map in pixels (it's a square).
19
+ map_rotated_size (int): The size of the map in pixels after rotation.
20
+ rotation (int): The rotation angle of the map.
21
+ map_directory (str): The directory where the map files are stored.
22
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
23
+ info, warning. If not provided, default logging will be used.
24
+ """
25
+
26
+ def preprocess(self) -> None:
27
+ """Gets the path to the map XML file and saves it to the instance variable."""
28
+ self._map_xml_path = self.game.map_xml_path(self.map_directory)
29
+ self.logger.debug("Map XML path: %s.", self._map_xml_path)
30
+
31
+ def process(self) -> None:
32
+ """Sets the map size in the map.xml file."""
33
+ self._set_map_size()
34
+
35
+ def _set_map_size(self) -> None:
36
+ """Edits map.xml file to set correct map size."""
37
+ if not os.path.isfile(self._map_xml_path):
38
+ self.logger.warning("Map XML file not found: %s.", self._map_xml_path)
39
+ return
40
+ tree = ET.parse(self._map_xml_path)
41
+ self.logger.debug("Map XML file loaded from: %s.", self._map_xml_path)
42
+ root = tree.getroot()
43
+ for map_elem in root.iter("map"):
44
+ map_elem.set("width", str(self.map_size))
45
+ map_elem.set("height", str(self.map_size))
46
+ self.logger.debug(
47
+ "Map size set to %sx%s in Map XML file.",
48
+ self.map_size,
49
+ self.map_size,
50
+ )
51
+ tree.write(self._map_xml_path)
52
+ self.logger.debug("Map XML file saved to: %s.", self._map_xml_path)
53
+
54
+ def previews(self) -> list[str]:
55
+ """Returns a list of paths to the preview images (empty list).
56
+ The component does not generate any preview images so it returns an empty list.
57
+
58
+ Returns:
59
+ list[str]: An empty list.
60
+ """
61
+ return []
62
+
63
+ def info_sequence(self) -> dict[str, dict[str, str | float | int]]:
64
+ """Returns information about the component.
65
+ Overview section is needed to create the overview file (in-game map).
66
+
67
+ Returns:
68
+ dict[str, dict[str, str | float | int]]: Information about the component.
69
+ """
70
+ # The overview file is exactly 2X bigger than the map size, does not matter
71
+ # if the map is 2048x2048 or 4096x4096, the overview will be 4096x4096
72
+ # and the map will be in the center of the overview.
73
+ # That's why the distance is set to the map height not as a half of it.
74
+ bbox = self.get_bbox(distance=self.map_size)
75
+ south, west, north, east = bbox
76
+ epsg3857_string = self.get_epsg3857_string(bbox=bbox)
77
+ epsg3857_string_with_margin = self.get_epsg3857_string(bbox=bbox, add_margin=True)
78
+
79
+ self.qgis_sequence()
80
+
81
+ overview_data = {
82
+ "epsg3857_string": epsg3857_string,
83
+ "epsg3857_string_with_margin": epsg3857_string_with_margin,
84
+ "south": south,
85
+ "west": west,
86
+ "north": north,
87
+ "east": east,
88
+ "height": self.map_size * 2,
89
+ "width": self.map_size * 2,
90
+ }
91
+
92
+ data = {
93
+ "Overview": overview_data,
94
+ }
95
+
96
+ return data # type: ignore
97
+
98
+ def qgis_sequence(self) -> None:
99
+ """Generates QGIS scripts for creating bounding box layers and rasterizing them."""
100
+ bbox = self.get_bbox(distance=self.map_size)
101
+ espg3857_bbox = self.get_espg3857_bbox(bbox=bbox)
102
+ espg3857_bbox_with_margin = self.get_espg3857_bbox(bbox=bbox, add_margin=True)
103
+
104
+ qgis_layers = [("Overview_bbox", *espg3857_bbox)]
105
+ qgis_layers_with_margin = [("Overview_bbox_with_margin", *espg3857_bbox_with_margin)]
106
+
107
+ layers = qgis_layers + qgis_layers_with_margin
108
+
109
+ self.create_qgis_scripts(layers)