maps4fs 1.0.9__py3-none-any.whl → 1.1.1__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.
@@ -15,87 +15,92 @@ from maps4fs.generator.dem import (
15
15
  DEFAULT_BLUR_RADIUS,
16
16
  DEFAULT_MULTIPLIER,
17
17
  DEFAULT_PLATEAU,
18
+ DEM,
18
19
  )
19
- from maps4fs.generator.path_steps import (
20
- PATH_FULL_NAME,
21
- PATH_FULL_PREVIEW_NAME,
22
- get_steps,
23
- )
24
- from maps4fs.generator.tile import Tile
25
20
 
26
- RESIZE_FACTOR = 1 / 4
27
- FULL_RESIZE_FACTOR = 1 / 8
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]
28
26
 
29
27
 
30
28
  class Background(Component):
31
29
  """Component for creating 3D obj files based on DEM data around the map.
32
30
 
33
31
  Arguments:
32
+ game (Game): The game instance for which the map is generated.
34
33
  coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
35
- map_height (int): The height of the map in pixels.
36
- 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.
37
37
  map_directory (str): The directory where the map files are stored.
38
38
  logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
39
39
  info, warning. If not provided, default logging will be used.
40
40
  """
41
41
 
42
+ # pylint: disable=R0801
42
43
  def preprocess(self) -> None:
43
- """Prepares the component for processing. Registers the tiles around the map by moving
44
- clockwise from North, then clockwise."""
45
- self.tiles: list[Tile] = []
46
- origin = self.coordinates
47
-
48
- only_full_tiles = self.kwargs.get("only_full_tiles", True)
44
+ """Registers the DEMs for the background terrain."""
49
45
  self.light_version = self.kwargs.get("light_version", False)
46
+ self.stl_preview_path: str | None = None
50
47
 
51
- # Getting a list of 8 tiles around the map starting from the N(North) tile.
52
- for path_step in get_steps(self.map_height, self.map_width, only_full_tiles):
53
- # Getting the destination coordinates for the current tile.
54
- if path_step.angle is None:
55
- # For the case when generating the overview map, which has the same
56
- # center as the main map.
57
- tile_coordinates = self.coordinates
58
- else:
59
- tile_coordinates = path_step.get_destination(origin)
60
-
61
- if path_step.code == PATH_FULL_PREVIEW_NAME:
62
- auto_process = False
63
- else:
64
- auto_process = self.kwargs.get("auto_process", False)
65
-
66
- # Create a Tile component, which is needed to save the DEM image.
67
- tile = Tile(
68
- game=self.game,
69
- coordinates=tile_coordinates,
70
- map_height=path_step.size[1],
71
- map_width=path_step.size[0],
72
- map_directory=self.map_directory,
73
- logger=self.logger,
74
- tile_code=path_step.code,
75
- auto_process=auto_process,
48
+ if self.rotation:
49
+ self.logger.debug("Rotation is enabled: %s.", self.rotation)
50
+ output_size_multiplier = 1.5
51
+ else:
52
+ output_size_multiplier = 1
53
+
54
+ background_size = self.map_size + DEFAULT_DISTANCE * 2
55
+ rotated_size = int(background_size * output_size_multiplier)
56
+
57
+ self.background_directory = os.path.join(self.map_directory, "background")
58
+ os.makedirs(self.background_directory, exist_ok=True)
59
+
60
+ autoprocesses = [self.kwargs.get("auto_process", False), False]
61
+ dems = []
62
+
63
+ for name, autoprocess in zip(ELEMENTS, autoprocesses):
64
+ dem = DEM(
65
+ self.game,
66
+ self.coordinates,
67
+ background_size,
68
+ rotated_size,
69
+ self.rotation,
70
+ self.map_directory,
71
+ self.logger,
72
+ auto_process=autoprocess,
76
73
  blur_radius=self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS),
77
74
  multiplier=self.kwargs.get("multiplier", DEFAULT_MULTIPLIER),
78
75
  plateau=self.kwargs.get("plateau", DEFAULT_PLATEAU),
79
76
  )
