maps4fs 1.5.7__py3-none-any.whl → 1.6.91__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,226 @@
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 numpy as np
11
+
12
+ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
13
+
14
+
15
+ class SRTM30ProviderSettings(DTMProviderSettings):
16
+ """Settings for SRTM 30m provider."""
17
+
18
+ easy_mode: bool = True
19
+ power_factor: int = 0
20
+
21
+
22
+ class SRTM30Provider(DTMProvider):
23
+ """Provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
24
+
25
+ _code = "srtm30"
26
+ _name = "SRTM 30 m"
27
+ _region = "Global"
28
+ _icon = "🌎"
29
+ _resolution = 30.0
30
+
31
+ _url = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
32
+
33
+ _author = "[iwatkot](https://github.com/iwatkot)"
34
+
35
+ _instructions = (
36
+ "ℹ️ If you don't know how to work with DEM data, it is recommended to use the "
37
+ "**Easy mode** option. It will automatically change the values in the image, so the "
38
+ "terrain will be visible in the Giants Editor. If you're an experienced modder, it's "
39
+ "recommended to disable this option and work with the DEM data in a usual way. \n"
40
+ "ℹ️ If the terrain height difference in the real world is bigger than 255 meters, "
41
+ "the [Height scale](https://github.com/iwatkot/maps4fs/blob/main/docs/dem.md#height-scale)"
42
+ " parameter in the **map.i3d** file will be changed automatically. \n"
43
+ "⚡ If the **Easy mode** option is disabled, you will probably get completely flat "
44
+ "terrain, unless you adjust the DEM Multiplier Setting or the Height scale parameter in "
45
+ "the Giants Editor. \n"
46
+ "💡 You can use the **Power factor** setting to make the difference between heights "
47
+ "bigger. Be extremely careful with this setting, and use only low values, otherwise your "
48
+ "terrain may be completely broken. \n"
49
+ )
50
+
51
+ _settings = SRTM30ProviderSettings
52
+
53
+ def __init__(self, *args, **kwargs):
54
+ super().__init__(*args, **kwargs)
55
+ self.hgt_directory = os.path.join(self._tile_directory, "hgt")
56
+ self.gz_directory = os.path.join(self._tile_directory, "gz")
57
+ os.makedirs(self.hgt_directory, exist_ok=True)
58
+ os.makedirs(self.gz_directory, exist_ok=True)
59
+ self.data_info: dict[str, int | str | float] | None = None # type: ignore
60
+
61
+ def get_tile_parameters(self, *args, **kwargs) -> dict[str, str]:
62
+ """Returns latitude band and tile name for SRTM tile from coordinates.
63
+
64
+ Arguments:
65
+ lat (float): Latitude.
66
+ lon (float): Longitude.
67
+
68
+ Returns:
69
+ dict: Tile parameters.
70
+ """
71
+ lat, lon = args
72
+
73
+ tile_latitude = math.floor(lat)
74
+ tile_longitude = math.floor(lon)
75
+
76
+ latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
77
+ if lon < 0:
78
+ tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
79
+ else:
80
+ tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
81
+
82
+ self.logger.debug(
83
+ "Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
84
+ )
85
+ return {"latitude_band": latitude_band, "tile_name": tile_name}
86
+
87
+ def get_numpy(self) -> np.ndarray:
88
+ """Get numpy array of the tile.
89
+
90
+ Returns:
91
+ np.ndarray: Numpy array of the tile.
92
+ """
93
+ tile_parameters = self.get_tile_parameters(*self.coordinates)
94
+ tile_name = tile_parameters["tile_name"]
95
+ decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
96
+
97
+ if not os.path.isfile(decompressed_tile_path):
98
+ compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
99
+ if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
100
+ raise FileNotFoundError(f"Tile {tile_name} not found.")
101
+
102
+ with gzip.open(compressed_tile_path, "rb") as f_in:
103
+ with open(decompressed_tile_path, "wb") as f_out:
104
+ shutil.copyfileobj(f_in, f_out)
105
+
106
+ data = self.extract_roi(decompressed_tile_path)
107
+
108
+ self.data_info = {}
109
+ self.add_numpy_params(data, "original")
110
+
111
+ data = self.signed_to_unsigned(data)
112
+ self.add_numpy_params(data, "grounded")
113
+
114
+ original_deviation = int(self.data_info["original_deviation"])
115
+ in_game_maximum_height = 65535 // 255
116
+ if original_deviation > in_game_maximum_height:
117
+ suggested_height_scale_multiplier = (
118
+ original_deviation / in_game_maximum_height # type: ignore
119
+ )
120
+ suggested_height_scale_value = int(255 * suggested_height_scale_multiplier)
121
+ else:
122
+ suggested_height_scale_multiplier = 1
123
+ suggested_height_scale_value = 255
124
+
125
+ self.data_info["suggested_height_scale_multiplier"] = suggested_height_scale_multiplier
126
+ self.data_info["suggested_height_scale_value"] = suggested_height_scale_value
127
+
128
+ self.map.shared_settings.height_scale_multiplier = ( # type: ignore
129
+ suggested_height_scale_multiplier
130
+ )
131
+ self.map.shared_settings.height_scale_value = suggested_height_scale_value # type: ignore
132
+
133
+ if self.user_settings.easy_mode: # type: ignore
134
+ try:
135
+ data = self.normalize_dem(data)
136
+ self.add_numpy_params(data, "normalized")
137
+
138
+ normalized_deviation = self.data_info["normalized_deviation"]
139
+ z_scaling_factor = normalized_deviation / original_deviation # type: ignore
140
+ self.data_info["z_scaling_factor"] = z_scaling_factor
141
+
142
+ self.map.shared_settings.mesh_z_scaling_factor = z_scaling_factor # type: ignore
143
+ self.map.shared_settings.change_height_scale = True # type: ignore
144
+
145
+ except Exception as e: # pylint: disable=W0718
146
+ self.logger.error(
147
+ "Failed to normalize DEM data. Error: %s. Using original data.", e
148
+ )
149
+
150
+ return data
151
+
152
+ def add_numpy_params(
153
+ self,
154
+ data: np.ndarray,
155
+ prefix: str,
156
+ ) -> None:
157
+ """Add numpy array parameters to the data_info dictionary.
158
+
159
+ Arguments:
160
+ data (np.ndarray): Numpy array of the tile.
161
+ prefix (str): Prefix for the parameters.
162
+ """
163
+ self.data_info[f"{prefix}_minimum_height"] = int(data.min()) # type: ignore
164
+ self.data_info[f"{prefix}_maximum_height"] = int(data.max()) # type: ignore
165
+ self.data_info[f"{prefix}_deviation"] = int(data.max() - data.min()) # type: ignore
166
+ self.data_info[f"{prefix}_unique_values"] = int(np.unique(data).size) # type: ignore
167
+
168
+ def signed_to_unsigned(self, data: np.ndarray, add_one: bool = True) -> np.ndarray:
169
+ """Convert signed 16-bit integer to unsigned 16-bit integer.
170
+
171
+ Arguments:
172
+ data (np.ndarray): DEM data from SRTM file after cropping.
173
+
174
+ Returns:
175
+ np.ndarray: Unsigned DEM data.
176
+ """
177
+ data = data - data.min()
178
+ if add_one:
179
+ data = data + 1
180
+ return data.astype(np.uint16)
181
+
182
+ def normalize_dem(self, data: np.ndarray) -> np.ndarray:
183
+ """Normalize DEM data to 16-bit unsigned integer using max height from settings.
184
+
185
+ Arguments:
186
+ data (np.ndarray): DEM data from SRTM file after cropping.
187
+
188
+ Returns:
189
+ np.ndarray: Normalized DEM data.
190
+ """
191
+ maximum_height = int(data.max())
192
+ minimum_height = int(data.min())
193
+ deviation = maximum_height - minimum_height
194
+ self.logger.debug(
195
+ "Maximum height: %s. Minimum height: %s. Deviation: %s.",
196
+ maximum_height,
197
+ minimum_height,
198
+ deviation,
199
+ )
200
+ self.logger.debug("Number of unique values in original DEM data: %s.", np.unique(data).size)
201
+
202
+ adjusted_maximum_height = maximum_height * 255
203
+ adjusted_maximum_height = min(adjusted_maximum_height, 65535)
204
+ scaling_factor = adjusted_maximum_height / maximum_height
205
+ self.logger.debug(
206
+ "Adjusted maximum height: %s. Scaling factor: %s.",
207
+ adjusted_maximum_height,
208
+ scaling_factor,
209
+ )
210
+
211
+ if self.user_settings.power_factor: # type: ignore
212
+ power_factor = 1 + self.user_settings.power_factor / 10 # type: ignore
213
+ self.logger.debug(
214
+ "Applying power factor: %s to the DEM data.",
215
+ power_factor,
216
+ )
217
+ data = np.power(data, power_factor).astype(np.uint16)
218
+
219
+ normalized_data = np.round(data * scaling_factor).astype(np.uint16)
220
+ self.logger.debug(
221
+ "Normalized data maximum height: %s. Minimum height: %s. Number of unique values: %s.",
222
+ normalized_data.max(),
223
+ normalized_data.min(),
224
+ np.unique(normalized_data).size,
225
+ )
226
+ return normalized_data
@@ -0,0 +1,351 @@
1
+ """This module contains provider of USGS data."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from zipfile import ZipFile
6
+
7
+ import numpy as np
8
+ import rasterio
9
+ import requests
10
+ from rasterio.enums import Resampling
11
+ from rasterio.merge import merge
12
+ from rasterio.warp import calculate_default_transform, reproject
13
+ from rasterio.windows import from_bounds
14
+
15
+ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
16
+
17
+
18
+ class USGSProviderSettings(DTMProviderSettings):
19
+ """Settings for the USGS provider."""
20
+
21
+ max_local_elevation: int = 255
22
+ dataset: tuple | str = (
23
+ 'Digital Elevation Model (DEM) 1 meter',
24
+ 'Alaska IFSAR 5 meter DEM',
25
+ 'National Elevation Dataset (NED) 1/9 arc-second',
26
+ 'National Elevation Dataset (NED) 1/3 arc-second',
27
+ 'National Elevation Dataset (NED) 1 arc-second',
28
+ 'National Elevation Dataset (NED) Alaska 2 arc-second',
29
+ 'Original Product Resolution (OPR) Digital Elevation Model (DEM)',
30
+ )
31
+
32
+
33
+ class USGSProvider(DTMProvider):
34
+ """Provider of USGS."""
35
+
36
+ _code = "USGS"
37
+ _name = "USGS"
38
+ _region = "USA"
39
+ _icon = "🇺🇸"
40
+ _resolution = 'variable'
41
+ _data: np.ndarray | None = None
42
+ _settings = USGSProviderSettings
43
+ _author = "[ZenJakey](https://github.com/ZenJakey)"
44
+ _contributors = "[kbrandwijk](https://github.com/kbrandwijk)"
45
+ _is_community = True
46
+ _instructions = (
47
+ "ℹ️ Set the max local elevation to approx the local max elevation for your area in"
48
+ " meters. This will allow you to use heightScale 255 in GE with minimal tweaking."
49
+ " Setting this value too low can cause a flat map!"
50
+ )
51
+
52
+ _url = (
53
+ "https://tnmaccess.nationalmap.gov/api/v1/products?prodFormats=GeoTIFF,IMG"
54
+
55
+ )
56
+
57
+ def __init__(self, *args, **kwargs):
58
+ super().__init__(*args, **kwargs)
59
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
60
+ self.shared_tiff_path = os.path.join(self._tile_directory, "shared")
61
+ os.makedirs(self.shared_tiff_path, exist_ok=True)
62
+ self.output_path = os.path.join(self._tile_directory, f"timestamp_{timestamp}")
63
+ os.makedirs(self.output_path, exist_ok=True)
64
+
65
+ def get_download_urls(self) -> list[str]:
66
+ """Get download URLs of the GeoTIFF files from the USGS API.
67
+
68
+ Returns:
69
+ list: List of download URLs.
70
+ """
71
+ urls = []
72
+ try:
73
+ # Make the GET request
74
+ (north, south, east, west) = self.get_bbox()
75
+ response = requests.get( # pylint: disable=W3101
76
+ self.url # type: ignore
77
+ + f"&datasets={self.user_settings.dataset}" # type: ignore
78
+ + f"&bbox={west},{north},{east},{south}"
79
+ )
80
+ self.logger.debug("Getting file locations from USGS...")
81
+
82
+ # Check if the request was successful (HTTP status code 200)
83
+ if response.status_code == 200:
84
+ # Parse the JSON response
85
+ json_data = response.json()
86
+ items = json_data["items"]
87
+ for item in items:
88
+ urls.append(item["downloadURL"])
89
+ # self.download_tif_files(urls)
90
+ else:
91
+ self.logger.error("Failed to get data. HTTP Status Code: %s", response.status_code)
92
+ except requests.exceptions.RequestException as e:
93
+ self.logger.error("Failed to get data. Error: %s", e)
94
+ self.logger.debug("Received %s urls", len(urls))
95
+ return urls
96
+
97
+ def download_tif_files(self, urls: list[str]) -> list[str]:
98
+ """Download GeoTIFF files from the given URLs.
99
+
100
+ Arguments:
101
+ urls (list): List of URLs to download GeoTIFF files from.
102
+
103
+ Returns:
104
+ list: List of paths to the downloaded GeoTIFF files.
105
+ """
106
+ tif_files = []
107
+ for url in urls:
108
+ file_name = os.path.basename(url)
109
+ self.logger.debug("Retrieving TIFF: %s", file_name)
110
+ file_path = os.path.join(self.shared_tiff_path, file_name)
111
+ if not os.path.exists(file_path):
112
+ try:
113
+ # Send a GET request to the file URL
114
+ response = requests.get(url, stream=True) # pylint: disable=W3101
115
+ response.raise_for_status() # Raise an error for HTTP status codes 4xx/5xx
116
+
117
+ # Write the content of the response to the file
118
+ with open(file_path, "wb") as file:
119
+ for chunk in response.iter_content(chunk_size=8192): # Download in chunks
120
+ file.write(chunk)
121
+ self.logger.info("File downloaded successfully: %s", file_path)
122
+ if file_name.endswith('.zip'):
123
+ with ZipFile(file_path, "r") as f_in:
124
+ f_in.extract(file_name.replace('.zip', '.img'), self.shared_tiff_path)
125
+ tif_files.append(file_path.replace('.zip', '.img'))
126
+ else:
127
+ tif_files.append(file_path)
128
+ except requests.exceptions.RequestException as e:
129
+ self.logger.error("Failed to download file: %s", e)
130
+ else:
131
+ self.logger.debug("File already exists: %s", file_name)
132
+ if file_name.endswith('.zip'):
133
+ if not os.path.exists(file_path.replace('.zip', '.img')):
134
+ with ZipFile(file_path, "r") as f_in:
135
+ f_in.extract(file_name.replace('.zip', '.img'), self.shared_tiff_path)
136
+ tif_files.append(file_path.replace('.zip', '.img'))
137
+ else:
138
+ tif_files.append(file_path)
139
+
140
+ return tif_files
141
+
142
+ def merge_geotiff(self, input_files: list[str], output_file: str) -> None:
143
+ """Merge multiple GeoTIFF files into a single GeoTIFF file.
144
+
145
+ Arguments:
146
+ input_files (list): List of input GeoTIFF files to merge.
147
+ output_file (str): Path to save the merged GeoTIFF file.
148
+ """
149
+ # Open all input GeoTIFF files as datasets
150
+ self.logger.debug("Merging tiff files...")
151
+ datasets = [rasterio.open(file) for file in input_files]
152
+
153
+ # Merge datasets
154
+ mosaic, out_transform = merge(datasets, nodata=0)
155
+
156
+ # Get metadata from the first file and update it for the output
157
+ out_meta = datasets[0].meta.copy()
158
+ out_meta.update(
159
+ {
160
+ "driver": "GTiff",
161
+ "height": mosaic.shape[1],
162
+ "width": mosaic.shape[2],
163
+ "transform": out_transform,
164
+ "count": mosaic.shape[0], # Number of bands
165
+ }
166
+ )
167
+
168
+ # Write merged GeoTIFF to the output file
169
+ with rasterio.open(output_file, "w", **out_meta) as dest:
170
+ dest.write(mosaic)
171
+
172
+ self.logger.debug("GeoTIFF images merged successfully into %s", output_file)
173
+
174
+ def reproject_geotiff(self, input_tiff: str, output_tiff: str, target_crs: str) -> None:
175
+ """Reproject a GeoTIFF file to a new coordinate reference system (CRS).
176
+
177
+ Arguments:
178
+ input_tiff (str): Path to the input GeoTIFF file.
179
+ output_tiff (str): Path to save the reprojected GeoTIFF file.
180
+ target_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
181
+ """
182
+ # Open the source GeoTIFF
183
+ self.logger.debug("Reprojecting GeoTIFF to %s CRS...", target_crs)
184
+ with rasterio.open(input_tiff) as src:
185
+ # Get the transform, width, and height of the target CRS
186
+ transform, width, height = calculate_default_transform(
187
+ src.crs, target_crs, src.width, src.height, *src.bounds
188
+ )
189
+
190
+ # Update the metadata for the target GeoTIFF
191
+ kwargs = src.meta.copy()
192
+ kwargs.update(
193
+ {"crs": target_crs, "transform": transform, "width": width, "height": height}
194
+ )
195
+
196
+ # Open the destination GeoTIFF file and reproject
197
+ with rasterio.open(output_tiff, "w", **kwargs) as dst:
198
+ for i in range(1, src.count + 1): # Iterate over all raster bands
199
+ reproject(
200
+ source=rasterio.band(src, i),
201
+ destination=rasterio.band(dst, i),
202
+ src_transform=src.transform,
203
+ src_crs=src.crs,
204
+ dst_transform=transform,
205
+ dst_crs=target_crs,
206
+ resampling=Resampling.nearest, # Choose resampling method
207
+ )
208
+ self.logger.debug("Reprojected GeoTIFF saved to %s", output_tiff)
209
+
210
+ def extract_roi(self, input_tiff: str) -> np.ndarray: # pylint: disable=W0237
211
+ """
212
+ Crop a GeoTIFF based on given geographic bounding box and save to a new file.
213
+
214
+ Arguments:
215
+ input_tiff (str): Path to the input GeoTIFF file.
216
+
217
+ Returns:
218
+ np.ndarray: Numpy array of the cropped GeoTIFF.
219
+ """
220
+ self.logger.debug("Extracting ROI...")
221
+ # Open the input GeoTIFF
222
+ with rasterio.open(input_tiff) as src:
223
+
224
+ # Create a rasterio window from the bounding box
225
+ (north, south, east, west) = self.get_bbox()
226
+ window = from_bounds(west, south, east, north, transform=src.transform)
227
+
228
+ data = src.read(1, window=window)
229
+ self.logger.debug("Extracted ROI")
230
+ return data
231
+
232
+ # pylint: disable=R0914, R0917, R0913
233
+ def convert_geotiff_to_geotiff(
234
+ self,
235
+ input_tiff: str,
236
+ output_tiff: str,
237
+ min_height: float,
238
+ max_height: float,
239
+ target_crs: str,
240
+ ) -> None:
241
+ """
242
+ Convert a GeoTIFF to a scaled GeoTIFF with UInt16 values using a specific coordinate
243
+ system and output size.
244
+
245
+ Arguments:
246
+ input_tiff (str): Path to the input GeoTIFF file.
247
+ output_tiff (str): Path to save the output GeoTIFF file.
248
+ min_height (float): Minimum terrain height (input range).
249
+ max_height (float): Maximum terrain height (input range).
250
+ target_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
251
+ """
252
+ # Open the input GeoTIFF file
253
+ self.logger.debug("Converting to uint16")
254
+ with rasterio.open(input_tiff) as src:
255
+ # Ensure the input CRS matches the target CRS (reprojection may be required)
256
+ if str(src.crs) != str(target_crs):
257
+ raise ValueError(
258
+ f"The GeoTIFF CRS is {src.crs}, but the target CRS is {target_crs}. "
259
+ "Reprojection may be required."
260
+ )
261
+
262
+ # Read the data from the first band
263
+ data = src.read(1) # Assuming the input GeoTIFF has only a single band
264
+
265
+ # Identify the input file's NoData value
266
+ input_nodata = src.nodata
267
+ if input_nodata is None:
268
+ input_nodata = -999999.0 # Default fallback if no NoData value is defined
269
+ nodata_value = 0
270
+ # Replace NoData values (e.g., -999999.0) with the new NoData value
271
+ # (e.g., 65535 for UInt16)
272
+ data[data == input_nodata] = nodata_value
273
+
274
+ # Scale the data to the 0–65535 range (UInt16), avoiding NoData areas
275
+ scaled_data = np.clip(
276
+ (data - min_height) * (65535 / (max_height - min_height)), 0, 65535
277
+ ).astype(np.uint16)
278
+ scaled_data[data == nodata_value] = (
279
+ nodata_value # Preserve NoData value in the scaled array
280
+ )
281
+
282
+ # Compute the proper transform to ensure consistency
283
+ # Get the original transform, width, and height
284
+ transform = src.transform
285
+ width = src.width
286
+ height = src.height
287
+ left, bottom, right, top = src.bounds
288
+
289
+ # Adjust the transform matrix to make sure bounds and transform align correctly
290
+ transform = rasterio.transform.from_bounds(left, bottom, right, top, width, height)
291
+
292
+ # Prepare metadata for the output GeoTIFF
293
+ metadata = src.meta.copy()
294
+ metadata.update(
295
+ {
296
+ "dtype": rasterio.uint16, # Update dtype for uint16
297
+ "crs": target_crs, # Update CRS if needed
298
+ "nodata": nodata_value, # Set the new NoData value
299
+ "transform": transform, # Use the updated, consistent transform
300
+ }
301
+ )
302
+
303
+ # Write the scaled data to the output GeoTIFF
304
+ with rasterio.open(output_tiff, "w", **metadata) as dst:
305
+ dst.write(scaled_data, 1) # Write the first band
306
+
307
+ self.logger.debug(
308
+ "GeoTIFF successfully converted and saved to %s, with nodata value: %s.",
309
+ output_tiff,
310
+ nodata_value,
311
+ )
312
+
313
+ def generate_data(self) -> np.ndarray:
314
+ """Generate data from the USGS 1m provider.
315
+
316
+ Returns:
317
+ np.ndarray: Numpy array of the data.
318
+ """
319
+ download_urls = self.get_download_urls()
320
+ all_tif_files = self.download_tif_files(download_urls)
321
+ self.merge_geotiff(all_tif_files, os.path.join(self.output_path, "merged.tif"))
322
+ self.reproject_geotiff(
323
+ os.path.join(self.output_path, "merged.tif"),
324
+ os.path.join(self.output_path, "reprojected.tif"),
325
+ "EPSG:4326",
326
+ )
327
+ self.convert_geotiff_to_geotiff(
328
+ os.path.join(self.output_path, "reprojected.tif"),
329
+ os.path.join(self.output_path, "translated.tif"),
330
+ min_height=0,
331
+ max_height=self.user_settings.max_local_elevation, # type: ignore
332
+ target_crs="EPSG:4326",
333
+ )
334
+ return self.extract_roi(os.path.join(self.output_path, "translated.tif"))
335
+
336
+ def get_numpy(self) -> np.ndarray:
337
+ """Get numpy array of the tile.
338
+
339
+ Returns:
340
+ np.ndarray: Numpy array of the tile.
341
+ """
342
+ if not self.user_settings:
343
+ raise ValueError("user_settings is 'none'")
344
+ if self.user_settings.max_local_elevation <= 0: # type: ignore
345
+ raise ValueError(
346
+ "Entered 'max_local_elevation' value is unable to be used. "
347
+ "Use a value greater than 0."
348
+ )
349
+ if not self._data:
350
+ self._data = self.generate_data()
351
+ return self._data
maps4fs/generator/game.py CHANGED
@@ -40,7 +40,7 @@ class Game:
40
40
  _tree_schema: str | None = None
41
41
 
42
42
  # Order matters! Some components depend on others.
43
- components = [Texture, GRLE, Background, I3d, Config, Satellite]
43
+ components = [Texture, Background, GRLE, I3d, Config, Satellite]
44
44
 
45
45
  def __init__(self, map_template_path: str | None = None):
46
46
  if map_template_path: