maps4fs 1.5.6__py3-none-any.whl → 1.5.9__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 +3 -1
- maps4fs/generator/background.py +74 -110
- maps4fs/generator/dem.py +2 -8
- maps4fs/generator/dtm/__init__.py +0 -0
- maps4fs/generator/{dtm.py → dtm/dtm.py} +0 -71
- maps4fs/generator/dtm/srtm.py +85 -0
- maps4fs/generator/dtm/usgs.py +323 -0
- maps4fs/generator/i3d.py +4 -14
- maps4fs/generator/map.py +1 -1
- maps4fs/generator/settings.py +0 -2
- {maps4fs-1.5.6.dist-info → maps4fs-1.5.9.dist-info}/METADATA +12 -3
- maps4fs-1.5.9.dist-info/RECORD +27 -0
- maps4fs-1.5.6.dist-info/RECORD +0 -24
- {maps4fs-1.5.6.dist-info → maps4fs-1.5.9.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.5.6.dist-info → maps4fs-1.5.9.dist-info}/WHEEL +0 -0
- {maps4fs-1.5.6.dist-info → maps4fs-1.5.9.dist-info}/top_level.txt +0 -0
maps4fs/__init__.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# pylint: disable=missing-module-docstring
|
2
|
-
from maps4fs.generator.dtm import DTMProvider
|
2
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
3
|
+
from maps4fs.generator.dtm.srtm import SRTM30Provider
|
4
|
+
from maps4fs.generator.dtm.usgs import USGS1mProvider
|
3
5
|
from maps4fs.generator.game import Game
|
4
6
|
from maps4fs.generator.map import Map
|
5
7
|
from maps4fs.generator.settings import (
|
maps4fs/generator/background.py
CHANGED
@@ -57,36 +57,23 @@ class Background(Component):
|
|
57
57
|
os.makedirs(self.background_directory, exist_ok=True)
|
58
58
|
os.makedirs(self.water_directory, exist_ok=True)
|
59
59
|
|
60
|
-
|
61
|
-
self.output_paths = [
|
62
|
-
os.path.join(self.background_directory, f"{name}.png") for name in ELEMENTS
|
63
|
-
]
|
60
|
+
self.output_path = os.path.join(self.background_directory, f"{FULL_NAME}.png")
|
64
61
|
self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
|
65
62
|
self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
|
66
63
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
dem.preprocess()
|
81
|
-
dem.is_preview = self.is_preview(name) # type: ignore
|
82
|
-
if dem.is_preview: # type: ignore
|
83
|
-
dem.multiplier = 1
|
84
|
-
dem.auto_process = autoprocess
|
85
|
-
dem.set_output_resolution((self.rotated_size, self.rotated_size))
|
86
|
-
dem.set_dem_path(output_path)
|
87
|
-
dems.append(dem)
|
88
|
-
|
89
|
-
self.dems = dems
|
64
|
+
self.dem = DEM(
|
65
|
+
self.game,
|
66
|
+
self.map,
|
67
|
+
self.coordinates,
|
68
|
+
self.background_size,
|
69
|
+
self.rotated_size,
|
70
|
+
self.rotation,
|
71
|
+
self.map_directory,
|
72
|
+
self.logger,
|
73
|
+
)
|
74
|
+
self.dem.preprocess()
|
75
|
+
self.dem.set_output_resolution((self.rotated_size, self.rotated_size))
|
76
|
+
self.dem.set_dem_path(self.output_path)
|
90
77
|
|
91
78
|
def is_preview(self, name: str) -> bool:
|
92
79
|
"""Checks if the DEM is a preview.
|
@@ -104,21 +91,17 @@ class Background(Component):
|
|
104
91
|
as a result the DEM files will be saved, then based on them the obj files will be
|
105
92
|
generated."""
|
106
93
|
self.create_background_textures()
|
94
|
+
self.dem.process()
|
107
95
|
|
108
|
-
|
109
|
-
|
110
|
-
if not dem.is_preview: # type: ignore
|
111
|
-
shutil.copyfile(dem.dem_path, self.not_substracted_path)
|
112
|
-
self.cutout(dem.dem_path, save_path=self.not_resized_path)
|
96
|
+
shutil.copyfile(self.dem.dem_path, self.not_substracted_path)
|
97
|
+
self.cutout(self.dem.dem_path, save_path=self.not_resized_path)
|
113
98
|
|
114
99
|
if self.map.dem_settings.water_depth:
|
115
100
|
self.subtraction()
|
116
101
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
if self.game.additional_dem_name is not None:
|
121
|
-
self.make_copy(cutted_dem_path, self.game.additional_dem_name)
|
102
|
+
cutted_dem_path = self.cutout(self.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)
|
122
105
|
|
123
106
|
if self.map.background_settings.generate_background:
|
124
107
|
self.generate_obj_files()
|
@@ -149,19 +132,17 @@ class Background(Component):
|
|
149
132
|
"""
|
150
133
|
self.qgis_sequence()
|
151
134
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
epsg3857_string = dem.get_epsg3857_string()
|
156
|
-
epsg3857_string_with_margin = dem.get_epsg3857_string(add_margin=True)
|
135
|
+
north, south, east, west = self.dem.bbox
|
136
|
+
epsg3857_string = self.dem.get_epsg3857_string()
|
137
|
+
epsg3857_string_with_margin = self.dem.get_epsg3857_string(add_margin=True)
|
157
138
|
|
158
139
|
data = {
|
159
|
-
"center_latitude": dem.coordinates[0],
|
160
|
-
"center_longitude": dem.coordinates[1],
|
140
|
+
"center_latitude": self.dem.coordinates[0],
|
141
|
+
"center_longitude": self.dem.coordinates[1],
|
161
142
|
"epsg3857_string": epsg3857_string,
|
162
143
|
"epsg3857_string_with_margin": epsg3857_string_with_margin,
|
163
|
-
"height": dem.map_size,
|
164
|
-
"width": dem.map_size,
|
144
|
+
"height": self.dem.map_size,
|
145
|
+
"width": self.dem.map_size,
|
165
146
|
"north": north,
|
166
147
|
"south": south,
|
167
148
|
"east": east,
|
@@ -171,10 +152,10 @@ class Background(Component):
|
|
171
152
|
|
172
153
|
def qgis_sequence(self) -> None:
|
173
154
|
"""Generates QGIS scripts for creating bounding box layers and rasterizing them."""
|
174
|
-
qgis_layer = (f"Background_{FULL_NAME}", *self.
|
155
|
+
qgis_layer = (f"Background_{FULL_NAME}", *self.dem.get_espg3857_bbox())
|
175
156
|
qgis_layer_with_margin = (
|
176
157
|
f"Background_{FULL_NAME}_margin",
|
177
|
-
*self.
|
158
|
+
*self.dem.get_espg3857_bbox(add_margin=True),
|
178
159
|
)
|
179
160
|
self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
|
180
161
|
|
@@ -182,21 +163,20 @@ class Background(Component):
|
|
182
163
|
"""Iterates over all dems and generates 3D obj files based on DEM data.
|
183
164
|
If at least one DEM file is missing, the generation will be stopped at all.
|
184
165
|
"""
|
185
|
-
|
186
|
-
|
187
|
-
self.
|
188
|
-
|
189
|
-
|
190
|
-
return
|
166
|
+
if not os.path.isfile(self.dem.dem_path):
|
167
|
+
self.logger.warning(
|
168
|
+
"DEM file not found, generation will be stopped: %s", self.dem.dem_path
|
169
|
+
)
|
170
|
+
return
|
191
171
|
|
192
|
-
|
172
|
+
self.logger.debug("DEM file for found: %s", self.dem.dem_path)
|
193
173
|
|
194
|
-
|
195
|
-
|
196
|
-
|
174
|
+
filename = os.path.splitext(os.path.basename(self.dem.dem_path))[0]
|
175
|
+
save_path = os.path.join(self.background_directory, f"{filename}.obj")
|
176
|
+
self.logger.debug("Generating obj file in path: %s", save_path)
|
197
177
|
|
198
|
-
|
199
|
-
|
178
|
+
dem_data = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
179
|
+
self.plane_from_np(dem_data, save_path) # type: ignore
|
200
180
|
|
201
181
|
# pylint: disable=too-many-locals
|
202
182
|
def cutout(self, dem_path: str, save_path: str | None = None) -> str:
|
@@ -248,7 +228,6 @@ class Background(Component):
|
|
248
228
|
self,
|
249
229
|
dem_data: np.ndarray,
|
250
230
|
save_path: str,
|
251
|
-
is_preview: bool = False,
|
252
231
|
include_zeros: bool = True,
|
253
232
|
) -> None:
|
254
233
|
"""Generates a 3D obj file based on DEM data.
|
@@ -256,7 +235,6 @@ class Background(Component):
|
|
256
235
|
Arguments:
|
257
236
|
dem_data (np.ndarray) -- The DEM data as a numpy array.
|
258
237
|
save_path (str) -- The path where the obj file will be saved.
|
259
|
-
is_preview (bool, optional) -- If True, the preview mesh will be generated.
|
260
238
|
include_zeros (bool, optional) -- If True, the mesh will include the zero height values.
|
261
239
|
"""
|
262
240
|
resize_factor = 1 / self.map.background_settings.resize_factor
|
@@ -315,25 +293,21 @@ class Background(Component):
|
|
315
293
|
mesh.apply_transform(rotation_matrix_y)
|
316
294
|
mesh.apply_transform(rotation_matrix_z)
|
317
295
|
|
318
|
-
if
|
296
|
+
# if not include_zeros:
|
297
|
+
z_scaling_factor = 1 / self.map.dem_settings.multiplier
|
298
|
+
self.logger.debug("Z scaling factor: %s", z_scaling_factor)
|
299
|
+
mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
|
300
|
+
|
301
|
+
mesh.export(save_path)
|
302
|
+
self.logger.debug("Obj file saved: %s", save_path)
|
303
|
+
|
304
|
+
if include_zeros:
|
319
305
|
# Simplify the preview mesh to reduce the size of the file.
|
320
306
|
mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
|
321
307
|
|
322
308
|
# Apply scale to make the preview mesh smaller in the UI.
|
323
309
|
mesh.apply_scale([0.5, 0.5, 0.5])
|
324
310
|
self.mesh_to_stl(mesh)
|
325
|
-
else:
|
326
|
-
if not include_zeros:
|
327
|
-
multiplier = self.map.dem_settings.multiplier
|
328
|
-
if multiplier != 1:
|
329
|
-
z_scaling_factor = 1 / multiplier
|
330
|
-
else:
|
331
|
-
z_scaling_factor = 1 / 2**5
|
332
|
-
self.logger.debug("Z scaling factor: %s", z_scaling_factor)
|
333
|
-
mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
|
334
|
-
|
335
|
-
mesh.export(save_path)
|
336
|
-
self.logger.debug("Obj file saved: %s", save_path)
|
337
311
|
|
338
312
|
def mesh_to_stl(self, mesh: trimesh.Trimesh) -> None:
|
339
313
|
"""Converts the mesh to an STL file and saves it in the previews directory.
|
@@ -358,25 +332,22 @@ class Background(Component):
|
|
358
332
|
list[str] -- A list of paths to the previews.
|
359
333
|
"""
|
360
334
|
preview_paths = self.dem_previews(self.game.dem_file_path(self.map_directory))
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
|
379
|
-
preview_paths.append(background_dem_preview_path)
|
335
|
+
|
336
|
+
background_dem_preview_path = os.path.join(self.previews_directory, "background_dem.png")
|
337
|
+
background_dem_preview_image = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED)
|
338
|
+
|
339
|
+
background_dem_preview_image = cv2.resize(
|
340
|
+
background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
|
341
|
+
)
|
342
|
+
background_dem_preview_image = cv2.normalize( # type: ignore
|
343
|
+
background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
|
344
|
+
)
|
345
|
+
background_dem_preview_image = cv2.cvtColor(
|
346
|
+
background_dem_preview_image, cv2.COLOR_GRAY2BGR
|
347
|
+
)
|
348
|
+
|
349
|
+
cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
|
350
|
+
preview_paths.append(background_dem_preview_path)
|
380
351
|
|
381
352
|
if self.stl_preview_path:
|
382
353
|
preview_paths.append(self.stl_preview_path)
|
@@ -525,18 +496,15 @@ class Background(Component):
|
|
525
496
|
bool
|
526
497
|
)
|
527
498
|
|
528
|
-
|
529
|
-
if FULL_PREVIEW_NAME in output_path:
|
530
|
-
continue
|
531
|
-
dem_image = cv2.imread(output_path, cv2.IMREAD_UNCHANGED)
|
499
|
+
dem_image = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
|
532
500
|
|
533
|
-
|
534
|
-
|
535
|
-
|
501
|
+
# Create a mask where water_resources_image is 255 (or not 0)
|
502
|
+
# Subtract water_depth from dem_image where mask is True
|
503
|
+
dem_image[mask] = dem_image[mask] - self.map.dem_settings.water_depth
|
536
504
|
|
537
|
-
|
538
|
-
|
539
|
-
|
505
|
+
# Save the modified dem_image back to the output path
|
506
|
+
cv2.imwrite(self.output_path, dem_image)
|
507
|
+
self.logger.debug("Water depth subtracted from DEM data: %s", self.output_path)
|
540
508
|
|
541
509
|
def generate_water_resources_obj(self) -> None:
|
542
510
|
"""Generates 3D obj files based on water resources data."""
|
@@ -550,9 +518,7 @@ class Background(Component):
|
|
550
518
|
plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
|
551
519
|
).astype(np.uint8)
|
552
520
|
plane_save_path = os.path.join(self.water_directory, "plane_water.obj")
|
553
|
-
self.plane_from_np(
|
554
|
-
dilated_plane_water, plane_save_path, is_preview=False, include_zeros=False
|
555
|
-
)
|
521
|
+
self.plane_from_np(dilated_plane_water, plane_save_path, include_zeros=False)
|
556
522
|
|
557
523
|
# Single channeled 16 bit DEM image of terrain.
|
558
524
|
background_dem = cv2.imread(self.not_substracted_path, cv2.IMREAD_UNCHANGED)
|
@@ -570,6 +536,4 @@ class Background(Component):
|
|
570
536
|
elevated_water = np.where(mask, background_dem, elevated_water)
|
571
537
|
elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
|
572
538
|
|
573
|
-
self.plane_from_np(
|
574
|
-
elevated_water, elevated_save_path, is_preview=False, include_zeros=False
|
575
|
-
)
|
539
|
+
self.plane_from_np(elevated_water, elevated_save_path, include_zeros=False)
|
maps4fs/generator/dem.py
CHANGED
@@ -9,7 +9,7 @@ import numpy as np
|
|
9
9
|
from pympler import asizeof # type: ignore
|
10
10
|
|
11
11
|
from maps4fs.generator.component import Component
|
12
|
-
from maps4fs.generator.dtm import DTMProvider
|
12
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
13
13
|
|
14
14
|
|
15
15
|
# pylint: disable=R0903, R0902
|
@@ -57,8 +57,6 @@ class DEM(Component):
|
|
57
57
|
self.blur_radius,
|
58
58
|
)
|
59
59
|
|
60
|
-
self.auto_process = self.map.dem_settings.auto_process
|
61
|
-
|
62
60
|
self.dtm_provider: DTMProvider = self.map.dtm_provider( # type: ignore
|
63
61
|
coordinates=self.coordinates,
|
64
62
|
user_settings=self.map.dtm_provider_settings,
|
@@ -181,11 +179,7 @@ class DEM(Component):
|
|
181
179
|
resampled_data.dtype,
|
182
180
|
)
|
183
181
|
|
184
|
-
if self.
|
185
|
-
self.logger.debug("Auto processing is enabled, will normalize DEM data.")
|
186
|
-
resampled_data = self._normalize_dem(resampled_data)
|
187
|
-
else:
|
188
|
-
self.logger.debug("Auto processing is disabled, DEM data will not be normalized.")
|
182
|
+
if self.multiplier != 1:
|
189
183
|
resampled_data = resampled_data * self.multiplier
|
190
184
|
|
191
185
|
self.logger.debug(
|
File without changes
|
@@ -4,10 +4,7 @@ and specific settings for downloading and processing the data."""
|
|
4
4
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
|
-
import gzip
|
8
|
-
import math
|
9
7
|
import os
|
10
|
-
import shutil
|
11
8
|
from typing import Type
|
12
9
|
|
13
10
|
import numpy as np
|
@@ -263,71 +260,3 @@ class DTMProvider:
|
|
263
260
|
raise ValueError("No data in the tile.")
|
264
261
|
|
265
262
|
return data
|
266
|
-
|
267
|
-
|
268
|
-
class SRTM30Provider(DTMProvider):
|
269
|
-
"""Provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
|
270
|
-
|
271
|
-
_code = "srtm30"
|
272
|
-
_name = "SRTM 30 m"
|
273
|
-
_region = "Global"
|
274
|
-
_icon = "🌎"
|
275
|
-
_resolution = 30.0
|
276
|
-
|
277
|
-
_url = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
|
278
|
-
|
279
|
-
_author = "[iwatkot](https://github.com/iwatkot)"
|
280
|
-
|
281
|
-
def __init__(self, *args, **kwargs):
|
282
|
-
super().__init__(*args, **kwargs)
|
283
|
-
self.hgt_directory = os.path.join(self._tile_directory, "hgt")
|
284
|
-
self.gz_directory = os.path.join(self._tile_directory, "gz")
|
285
|
-
os.makedirs(self.hgt_directory, exist_ok=True)
|
286
|
-
os.makedirs(self.gz_directory, exist_ok=True)
|
287
|
-
|
288
|
-
def get_tile_parameters(self, *args, **kwargs) -> dict[str, str]:
|
289
|
-
"""Returns latitude band and tile name for SRTM tile from coordinates.
|
290
|
-
|
291
|
-
Arguments:
|
292
|
-
lat (float): Latitude.
|
293
|
-
lon (float): Longitude.
|
294
|
-
|
295
|
-
Returns:
|
296
|
-
dict: Tile parameters.
|
297
|
-
"""
|
298
|
-
lat, lon = args
|
299
|
-
|
300
|
-
tile_latitude = math.floor(lat)
|
301
|
-
tile_longitude = math.floor(lon)
|
302
|
-
|
303
|
-
latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
|
304
|
-
if lon < 0:
|
305
|
-
tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
|
306
|
-
else:
|
307
|
-
tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
|
308
|
-
|
309
|
-
self.logger.debug(
|
310
|
-
"Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
|
311
|
-
)
|
312
|
-
return {"latitude_band": latitude_band, "tile_name": tile_name}
|
313
|
-
|
314
|
-
def get_numpy(self) -> np.ndarray:
|
315
|
-
"""Get numpy array of the tile.
|
316
|
-
|
317
|
-
Returns:
|
318
|
-
np.ndarray: Numpy array of the tile.
|
319
|
-
"""
|
320
|
-
tile_parameters = self.get_tile_parameters(*self.coordinates)
|
321
|
-
tile_name = tile_parameters["tile_name"]
|
322
|
-
decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
|
323
|
-
|
324
|
-
if not os.path.isfile(decompressed_tile_path):
|
325
|
-
compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
|
326
|
-
if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
|
327
|
-
raise FileNotFoundError(f"Tile {tile_name} not found.")
|
328
|
-
|
329
|
-
with gzip.open(compressed_tile_path, "rb") as f_in:
|
330
|
-
with open(decompressed_tile_path, "wb") as f_out:
|
331
|
-
shutil.copyfileobj(f_in, f_out)
|
332
|
-
|
333
|
-
return self.extract_roi(decompressed_tile_path)
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""This module contains provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
|
2
|
+
|
3
|
+
import gzip
|
4
|
+
import math
|
5
|
+
import os
|
6
|
+
import shutil
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
|
10
|
+
from maps4fs.generator.dtm.dtm import DTMProvider
|
11
|
+
|
12
|
+
|
13
|
+
class SRTM30Provider(DTMProvider):
|
14
|
+
"""Provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
|
15
|
+
|
16
|
+
_code = "srtm30"
|
17
|
+
_name = "SRTM 30 m"
|
18
|
+
_region = "Global"
|
19
|
+
_icon = "🌎"
|
20
|
+
_resolution = 30.0
|
21
|
+
|
22
|
+
_url = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
|
23
|
+
|
24
|
+
_author = "[iwatkot](https://github.com/iwatkot)"
|
25
|
+
|
26
|
+
_instructions = (
|
27
|
+
"ℹ️ Set the Multiplier value in the DEM Settings, when using this DTM provider. "
|
28
|
+
"Otherwise, the dem file will contain values in meters exactly as on Earth "
|
29
|
+
"and you probably won't see any terrain by eye. "
|
30
|
+
"Note that the multiplier value may be big enough to make the terrain visible."
|
31
|
+
)
|
32
|
+
|
33
|
+
def __init__(self, *args, **kwargs):
|
34
|
+
super().__init__(*args, **kwargs)
|
35
|
+
self.hgt_directory = os.path.join(self._tile_directory, "hgt")
|
36
|
+
self.gz_directory = os.path.join(self._tile_directory, "gz")
|
37
|
+
os.makedirs(self.hgt_directory, exist_ok=True)
|
38
|
+
os.makedirs(self.gz_directory, exist_ok=True)
|
39
|
+
|
40
|
+
def get_tile_parameters(self, *args, **kwargs) -> dict[str, str]:
|
41
|
+
"""Returns latitude band and tile name for SRTM tile from coordinates.
|
42
|
+
|
43
|
+
Arguments:
|
44
|
+
lat (float): Latitude.
|
45
|
+
lon (float): Longitude.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
dict: Tile parameters.
|
49
|
+
"""
|
50
|
+
lat, lon = args
|
51
|
+
|
52
|
+
tile_latitude = math.floor(lat)
|
53
|
+
tile_longitude = math.floor(lon)
|
54
|
+
|
55
|
+
latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
|
56
|
+
if lon < 0:
|
57
|
+
tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
|
58
|
+
else:
|
59
|
+
tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
|
60
|
+
|
61
|
+
self.logger.debug(
|
62
|
+
"Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
|
63
|
+
)
|
64
|
+
return {"latitude_band": latitude_band, "tile_name": tile_name}
|
65
|
+
|
66
|
+
def get_numpy(self) -> np.ndarray:
|
67
|
+
"""Get numpy array of the tile.
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
np.ndarray: Numpy array of the tile.
|
71
|
+
"""
|
72
|
+
tile_parameters = self.get_tile_parameters(*self.coordinates)
|
73
|
+
tile_name = tile_parameters["tile_name"]
|
74
|
+
decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
|
75
|
+
|
76
|
+
if not os.path.isfile(decompressed_tile_path):
|
77
|
+
compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
|
78
|
+
if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
|
79
|
+
raise FileNotFoundError(f"Tile {tile_name} not found.")
|
80
|
+
|
81
|
+
with gzip.open(compressed_tile_path, "rb") as f_in:
|
82
|
+
with open(decompressed_tile_path, "wb") as f_out:
|
83
|
+
shutil.copyfileobj(f_in, f_out)
|
84
|
+
|
85
|
+
return self.extract_roi(decompressed_tile_path)
|
@@ -0,0 +1,323 @@
|
|
1
|
+
"""This module contains provider of USGS 1m data."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from datetime import datetime
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
import rasterio # type: ignore
|
8
|
+
import requests
|
9
|
+
from rasterio._warp import Resampling # type: ignore # pylint: disable=E0611
|
10
|
+
from rasterio.merge import merge # type: ignore
|
11
|
+
from rasterio.warp import calculate_default_transform, reproject # type: ignore
|
12
|
+
from rasterio.windows import from_bounds # type: ignore
|
13
|
+
|
14
|
+
from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
|
15
|
+
|
16
|
+
|
17
|
+
class USGS1mProviderSettings(DTMProviderSettings):
|
18
|
+
"""Settings for the USGS 1m provider."""
|
19
|
+
|
20
|
+
max_local_elevation: int = 255
|
21
|
+
|
22
|
+
|
23
|
+
# pylint: disable=W0223
|
24
|
+
class USGS1mProvider(DTMProvider):
|
25
|
+
"""Provider of USGS."""
|
26
|
+
|
27
|
+
_code = "USGS1m"
|
28
|
+
_name = "USGS 1m"
|
29
|
+
_region = "USA"
|
30
|
+
_icon = "🇺🇸"
|
31
|
+
_resolution = 1
|
32
|
+
_data: np.ndarray | None = None
|
33
|
+
_settings = USGS1mProviderSettings
|
34
|
+
_author = "[ZenJakey](https://github.com/ZenJakey)"
|
35
|
+
_is_community = True
|
36
|
+
|
37
|
+
_url = (
|
38
|
+
"https://tnmaccess.nationalmap.gov/api/v1/products?prodFormats=GeoTIFF,IMG&prodExtents="
|
39
|
+
"10000 x 10000 meter&datasets=Digital Elevation Model (DEM) 1 meter&polygon="
|
40
|
+
)
|
41
|
+
|
42
|
+
def __init__(self, *args, **kwargs):
|
43
|
+
super().__init__(*args, **kwargs)
|
44
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
45
|
+
self.shared_tiff_path = os.path.join(self._tile_directory, "shared")
|
46
|
+
os.makedirs(self.shared_tiff_path, exist_ok=True)
|
47
|
+
self.output_path = os.path.join(self._tile_directory, f"timestamp_{timestamp}")
|
48
|
+
os.makedirs(self.output_path, exist_ok=True)
|
49
|
+
|
50
|
+
def get_download_urls(self) -> list[str]:
|
51
|
+
"""Get download URLs of the GeoTIFF files from the USGS API.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
list: List of download URLs.
|
55
|
+
"""
|
56
|
+
urls = []
|
57
|
+
try:
|
58
|
+
# Make the GET request
|
59
|
+
(north, south, east, west) = self.get_bbox()
|
60
|
+
response = requests.get( # pylint: disable=W3101
|
61
|
+
self.url # type: ignore
|
62
|
+
+ f"{west} {south},{east} {south},{east} {north},{west} {north},{west} {south}&="
|
63
|
+
)
|
64
|
+
self.logger.debug("Getting file locations from USGS...")
|
65
|
+
|
66
|
+
# Check if the request was successful (HTTP status code 200)
|
67
|
+
if response.status_code == 200:
|
68
|
+
# Parse the JSON response
|
69
|
+
json_data = response.json()
|
70
|
+
items = json_data["items"]
|
71
|
+
for item in items:
|
72
|
+
urls.append(item["downloadURL"])
|
73
|
+
self.download_tif_files(urls)
|
74
|
+
else:
|
75
|
+
self.logger.error("Failed to get data. HTTP Status Code: %s", response.status_code)
|
76
|
+
except requests.exceptions.RequestException as e:
|
77
|
+
self.logger.error("Failed to get data. Error: %s", e)
|
78
|
+
self.logger.debug("Received %s urls", len(urls))
|
79
|
+
return urls
|
80
|
+
|
81
|
+
def download_tif_files(self, urls: list[str]) -> list[str]:
|
82
|
+
"""Download GeoTIFF files from the given URLs.
|
83
|
+
|
84
|
+
Arguments:
|
85
|
+
urls (list): List of URLs to download GeoTIFF files from.
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
list: List of paths to the downloaded GeoTIFF files.
|
89
|
+
"""
|
90
|
+
tif_files = []
|
91
|
+
for url in urls:
|
92
|
+
file_name = os.path.basename(url)
|
93
|
+
self.logger.debug("Retrieving TIFF: %s", file_name)
|
94
|
+
file_path = os.path.join(self.shared_tiff_path, file_name)
|
95
|
+
if not os.path.exists(file_path):
|
96
|
+
try:
|
97
|
+
# Send a GET request to the file URL
|
98
|
+
response = requests.get(url, stream=True) # pylint: disable=W3101
|
99
|
+
response.raise_for_status() # Raise an error for HTTP status codes 4xx/5xx
|
100
|
+
|
101
|
+
# Write the content of the response to the file
|
102
|
+
with open(file_path, "wb") as file:
|
103
|
+
for chunk in response.iter_content(chunk_size=8192): # Download in chunks
|
104
|
+
file.write(chunk)
|
105
|
+
self.logger.info("File downloaded successfully: %s", file_path)
|
106
|
+
except requests.exceptions.RequestException as e:
|
107
|
+
self.logger.error("Failed to download file: %s", e)
|
108
|
+
else:
|
109
|
+
self.logger.debug("File already exists: %s", file_name)
|
110
|
+
|
111
|
+
tif_files.append(file_path)
|
112
|
+
return tif_files
|
113
|
+
|
114
|
+
def merge_geotiff(self, input_files: list[str], output_file: str) -> None:
|
115
|
+
"""Merge multiple GeoTIFF files into a single GeoTIFF file.
|
116
|
+
|
117
|
+
Arguments:
|
118
|
+
input_files (list): List of input GeoTIFF files to merge.
|
119
|
+
output_file (str): Path to save the merged GeoTIFF file.
|
120
|
+
"""
|
121
|
+
# Open all input GeoTIFF files as datasets
|
122
|
+
self.logger.debug("Merging tiff files...")
|
123
|
+
datasets = [rasterio.open(file) for file in input_files]
|
124
|
+
|
125
|
+
# Merge datasets
|
126
|
+
mosaic, out_transform = merge(datasets)
|
127
|
+
|
128
|
+
# Get metadata from the first file and update it for the output
|
129
|
+
out_meta = datasets[0].meta.copy()
|
130
|
+
out_meta.update(
|
131
|
+
{
|
132
|
+
"driver": "GTiff",
|
133
|
+
"height": mosaic.shape[1],
|
134
|
+
"width": mosaic.shape[2],
|
135
|
+
"transform": out_transform,
|
136
|
+
"count": mosaic.shape[0], # Number of bands
|
137
|
+
}
|
138
|
+
)
|
139
|
+
|
140
|
+
# Write merged GeoTIFF to the output file
|
141
|
+
with rasterio.open(output_file, "w", **out_meta) as dest:
|
142
|
+
dest.write(mosaic)
|
143
|
+
|
144
|
+
self.logger.debug("GeoTIFF images merged successfully into %s", output_file)
|
145
|
+
|
146
|
+
def reproject_geotiff(self, input_tiff: str, output_tiff: str, target_crs: str) -> None:
|
147
|
+
"""Reproject a GeoTIFF file to a new coordinate reference system (CRS).
|
148
|
+
|
149
|
+
Arguments:
|
150
|
+
input_tiff (str): Path to the input GeoTIFF file.
|
151
|
+
output_tiff (str): Path to save the reprojected GeoTIFF file.
|
152
|
+
target_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
|
153
|
+
"""
|
154
|
+
# Open the source GeoTIFF
|
155
|
+
self.logger.debug("Reprojecting GeoTIFF to %s CRS...", target_crs)
|
156
|
+
with rasterio.open(input_tiff) as src:
|
157
|
+
# Get the transform, width, and height of the target CRS
|
158
|
+
transform, width, height = calculate_default_transform(
|
159
|
+
src.crs, target_crs, src.width, src.height, *src.bounds
|
160
|
+
)
|
161
|
+
|
162
|
+
# Update the metadata for the target GeoTIFF
|
163
|
+
kwargs = src.meta.copy()
|
164
|
+
kwargs.update(
|
165
|
+
{"crs": target_crs, "transform": transform, "width": width, "height": height}
|
166
|
+
)
|
167
|
+
|
168
|
+
# Open the destination GeoTIFF file and reproject
|
169
|
+
with rasterio.open(output_tiff, "w", **kwargs) as dst:
|
170
|
+
for i in range(1, src.count + 1): # Iterate over all raster bands
|
171
|
+
reproject(
|
172
|
+
source=rasterio.band(src, i),
|
173
|
+
destination=rasterio.band(dst, i),
|
174
|
+
src_transform=src.transform,
|
175
|
+
src_crs=src.crs,
|
176
|
+
dst_transform=transform,
|
177
|
+
dst_crs=target_crs,
|
178
|
+
resampling=Resampling.nearest, # Choose resampling method
|
179
|
+
)
|
180
|
+
self.logger.debug("Reprojected GeoTIFF saved to %s", output_tiff)
|
181
|
+
|
182
|
+
def extract_roi(self, input_tiff: str) -> np.ndarray: # pylint: disable=W0237
|
183
|
+
"""
|
184
|
+
Crop a GeoTIFF based on given geographic bounding box and save to a new file.
|
185
|
+
|
186
|
+
Arguments:
|
187
|
+
input_tiff (str): Path to the input GeoTIFF file.
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
np.ndarray: Numpy array of the cropped GeoTIFF.
|
191
|
+
"""
|
192
|
+
self.logger.debug("Extracting ROI...")
|
193
|
+
# Open the input GeoTIFF
|
194
|
+
with rasterio.open(input_tiff) as src:
|
195
|
+
|
196
|
+
# Create a rasterio window from the bounding box
|
197
|
+
(north, south, east, west) = self.get_bbox()
|
198
|
+
window = from_bounds(west, south, east, north, transform=src.transform)
|
199
|
+
|
200
|
+
data = src.read(1, window=window)
|
201
|
+
self.logger.debug("Extracted ROI")
|
202
|
+
return data
|
203
|
+
|
204
|
+
# pylint: disable=R0914, R0917, R0913
|
205
|
+
def convert_geotiff_to_geotiff(
|
206
|
+
self,
|
207
|
+
input_tiff: str,
|
208
|
+
output_tiff: str,
|
209
|
+
min_height: float,
|
210
|
+
max_height: float,
|
211
|
+
target_crs: str,
|
212
|
+
) -> None:
|
213
|
+
"""
|
214
|
+
Convert a GeoTIFF to a scaled GeoTIFF with UInt16 values using a specific coordinate
|
215
|
+
system and output size.
|
216
|
+
|
217
|
+
Arguments:
|
218
|
+
input_tiff (str): Path to the input GeoTIFF file.
|
219
|
+
output_tiff (str): Path to save the output GeoTIFF file.
|
220
|
+
min_height (float): Minimum terrain height (input range).
|
221
|
+
max_height (float): Maximum terrain height (input range).
|
222
|
+
target_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
|
223
|
+
"""
|
224
|
+
# Open the input GeoTIFF file
|
225
|
+
self.logger.debug("Converting to uint16")
|
226
|
+
with rasterio.open(input_tiff) as src:
|
227
|
+
# Ensure the input CRS matches the target CRS (reprojection may be required)
|
228
|
+
if str(src.crs) != str(target_crs):
|
229
|
+
raise ValueError(
|
230
|
+
f"The GeoTIFF CRS is {src.crs}, but the target CRS is {target_crs}. "
|
231
|
+
"Reprojection may be required."
|
232
|
+
)
|
233
|
+
|
234
|
+
# Read the data from the first band
|
235
|
+
data = src.read(1) # Assuming the input GeoTIFF has only a single band
|
236
|
+
|
237
|
+
# Identify the input file's NoData value
|
238
|
+
input_nodata = src.nodata
|
239
|
+
if input_nodata is None:
|
240
|
+
input_nodata = -999999.0 # Default fallback if no NoData value is defined
|
241
|
+
nodata_value = 0
|
242
|
+
# Replace NoData values (e.g., -999999.0) with the new NoData value
|
243
|
+
# (e.g., 65535 for UInt16)
|
244
|
+
data[data == input_nodata] = nodata_value
|
245
|
+
|
246
|
+
# Scale the data to the 0–65535 range (UInt16), avoiding NoData areas
|
247
|
+
scaled_data = np.clip(
|
248
|
+
(data - min_height) * (65535 / (max_height - min_height)), 0, 65535
|
249
|
+
).astype(np.uint16)
|
250
|
+
scaled_data[data == nodata_value] = (
|
251
|
+
nodata_value # Preserve NoData value in the scaled array
|
252
|
+
)
|
253
|
+
|
254
|
+
# Compute the proper transform to ensure consistency
|
255
|
+
# Get the original transform, width, and height
|
256
|
+
transform = src.transform
|
257
|
+
width = src.width
|
258
|
+
height = src.height
|
259
|
+
left, bottom, right, top = src.bounds
|
260
|
+
|
261
|
+
# Adjust the transform matrix to make sure bounds and transform align correctly
|
262
|
+
transform = rasterio.transform.from_bounds(left, bottom, right, top, width, height)
|
263
|
+
|
264
|
+
# Prepare metadata for the output GeoTIFF
|
265
|
+
metadata = src.meta.copy()
|
266
|
+
metadata.update(
|
267
|
+
{
|
268
|
+
"dtype": rasterio.uint16, # Update dtype for uint16
|
269
|
+
"crs": target_crs, # Update CRS if needed
|
270
|
+
"nodata": nodata_value, # Set the new NoData value
|
271
|
+
"transform": transform, # Use the updated, consistent transform
|
272
|
+
}
|
273
|
+
)
|
274
|
+
|
275
|
+
# Write the scaled data to the output GeoTIFF
|
276
|
+
with rasterio.open(output_tiff, "w", **metadata) as dst:
|
277
|
+
dst.write(scaled_data, 1) # Write the first band
|
278
|
+
|
279
|
+
self.logger.debug(
|
280
|
+
"GeoTIFF successfully converted and saved to %s, with nodata value: %s.",
|
281
|
+
output_tiff,
|
282
|
+
nodata_value,
|
283
|
+
)
|
284
|
+
|
285
|
+
def generate_data(self) -> np.ndarray:
|
286
|
+
"""Generate data from the USGS 1m provider.
|
287
|
+
|
288
|
+
Returns:
|
289
|
+
np.ndarray: Numpy array of the data.
|
290
|
+
"""
|
291
|
+
download_urls = self.get_download_urls()
|
292
|
+
all_tif_files = self.download_tif_files(download_urls)
|
293
|
+
self.merge_geotiff(all_tif_files, os.path.join(self.output_path, "merged.tif"))
|
294
|
+
self.reproject_geotiff(
|
295
|
+
os.path.join(self.output_path, "merged.tif"),
|
296
|
+
os.path.join(self.output_path, "reprojected.tif"),
|
297
|
+
"EPSG:4326",
|
298
|
+
)
|
299
|
+
self.convert_geotiff_to_geotiff(
|
300
|
+
os.path.join(self.output_path, "reprojected.tif"),
|
301
|
+
os.path.join(self.output_path, "translated.tif"),
|
302
|
+
min_height=0,
|
303
|
+
max_height=self.user_settings.max_local_elevation, # type: ignore
|
304
|
+
target_crs="EPSG:4326",
|
305
|
+
)
|
306
|
+
return self.extract_roi(os.path.join(self.output_path, "translated.tif"))
|
307
|
+
|
308
|
+
def get_numpy(self) -> np.ndarray:
|
309
|
+
"""Get numpy array of the tile.
|
310
|
+
|
311
|
+
Returns:
|
312
|
+
np.ndarray: Numpy array of the tile.
|
313
|
+
"""
|
314
|
+
if not self.user_settings:
|
315
|
+
raise ValueError("user_settings is 'none'")
|
316
|
+
if self.user_settings.max_local_elevation <= 0: # type: ignore
|
317
|
+
raise ValueError(
|
318
|
+
"Entered 'max_local_elevation' value is unable to be used. "
|
319
|
+
"Use a value greater than 0."
|
320
|
+
)
|
321
|
+
if not self._data:
|
322
|
+
self._data = self.generate_data()
|
323
|
+
return self._data
|
maps4fs/generator/i3d.py
CHANGED
@@ -81,20 +81,6 @@ class I3d(Component):
|
|
81
81
|
|
82
82
|
root = tree.getroot()
|
83
83
|
for map_elem in root.iter("Scene"):
|
84
|
-
for terrain_elem in map_elem.iter("TerrainTransformGroup"):
|
85
|
-
if self.map.dem_settings.auto_process:
|
86
|
-
terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
|
87
|
-
self.logger.debug(
|
88
|
-
"heightScale attribute set to %s in TerrainTransformGroup element.",
|
89
|
-
DEFAULT_HEIGHT_SCALE,
|
90
|
-
)
|
91
|
-
else:
|
92
|
-
self.logger.debug(
|
93
|
-
"Auto process is disabled, skipping the heightScale attribute update."
|
94
|
-
)
|
95
|
-
|
96
|
-
self.logger.debug("TerrainTransformGroup element updated in I3D file.")
|
97
|
-
|
98
84
|
sun_elem = map_elem.find(".//Light[@name='sun']")
|
99
85
|
|
100
86
|
if sun_elem is not None:
|
@@ -111,6 +97,10 @@ class I3d(Component):
|
|
111
97
|
)
|
112
98
|
|
113
99
|
if self.map_size > 4096:
|
100
|
+
terrain_elem = root.find(".//TerrainTransformGroup")
|
101
|
+
if terrain_elem is None:
|
102
|
+
self.logger.warning("TerrainTransformGroup element not found in I3D file.")
|
103
|
+
return
|
114
104
|
displacement_layer = terrain_elem.find(".//DisplacementLayer") # pylint: disable=W0631
|
115
105
|
|
116
106
|
if displacement_layer is not None:
|
maps4fs/generator/map.py
CHANGED
@@ -8,7 +8,7 @@ import shutil
|
|
8
8
|
from typing import Any, Generator
|
9
9
|
|
10
10
|
from maps4fs.generator.component import Component
|
11
|
-
from maps4fs.generator.dtm import DTMProvider, DTMProviderSettings
|
11
|
+
from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
|
12
12
|
from maps4fs.generator.game import Game
|
13
13
|
from maps4fs.generator.settings import (
|
14
14
|
BackgroundSettings,
|
maps4fs/generator/settings.py
CHANGED
@@ -58,7 +58,6 @@ class DEMSettings(SettingsModel):
|
|
58
58
|
"""Represents the advanced settings for DEM component.
|
59
59
|
|
60
60
|
Attributes:
|
61
|
-
auto_process (bool): use the auto preset to change the multiplier.
|
62
61
|
multiplier (int): multiplier for the heightmap, every pixel will be multiplied by this
|
63
62
|
value.
|
64
63
|
blur_radius (int): radius of the blur filter.
|
@@ -67,7 +66,6 @@ class DEMSettings(SettingsModel):
|
|
67
66
|
is present.
|
68
67
|
"""
|
69
68
|
|
70
|
-
auto_process: bool = True
|
71
69
|
multiplier: int = 1
|
72
70
|
blur_radius: int = 35
|
73
71
|
plateau: int = 0
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: maps4fs
|
3
|
-
Version: 1.5.
|
3
|
+
Version: 1.5.9
|
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
|
@@ -48,6 +48,7 @@ Requires-Dist: pydantic
|
|
48
48
|
<a href="#Expert-settings">Expert settings</a> •
|
49
49
|
<a href="#Resources">Resources</a> •
|
50
50
|
<a href="#Bugs-and-feature-requests">Bugs and feature requests</a><br>
|
51
|
+
<a href="#DTM-Providers">DTM Providers</a> •
|
51
52
|
<a href="#Special-thanks">Special thanks</a>
|
52
53
|
</p>
|
53
54
|
|
@@ -69,6 +70,7 @@ Requires-Dist: pydantic
|
|
69
70
|
|
70
71
|
🗺️ Supports 2x2, 4x4, 8x8, 16x16 and any custom size maps<br>
|
71
72
|
🔄 Support map rotation 🆕<br>
|
73
|
+
🌐 Supports custom [DTM Providers](#DTM-Providers) 🆕<br>
|
72
74
|
🌾 Automatically generates fields 🆕<br>
|
73
75
|
🌽 Automatically generates farmlands 🆕<br>
|
74
76
|
🌿 Automatically generates decorative foliage 🆕<br>
|
@@ -475,8 +477,6 @@ You can also apply some advanced settings to the map generation process. Note th
|
|
475
477
|
|
476
478
|
### DEM Advanced settings
|
477
479
|
|
478
|
-
- Auto process: the tool will automatically try to find suitable multiplier. As a result, the DEM image WILL not match real world values. If this option is disabled, you'll probably see completely black DEM image, but it's not empty. It's just you can't see the values of 16-bit image by eye, because they're too small. Learn more what's DEM image and how to work with it in [docs](docs/dem.md). By default, it's set to True.
|
479
|
-
|
480
480
|
- Multiplier: the height of the map is multiplied by this value. So the DEM map is just a 16-bit grayscale image, which means that the maximum available value there is 65535, while the actual difference between the deepest and the highest point on Earth is about 20 km. Just note that this setting mostly does not matter, because you can always adjust it in the Giants Editor, learn more about the DEM file and the heightScale parameter in [docs](docs/dem.md). By default, it's set to 1.
|
481
481
|
|
482
482
|
- Blur radius: the radius of the Gaussian blur filter applied to the DEM map. By default, it's set to 21. This filter just makes the DEM map smoother, so the height transitions will be more natural. You can set it to 1 to disable the filter, but it will result in a Minecraft-like map.
|
@@ -549,6 +549,15 @@ To create a basic map, you only need the Giants Editor. But if you want to creat
|
|
549
549
|
➡️ Please, before creating an issue or asking some questions, check the [FAQ](docs/FAQ.md) section.<br>
|
550
550
|
If you find a bug or have an idea for a new feature, please create an issue [here](https://github.com/iwatkot/maps4fs/issues) or contact me directly on [Telegram](https://t.me/iwatkot) or on Discord: `iwatkot`.
|
551
551
|
|
552
|
+
## DTM Providers
|
553
|
+
|
554
|
+
The generator supports adding the own DTM providers, please refer to the [DTM Providers](docs/dtm_providers.md) section to learn how to add the custom DTM provider.
|
555
|
+
|
556
|
+
### Supported DTM providers
|
557
|
+
|
558
|
+
- [SRTM 30m](https://dwtkns.com/srtm30m/) - the 30 meters resolution DEM data from the SRTM mission for the whole world.
|
559
|
+
- [USGS 1m](https://portal.opentopography.org/raster?opentopoID=OTNED.012021.4269.3) - the 1-meter resolution DEM data from the USGS for the USA. Developed by [ZenJakey](https://github.com/ZenJakey).
|
560
|
+
|
552
561
|
## Special thanks
|
553
562
|
|
554
563
|
Of course, first of all, thanks to the direct [contributors](https://github.com/iwatkot/maps4fs/graphs/contributors) of the project.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
maps4fs/__init__.py,sha256=WbT36EzJ_74GN0RUUrLIYECdSdtRiZaxKl17KUt7pjA,492
|
2
|
+
maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
|
3
|
+
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
4
|
+
maps4fs/generator/background.py,sha256=moTsEJM-hZgHQQiBjFVTWBKgPMqxup-58EErh4bq_dE,21342
|
5
|
+
maps4fs/generator/component.py,sha256=RtXruvT4Fxfr7_xo9Bi-i3IIWcPd5QQOSpYJ_cNC49o,20408
|
6
|
+
maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
|
7
|
+
maps4fs/generator/dem.py,sha256=vGz-gUg_JArqHO7qewdnSR7WiF7ciUzY-OSqOluUDWw,12304
|
8
|
+
maps4fs/generator/game.py,sha256=QHgVnyGYvEnfwGZ84-u-dpbCRr3UeVVqBbrwr5WG8dE,7992
|
9
|
+
maps4fs/generator/grle.py,sha256=u8ZwSs313PIOkH_0B_O2tVTaZ-eYNkc30eKGtBxWzTM,17846
|
10
|
+
maps4fs/generator/i3d.py,sha256=FLVlj0g90IXRuaRARD1HTnufsLpuaa5kHKdiME-LUZY,24329
|
11
|
+
maps4fs/generator/map.py,sha256=-iUFGqe11Df-oUrxhcnJzRZ0o6NZKRQ_blTq1h1Wezg,9287
|
12
|
+
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
13
|
+
maps4fs/generator/satellite.py,sha256=Qnb6XxmXKnHdHKVMb9mJ3vDGtGkDHCOv_81hrrXdx3k,3660
|
14
|
+
maps4fs/generator/settings.py,sha256=NWuK76ICr8gURQnzePat4JH9w-iACbQEKQebqu51gBE,4470
|
15
|
+
maps4fs/generator/texture.py,sha256=sErusfv1AqQfP-veMrZ921Tz8DnGEhfB4ucggMmKrD4,31231
|
16
|
+
maps4fs/generator/dtm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
+
maps4fs/generator/dtm/dtm.py,sha256=THQ3RWVL9ut7A4omS8hEog-oQSSwYV0JcDMe0Iiw4fY,8009
|
18
|
+
maps4fs/generator/dtm/srtm.py,sha256=7uEb-Pde_uTG4D311mZ634QYkB5yvV8t2DfNfXuhYGY,3066
|
19
|
+
maps4fs/generator/dtm/usgs.py,sha256=U0kDog1UAa1lWiK4Pe3nnhXnnplOS4HZde1yqYqCiDw,13123
|
20
|
+
maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
21
|
+
maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
|
22
|
+
maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
|
23
|
+
maps4fs-1.5.9.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
|
24
|
+
maps4fs-1.5.9.dist-info/METADATA,sha256=CEhXKo9Y6Nqmh7SL-_BV7XSHtD7eApd8-81Grfd-zp4,36231
|
25
|
+
maps4fs-1.5.9.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
26
|
+
maps4fs-1.5.9.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
27
|
+
maps4fs-1.5.9.dist-info/RECORD,,
|
maps4fs-1.5.6.dist-info/RECORD
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
maps4fs/__init__.py,sha256=EJzbqRrSGltSMUI-dHgONODxKt9YvP_ElwFmXV8M_MA,380
|
2
|
-
maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
|
3
|
-
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
4
|
-
maps4fs/generator/background.py,sha256=ySABP9HLji8R0aXi1BwjUQtP2uDqZPkrlmugowa9Gkk,22836
|
5
|
-
maps4fs/generator/component.py,sha256=RtXruvT4Fxfr7_xo9Bi-i3IIWcPd5QQOSpYJ_cNC49o,20408
|
6
|
-
maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
|
7
|
-
maps4fs/generator/dem.py,sha256=aJva77k_00SKrqnRLF_BXr8eGR5flifrh72kSBq1saI,12621
|
8
|
-
maps4fs/generator/dtm.py,sha256=5_1e-kQcZ7c1Xg3tvuTyumzfTAcUPmDkIyZd5VagyOk,10550
|
9
|
-
maps4fs/generator/game.py,sha256=QHgVnyGYvEnfwGZ84-u-dpbCRr3UeVVqBbrwr5WG8dE,7992
|
10
|
-
maps4fs/generator/grle.py,sha256=u8ZwSs313PIOkH_0B_O2tVTaZ-eYNkc30eKGtBxWzTM,17846
|
11
|
-
maps4fs/generator/i3d.py,sha256=qeZYqfuhbhRPlSAuQHXaq6RmIO7314oMN68Ivebp1YQ,24786
|
12
|
-
maps4fs/generator/map.py,sha256=flU0b2TrVYLxj9o3v_YRvNz9YB3s4w6YFSv4Jka5ojM,9283
|
13
|
-
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
14
|
-
maps4fs/generator/satellite.py,sha256=Qnb6XxmXKnHdHKVMb9mJ3vDGtGkDHCOv_81hrrXdx3k,3660
|
15
|
-
maps4fs/generator/settings.py,sha256=gBMjXpz0hcUsCAw8MS_SsuFKHaI41RK6dclEEepsx2M,4575
|
16
|
-
maps4fs/generator/texture.py,sha256=sErusfv1AqQfP-veMrZ921Tz8DnGEhfB4ucggMmKrD4,31231
|
17
|
-
maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
18
|
-
maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
|
19
|
-
maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
|
20
|
-
maps4fs-1.5.6.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
|
21
|
-
maps4fs-1.5.6.dist-info/METADATA,sha256=0OuPPRh06Av71q90zEiVokNCHSh7QLCULKjcBNyLoYA,36012
|
22
|
-
maps4fs-1.5.6.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
23
|
-
maps4fs-1.5.6.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
24
|
-
maps4fs-1.5.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|