maps4fs 0.9.93__py3-none-any.whl → 1.1.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.
@@ -4,6 +4,7 @@ around the map."""
4
4
  from __future__ import annotations
5
5
 
6
6
  import os
7
+ import shutil
7
8
 
8
9
  import cv2
9
10
  import numpy as np
@@ -14,167 +15,225 @@ from maps4fs.generator.dem import (
14
15
  DEFAULT_BLUR_RADIUS,
15
16
  DEFAULT_MULTIPLIER,
16
17
  DEFAULT_PLATEAU,
18
+ DEM,
17
19
  )
18
- from maps4fs.generator.path_steps import DEFAULT_DISTANCE, PATH_FULL_NAME, get_steps
19
- from maps4fs.generator.tile import Tile
20
- from maps4fs.logger import timeit
21
20
 
22
- RESIZE_FACTOR = 1 / 4
23
- SIMPLIFY_FACTOR = 10
24
- FULL_RESIZE_FACTOR = 1 / 8
25
- FULL_SIMPLIFY_FACTOR = 20
21
+ DEFAULT_DISTANCE = 2048
22
+ RESIZE_FACTOR = 1 / 8
23
+ FULL_NAME = "FULL"
24
+ FULL_PREVIEW_NAME = "PREVIEW"
25
+ ELEMENTS = [FULL_NAME, FULL_PREVIEW_NAME]
26
26
 
27
27
 
28
28
  class Background(Component):
29
29
  """Component for creating 3D obj files based on DEM data around the map.
30
30
 
31
- Args:
31
+ Arguments:
32
+ game (Game): The game instance for which the map is generated.
32
33
  coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
33
- map_height (int): The height of the map in pixels.
34
- map_width (int): The width of the map in pixels.
34
+ map_size (int): The size of the map in pixels (it's a square).
35
+ rotated_map_size (int): The size of the map in pixels after rotation.
36
+ rotation (int): The rotation angle of the map.
35
37
  map_directory (str): The directory where the map files are stored.
36
38
  logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
37
39
  info, warning. If not provided, default logging will be used.
38
40
  """
39
41
 
42
+ # pylint: disable=R0801
40
43
  def preprocess(self) -> None:
41
- """Prepares the component for processing. Registers the tiles around the map by moving
42
- clockwise from North, then clockwise."""
43
- self.tiles: list[Tile] = []
44
- origin = self.coordinates
45
-
46
- # Getting a list of 8 tiles around the map starting from the N(North) tile.
47
- for path_step in get_steps(self.map_height, self.map_width):
48
- # Getting the destination coordinates for the current tile.
49
- if path_step.angle is None:
50
- # For the case when generating the overview map, which has the same
51
- # center as the main map.
52
- tile_coordinates = self.coordinates
53
- else:
54
- tile_coordinates = path_step.get_destination(origin)
55
-
56
- # Create a Tile component, which is needed to save the DEM image.
57
- tile = Tile(
58
- game=self.game,
59
- coordinates=tile_coordinates,
60
- map_height=path_step.size[1],
61
- map_width=path_step.size[0],
62
- map_directory=self.map_directory,
63
- logger=self.logger,
64
- tile_code=path_step.code,
65
- auto_process=False,
44
+ """Registers the DEMs for the background terrain."""
45
+ self.light_version = self.kwargs.get("light_version", False)
46
+
47
+ if self.rotation:
48
+ self.logger.debug("Rotation is enabled: %s.", self.rotation)
49
+ output_size_multiplier = 1.5
50
+ else:
51
+ output_size_multiplier = 1
52
+
53
+ background_size = self.map_size + DEFAULT_DISTANCE * 2
54
+ rotated_size = int(background_size * output_size_multiplier)
55
+
56
+ self.background_directory = os.path.join(self.map_directory, "background")
57
+ os.makedirs(self.background_directory, exist_ok=True)
58
+
59
+ autoprocesses = [self.kwargs.get("auto_process", False), False]
60
+ dems = []
61
+
62
+ for name, autoprocess in zip(ELEMENTS, autoprocesses):
63
+ dem = DEM(
64
+ self.game,
65
+ self.coordinates,
66
+ background_size,
67
+ rotated_size,
68
+ self.rotation,
69
+ self.map_directory,
70
+ self.logger,
71
+ auto_process=autoprocess,
66
72
  blur_radius=self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS),
67
73
  multiplier=self.kwargs.get("multiplier", DEFAULT_MULTIPLIER),
68
74
  plateau=self.kwargs.get("plateau", DEFAULT_PLATEAU),
69
75
  )
76
+ dem.preprocess()
77
+ dem.is_preview = self.is_preview(name) # type: ignore
78
+ dem.set_output_resolution((rotated_size, rotated_size))
79
+ dem.set_dem_path(os.path.join(self.background_directory, f"{name}.png"))
80
+ dems.append(dem)
70
81
 
71
- # Update the origin for the next tile.
72
- origin = tile_coordinates
73
- self.tiles.append(tile)
74
- self.logger.debug(
75
- "Registered tile: %s, coordinates: %s, size: %s",
76
- tile.code,
77
- tile_coordinates,
78
- path_step.size,
79
- )
82
+ self.dems = dems
83
+
84
+ def is_preview(self, name: str) -> bool:
85
+ """Checks if the DEM is a preview.
86
+
87
+ Arguments:
88
+ name (str): The name of the DEM.
89
+
90
+ Returns:
91
+ bool: True if the DEM is a preview, False otherwise.
92
+ """
93
+ return name == FULL_PREVIEW_NAME
80
94
 
81
95
  def process(self) -> None:
82
96
  """Launches the component processing. Iterates over all tiles and processes them
83
97
  as a result the DEM files will be saved, then based on them the obj files will be
84
98
  generated."""
85
- for tile in self.tiles:
86
- tile.process()
99
+ for dem in self.dems:
100
+ dem.process()
101
+ if not dem.is_preview: # type: ignore
102
+ cutted_dem_path = self.cutout(dem.dem_path)
103
+ if self.game.additional_dem_name is not None:
104
+ self.make_copy(cutted_dem_path, self.game.additional_dem_name)
105
+
106
+ if not self.light_version:
107
+ self.generate_obj_files()
108
+ else:
109
+ self.logger.info("Light version is enabled, obj files will not be generated.")
110
+
111
+ def make_copy(self, dem_path: str, dem_name: str) -> None:
112
+ """Copies DEM data to additional DEM file.
87
113
 
88
- self.generate_obj_files()
114
+ Arguments:
115
+ dem_path (str): Path to the DEM file.
116
+ dem_name (str): Name of the additional DEM file.
117
+ """
118
+ dem_directory = os.path.dirname(dem_path)
89
119
 
90
- def info_sequence(self) -> dict[str, dict[str, str | float | int]]:
91
- """Returns a dictionary with information about the tiles around the map.
120
+ additional_dem_path = os.path.join(dem_directory, dem_name)
121
+
122
+ shutil.copyfile(dem_path, additional_dem_path)
123
+ self.logger.info("Additional DEM data was copied to %s.", additional_dem_path)
124
+
125
+ def info_sequence(self) -> dict[str, str | float | int]:
126
+ """Returns a dictionary with information about the background terrain.
92
127
  Adds the EPSG:3857 string to the data for convenient usage in QGIS.
93
128
 
94
129
  Returns:
95
- dict[str, dict[str, float | int]] -- A dictionary with information about the tiles.
130
+ dict[str, str, float | int] -- A dictionary with information about the background
131
+ terrain.
96
132
  """
97
- data = {}
98
133
  self.qgis_sequence()
99
134
 
100
- for tile in self.tiles:
101
- north, south, east, west = tile.bbox
102
- epsg3857_string = tile.get_epsg3857_string()
103
- epsg3857_string_with_margin = tile.get_epsg3857_string(add_margin=True)
104
-
105
- tile_entry = {
106
- "center_latitude": tile.coordinates[0],
107
- "center_longitude": tile.coordinates[1],
108
- "epsg3857_string": epsg3857_string,
109
- "epsg3857_string_with_margin": epsg3857_string_with_margin,
110
- "height": tile.map_height,
111
- "width": tile.map_width,
112
- "north": north,
113
- "south": south,
114
- "east": east,
115
- "west": west,
116
- }
117
- if tile.code is not None:
118
- data[tile.code] = tile_entry
119
-
135
+ dem = self.dems[0]
136
+
137
+ north, south, east, west = dem.bbox
138
+ epsg3857_string = dem.get_epsg3857_string()
139
+ epsg3857_string_with_margin = dem.get_epsg3857_string(add_margin=True)
140
+
141
+ data = {
142
+ "center_latitude": dem.coordinates[0],
143
+ "center_longitude": dem.coordinates[1],
144
+ "epsg3857_string": epsg3857_string,
145
+ "epsg3857_string_with_margin": epsg3857_string_with_margin,
146
+ "height": dem.map_size,
147
+ "width": dem.map_size,
148
+ "north": north,
149
+ "south": south,
150
+ "east": east,
151
+ "west": west,
152
+ }
120
153
  return data # type: ignore
121
154
 
122
155
  def qgis_sequence(self) -> None:
123
156
  """Generates QGIS scripts for creating bounding box layers and rasterizing them."""
124
- qgis_layers = [
125
- (f"Background_{tile.code}", *tile.get_espg3857_bbox()) for tile in self.tiles
126
- ]
127
- qgis_layers_with_margin = [
128
- (f"Background_{tile.code}_margin", *tile.get_espg3857_bbox(add_margin=True))
129
- for tile in self.tiles
130
- ]
131
-
132
- layers = qgis_layers + qgis_layers_with_margin
133
-
134
- self.create_qgis_scripts(layers)
157
+ qgis_layer = (f"Background_{FULL_NAME}", *self.dems[0].get_espg3857_bbox())
158
+ qgis_layer_with_margin = (
159
+ f"Background_{FULL_NAME}_margin",
160
+ *self.dems[0].get_espg3857_bbox(add_margin=True),
161
+ )
162
+ self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
135
163
 
136
164
  def generate_obj_files(self) -> None:
137
- """Iterates over all tiles and generates 3D obj files based on DEM data.
165
+ """Iterates over all dems and generates 3D obj files based on DEM data.
138
166
  If at least one DEM file is missing, the generation will be stopped at all.
139
167
  """
140
- for tile in self.tiles:
141
- # Read DEM data from the tile.
142
- dem_path = tile.dem_path
143
- if not os.path.isfile(dem_path):
144
- self.logger.warning("DEM file not found, generation will be stopped: %s", dem_path)
168
+ for dem in self.dems:
169
+ if not os.path.isfile(dem.dem_path):
170
+ self.logger.warning(
171
+ "DEM file not found, generation will be stopped: %s", dem.dem_path
172
+ )
145
173
  return
146
174
 
147
- self.logger.info("DEM file for tile %s found: %s", tile.code, dem_path)
175
+ self.logger.debug("DEM file for found: %s", dem.dem_path)
176
+
177
+ filename = os.path.splitext(os.path.basename(dem.dem_path))[0]
178
+ save_path = os.path.join(self.background_directory, f"{filename}.obj")
179
+ self.logger.debug("Generating obj file in path: %s", save_path)
180
+
181
+ dem_data = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
182
+ self.plane_from_np(dem_data, save_path, is_preview=dem.is_preview) # type: ignore
183
+
184
+ # pylint: disable=too-many-locals
185
+ def cutout(self, dem_path: str) -> str:
186
+ """Cuts out the center of the DEM (the actual map) and saves it as a separate file.
187
+
188
+ Arguments:
189
+ dem_path (str): The path to the DEM file.
190
+
191
+ Returns:
192
+ str -- The path to the cutout DEM file.
193
+ """
194
+ dem_data = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
195
+
196
+ center = (dem_data.shape[0] // 2, dem_data.shape[1] // 2)
197
+ half_size = self.map_size // 2
198
+ x1 = center[0] - half_size
199
+ x2 = center[0] + half_size
200
+ y1 = center[1] - half_size
201
+ y2 = center[1] + half_size
202
+ dem_data = dem_data[x1:x2, y1:y2]
203
+
204
+ output_size = self.map_size + 1
205
+
206
+ main_dem_path = self.game.dem_file_path(self.map_directory)
207
+
208
+ try:
209
+ os.remove(main_dem_path)
210
+ except FileNotFoundError:
211
+ pass
212
+
213
+ # pylint: disable=no-member
214
+ resized_dem_data = cv2.resize(
215
+ dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR
216
+ )
148
217
 
149
- base_directory = os.path.dirname(dem_path)
150
- save_path = os.path.join(base_directory, f"{tile.code}.obj")
151
- self.logger.debug("Generating obj file for tile %s in path: %s", tile.code, save_path)
218
+ cv2.imwrite(main_dem_path, resized_dem_data) # pylint: disable=no-member
219
+ self.logger.info("DEM cutout saved: %s", main_dem_path)
152
220
 
153
- dem_data = cv2.imread(tile.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
154
- self.plane_from_np(tile.code, dem_data, save_path)
221
+ return main_dem_path
155
222
 
156
223
  # pylint: disable=too-many-locals
157
- @timeit
158
- def plane_from_np(self, tile_code: str, dem_data: np.ndarray, save_path: str) -> None:
224
+ def plane_from_np(self, dem_data: np.ndarray, save_path: str, is_preview: bool = False) -> None:
159
225
  """Generates a 3D obj file based on DEM data.
160
226
 
161
227
  Arguments:
162
- tile_code (str) -- The code of the tile.
163
228
  dem_data (np.ndarray) -- The DEM data as a numpy array.
164
229
  save_path (str) -- The path where the obj file will be saved.
230
+ is_preview (bool, optional) -- If True, the preview mesh will be generated.
165
231
  """
166
- if tile_code == PATH_FULL_NAME:
167
- resize_factor = FULL_RESIZE_FACTOR
168
- simplify_factor = FULL_SIMPLIFY_FACTOR
169
- self.logger.info("Generating a full map obj file")
170
- else:
171
- resize_factor = RESIZE_FACTOR
172
- simplify_factor = SIMPLIFY_FACTOR
173
232
  dem_data = cv2.resize( # pylint: disable=no-member
174
- dem_data, (0, 0), fx=resize_factor, fy=resize_factor
233
+ dem_data, (0, 0), fx=RESIZE_FACTOR, fy=RESIZE_FACTOR
175
234
  )
176
235
  self.logger.debug(
177
- "DEM data resized to shape: %s with factor: %s", dem_data.shape, resize_factor
236
+ "DEM data resized to shape: %s with factor: %s", dem_data.shape, RESIZE_FACTOR
178
237
  )
179
238
 
180
239
  # Invert the height values.
@@ -186,10 +245,8 @@ class Background(Component):
186
245
  x, y = np.meshgrid(x, y)
187
246
  z = dem_data
188
247
 
189
- self.logger.info(
190
- "Starting to generate a mesh for tile %s with shape: %s x %s. "
191
- "This may take a while...",
192
- tile_code,
248
+ self.logger.debug(
249
+ "Starting to generate a mesh for with shape: %s x %s. This may take a while...",
193
250
  cols,
194
251
  rows,
195
252
  )
@@ -216,17 +273,24 @@ class Background(Component):
216
273
  mesh.apply_transform(rotation_matrix_y)
217
274
  mesh.apply_transform(rotation_matrix_z)
218
275
 
219
- self.logger.info("Mesh generated with %s faces, will be simplified", len(mesh.faces))
276
+ if is_preview:
277
+ # Simplify the preview mesh to reduce the size of the file.
278
+ mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
220
279
 
221
- # Simplify the mesh to reduce the number of faces.
222
- mesh = mesh.simplify_quadric_decimation(face_count=len(faces) // simplify_factor)
223
- self.logger.debug("Mesh simplified to %s faces", len(mesh.faces))
224
-
225
- if tile_code == PATH_FULL_NAME:
280
+ # Apply scale to make the preview mesh smaller in the UI.
281
+ mesh.apply_scale([0.5, 0.5, 0.5])
226
282
  self.mesh_to_stl(mesh)
283
+ else:
284
+ multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
285
+ if multiplier != 1:
286
+ z_scaling_factor = 1 / multiplier
287
+ else:
288
+ z_scaling_factor = 1 / 2**5
289
+ self.logger.debug("Z scaling factor: %s", z_scaling_factor)
290
+ mesh.apply_scale([1 / RESIZE_FACTOR, 1 / RESIZE_FACTOR, z_scaling_factor])
227
291
 
228
292
  mesh.export(save_path)
229
- self.logger.info("Obj file saved: %s", save_path)
293
+ self.logger.debug("Obj file saved: %s", save_path)
230
294
 
231
295
  def mesh_to_stl(self, mesh: trimesh.Trimesh) -> None:
232
296
  """Converts the mesh to an STL file and saves it in the previews directory.
@@ -237,126 +301,112 @@ class Background(Component):
237
301
  mesh (trimesh.Trimesh) -- The mesh to convert to an STL file.
238
302
  """
239
303
  preview_path = os.path.join(self.previews_directory, "background_dem.stl")
240
- mesh = mesh.simplify_quadric_decimation(percent=0.05)
241
304
  mesh.export(preview_path)
242
305
 
243
306
  self.logger.info("STL file saved: %s", preview_path)
244
307
 
245
308
  self.stl_preview_path = preview_path # pylint: disable=attribute-defined-outside-init
246
309
 
310
+ # pylint: disable=no-member
247
311
  def previews(self) -> list[str]:
248
- """Generates a preview by combining all tiles into one image.
249
- NOTE: The map itself is not included in the preview, so it will be empty.
312
+ """Returns the path to the image previews paths and the path to the STL preview file.
250
313
 
251
314
  Returns:
252
- list[str] -- A list of paths to the preview images."""
253
-
254
- self.logger.info("Generating a preview image for the background DEM")
255
-
256
- image_height = self.map_height + DEFAULT_DISTANCE * 2
257
- image_width = self.map_width + DEFAULT_DISTANCE * 2
258
- self.logger.debug("Full size of the preview image: %s x %s", image_width, image_height)
259
-
260
- image = np.zeros((image_height, image_width), np.uint16) # pylint: disable=no-member
261
- self.logger.debug("Empty image created: %s", image.shape)
262
-
263
- for tile in self.tiles:
264
- # pylint: disable=no-member
265
- if tile.code == PATH_FULL_NAME:
266
- continue
267
- tile_image = cv2.imread(tile.dem_path, cv2.IMREAD_UNCHANGED)
268
-
269
- self.logger.debug(
270
- "Tile %s image shape: %s, dtype: %s, max: %s, min: %s",
271
- tile.code,
272
- tile_image.shape,
273
- tile_image.dtype,
274
- tile_image.max(),
275
- tile_image.min(),
276
- )
315
+ list[str] -- A list of paths to the previews.
316
+ """
317
+ preview_paths = self.dem_previews(self.game.dem_file_path(self.map_directory))
318
+ for dem in self.dems:
319
+ if dem.is_preview: # type: ignore
320
+ background_dem_preview_path = os.path.join(
321
+ self.previews_directory, "background_dem.png"
322
+ )
323
+ background_dem_preview_image = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED)
324
+
325
+ background_dem_preview_image = cv2.resize(
326
+ background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
327
+ )
328
+ background_dem_preview_image = cv2.normalize( # type: ignore
329
+ background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
330
+ )
331
+ background_dem_preview_image = cv2.cvtColor(
332
+ background_dem_preview_image, cv2.COLOR_GRAY2BGR
333
+ )
334
+
335
+ cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
336
+ preview_paths.append(background_dem_preview_path)
337
+
338
+ preview_paths.append(self.stl_preview_path)
339
+
340
+ return preview_paths
341
+
342
+ def dem_previews(self, image_path: str) -> list[str]:
343
+ """Get list of preview images.
277
344
 
278
- tile_height, tile_width = tile_image.shape
279
- self.logger.debug("Tile %s size: %s x %s", tile.code, tile_width, tile_height)
280
-
281
- # Calculate the position based on the tile code
282
- if tile.code == "N":
283
- x = DEFAULT_DISTANCE
284
- y = 0
285
- elif tile.code == "NE":
286
- x = self.map_width + DEFAULT_DISTANCE
287
- y = 0
288
- elif tile.code == "E":
289
- x = self.map_width + DEFAULT_DISTANCE
290
- y = DEFAULT_DISTANCE
291
- elif tile.code == "SE":
292
- x = self.map_width + DEFAULT_DISTANCE
293
- y = self.map_height + DEFAULT_DISTANCE
294
- elif tile.code == "S":
295
- x = DEFAULT_DISTANCE
296
- y = self.map_height + DEFAULT_DISTANCE
297
- elif tile.code == "SW":
298
- x = 0
299
- y = self.map_height + DEFAULT_DISTANCE
300
- elif tile.code == "W":
301
- x = 0
302
- y = DEFAULT_DISTANCE
303
- elif tile.code == "NW":
304
- x = 0
305
- y = 0
306
-
307
- # pylint: disable=possibly-used-before-assignment
308
- x2 = x + tile_width
309
- y2 = y + tile_height
310
-
311
- self.logger.debug(
312
- "Tile %s position. X from %s to %s, Y from %s to %s", tile.code, x, x2, y, y2
313
- )
345
+ Arguments:
346
+ image_path (str): Path to the DEM file.
314
347
 
315
- # pylint: disable=possibly-used-before-assignment
316
- image[y:y2, x:x2] = tile_image
348
+ Returns:
349
+ list[str]: List of preview images.
350
+ """
351
+ self.logger.debug("Starting DEM previews generation.")
352
+ return [self.grayscale_preview(image_path), self.colored_preview(image_path)]
317
353
 
318
- # Save image to the map directory.
319
- preview_path = os.path.join(self.previews_directory, "background_dem.png")
354
+ def grayscale_preview(self, image_path: str) -> str:
355
+ """Converts DEM image to grayscale RGB image and saves it to the map directory.
356
+ Returns path to the preview image.
320
357
 
321
- # pylint: disable=no-member
322
- image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U) # type: ignore
323
- image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # type: ignore
324
- cv2.imwrite(preview_path, image)
325
-
326
- return [preview_path, self.stl_preview_path]
327
-
328
-
329
- # Creates tiles around the map.
330
- # The one on corners 2048x2048, on sides and in the middle map_size x 2048.
331
- # So 2048 is a distance FROM the edge of the map, but the other size depends on the map size.
332
- # But for corner tiles it's always 2048.
333
-
334
- # In the beginning we have coordinates of the central point of the map and it's size.
335
- # We need to calculate the coordinates of central points all 8 tiles around the map.
336
-
337
- # Latitude is a vertical line, Longitude is a horizontal line.
338
-
339
- # 2048
340
- # | |
341
- # ____________________|_________|___
342
- # | | | |
343
- # | NW | N | NE | 2048
344
- # |_________|_________|_________|___
345
- # | | | |
346
- # | W | C | E |
347
- # |_________|_________|_________|
348
- # | | | |
349
- # | SW | S | SE |
350
- # |_________|_________|_________|
351
- #
352
- # N = C map_height / 2 + 1024; N_width = map_width; N_height = 2048
353
- # NW = N - map_width / 2 - 1024; NW_width = 2048; NW_height = 2048
354
- # and so on...
355
-
356
- # lat, lon = 45.28565000315636, 20.237121355049904
357
- # dst = 1024
358
-
359
- # # N
360
- # destination = distance(meters=dst).destination((lat, lon), 0)
361
- # lat, lon = destination.latitude, destination.longitude
362
- # print(lat, lon)
358
+ Arguments:
359
+ image_path (str): Path to the DEM file.
360
+
361
+ Returns:
362
+ str: Path to the preview image.
363
+ """
364
+ grayscale_dem_path = os.path.join(self.previews_directory, "dem_grayscale.png")
365
+
366
+ self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
367
+
368
+ dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
369
+ dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
370
+ cv2.imwrite(grayscale_dem_path, dem_data_rgb)
371
+ return grayscale_dem_path
372
+
373
+ def colored_preview(self, image_path: str) -> str:
374
+ """Converts DEM image to colored RGB image and saves it to the map directory.
375
+ Returns path to the preview image.
376
+
377
+ Arguments:
378
+ image_path (str): Path to the DEM file.
379
+
380
+ Returns:
381
+ list[str]: List with a single path to the DEM file
382
+ """
383
+ colored_dem_path = os.path.join(self.previews_directory, "dem_colored.png")
384
+
385
+ self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
386
+
387
+ dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
388
+
389
+ self.logger.debug(
390
+ "DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
391
+ dem_data.shape,
392
+ dem_data.dtype,
393
+ dem_data.min(),
394
+ dem_data.max(),
395
+ )
396
+
397
+ # Create an empty array with the same shape and type as dem_data.
398
+ dem_data_normalized = np.empty_like(dem_data)
399
+
400
+ # Normalize the DEM data to the range [0, 255]
401
+ cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
402
+ self.logger.debug(
403
+ "DEM data after normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
404
+ dem_data_normalized.shape,
405
+ dem_data_normalized.dtype,
406
+ dem_data_normalized.min(),
407
+ dem_data_normalized.max(),
408
+ )
409
+ dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
410
+
411
+ cv2.imwrite(colored_dem_path, dem_data_colored)
412
+ return colored_dem_path