maps4fs 1.5.2__py3-none-any.whl → 1.5.4__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 CHANGED
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=missing-module-docstring
2
+ from maps4fs.generator.dtm import DTMProvider
2
3
  from maps4fs.generator.game import Game
3
4
  from maps4fs.generator.map import (
4
5
  BackgroundSettings,
@@ -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
- import rasterio # type: ignore
11
- import requests
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,13 @@ 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
+ size=self.map_rotated_size,
65
+ directory=self.temp_dir,
66
+ logger=self.logger,
67
+ )
68
+
66
69
  @property
67
70
  def dem_path(self) -> str:
68
71
  """Returns path to the DEM file.
@@ -132,36 +135,29 @@ class DEM(Component):
132
135
  def process(self) -> None:
133
136
  """Reads SRTM file, crops it to map size, normalizes and blurs it,
134
137
  saves to map directory."""
135
- north, south, east, west = self.bbox
136
138
 
137
139
  dem_output_resolution = self.output_resolution
138
140
  self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
139
141
 
140
- tile_path = self._srtm_tile()
141
- if not tile_path:
142
- self.logger.warning("Tile was not downloaded, DEM file will be filled with zeros.")
142
+ try:
143
+ data = self.dtm_provider.get_numpy()
144
+ except Exception as e: # pylint: disable=W0718
145
+ self.logger.error("Failed to get DEM data from SRTM: %s.", e)
143
146
  self._save_empty_dem(dem_output_resolution)
144
147
  return
145
148
 
146
- with rasterio.open(tile_path) as src:
147
- self.logger.debug("Opened tile, shape: %s, dtype: %s.", src.shape, src.dtypes[0])
148
- window = rasterio.windows.from_bounds(west, south, east, north, src.transform)
149
- self.logger.debug(
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)
149
+ if len(data.shape) != 2:
150
+ self.logger.error("DTM provider returned incorrect data: more than 1 channel.")
151
+ self._save_empty_dem(dem_output_resolution)
152
+ return
157
153
 
158
- if not data.size > 0:
159
- self.logger.warning("DEM data is empty, DEM file will be filled with zeros.")
154
+ if data.dtype not in ["int16", "uint16"]:
155
+ self.logger.error("DTM provider returned incorrect data type: %s.", data.dtype)
160
156
  self._save_empty_dem(dem_output_resolution)
161
157
  return
162
158
 
163
159
  self.logger.debug(
164
- "DEM data was read from SRTM file. Shape: %s, dtype: %s. Min: %s, max: %s.",
160
+ "DEM data was retrieved from DTM provider. Shape: %s, dtype: %s. Min: %s, max: %s.",
165
161
  data.shape,
166
162
  data.dtype,
167
163
  data.min(),
@@ -276,81 +272,6 @@ class DEM(Component):
276
272
  output_width=output_width,
277
273
  )
278
274
 
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
275
  def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
355
276
  """Saves empty DEM file filled with zeros."""
356
277
  dem_data = np.zeros(dem_output_resolution, dtype="uint16")
@@ -0,0 +1,266 @@
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
+
18
+ from maps4fs.logger import Logger
19
+
20
+
21
+ class DTMProvider:
22
+ """Base class for DTM providers."""
23
+
24
+ _code: str | None = None
25
+ _name: str | None = None
26
+ _region: str | None = None
27
+ _icon: str | None = None
28
+ _resolution: float | None = None
29
+
30
+ _url: str | None = None
31
+
32
+ def __init__(self, coordinates: tuple[float, float], size: int, directory: str, logger: Logger):
33
+ self._coordinates = coordinates
34
+ self._size = size
35
+
36
+ if not self._code:
37
+ raise ValueError("Provider code must be defined.")
38
+ self._tile_directory = os.path.join(directory, self._code)
39
+ os.makedirs(self._tile_directory, exist_ok=True)
40
+
41
+ self.logger = logger
42
+
43
+ @property
44
+ def coordinates(self) -> tuple[float, float]:
45
+ """Coordinates of the center point of the DTM data.
46
+
47
+ Returns:
48
+ tuple: Latitude and longitude of the center point.
49
+ """
50
+ return self._coordinates
51
+
52
+ @property
53
+ def size(self) -> int:
54
+ """Size of the DTM data in meters.
55
+
56
+ Returns:
57
+ int: Size of the DTM data.
58
+ """
59
+ return self._size
60
+
61
+ @property
62
+ def url(self) -> str | None:
63
+ """URL of the provider."""
64
+ return self._url
65
+
66
+ def formatted_url(self, **kwargs) -> str:
67
+ """Formatted URL of the provider."""
68
+ if not self.url:
69
+ raise ValueError("URL must be defined.")
70
+ return self.url.format(**kwargs)
71
+
72
+ @classmethod
73
+ def description(cls) -> str:
74
+ """Description of the provider.
75
+
76
+ Returns:
77
+ str: Provider description.
78
+ """
79
+ return f"{cls._icon} {cls._region} [{cls._resolution} m/px] {cls._name}"
80
+
81
+ @classmethod
82
+ def get_provider_by_code(cls, code: str) -> Type[DTMProvider] | None:
83
+ """Get a provider by its code.
84
+
85
+ Arguments:
86
+ code (str): Provider code.
87
+
88
+ Returns:
89
+ DTMProvider: Provider class or None if not found.
90
+ """
91
+ for provider in cls.__subclasses__():
92
+ if provider._code == code: # pylint: disable=W0212
93
+ return provider
94
+ return None
95
+
96
+ @classmethod
97
+ def get_provider_descriptions(cls) -> dict[str, str]:
98
+ """Get descriptions of all providers, where keys are provider codes and
99
+ values are provider descriptions.
100
+
101
+ Returns:
102
+ dict: Provider descriptions.
103
+ """
104
+ providers = {}
105
+ for provider in cls.__subclasses__():
106
+ providers[provider._code] = provider.description() # pylint: disable=W0212
107
+ return providers # type: ignore
108
+
109
+ def download_tile(self, output_path: str, **kwargs) -> bool:
110
+ """Download a tile from the provider.
111
+
112
+ Arguments:
113
+ output_path (str): Path to save the downloaded tile.
114
+
115
+ Returns:
116
+ bool: True if the tile was downloaded successfully, False otherwise.
117
+ """
118
+ url = self.formatted_url(**kwargs)
119
+ response = requests.get(url, stream=True, timeout=10)
120
+ if response.status_code == 200:
121
+ with open(output_path, "wb") as file:
122
+ for chunk in response.iter_content(chunk_size=1024):
123
+ file.write(chunk)
124
+ return True
125
+ return False
126
+
127
+ def get_or_download_tile(self, output_path: str, **kwargs) -> str | None:
128
+ """Get or download a tile from the provider.
129
+
130
+ Arguments:
131
+ output_path (str): Path to save the downloaded tile.
132
+
133
+ Returns:
134
+ str: Path to the downloaded tile or None if the tile not exists and was
135
+ not downloaded.
136
+ """
137
+ if not os.path.exists(output_path):
138
+ if not self.download_tile(output_path, **kwargs):
139
+ return None
140
+ return output_path
141
+
142
+ def get_tile_parameters(self, *args, **kwargs) -> dict:
143
+ """Get parameters for the tile, that will be used to format the URL.
144
+ Must be implemented in subclasses.
145
+
146
+ Returns:
147
+ dict: Tile parameters to format the URL.
148
+ """
149
+ raise NotImplementedError
150
+
151
+ def get_numpy(self) -> np.ndarray:
152
+ """Get numpy array of the tile.
153
+ Resulting array must be 16 bit (signed or unsigned) integer and it should be already
154
+ windowed to the bounding box of ROI. It also must have only one channel.
155
+
156
+ Returns:
157
+ np.ndarray: Numpy array of the tile.
158
+ """
159
+ raise NotImplementedError
160
+
161
+ def get_bbox(self) -> tuple[float, float, float, float]:
162
+ """Get bounding box of the tile based on the center point and size.
163
+
164
+ Returns:
165
+ tuple: Bounding box of the tile (north, south, east, west).
166
+ """
167
+ west, south, east, north = ox.utils_geo.bbox_from_point( # type: ignore
168
+ self.coordinates, dist=self.size // 2, project_utm=False
169
+ )
170
+ bbox = north, south, east, west
171
+ return bbox
172
+
173
+ def extract_roi(self, tile_path: str) -> np.ndarray:
174
+ """Extract region of interest (ROI) from the GeoTIFF file.
175
+
176
+ Arguments:
177
+ tile_path (str): Path to the GeoTIFF file.
178
+
179
+ Raises:
180
+ ValueError: If the tile does not contain any data.
181
+
182
+ Returns:
183
+ np.ndarray: Numpy array of the ROI.
184
+ """
185
+ north, south, east, west = self.get_bbox()
186
+ with rasterio.open(tile_path) as src:
187
+ self.logger.debug("Opened tile, shape: %s, dtype: %s.", src.shape, src.dtypes[0])
188
+ window = rasterio.windows.from_bounds(west, south, east, north, src.transform)
189
+ self.logger.debug(
190
+ "Window parameters. Column offset: %s, row offset: %s, width: %s, height: %s.",
191
+ window.col_off,
192
+ window.row_off,
193
+ window.width,
194
+ window.height,
195
+ )
196
+ data = src.read(1, window=window)
197
+ if not data.size > 0:
198
+ raise ValueError("No data in the tile.")
199
+
200
+ return data
201
+
202
+
203
+ class SRTM30Provider(DTMProvider):
204
+ """Provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
205
+
206
+ _code = "srtm30"
207
+ _name = "SRTM 30 m"
208
+ _region = "Global"
209
+ _icon = "🌎"
210
+ _resolution = 30.0
211
+
212
+ _url = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
213
+
214
+ def __init__(self, *args, **kwargs):
215
+ super().__init__(*args, **kwargs)
216
+ self.hgt_directory = os.path.join(self._tile_directory, "hgt")
217
+ self.gz_directory = os.path.join(self._tile_directory, "gz")
218
+ os.makedirs(self.hgt_directory, exist_ok=True)
219
+ os.makedirs(self.gz_directory, exist_ok=True)
220
+
221
+ def get_tile_parameters(self, *args, **kwargs) -> dict[str, str]:
222
+ """Returns latitude band and tile name for SRTM tile from coordinates.
223
+
224
+ Arguments:
225
+ lat (float): Latitude.
226
+ lon (float): Longitude.
227
+
228
+ Returns:
229
+ dict: Tile parameters.
230
+ """
231
+ lat, lon = args
232
+
233
+ tile_latitude = math.floor(lat)
234
+ tile_longitude = math.floor(lon)
235
+
236
+ latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
237
+ if lon < 0:
238
+ tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
239
+ else:
240
+ tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
241
+
242
+ self.logger.debug(
243
+ "Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
244
+ )
245
+ return {"latitude_band": latitude_band, "tile_name": tile_name}
246
+
247
+ def get_numpy(self) -> np.ndarray:
248
+ """Get numpy array of the tile.
249
+
250
+ Returns:
251
+ np.ndarray: Numpy array of the tile.
252
+ """
253
+ tile_parameters = self.get_tile_parameters(*self.coordinates)
254
+ tile_name = tile_parameters["tile_name"]
255
+ decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
256
+
257
+ if not os.path.isfile(decompressed_tile_path):
258
+ compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
259
+ if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
260
+ raise FileNotFoundError(f"Tile {tile_name} not found.")
261
+
262
+ with gzip.open(compressed_tile_path, "rb") as f_in:
263
+ with open(decompressed_tile_path, "wb") as f_out:
264
+ shutil.copyfileobj(f_in, f_out)
265
+
266
+ 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
@@ -10,6 +10,7 @@ from typing import Any, Generator
10
10
  from pydantic import BaseModel
