maps4fs 1.7.2__tar.gz → 1.7.4__tar.gz

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.
Files changed (38) hide show
  1. {maps4fs-1.7.2 → maps4fs-1.7.4}/PKG-INFO +4 -2
  2. {maps4fs-1.7.2 → maps4fs-1.7.4}/README.md +3 -1
  3. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/__init__.py +1 -0
  4. maps4fs-1.7.4/maps4fs/generator/dtm/dtm.py +549 -0
  5. maps4fs-1.7.4/maps4fs/generator/dtm/nrw.py +127 -0
  6. maps4fs-1.7.4/maps4fs/generator/dtm/srtm.py +124 -0
  7. maps4fs-1.7.4/maps4fs/generator/dtm/usgs.py +135 -0
  8. maps4fs-1.7.4/maps4fs/toolbox/custom_osm.py +67 -0
  9. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs.egg-info/PKG-INFO +4 -2
  10. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs.egg-info/SOURCES.txt +2 -0
  11. {maps4fs-1.7.2 → maps4fs-1.7.4}/pyproject.toml +1 -1
  12. maps4fs-1.7.2/maps4fs/generator/dtm/dtm.py +0 -321
  13. maps4fs-1.7.2/maps4fs/generator/dtm/srtm.py +0 -226
  14. maps4fs-1.7.2/maps4fs/generator/dtm/usgs.py +0 -351
  15. {maps4fs-1.7.2 → maps4fs-1.7.4}/LICENSE.md +0 -0
  16. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/__init__.py +0 -0
  17. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/background.py +0 -0
  18. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/component.py +0 -0
  19. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/config.py +0 -0
  20. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/dem.py +0 -0
  21. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/dtm/__init__.py +0 -0
  22. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/game.py +0 -0
  23. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/grle.py +0 -0
  24. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/i3d.py +0 -0
  25. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/map.py +0 -0
  26. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/qgis.py +0 -0
  27. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/satellite.py +0 -0
  28. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/settings.py +0 -0
  29. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/generator/texture.py +0 -0
  30. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/logger.py +0 -0
  31. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/toolbox/__init__.py +0 -0
  32. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/toolbox/background.py +0 -0
  33. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs/toolbox/dem.py +0 -0
  34. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs.egg-info/dependency_links.txt +0 -0
  35. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs.egg-info/requires.txt +0 -0
  36. {maps4fs-1.7.2 → maps4fs-1.7.4}/maps4fs.egg-info/top_level.txt +0 -0
  37. {maps4fs-1.7.2 → maps4fs-1.7.4}/setup.cfg +0 -0
  38. {maps4fs-1.7.2 → maps4fs-1.7.4}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: maps4fs
3
- Version: 1.7.2
3
+ Version: 1.7.4
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: MIT License
@@ -36,7 +36,6 @@ Requires-Dist: pydantic
36
36
  <a href="#How-To-Run">How-To-Run</a><br>
37
37
  <a href="docs/FAQ.md">FAQ</a> •
38
38
  <a href="docs/map_structure.md">Map Structure</a> •
39
- <a href="docs/tips_and_hints.md">Tips and Hints</a> •
40
39
  <a href="#Modder-Toolbox">Modder Toolbox</a><br>
41
40
  <a href="#Supported-objects">Supported objects</a> •
42
41
  <a href="#Generation-info">Generation info</a> •
@@ -293,6 +292,9 @@ Tools are divided into categories, which are listed below.
293
292
  - **Texture Schema Editor** - allows you to view all the supported textures and edit their parameters, such as priority, OSM tags and so on. After editing, you should click the Show updated schema button and copy the JSON schema to the clipboard. Then you can use it in the Expert settings to generate the map with the updated textures.
294
293
 
295
294
  #### For Textures and DEM
