maps4fs 1.8.0__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.
@@ -0,0 +1,39 @@
1
+ """This module contains provider of Niedersachsen data."""
2
+
3
+ from maps4fs.generator.dtm.base.wms import WMSProvider
4
+ from maps4fs.generator.dtm.dtm import DTMProvider
5
+
6
+
7
+ # pylint: disable=R0801
8
+ class NiedersachsenProvider(WMSProvider, DTMProvider):
9
+ """Provider of Niedersachsen data."""
10
+
11
+ _code = "niedersachsen"
12
+ _name = "Lower Saxony DGM1"
13
+ _region = "DE"
14
+ _icon = "🇩🇪󠁥󠁢󠁹󠁿"
15
+ _resolution = 1
16
+ _author = "[kbrandwijk](https://github.com/kbrandwijk)"
17
+ _is_community = True
18
+ _instructions = (
19
+ "Warning: The Niedersachsen DGM1 data is provided as 8-bit Cloud Optimized GeoTIFF "
20
+ "(whole meters only). You will need to use blur ('Blur Radius' under 'DEM Settings') "
21
+ "to smooth the data."
22
+ )
23
+ _is_base = False
24
+ _extents = (54.148101, 51.153098, 11.754046, 6.505772)
25
+
26
+ _url = "https://opendata.lgln.niedersachsen.de/doorman/noauth/dgm_wms"
27
+ _source_crs = "EPSG:25832"
28
+ _tile_size = 2000
29
+ _wms_version = "1.3.0"
30
+
31
+ def get_wms_parameters(self, tile):
32
+ return {
33
+ "layers": ["ni_dgm1_grau"],
34
+ "srs": "EPSG:25832",
35
+ "bbox": (tile[1], tile[0], tile[3], tile[2]),
36
+ "size": (2000, 2000),
37
+ "format": "image/tiff",
38
+ "transparent": False,
39
+ }
@@ -0,0 +1,30 @@
1
+ """This module contains provider of NRW data."""
2
+
3
+ from maps4fs.generator.dtm.base.wcs import WCSProvider
4
+ from maps4fs.generator.dtm.dtm import DTMProvider
5
+
6
+
7
+ class NRWProvider(WCSProvider, DTMProvider):
8
+ """Provider of NRW data."""
9
+
10
+ _code = "NRW"
11
+ _name = "North Rhine-Westphalia DGM1"
12
+ _region = "DE"
13
+ _icon = "🇩🇪󠁥󠁢󠁹󠁿"
14
+ _resolution = 1
15
+ _author = "[kbrandwijk](https://github.com/kbrandwijk)"
16
+ _is_community = True
17
+ _is_base = False
18
+ _extents = (52.6008271, 50.1506045, 9.5315425, 5.8923538)
19
+
20
+ _url = "https://www.wcs.nrw.de/geobasis/wcs_nw_dgm"
21
+ _wcs_version = "2.0.1"
22
+ _source_crs = "EPSG:25832"
23
+ _tile_size = 1000
24
+
25
+ def get_wcs_parameters(self, tile: tuple[float, float, float, float]) -> dict:
26
+ return {
27
+ "identifier": ["nw_dgm"],
28
+ "subsets": [("y", str(tile[0]), str(tile[2])), ("x", str(tile[1]), str(tile[3]))],
29
+ "format": "image/tiff",
30
+ }
@@ -0,0 +1,127 @@
1
+ """This module contains provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
2
+
3
+ # Author: https://github.com/iwatkot
4
+
5
+ import gzip
6
+ import math
7
+ import os
8
+ import shutil
9
+
10
+ import requests
11
+
12
+ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
13
+
14
+
15
+ class SRTM30ProviderSettings(DTMProviderSettings):
16
+ """Settings for SRTM 30m provider."""
17
+
18
+
19
+ class SRTM30Provider(DTMProvider):
20
+ """Provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
21
+
22
+ _code = "srtm30"
23
+ _name = "SRTM 30 m"
24
+ _region = "Global"
25
+ _icon = "🌎"
26
+ _resolution = 30.0
27
+
28
+ _url = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
29
+
30
+ _author = "[iwatkot](https://github.com/iwatkot)"
31
+
32
+ _extents = (60, -65, 180, -180)
33
+
34
+ _settings = SRTM30ProviderSettings
35
+
36
+ def __init__(self, *args, **kwargs):
37
+ super().__init__(*args, **kwargs)
38
+ self.hgt_directory = os.path.join(self._tile_directory, "hgt")
39
+ self.gz_directory = os.path.join(self._tile_directory, "gz")
40
+ os.makedirs(self.hgt_directory, exist_ok=True)
41
+ os.makedirs(self.gz_directory, exist_ok=True)
42
+ self.data_info: dict[str, int | str | float] | None = None # type: ignore
43
+
44
+ def download_tiles(self):
45
+ """Download SRTM tiles."""
46
+ north, south, east, west = self.get_bbox()
47
+
48
+ tiles = []
49
+ # Look at each corner of the bbox in case the bbox spans across multiple tiles
50
+ for pair in [(north, east), (south, west), (south, east), (north, west)]:
51
+ tile_parameters = self.get_tile_parameters(*pair)
52
+ tile_name = tile_parameters["tile_name"]
53
+ decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
54
+
55
+ if not os.path.isfile(decompressed_tile_path):
56
+ compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
57
+ if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
58
+ raise FileNotFoundError(f"Tile {tile_name} not found.")
59
+
60
+ with gzip.open(compressed_tile_path, "rb") as f_in:
61
+ with open(decompressed_tile_path, "wb") as f_out:
62
+ shutil.copyfileobj(f_in, f_out)
63
+ tiles.append(decompressed_tile_path)
64
+
65
+ return list(set(tiles))
66
+
67
+ # region provider specific helpers
68
+ def download_tile(self, output_path: str, **kwargs) -> bool:
69
+ """Download a tile from the provider.
70
+
71
+ Arguments:
72
+ output_path (str): Path to save the downloaded tile.
73
+
74
+ Returns:
75
+ bool: True if the tile was downloaded successfully, False otherwise.
76
+ """
77
+ url = self.formatted_url(**kwargs)
78
+ response = requests.get(url, stream=True, timeout=10)
79
+ if response.status_code == 200:
80
+ with open(output_path, "wb") as file:
81
+ for chunk in response.iter_content(chunk_size=1024):
82
+ file.write(chunk)
83
+ return True
84
+ return False
85
+
86
+ def get_or_download_tile(self, output_path: str, **kwargs) -> str | None:
87
+ """Get or download a tile from the provider.
88
+
89
+ Arguments:
90
+ output_path (str): Path to save the downloaded tile.
91
+
92
+ Returns:
93
+ str: Path to the downloaded tile or None if the tile not exists and was
94
+ not downloaded.
95
+ """
96
+ if not os.path.exists(output_path):
97
+ if not self.download_tile(output_path, **kwargs):
98
+ return None
99
+ return output_path
100
+
101
+ def get_tile_parameters(self, *args) -> dict[str, str]:
102
+ """Returns latitude band and tile name for SRTM tile from coordinates.
103
+
104
+ Arguments:
105
+ lat (float): Latitude.
106
+ lon (float): Longitude.
107
+
108
+ Returns:
109
+ dict: Tile parameters.
110
+ """
111
+ lat, lon = args
112
+
113
+ tile_latitude = math.floor(lat)
114
+ tile_longitude = math.floor(lon)
115
+
116
+ latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
117
+ if lon < 0:
118
+ tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
119
+ else:
120
+ tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
121
+
122
+ self.logger.debug(
123
+ "Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
124
+ )
125
+ return {"latitude_band": latitude_band, "tile_name": tile_name}
126
+
127
+ # endregion
@@ -0,0 +1,87 @@
1
+ """This module contains provider of USGS data."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+
6
+ import numpy as np
7
+ import requests
8
+
9
+ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
10
+
11
+
12
+ class USGSProviderSettings(DTMProviderSettings):
13
+ """Settings for the USGS provider."""
14
+
15
+ dataset: tuple | str = (
16
+ "Digital Elevation Model (DEM) 1 meter",
17
+ "Alaska IFSAR 5 meter DEM",
18
+ "National Elevation Dataset (NED) 1/9 arc-second",
19
+ "National Elevation Dataset (NED) 1/3 arc-second",
20
+ "National Elevation Dataset (NED) 1 arc-second",
21
+ "National Elevation Dataset (NED) Alaska 2 arc-second",
22
+ "Original Product Resolution (OPR) Digital Elevation Model (DEM)",
23
+ )
24
+
25
+
26
+ class USGSProvider(DTMProvider):
27
+ """Provider of USGS."""
28
+
29
+ _code = "USGS"
30
+ _name = "USGS"
31
+ _region = "USA"
32
+ _icon = "🇺🇸"
33
+ _resolution = "variable"
34
+ _data: np.ndarray | None = None
35
+ _settings = USGSProviderSettings
36
+ _author = "[ZenJakey](https://github.com/ZenJakey)"
37
+ _contributors = "[kbrandwijk](https://github.com/kbrandwijk)"
38
+ _is_community = True
39
+ _instructions = None
40
+ _extents = (50.0, 17.0, -64.0, -162.0)
41
+
42
+ _url = "https://tnmaccess.nationalmap.gov/api/v1/products?prodFormats=GeoTIFF,IMG"
43
+
44
+ def download_tiles(self):
45
+ download_urls = self.get_download_urls()
46
+ all_tif_files = self.download_tif_files(download_urls, self.shared_tiff_path)
47
+ return all_tif_files
48
+
49
+ def __init__(self, *args, **kwargs):
50
+ super().__init__(*args, **kwargs)
51
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
52
+ self.shared_tiff_path = os.path.join(self._tile_directory, "shared")
53
+ os.makedirs(self.shared_tiff_path, exist_ok=True)
54
+ self.output_path = os.path.join(self._tile_directory, f"timestamp_{timestamp}")
55
+ os.makedirs(self.output_path, exist_ok=True)
56
+
57
+ def get_download_urls(self) -> list[str]:
58
+ """Get download URLs of the GeoTIFF files from the USGS API.
59
+
60
+ Returns:
61
+ list: List of download URLs.
62
+ """
63
+ urls = []
64
+ try:
65
+ # Make the GET request
66
+ (north, south, east, west) = self.get_bbox()
67
+ response = requests.get( # pylint: disable=W3101
68
+ self.url # type: ignore
69
+ + f"&datasets={self.user_settings.dataset}" # type: ignore
70
+ + f"&bbox={west},{north},{east},{south}"
71
+ )
72
+ self.logger.debug("Getting file locations from USGS...")
73
+
74
+ # Check if the request was successful (HTTP status code 200)
75
+ if response.status_code == 200:
76
+ # Parse the JSON response
77
+ json_data = response.json()
78
+ items = json_data["items"]
79
+ for item in items:
80
+ urls.append(item["downloadURL"])
81
+ # self.download_tif_files(urls)
82
+ else:
83
+ self.logger.error("Failed to get data. HTTP Status Code: %s", response.status_code)
84
+ except requests.exceptions.RequestException as e:
85
+ self.logger.error("Failed to get data. Error: %s", e)
86
+ self.logger.debug("Received %s urls", len(urls))
87
+ return urls
@@ -0,0 +1,61 @@
1
+ """Utility functions for the DTM generator."""
2
+
3
+ import numpy as np
4
+ from pyproj import Transformer
5
+
6
+
7
+ def tile_bbox(
8
+ bbox: tuple[float, float, float, float], tile_size: float
9
+ ) -> list[tuple[float, float, float, float]]:
10
+ """Tile the bounding box into smaller bounding boxes of a specified size.
11
+
12
+ Arguments:
13
+ bbox (tuple): Bounding box to tile (north, south, east, west).
14
+ tile_size (int): Size of the tiles in meters.
15
+
16
+ Returns:
17
+ list: List of smaller bounding boxes (north, south, east, west).
18
+ """
19
+ north, south, east, west = bbox
20
+ x_coords = np.arange(west, east, tile_size if east > west else -tile_size)
21
+ y_coords = np.arange(north, south, tile_size if south > north else -tile_size)
22
+ x_coords = np.append(x_coords, east).astype(x_coords.dtype)
23
+ y_coords = np.append(y_coords, south).astype(y_coords.dtype)
24
+
25
+ x_min, y_min = np.meshgrid(x_coords[:-1], y_coords[:-1], indexing="ij")
26
+ x_max, y_max = np.meshgrid(x_coords[1:], y_coords[1:], indexing="ij")
27
+
28
+ tiles = np.stack(
29
+ [x_min.ravel(), y_min.ravel(), x_max.ravel(), y_max.ravel()], axis=1, dtype=float
30
+ )
31
+
32
+ return [tuple(tile[i].item() for i in range(4)) for tile in tiles]
33
+
34
+
35
+ def transform_bbox(
36
+ bbox: tuple[float, float, float, float], to_crs: str
37
+ ) -> tuple[float, float, float, float]:
38
+ """Transform the bounding box to a different coordinate reference system (CRS).
39
+
40
+ Arguments:
41
+ bbox (tuple): Bounding box to transform (north, south, east, west).
42
+ to_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
43
+
44
+ Returns:
45
+ tuple: Transformed bounding box (north, south, east, west).
46
+ """
47
+ transformer = Transformer.from_crs("epsg:4326", to_crs)
48
+ north, south, east, west = bbox
49
+
50
+ # EPSG:4326 is lat, lon, so xx is lat and yy is lon
51
+ bottom_left_x, bottom_left_y = transformer.transform(xx=south, yy=west)
52
+ top_left_x, top_left_y = transformer.transform(xx=north, yy=west)
53
+ top_right_x, top_right_y = transformer.transform(xx=north, yy=east)
54
+ bottom_right_x, bottom_right_y = transformer.transform(xx=south, yy=east)
55
+
56
+ west = min(bottom_left_y, bottom_right_y)
57
+ east = max(top_left_y, top_right_y)
58
+ north = min(bottom_left_x, top_left_x)
59
+ south = max(bottom_right_x, top_right_x)
60
+
61
+ return north, south, east, west
@@ -0,0 +1,247 @@
1
+ """This module contains the Game class and its subclasses. Game class is used to define
2
+ different versions of the game for which the map is generated. Each game has its own map
3
+ template file and specific settings for map generation."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+
9
+ from maps4fs.generator.background import Background
10
+ from maps4fs.generator.config import Config
11
+ from maps4fs.generator.grle import GRLE
12
+ from maps4fs.generator.i3d import I3d
13
+ from maps4fs.generator.satellite import Satellite
14
+ from maps4fs.generator.texture import Texture
15
+
16
+ working_directory = os.getcwd()
17
+
18
+
19
+ class Game:
20
+ """Class used to define different versions of the game for which the map is generated.
21
+
22
+ Arguments:
23
+ map_template_path (str, optional): Path to the map template file. Defaults to None.
24
+
25
+ Attributes and Properties:
26
+ code (str): The code of the game.
27
+ components (list[Type[Component]]): List of components used for map generation.
28
+ map_template_path (str): Path to the map template file.
29
+
30
+ Public Methods:
31
+ from_code(cls, code: str) -> Game: Returns the game instance based on the game code.
32
+ """
33
+
34
+ code: str | None = None
35
+ dem_multipliyer: int = 2
36
+ _additional_dem_name: str | None = None
37
+ _map_template_path: str | None = None
38
+ _texture_schema: str | None = None
39
+ _grle_schema: str | None = None
40
+ _tree_schema: str | None = None
41
+
42
+ # Order matters! Some components depend on others.
43
+ components = [Texture, Background, GRLE, I3d, Config, Satellite]
44
+
45
+ def __init__(self, map_template_path: str | None = None):
46
+ if map_template_path:
47
+ self._map_template_path = map_template_path
48
+
49
+ def map_xml_path(self, map_directory: str) -> str:
50
+ """Returns the path to the map.xml file.
51
+
52
+ Arguments:
53
+ map_directory (str): The path to the map directory.
54
+
55
+ Returns:
56
+ str: The path to the map.xml file.
57
+ """
58
+ return os.path.join(map_directory, "maps", "map", "map.xml")
59
+
60
+ @classmethod
61
+ def from_code(cls, code: str, map_template_path: str | None = None) -> Game:
62
+ """Returns the game instance based on the game code.
63
+
64
+ Arguments:
65
+ code (str): The code of the game.
66
+ map_template_path (str, optional): Path to the map template file. Defaults to None.
67
+
68
+ Returns:
69
+ Game: The game instance.
70
+ """
71
+ for game in cls.__subclasses__():
72
+ if game.code and game.code.lower() == code.lower():
73
+ return game(map_template_path)
74
+ raise ValueError(f"Game with code {code} not found.")
75
+
76
+ @property
77
+ def template_path(self) -> str:
78
+ """Returns the path to the map template file.
79
+
80
+ Raises:
81
+ ValueError: If the map template path is not set.
82
+
83
+ Returns:
84
+ str: The path to the map template file."""
85
+ if not self._map_template_path:
86
+ raise ValueError("Map template path not set.")
87
+ return self._map_template_path
88
+
89
+ @property
90
+ def texture_schema(self) -> str:
91
+ """Returns the path to the texture layers schema file.
92
+
93
+ Raises:
94
+ ValueError: If the texture layers schema path is not set.
95
+
96
+ Returns:
97
+ str: The path to the texture layers schema file."""
98
+ if not self._texture_schema:
99
+ raise ValueError("Texture layers schema path not set.")
100
+ return self._texture_schema
101
+
102
+ @property
103
+ def grle_schema(self) -> str:
104
+ """Returns the path to the GRLE layers schema file.
105
+
106
+ Raises:
107
+ ValueError: If the GRLE layers schema path is not set.
108
+
109
+ Returns:
110
+ str: The path to the GRLE layers schema file."""
111
+ if not self._grle_schema:
112
+ raise ValueError("GRLE layers schema path not set.")
113
+ return self._grle_schema
114
+
115
+ @property
116
+ def tree_schema(self) -> str:
117
+ """Returns the path to the tree layers schema file.
118
+
119
+ Raises:
120
+ ValueError: If the tree layers schema path is not set.
121
+
122
+ Returns:
123
+ str: The path to the tree layers schema file."""
124
+ if not self._tree_schema:
125
+ raise ValueError("Tree layers schema path not set.")
126
+ return self._tree_schema
127
+
128
+ def dem_file_path(self, map_directory: str) -> str:
129
+ """Returns the path to the DEM file.
130
+
131
+ Arguments:
132
+ map_directory (str): The path to the map directory.
133
+
134
+ Returns:
135
+ str: The path to the DEM file.
136
+ """
137
+ raise NotImplementedError
138
+
139
+ def weights_dir_path(self, map_directory: str) -> str:
140
+ """Returns the path to the weights directory.
141
+
142
+ Arguments:
143
+ map_directory (str): The path to the map directory.
144
+
145
+ Returns:
146
+ str: The path to the weights directory."""
147
+ raise NotImplementedError
148
+
149
+ def i3d_file_path(self, map_directory: str) -> str:
150
+ """Returns the path to the i3d file.
151
+
152
+ Arguments:
153
+ map_directory (str): The path to the map directory.
154
+
155
+ Returns:
156
+ str: The path to the i3d file."""
157
+ raise NotImplementedError
158
+
159
+ @property
160
+ def additional_dem_name(self) -> str | None:
161
+ """Returns the name of the additional DEM file.
162
+
163
+ Returns:
164
+ str | None: The name of the additional DEM file."""
165
+ return self._additional_dem_name
166
+
167
+
168
+ # pylint: disable=W0223
169
+ class FS22(Game):
170
+ """Class used to define the game version FS22."""
171
+
172
+ code = "FS22"
173
+ _map_template_path = os.path.join(working_directory, "data", "fs22-map-template.zip")
174
+ _texture_schema = os.path.join(working_directory, "data", "fs22-texture-schema.json")
175
+
176
+ def dem_file_path(self, map_directory: str) -> str:
177
+ """Returns the path to the DEM file.
178
+
179
+ Arguments:
180
+ map_directory (str): The path to the map directory.
181
+
182
+ Returns:
183
+ str: The path to the DEM file."""
184
+ return os.path.join(map_directory, "maps", "map", "data", "map_dem.png")
185
+
186
+ def weights_dir_path(self, map_directory: str) -> str:
187
+ """Returns the path to the weights directory.
188
+
189
+ Arguments:
190
+ map_directory (str): The path to the map directory.
191
+
192
+ Returns:
193
+ str: The path to the weights directory."""
194
+ return os.path.join(map_directory, "maps", "map", "data")
195
+
196
+
197
+ class FS25(Game):
198
+ """Class used to define the game version FS25."""
199
+
200
+ code = "FS25"
201
+ dem_multipliyer: int = 2
202
+ _additional_dem_name = "unprocessedHeightMap.png"
203
+ _map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip")
204
+ _texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json")
205
+ _grle_schema = os.path.join(working_directory, "data", "fs25-grle-schema.json")
206
+ _tree_schema = os.path.join(working_directory, "data", "fs25-tree-schema.json")
207
+
208
+ def dem_file_path(self, map_directory: str) -> str:
209
+ """Returns the path to the DEM file.
210
+
211
+ Arguments:
212
+ map_directory (str): The path to the map directory.
213
+
214
+ Returns:
215
+ str: The path to the DEM file."""
216
+ return os.path.join(map_directory, "map", "data", "dem.png")
217
+
218
+ def map_xml_path(self, map_directory: str) -> str:
219
+ """Returns the path to the map.xml file.
220
+
221
+ Arguments:
222
+ map_directory (str): The path to the map directory.
223
+
224
+ Returns:
225
+ str: The path to the map.xml file.
226
+ """
227
+ return os.path.join(map_directory, "map", "map.xml")
228
+
229
+ def weights_dir_path(self, map_directory: str) -> str:
230
+ """Returns the path to the weights directory.
231
+
232
+ Arguments:
233
+ map_directory (str): The path to the map directory.
234
+
235
+ Returns:
236
+ str: The path to the weights directory."""
237
+ return os.path.join(map_directory, "map", "data")
238
+
239
+ def i3d_file_path(self, map_directory: str) -> str:
240
+ """Returns the path to the i3d file.
241
+
242
+ Arguments:
243
+ map_directory (str): The path to the map directory.
244
+
245
+ Returns:
246
+ str: The path to the i3d file."""
247
+ return os.path.join(map_directory, "map", "map.i3d")