11
11
 
12
12
  from maps4fs.generator.component import Component
13
+ from maps4fs.generator.dtm import DTMProvider
13
14
  from maps4fs.generator.game import Game
14
15
  from maps4fs.logger import Logger
15
16
 
@@ -91,8 +92,8 @@ class BackgroundSettings(SettingsModel):
91
92
  It will be used as 1 / resize_factor of the original size.
92
93
  """
93
94
 
94
- generate_background: bool = True
95
- generate_water: bool = True
95
+ generate_background: bool = False
96
+ generate_water: bool = False
96
97
  resize_factor: int = 8
97
98
 
98
99
 
@@ -146,6 +147,19 @@ class SplineSettings(SettingsModel):
146
147
  spline_density: int = 2
147
148
 
148
149
 
150
+ class SatelliteSettings(SettingsModel):
151
+ """Represents the advanced settings for satellite component.
152
+
153
+ Attributes:
154
+ download_images (bool): download satellite images.
155
+ margin (int): margin around the map.
156
+ """
157
+
158
+ download_images: bool = False
159
+ satellite_margin: int = 100
160
+ zoom_level: int = 14
161
+
162
+
149
163
  # pylint: disable=R0913, R0902, R0914
150
164
  class Map:
151
165
  """Class used to generate map using all components.
