maps4fs 1.8.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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")