77
+ dem.preprocess()
78
+ dem.is_preview = self.is_preview(name) # type: ignore
79
+ dem.set_output_resolution((rotated_size, rotated_size))
80
+ dem.set_dem_path(os.path.join(self.background_directory, f"{name}.png"))
81
+ dems.append(dem)
80
82
 
81
- # Update the origin for the next tile.
82
- origin = tile_coordinates
83
- self.tiles.append(tile)
84
- self.logger.debug(
85
- "Registered tile: %s, coordinates: %s, size: %s",
86
- tile.code,
87
- tile_coordinates,
88
- path_step.size,
89
- )
83
+ self.dems = dems
84
+
85
+ def is_preview(self, name: str) -> bool:
86
+ """Checks if the DEM is a preview.
87
+
88
+ Arguments:
89
+ name (str): The name of the DEM.
90
+
91
+ Returns:
92
+ bool: True if the DEM is a preview, False otherwise.
93
+ """
94
+ return name == FULL_PREVIEW_NAME
90
95
 
91
96
  def process(self) -> None:
92
97
  """Launches the component processing. Iterates over all tiles and processes them
93
98
  as a result the DEM files will be saved, then based on them the obj files will be
94
99
  generated."""
95
- for tile in self.tiles:
96
- tile.process()
97
- if tile.code == PATH_FULL_NAME:
98
- cutted_dem_path = self.cutout(tile.dem_path)
100
+ for dem in self.dems:
101
+ dem.process()
102
+ if not dem.is_preview: # type: ignore
103
+ cutted_dem_path = self.cutout(dem.dem_path)
99
104
  if self.game.additional_dem_name is not None:
100
105
  self.make_copy(cutted_dem_path, self.game.additional_dem_name)
101
106
 
@@ -118,71 +123,64 @@ class Background(Component):
118
123
  shutil.copyfile(dem_path, additional_dem_path)
119
124
  self.logger.info("Additional DEM data was copied to %s.", additional_dem_path)
120
125
 
121
- def info_sequence(self) -> dict[str, dict[str, str | float | int]]:
122
- """Returns a dictionary with information about the tiles around the map.
126
+ def info_sequence(self) -> dict[str, str | float | int]:
127
+ """Returns a dictionary with information about the background terrain.
123
128
  Adds the EPSG:3857 string to the data for convenient usage in QGIS.
124
129
 
125
130
  Returns:
126
- dict[str, dict[str, float | int]] -- A dictionary with information about the tiles.
131
+ dict[str, str, float | int] -- A dictionary with information about the background
132
+ terrain.
127
133
  """
128
- data = {}
129
134
  self.qgis_sequence()
130
135
 
131
- for tile in self.tiles:
132
- north, south, east, west = tile.bbox
133
- epsg3857_string = tile.get_epsg3857_string()
134
- epsg3857_string_with_margin = tile.get_epsg3857_string(add_margin=True)
135
-
136
- tile_entry = {
137
- "center_latitude": tile.coordinates[0],
138
- "center_longitude": tile.coordinates[1],
139
- "epsg3857_string": epsg3857_string,
140
- "epsg3857_string_with_margin": epsg3857_string_with_margin,
141
- "height": tile.map_height,
142
- "width": tile.map_width,
143
- "north": north,
144
- "south": south,
145
- "east": east,
146
- "west": west,
147
- }
148
- if tile.code is not None:
149
- data[tile.code] = tile_entry
150
-
136
+ dem = self.dems[0]
137
+
138
+ north, south, east, west = dem.bbox
139
+ epsg3857_string = dem.get_epsg3857_string()
140
+ epsg3857_string_with_margin = dem.get_epsg3857_string(add_margin=True)
141
+
142
+ data = {
143
+ "center_latitude": dem.coordinates[0],
144
+ "center_longitude": dem.coordinates[1],
145
+ "epsg3857_string": epsg3857_string,
146
+ "epsg3857_string_with_margin": epsg3857_string_with_margin,
147
+ "height": dem.map_size,
148
+ "width": dem.map_size,
149
+ "north": north,
150
+ "south": south,
151
+ "east": east,
152
+ "west": west,
153
+ }
151
154
  return data # type: ignore
152
155
 
153
156
  def qgis_sequence(self) -> None:
154
157
  """Generates QGIS scripts for creating bounding box layers and rasterizing them."""
155
- qgis_layers = [
156
- (f"Background_{tile.code}", *tile.get_espg3857_bbox()) for tile in self.tiles
157
- ]
158
- qgis_layers_with_margin = [
159
- (f"Background_{tile.code}_margin", *tile.get_espg3857_bbox(add_margin=True))
160
- for tile in self.tiles
161
- ]
162
-
163
- layers = qgis_layers + qgis_layers_with_margin
164
-
165
- self.create_qgis_scripts(layers)
158
+ qgis_layer = (f"Background_{FULL_NAME}", *self.dems[0].get_espg3857_bbox())
159
+ qgis_layer_with_margin = (
160
+ f"Background_{FULL_NAME}_margin",
161
+ *self.dems[0].get_espg3857_bbox(add_margin=True),
162
+ )
163
+ self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
166
164
 
167
165
  def generate_obj_files(self) -> None:
168
- """Iterates over all tiles and generates 3D obj files based on DEM data.
166
+ """Iterates over all dems and generates 3D obj files based on DEM data.
169
167
  If at least one DEM file is missing, the generation will be stopped at all.
170
168
  """
171
- for tile in self.tiles:
172
- # Read DEM data from the tile.
173
- dem_path = tile.dem_path
174
- if not os.path.isfile(dem_path):
175
- self.logger.warning("DEM file not found, generation will be stopped: %s", dem_path)
169
+ for dem in self.dems:
170
+ if not os.path.isfile(dem.dem_path):
171
+ self.logger.warning(
172
+ "DEM file not found, generation will be stopped: %s", dem.dem_path
173
+ )
176
174
  return
177
175
 
178
- self.logger.debug("DEM file for tile %s found: %s", tile.code, dem_path)
176
+ self.logger.debug("DEM file for found: %s", dem.dem_path)
179
177
 
180
- base_directory = os.path.dirname(dem_path)
181
- save_path = os.path.join(base_directory, f"{tile.code}.obj")
182
- self.logger.debug("Generating obj file for tile %s in path: %s", tile.code, save_path)
178
+ filename = os.path.splitext(os.path.basename(dem.dem_path))[0]
179
+ save_path = os.path.join(self.background_directory, f"{filename}.obj")
180
+ self.logger.debug("Generating obj file in path: %s", save_path)
183
181
 
184
- dem_data = cv2.imread(tile.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
185
- self.plane_from_np(tile.code, dem_data, save_path) # type: ignore
182
+ dem_data = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
183
+ self.plane_from_np(dem_data, save_path, is_preview=dem.is_preview) # type: ignore
186
184
 
187
185
  # pylint: disable=too-many-locals
188
186
  def cutout(self, dem_path: str) -> str:
@@ -197,17 +195,16 @@ class Background(Component):
197
195
  dem_data = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
198
196
 
199
197
  center = (dem_data.shape[0] // 2, dem_data.shape[1] // 2)
200
- half_size = self.map_height // 2
198
+ half_size = self.map_size // 2
201
199
  x1 = center[0] - half_size
202
200
  x2 = center[0] + half_size
203
201
  y1 = center[1] - half_size
204
202
  y2 = center[1] + half_size
205
203
  dem_data = dem_data[x1:x2, y1:y2]
206
204
 
207
- output_size = self.map_height + 1
205
+ output_size = self.map_size + 1
208
206
 
209
207
  main_dem_path = self.game.dem_file_path(self.map_directory)
210
- dem_directory = os.path.dirname(main_dem_path)
211
208
 
212
209
  try:
213
210
  os.remove(main_dem_path)
@@ -219,46 +216,25 @@ class Background(Component):
219
216
  dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR
220
217
  )
221
218
 
222
- # Giant Editor contains a bug for large maps, where the DEM should not match
223
- # the UnitsPerPixel value. For example, for map 8192x8192, without bug
224
- # the DEM image should be 8193x8193, but it does not work, so we need to
225
- # resize the DEM to 4097x4097.
226
- if self.map_height > 4096:
227
- correct_dem_path = os.path.join(dem_directory, "correct_dem.png")
228
- save_path = correct_dem_path
229
-
230
- output_size = self.map_height // 2 + 1
231
- bugged_dem_data = cv2.resize(
232
- dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR
233
- )
234
- # pylint: disable=no-member
235
- cv2.imwrite(main_dem_path, bugged_dem_data)
236
- else:
237
- save_path = main_dem_path
238
-
239
- cv2.imwrite(save_path, resized_dem_data) # pylint: disable=no-member
240
- self.logger.info("DEM cutout saved: %s", save_path)
219
+ cv2.imwrite(main_dem_path, resized_dem_data) # pylint: disable=no-member
220
+ self.logger.info("DEM cutout saved: %s", main_dem_path)
241
221
 
242
- return save_path
222
+ return main_dem_path
243
223
 
244
224
  # pylint: disable=too-many-locals
245
- def plane_from_np(self, tile_code: str, dem_data: np.ndarray, save_path: str) -> None:
225
+ def plane_from_np(self, dem_data: np.ndarray, save_path: str, is_preview: bool = False) -> None:
246
226
  """Generates a 3D obj file based on DEM data.
247
227
 
248
228
  Arguments:
249
- tile_code (str) -- The code of the tile.
250
229
  dem_data (np.ndarray) -- The DEM data as a numpy array.
251
230
  save_path (str) -- The path where the obj file will be saved.
231
+ is_preview (bool, optional) -- If True, the preview mesh will be generated.
252
232
  """
253
- if tile_code == PATH_FULL_NAME:
254
- resize_factor = FULL_RESIZE_FACTOR
255
- else:
256
- resize_factor = RESIZE_FACTOR
257
233
  dem_data = cv2.resize( # pylint: disable=no-member
258
- dem_data, (0, 0), fx=resize_factor, fy=resize_factor
234
+ dem_data, (0, 0), fx=RESIZE_FACTOR, fy=RESIZE_FACTOR
259
235
  )
260
236
  self.logger.debug(
261
- "DEM data resized to shape: %s with factor: %s", dem_data.shape, resize_factor
237
+ "DEM data resized to shape: %s with factor: %s", dem_data.shape, RESIZE_FACTOR
262
238
  )
263
239
 
264
240
  # Invert the height values.
@@ -271,9 +247,7 @@ class Background(Component):
271
247
  z = dem_data
272
248
 
273
249
  self.logger.debug(
274
- "Starting to generate a mesh for tile %s with shape: %s x %s. "
275
- "This may take a while...",
276
- tile_code,
250
+ "Starting to generate a mesh for with shape: %s x %s. This may take a while...",
277
251
  cols,
278
252
  rows,
279
253
  )
@@ -300,7 +274,7 @@ class Background(Component):
300
274
  mesh.apply_transform(rotation_matrix_y)
301
275
  mesh.apply_transform(rotation_matrix_z)
302
276
 
303
- if tile_code == PATH_FULL_PREVIEW_NAME:
277
+ if is_preview:
304
278
  # Simplify the preview mesh to reduce the size of the file.
305
279
  mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
306
280
 
@@ -314,7 +288,7 @@ class Background(Component):
314
288
  else:
315
289
  z_scaling_factor = 1 / 2**5
316
290
  self.logger.debug("Z scaling factor: %s", z_scaling_factor)
317
- mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
291
+ mesh.apply_scale([1 / RESIZE_FACTOR, 1 / RESIZE_FACTOR, z_scaling_factor])
318
292
 
319
293
  mesh.export(save_path)
320
294
  self.logger.debug("Obj file saved: %s", save_path)
@@ -336,19 +310,105 @@ class Background(Component):
336
310
 
337
311
  # pylint: disable=no-member
338
312
  def previews(self) -> list[str]:
339
- """Returns the path to the image of full tile and the path to the STL preview file.
313
+ """Returns the path to the image previews paths and the path to the STL preview file.
340
314
 
341
315
  Returns:
342
316
  list[str] -- A list of paths to the previews.
343
317
  """
344
- full_tile = next((tile for tile in self.tiles if tile.code == PATH_FULL_NAME), None)
345
- if full_tile:
346
- preview_path = os.path.join(self.previews_directory, "background_dem.png")
347
- full_tile_image = cv2.imread(full_tile.dem_path, cv2.IMREAD_UNCHANGED)
348
- full_tile_image = cv2.normalize( # type: ignore
349
- full_tile_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
350
- )
351
- full_tile_image = cv2.cvtColor(full_tile_image, cv2.COLOR_GRAY2BGR)
352
- cv2.imwrite(preview_path, full_tile_image)
353
- return [preview_path, self.stl_preview_path]
354
- return [self.stl_preview_path]
318
+ preview_paths = self.dem_previews(self.game.dem_file_path(self.map_directory))
319
+ for dem in self.dems:
320
+ if dem.is_preview: # type: ignore
321
+ background_dem_preview_path = os.path.join(
322
+ self.previews_directory, "background_dem.png"
323
+ )
324
+ background_dem_preview_image = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED)
325
+
326
+ background_dem_preview_image = cv2.resize(
327
+ background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
328
+ )
329
+ background_dem_preview_image = cv2.normalize( # type: ignore
330
+ background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
331
+ )
332
+ background_dem_preview_image = cv2.cvtColor(
333
+ background_dem_preview_image, cv2.COLOR_GRAY2BGR
334
+ )
335
+
336
+ cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
337
+ preview_paths.append(background_dem_preview_path)
338
+
339
+ if self.stl_preview_path:
340
+ preview_paths.append(self.stl_preview_path)
341
+
342
+ return preview_paths
343
+
344
+ def dem_previews(self, image_path: str) -> list[str]:
345
+ """Get list of preview images.
346
+
347
+ Arguments:
348
+ image_path (str): Path to the DEM file.
349
+
350
+ Returns:
351
+ list[str]: List of preview images.
352
+ """
353
+ self.logger.debug("Starting DEM previews generation.")
354
+ return [self.grayscale_preview(image_path), self.colored_preview(image_path)]
355
+
356
+ def grayscale_preview(self, image_path: str) -> str:
357
+ """Converts DEM image to grayscale RGB image and saves it to the map directory.
358
+ Returns path to the preview image.
359
+
360
+ Arguments:
361
+ image_path (str): Path to the DEM file.
362
+
363
+ Returns:
364
+ str: Path to the preview image.
365
+ """
366
+ grayscale_dem_path = os.path.join(self.previews_directory, "dem_grayscale.png")
367
+
368
+ self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
369
+
370
+ dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
371
+ dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
372
+ cv2.imwrite(grayscale_dem_path, dem_data_rgb)
373
+ return grayscale_dem_path
374
+
375
+ def colored_preview(self, image_path: str) -> str:
376
+ """Converts DEM image to colored RGB image and saves it to the map directory.
377
+ Returns path to the preview image.
378
+
379
+ Arguments:
380
+ image_path (str): Path to the DEM file.
381
+
382
+ Returns:
383
+ list[str]: List with a single path to the DEM file
384
+ """
385
+ colored_dem_path = os.path.join(self.previews_directory, "dem_colored.png")
386
+
387
+ self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
388
+
389
+ dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
390
+
391
+ self.logger.debug(
392
+ "DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
393
+ dem_data.shape,
394
+ dem_data.dtype,
395
+ dem_data.min(),
396
+ dem_data.max(),
397
+ )
398
+
399
+ # Create an empty array with the same shape and type as dem_data.
400
+ dem_data_normalized = np.empty_like(dem_data)
401
+
402
+ # Normalize the DEM data to the range [0, 255]
403
+ cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
404
+ self.logger.debug(
405
+ "DEM data after normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
406
+ dem_data_normalized.shape,
407
+ dem_data_normalized.dtype,
408
+ dem_data_normalized.min(),
409
+ dem_data_normalized.max(),
410
+ )
411
+ dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
412
+
413
+ cv2.imwrite(colored_dem_path, dem_data_colored)
414
+ return colored_dem_path