maps4fs 0.7.8__py3-none-any.whl → 0.9.93__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,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)
@@ -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
- self.height = abs(north - south)
152
- self.width = abs(east - west)
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
- info_sequence = {attr: getattr(self, attr, None) for attr in useful_attributes}
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
- for layer in self.layers:
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(img, [polygon], color=255) # type: ignore
255
- cv2.imwrite(layer_path, img)
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
- raw_x = x - self.minimum_x
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
- raw_y = y - self.minimum_y
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.map_directory, "preview_osm.png")
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 typing import Literal
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__(name)
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