295
+
296
+ - **Fix custom OSM file** - this tool fixes the most common errors in the custom OSM file, but it can not guarantee that the file will be fixed completely if some non-common errors are present.
297
+
296
298
  - **GeoTIFF windowing** - allows you to upload your GeoTIFF file and select the region of interest to extract it from the image. It's useful when you have high-resolution DEM data and want to create a height map using it.
297
299
 
298
300
  #### For Background terrain
@@ -10,7 +10,6 @@
10
10
  <a href="#How-To-Run">How-To-Run</a><br>
11
11
  <a href="docs/FAQ.md">FAQ</a> •
12
12
  <a href="docs/map_structure.md">Map Structure</a> •
13
- <a href="docs/tips_and_hints.md">Tips and Hints</a> •
14
13
  <a href="#Modder-Toolbox">Modder Toolbox</a><br>
15
14
  <a href="#Supported-objects">Supported objects</a> •
16
15
  <a href="#Generation-info">Generation info</a> •
@@ -267,6 +266,9 @@ Tools are divided into categories, which are listed below.
267
266
  - **Texture Schema Editor** - allows you to view all the supported textures and edit their parameters, such as priority, OSM tags and so on. After editing, you should click the Show updated schema button and copy the JSON schema to the clipboard. Then you can use it in the Expert settings to generate the map with the updated textures.
268
267
 
269
268
  #### For Textures and DEM
269
+
270
+ - **Fix custom OSM file** - this tool fixes the most common errors in the custom OSM file, but it can not guarantee that the file will be fixed completely if some non-common errors are present.
271
+
270
272
  - **GeoTIFF windowing** - allows you to upload your GeoTIFF file and select the region of interest to extract it from the image. It's useful when you have high-resolution DEM data and want to create a height map using it.
271
273
 
272
274
  #### For Background terrain
@@ -2,6 +2,7 @@
2
2
  from maps4fs.generator.dtm.dtm import DTMProvider
3
3
  from maps4fs.generator.dtm.srtm import SRTM30Provider, SRTM30ProviderSettings
4
4
  from maps4fs.generator.dtm.usgs import USGSProvider, USGSProviderSettings
5
+ from maps4fs.generator.dtm.nrw import NRWProvider, NRWProviderSettings
5
6
  from maps4fs.generator.game import Game
6
7
  from maps4fs.generator.map import Map
