maps4fs 0.7.8__py3-none-any.whl → 0.9.8__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/generator/background.py +362 -0
- maps4fs/generator/component.py +176 -4
- maps4fs/generator/config.py +52 -2
- maps4fs/generator/dem.py +146 -35
- maps4fs/generator/game.py +27 -4
- maps4fs/generator/i3d.py +89 -0
- maps4fs/generator/map.py +43 -28
- maps4fs/generator/path_steps.py +83 -0
- maps4fs/generator/qgis.py +196 -0
- maps4fs/generator/texture.py +88 -45
- maps4fs/generator/tile.py +55 -0
- maps4fs/logger.py +27 -3
- maps4fs/toolbox/__init__.py +1 -0
- maps4fs/toolbox/dem.py +112 -0
- maps4fs-0.9.8.dist-info/METADATA +450 -0
- maps4fs-0.9.8.dist-info/RECORD +21 -0
- maps4fs-0.7.8.dist-info/METADATA +0 -218
- maps4fs-0.7.8.dist-info/RECORD +0 -14
- {maps4fs-0.7.8.dist-info → maps4fs-0.9.8.dist-info}/LICENSE.md +0 -0
- {maps4fs-0.7.8.dist-info → maps4fs-0.9.8.dist-info}/WHEEL +0 -0
- {maps4fs-0.7.8.dist-info → maps4fs-0.9.8.dist-info}/top_level.txt +0 -0
@@ -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
|
+
Args:
|
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
|
+
Args:
|
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
|
+
Args:
|
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
|
+
Args:
|
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
|
+
Args:
|
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)
|
maps4fs/generator/texture.py
CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
import json
|
6
6
|
import os
|
7
7
|
import warnings
|
8
|
-
from typing import Callable, Generator, Optional
|
8
|
+
from typing import Any, Callable, Generator, Optional
|
9
9
|
|
10
10
|
import cv2
|
11
11
|
import numpy as np
|
@@ -57,6 +57,7 @@ class Texture(Component):
|
|
57
57
|
width: int | None = None,
|
58
58
|
color: tuple[int, int, int] | list[int] | None = None,
|
59
59
|
exclude_weight: bool = False,
|
60
|
+
priority: int | None = None,
|
60
61
|
):
|
61
62
|
self.name = name
|
62
63
|
self.count = count
|
@@ -64,6 +65,7 @@ class Texture(Component):
|
|
64
65
|
self.width = width
|
65
66
|
self.color = color if color else (255, 255, 255)
|
66
67
|
self.exclude_weight = exclude_weight
|
68
|
+
self.priority = priority
|
67
69
|
|
68
70
|
def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
|
69
71
|
"""Returns dictionary with layer data.
|
@@ -77,6 +79,7 @@ class Texture(Component):
|
|
77
79
|
"width": self.width,
|
78
80
|
"color": list(self.color),
|
79
81
|
"exclude_weight": self.exclude_weight,
|
82
|
+
"priority": self.priority,
|
80
83
|
}
|
81
84
|
|
82
85
|
data = {k: v for k, v in data.items() if v is not None}
|
@@ -120,16 +123,32 @@ class Texture(Component):
|
|
120
123
|
self.layers = [self.Layer.from_json(layer) for layer in layers_schema]
|
121
124
|
self.logger.info("Loaded %s layers.", len(self.layers))
|
122
125
|
|
126
|
+
base_layer = self.get_base_layer()
|
127
|
+
if base_layer:
|
128
|
+
self.logger.info("Base layer found: %s.", base_layer.name)
|
129
|
+
else:
|
130
|
+
self.logger.warning("No base layer found.")
|
131
|
+
|
123
132
|
self._weights_dir = self.game.weights_dir_path(self.map_directory)
|
124
133
|
self.logger.debug("Weights directory: %s.", self._weights_dir)
|
125
134
|
self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
|
126
135
|
self.logger.debug("Generation info save path: %s.", self.info_save_path)
|
127
136
|
|
137
|
+
def get_base_layer(self) -> Layer | None:
|
138
|
+
"""Returns base layer.
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
Layer | None: Base layer.
|
142
|
+
"""
|
143
|
+
for layer in self.layers:
|
144
|
+
if layer.priority == 0:
|
145
|
+
return layer
|
146
|
+
return None
|
147
|
+
|
128
148
|
def process(self):
|
129
149
|
self._prepare_weights()
|
130
150
|
self._read_parameters()
|
131
151
|
self.draw()
|
132
|
-
self.info_sequence()
|
133
152
|
|
134
153
|
# pylint: disable=W0201
|
135
154
|
def _read_parameters(self) -> None:
|
@@ -148,31 +167,8 @@ class Texture(Component):
|
|
148
167
|
self.logger.debug("Map minimum coordinates (XxY): %s x %s.", self.minimum_x, self.minimum_y)
|
149
168
|
self.logger.debug("Map maximum coordinates (XxY): %s x %s.", self.maximum_x, self.maximum_y)
|
150
169
|
|
151
|
-
|
152
|
-
|
153
|
-
self.logger.info("Map dimensions (HxW): %s x %s.", self.height, self.width)
|
154
|
-
|
155
|
-
self.height_coef = self.height / self.map_height
|
156
|
-
self.width_coef = self.width / self.map_width
|
157
|
-
self.logger.debug("Map coefficients (HxW): %s x %s.", self.height_coef, self.width_coef)
|
158
|
-
|
159
|
-
def info_sequence(self) -> None:
|
160
|
-
"""Saves generation info to JSON file "generation_info.json".
|
161
|
-
|
162
|
-
Info sequence contains following attributes:
|
163
|
-
- coordinates
|
164
|
-
- bbox
|
165
|
-
- map_height
|
166
|
-
- map_width
|
167
|
-
- minimum_x
|
168
|
-
- minimum_y
|
169
|
-
- maximum_x
|
170
|
-
- maximum_y
|
171
|
-
- height
|
172
|
-
- width
|
173
|
-
- height_coef
|
174
|
-
- width_coef
|
175
|
-
"""
|
170
|
+
def info_sequence(self) -> dict[str, Any]:
|
171
|
+
"""Returns the JSON representation of the generation info for textures."""
|
176
172
|
useful_attributes = [
|
177
173
|
"coordinates",
|
178
174
|
"bbox",
|
@@ -182,16 +178,8 @@ class Texture(Component):
|
|
182
178
|
"minimum_y",
|
183
179
|
"maximum_x",
|
184
180
|
"maximum_y",
|
185
|
-
"height",
|
186
|
-
"width",
|
187
|
-
"height_coef",
|
188
|
-
"width_coef",
|
189
181
|
]
|
190
|
-
|
191
|
-
|
192
|
-
with open(self.info_save_path, "w") as f: # pylint: disable=W1514
|
193
|
-
json.dump(info_sequence, f, indent=4)
|
194
|
-
self.logger.info("Generation info saved to %s.", self.info_save_path)
|
182
|
+
return {attr: getattr(self, attr, None) for attr in useful_attributes}
|
195
183
|
|
196
184
|
def _prepare_weights(self):
|
197
185
|
self.logger.debug("Starting preparing weights from %s layers.", len(self.layers))
|
@@ -239,22 +227,79 @@ class Texture(Component):
|
|
239
227
|
"""
|
240
228
|
self._layers = layers
|
241
229
|
|
230
|
+
def layers_by_priority(self) -> list[Layer]:
|
231
|
+
"""Returns list of layers sorted by priority: None priority layers are first,
|
232
|
+
then layers are sorted by priority (descending).
|
233
|
+
|
234
|
+
Returns:
|
235
|
+
list[Layer]: List of layers sorted by priority.
|
236
|
+
"""
|
237
|
+
return sorted(
|
238
|
+
self.layers,
|
239
|
+
key=lambda _layer: (
|
240
|
+
_layer.priority is not None,
|
241
|
+
-_layer.priority if _layer.priority is not None else float("inf"),
|
242
|
+
),
|
243
|
+
)
|
244
|
+
|
242
245
|
# pylint: disable=no-member
|
243
246
|
def draw(self) -> None:
|
244
247
|
"""Iterates over layers and fills them with polygons from OSM data."""
|
245
|
-
|
248
|
+
layers = self.layers_by_priority()
|
249
|
+
|
250
|
+
self.logger.debug(
|
251
|
+
"Sorted layers by priority: %s.", [(layer.name, layer.priority) for layer in layers]
|
252
|
+
)
|
253
|
+
|
254
|
+
cumulative_image = None
|
255
|
+
|
256
|
+
for layer in layers:
|
246
257
|
if not layer.tags:
|
247
258
|
self.logger.debug("Layer %s has no tags, there's nothing to draw.", layer.name)
|
248
259
|
continue
|
260
|
+
if layer.priority == 0:
|
261
|
+
self.logger.debug(
|
262
|
+
"Found base layer %s. Postponing that to be the last layer drawn.", layer.name
|
263
|
+
)
|
264
|
+
continue
|
249
265
|
layer_path = layer.path(self._weights_dir)
|
250
266
|
self.logger.debug("Drawing layer %s.", layer_path)
|
267
|
+
layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
|
268
|
+
|
269
|
+
if cumulative_image is None:
|
270
|
+
self.logger.debug("First layer, creating new cumulative image.")
|
271
|
+
cumulative_image = layer_image
|
272
|
+
|
273
|
+
mask = cv2.bitwise_not(cumulative_image)
|
251
274
|
|
252
|
-
img = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
|
253
275
|
for polygon in self.polygons(layer.tags, layer.width): # type: ignore
|
254
|
-
cv2.fillPoly(
|
255
|
-
|
276
|
+
cv2.fillPoly(layer_image, [polygon], color=255) # type: ignore
|
277
|
+
|
278
|
+
output_image = cv2.bitwise_and(layer_image, mask)
|
279
|
+
|
280
|
+
cumulative_image = cv2.bitwise_or(cumulative_image, output_image)
|
281
|
+
|
282
|
+
cv2.imwrite(layer_path, output_image)
|
256
283
|
self.logger.debug("Texture %s saved.", layer_path)
|
257
284
|
|
285
|
+
if cumulative_image is not None:
|
286
|
+
self.draw_base_layer(cumulative_image)
|
287
|
+
|
288
|
+
def draw_base_layer(self, cumulative_image: np.ndarray) -> None:
|
289
|
+
"""Draws base layer and saves it into the png file.
|
290
|
+
Base layer is the last layer to be drawn, it fills the remaining area of the map.
|
291
|
+
|
292
|
+
Args:
|
293
|
+
cumulative_image (np.ndarray): Cumulative image with all layers.
|
294
|
+
"""
|
295
|
+
base_layer = self.get_base_layer()
|
296
|
+
if base_layer is not None:
|
297
|
+
layer_path = base_layer.path(self._weights_dir)
|
298
|
+
self.logger.debug("Drawing base layer %s.", layer_path)
|
299
|
+
img = cv2.bitwise_not(cumulative_image)
|
300
|
+
cv2.imwrite(layer_path, img)
|
301
|
+
self.logger.debug("Base texture %s saved.", layer_path)
|
302
|
+
|
258
303
|
def get_relative_x(self, x: float) -> int:
|
259
304
|
"""Converts UTM X coordinate to relative X coordinate in map image.
|
260
305
|
|
@@ -264,8 +309,7 @@ class Texture(Component):
|
|
264
309
|
Returns:
|
265
310
|
int: Relative X coordinate in map image.
|
266
311
|
"""
|
267
|
-
|
268
|
-
return int(raw_x * self.height_coef)
|
312
|
+
return int(self.map_width * (x - self.minimum_x) / (self.maximum_x - self.minimum_x))
|
269
313
|
|
270
314
|
def get_relative_y(self, y: float) -> int:
|
271
315
|
"""Converts UTM Y coordinate to relative Y coordinate in map image.
|
@@ -276,8 +320,7 @@ class Texture(Component):
|
|
276
320
|
Returns:
|
277
321
|
int: Relative Y coordinate in map image.
|
278
322
|
"""
|
279
|
-
|
280
|
-
return self.height - int(raw_y * self.width_coef)
|
323
|
+
return int(self.map_height * (1 - (y - self.minimum_y) / (self.maximum_y - self.minimum_y)))
|
281
324
|
|
282
325
|
# pylint: disable=W0613
|
283
326
|
def _to_np(self, geometry: shapely.geometry.polygon.Polygon, *args) -> np.ndarray:
|
@@ -428,7 +471,7 @@ class Texture(Component):
|
|
428
471
|
merged.shape,
|
429
472
|
merged.dtype,
|
430
473
|
)
|
431
|
-
preview_path = os.path.join(self.
|
474
|
+
preview_path = os.path.join(self.previews_directory, "textures_osm.png")
|
432
475
|
cv2.imwrite(preview_path, merged) # pylint: disable=no-member
|
433
476
|
self.logger.info("Preview saved to %s.", preview_path)
|
434
477
|
return preview_path
|
@@ -0,0 +1,55 @@
|
|
1
|
+
"""This module contains the Tile component, which is used to generate a tile of DEM data around
|
2
|
+
the map."""
|
3
|
+
|
4
|
+
import os
|
5
|
+
|
6
|
+
from maps4fs.generator.dem import DEM
|
7
|
+
|
8
|
+
|
9
|
+
class Tile(DEM):
|
10
|
+
"""Component for creating a tile of DEM data around the map.
|
11
|
+
|
12
|
+
Arguments:
|
13
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
14
|
+
map_height (int): The height of the map in pixels.
|
15
|
+
map_width (int): The width of the map in pixels.
|
16
|
+
map_directory (str): The directory where the map files are stored.
|
17
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
18
|
+
info, warning. If not provided, default logging will be used.
|
19
|
+
|
20
|
+
Keyword Arguments:
|
21
|
+
tile_code (str): The code of the tile (N, NE, E, SE, S, SW, W, NW).
|
22
|
+
|
23
|
+
Public Methods:
|
24
|
+
get_output_resolution: Return the resolution of the output image.
|
25
|
+
process: Launch the component processing.
|
26
|
+
make_copy: Override the method to prevent copying the tile.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def preprocess(self) -> None:
|
30
|
+
"""Prepares the component for processing. Reads the tile code from the kwargs and sets the
|
31
|
+
DEM path for the tile."""
|
32
|
+
super().preprocess()
|
33
|
+
self.code = self.kwargs.get("tile_code")
|
34
|
+
if not self.code:
|
35
|
+
raise ValueError("Tile code was not provided")
|
36
|
+
|
37
|
+
self.logger.debug(f"Generating tile {self.code}")
|
38
|
+
|
39
|
+
tiles_directory = os.path.join(self.map_directory, "objects", "tiles")
|
40
|
+
os.makedirs(tiles_directory, exist_ok=True)
|
41
|
+
|
42
|
+
self._dem_path = os.path.join(tiles_directory, f"{self.code}.png")
|
43
|
+
self.logger.debug(f"DEM path for tile {self.code} is {self._dem_path}")
|
44
|
+
|
45
|
+
def get_output_resolution(self) -> tuple[int, int]:
|
46
|
+
"""Return the resolution of the output image.
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
tuple[int, int]: The width and height of the output image.
|
50
|
+
"""
|
51
|
+
return self.map_width, self.map_height
|
52
|
+
|
53
|
+
def make_copy(self, *args, **kwargs) -> None:
|
54
|
+
"""Override the method to prevent copying the tile."""
|
55
|
+
pass # pylint: disable=unnecessary-pass
|
maps4fs/logger.py
CHANGED
@@ -4,8 +4,11 @@ import logging
|
|
4
4
|
import os
|
5
5
|
import sys
|
6
6
|
from datetime import datetime
|
7
|
-
from
|
7
|
+
from logging import getLogger
|
8
|
+
from time import perf_counter
|
9
|
+
from typing import Any, Callable, Literal
|
8
10
|
|
11
|
+
LOGGER_NAME = "maps4fs"
|
9
12
|
log_directory = os.path.join(os.getcwd(), "logs")
|
10
13
|
os.makedirs(log_directory, exist_ok=True)
|
11
14
|
|
@@ -15,12 +18,11 @@ class Logger(logging.Logger):
|
|
15
18
|
|
16
19
|
def __init__(
|
17
20
|
self,
|
18
|
-
name: str,
|
19
21
|
level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "ERROR",
|
20
22
|
to_stdout: bool = True,
|
21
23
|
to_file: bool = True,
|
22
24
|
):
|
23
|
-
super().__init__(
|
25
|
+
super().__init__(LOGGER_NAME)
|
24
26
|
self.setLevel(level)
|
25
27
|
self.stdout_handler = logging.StreamHandler(sys.stdout)
|
26
28
|
self.file_handler = logging.FileHandler(
|
@@ -44,3 +46,25 @@ class Logger(logging.Logger):
|
|
44
46
|
today = datetime.now().strftime("%Y-%m-%d")
|
45
47
|
log_file = os.path.join(log_directory, f"{today}.txt")
|
46
48
|
return log_file
|
49
|
+
|
50
|
+
|
51
|
+
def timeit(func: Callable[..., Any]) -> Callable[..., Any]:
|
52
|
+
"""Decorator to log the time taken by a function to execute.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
func (function): The function to be timed.
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
function: The timed function.
|
59
|
+
"""
|
60
|
+
|
61
|
+
def timed(*args, **kwargs):
|
62
|
+
logger = getLogger("maps4fs")
|
63
|
+
start = perf_counter()
|
64
|
+
result = func(*args, **kwargs)
|
65
|
+
end = perf_counter()
|
66
|
+
if logger is not None:
|
67
|
+
logger.info("Function %s took %s seconds to execute", func.__name__, end - start)
|
68
|
+
return result
|
69
|
+
|
70
|
+
return timed
|
@@ -0,0 +1 @@
|
|
1
|
+
# pylint: disable=missing-module-docstring
|
maps4fs/toolbox/dem.py
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
"""This module contains functions for working with Digital Elevation Models (DEMs)."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
|
5
|
+
import rasterio # type: ignore
|
6
|
+
from pyproj import Transformer
|
7
|
+
from rasterio.io import DatasetReader # type: ignore
|
8
|
+
from rasterio.windows import from_bounds # type: ignore
|
9
|
+
|
10
|
+
|
11
|
+
def read_geo_tiff(file_path: str) -> DatasetReader:
|
12
|
+
"""Read a GeoTIFF file and return the DatasetReader object.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
file_path (str): The path to the GeoTIFF file.
|
16
|
+
|
17
|
+
Raises:
|
18
|
+
FileNotFoundError: If the file is not found.
|
19
|
+
RuntimeError: If there is an error reading the file.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
DatasetReader: The DatasetReader object for the GeoTIFF file.
|
23
|
+
"""
|
24
|
+
if not os.path.isfile(file_path):
|
25
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
26
|
+
|
27
|
+
try:
|
28
|
+
src = rasterio.open(file_path)
|
29
|
+
except Exception as e:
|
30
|
+
raise RuntimeError(f"Error reading file: {file_path}") from e
|
31
|
+
|
32
|
+
if not src.bounds or not src.crs:
|
33
|
+
raise RuntimeError(
|
34
|
+
f"Can not read bounds or CRS from file: {file_path}. "
|
35
|
+
f"Bounds: {src.bounds}, CRS: {src.crs}"
|
36
|
+
)
|
37
|
+
|
38
|
+
return src
|
39
|
+
|
40
|
+
|
41
|
+
def get_geo_tiff_bbox(
|
42
|
+
src: DatasetReader, dst_crs: str | None = "EPSG:4326"
|
43
|
+
) -> tuple[float, float, float, float]:
|
44
|
+
"""Return the bounding box of a GeoTIFF file in the destination CRS.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
src (DatasetReader): The DatasetReader object for the GeoTIFF file.
|
48
|
+
dst_crs (str, optional): The destination CRS. Defaults to "EPSG:4326".
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
tuple[float, float, float, float]: The bounding box in the destination CRS
|
52
|
+
as (north, south, east, west).
|
53
|
+
"""
|
54
|
+
left, bottom, right, top = src.bounds
|
55
|
+
|
56
|
+
transformer = Transformer.from_crs(src.crs, dst_crs, always_xy=True)
|
57
|
+
|
58
|
+
east, north = transformer.transform(left, top)
|
59
|
+
west, south = transformer.transform(right, bottom)
|
60
|
+
|
61
|
+
return north, south, east, west
|
62
|
+
|
63
|
+
|
64
|
+
# pylint: disable=R0914
|
65
|
+
def extract_roi(file_path: str, bbox: tuple[float, float, float, float]) -> str:
|
66
|
+
"""Extract a region of interest (ROI) from a GeoTIFF file and save it as a new file.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
file_path (str): The path to the GeoTIFF file.
|
70
|
+
bbox (tuple[float, float, float, float]): The bounding box of the region of interest
|
71
|
+
as (north, south, east, west).
|
72
|
+
|
73
|
+
Raises:
|
74
|
+
RuntimeError: If there is no data in the selected region.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
str: The path to the new GeoTIFF file containing the extracted ROI.
|
78
|
+
"""
|
79
|
+
with rasterio.open(file_path) as src:
|
80
|
+
transformer = Transformer.from_crs("EPSG:4326", src.crs, always_xy=True)
|
81
|
+
north, south, east, west = bbox
|
82
|
+
|
83
|
+
left, bottom = transformer.transform(west, south)
|
84
|
+
right, top = transformer.transform(east, north)
|
85
|
+
|
86
|
+
window = from_bounds(left, bottom, right, top, src.transform)
|
87
|
+
data = src.read(window=window)
|
88
|
+
|
89
|
+
if not data.size > 0:
|
90
|
+
raise RuntimeError("No data in the selected region.")
|
91
|
+
|
92
|
+
base_name = os.path.basename(file_path).split(".")[0]
|
93
|
+
dir_name = os.path.dirname(file_path)
|
94
|
+
|
95
|
+
output_name = f"{base_name}_{north}_{south}_{east}_{west}.tif"
|
96
|
+
|
97
|
+
output_path = os.path.join(dir_name, output_name)
|
98
|
+
|
99
|
+
with rasterio.open(
|
100
|
+
output_path,
|
101
|
+
"w",
|
102
|
+
driver="GTiff",
|
103
|
+
height=data.shape[1],
|
104
|
+
width=data.shape[2],
|
105
|
+
count=data.shape[0],
|
106
|
+
dtype=data.dtype,
|
107
|
+
crs=src.crs,
|
108
|
+
transform=src.window_transform(window),
|
109
|
+
) as dst:
|
110
|
+
dst.write(data)
|
111
|
+
|
112
|
+
return output_path
|