maps4fs 1.5.2__py3-none-any.whl → 1.5.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.
- maps4fs/__init__.py +3 -2
- maps4fs/generator/component.py +10 -0
- maps4fs/generator/dem.py +22 -100
- maps4fs/generator/dtm.py +333 -0
- maps4fs/generator/game.py +2 -1
- maps4fs/generator/map.py +17 -134
- maps4fs/generator/satellite.py +92 -0
- maps4fs/generator/settings.py +152 -0
- {maps4fs-1.5.2.dist-info → maps4fs-1.5.6.dist-info}/METADATA +11 -2
- {maps4fs-1.5.2.dist-info → maps4fs-1.5.6.dist-info}/RECORD +13 -10
- {maps4fs-1.5.2.dist-info → maps4fs-1.5.6.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.5.2.dist-info → maps4fs-1.5.6.dist-info}/WHEEL +0 -0
- {maps4fs-1.5.2.dist-info → maps4fs-1.5.6.dist-info}/top_level.txt +0 -0
maps4fs/__init__.py
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# pylint: disable=missing-module-docstring
|
2
|
+
from maps4fs.generator.dtm import DTMProvider
|
2
3
|
from maps4fs.generator.game import Game
|
3
|
-
from maps4fs.generator.map import
|
4
|
+
from maps4fs.generator.map import Map
|
5
|
+
from maps4fs.generator.settings import (
|
4
6
|
BackgroundSettings,
|
5
7
|
DEMSettings,
|
6
8
|
GRLESettings,
|
7
9
|
I3DSettings,
|
8
|
-
Map,
|
9
10
|
SettingsModel,
|
10
11
|
SplineSettings,
|
11
12
|
TextureSettings,
|
maps4fs/generator/component.py
CHANGED
@@ -68,6 +68,7 @@ class Component:
|
|
68
68
|
os.makedirs(self.previews_directory, exist_ok=True)
|
69
69
|
os.makedirs(self.scripts_directory, exist_ok=True)
|
70
70
|
os.makedirs(self.info_layers_directory, exist_ok=True)
|
71
|
+
os.makedirs(self.satellite_directory, exist_ok=True)
|
71
72
|
|
72
73
|
self.save_bbox()
|
73
74
|
self.preprocess()
|
@@ -123,6 +124,15 @@ class Component:
|
|
123
124
|
"""
|
124
125
|
return os.path.join(self.map_directory, "scripts")
|
125
126
|
|
127
|
+
@property
|
128
|
+
def satellite_directory(self) -> str:
|
129
|
+
"""The directory where the satellite images are stored.
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
str: The directory where the satellite images are stored.
|
133
|
+
"""
|
134
|
+
return os.path.join(self.map_directory, "satellite")
|
135
|
+
|
126
136
|
@property
|
127
137
|
def generation_info_path(self) -> str:
|
128
138
|
"""The path to the generation info JSON file.
|
maps4fs/generator/dem.py
CHANGED
@@ -1,19 +1,15 @@
|
|
1
1
|
"""This module contains DEM class for processing Digital Elevation Model data."""
|
2
2
|
|
3
|
-
import gzip
|
4
|
-
import math
|
5
3
|
import os
|
6
|
-
import shutil
|
7
4
|
|
8
5
|
import cv2
|
9
6
|
import numpy as np
|
10
|
-
|
11
|
-
import
|
7
|
+
|
8
|
+
# import rasterio # type: ignore
|
12
9
|
from pympler import asizeof # type: ignore
|
13
10
|
|
14
11
|
from maps4fs.generator.component import Component
|
15
|
-
|
16
|
-
SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
|
12
|
+
from maps4fs.generator.dtm import DTMProvider
|
17
13
|
|
18
14
|
|
19
15
|
# pylint: disable=R0903, R0902
|
@@ -63,6 +59,14 @@ class DEM(Component):
|
|
63
59
|
|
64
60
|
self.auto_process = self.map.dem_settings.auto_process
|
65
61
|
|
62
|
+
self.dtm_provider: DTMProvider = self.map.dtm_provider( # type: ignore
|
63
|
+
coordinates=self.coordinates,
|
64
|
+
user_settings=self.map.dtm_provider_settings,
|
65
|
+
size=self.map_rotated_size,
|
66
|
+
directory=self.temp_dir,
|
67
|
+
logger=self.logger,
|
68
|
+
)
|
69
|
+
|
66
70
|
@property
|
67
71
|
def dem_path(self) -> str:
|
68
72
|
"""Returns path to the DEM file.
|
@@ -132,36 +136,29 @@ class DEM(Component):
|
|
132
136
|
def process(self) -> None:
|
133
137
|
"""Reads SRTM file, crops it to map size, normalizes and blurs it,
|
134
138
|
saves to map directory."""
|
135
|
-
north, south, east, west = self.bbox
|
136
139
|
|
137
140
|
dem_output_resolution = self.output_resolution
|
138
141
|
self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
|
139
142
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
+
try:
|
144
|
+
data = self.dtm_provider.get_numpy()
|
145
|
+
except Exception as e: # pylint: disable=W0718
|
146
|
+
self.logger.error("Failed to get DEM data from SRTM: %s.", e)
|
143
147
|
self._save_empty_dem(dem_output_resolution)
|
144
148
|
return
|
145
149
|
|
146
|
-
|
147
|
-
self.logger.
|
148
|
-
|
149
|
-
|
150
|
-
"Window parameters. Column offset: %s, row offset: %s, width: %s, height: %s.",
|
151
|
-
window.col_off,
|
152
|
-
window.row_off,
|
153
|
-
window.width,
|
154
|
-
window.height,
|
155
|
-
)
|
156
|
-
data = src.read(1, window=window)
|
150
|
+
if len(data.shape) != 2:
|
151
|
+
self.logger.error("DTM provider returned incorrect data: more than 1 channel.")
|
152
|
+
self._save_empty_dem(dem_output_resolution)
|
153
|
+
return
|
157
154
|
|
158
|
-
if
|
159
|
-
self.logger.
|
155
|
+
if data.dtype not in ["int16", "uint16"]:
|
156
|
+
self.logger.error("DTM provider returned incorrect data type: %s.", data.dtype)
|
160
157
|
self._save_empty_dem(dem_output_resolution)
|
161
158
|
return
|
162
159
|
|
163
160
|
self.logger.debug(
|
164
|
-
"DEM data was
|
161
|
+
"DEM data was retrieved from DTM provider. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
165
162
|
data.shape,
|
166
163
|
data.dtype,
|
167
164
|
data.min(),
|
@@ -276,81 +273,6 @@ class DEM(Component):
|
|
276
273
|
output_width=output_width,
|
277
274
|
)
|
278
275
|
|
279
|
-
def _tile_info(self, lat: float, lon: float) -> tuple[str, str]:
|
280
|
-
"""Returns latitude band and tile name for SRTM tile from coordinates.
|
281
|
-
|
282
|
-
Arguments:
|
283
|
-
lat (float): Latitude.
|
284
|
-
lon (float): Longitude.
|
285
|
-
|
286
|
-
Returns:
|
287
|
-
tuple[str, str]: Latitude band and tile name.
|
288
|
-
"""
|
289
|
-
tile_latitude = math.floor(lat)
|
290
|
-
tile_longitude = math.floor(lon)
|
291
|
-
|
292
|
-
latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
|
293
|
-
if lon < 0:
|
294
|
-
tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
|
295
|
-
else:
|
296
|
-
tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
|
297
|
-
|
298
|
-
self.logger.debug(
|
299
|
-
"Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
|
300
|
-
)
|
301
|
-
return latitude_band, tile_name
|
302
|
-
|
303
|
-
def _download_tile(self) -> str | None:
|
304
|
-
"""Downloads SRTM tile from Amazon S3 using coordinates.
|
305
|
-
|
306
|
-
Returns:
|
307
|
-
str: Path to compressed tile or None if download failed.
|
308
|
-
"""
|
309
|
-
latitude_band, tile_name = self._tile_info(*self.coordinates)
|
310
|
-
compressed_file_path = os.path.join(self.gz_dir, f"{tile_name}.hgt.gz")
|
311
|
-
url = SRTM.format(latitude_band=latitude_band, tile_name=tile_name)
|
312
|
-
self.logger.debug("Trying to get response from %s...", url)
|
313
|
-
response = requests.get(url, stream=True, timeout=10)
|
314
|
-
|
315
|
-
if response.status_code == 200:
|
316
|
-
self.logger.debug("Response received. Saving to %s...", compressed_file_path)
|
317
|
-
with open(compressed_file_path, "wb") as f:
|
318
|
-
for chunk in response.iter_content(chunk_size=8192):
|
319
|
-
f.write(chunk)
|
320
|
-
self.logger.debug("Compressed tile successfully downloaded.")
|
321
|
-
else:
|
322
|
-
self.logger.error("Response was failed with status code %s.", response.status_code)
|
323
|
-
return None
|
324
|
-
|
325
|
-
return compressed_file_path
|
326
|
-
|
327
|
-
def _srtm_tile(self) -> str | None:
|
328
|
-
"""Determines SRTM tile name from coordinates downloads it if necessary, and decompresses.
|
329
|
-
|
330
|
-
Returns:
|
331
|
-
str: Path to decompressed tile or None if download failed.
|
332
|
-
"""
|
333
|
-
latitude_band, tile_name = self._tile_info(*self.coordinates)
|
334
|
-
self.logger.debug("SRTM tile name %s from latitude band %s.", tile_name, latitude_band)
|
335
|
-
|
336
|
-
decompressed_file_path = os.path.join(self.hgt_dir, f"{tile_name}.hgt")
|
337
|
-
if os.path.isfile(decompressed_file_path):
|
338
|
-
self.logger.debug(
|
339
|
-
"Decompressed tile already exists: %s, skipping download.",
|
340
|
-
decompressed_file_path,
|
341
|
-
)
|
342
|
-
return decompressed_file_path
|
343
|
-
|
344
|
-
compressed_file_path = self._download_tile()
|
345
|
-
if not compressed_file_path:
|
346
|
-
self.logger.error("Download from SRTM failed, DEM file will be filled with zeros.")
|
347
|
-
return None
|
348
|
-
with gzip.open(compressed_file_path, "rb") as f_in:
|
349
|
-
with open(decompressed_file_path, "wb") as f_out:
|
350
|
-
shutil.copyfileobj(f_in, f_out)
|
351
|
-
self.logger.debug("Tile decompressed to %s.", decompressed_file_path)
|
352
|
-
return decompressed_file_path
|
353
|
-
|
354
276
|
def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
|
355
277
|
"""Saves empty DEM file filled with zeros."""
|
356
278
|
dem_data = np.zeros(dem_output_resolution, dtype="uint16")
|
maps4fs/generator/dtm.py
ADDED
@@ -0,0 +1,333 @@
|
|
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
|
+
import gzip
|
8
|
+
import math
|
9
|
+
import os
|
10
|
+
import shutil
|
11
|
+
from typing import Type
|
12
|
+
|
13
|
+
import numpy as np
|
14
|
+
import osmnx as ox # type: ignore
|
15
|
+
import rasterio # type: ignore
|
16
|
+
import requests
|
17
|
+
from pydantic import BaseModel
|
18
|
+
|
19
|
+
from maps4fs.logger import Logger
|
20
|
+
|
21
|
+
|
22
|
+
class DTMProviderSettings(BaseModel):
|
23
|
+
"""Base class for DTM provider settings models."""
|
24
|
+
|
25
|
+
|
26
|
+
class DTMProvider:
|
27
|
+
"""Base class for DTM providers."""
|
28
|
+
|
29
|
+
_code: str | None = None
|
30
|
+
_name: str | None = None
|
31
|
+
_region: str | None = None
|
32
|
+
_icon: str | None = None
|
33
|
+
_resolution: float | None = None
|
34
|
+
|
35
|
+
_url: str | None = None
|
36
|
+
|
37
|
+
_author: str | None = None
|
38
|
+
_is_community: bool = False
|
39
|
+
_settings: Type[DTMProviderSettings] | None = None
|
40
|
+
|
41
|
+
_instructions: str | None = None
|
42
|
+
|
43
|
+
# pylint: disable=R0913, R0917
|
44
|
+
def __init__(
|
45
|
+
self,
|
46
|
+
coordinates: tuple[float, float],
|
47
|
+
user_settings: DTMProviderSettings | None,
|
48
|
+
size: int,
|
49
|
+
directory: str,
|
50
|
+
logger: Logger,
|
51
|
+
):
|
52
|
+
self._coordinates = coordinates
|
53
|
+
self._user_settings = user_settings
|
54
|
+
self._size = size
|
55
|
+
|
56
|
+
if not self._code:
|
57
|
+
raise ValueError("Provider code must be defined.")
|
58
|
+
self._tile_directory = os.path.join(directory, self._code)
|
59
|
+
os.makedirs(self._tile_directory, exist_ok=True)
|
60
|
+
|
61
|
+
self.logger = logger
|
62
|
+
|
63
|
+
@property
|
64
|
+
def coordinates(self) -> tuple[float, float]:
|
65
|
+
"""Coordinates of the center point of the DTM data.
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
tuple: Latitude and longitude of the center point.
|
69
|
+
"""
|
70
|
+
return self._coordinates
|
71
|
+
|
72
|
+
@property
|
73
|
+
def size(self) -> int:
|
74
|
+
"""Size of the DTM data in meters.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
int: Size of the DTM data.
|
78
|
+
"""
|
79
|
+
return self._size
|
80
|
+
|
81
|
+
@property
|
82
|
+
def url(self) -> str | None:
|
83
|
+
"""URL of the provider."""
|
84
|
+
return self._url
|
85
|
+
|
86
|
+
def formatted_url(self, **kwargs) -> str:
|
87
|
+
"""Formatted URL of the provider."""
|
88
|
+
if not self.url:
|
89
|
+
raise ValueError("URL must be defined.")
|
90
|
+
return self.url.format(**kwargs)
|
91
|
+
|
92
|
+
@classmethod
|
93
|
+
def author(cls) -> str | None:
|
94
|
+
"""Author of the provider.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
str: Author of the provider.
|
98
|
+
"""
|
99
|
+
return cls._author
|
100
|
+
|
101
|
+
@classmethod
|
102
|
+
def is_community(cls) -> bool:
|
103
|
+
"""Is the provider a community-driven project.
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
bool: True if the provider is a community-driven project, False otherwise.
|
107
|
+
"""
|
108
|
+
return cls._is_community
|
109
|
+
|
110
|
+
@classmethod
|
111
|
+
def settings(cls) -> Type[DTMProviderSettings] | None:
|
112
|
+
"""Settings model of the provider.
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
Type[DTMProviderSettings]: Settings model of the provider.
|
116
|
+
"""
|
117
|
+
return cls._settings
|
118
|
+
|
119
|
+
@classmethod
|
120
|
+
def instructions(cls) -> str | None:
|
121
|
+
"""Instructions for using the provider.
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
str: Instructions for using the provider.
|
125
|
+
"""
|
126
|
+
return cls._instructions
|
127
|
+
|
128
|
+
@property
|
129
|
+
def user_settings(self) -> DTMProviderSettings | None:
|
130
|
+
"""User settings of the provider.
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
DTMProviderSettings: User settings of the provider.
|
134
|
+
"""
|
135
|
+
return self._user_settings
|
136
|
+
|
137
|
+
@classmethod
|
138
|
+
def description(cls) -> str:
|
139
|
+
"""Description of the provider.
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
str: Provider description.
|
143
|
+
"""
|
144
|
+
return f"{cls._icon} {cls._region} [{cls._resolution} m/px] {cls._name}"
|
145
|
+
|
146
|
+
@classmethod
|
147
|
+
def get_provider_by_code(cls, code: str) -> Type[DTMProvider] | None:
|
148
|
+
"""Get a provider by its code.
|
149
|
+
|
150
|
+
Arguments:
|
151
|
+
code (str): Provider code.
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
DTMProvider: Provider class or None if not found.
|
155
|
+
"""
|
156
|
+
for provider in cls.__subclasses__():
|
157
|
+
if provider._code == code: # pylint: disable=W0212
|
158
|
+
return provider
|
159
|
+
return None
|
160
|
+
|
161
|
+
@classmethod
|
162
|
+
def get_provider_descriptions(cls) -> dict[str, str]:
|
163
|
+
"""Get descriptions of all providers, where keys are provider codes and
|
164
|
+
values are provider descriptions.
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
dict: Provider descriptions.
|
168
|
+
"""
|
169
|
+
providers = {}
|
170
|
+
for provider in cls.__subclasses__():
|
171
|
+
providers[provider._code] = provider.description() # pylint: disable=W0212
|
172
|
+
return providers # type: ignore
|
173
|
+
|
174
|
+
def download_tile(self, output_path: str, **kwargs) -> bool:
|
175
|
+
"""Download a tile from the provider.
|
176
|
+
|
177
|
+
Arguments:
|
178
|
+
output_path (str): Path to save the downloaded tile.
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
bool: True if the tile was downloaded successfully, False otherwise.
|
182
|
+
"""
|
183
|
+
url = self.formatted_url(**kwargs)
|
184
|
+
response = requests.get(url, stream=True, timeout=10)
|
185
|
+
if response.status_code == 200:
|
186
|
+
with open(output_path, "wb") as file:
|
187
|
+
for chunk in response.iter_content(chunk_size=1024):
|
188
|
+
file.write(chunk)
|
189
|
+
return True
|
190
|
+
return False
|
191
|
+
|
192
|
+
def get_or_download_tile(self, output_path: str, **kwargs) -> str | None:
|
193
|
+
"""Get or download a tile from the provider.
|
194
|
+
|
195
|
+
Arguments:
|
196
|
+
output_path (str): Path to save the downloaded tile.
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
str: Path to the downloaded tile or None if the tile not exists and was
|
200
|
+
not downloaded.
|
201
|
+
"""
|
202
|
+
if not os.path.exists(output_path):
|
203
|
+
if not self.download_tile(output_path, **kwargs):
|
204
|
+
return None
|
205
|
+
return output_path
|
206
|
+
|
207
|
+
def get_tile_parameters(self, *args, **kwargs) -> dict:
|
208
|
+
"""Get parameters for the tile, that will be used to format the URL.
|
209
|
+
Must be implemented in subclasses.
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
dict: Tile parameters to format the URL.
|
213
|
+
"""
|
214
|
+
raise NotImplementedError
|
215
|
+
|
216
|
+
def get_numpy(self) -> np.ndarray:
|
217
|
+
"""Get numpy array of the tile.
|
218
|
+
Resulting array must be 16 bit (signed or unsigned) integer and it should be already
|
219
|
+
windowed to the bounding box of ROI. It also must have only one channel.
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
np.ndarray: Numpy array of the tile.
|
223
|
+
"""
|
224
|
+
raise NotImplementedError
|
225
|
+
|
226
|
+
def get_bbox(self) -> tuple[float, float, float, float]:
|
227
|
+
"""Get bounding box of the tile based on the center point and size.
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
tuple: Bounding box of the tile (north, south, east, west).
|
231
|
+
"""
|
232
|
+
west, south, east, north = ox.utils_geo.bbox_from_point( # type: ignore
|
233
|
+
self.coordinates, dist=self.size // 2, project_utm=False
|
234
|
+
)
|
235
|
+
bbox = north, south, east, west
|
236
|
+
return bbox
|
237
|
+
|
238
|
+
def extract_roi(self, tile_path: str) -> np.ndarray:
|
239
|
+
"""Extract region of interest (ROI) from the GeoTIFF file.
|
240
|
+
|
241
|
+
Arguments:
|
242
|
+
tile_path (str): Path to the GeoTIFF file.
|
243
|
+
|
244
|
+
Raises:
|
245
|
+
ValueError: If the tile does not contain any data.
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
np.ndarray: Numpy array of the ROI.
|
249
|
+
"""
|
250
|
+
north, south, east, west = self.get_bbox()
|
251
|
+
with rasterio.open(tile_path) as src:
|
252
|
+
self.logger.debug("Opened tile, shape: %s, dtype: %s.", src.shape, src.dtypes[0])
|
253
|
+
window = rasterio.windows.from_bounds(west, south, east, north, src.transform)
|
254
|
+
self.logger.debug(
|
255
|
+
"Window parameters. Column offset: %s, row offset: %s, width: %s, height: %s.",
|
256
|
+
window.col_off,
|
257
|
+
window.row_off,
|
258
|
+
window.width,
|
259
|
+
window.height,
|
260
|
+
)
|
261
|
+
data = src.read(1, window=window)
|
262
|
+
if not data.size > 0:
|
263
|
+
raise ValueError("No data in the tile.")
|
264
|
+
|
265
|
+
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)
|
maps4fs/generator/game.py
CHANGED
@@ -10,6 +10,7 @@ from maps4fs.generator.background import Background
|
|
10
10
|
from maps4fs.generator.config import Config
|
11
11
|
from maps4fs.generator.grle import GRLE
|
12
12
|
from maps4fs.generator.i3d import I3d
|
13
|
+
from maps4fs.generator.satellite import Satellite
|
13
14
|
from maps4fs.generator.texture import Texture
|
14
15
|
|
15
16
|
working_directory = os.getcwd()
|
@@ -39,7 +40,7 @@ class Game:
|
|
39
40
|
_tree_schema: str | None = None
|
40
41
|
|
41
42
|
# Order matters! Some components depend on others.
|
42
|
-
components = [Texture, GRLE, Background, I3d, Config]
|
43
|
+
components = [Texture, GRLE, Background, I3d, Config, Satellite]
|
43
44
|
|
44
45
|
def __init__(self, map_template_path: str | None = None):
|
45
46
|
if map_template_path:
|
maps4fs/generator/map.py
CHANGED
@@ -7,145 +7,21 @@ import os
|
|
7
7
|
import shutil
|
8
8
|
from typing import Any, Generator
|
9
9
|
|
10
|
-
from pydantic import BaseModel
|
11
|
-
|
12
10
|
from maps4fs.generator.component import Component
|
11
|
+
from maps4fs.generator.dtm import DTMProvider, DTMProviderSettings
|
13
12
|
from maps4fs.generator.game import Game
|
13
|
+
from maps4fs.generator.settings import (
|
14
|
+
BackgroundSettings,
|
15
|
+
DEMSettings,
|
16
|
+
GRLESettings,
|
17
|
+
I3DSettings,
|
18
|
+
SatelliteSettings,
|
19
|
+
SplineSettings,
|
20
|
+
TextureSettings,
|
21
|
+
)
|
14
22
|
from maps4fs.logger import Logger
|
15
23
|
|
16
24
|
|
17
|
-
class SettingsModel(BaseModel):
|
18
|
-
"""Base class for settings models. It provides methods to convert settings to and from JSON."""
|
19
|
-
|
20
|
-
@classmethod
|
21
|
-
def all_settings_to_json(cls) -> dict[str, dict[str, Any]]:
|
22
|
-
"""Get all settings of the current class and its subclasses as a dictionary.
|
23
|
-
|
24
|
-
Returns:
|
25
|
-
dict[str, dict[str, Any]]: Dictionary with settings of the current class and its
|
26
|
-
subclasses.
|
27
|
-
"""
|
28
|
-
all_settings = {}
|
29
|
-
for subclass in cls.__subclasses__():
|
30
|
-
all_settings[subclass.__name__] = subclass().model_dump()
|
31
|
-
|
32
|
-
return all_settings
|
33
|
-
|
34
|
-
@classmethod
|
35
|
-
def all_settings_from_json(cls, data: dict) -> dict[str, SettingsModel]:
|
36
|
-
"""Create settings instances from JSON data.
|
37
|
-
|
38
|
-
Arguments:
|
39
|
-
data (dict): JSON data.
|
40
|
-
|
41
|
-
Returns:
|
42
|
-
dict[str, Type[SettingsModel]]: Dictionary with settings instances.
|
43
|
-
"""
|
44
|
-
settings = {}
|
45
|
-
for subclass in cls.__subclasses__():
|
46
|
-
settings[subclass.__name__] = subclass(**data[subclass.__name__])
|
47
|
-
|
48
|
-
return settings
|
49
|
-
|
50
|
-
@classmethod
|
51
|
-
def all_settings(cls) -> list[SettingsModel]:
|
52
|
-
"""Get all settings of the current class and its subclasses.
|
53
|
-
|
54
|
-
Returns:
|
55
|
-
list[SettingsModel]: List with settings of the current class and its subclasses.
|
56
|
-
"""
|
57
|
-
settings = []
|
58
|
-
for subclass in cls.__subclasses__():
|
59
|
-
settings.append(subclass())
|
60
|
-
|
61
|
-
return settings
|
62
|
-
|
63
|
-
|
64
|
-
class DEMSettings(SettingsModel):
|
65
|
-
"""Represents the advanced settings for DEM component.
|
66
|
-
|
67
|
-
Attributes:
|
68
|
-
auto_process (bool): use the auto preset to change the multiplier.
|
69
|
-
multiplier (int): multiplier for the heightmap, every pixel will be multiplied by this
|
70
|
-
value.
|
71
|
-
blur_radius (int): radius of the blur filter.
|
72
|
-
plateau (int): plateau height, will be added to each pixel.
|
73
|
-
water_depth (int): water depth, will be subtracted from each pixel where the water
|
74
|
-
is present.
|
75
|
-
"""
|
76
|
-
|
77
|
-
auto_process: bool = True
|
78
|
-
multiplier: int = 1
|
79
|
-
blur_radius: int = 35
|
80
|
-
plateau: int = 0
|
81
|
-
water_depth: int = 0
|
82
|
-
|
83
|
-
|
84
|
-
class BackgroundSettings(SettingsModel):
|
85
|
-
"""Represents the advanced settings for background component.
|
86
|
-
|
87
|
-
Attributes:
|
88
|
-
generate_background (bool): generate obj files for the background terrain.
|
89
|
-
generate_water (bool): generate obj files for the water.
|
90
|
-
resize_factor (int): resize factor for the background terrain and water.
|
91
|
-
It will be used as 1 / resize_factor of the original size.
|
92
|
-
"""
|
93
|
-
|
94
|
-
generate_background: bool = True
|
95
|
-
generate_water: bool = True
|
96
|
-
resize_factor: int = 8
|
97
|
-
|
98
|
-
|
99
|
-
class GRLESettings(SettingsModel):
|
100
|
-
"""Represents the advanced settings for GRLE component.
|
101
|
-
|
102
|
-
Attributes:
|
103
|
-
farmland_margin (int): margin around the farmland.
|
104
|
-
random_plants (bool): generate random plants on the map or use the default one.
|
105
|
-
add_farmyards (bool): If True, regions of frarmyards will be added to the map
|
106
|
-
without corresponding fields.
|
107
|
-
"""
|
108
|
-
|
109
|
-
farmland_margin: int = 0
|
110
|
-
random_plants: bool = True
|
111
|
-
add_farmyards: bool = False
|
112
|
-
|
113
|
-
|
114
|
-
class I3DSettings(SettingsModel):
|
115
|
-
"""Represents the advanced settings for I3D component.
|
116
|
-
|
117
|
-
Attributes:
|
118
|
-
forest_density (int): density of the forest (distance between trees).
|
119
|
-
"""
|
120
|
-
|
121
|
-
forest_density: int = 10
|
122
|
-
|
123
|
-
|
124
|
-
class TextureSettings(SettingsModel):
|
125
|
-
"""Represents the advanced settings for texture component.
|
126
|
-
|
127
|
-
Attributes:
|
128
|
-
dissolve (bool): dissolve the texture into several images.
|
129
|
-
fields_padding (int): padding around the fields.
|
130
|
-
skip_drains (bool): skip drains generation.
|
131
|
-
"""
|
132
|
-
|
133
|
-
dissolve: bool = False
|
134
|
-
fields_padding: int = 0
|
135
|
-
skip_drains: bool = False
|
136
|
-
|
137
|
-
|
138
|
-
class SplineSettings(SettingsModel):
|
139
|
-
"""Represents the advanced settings for spline component.
|
140
|
-
|
141
|
-
Attributes:
|
142
|
-
spline_density (int): the number of extra points that will be added between each two
|
143
|
-
existing points.
|
144
|
-
"""
|
145
|
-
|
146
|
-
spline_density: int = 2
|
147
|
-
|
148
|
-
|
149
25
|
# pylint: disable=R0913, R0902, R0914
|
150
26
|
class Map:
|
151
27
|
"""Class used to generate map using all components.
|
@@ -161,6 +37,8 @@ class Map:
|
|
161
37
|
def __init__( # pylint: disable=R0917, R0915
|
162
38
|
self,
|
163
39
|
game: Game,
|
40
|
+
dtm_provider: DTMProvider,
|
41
|
+
dtm_provider_settings: DTMProviderSettings,
|
164
42
|
coordinates: tuple[float, float],
|
165
43
|
size: int,
|
166
44
|
rotation: int,
|
@@ -173,6 +51,7 @@ class Map:
|
|
173
51
|
i3d_settings: I3DSettings = I3DSettings(),
|
174
52
|
texture_settings: TextureSettings = TextureSettings(),
|
175
53
|
spline_settings: SplineSettings = SplineSettings(),
|
54
|
+
satellite_settings: SatelliteSettings = SatelliteSettings(),
|
176
55
|
**kwargs,
|
177
56
|
):
|
178
57
|
if not logger:
|
@@ -189,6 +68,8 @@ class Map:
|
|
189
68
|
self.rotated_size = int(size * rotation_multiplier)
|
190
69
|
|
191
70
|
self.game = game
|
71
|
+
self.dtm_provider = dtm_provider
|
72
|
+
self.dtm_provider_settings = dtm_provider_settings
|
192
73
|
self.components: list[Component] = []
|
193
74
|
self.coordinates = coordinates
|
194
75
|
self.map_directory = map_directory
|
@@ -217,6 +98,7 @@ class Map:
|
|
217
98
|
self.logger.info("Texture settings: %s", texture_settings)
|
218
99
|
self.spline_settings = spline_settings
|
219
100
|
self.logger.info("Spline settings: %s", spline_settings)
|
101
|
+
self.satellite_settings = satellite_settings
|
220
102
|
|
221
103
|
os.makedirs(self.map_directory, exist_ok=True)
|
222
104
|
self.logger.debug("Map directory created: %s", self.map_directory)
|
@@ -228,6 +110,7 @@ class Map:
|
|
228
110
|
i3d_settings,
|
229
111
|
texture_settings,
|
230
112
|
spline_settings,
|
113
|
+
satellite_settings,
|
231
114
|
]
|
232
115
|
|
233
116
|
settings_json = {}
|
@@ -0,0 +1,92 @@
|
|
1
|
+
"""This module contains the Satellite class for the maps4fs package to download satellite images
|
2
|
+
for the map."""
|
3
|
+
|
4
|
+
import os
|
5
|
+
|
6
|
+
import cv2
|
7
|
+
from pygmdl import save_image # type: ignore
|
8
|
+
|
9
|
+
from maps4fs.generator.background import DEFAULT_DISTANCE
|
10
|
+
from maps4fs.generator.component import Component
|
11
|
+
from maps4fs.generator.texture import PREVIEW_MAXIMUM_SIZE
|
12
|
+
|
13
|
+
|
14
|
+
# pylint: disable=W0223
|
15
|
+
class Satellite(Component):
|
16
|
+
"""Component for to download satellite images for the map.
|
17
|
+
|
18
|
+
Arguments:
|
19
|
+
game (Game): The game instance for which the map is generated.
|
20
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
21
|
+
map_size (int): The size of the map in pixels.
|
22
|
+
map_rotated_size (int): The size of the map in pixels after rotation.
|
23
|
+
rotation (int): The rotation angle of the map.
|
24
|
+
map_directory (str): The directory where the map files are stored.
|
25
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
26
|
+
info, warning. If not provided, default logging will be used.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def preprocess(self) -> None:
|
30
|
+
"""This component does not require any preprocessing."""
|
31
|
+
return
|
32
|
+
|
33
|
+
def process(self) -> None:
|
34
|
+
"""Downloads the satellite images for the map."""
|
35
|
+
self.image_paths = [] # pylint: disable=W0201
|
36
|
+
if not self.map.satellite_settings.download_images:
|
37
|
+
self.logger.info("Satellite images download is disabled.")
|
38
|
+
return
|
39
|
+
|
40
|
+
margin = self.map.satellite_settings.satellite_margin
|
41
|
+
overview_size = (self.map_size + margin) * 2
|
42
|
+
overwiew_path = os.path.join(self.satellite_directory, "satellite_overview.png")
|
43
|
+
|
44
|
+
background_size = self.map_size + (DEFAULT_DISTANCE + margin) * 2
|
45
|
+
background_path = os.path.join(self.satellite_directory, "satellite_background.png")
|
46
|
+
|
47
|
+
sizes = [overview_size, background_size]
|
48
|
+
self.image_paths = [overwiew_path, background_path] # pylint: disable=W0201
|
49
|
+
|
50
|
+
for size, path in zip(sizes, self.image_paths):
|
51
|
+
try:
|
52
|
+
lat, lon = self.coordinates
|
53
|
+
zoom = self.map.satellite_settings.zoom_level
|
54
|
+
save_image(
|
55
|
+
lat,
|
56
|
+
lon,
|
57
|
+
size,
|
58
|
+
output_path=path,
|
59
|
+
rotation=self.rotation,
|
60
|
+
zoom=zoom,
|
61
|
+
from_center=True,
|
62
|
+
logger=self.logger,
|
63
|
+
)
|
64
|
+
except Exception as e: # pylint: disable=W0718
|
65
|
+
self.logger.error(f"Failed to download satellite image: {e}")
|
66
|
+
continue
|
67
|
+
|
68
|
+
# pylint: disable=no-member
|
69
|
+
def previews(self) -> list[str]:
|
70
|
+
"""Returns the paths to the preview images.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
list[str]: List of paths to the preview images.
|
74
|
+
"""
|
75
|
+
previews = []
|
76
|
+
for image_path in self.image_paths:
|
77
|
+
if not os.path.isfile(image_path):
|
78
|
+
self.logger.warning(f"File {image_path} does not exist.")
|
79
|
+
continue
|
80
|
+
image = cv2.imread(image_path)
|
81
|
+
if image is None:
|
82
|
+
self.logger.warning(f"Failed to read image from {image_path}")
|
83
|
+
continue
|
84
|
+
|
85
|
+
if image.shape[0] > PREVIEW_MAXIMUM_SIZE or image.shape[1] > PREVIEW_MAXIMUM_SIZE:
|
86
|
+
image = cv2.resize(image, (PREVIEW_MAXIMUM_SIZE, PREVIEW_MAXIMUM_SIZE))
|
87
|
+
|
88
|
+
preview_path = os.path.join(self.previews_directory, os.path.basename(image_path))
|
89
|
+
cv2.imwrite(preview_path, image)
|
90
|
+
previews.append(preview_path)
|
91
|
+
|
92
|
+
return previews
|
@@ -0,0 +1,152 @@
|
|
1
|
+
"""This module contains settings models for all components."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
|
10
|
+
class SettingsModel(BaseModel):
|
11
|
+
"""Base class for settings models. It provides methods to convert settings to and from JSON."""
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
def all_settings_to_json(cls) -> dict[str, dict[str, Any]]:
|
15
|
+
"""Get all settings of the current class and its subclasses as a dictionary.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
dict[str, dict[str, Any]]: Dictionary with settings of the current class and its
|
19
|
+
subclasses.
|
20
|
+
"""
|
21
|
+
all_settings = {}
|
22
|
+
for subclass in cls.__subclasses__():
|
23
|
+
all_settings[subclass.__name__] = subclass().model_dump()
|
24
|
+
|
25
|
+
return all_settings
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def all_settings_from_json(cls, data: dict) -> dict[str, SettingsModel]:
|
29
|
+
"""Create settings instances from JSON data.
|
30
|
+
|
31
|
+
Arguments:
|
32
|
+
data (dict): JSON data.
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
dict[str, Type[SettingsModel]]: Dictionary with settings instances.
|
36
|
+
"""
|
37
|
+
settings = {}
|
38
|
+
for subclass in cls.__subclasses__():
|
39
|
+
settings[subclass.__name__] = subclass(**data[subclass.__name__])
|
40
|
+
|
41
|
+
return settings
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def all_settings(cls) -> list[SettingsModel]:
|
45
|
+
"""Get all settings of the current class and its subclasses.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
list[SettingsModel]: List with settings of the current class and its subclasses.
|
49
|
+
"""
|
50
|
+
settings = []
|
51
|
+
for subclass in cls.__subclasses__():
|
52
|
+
settings.append(subclass())
|
53
|
+
|
54
|
+
return settings
|
55
|
+
|
56
|
+
|
57
|
+
class DEMSettings(SettingsModel):
|
58
|
+
"""Represents the advanced settings for DEM component.
|
59
|
+
|
60
|
+
Attributes:
|
61
|
+
auto_process (bool): use the auto preset to change the multiplier.
|
62
|
+
multiplier (int): multiplier for the heightmap, every pixel will be multiplied by this
|
63
|
+
value.
|
64
|
+
blur_radius (int): radius of the blur filter.
|
65
|
+
plateau (int): plateau height, will be added to each pixel.
|
66
|
+
water_depth (int): water depth, will be subtracted from each pixel where the water
|
67
|
+
is present.
|
68
|
+
"""
|
69
|
+
|
70
|
+
auto_process: bool = True
|
71
|
+
multiplier: int = 1
|
72
|
+
blur_radius: int = 35
|
73
|
+
plateau: int = 0
|
74
|
+
water_depth: int = 0
|
75
|
+
|
76
|
+
|
77
|
+
class BackgroundSettings(SettingsModel):
|
78
|
+
"""Represents the advanced settings for background component.
|
79
|
+
|
80
|
+
Attributes:
|
81
|
+
generate_background (bool): generate obj files for the background terrain.
|
82
|
+
generate_water (bool): generate obj files for the water.
|
83
|
+
resize_factor (int): resize factor for the background terrain and water.
|
84
|
+
It will be used as 1 / resize_factor of the original size.
|
85
|
+
"""
|
86
|
+
|
87
|
+
generate_background: bool = False
|
88
|
+
generate_water: bool = False
|
89
|
+
resize_factor: int = 8
|
90
|
+
|
91
|
+
|
92
|
+
class GRLESettings(SettingsModel):
|
93
|
+
"""Represents the advanced settings for GRLE component.
|
94
|
+
|
95
|
+
Attributes:
|
96
|
+
farmland_margin (int): margin around the farmland.
|
97
|
+
random_plants (bool): generate random plants on the map or use the default one.
|
98
|
+
add_farmyards (bool): If True, regions of frarmyards will be added to the map
|
99
|
+
without corresponding fields.
|
100
|
+
"""
|
101
|
+
|
102
|
+
farmland_margin: int = 0
|
103
|
+
random_plants: bool = True
|
104
|
+
add_farmyards: bool = False
|
105
|
+
|
106
|
+
|
107
|
+
class I3DSettings(SettingsModel):
|
108
|
+
"""Represents the advanced settings for I3D component.
|
109
|
+
|
110
|
+
Attributes:
|
111
|
+
forest_density (int): density of the forest (distance between trees).
|
112
|
+
"""
|
113
|
+
|
114
|
+
forest_density: int = 10
|
115
|
+
|
116
|
+
|
117
|
+
class TextureSettings(SettingsModel):
|
118
|
+
"""Represents the advanced settings for texture component.
|
119
|
+
|
120
|
+
Attributes:
|
121
|
+
dissolve (bool): dissolve the texture into several images.
|
122
|
+
fields_padding (int): padding around the fields.
|
123
|
+
skip_drains (bool): skip drains generation.
|
124
|
+
"""
|
125
|
+
|
126
|
+
dissolve: bool = False
|
127
|
+
fields_padding: int = 0
|
128
|
+
skip_drains: bool = False
|
129
|
+
|
130
|
+
|
131
|
+
class SplineSettings(SettingsModel):
|
132
|
+
"""Represents the advanced settings for spline component.
|
133
|
+
|
134
|
+
Attributes:
|
135
|
+
spline_density (int): the number of extra points that will be added between each two
|
136
|
+
existing points.
|
137
|
+
"""
|
138
|
+
|
139
|
+
spline_density: int = 2
|
140
|
+
|
141
|
+
|
142
|
+
class SatelliteSettings(SettingsModel):
|
143
|
+
"""Represents the advanced settings for satellite component.
|
144
|
+
|
145
|
+
Attributes:
|
146
|
+
download_images (bool): download satellite images.
|
147
|
+
margin (int): margin around the map.
|
148
|
+
"""
|
149
|
+
|
150
|
+
download_images: bool = False
|
151
|
+
satellite_margin: int = 100
|
152
|
+
zoom_level: int = 14
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: maps4fs
|
3
|
-
Version: 1.5.
|
3
|
+
Version: 1.5.6
|
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
|
@@ -75,6 +75,8 @@ Requires-Dist: pydantic
|
|
75
75
|
🌲 Automatically generates forests 🆕<br>
|
76
76
|
🌊 Automatically generates water planes 🆕<br>
|
77
77
|
📈 Automatically generates splines 🆕<br>
|
78
|
+
🛰️ Automatically downloads high resolution satellite images 🆕<br>
|
79
|
+
🏔️ Allows to use multiple DTM providers for elevation models 🆕<br>
|
78
80
|
🌍 Based on real-world data from OpenStreetMap<br>
|
79
81
|
🗺️ Supports [custom OSM maps](/docs/custom_osm.md)<br>
|
80
82
|
🏞️ Generates height map using SRTM dataset<br>
|
@@ -511,10 +513,16 @@ You can also apply some advanced settings to the map generation process. Note th
|
|
511
513
|
|
512
514
|
- Skip drains - if enabled, the tool will not generate the drains and ditches on the map. By default, it's set to False. Use this if you don't need the drains on the map.
|
513
515
|
|
514
|
-
|
516
|
+
### Splines Advanced settings
|
515
517
|
|
516
518
|
- Splines density - number of points, which will be added (interpolate) between each pair of existing points. The higher the value, the denser the spline will be. It can smooth the splines, but high values can in opposite make the splines look unnatural.
|
517
519
|
|
520
|
+
### Satellite Advanced settings
|
521
|
+
|
522
|
+
- Download images - if enabled, the tool will download the satellite images for the background terrain and the overview image. If you already have the images, you can turn it off.
|
523
|
+
- Satellite margin - the margin around the map in meters. It's useful when you want to have some space around the map on the satellite images. By default, it's set to 100.
|
524
|
+
- Zoom level - the zoom level of the satellite images. The higher the value, the more detailed the images will be. By default, it's set to 14 and this option is disabled on a public version of the app.
|
525
|
+
|
518
526
|
## Expert Settings
|
519
527
|
The tool also supports the expert settings. Do not use them until you read the documentation and understand what they do. Here's the list of the expert settings:
|
520
528
|
|
@@ -555,3 +563,4 @@ But also, I want to thank the people who helped me with the project in some way,
|
|
555
563
|
- [Tox3](https://github.com/Tox3) - for the manual tests of the app.
|
556
564
|
- [Lucandia](https://github.com/Lucandia) - for the awesome StreamLit [widget to preview STL files](https://github.com/Lucandia/streamlit_stl).
|
557
565
|
- [H4rdB4se](https://github.com/H4rdB4se) - for investigating the issue with custom OSM files and finding a proper way to work with the files in JOSM.
|
566
|
+
- [kbrandwijk](https://github.com/kbrandwijk) - for providing [awesome tool](https://github.com/Paint-a-Farm/satmap_downloader) to download the satellite images from the Google Maps and giving a permission to modify it and create a Python Package.
|
@@ -1,21 +1,24 @@
|
|
1
|
-
maps4fs/__init__.py,sha256=
|
1
|
+
maps4fs/__init__.py,sha256=EJzbqRrSGltSMUI-dHgONODxKt9YvP_ElwFmXV8M_MA,380
|
2
2
|
maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
|
3
3
|
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
4
4
|
maps4fs/generator/background.py,sha256=ySABP9HLji8R0aXi1BwjUQtP2uDqZPkrlmugowa9Gkk,22836
|
5
|
-
maps4fs/generator/component.py,sha256=
|
5
|
+
maps4fs/generator/component.py,sha256=RtXruvT4Fxfr7_xo9Bi-i3IIWcPd5QQOSpYJ_cNC49o,20408
|
6
6
|
maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
|
7
|
-
maps4fs/generator/dem.py,sha256=
|
8
|
-
maps4fs/generator/
|
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
|
9
10
|
maps4fs/generator/grle.py,sha256=u8ZwSs313PIOkH_0B_O2tVTaZ-eYNkc30eKGtBxWzTM,17846
|
10
11
|
maps4fs/generator/i3d.py,sha256=qeZYqfuhbhRPlSAuQHXaq6RmIO7314oMN68Ivebp1YQ,24786
|
11
|
-
maps4fs/generator/map.py,sha256=
|
12
|
+
maps4fs/generator/map.py,sha256=flU0b2TrVYLxj9o3v_YRvNz9YB3s4w6YFSv4Jka5ojM,9283
|
12
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
|
13
16
|
maps4fs/generator/texture.py,sha256=sErusfv1AqQfP-veMrZ921Tz8DnGEhfB4ucggMmKrD4,31231
|
14
17
|
maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
15
18
|
maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
|
16
19
|
maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
|
17
|
-
maps4fs-1.5.
|
18
|
-
maps4fs-1.5.
|
19
|
-
maps4fs-1.5.
|
20
|
-
maps4fs-1.5.
|
21
|
-
maps4fs-1.5.
|
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
|