7
8
  from maps4fs.generator.settings import (
@@ -0,0 +1,549 @@
1
+ """This module contains the DTMProvider class and its subclasses. DTMProvider class is used to
2
+ define different providers of digital terrain models (DTM) data. Each provider has its own URL
3
+ and specific settings for downloading and processing the data."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from abc import ABC, abstractmethod
8
+ import os
9
+ from typing import TYPE_CHECKING, Type
10
+
11
+ import numpy as np
12
+ import osmnx as ox # type: ignore
13
+ import rasterio # type: ignore
14
+ from rasterio.warp import calculate_default_transform, reproject
15
+ from rasterio.enums import Resampling
16
+ from rasterio.merge import merge
17
+
18
+ from pydantic import BaseModel
19
+
20
+ from maps4fs.logger import Logger
21
+
22
+ if TYPE_CHECKING:
23
+ from maps4fs.generator.map import Map
24
+
25
+
26
+ class DTMProviderSettings(BaseModel):
27
+ """Base class for DTM provider settings models."""
28
+
29
+ easy_mode: bool = True
30
+ power_factor: int = 0
31
+
32
+
33
+ # pylint: disable=too-many-public-methods, too-many-instance-attributes
34
+ class DTMProvider(ABC):
35
+ """Base class for DTM providers."""
36
+
37
+ _code: str | None = None
38
+ _name: str | None = None
39
+ _region: str | None = None
40
+ _icon: str | None = None
41
+ _resolution: float | str | None = None
42
+
43
+ _url: str | None = None
44
+
45
+ _author: str | None = None
46
+ _contributors: str | None = None
47
+ _is_community: bool = False
48
+ _is_base: bool = False
49
+ _settings: Type[DTMProviderSettings] | None = None
50
+
51
+ _instructions: str | None = None
52
+
53
+ _base_instructions = (
54
+ "ℹ️ Using **Easy mode** is recommended, as it automatically adjusts the values in the "
55
+ "image, so the terrain elevation in Giants Editor will match real world "
56
+ "elevation in meters. \n"
57
+ "ℹ️ If the terrain height difference in the real world is bigger than 255 meters, "
58
+ "the [Height scale](https://github.com/iwatkot/maps4fs/blob/main/docs/dem.md#height-scale)"
59
+ " parameter in the **map.i3d** file will be automatically adjusted. \n"
60
+ "⚡ If the **Easy mode** option is disabled, you will probably get completely flat "
61
+ "terrain, unless you adjust the DEM Multiplier Setting or the Height scale parameter in "
62
+ "the Giants Editor. \n"
63
+ "💡 You can use the **Power factor** setting to make the difference between heights "
64
+ "bigger. Be extremely careful with this setting, and use only low values, otherwise your "
65
+ "terrain may be completely broken. \n"
66
+ )
67
+
68
+ # pylint: disable=R0913, R0917
69
+ def __init__(
70
+ self,
71
+ coordinates: tuple[float, float],
72
+ user_settings: DTMProviderSettings | None,
73
+ size: int,
74
+ directory: str,
75
+ logger: Logger,
76
+ map: Map | None = None, # pylint: disable=W0622
77
+ ):
78
+ self._coordinates = coordinates
79
+ self._user_settings = user_settings
80
+ self._size = size
81
+
82
+ if not self._code:
83
+ raise ValueError("Provider code must be defined.")
84
+ self._tile_directory = os.path.join(directory, self._code)
85
+ os.makedirs(self._tile_directory, exist_ok=True)
86
+
87
+ self.logger = logger
88
+ self.map = map
89
+
90
+ self._data_info: dict[str, int | str | float] | None = None
91
+
92
+ @property
93
+ def data_info(self) -> dict[str, int | str | float] | None:
94
+ """Information about the DTM data.
95
+
96
+ Returns:
97
+ dict: Information about the DTM data.
98
+ """
99
+ return self._data_info
100
+
101
+ @data_info.setter
102
+ def data_info(self, value: dict[str, int | str | float] | None) -> None:
103
+ """Set information about the DTM data.
104
+
105
+ Arguments:
106
+ value (dict): Information about the DTM data.
107
+ """
108
+ self._data_info = value
109
+
110
+ @property
111
+ def coordinates(self) -> tuple[float, float]:
112
+ """Coordinates of the center point of the DTM data.
113
+
114
+ Returns:
115
+ tuple: Latitude and longitude of the center point.
116
+ """
117
+ return self._coordinates
118
+
119
+ @property
120
+ def size(self) -> int:
121
+ """Size of the DTM data in meters.
122
+
123
+ Returns:
124
+ int: Size of the DTM data.
125
+ """
126
+ return self._size
127
+
128
+ @property
129
+ def url(self) -> str | None:
130
+ """URL of the provider."""
131
+ return self._url
132
+
133
+ def formatted_url(self, **kwargs) -> str:
134
+ """Formatted URL of the provider."""
135
+ if not self.url:
136
+ raise ValueError("URL must be defined.")
137
+ return self.url.format(**kwargs)
138
+
139
+ @classmethod
140
+ def author(cls) -> str | None:
141
+ """Author of the provider.
142
+
143
+ Returns:
144
+ str: Author of the provider.
145
+ """
146
+ return cls._author
147
+
148
+ @classmethod
149
+ def contributors(cls) -> str | None:
150
+ """Contributors of the provider.
151
+
152
+ Returns:
153
+ str: Contributors of the provider.
154
+ """
155
+ return cls._contributors
156
+
157
+ @classmethod
158
+ def is_base(cls) -> bool:
159
+ """Is the provider a base provider.
160
+
161
+ Returns:
162
+ bool: True if the provider is a base provider, False otherwise.
163
+ """
164
+ return cls._is_base
165
+
166
+ @classmethod
167
+ def is_community(cls) -> bool:
168
+ """Is the provider a community-driven project.
169
+
170
+ Returns:
171
+ bool: True if the provider is a community-driven project, False otherwise.
172
+ """
173
+ return cls._is_community
174
+
175
+ @classmethod
176
+ def settings(cls) -> Type[DTMProviderSettings] | None:
177
+ """Settings model of the provider.
178
+
179
+ Returns:
180
+ Type[DTMProviderSettings]: Settings model of the provider.
181
+ """
182
+ return cls._settings
183
+
184
+ @classmethod
185
+ def instructions(cls) -> str | None:
186
+ """Instructions for using the provider.
187
+
188
+ Returns:
189
+ str: Instructions for using the provider.
190
+ """
191
+ return cls._instructions
192
+
193
+ @classmethod
194
+ def base_instructions(cls) -> str | None:
195
+ """Instructions for using any provider.
196
+
197
+ Returns:
198
+ str: Instructions for using any provider.
199
+ """
200
+ return cls._base_instructions
201
+
202
+ @property
203
+ def user_settings(self) -> DTMProviderSettings | None:
204
+ """User settings of the provider.
205
+
206
+ Returns:
207
+ DTMProviderSettings: User settings of the provider.
208
+ """
209
+ return self._user_settings
210
+
211
+ @classmethod
212
+ def description(cls) -> str:
213
+ """Description of the provider.
214
+
215
+ Returns:
216
+ str: Provider description.
217
+ """
218
+ return f"{cls._icon} {cls._region} [{cls._resolution} m/px] {cls._name}"
219
+
220
+ @classmethod
221
+ def get_provider_by_code(cls, code: str) -> Type[DTMProvider] | None:
222
+ """Get a provider by its code.
223
+
224
+ Arguments:
225
+ code (str): Provider code.
226
+
227
+ Returns:
228
+ DTMProvider: Provider class or None if not found.
229
+ """
230
+ for provider in cls.__subclasses__():
231
+ if provider._code == code: # pylint: disable=W0212
232
+ return provider
233
+ return None
234
+
235
+ @classmethod
236
+ def get_provider_descriptions(cls) -> dict[str, str]:
237
+ """Get descriptions of all providers, where keys are provider codes and
238
+ values are provider descriptions.
239
+
240
+ Returns:
241
+ dict: Provider descriptions.
242
+ """
243
+ providers = {}
244
+ for provider in cls.__subclasses__():
245
+ providers[provider._code] = provider.description() # pylint: disable=W0212
246
+ return providers # type: ignore
247
+
248
+ @abstractmethod
249
+ def download_tiles(self) -> list[str]:
250
+ """Download tiles from the provider.
251
+
252
+ Returns:
253
+ list: List of paths to the downloaded tiles.
254
+ """
255
+ raise NotImplementedError
256
+
257
+ def get_numpy(self) -> np.ndarray:
258
+ """Get numpy array of the tile.
259
+ Resulting array must be 16 bit (signed or unsigned) integer and it should be already
260
+ windowed to the bounding box of ROI. It also must have only one channel.
261
+
262
+ Returns:
263
+ np.ndarray: Numpy array of the tile.
264
+ """
265
+ # download tiles using DTM provider implementation
266
+ tiles = self.download_tiles()
267
+ self.logger.debug(f"Downloaded {len(tiles)} DEM tiles")
268
+
269
+ # merge tiles if necessary
270
+ if len(tiles) > 1:
271
+ self.logger.debug("Multiple tiles downloaded. Merging tiles")
272
+ tile, _ = self.merge_geotiff(tiles)
273
+ else:
274
+ tile = tiles[0]
275
+
276
+ # determine CRS of the resulting tile and reproject if necessary
277
+ with rasterio.open(tile) as src:
278
+ crs = src.crs
279
+ if crs != "EPSG:4326":
280
+ self.logger.debug(f"Reprojecting GeoTIFF from {crs} to EPSG:4326...")
281
+ tile = self.reproject_geotiff(tile)
282
+
283
+ # extract region of interest from the tile
284
+ data = self.extract_roi(tile)
285
+
286
+ # process elevation data to be compatible with the game
287
+ data = self.process_elevation(data)
288
+
289
+ return data
290
+
291
+ def process_elevation(self, data: np.ndarray) -> np.ndarray:
292
+ """Process elevation data.
293
+
294
+ Arguments:
295
+ data (np.ndarray): Elevation data.
296
+
297
+ Returns:
298
+ np.ndarray: Processed elevation data.
299
+ """
300
+ self.data_info = {}
301
+ self.add_numpy_params(data, "original")
302
+
303
+ data = self.signed_to_unsigned(data)
304
+ self.add_numpy_params(data, "grounded")
305
+
306
+ original_deviation = int(self.data_info["original_deviation"])
307
+ in_game_maximum_height = 65535 // 255
308
+ if original_deviation > in_game_maximum_height:
309
+ suggested_height_scale_multiplier = (
310
+ original_deviation / in_game_maximum_height # type: ignore
311
+ )
312
+ suggested_height_scale_value = int(255 * suggested_height_scale_multiplier)
313
+ else:
314
+ suggested_height_scale_multiplier = 1
315
+ suggested_height_scale_value = 255
316
+
317
+ self.data_info["suggested_height_scale_multiplier"] = suggested_height_scale_multiplier
318
+ self.data_info["suggested_height_scale_value"] = suggested_height_scale_value
319
+
320
+ self.map.shared_settings.height_scale_multiplier = ( # type: ignore
321
+ suggested_height_scale_multiplier
322
+ )
323
+ self.map.shared_settings.height_scale_value = suggested_height_scale_value # type: ignore
324
+
325
+ if self.user_settings.easy_mode: # type: ignore
326
+ try:
327
+ data = self.normalize_dem(data)
328
+ self.add_numpy_params(data, "normalized")
329
+
330
+ normalized_deviation = self.data_info["normalized_deviation"]
331
+ z_scaling_factor = normalized_deviation / original_deviation # type: ignore
332
+ self.data_info["z_scaling_factor"] = z_scaling_factor
333
+
334
+ self.map.shared_settings.mesh_z_scaling_factor = z_scaling_factor # type: ignore
335
+ self.map.shared_settings.change_height_scale = True # type: ignore
336
+
337
+ except Exception as e: # pylint: disable=W0718
338
+ self.logger.error(
339
+ "Failed to normalize DEM data. Error: %s. Using original data.", e
340
+ )
341
+
342
+ return data
343
+
344
+ def info_sequence(self) -> dict[str, int | str | float] | None:
345
+ """Returns the information sequence for the component. Must be implemented in the child
346
+ class. If the component does not have an information sequence, an empty dictionary must be
347
+ returned.
348
+
349
+ Returns:
350
+ dict[str, int | str | float] | None: Information sequence for the component.
351
+ """
352
+ return self.data_info
353
+
354
+ # region helpers
355
+ def get_bbox(self) -> tuple[float, float, float, float]:
356
+ """Get bounding box of the tile based on the center point and size.
357
+
358
+ Returns:
359
+ tuple: Bounding box of the tile (north, south, east, west).
360
+ """
361
+ west, south, east, north = ox.utils_geo.bbox_from_point( # type: ignore
362
+ self.coordinates, dist=self.size // 2, project_utm=False
363
+ )
364
+ bbox = north, south, east, west
365
+ return bbox
366
+
367
+ def reproject_geotiff(self, input_tiff: str) -> str:
368
+ """Reproject a GeoTIFF file to a new coordinate reference system (CRS).
369
+
370
+ Arguments:
371
+ input_tiff (str): Path to the input GeoTIFF file.
372
+ target_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
373
+
374
+ Returns:
375
+ str: Path to the reprojected GeoTIFF file.
376
+ """
377
+ output_tiff = os.path.join(self._tile_directory, "merged.tif")
378
+
379
+ # Open the source GeoTIFF
380
+ self.logger.debug("Reprojecting GeoTIFF to %s CRS...", "EPSG:4326")
381
+ with rasterio.open(input_tiff) as src:
382
+ # Get the transform, width, and height of the target CRS
383
+ transform, width, height = calculate_default_transform(
384
+ src.crs, "EPSG:4326", src.width, src.height, *src.bounds
385
+ )
386
+
387
+ # Update the metadata for the target GeoTIFF
388
+ kwargs = src.meta.copy()
389
+ kwargs.update(
390
+ {"crs": "EPSG:4326", "transform": transform, "width": width, "height": height}
391
+ )
392
+
393
+ # Open the destination GeoTIFF file and reproject
394
+ with rasterio.open(output_tiff, "w", **kwargs) as dst:
395
+ for i in range(1, src.count + 1): # Iterate over all raster bands
396
+ reproject(
397
+ source=rasterio.band(src, i),
398
+ destination=rasterio.band(dst, i),
399
+ src_transform=src.transform,
400
+ src_crs=src.crs,
401
+ dst_transform=transform,
402
+ dst_crs="EPSG:4326",
403
+ resampling=Resampling.nearest, # Choose resampling method
404
+ )
405
+
406
+ self.logger.debug("Reprojected GeoTIFF saved to %s", output_tiff)
407
+ return output_tiff
408
+
409
+ def merge_geotiff(self, input_files: list[str]) -> tuple[str, str]:
410
+ """Merge multiple GeoTIFF files into a single GeoTIFF file.
411
+
412
+ Arguments:
413
+ input_files (list): List of input GeoTIFF files to merge.
414
+ output_file (str): Path to save the merged GeoTIFF file.
415
+ """
416
+ output_file = os.path.join(self._tile_directory, "merged.tif")
417
+ # Open all input GeoTIFF files as datasets
418
+ self.logger.debug("Merging tiff files...")
419
+ datasets = [rasterio.open(file) for file in input_files]
420
+
421
+ # Merge datasets
422
+ crs = datasets[0].crs
423
+ mosaic, out_transform = merge(datasets, nodata=0)
424
+
425
+ # Get metadata from the first file and update it for the output
426
+ out_meta = datasets[0].meta.copy()
427
+ out_meta.update(
428
+ {
429
+ "driver": "GTiff",
430
+ "height": mosaic.shape[1],
431
+ "width": mosaic.shape[2],
432
+ "transform": out_transform,
433
+ "count": mosaic.shape[0], # Number of bands
434
+ }
435
+ )
436
+
437
+ # Write merged GeoTIFF to the output file
438
+ with rasterio.open(output_file, "w", **out_meta) as dest:
439
+ dest.write(mosaic)
440
+
441
+ self.logger.debug("GeoTIFF images merged successfully into %s", output_file)
442
+ return output_file, crs
443
+
444
+ def extract_roi(self, tile_path: str) -> np.ndarray:
445
+ """Extract region of interest (ROI) from the GeoTIFF file.
446
+
447
+ Arguments:
448
+ tile_path (str): Path to the GeoTIFF file.
449
+
450
+ Raises:
451
+ ValueError: If the tile does not contain any data.
452
+
453
+ Returns:
454
+ np.ndarray: Numpy array of the ROI.
455
+ """
456
+ north, south, east, west = self.get_bbox()
457
+ with rasterio.open(tile_path) as src:
458
+ self.logger.debug("Opened tile, shape: %s, dtype: %s.", src.shape, src.dtypes[0])
459
+ window = rasterio.windows.from_bounds(west, south, east, north, src.transform)
460
+ self.logger.debug(
461
+ "Window parameters. Column offset: %s, row offset: %s, width: %s, height: %s.",
462
+ window.col_off,
463
+ window.row_off,
464
+ window.width,
465
+ window.height,
466
+ )
467
+ data = src.read(1, window=window, masked=True)
468
+ if not data.size > 0:
469
+ raise ValueError("No data in the tile.")
470
+
471
+ return data
472
+
473
+ def normalize_dem(self, data: np.ndarray) -> np.ndarray:
474
+ """Normalize DEM data to 16-bit unsigned integer using max height from settings.
475
+
476
+ Arguments:
477
+ data (np.ndarray): DEM data from SRTM file after cropping.
478
+
479
+ Returns:
480
+ np.ndarray: Normalized DEM data.
481
+ """
482
+ maximum_height = int(data.max())
483
+ minimum_height = int(data.min())
484
+ deviation = maximum_height - minimum_height
485
+ self.logger.debug(
486
+ "Maximum height: %s. Minimum height: %s. Deviation: %s.",
487
+ maximum_height,
488
+ minimum_height,
489
+ deviation,
490
+ )
491
+ self.logger.debug("Number of unique values in original DEM data: %s.", np.unique(data).size)
492
+
493
+ adjusted_maximum_height = maximum_height * 255
494
+ adjusted_maximum_height = min(adjusted_maximum_height, 65535)
495
+ scaling_factor = adjusted_maximum_height / maximum_height
496
+ self.logger.debug(
497
+ "Adjusted maximum height: %s. Scaling factor: %s.",
498
+ adjusted_maximum_height,
499
+ scaling_factor,
500
+ )
501
+
502
+ if self.user_settings.power_factor: # type: ignore
503
+ power_factor = 1 + self.user_settings.power_factor / 10 # type: ignore
504
+ self.logger.debug(
505
+ "Applying power factor: %s to the DEM data.",
506
+ power_factor,
507
+ )
508
+ data = np.power(data, power_factor).astype(np.uint16)
509
+
510
+ normalized_data = np.round(data * scaling_factor).astype(np.uint16)
511
+ self.logger.debug(
512
+ "Normalized data maximum height: %s. Minimum height: %s. Number of unique values: %s.",
513
+ normalized_data.max(),
514
+ normalized_data.min(),
515
+ np.unique(normalized_data).size,
516
+ )
517
+ return normalized_data
518
+
519
+ def signed_to_unsigned(self, data: np.ndarray, add_one: bool = True) -> np.ndarray:
520
+ """Convert signed 16-bit integer to unsigned 16-bit integer.
521
+
522
+ Arguments:
523
+ data (np.ndarray): DEM data from SRTM file after cropping.
524
+
525
+ Returns:
526
+ np.ndarray: Unsigned DEM data.
527
+ """
528
+ data = data - data.min()
529
+ if add_one:
530
+ data = data + 1
531
+ return data.astype(np.uint16)
532
+
533
+ def add_numpy_params(
534
+ self,
535
+ data: np.ndarray,
536
+ prefix: str,
537
+ ) -> None:
538
+ """Add numpy array parameters to the data_info dictionary.
539
+
540
+ Arguments:
541
+ data (np.ndarray): Numpy array of the tile.
542
+ prefix (str): Prefix for the parameters.
543
+ """
544
+ self.data_info[f"{prefix}_minimum_height"] = int(data.min()) # type: ignore
545
+ self.data_info[f"{prefix}_maximum_height"] = int(data.max()) # type: ignore
546
+ self.data_info[f"{prefix}_deviation"] = int(data.max() - data.min()) # type: ignore
547
+ self.data_info[f"{prefix}_unique_values"] = int(np.unique(data).size) # type: ignore
548
+
549
+ # endregion