maps4fs 1.5.6__py3-none-any.whl → 1.5.8__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,5 +1,7 @@
1
1
  # pylint: disable=missing-module-docstring
2
- from maps4fs.generator.dtm import DTMProvider
2
+ from maps4fs.generator.dtm.dtm import DTMProvider
3
+ from maps4fs.generator.dtm.srtm import SRTM30Provider
4
+ from maps4fs.generator.dtm.usgs import USGS1mProvider
3
5
  from maps4fs.generator.game import Game
4
6
  from maps4fs.generator.map import Map
5
7
  from maps4fs.generator.settings import (
@@ -57,36 +57,23 @@ class Background(Component):
57
57
  os.makedirs(self.background_directory, exist_ok=True)
58
58
  os.makedirs(self.water_directory, exist_ok=True)
59
59
 
60
- autoprocesses = [self.map.dem_settings.auto_process, False]
61
- self.output_paths = [
62
- os.path.join(self.background_directory, f"{name}.png") for name in ELEMENTS
63
- ]
60
+ self.output_path = os.path.join(self.background_directory, f"{FULL_NAME}.png")
64
61
  self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
65
62
  self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
66
63
 
67
- dems = []
68
-
69
- for name, autoprocess, output_path in zip(ELEMENTS, autoprocesses, self.output_paths):
70
- dem = DEM(
71
- self.game,
72
- self.map,
73
- self.coordinates,
74
- self.background_size,
75
- self.rotated_size,
76
- self.rotation,
77
- self.map_directory,
78
- self.logger,
79
- )
80
- dem.preprocess()
81
- dem.is_preview = self.is_preview(name) # type: ignore
82
- if dem.is_preview: # type: ignore
83
- dem.multiplier = 1
84
- dem.auto_process = autoprocess
85
- dem.set_output_resolution((self.rotated_size, self.rotated_size))
86
- dem.set_dem_path(output_path)
87
- dems.append(dem)
88
-
89
- self.dems = dems
64
+ self.dem = DEM(
65
+ self.game,
66
+ self.map,
67
+ self.coordinates,
68
+ self.background_size,
69
+ self.rotated_size,
70
+ self.rotation,
71
+ self.map_directory,
72
+ self.logger,
73
+ )
74
+ self.dem.preprocess()
75
+ self.dem.set_output_resolution((self.rotated_size, self.rotated_size))
76
+ self.dem.set_dem_path(self.output_path)
90
77
 
91
78
  def is_preview(self, name: str) -> bool:
92
79
  """Checks if the DEM is a preview.
@@ -104,21 +91,17 @@ class Background(Component):
104
91
  as a result the DEM files will be saved, then based on them the obj files will be
105
92
  generated."""
106
93
  self.create_background_textures()
94
+ self.dem.process()
107
95
 
108
- for dem in self.dems:
109
- dem.process()
110
- if not dem.is_preview: # type: ignore
111
- shutil.copyfile(dem.dem_path, self.not_substracted_path)
112
- self.cutout(dem.dem_path, save_path=self.not_resized_path)
96
+ shutil.copyfile(self.dem.dem_path, self.not_substracted_path)
97
+ self.cutout(self.dem.dem_path, save_path=self.not_resized_path)
113
98
 
114
99
  if self.map.dem_settings.water_depth:
115
100
  self.subtraction()
116
101
 
117
- for dem in self.dems:
118
- if not dem.is_preview: # type: ignore
119
- cutted_dem_path = self.cutout(dem.dem_path)
120
- if self.game.additional_dem_name is not None:
121
- self.make_copy(cutted_dem_path, self.game.additional_dem_name)
102
+ cutted_dem_path = self.cutout(self.dem.dem_path)
103
+ if self.game.additional_dem_name is not None:
104
+ self.make_copy(cutted_dem_path, self.game.additional_dem_name)
122
105
 
123
106
  if self.map.background_settings.generate_background:
124
107
  self.generate_obj_files()
@@ -149,19 +132,17 @@ class Background(Component):
149
132
  """
150
133
  self.qgis_sequence()
151
134
 
152
- dem = self.dems[0]
153
-
154
- north, south, east, west = dem.bbox
155
- epsg3857_string = dem.get_epsg3857_string()
156
- epsg3857_string_with_margin = dem.get_epsg3857_string(add_margin=True)
135
+ north, south, east, west = self.dem.bbox
136
+ epsg3857_string = self.dem.get_epsg3857_string()
137
+ epsg3857_string_with_margin = self.dem.get_epsg3857_string(add_margin=True)
157
138
 
158
139
  data = {
159
- "center_latitude": dem.coordinates[0],
160
- "center_longitude": dem.coordinates[1],
140
+ "center_latitude": self.dem.coordinates[0],
141
+ "center_longitude": self.dem.coordinates[1],
161
142
  "epsg3857_string": epsg3857_string,
162
143
  "epsg3857_string_with_margin": epsg3857_string_with_margin,
163
- "height": dem.map_size,
164
- "width": dem.map_size,
144
+ "height": self.dem.map_size,
145
+ "width": self.dem.map_size,
165
146
  "north": north,
166
147
  "south": south,
167
148
  "east": east,
@@ -171,10 +152,10 @@ class Background(Component):
171
152
 
172
153
  def qgis_sequence(self) -> None:
173
154
  """Generates QGIS scripts for creating bounding box layers and rasterizing them."""
174
- qgis_layer = (f"Background_{FULL_NAME}", *self.dems[0].get_espg3857_bbox())
155
+ qgis_layer = (f"Background_{FULL_NAME}", *self.dem.get_espg3857_bbox())
175
156
  qgis_layer_with_margin = (
176
157
  f"Background_{FULL_NAME}_margin",
177
- *self.dems[0].get_espg3857_bbox(add_margin=True),
158
+ *self.dem.get_espg3857_bbox(add_margin=True),
178
159
  )
179
160
  self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
180
161
 
@@ -182,21 +163,20 @@ class Background(Component):
182
163
  """Iterates over all dems and generates 3D obj files based on DEM data.
183
164
  If at least one DEM file is missing, the generation will be stopped at all.
184
165
  """
185
- for dem in self.dems:
186
- if not os.path.isfile(dem.dem_path):
187
- self.logger.warning(
188
- "DEM file not found, generation will be stopped: %s", dem.dem_path
189
- )
190
- return
166
+ if not os.path.isfile(self.dem.dem_path):
167
+ self.logger.warning(
168
+ "DEM file not found, generation will be stopped: %s", self.dem.dem_path
169
+ )
170
+ return
191
171
 
192
- self.logger.debug("DEM file for found: %s", dem.dem_path)
172
+ self.logger.debug("DEM file for found: %s", self.dem.dem_path)
193
173
 
194
- filename = os.path.splitext(os.path.basename(dem.dem_path))[0]
195
- save_path = os.path.join(self.background_directory, f"{filename}.obj")
196
- self.logger.debug("Generating obj file in path: %s", save_path)
174
+ filename = os.path.splitext(os.path.basename(self.dem.dem_path))[0]
175
+ save_path = os.path.join(self.background_directory, f"{filename}.obj")
176
+ self.logger.debug("Generating obj file in path: %s", save_path)
197
177
 
198
- dem_data = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
199
- self.plane_from_np(dem_data, save_path, is_preview=dem.is_preview) # type: ignore
178
+ dem_data = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
179
+ self.plane_from_np(dem_data, save_path) # type: ignore
200
180
 
201
181
  # pylint: disable=too-many-locals
202
182
  def cutout(self, dem_path: str, save_path: str | None = None) -> str:
@@ -248,7 +228,6 @@ class Background(Component):
248
228
  self,
249
229
  dem_data: np.ndarray,
250
230
  save_path: str,
251
- is_preview: bool = False,
252
231
  include_zeros: bool = True,
253
232
  ) -> None:
254
233
  """Generates a 3D obj file based on DEM data.
@@ -256,7 +235,6 @@ class Background(Component):
256
235
  Arguments:
257
236
  dem_data (np.ndarray) -- The DEM data as a numpy array.
258
237
  save_path (str) -- The path where the obj file will be saved.
259
- is_preview (bool, optional) -- If True, the preview mesh will be generated.
260
238
  include_zeros (bool, optional) -- If True, the mesh will include the zero height values.
261
239
  """
262
240
  resize_factor = 1 / self.map.background_settings.resize_factor
@@ -315,25 +293,21 @@ class Background(Component):
315
293
  mesh.apply_transform(rotation_matrix_y)
316
294
  mesh.apply_transform(rotation_matrix_z)
317
295
 
318
- if is_preview:
296
+ # if not include_zeros:
297
+ z_scaling_factor = 1 / self.map.dem_settings.multiplier
298
+ self.logger.debug("Z scaling factor: %s", z_scaling_factor)
299
+ mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
300
+
301
+ mesh.export(save_path)
302
+ self.logger.debug("Obj file saved: %s", save_path)
303
+
304
+ if include_zeros:
319
305
  # Simplify the preview mesh to reduce the size of the file.
320
306
  mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
321
307
 
322
308
  # Apply scale to make the preview mesh smaller in the UI.
323
309
  mesh.apply_scale([0.5, 0.5, 0.5])
324
310
  self.mesh_to_stl(mesh)
325
- else:
326
- if not include_zeros:
327
- multiplier = self.map.dem_settings.multiplier
328
- if multiplier != 1:
329
- z_scaling_factor = 1 / multiplier
330
- else:
331
- z_scaling_factor = 1 / 2**5
332
- self.logger.debug("Z scaling factor: %s", z_scaling_factor)
333
- mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
334
-
335
- mesh.export(save_path)
336
- self.logger.debug("Obj file saved: %s", save_path)
337
311
 
338
312
  def mesh_to_stl(self, mesh: trimesh.Trimesh) -> None:
339
313
  """Converts the mesh to an STL file and saves it in the previews directory.
@@ -358,25 +332,22 @@ class Background(Component):
358
332
  list[str] -- A list of paths to the previews.
359
333
  """
360
334
  preview_paths = self.dem_previews(self.game.dem_file_path(self.map_directory))
361
- for dem in self.dems:
362
- if dem.is_preview: # type: ignore
363
- background_dem_preview_path = os.path.join(
364
- self.previews_directory, "background_dem.png"
365
- )
366
- background_dem_preview_image = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED)
367
-
368
- background_dem_preview_image = cv2.resize(
369
- background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
370
- )
371
- background_dem_preview_image = cv2.normalize( # type: ignore
372
- background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
373
- )
374
- background_dem_preview_image = cv2.cvtColor(
375
- background_dem_preview_image, cv2.COLOR_GRAY2BGR
376
- )
377
-
378
- cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
379
- preview_paths.append(background_dem_preview_path)
335
+
336
+ background_dem_preview_path = os.path.join(self.previews_directory, "background_dem.png")
337
+ background_dem_preview_image = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED)
338
+
339
+ background_dem_preview_image = cv2.resize(
340
+ background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
341
+ )
342
+ background_dem_preview_image = cv2.normalize( # type: ignore
343
+ background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
344
+ )
345
+ background_dem_preview_image = cv2.cvtColor(
346
+ background_dem_preview_image, cv2.COLOR_GRAY2BGR
347
+ )
348
+
349
+ cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
350
+ preview_paths.append(background_dem_preview_path)
380
351
 
381
352
  if self.stl_preview_path:
382
353
  preview_paths.append(self.stl_preview_path)
@@ -525,18 +496,15 @@ class Background(Component):
525
496
  bool
526
497
  )
527
498
 
528
- for output_path in self.output_paths:
529
- if FULL_PREVIEW_NAME in output_path:
530
- continue
531
- dem_image = cv2.imread(output_path, cv2.IMREAD_UNCHANGED)
499
+ dem_image = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
532
500
 
533
- # Create a mask where water_resources_image is 255 (or not 0)
534
- # Subtract water_depth from dem_image where mask is True
535
- dem_image[mask] = dem_image[mask] - self.map.dem_settings.water_depth
501
+ # Create a mask where water_resources_image is 255 (or not 0)
502
+ # Subtract water_depth from dem_image where mask is True
503
+ dem_image[mask] = dem_image[mask] - self.map.dem_settings.water_depth
536
504
 
537
- # Save the modified dem_image back to the output path
538
- cv2.imwrite(output_path, dem_image)
539
- self.logger.debug("Water depth subtracted from DEM data: %s", output_path)
505
+ # Save the modified dem_image back to the output path
506
+ cv2.imwrite(self.output_path, dem_image)
507
+ self.logger.debug("Water depth subtracted from DEM data: %s", self.output_path)
540
508
 
541
509
  def generate_water_resources_obj(self) -> None:
542
510
  """Generates 3D obj files based on water resources data."""
@@ -550,9 +518,7 @@ class Background(Component):
550
518
  plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
551
519
  ).astype(np.uint8)
552
520
  plane_save_path = os.path.join(self.water_directory, "plane_water.obj")
553
- self.plane_from_np(
554
- dilated_plane_water, plane_save_path, is_preview=False, include_zeros=False
555
- )
521
+ self.plane_from_np(dilated_plane_water, plane_save_path, include_zeros=False)
556
522
 
557
523
  # Single channeled 16 bit DEM image of terrain.
558
524
  background_dem = cv2.imread(self.not_substracted_path, cv2.IMREAD_UNCHANGED)
@@ -570,6 +536,4 @@ class Background(Component):
570
536
  elevated_water = np.where(mask, background_dem, elevated_water)
571
537
  elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
572
538
 
573
- self.plane_from_np(
574
- elevated_water, elevated_save_path, is_preview=False, include_zeros=False
575
- )
539
+ self.plane_from_np(elevated_water, elevated_save_path, include_zeros=False)
maps4fs/generator/dem.py CHANGED
@@ -9,7 +9,7 @@ import numpy as np
9
9
  from pympler import asizeof # type: ignore
10
10
 
11
11
  from maps4fs.generator.component import Component
12
- from maps4fs.generator.dtm import DTMProvider
12
+ from maps4fs.generator.dtm.dtm import DTMProvider
13
13
 
14
14
 
15
15
  # pylint: disable=R0903, R0902
@@ -57,8 +57,6 @@ class DEM(Component):
57
57
  self.blur_radius,
58
58
  )
59
59
 
60
- self.auto_process = self.map.dem_settings.auto_process
61
-
62
60
  self.dtm_provider: DTMProvider = self.map.dtm_provider( # type: ignore
63
61
  coordinates=self.coordinates,
64
62
  user_settings=self.map.dtm_provider_settings,
@@ -181,11 +179,7 @@ class DEM(Component):
181
179
  resampled_data.dtype,
182
180
  )
183
181
 
184
- if self.auto_process:
185
- self.logger.debug("Auto processing is enabled, will normalize DEM data.")
186
- resampled_data = self._normalize_dem(resampled_data)
187
- else:
188
- self.logger.debug("Auto processing is disabled, DEM data will not be normalized.")
182
+ if self.multiplier != 1:
189
183
  resampled_data = resampled_data * self.multiplier
190
184
 
191
185
  self.logger.debug(
File without changes
@@ -4,10 +4,7 @@ and specific settings for downloading and processing the data."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- import gzip
8
- import math
9
7
  import os
10
- import shutil
11
8
  from typing import Type
12
9
 
13
10
  import numpy as np
@@ -263,71 +260,3 @@ class DTMProvider:
263
260
  raise ValueError("No data in the tile.")
264
261
 
265
262
  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)
@@ -0,0 +1,78 @@
1
+ """This module contains provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
2
+
3
+ import gzip
4
+ import math
5
+ import os
6
+ import shutil
7
+
8
+ import numpy as np
9
+
10
+ from maps4fs.generator.dtm.dtm import DTMProvider
11
+
12
+
13
+ class SRTM30Provider(DTMProvider):
14
+ """Provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
15
+
16
+ _code = "srtm30"
17
+ _name = "SRTM 30 m"
18
+ _region = "Global"
19
+ _icon = "🌎"
20
+ _resolution = 30.0
21
+
22
+ _url = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
23
+
24
+ _author = "[iwatkot](https://github.com/iwatkot)"
25
+
26
+ def __init__(self, *args, **kwargs):
27
+ super().__init__(*args, **kwargs)
28
+ self.hgt_directory = os.path.join(self._tile_directory, "hgt")
29
+ self.gz_directory = os.path.join(self._tile_directory, "gz")
30
+ os.makedirs(self.hgt_directory, exist_ok=True)
31
+ os.makedirs(self.gz_directory, exist_ok=True)
32
+
33
+ def get_tile_parameters(self, *args, **kwargs) -> dict[str, str]:
34
+ """Returns latitude band and tile name for SRTM tile from coordinates.
35
+
36
+ Arguments:
37
+ lat (float): Latitude.
38
+ lon (float): Longitude.
39
+
40
+ Returns:
41
+ dict: Tile parameters.
42
+ """
43
+ lat, lon = args
44
+
45
+ tile_latitude = math.floor(lat)
46
+ tile_longitude = math.floor(lon)
47
+
48
+ latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
49
+ if lon < 0:
50
+ tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
51
+ else:
52
+ tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
53
+
54
+ self.logger.debug(
55
+ "Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
56
+ )
57
+ return {"latitude_band": latitude_band, "tile_name": tile_name}
58
+
59
+ def get_numpy(self) -> np.ndarray:
60
+ """Get numpy array of the tile.
61
+
62
+ Returns:
63
+ np.ndarray: Numpy array of the tile.
64
+ """
65
+ tile_parameters = self.get_tile_parameters(*self.coordinates)
66
+ tile_name = tile_parameters["tile_name"]
67
+ decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
68
+
69
+ if not os.path.isfile(decompressed_tile_path):
70
+ compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
71
+ if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
72
+ raise FileNotFoundError(f"Tile {tile_name} not found.")
73
+
74
+ with gzip.open(compressed_tile_path, "rb") as f_in:
75
+ with open(decompressed_tile_path, "wb") as f_out:
76
+ shutil.copyfileobj(f_in, f_out)
77
+
78
+ return self.extract_roi(decompressed_tile_path)
@@ -0,0 +1,322 @@
1
+ """This module contains provider of USGS 1m data."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+
6
+ import numpy as np
7
+ import rasterio # type: ignore
8
+ import requests
9
+ from rasterio._warp import Resampling # type: ignore # pylint: disable=E0611
10
+ from rasterio.merge import merge # type: ignore
11
+ from rasterio.warp import calculate_default_transform, reproject # type: ignore
12
+ from rasterio.windows import from_bounds # type: ignore
13
+
14
+ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
15
+
16
+
17
+ class USGS1mProviderSettings(DTMProviderSettings):
18
+ """Settings for the USGS 1m provider."""
19
+
20
+ max_local_elevation: int = 255
21
+
22
+
23
+ # pylint: disable=W0223
24
+ class USGS1mProvider(DTMProvider):
25
+ """Provider of USGS."""
26
+
27
+ _code = "USGS1m"
28
+ _name = "USGS 1m"
29
+ _region = "USA"
30
+ _icon = "🇺🇸"
31
+ _resolution = 1
32
+ _data: np.ndarray | None = None
33
+ _settings = USGS1mProviderSettings
34
+ _author = "[ZenJakey](https://github.com/ZenJakey)"
35
+
36
+ _url = (
37
+ "https://tnmaccess.nationalmap.gov/api/v1/products?prodFormats=GeoTIFF,IMG&prodExtents="
38
+ "10000 x 10000 meter&datasets=Digital Elevation Model (DEM) 1 meter&polygon="
39
+ )
40
+
41
+ def __init__(self, *args, **kwargs):
42
+ super().__init__(*args, **kwargs)
43
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
44
+ self.shared_tiff_path = os.path.join(self._tile_directory, "shared")
45
+ os.makedirs(self.shared_tiff_path, exist_ok=True)
46
+ self.output_path = os.path.join(self._tile_directory, f"timestamp_{timestamp}")
47
+ os.makedirs(self.output_path, exist_ok=True)
48
+
49
+ def get_download_urls(self) -> list[str]:
50
+ """Get download URLs of the GeoTIFF files from the USGS API.
51
+
52
+ Returns:
53
+ list: List of download URLs.
54
+ """
55
+ urls = []
56
+ try:
57
+ # Make the GET request
58
+ (north, south, east, west) = self.get_bbox()
59
+ response = requests.get( # pylint: disable=W3101
60
+ self.url # type: ignore
61
+ + f"{west} {south},{east} {south},{east} {north},{west} {north},{west} {south}&="
62
+ )
63
+ self.logger.debug("Getting file locations from USGS...")
64
+
65
+ # Check if the request was successful (HTTP status code 200)
66
+ if response.status_code == 200:
67
+ # Parse the JSON response
68
+ json_data = response.json()
69
+ items = json_data["items"]
70
+ for item in items:
71
+ urls.append(item["downloadURL"])
72
+ self.download_tif_files(urls)
73
+ else:
74
+ self.logger.error("Failed to get data. HTTP Status Code: %s", response.status_code)
75
+ except requests.exceptions.RequestException as e:
76
+ self.logger.error("Failed to get data. Error: %s", e)
77
+ self.logger.debug("Received %s urls", len(urls))
78
+ return urls
79
+
80
+ def download_tif_files(self, urls: list[str]) -> list[str]:
81
+ """Download GeoTIFF files from the given URLs.
82
+
83
+ Arguments:
84
+ urls (list): List of URLs to download GeoTIFF files from.
85
+
86
+ Returns:
87
+ list: List of paths to the downloaded GeoTIFF files.
88
+ """
89
+ tif_files = []
90
+ for url in urls:
91
+ file_name = os.path.basename(url)
92
+ self.logger.debug("Retrieving TIFF: %s", file_name)
93
+ file_path = os.path.join(self.shared_tiff_path, file_name)
94
+ if not os.path.exists(file_path):
95
+ try:
96
+ # Send a GET request to the file URL
97
+ response = requests.get(url, stream=True) # pylint: disable=W3101
98
+ response.raise_for_status() # Raise an error for HTTP status codes 4xx/5xx
99
+
100
+ # Write the content of the response to the file
101
+ with open(file_path, "wb") as file:
102
+ for chunk in response.iter_content(chunk_size=8192): # Download in chunks
103
+ file.write(chunk)
104
+ self.logger.info("File downloaded successfully: %s", file_path)
105
+ except requests.exceptions.RequestException as e:
106
+ self.logger.error("Failed to download file: %s", e)
107
+ else:
108
+ self.logger.debug("File already exists: %s", file_name)
109
+
110
+ tif_files.append(file_path)
111
+ return tif_files
112
+
113
+ def merge_geotiff(self, input_files: list[str], output_file: str) -> None:
114
+ """Merge multiple GeoTIFF files into a single GeoTIFF file.
115
+
116
+ Arguments:
117
+ input_files (list): List of input GeoTIFF files to merge.
118
+ output_file (str): Path to save the merged GeoTIFF file.
119
+ """
120
+ # Open all input GeoTIFF files as datasets
121
+ self.logger.debug("Merging tiff files...")
122
+ datasets = [rasterio.open(file) for file in input_files]
123
+
124
+ # Merge datasets
125
+ mosaic, out_transform = merge(datasets)
126
+
127
+ # Get metadata from the first file and update it for the output
128
+ out_meta = datasets[0].meta.copy()
129
+ out_meta.update(
130
+ {
131
+ "driver": "GTiff",
132
+ "height": mosaic.shape[1],
133
+ "width": mosaic.shape[2],
134
+ "transform": out_transform,
135
+ "count": mosaic.shape[0], # Number of bands
136
+ }
137
+ )
138
+
139
+ # Write merged GeoTIFF to the output file
140
+ with rasterio.open(output_file, "w", **out_meta) as dest:
141
+ dest.write(mosaic)
142
+
143
+ self.logger.debug("GeoTIFF images merged successfully into %s", output_file)
144
+
145
+ def reproject_geotiff(self, input_tiff: str, output_tiff: str, target_crs: str) -> None:
146
+ """Reproject a GeoTIFF file to a new coordinate reference system (CRS).
147
+
148
+ Arguments:
149
+ input_tiff (str): Path to the input GeoTIFF file.
150
+ output_tiff (str): Path to save the reprojected GeoTIFF file.
151
+ target_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
152
+ """
153
+ # Open the source GeoTIFF
154
+ self.logger.debug("Reprojecting GeoTIFF to %s CRS...", target_crs)
155
+ with rasterio.open(input_tiff) as src:
156
+ # Get the transform, width, and height of the target CRS
157
+ transform, width, height = calculate_default_transform(
158
+ src.crs, target_crs, src.width, src.height, *src.bounds
159
+ )
160
+
161
+ # Update the metadata for the target GeoTIFF
162
+ kwargs = src.meta.copy()
163
+ kwargs.update(
164
+ {"crs": target_crs, "transform": transform, "width": width, "height": height}
165
+ )
166
+
167
+ # Open the destination GeoTIFF file and reproject
168
+ with rasterio.open(output_tiff, "w", **kwargs) as dst:
169
+ for i in range(1, src.count + 1): # Iterate over all raster bands
170
+ reproject(
171
+ source=rasterio.band(src, i),
172
+ destination=rasterio.band(dst, i),
173
+ src_transform=src.transform,
174
+ src_crs=src.crs,
175
+ dst_transform=transform,
176
+ dst_crs=target_crs,
177
+ resampling=Resampling.nearest, # Choose resampling method
178
+ )
179
+ self.logger.debug("Reprojected GeoTIFF saved to %s", output_tiff)
180
+
181
+ def extract_roi(self, input_tiff: str) -> np.ndarray: # pylint: disable=W0237
182
+ """
183
+ Crop a GeoTIFF based on given geographic bounding box and save to a new file.
184
+
185
+ Arguments:
186
+ input_tiff (str): Path to the input GeoTIFF file.
187
+
188
+ Returns:
189
+ np.ndarray: Numpy array of the cropped GeoTIFF.
190
+ """
191
+ self.logger.debug("Extracting ROI...")
192
+ # Open the input GeoTIFF
193
+ with rasterio.open(input_tiff) as src:
194
+
195
+ # Create a rasterio window from the bounding box
196
+ (north, south, east, west) = self.get_bbox()
197
+ window = from_bounds(west, south, east, north, transform=src.transform)
198
+
199
+ data = src.read(1, window=window)
200
+ self.logger.debug("Extracted ROI")
201
+ return data
202
+
203
+ # pylint: disable=R0914, R0917, R0913
204
+ def convert_geotiff_to_geotiff(
205
+ self,
206
+ input_tiff: str,
207
+ output_tiff: str,
208
+ min_height: float,
209
+ max_height: float,
210
+ target_crs: str,
211
+ ) -> None:
212
+ """
213
+ Convert a GeoTIFF to a scaled GeoTIFF with UInt16 values using a specific coordinate
214
+ system and output size.
215
+
216
+ Arguments:
217
+ input_tiff (str): Path to the input GeoTIFF file.
218
+ output_tiff (str): Path to save the output GeoTIFF file.
219
+ min_height (float): Minimum terrain height (input range).
220
+ max_height (float): Maximum terrain height (input range).
221
+ target_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
222
+ """
223
+ # Open the input GeoTIFF file
224
+ self.logger.debug("Converting to uint16")
225
+ with rasterio.open(input_tiff) as src:
226
+ # Ensure the input CRS matches the target CRS (reprojection may be required)
227
+ if str(src.crs) != str(target_crs):
228
+ raise ValueError(
229
+ f"The GeoTIFF CRS is {src.crs}, but the target CRS is {target_crs}. "
230
+ "Reprojection may be required."
231
+ )
232
+
233
+ # Read the data from the first band
234
+ data = src.read(1) # Assuming the input GeoTIFF has only a single band
235
+
236
+ # Identify the input file's NoData value
237
+ input_nodata = src.nodata
238
+ if input_nodata is None:
239
+ input_nodata = -999999.0 # Default fallback if no NoData value is defined
240
+ nodata_value = 0
241
+ # Replace NoData values (e.g., -999999.0) with the new NoData value
242
+ # (e.g., 65535 for UInt16)
243
+ data[data == input_nodata] = nodata_value
244
+
245
+ # Scale the data to the 0–65535 range (UInt16), avoiding NoData areas
246
+ scaled_data = np.clip(
247
+ (data - min_height) * (65535 / (max_height - min_height)), 0, 65535
248
+ ).astype(np.uint16)
249
+ scaled_data[data == nodata_value] = (
250
+ nodata_value # Preserve NoData value in the scaled array
251
+ )
252
+
253
+ # Compute the proper transform to ensure consistency
254
+ # Get the original transform, width, and height
255
+ transform = src.transform
256
+ width = src.width
257
+ height = src.height
258
+ left, bottom, right, top = src.bounds
259
+
260
+ # Adjust the transform matrix to make sure bounds and transform align correctly
261
+ transform = rasterio.transform.from_bounds(left, bottom, right, top, width, height)
262
+
263
+ # Prepare metadata for the output GeoTIFF
264
+ metadata = src.meta.copy()
265
+ metadata.update(
266
+ {
267
+ "dtype": rasterio.uint16, # Update dtype for uint16
268
+ "crs": target_crs, # Update CRS if needed
269
+ "nodata": nodata_value, # Set the new NoData value
270
+ "transform": transform, # Use the updated, consistent transform
271
+ }
272
+ )
273
+
274
+ # Write the scaled data to the output GeoTIFF
275
+ with rasterio.open(output_tiff, "w", **metadata) as dst:
276
+ dst.write(scaled_data, 1) # Write the first band
277
+
278
+ self.logger.debug(
279
+ "GeoTIFF successfully converted and saved to %s, with nodata value: %s.",
280
+ output_tiff,
281
+ nodata_value,
282
+ )
283
+
284
+ def generate_data(self) -> np.ndarray:
285
+ """Generate data from the USGS 1m provider.
286
+
287
+ Returns:
288
+ np.ndarray: Numpy array of the data.
289
+ """
290
+ download_urls = self.get_download_urls()
291
+ all_tif_files = self.download_tif_files(download_urls)
292
+ self.merge_geotiff(all_tif_files, os.path.join(self.output_path, "merged.tif"))
293
+ self.reproject_geotiff(
294
+ os.path.join(self.output_path, "merged.tif"),
295
+ os.path.join(self.output_path, "reprojected.tif"),
296
+ "EPSG:4326",
297
+ )
298
+ self.convert_geotiff_to_geotiff(
299
+ os.path.join(self.output_path, "reprojected.tif"),
300
+ os.path.join(self.output_path, "translated.tif"),
301
+ min_height=0,
302
+ max_height=self.user_settings.max_local_elevation, # type: ignore
303
+ target_crs="EPSG:4326",
304
+ )
305
+ return self.extract_roi(os.path.join(self.output_path, "translated.tif"))
306
+
307
+ def get_numpy(self) -> np.ndarray:
308
+ """Get numpy array of the tile.
309
+
310
+ Returns:
311
+ np.ndarray: Numpy array of the tile.
312
+ """
313
+ if not self.user_settings:
314
+ raise ValueError("user_settings is 'none'")
315
+ if self.user_settings.max_local_elevation <= 0: # type: ignore
316
+ raise ValueError(
317
+ "Entered 'max_local_elevation' value is unable to be used. "
318
+ "Use a value greater than 0."
319
+ )
320
+ if not self._data:
321
+ self._data = self.generate_data()
322
+ return self._data
maps4fs/generator/i3d.py CHANGED
@@ -81,20 +81,6 @@ class I3d(Component):
81
81
 
82
82
  root = tree.getroot()
83
83
  for map_elem in root.iter("Scene"):
84
- for terrain_elem in map_elem.iter("TerrainTransformGroup"):
85
- if self.map.dem_settings.auto_process:
86
- terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
87
- self.logger.debug(
88
- "heightScale attribute set to %s in TerrainTransformGroup element.",
89
- DEFAULT_HEIGHT_SCALE,
90
- )
91
- else:
92
- self.logger.debug(
93
- "Auto process is disabled, skipping the heightScale attribute update."
94
- )
95
-
96
- self.logger.debug("TerrainTransformGroup element updated in I3D file.")
97
-
98
84
  sun_elem = map_elem.find(".//Light[@name='sun']")
99
85
 
100
86
  if sun_elem is not None:
@@ -111,6 +97,10 @@ class I3d(Component):
111
97
  )
