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,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)