maps4fs 1.7.1__py3-none-any.whl → 1.7.6__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.
Potentially problematic release.
This version of maps4fs might be problematic. Click here for more details.
- maps4fs/__init__.py +5 -2
- maps4fs/generator/dtm/bavaria.py +115 -0
- maps4fs/generator/dtm/dtm.py +335 -47
- maps4fs/generator/dtm/nrw.py +127 -0
- maps4fs/generator/dtm/srtm.py +59 -161
- maps4fs/generator/dtm/usgs.py +6 -268
- maps4fs/generator/texture.py +9 -2
- maps4fs/toolbox/custom_osm.py +67 -0
- {maps4fs-1.7.1.dist-info → maps4fs-1.7.6.dist-info}/METADATA +51 -21
- {maps4fs-1.7.1.dist-info → maps4fs-1.7.6.dist-info}/RECORD +13 -10
- {maps4fs-1.7.1.dist-info → maps4fs-1.7.6.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.7.1.dist-info → maps4fs-1.7.6.dist-info}/WHEEL +0 -0
- {maps4fs-1.7.1.dist-info → maps4fs-1.7.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
"""This module contains provider of NRW data."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
from owslib.wcs import WebCoverageService
|
7
|
+
from owslib.util import Authentication
|
8
|
+
from pyproj import Transformer
|
9
|
+
|
10
|
+
from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
|
11
|
+
|
12
|
+
|
13
|
+
class NRWProviderSettings(DTMProviderSettings):
|
14
|
+
"""Settings for the NRW provider."""
|
15
|
+
|
16
|
+
|
17
|
+
# pylint: disable=too-many-locals
|
18
|
+
class NRWProvider(DTMProvider):
|
19
|
+
"""Generic provider of WCS sources."""
|
20
|
+
|
21
|
+
_code = "NRW"
|
22
|
+
_name = "North Rhine-Westphalia DGM1"
|
23
|
+
_region = "DE"
|
24
|
+
_icon = "🇩🇪"
|
25
|
+
_resolution = 1
|
26
|
+
_data: np.ndarray | None = None
|
27
|
+
_settings = NRWProviderSettings
|
28
|
+
_author = "[kbrandwijk](https://github.com/kbrandwijk)"
|
29
|
+
_is_community = True
|
30
|
+
_instructions = None
|
31
|
+
|
32
|
+
def __init__(self, *args, **kwargs):
|
33
|
+
super().__init__(*args, **kwargs)
|
34
|
+
self.shared_tiff_path = os.path.join(self._tile_directory, "shared")
|
35
|
+
os.makedirs(self.shared_tiff_path, exist_ok=True)
|
36
|
+
|
37
|
+
def download_tiles(self) -> list[str]:
|
38
|
+
bbox = self.get_bbox()
|
39
|
+
bbox = self.transform_bbox(bbox, "EPSG:25832")
|
40
|
+
|
41
|
+
tiles = self.tile_bbox(bbox, 1000)
|
42
|
+
|
43
|
+
all_tif_files = self.download_all_tiles(tiles)
|
44
|
+
return all_tif_files
|
45
|
+
|
46
|
+
def tile_bbox(
|
47
|
+
self,
|
48
|
+
bbox: tuple[float, float, float, float],
|
49
|
+
tile_size: int) -> list[tuple[float, float, float, float]]:
|
50
|
+
"""Tile the bounding box into smaller bounding boxes of a specified size.
|
51
|
+
|
52
|
+
Arguments:
|
53
|
+
bbox (tuple): Bounding box to tile (north, south, east, west).
|
54
|
+
tile_size (int): Size of the tiles in meters.
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
list: List of smaller bounding boxes (north, south, east, west).
|
58
|
+
"""
|
59
|
+
north, south, east, west = bbox
|
60
|
+
x_coords = np.arange(west, east, tile_size)
|
61
|
+
y_coords = np.arange(south, north, tile_size)
|
62
|
+
x_coords = np.append(x_coords, east).astype(x_coords.dtype)
|
63
|
+
y_coords = np.append(y_coords, north).astype(y_coords.dtype)
|
64
|
+
|
65
|
+
x_min, y_min = np.meshgrid(x_coords[:-1], y_coords[:-1], indexing="ij")
|
66
|
+
x_max, y_max = np.meshgrid(x_coords[1:], y_coords[1:], indexing="ij")
|
67
|
+
|
68
|
+
tiles = np.stack([x_min.ravel(), y_min.ravel(), x_max.ravel(), y_max.ravel()], axis=1)
|
69
|
+
|
70
|
+
return tiles
|
71
|
+
|
72
|
+
def download_all_tiles(self, tiles: list[tuple[float, float, float, float]]) -> list[str]:
|
73
|
+
"""Download tiles from the NRW provider.
|
74
|
+
|
75
|
+
Arguments:
|
76
|
+
tiles (list): List of tiles to download.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
list: List of paths to the downloaded GeoTIFF files.
|
80
|
+
"""
|
81
|
+
all_tif_files = []
|
82
|
+
wcs = WebCoverageService(
|
83
|
+
'https://www.wcs.nrw.de/geobasis/wcs_nw_dgm',
|
84
|
+
auth=Authentication(verify=False),
|
85
|
+
timeout=600)
|
86
|
+
for tile in tiles:
|
87
|
+
file_name = '_'.join(map(str, tile)) + '.tif'
|
88
|
+
file_path = os.path.join(self.shared_tiff_path, file_name)
|
89
|
+
|
90
|
+
if not os.path.exists(file_path):
|
91
|
+
output = wcs.getCoverage(
|
92
|
+
identifier=['nw_dgm'],
|
93
|
+
subsets=[('y', str(tile[0]), str(tile[2])), ('x', str(tile[1]), str(tile[3]))],
|
94
|
+
format='image/tiff'
|
95
|
+
)
|
96
|
+
with open(file_path, 'wb') as f:
|
97
|
+
f.write(output.read())
|
98
|
+
|
99
|
+
all_tif_files.append(file_path)
|
100
|
+
return all_tif_files
|
101
|
+
|
102
|
+
def transform_bbox(
|
103
|
+
self,
|
104
|
+
bbox: tuple[float, float, float, float],
|
105
|
+
to_crs: str) -> tuple[float, float, float, float]:
|
106
|
+
"""Transform the bounding box to a different coordinate reference system (CRS).
|
107
|
+
|
108
|
+
Arguments:
|
109
|
+
bbox (tuple): Bounding box to transform (north, south, east, west).
|
110
|
+
to_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
tuple: Transformed bounding box (north, south, east, west).
|
114
|
+
"""
|
115
|
+
transformer = Transformer.from_crs("epsg:4326", to_crs)
|
116
|
+
north, south, east, west = bbox
|
117
|
+
bottom_left_x, bottom_left_y = transformer.transform(xx=south, yy=west)
|
118
|
+
top_left_x, top_left_y = transformer.transform(xx=north, yy=west)
|
119
|
+
top_right_x, top_right_y = transformer.transform(xx=north, yy=east)
|
120
|
+
bottom_right_x, bottom_right_y = transformer.transform(xx=south, yy=east)
|
121
|
+
|
122
|
+
west = min(bottom_left_y, bottom_right_y)
|
123
|
+
east = max(top_left_y, top_right_y)
|
124
|
+
south = min(bottom_left_x, top_left_x)
|
125
|
+
north = max(bottom_right_x, top_right_x)
|
126
|
+
|
127
|
+
return north, south, east, west
|
maps4fs/generator/dtm/srtm.py
CHANGED
@@ -7,7 +7,7 @@ import math
|
|
7
7
|
import os
|
8
8
|
import shutil
|
9
9
|
|
10
|
-
import
|
10
|
+
import requests
|
11
11
|
|
12
12
|
from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
|
13
13
|
|
@@ -15,9 +15,6 @@ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
|
|
15
15
|
class SRTM30ProviderSettings(DTMProviderSettings):
|
16
16
|
"""Settings for SRTM 30m provider."""
|
17
17
|
|
18
|
-
easy_mode: bool = True
|
19
|
-
power_factor: int = 0
|
20
|
-
|
21
18
|
|
22
19
|
class SRTM30Provider(DTMProvider):
|
23
20
|
"""Provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
|
@@ -32,22 +29,6 @@ class SRTM30Provider(DTMProvider):
|
|
32
29
|
|
33
30
|
_author = "[iwatkot](https://github.com/iwatkot)"
|
34
31
|
|
35
|
-
_instructions = (
|
36
|
-
"ℹ️ If you don't know how to work with DEM data, it is recommended to use the "
|
37
|
-
"**Easy mode** option. It will automatically change the values in the image, so the "
|
38
|
-
"terrain will be visible in the Giants Editor. If you're an experienced modder, it's "
|
39
|
-
"recommended to disable this option and work with the DEM data in a usual way. \n"
|
40
|
-
"ℹ️ If the terrain height difference in the real world is bigger than 255 meters, "
|
41
|
-
"the [Height scale](https://github.com/iwatkot/maps4fs/blob/main/docs/dem.md#height-scale)"
|
42
|
-
" parameter in the **map.i3d** file will be changed automatically. \n"
|
43
|
-
"⚡ If the **Easy mode** option is disabled, you will probably get completely flat "
|
44
|
-
"terrain, unless you adjust the DEM Multiplier Setting or the Height scale parameter in "
|
45
|
-
"the Giants Editor. \n"
|
46
|
-
"💡 You can use the **Power factor** setting to make the difference between heights "
|
47
|
-
"bigger. Be extremely careful with this setting, and use only low values, otherwise your "
|
48
|
-
"terrain may be completely broken. \n"
|
49
|
-
)
|
50
|
-
|
51
32
|
_settings = SRTM30ProviderSettings
|
52
33
|
|
53
34
|
def __init__(self, *args, **kwargs):
|
@@ -58,169 +39,86 @@ class SRTM30Provider(DTMProvider):
|
|
58
39
|
os.makedirs(self.gz_directory, exist_ok=True)
|
59
40
|
self.data_info: dict[str, int | str | float] | None = None # type: ignore
|
60
41
|
|
61
|
-
def
|
62
|
-
"""
|
42
|
+
def download_tiles(self):
|
43
|
+
"""Download SRTM tiles."""
|
44
|
+
north, south, east, west = self.get_bbox()
|
63
45
|
|
64
|
-
|
65
|
-
|
66
|
-
|
46
|
+
tiles = []
|
47
|
+
# Look at each corner of the bbox in case the bbox spans across multiple tiles
|
48
|
+
for pair in [(north, east), (south, west), (south, east), (north, west)]:
|
49
|
+
tile_parameters = self.get_tile_parameters(*pair)
|
50
|
+
tile_name = tile_parameters["tile_name"]
|
51
|
+
decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
|
67
52
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
53
|
+
if not os.path.isfile(decompressed_tile_path):
|
54
|
+
compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
|
55
|
+
if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
|
56
|
+
raise FileNotFoundError(f"Tile {tile_name} not found.")
|
72
57
|
|
73
|
-
|
74
|
-
|
58
|
+
with gzip.open(compressed_tile_path, "rb") as f_in:
|
59
|
+
with open(decompressed_tile_path, "wb") as f_out:
|
60
|
+
shutil.copyfileobj(f_in, f_out)
|
61
|
+
tiles.append(decompressed_tile_path)
|
75
62
|
|
76
|
-
|
77
|
-
if lon < 0:
|
78
|
-
tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
|
79
|
-
else:
|
80
|
-
tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
|
63
|
+
return tiles
|
81
64
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
return {"latitude_band": latitude_band, "tile_name": tile_name}
|
65
|
+
# region provider specific helpers
|
66
|
+
def download_tile(self, output_path: str, **kwargs) -> bool:
|
67
|
+
"""Download a tile from the provider.
|
86
68
|
|
87
|
-
|
88
|
-
|
69
|
+
Arguments:
|
70
|
+
output_path (str): Path to save the downloaded tile.
|
89
71
|
|
90
72
|
Returns:
|
91
|
-
|
92
|
-
"""
|
93
|
-
tile_parameters = self.get_tile_parameters(*self.coordinates)
|
94
|
-
tile_name = tile_parameters["tile_name"]
|
95
|
-
decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
|
96
|
-
|
97
|
-
if not os.path.isfile(decompressed_tile_path):
|
98
|
-
compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
|
99
|
-
if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
|
100
|
-
raise FileNotFoundError(f"Tile {tile_name} not found.")
|
101
|
-
|
102
|
-
with gzip.open(compressed_tile_path, "rb") as f_in:
|
103
|
-
with open(decompressed_tile_path, "wb") as f_out:
|
104
|
-
shutil.copyfileobj(f_in, f_out)
|
105
|
-
|
106
|
-
data = self.extract_roi(decompressed_tile_path)
|
107
|
-
|
108
|
-
self.data_info = {}
|
109
|
-
self.add_numpy_params(data, "original")
|
110
|
-
|
111
|
-
data = self.signed_to_unsigned(data)
|
112
|
-
self.add_numpy_params(data, "grounded")
|
113
|
-
|
114
|
-
original_deviation = int(self.data_info["original_deviation"])
|
115
|
-
in_game_maximum_height = 65535 // 255
|
116
|
-
if original_deviation > in_game_maximum_height:
|
117
|
-
suggested_height_scale_multiplier = (
|
118
|
-
original_deviation / in_game_maximum_height # type: ignore
|
119
|
-
)
|
120
|
-
suggested_height_scale_value = int(255 * suggested_height_scale_multiplier)
|
121
|
-
else:
|
122
|
-
suggested_height_scale_multiplier = 1
|
123
|
-
suggested_height_scale_value = 255
|
124
|
-
|
125
|
-
self.data_info["suggested_height_scale_multiplier"] = suggested_height_scale_multiplier
|
126
|
-
self.data_info["suggested_height_scale_value"] = suggested_height_scale_value
|
127
|
-
|
128
|
-
self.map.shared_settings.height_scale_multiplier = ( # type: ignore
|
129
|
-
suggested_height_scale_multiplier
|
130
|
-
)
|
131
|
-
self.map.shared_settings.height_scale_value = suggested_height_scale_value # type: ignore
|
132
|
-
|
133
|
-
if self.user_settings.easy_mode: # type: ignore
|
134
|
-
try:
|
135
|
-
data = self.normalize_dem(data)
|
136
|
-
self.add_numpy_params(data, "normalized")
|
137
|
-
|
138
|
-
normalized_deviation = self.data_info["normalized_deviation"]
|
139
|
-
z_scaling_factor = normalized_deviation / original_deviation # type: ignore
|
140
|
-
self.data_info["z_scaling_factor"] = z_scaling_factor
|
141
|
-
|
142
|
-
self.map.shared_settings.mesh_z_scaling_factor = z_scaling_factor # type: ignore
|
143
|
-
self.map.shared_settings.change_height_scale = True # type: ignore
|
144
|
-
|
145
|
-
except Exception as e: # pylint: disable=W0718
|
146
|
-
self.logger.error(
|
147
|
-
"Failed to normalize DEM data. Error: %s. Using original data.", e
|
148
|
-
)
|
149
|
-
|
150
|
-
return data
|
151
|
-
|
152
|
-
def add_numpy_params(
|
153
|
-
self,
|
154
|
-
data: np.ndarray,
|
155
|
-
prefix: str,
|
156
|
-
) -> None:
|
157
|
-
"""Add numpy array parameters to the data_info dictionary.
|
158
|
-
|
159
|
-
Arguments:
|
160
|
-
data (np.ndarray): Numpy array of the tile.
|
161
|
-
prefix (str): Prefix for the parameters.
|
73
|
+
bool: True if the tile was downloaded successfully, False otherwise.
|
162
74
|
"""
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
75
|
+
url = self.formatted_url(**kwargs)
|
76
|
+
response = requests.get(url, stream=True, timeout=10)
|
77
|
+
if response.status_code == 200:
|
78
|
+
with open(output_path, "wb") as file:
|
79
|
+
for chunk in response.iter_content(chunk_size=1024):
|
80
|
+
file.write(chunk)
|
81
|
+
return True
|
82
|
+
return False
|
83
|
+
|
84
|
+
def get_or_download_tile(self, output_path: str, **kwargs) -> str | None:
|
85
|
+
"""Get or download a tile from the provider.
|
170
86
|
|
171
87
|
Arguments:
|
172
|
-
|
88
|
+
output_path (str): Path to save the downloaded tile.
|
173
89
|
|
174
90
|
Returns:
|
175
|
-
|
91
|
+
str: Path to the downloaded tile or None if the tile not exists and was
|
92
|
+
not downloaded.
|
176
93
|
"""
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
return
|
94
|
+
if not os.path.exists(output_path):
|
95
|
+
if not self.download_tile(output_path, **kwargs):
|
96
|
+
return None
|
97
|
+
return output_path
|
181
98
|
|
182
|
-
def
|
183
|
-
"""
|
99
|
+
def get_tile_parameters(self, *args) -> dict[str, str]:
|
100
|
+
"""Returns latitude band and tile name for SRTM tile from coordinates.
|
184
101
|
|
185
102
|
Arguments:
|
186
|
-
|
103
|
+
lat (float): Latitude.
|
104
|
+
lon (float): Longitude.
|
187
105
|
|
188
106
|
Returns:
|
189
|
-
|
107
|
+
dict: Tile parameters.
|
190
108
|
"""
|
191
|
-
|
192
|
-
minimum_height = int(data.min())
|
193
|
-
deviation = maximum_height - minimum_height
|
194
|
-
self.logger.debug(
|
195
|
-
"Maximum height: %s. Minimum height: %s. Deviation: %s.",
|
196
|
-
maximum_height,
|
197
|
-
minimum_height,
|
198
|
-
deviation,
|
199
|
-
)
|
200
|
-
self.logger.debug("Number of unique values in original DEM data: %s.", np.unique(data).size)
|
109
|
+
lat, lon = args
|
201
110
|
|
202
|
-
|
203
|
-
|
204
|
-
scaling_factor = adjusted_maximum_height / maximum_height
|
205
|
-
self.logger.debug(
|
206
|
-
"Adjusted maximum height: %s. Scaling factor: %s.",
|
207
|
-
adjusted_maximum_height,
|
208
|
-
scaling_factor,
|
209
|
-
)
|
111
|
+
tile_latitude = math.floor(lat)
|
112
|
+
tile_longitude = math.floor(lon)
|
210
113
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
)
|
217
|
-
data = np.power(data, power_factor).astype(np.uint16)
|
114
|
+
latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
|
115
|
+
if lon < 0:
|
116
|
+
tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
|
117
|
+
else:
|
118
|
+
tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
|
218
119
|
|
219
|
-
normalized_data = np.round(data * scaling_factor).astype(np.uint16)
|
220
120
|
self.logger.debug(
|
221
|
-
"
|
222
|
-
normalized_data.max(),
|
223
|
-
normalized_data.min(),
|
224
|
-
np.unique(normalized_data).size,
|
121
|
+
"Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
|
225
122
|
)
|
226
|
-
return
|
123
|
+
return {"latitude_band": latitude_band, "tile_name": tile_name}
|
124
|
+
# endregion
|