112
98
 
113
99
  if self.map_size > 4096:
100
+ terrain_elem = root.find(".//TerrainTransformGroup")
101
+ if terrain_elem is None:
102
+ self.logger.warning("TerrainTransformGroup element not found in I3D file.")
103
+ return
114
104
  displacement_layer = terrain_elem.find(".//DisplacementLayer") # pylint: disable=W0631
115
105
 
116
106
  if displacement_layer is not None:
maps4fs/generator/map.py CHANGED
@@ -8,7 +8,7 @@ import shutil
8
8
  from typing import Any, Generator
9
9
 
10
10
  from maps4fs.generator.component import Component
11
- from maps4fs.generator.dtm import DTMProvider, DTMProviderSettings
11
+ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
12
12
  from maps4fs.generator.game import Game
13
13
  from maps4fs.generator.settings import (
14
14
  BackgroundSettings,
@@ -58,7 +58,6 @@ class DEMSettings(SettingsModel):
58
58
  """Represents the advanced settings for DEM component.
59
59
 
60
60
  Attributes:
61
- auto_process (bool): use the auto preset to change the multiplier.
62
61
  multiplier (int): multiplier for the heightmap, every pixel will be multiplied by this
63
62
  value.
64
63
  blur_radius (int): radius of the blur filter.
@@ -67,7 +66,6 @@ class DEMSettings(SettingsModel):
67
66
  is present.
68
67
  """
69
68
 
70
- auto_process: bool = True
71
69
  multiplier: int = 1
72
70
  blur_radius: int = 35
73
71
  plateau: int = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maps4fs
3
- Version: 1.5.6
3
+ Version: 1.5.8
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
@@ -48,6 +48,7 @@ Requires-Dist: pydantic
48
48
  <a href="#Expert-settings">Expert settings</a> •
49
49
  <a href="#Resources">Resources</a> •
50
50
  <a href="#Bugs-and-feature-requests">Bugs and feature requests</a><br>
51
+ <a href="#DTM-Providers">DTM Providers</a> •
51
52
  <a href="#Special-thanks">Special thanks</a>
52
53
  </p>
53
54
 
@@ -69,6 +70,7 @@ Requires-Dist: pydantic
69
70
 
70
71
  🗺️ Supports 2x2, 4x4, 8x8, 16x16 and any custom size maps<br>
71
72
  🔄 Support map rotation 🆕<br>
73
+ 🌐 Supports custom [DTM Providers](#DTM-Providers) 🆕<br>
72
74
  🌾 Automatically generates fields 🆕<br>
73
75
  🌽 Automatically generates farmlands 🆕<br>
74
76
  🌿 Automatically generates decorative foliage 🆕<br>
@@ -475,8 +477,6 @@ You can also apply some advanced settings to the map generation process. Note th
475
477
 
476
478
  ### DEM Advanced settings
477
479
 
478
- - Auto process: the tool will automatically try to find suitable multiplier. As a result, the DEM image WILL not match real world values. If this option is disabled, you'll probably see completely black DEM image, but it's not empty. It's just you can't see the values of 16-bit image by eye, because they're too small. Learn more what's DEM image and how to work with it in [docs](docs/dem.md). By default, it's set to True.
479
-
480
480
  - Multiplier: the height of the map is multiplied by this value. So the DEM map is just a 16-bit grayscale image, which means that the maximum available value there is 65535, while the actual difference between the deepest and the highest point on Earth is about 20 km. Just note that this setting mostly does not matter, because you can always adjust it in the Giants Editor, learn more about the DEM file and the heightScale parameter in [docs](docs/dem.md). By default, it's set to 1.
481
481
 
482
482
  - Blur radius: the radius of the Gaussian blur filter applied to the DEM map. By default, it's set to 21. This filter just makes the DEM map smoother, so the height transitions will be more natural. You can set it to 1 to disable the filter, but it will result in a Minecraft-like map.
@@ -549,6 +549,15 @@ To create a basic map, you only need the Giants Editor. But if you want to creat
549
549
  ➡️ Please, before creating an issue or asking some questions, check the [FAQ](docs/FAQ.md) section.<br>
550
550
  If you find a bug or have an idea for a new feature, please create an issue [here](https://github.com/iwatkot/maps4fs/issues) or contact me directly on [Telegram](https://t.me/iwatkot) or on Discord: `iwatkot`.
551
551
 
552
+ ## DTM Providers
553
+
554
+ The generator supports adding the own DTM providers, please refer to the [DTM Providers](docs/dtm_providers.md) section to learn how to add the custom DTM provider.
555
+
556
+ ### Supported DTM providers
557
+
558
+ - [SRTM 30m](https://dwtkns.com/srtm30m/) - the 30 meters resolution DEM data from the SRTM mission for the whole world.
559
+ - [USGS 1m](https://portal.opentopography.org/raster?opentopoID=OTNED.012021.4269.3) - the 1-meter resolution DEM data from the USGS for the USA. Developed by [ZenJakey](https://github.com/ZenJakey).
560
+
552
561
  ## Special thanks
553
562
 
554
563
  Of course, first of all, thanks to the direct [contributors](https://github.com/iwatkot/maps4fs/graphs/contributors) of the project.
@@ -0,0 +1,27 @@
1
+ maps4fs/__init__.py,sha256=WbT36EzJ_74GN0RUUrLIYECdSdtRiZaxKl17KUt7pjA,492
2
+ maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
3
+ maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
+ maps4fs/generator/background.py,sha256=moTsEJM-hZgHQQiBjFVTWBKgPMqxup-58EErh4bq_dE,21342
5
+ maps4fs/generator/component.py,sha256=RtXruvT4Fxfr7_xo9Bi-i3IIWcPd5QQOSpYJ_cNC49o,20408
6
+ maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
7
+ maps4fs/generator/dem.py,sha256=vGz-gUg_JArqHO7qewdnSR7WiF7ciUzY-OSqOluUDWw,12304
8
+ maps4fs/generator/game.py,sha256=QHgVnyGYvEnfwGZ84-u-dpbCRr3UeVVqBbrwr5WG8dE,7992
9
+ maps4fs/generator/grle.py,sha256=u8ZwSs313PIOkH_0B_O2tVTaZ-eYNkc30eKGtBxWzTM,17846
10
+ maps4fs/generator/i3d.py,sha256=FLVlj0g90IXRuaRARD1HTnufsLpuaa5kHKdiME-LUZY,24329
11
+ maps4fs/generator/map.py,sha256=-iUFGqe11Df-oUrxhcnJzRZ0o6NZKRQ_blTq1h1Wezg,9287
12
+ maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
13
+ maps4fs/generator/satellite.py,sha256=Qnb6XxmXKnHdHKVMb9mJ3vDGtGkDHCOv_81hrrXdx3k,3660
14
+ maps4fs/generator/settings.py,sha256=NWuK76ICr8gURQnzePat4JH9w-iACbQEKQebqu51gBE,4470
15
+ maps4fs/generator/texture.py,sha256=sErusfv1AqQfP-veMrZ921Tz8DnGEhfB4ucggMmKrD4,31231
16
+ maps4fs/generator/dtm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ maps4fs/generator/dtm/dtm.py,sha256=THQ3RWVL9ut7A4omS8hEog-oQSSwYV0JcDMe0Iiw4fY,8009
18
+ maps4fs/generator/dtm/srtm.py,sha256=_A2Zi12-uTl5fZY0lBsCF6NnxQqfqR_TZgsKp96uvns,2712
19
+ maps4fs/generator/dtm/usgs.py,sha256=c0e3xVc0XJqTDgxJHWVKDeKbwAY_qurSYf1LIUe4inQ,13098
20
+ maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
21
+ maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
22
+ maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
23
+ maps4fs-1.5.8.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
24
+ maps4fs-1.5.8.dist-info/METADATA,sha256=Q64Jh-pvQhQSgZZNnWOJHOs4sSe12iTqzEiyfRx4P5A,36231
25
+ maps4fs-1.5.8.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
26
+ maps4fs-1.5.8.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
27
+ maps4fs-1.5.8.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- maps4fs/__init__.py,sha256=EJzbqRrSGltSMUI-dHgONODxKt9YvP_ElwFmXV8M_MA,380
2
- maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
3
- maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
- maps4fs/generator/background.py,sha256=ySABP9HLji8R0aXi1BwjUQtP2uDqZPkrlmugowa9Gkk,22836
5
- maps4fs/generator/component.py,sha256=RtXruvT4Fxfr7_xo9Bi-i3IIWcPd5QQOSpYJ_cNC49o,20408
6
- maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
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
10
- maps4fs/generator/grle.py,sha256=u8ZwSs313PIOkH_0B_O2tVTaZ-eYNkc30eKGtBxWzTM,17846
11
- maps4fs/generator/i3d.py,sha256=qeZYqfuhbhRPlSAuQHXaq6RmIO7314oMN68Ivebp1YQ,24786
12
- maps4fs/generator/map.py,sha256=flU0b2TrVYLxj9o3v_YRvNz9YB3s4w6YFSv4Jka5ojM,9283
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
16
- maps4fs/generator/texture.py,sha256=sErusfv1AqQfP-veMrZ921Tz8DnGEhfB4ucggMmKrD4,31231
17
- maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
18
- maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
19
- maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
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,,