@@ -161,6 +175,7 @@ class Map:
161
175
  def __init__( # pylint: disable=R0917, R0915
162
176
  self,
163
177
  game: Game,
178
+ dtm_provider: DTMProvider,
164
179
  coordinates: tuple[float, float],
165
180
  size: int,
166
181
  rotation: int,
@@ -173,6 +188,7 @@ class Map:
173
188
  i3d_settings: I3DSettings = I3DSettings(),
174
189
  texture_settings: TextureSettings = TextureSettings(),
175
190
  spline_settings: SplineSettings = SplineSettings(),
191
+ satellite_settings: SatelliteSettings = SatelliteSettings(),
176
192
  **kwargs,
177
193
  ):
178
194
  if not logger:
@@ -189,6 +205,7 @@ class Map:
189
205
  self.rotated_size = int(size * rotation_multiplier)
190
206
 
191
207
  self.game = game
208
+ self.dtm_provider = dtm_provider
192
209
  self.components: list[Component] = []
193
210
  self.coordinates = coordinates
194
211
  self.map_directory = map_directory
@@ -217,6 +234,7 @@ class Map:
217
234
  self.logger.info("Texture settings: %s", texture_settings)
218
235
  self.spline_settings = spline_settings
219
236
  self.logger.info("Spline settings: %s", spline_settings)
