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 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,
@@ -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,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
- 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.")
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
- 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)
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 not data.size > 0:
159
- self.logger.warning("DEM data is empty, DEM file will be filled with zeros.")
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 read from SRTM file. Shape: %s, dtype: %s. Min: %s, max: %s.",
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")
@@ -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.2
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
- ## 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,24 @@
1
- maps4fs/__init__.py,sha256=LMzzORK3Q3OjXmmRJ03CpS2SMP6zTwKNnUUei3P7s40,300
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=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=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=jIdekpiymhHqKx4FaAwjtq3hMnRdKYo6TvJLX1fSD0k,12814
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.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,,
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,,