237
+ self.satellite_settings = satellite_settings
220
238
 
221
239
  os.makedirs(self.map_directory, exist_ok=True)
222
240
  self.logger.debug("Map directory created: %s", self.map_directory)
@@ -228,6 +246,7 @@ class Map:
228
246
  i3d_settings,
229
247
  texture_settings,
230
248
  spline_settings,
249
+ satellite_settings,
231
250
  ]
232
251
 
233
252
  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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maps4fs
3
- Version: 1.5.2
3
+ Version: 1.5.4
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: MIT License
@@ -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
- ## Splines Advanced settings
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,23 @@
1
- maps4fs/__init__.py,sha256=LMzzORK3Q3OjXmmRJ03CpS2SMP6zTwKNnUUei3P7s40,300
1
+ maps4fs/__init__.py,sha256=wXI0wsgNpS2Pr7GKPw4akJyChQzehMXNuwjF7vG81tA,346
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=58UQgdR-7KlWHTfwLesNNK76BTRsiVngRa6B64OKjhc,20065
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=MZf3ZjawJ977TxqB1q9nNpvPZUNwfmm2EaJDtVU-eCU,15939
8
- maps4fs/generator/game.py,sha256=jjo7CTwHHSkRpeD_QgRXkhR_NxI09C4kMxz-nYOTM4A,7931
7
+ maps4fs/generator/dem.py,sha256=hW9zNKX-MSUI-Cw7-x8tNMGF8NyzdVDLNjsCx4wHfuQ,12563
8
+ maps4fs/generator/dtm.py,sha256=DsQsV4t0124-2xdr8L29JX-csD0XNOdCF0MFm1cZQF0,8872
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=jIdekpiymhHqKx4FaAwjtq3hMnRdKYo6TvJLX1fSD0k,12814
12
+ maps4fs/generator/map.py,sha256=wLu8kexBgvHTO_RGnUupWoD-LMsoPXjNuPivgVH3zTw,13420
12
13
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
14
+ maps4fs/generator/satellite.py,sha256=Qnb6XxmXKnHdHKVMb9mJ3vDGtGkDHCOv_81hrrXdx3k,3660
13
15
  maps4fs/generator/texture.py,sha256=sErusfv1AqQfP-veMrZ921Tz8DnGEhfB4ucggMmKrD4,31231
14
16
  maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
15
17
  maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
16
18
  maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
17
- maps4fs-1.5.2.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
18
- maps4fs-1.5.2.dist-info/METADATA,sha256=Hj01MkOYD87umoXLB2iwwjcS2eGxuUdYGv4SyYWO9Xw,35026
19
- maps4fs-1.5.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
20
- maps4fs-1.5.2.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
21
- maps4fs-1.5.2.dist-info/RECORD,,
19
+ maps4fs-1.5.4.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
20
+ maps4fs-1.5.4.dist-info/METADATA,sha256=UkTftX1zaae9QM1r7t994hyhk10dOASfocXg8KKbRRA,36012
21
+ maps4fs-1.5.4.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
22
+ maps4fs-1.5.4.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
23
+ maps4fs-1.5.4.dist-info/RECORD,,