maps4fs 1.8.171__py3-none-any.whl → 1.8.173__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.
@@ -235,24 +235,16 @@ class Background(MeshComponent, ImageComponent):
235
235
  This setting is used for a Background Terrain, where the center part where the
236
236
  playable area is will be cut out.
237
237
  """
238
- resize_factor = 1 / self.map.background_settings.resize_factor
239
- dem_data = cv2.resize(dem_data, (0, 0), fx=resize_factor, fy=resize_factor)
240
- if remove_center:
241
- half_size = int(self.map_size // 2 * resize_factor)
242
- dem_data = self.cut_out_np(dem_data, half_size, set_zeros=True)
243
- self.logger.debug("Center removed from DEM data.")
244
- self.logger.debug(
245
- "DEM data resized to shape: %s with factor: %s", dem_data.shape, resize_factor
246
- )
247
-
248
238
  mesh = self.mesh_from_np(
249
239
  dem_data,
250
240
  include_zeros=include_zeros,
251
241
  z_scaling_factor=self.get_z_scaling_factor(),
252
- resize_factor=resize_factor,
242
+ resize_factor=self.map.background_settings.resize_factor,
253
243
  apply_decimation=self.map.background_settings.apply_decimation,
254
244
  decimation_percent=self.map.background_settings.decimation_percent,
255
245
  decimation_agression=self.map.background_settings.decimation_agression,
246
+ remove_center=remove_center,
247
+ remove_size=self.map_size,
256
248
  )
257
249
 
258
250
  mesh.export(save_path)
@@ -414,10 +406,18 @@ class Background(MeshComponent, ImageComponent):
414
406
  water_resources_image = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
415
407
  dem_image = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
416
408
 
409
+ # fall back to default value for height_scale 255, it is defined as float | None
410
+ # but it is always set at this point
411
+ z_scaling_factor: float = (
412
+ self.map.shared_settings.mesh_z_scaling_factor
413
+ if self.map.shared_settings.mesh_z_scaling_factor is not None
414
+ else 257
415
+ )
416
+
417
417
  dem_image = self.subtract_by_mask(
418
418
  dem_image,
419
419
  water_resources_image,
420
- self.map.dem_settings.water_depth,
420
+ int(self.map.dem_settings.water_depth * z_scaling_factor),
421
421
  )
422
422
 
423
423
  # Save the modified dem_image back to the output path
@@ -432,6 +432,12 @@ class Background(MeshComponent, ImageComponent):
432
432
 
433
433
  # Single channeled 8 bit image, where the water have values of 255, and the rest 0.
434
434
  plane_water = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
435
+
436
+ # Check if the image contains non-zero values.
437
+ if not np.any(plane_water):
438
+ self.logger.warning("Water resources image is empty, skipping water generation.")
439
+ return
440
+
435
441
  dilated_plane_water = cv2.dilate(
436
442
  plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
437
443
  ).astype(np.uint8)
@@ -53,10 +53,12 @@ class MeshComponent(Component):
53
53
  image: np.ndarray,
54
54
  include_zeros: bool,
55
55
  z_scaling_factor: float,
56
- resize_factor: float,
56
+ resize_factor: int,
57
57
  apply_decimation: bool,
58
58
  decimation_percent: int,
59
59
  decimation_agression: int,
60
+ remove_center: bool,
61
+ remove_size: int,
60
62
  ) -> trimesh.Trimesh:
61
63
  """Generates a mesh from the given numpy array.
62
64
 
@@ -64,16 +66,21 @@ class MeshComponent(Component):
64
66
  image (np.ndarray): The numpy array to generate the mesh from.
65
67
  include_zeros (bool): Whether to include zero values in the mesh.
66
68
  z_scaling_factor (float): The scaling factor for the Z-axis.
67
- resize_factor (float): The resizing factor.
69
+ resize_factor (int): The resizing factor.
68
70
  apply_decimation (bool): Whether to apply decimation to the mesh.
69
71
  decimation_percent (int): The percent of the decimation.
70
72
  decimation_agression (int): The agression of the decimation.
73
+ remove_center (bool): Whether to remove the center from the mesh.
74
+ remove_size (int): The size of the center to remove.
71
75
 
72
76
  Returns:
73
77
  trimesh.Trimesh: The generated mesh.
74
78
  """
79
+ output_x_size, _ = image.shape
75
80
  image = image.max() - image
76
81
 
82
+ image = image[::resize_factor, ::resize_factor]
83
+
77
84
  rows, cols = image.shape
78
85
  x = np.linspace(0, cols - 1, cols)
79
86
  y = np.linspace(0, rows - 1, rows)
@@ -106,15 +113,7 @@ class MeshComponent(Component):
106
113
 
107
114
  faces_np = np.array(faces)
108
115
  mesh = trimesh.Trimesh(vertices=vertices, faces=faces_np)
109
-
110
- # Apply rotation: 180 degrees around Y-axis and Z-axis
111
- rotation_matrix_y = trimesh.transformations.rotation_matrix(np.pi, [0, 1, 0])
112
- rotation_matrix_z = trimesh.transformations.rotation_matrix(np.pi, [0, 0, 1])
113
- mesh.apply_transform(rotation_matrix_y)
114
- mesh.apply_transform(rotation_matrix_z)
115
-
116
- # if not include_zeros:
117
- mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
116
+ mesh = MeshComponent.rotate_mesh(mesh)
118
117
 
119
118
  if apply_decimation:
120
119
  percent = decimation_percent / 100
@@ -122,4 +121,122 @@ class MeshComponent(Component):
122
121
  percent=percent, aggression=decimation_agression
123
122
  )
124
123
 
124
+ try:
125
+ if not mesh.is_watertight:
126
+ mesh = MeshComponent.fix_mesh(mesh)
127
+ except Exception:
128
+ pass
129
+
130
+ mesh = MeshComponent.mesh_to_output_size(
131
+ mesh,
132
+ resize_factor,
133
+ z_scaling_factor,
134
+ output_x_size,
135
+ skip_resize_to_expected_size=not include_zeros,
136
+ )
137
+
138
+ if remove_center:
139
+ mesh = MeshComponent.remove_center_from_mesh(mesh, remove_size)
140
+
125
141
  return mesh
142
+
143
+ @staticmethod
144
+ def rotate_mesh(mesh: trimesh.Trimesh) -> trimesh.Trimesh:
145
+ """Rotates the given mesh by 180 degrees around the Y-axis and Z-axis.
146
+
147
+ Arguments:
148
+ mesh (trimesh.Trimesh): The mesh to rotate.
149
+
150
+ Returns:
151
+ trimesh.Trimesh: The rotated mesh.
152
+ """
153
+ mesh_copy = mesh.copy()
154
+
155
+ rotation_matrices = [
156
+ trimesh.transformations.rotation_matrix(np.pi, [0, 1, 0]),
157
+ trimesh.transformations.rotation_matrix(np.pi, [0, 0, 1]),
158
+ ]
159
+
160
+ for rotation_matrix in tqdm(rotation_matrices, desc="Rotating mesh", unit="rotation"):
161
+ mesh_copy.apply_transform(rotation_matrix)
162
+
163
+ return mesh_copy
164
+
165
+ @staticmethod
166
+ def fix_mesh(mesh: trimesh.Trimesh) -> trimesh.Trimesh:
167
+ """Fixes the given mesh by filling holes, fixing normals, fixing winding, fixing inversion,
168
+ fixing broken faces, and stitching.
169
+
170
+ Arguments:
171
+ mesh (trimesh.Trimesh): The mesh to fix.
172
+
173
+ Returns:
174
+ trimesh.Trimesh: The fixed mesh.
175
+ """
176
+ mesh_copy = mesh.copy()
177
+
178
+ fix_methods = [
179
+ trimesh.repair.fill_holes,
180
+ trimesh.repair.fix_normals,
181
+ trimesh.repair.fix_winding,
182
+ trimesh.repair.fix_inversion,
183
+ trimesh.repair.broken_faces,
184
+ trimesh.repair.stitch,
185
+ ]
186
+
187
+ for method in tqdm(fix_methods, desc="Fixing mesh", unit="method"):
188
+ method(mesh_copy) # type: ignore
189
+
190
+ return mesh_copy
191
+
192
+ @staticmethod
193
+ def mesh_to_output_size(
194
+ mesh: trimesh.Trimesh,
195
+ resize_factor: int,
196
+ z_scaling_factor: float,
197
+ expected_size: int,
198
+ skip_resize_to_expected_size: bool = False,
199
+ ) -> trimesh.Trimesh:
200
+ """Resizes the given mesh to the expected size.
201
+
202
+ Arguments:
203
+ mesh (trimesh.Trimesh): The mesh to resize.
204
+ resize_factor (int): The resizing factor.
205
+ z_scaling_factor (float): The scaling factor for the Z-axis.
206
+ expected_size (int): The expected size.
207
+ skip_resize_to_expected_size (bool): Whether to skip resizing to the expected size.
208
+
209
+ Returns:
210
+ trimesh.Trimesh: The resized mesh.
211
+ """
212
+ mesh_copy = mesh.copy()
213
+
214
+ mesh_copy.apply_scale([resize_factor / 1, resize_factor / 1, z_scaling_factor])
215
+
216
+ if not skip_resize_to_expected_size:
217
+ x_size, y_size, _ = mesh_copy.extents
218
+ x_resize_factor = expected_size / x_size
219
+ y_resize_factor = expected_size / y_size
220
+
221
+ mesh_copy.apply_scale([x_resize_factor, y_resize_factor, 1])
222
+ return mesh_copy
223
+
224
+ @staticmethod
225
+ def remove_center_from_mesh(mesh: trimesh.Trimesh, remove_size: int) -> trimesh.Trimesh:
226
+ """Removes the center from the given mesh.
227
+
228
+ Arguments:
229
+ mesh (trimesh.Trimesh): The mesh to remove the center from.
230
+ remove_size (int): The size of the center to remove.
231
+
232
+ Returns:
233
+ trimesh.Trimesh: The mesh with the center removed.
234
+ """
235
+ mesh_copy = mesh.copy()
236
+
237
+ _, _, z_size = mesh_copy.extents
238
+
239
+ cube_mesh = trimesh.creation.box([remove_size, remove_size, z_size * 4])
240
+ cube_mesh.apply_translation(mesh_copy.centroid - cube_mesh.centroid)
241
+
242
+ return trimesh.boolean.difference([mesh_copy, cube_mesh], check_volume=False)
maps4fs/generator/dem.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """This module contains DEM class for processing Digital Elevation Model data."""
2
2
 
3
- import os
3
+ import math
4
4
  from typing import Any
5
5
 
6
6
  import cv2
@@ -31,10 +31,6 @@ class DEM(Component):
31
31
  def preprocess(self) -> None:
32
32
  self._dem_path = self.game.dem_file_path(self.map_directory)
33
33
  self.temp_dir = "temp"
34
- self.hgt_dir = os.path.join(self.temp_dir, "hgt")
35
- self.gz_dir = os.path.join(self.temp_dir, "gz")
36
- os.makedirs(self.hgt_dir, exist_ok=True)
37
- os.makedirs(self.gz_dir, exist_ok=True)
38
34
 
39
35
  self.logger.debug("Map size: %s x %s.", self.map_size, self.map_size)
40
36
  self.logger.debug(
@@ -116,24 +112,8 @@ class DEM(Component):
116
112
  )
117
113
  return dem_size, dem_size
118
114
 
119
- def to_ground(self, data: np.ndarray) -> np.ndarray:
120
- """Receives the signed 16-bit integer array and converts it to the ground level.
121
- If the min value is negative, it will become zero value and the rest of the values
122
- will be shifted accordingly.
123
- """
124
- # For examlem, min value was -50, it will become 0 and for all values we'll +50.
125
-
126
- if data.min() < 0:
127
- self.logger.debug("Array contains negative values, will be shifted to the ground.")
128
- data = data + abs(data.min())
129
-
130
- self.logger.debug(
131
- "Array was shifted to the ground. Min: %s, max: %s.", data.min(), data.max()
132
- )
133
- return data
134
-
135
115
  def process(self) -> None:
136
- """Reads SRTM file, crops it to map size, normalizes and blurs it,
116
+ """Reads DTM file, crops it to map size, normalizes and blurs it,
137
117
  saves to map directory."""
138
118
 
139
119
  dem_output_resolution = self.output_resolution
@@ -142,7 +122,7 @@ class DEM(Component):
142
122
  try:
143
123
  data = self.dtm_provider.get_numpy()
144
124
  except Exception as e: # pylint: disable=W0718
145
- self.logger.error("Failed to get DEM data from SRTM: %s.", e)
125
+ self.logger.error("Failed to get DEM data from DTM provider: %s.", e)
146
126
  self._save_empty_dem(dem_output_resolution)
147
127
  return
148
128
 
@@ -151,7 +131,7 @@ class DEM(Component):
151
131
  self._save_empty_dem(dem_output_resolution)
152
132
  return
153
133
 
154
- if data.dtype not in ["int16", "uint16"]:
134
+ if data.dtype not in ["int16", "uint16", "float", "float32"]:
155
135
  self.logger.error("DTM provider returned incorrect data type: %s.", data.dtype)
156
136
  self._save_empty_dem(dem_output_resolution)
157
137
  return
@@ -164,93 +144,158 @@ class DEM(Component):
164
144
  data.max(),
165
145
  )
166
146
 
167
- data = self.to_ground(data)
147
+ # 1. Resize DEM data to the output resolution.
148
+ resampled_data = self.resize_to_output(data)
168
149
 
169
- resampled_data = cv2.resize(
170
- data, dem_output_resolution, interpolation=cv2.INTER_LINEAR
171
- ).astype("uint16")
150
+ # 2. Apply multiplier (-10 to 120.4 becomes -20 to 240.8)
151
+ resampled_data = self.apply_multiplier(resampled_data)
172
152
 
173
- size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
174
- self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
153
+ # 3. Raise terrain, and optionally lower to plateau level+water depth
154
+ # e.g. -20 to 240.8m becomes 20 to 280.8m
155
+ resampled_data = self.raise_or_lower(resampled_data)
156
+
157
+ # 4. Determine actual height scale value using ceiling
158
+ # e.g. 255 becomes 280.8+10 = 291
159
+ height_scale_value = self.determine_height_scale(resampled_data)
160
+
161
+ # 5. Normalize DEM data to 16-bit unsigned integer range (0 to 65535)
162
+ # e.g. multiply by 65535/291, clip and return as uint16
163
+ resampled_data = self.normalize_data(resampled_data, height_scale_value)
164
+
165
+ # 6. Blur DEM data.
166
+ resampled_data = self.apply_blur(resampled_data)
167
+
168
+ cv2.imwrite(self._dem_path, resampled_data)
169
+ self.logger.debug("DEM data was saved to %s.", self._dem_path)
170
+
171
+ if self.rotation:
172
+ self.rotate_dem()
175
173
 
174
+ def normalize_data(self, data: np.ndarray, height_scale_value: int) -> np.ndarray:
175
+ """Normalize DEM data to 16-bit unsigned integer range (0 to 65535).
176
+
177
+ Arguments:
178
+ data (np.ndarray): DEM data.
179
+ height_scale_value (int): Height scale value.
180
+
181
+ Returns:
182
+ np.ndarray: Normalized DEM data.
183
+ """
184
+ normalized_data = np.clip((data / height_scale_value) * 65535, 0, 65535).astype(np.uint16)
176
185
  self.logger.debug(
177
- "Maximum value in resampled data: %s, minimum value: %s. Data type: %s.",
178
- resampled_data.max(),
179
- resampled_data.min(),
180
- resampled_data.dtype,
186
+ "DEM data was normalized and clipped to 16-bit unsigned integer range. "
187
+ "Min: %s, max: %s.",
188
+ normalized_data.min(),
189
+ normalized_data.max(),
181
190
  )
191
+ return normalized_data
192
+
193
+ def determine_height_scale(self, data: np.ndarray) -> int:
194
+ """Determine height scale value using ceiling.
195
+
196
+ Arguments:
197
+ data (np.ndarray): DEM data.
198
+
199
+ Returns:
200
+ int: Height scale value.
201
+ """
202
+ height_scale = self.map.dem_settings.minimum_height_scale
203
+ adjusted_height_scale = math.ceil(
204
+ max(height_scale, data.max() + self.map.dem_settings.ceiling)
205
+ )
206
+
207
+ self.map.shared_settings.height_scale_value = adjusted_height_scale # type: ignore
208
+ self.map.shared_settings.mesh_z_scaling_factor = 65535 / adjusted_height_scale
209
+ self.map.shared_settings.height_scale_multiplier = adjusted_height_scale / 255
210
+ self.map.shared_settings.change_height_scale = True # type: ignore
211
+
212
+ self.logger.debug("Height scale value is %s.", adjusted_height_scale)
213
+ return adjusted_height_scale
214
+
215
+ def raise_or_lower(self, data: np.ndarray) -> np.ndarray:
216
+ """Raise or lower terrain to the level of plateau+water depth."""
217
+
218
+ if not self.map.dem_settings.adjust_terrain_to_ground_level:
219
+ return data
220
+
221
+ desired_ground_level = self.map.dem_settings.plateau + self.map.dem_settings.water_depth
222
+ current_ground_level = data.min()
182
223
 
183
- if self.multiplier != 1:
184
- resampled_data = resampled_data * self.multiplier
185
-
186
- self.logger.debug(
187
- "DEM data was multiplied by %s. Min: %s, max: %s. Data type: %s.",
188
- self.multiplier,
189
- resampled_data.min(),
190
- resampled_data.max(),
191
- resampled_data.dtype,
192
- )
193
-
194
- size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
195
- self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
196
-
197
- # Clip values to 16-bit unsigned integer range.
198
- resampled_data = np.clip(resampled_data, 0, 65535)
199
- resampled_data = resampled_data.astype("uint16")
200
- self.logger.debug(
201
- "DEM data was multiplied by %s and clipped to 16-bit unsigned integer range. "
202
- "Min: %s, max: %s.",
203
- self.multiplier,
204
- resampled_data.min(),
205
- resampled_data.max(),
206
- )
224
+ data = data + (desired_ground_level - current_ground_level)
207
225
 
208
226
  self.logger.debug(
209
- "DEM data was resampled. Shape: %s, dtype: %s. Min: %s, max: %s.",
210
- resampled_data.shape,
211
- resampled_data.dtype,
212
- resampled_data.min(),
213
- resampled_data.max(),
227
+ "Array was shifted to the ground level %s. Min: %s, max: %s.",
228
+ desired_ground_level,
229
+ data.min(),
230
+ data.max(),
214
231
  )
232
+ return data
215
233
 
216
- if self.blur_radius > 0:
217
- resampled_data = cv2.GaussianBlur(
218
- resampled_data, (self.blur_radius, self.blur_radius), sigmaX=40, sigmaY=40
219
- )
220
- self.logger.debug(
221
- "Gaussion blur applied to DEM data with kernel size %s.",
222
- self.blur_radius,
223
- )
234
+ def apply_multiplier(self, data: np.ndarray) -> np.ndarray:
235
+ """Apply multiplier to DEM data.
236
+
237
+ Arguments:
238
+ data (np.ndarray): DEM data.
224
239
 
240
+ Returns:
241
+ np.ndarray: Multiplied DEM data.
242
+ """
243
+ if not self.multiplier != 1:
244
+ return data
245
+
246
+ multiplied_data = data * self.multiplier
225
247
  self.logger.debug(
226
- "DEM data was blurred. Shape: %s, dtype: %s. Min: %s, max: %s.",
227
- resampled_data.shape,
228
- resampled_data.dtype,
229
- resampled_data.min(),
230
- resampled_data.max(),
248
+ "DEM data was multiplied by %s. Min: %s, max: %s.",
249
+ self.multiplier,
250
+ multiplied_data.min(),
251
+ multiplied_data.max(),
231
252
  )
253
+ return multiplied_data
232
254
 
233
- if self.map.dem_settings.plateau:
234
- # Plateau is a flat area with a constant height.
235
- # So we just add this value to each pixel of the DEM.
236
- # And also need to ensure that there will be no values with height greater than
237
- # it's allowed in 16-bit unsigned integer.
255
+ def resize_to_output(self, data: np.ndarray) -> np.ndarray:
256
+ """Resize DEM data to the output resolution.
238
257
 
239
- resampled_data += self.map.dem_settings.plateau
240
- resampled_data = np.clip(resampled_data, 0, 65535)
258
+ Arguments:
259
+ data (np.ndarray): DEM data.
241
260
 
242
- self.logger.debug(
243
- "Plateau with height %s was added to DEM data. Min: %s, max: %s.",
244
- self.map.dem_settings.plateau,
245
- resampled_data.min(),
246
- resampled_data.max(),
247
- )
261
+ Returns:
262
+ np.ndarray: Resized DEM data.
263
+ """
264
+ resampled_data = cv2.resize(data, self.output_resolution, interpolation=cv2.INTER_LINEAR)
248
265
 
249
- cv2.imwrite(self._dem_path, resampled_data)
250
- self.logger.debug("DEM data was saved to %s.", self._dem_path)
266
+ size_of_resampled_data = asizeof.asizeof(resampled_data) / 1024 / 1024
267
+ self.logger.debug("Size of resampled data: %s MB.", size_of_resampled_data)
251
268
 
252
- if self.rotation:
253
- self.rotate_dem()
269
+ return resampled_data
270
+
271
+ def apply_blur(self, data: np.ndarray) -> np.ndarray:
272
+ """Apply blur to DEM data.
273
+
274
+ Arguments:
275
+ data (np.ndarray): DEM data.
276
+
277
+ Returns:
278
+ np.ndarray: Blurred DEM data.
279
+ """
280
+ if self.blur_radius == 0:
281
+ return data
282
+
283
+ self.logger.debug(
284
+ "Applying Gaussion blur to DEM data with kernel size %s.",
285
+ self.blur_radius,
286
+ )
287
+
288
+ blurred_data = cv2.GaussianBlur(
289
+ data, (self.blur_radius, self.blur_radius), sigmaX=10, sigmaY=10
290
+ )
291
+ self.logger.debug(
292
+ "DEM data was blurred. Shape: %s, dtype: %s. Min: %s, max: %s.",
293
+ blurred_data.shape,
294
+ blurred_data.dtype,
295
+ blurred_data.min(),
296
+ blurred_data.max(),
297
+ )
298
+ return blurred_data
254
299
 
255
300
  def rotate_dem(self) -> None:
256
301
  """Rotate DEM image."""
@@ -29,9 +29,6 @@ if TYPE_CHECKING:
29
29
  class DTMProviderSettings(BaseModel):
30
30
  """Base class for DTM provider settings models."""
31
31
 
32
- easy_mode: bool = True
33
- power_factor: int = 0
34
-
35
32
 
36
33
  # pylint: disable=too-many-public-methods, too-many-instance-attributes
37
34
  class DTMProvider(ABC):
@@ -56,20 +53,7 @@ class DTMProvider(ABC):
56
53
 
57
54
  _instructions: str | None = None
58
55
 
59
- _base_instructions = (
60
- "ℹ️ Using **Easy mode** is recommended, as it automatically adjusts the values in the "
61
- "image, so the terrain elevation in Giants Editor will match real world "
62
- "elevation in meters. \n"
63
- "ℹ️ If the terrain height difference in the real world is bigger than 255 meters, "
64
- "the [Height scale](https://github.com/iwatkot/maps4fs/blob/main/docs/dem.md#height-scale)"
65
- " parameter in the **map.i3d** file will be automatically adjusted. \n"
66
- "⚡ If the **Easy mode** option is disabled, you will probably get completely flat "
67
- "terrain, unless you adjust the DEM Multiplier Setting or the Height scale parameter in "
68
- "the Giants Editor. \n"
69
- "💡 You can use the **Power factor** setting to make the difference between heights "
70
- "bigger. Be extremely careful with this setting, and use only low values, otherwise your "
71
- "terrain may be completely broken. \n"
72
- )
56
+ _base_instructions = None
73
57
 
74
58
  # pylint: disable=R0913, R0917
75
59
  def __init__(
@@ -304,64 +288,8 @@ class DTMProvider(ABC):
304
288
  # extract region of interest from the tile
305
289
  data = self.extract_roi(tile)
306
290
 
307
- # process elevation data to be compatible with the game
308
- data = self.process_elevation(data)
309
-
310
291
  return data
311
292
 
312
- def process_elevation(self, data: np.ndarray) -> np.ndarray:
313
- """Process elevation data.
314
-
315
- Arguments:
316
- data (np.ndarray): Elevation data.
317
-
318
- Returns:
319
- np.ndarray: Processed elevation data.
320
- """
321
- self.data_info = {}
322
- self.add_numpy_params(data, "original")
323
-
324
- data = self.ground_height_data(data)
325
- self.add_numpy_params(data, "grounded")
326
-
327
- original_deviation = int(self.data_info["original_deviation"])
328
- in_game_maximum_height = 65535 // 255
329
- if original_deviation > in_game_maximum_height:
330
- suggested_height_scale_multiplier = (
331
- original_deviation / in_game_maximum_height # type: ignore
332
- )
333
- suggested_height_scale_value = int(255 * suggested_height_scale_multiplier)
334
- else:
335
- suggested_height_scale_multiplier = 1
336
- suggested_height_scale_value = 255
337
-
338
- self.data_info["suggested_height_scale_multiplier"] = suggested_height_scale_multiplier
339
- self.data_info["suggested_height_scale_value"] = suggested_height_scale_value
340
-
341
- self.map.shared_settings.height_scale_multiplier = ( # type: ignore
342
- suggested_height_scale_multiplier
343
- )
344
- self.map.shared_settings.height_scale_value = suggested_height_scale_value # type: ignore
345
-
346
- if self.user_settings.easy_mode: # type: ignore
347
- try:
348
- data = self.normalize_dem(data)
349
- self.add_numpy_params(data, "normalized")
350
-
351
- normalized_deviation = self.data_info["normalized_deviation"]
352
- z_scaling_factor = normalized_deviation / original_deviation # type: ignore
353
- self.data_info["z_scaling_factor"] = z_scaling_factor
354
-
355
- self.map.shared_settings.mesh_z_scaling_factor = z_scaling_factor # type: ignore
356
- self.map.shared_settings.change_height_scale = True # type: ignore
357
-
358
- except Exception as e: # pylint: disable=W0718
359
- self.logger.error(
360
- "Failed to normalize DEM data. Error: %s. Using original data.", e
361
- )
362
-
363
- return data.astype(np.uint16)
364
-
365
293
  def info_sequence(self) -> dict[str, int | str | float] | None:
366
294
  """Returns the information sequence for the component. Must be implemented in the child
367
295
  class. If the component does not have an information sequence, an empty dictionary must be
@@ -414,6 +342,7 @@ class DTMProvider(ABC):
414
342
  desc="Downloading tiles",
415
343
  unit="tile",
416
344
  initial=len(tif_files),
345
+ total=len(urls),
417
346
  ):
418
347
  try:
419
348
  file_name = os.path.basename(url)
@@ -570,84 +499,4 @@ class DTMProvider(ABC):
570
499
 
571
500
  return data
572
501
 
573
- def normalize_dem(self, data: np.ndarray) -> np.ndarray:
574
- """Normalize DEM data to 16-bit unsigned integer using max height from settings.
575
-
576
- Arguments:
577
- data (np.ndarray): DEM data from SRTM file after cropping.
578
-
579
- Returns:
580
- np.ndarray: Normalized DEM data.
581
- """
582
- maximum_height = int(data.max())
583
- minimum_height = int(data.min())
584
- deviation = maximum_height - minimum_height
585
- self.logger.debug(
586
- "Maximum height: %s. Minimum height: %s. Deviation: %s.",
587
- maximum_height,
588
- minimum_height,
589
- deviation,
590
- )
591
- self.logger.debug("Number of unique values in original DEM data: %s.", np.unique(data).size)
592
-
593
- adjusted_maximum_height = maximum_height * 255
594
- adjusted_maximum_height = min(adjusted_maximum_height, 65535)
595
- scaling_factor = adjusted_maximum_height / maximum_height
596
- self.logger.debug(
597
- "Adjusted maximum height: %s. Scaling factor: %s.",
598
- adjusted_maximum_height,
599
- scaling_factor,
600
- )
601
-
602
- if self.user_settings.power_factor: # type: ignore
603
- power_factor = 1 + self.user_settings.power_factor / 10 # type: ignore
604
- self.logger.debug(
605
- "Applying power factor: %s to the DEM data.",
606
- power_factor,
607
- )
608
- data = np.power(data, power_factor)
609
-
610
- normalized_data = np.round(data * scaling_factor).astype(np.uint16)
611
- self.logger.debug(
612
- "Normalized data maximum height: %s. Minimum height: %s. Number of unique values: %s.",
613
- normalized_data.max(),
614
- normalized_data.min(),
615
- np.unique(normalized_data).size,
616
- )
617
- return normalized_data
618
-
619
- @staticmethod
620
- def ground_height_data(data: np.ndarray, add_one: bool = True) -> np.ndarray:
621
- """Shift the data to ground level (0 meter).
622
- Optionally add one meter to the data to leave some room
623
- for the water level and pit modifications.
624
-
625
- Arguments:
626
- data (np.ndarray): DEM data after cropping.
627
- add_one (bool): Add one meter to the data
628
-
629
- Returns:
630
- np.ndarray: Unsigned DEM data.
631
- """
632
- data = data - data.min()
633
- if add_one:
634
- data = data + 1
635
- return data
636
-
637
- def add_numpy_params(
638
- self,
639
- data: np.ndarray,
640
- prefix: str,
641
- ) -> None:
642
- """Add numpy array parameters to the data_info dictionary.
643
-
644
- Arguments:
645
- data (np.ndarray): Numpy array of the tile.
646
- prefix (str): Prefix for the parameters.
647
- """
648
- self.data_info[f"{prefix}_minimum_height"] = int(data.min()) # type: ignore
649
- self.data_info[f"{prefix}_maximum_height"] = int(data.max()) # type: ignore
650
- self.data_info[f"{prefix}_deviation"] = int(data.max() - data.min()) # type: ignore
651
- self.data_info[f"{prefix}_unique_values"] = int(np.unique(data).size) # type: ignore
652
-
653
502
  # endregion
@@ -31,6 +31,8 @@ class SRTM30Provider(DTMProvider):
31
31
 
32
32
  _settings = SRTM30ProviderSettings
33
33
 
34
+ _instructions = "ℹ️ Recommended settings: \nDEM Settings -> Blur Radius: **35**"
35
+
34
36
  def __init__(self, *args, **kwargs):
35
37
  super().__init__(*args, **kwargs)
36
38
  self.hgt_directory = os.path.join(self._tile_directory, "hgt")
@@ -102,18 +102,22 @@ class DEMSettings(SettingsModel):
102
102
  """Represents the advanced settings for DEM component.
103
103
 
104
104
  Attributes:
105
- multiplier (int): multiplier for the heightmap, every pixel will be multiplied by this
106
- value.
105
+ adjust_terrain_to_ground_level (bool): adjust terrain to ground level or not.
106
+ multiplier (int): multiplier for the heightmap.
107
107
  blur_radius (int): radius of the blur filter.
108
- plateau (int): plateau height, will be added to each pixel.
109
- water_depth (int): water depth, will be subtracted from each pixel where the water
110
- is present.
108
+ minimum_height_scale (int): minimum height scale for the i3d.
109
+ plateau (int): plateau height.
110
+ ceiling (int): ceiling height.
111
+ water_depth (int): water depth.
111
112
  """
112
113
 
114
+ adjust_terrain_to_ground_level: bool = True
113
115
  multiplier: int = 1
114
- blur_radius: int = 35
116
+ minimum_height_scale: int = 255
115
117
  plateau: int = 0
118
+ ceiling: int = 0
116
119
  water_depth: int = 0
120
+ blur_radius: int = 3
117
121
 
118
122
 
119
123
  class BackgroundSettings(SettingsModel):
@@ -129,7 +133,7 @@ class BackgroundSettings(SettingsModel):
129
133
  generate_background: bool = False
130
134
  generate_water: bool = False
131
135
  resize_factor: int = 8
132
- remove_center: bool = False
136
+ remove_center: bool = True
133
137
  apply_decimation: bool = False
134
138
  decimation_percent: int = 25
135
139
  decimation_agression: int = 3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: maps4fs
3
- Version: 1.8.171
3
+ Version: 1.8.173
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
@@ -562,13 +562,17 @@ You can also apply some advanced settings to the map generation process.<br>
562
562
 
563
563
  ### DEM Advanced settings
564
564
 
565
- - 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). To match the in-game heights with SRTM Data provider, the recommended value is 255 (if easy mode is disabled), but depending on the place, you will need to play with both multiplier and the height scale in Giants Editor to find the best values.
565
+ - Adjust terrain to ground level: Enabling this setting (default) will raise or lower the terrain so that it's lowest point is at ground level (taking into account the plateau and water depth settings).
566
566
 
567
- - 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.
567
+ - Multiplier: DEM multiplier can be used to make the terrain more pronounced. By default the DEM file will be exact copy of the real terrain. If you want to make it more steep, you can increase this value. The recommended value of the multiplier is 1.
568
568
 
569
- - Plateau: this value will be added to each pixel of the DEM image, making it "higher". It's useful when you want to add some negative heights on the map, that appear to be in a "low" place. By default, it's set to 0.
569
+ - Minimum height scale: This value is used as the heightScale in your map i3d. It will automatically be set higher if the elevation in your map (plus plateau, ceiling and water depth) is higher than this value.
570
570
 
571
- - Water depth: this value will be subtracted from each pixel of the DEM image, where water resources are located. Pay attention that it's not in meters, instead it in the pixel value of DEM, which is 16 bit image with possible values from 0 to 65535. When this value is set, the same value will be added to the plateau setting to avoid negative heights.
571
+ - Plateau: DEM plateau value (in meters) is used to make the whole map higher or lower. This value will be added to each pixel of the DEM image, making it higher. It can be useful if you're working on a plain area and need to add some negative height (to make rivers, for example).
572
+
573
+ - Ceiling: DEM ceiling value (in meters) is used to add padding in the DEM above the highest elevation in your map area. It can be useful if you plan to manually add some height to the map by sculpting the terrain in GE.
574
+
575
+ - Water depth: Water depth value (in meters) will be subtracted from the DEM image, making the water deeper. The pixel value used for this is calculated based on the heightScale value for your map.
572
576
 
573
577
  ### Background terrain Advanced settings
574
578
 
@@ -578,7 +582,7 @@ You can also apply some advanced settings to the map generation process.<br>
578
582
 
579
583
  - Resize factor - the factor by which the background terrain will be resized. It will be used as 1 / resize_factor while generating the models. Which means that the larger the value the more the terrain will be resized. The lowest value is 1, in this case background terrain will not be resized. Note, than low values will lead to long processing and enormous size of the obj files.
580
584
 
581
- - Remove center - if enabled, the playable region (map terrain) will be removed from the background terrain. Note, that it will require low resize factors, to avoid gaps between the map and the background terrain.
585
+ - Remove center - if enabled, the playable region (map terrain) will be removed from the background terrain. By default, it's set to True.
582
586
 
583
587
  - Apply decimation - if enabled, the mesh will be simplified to reduce the number of faces.
584
588
 
@@ -703,4 +707,3 @@ But also, I want to thank the people who helped me with the project in some way,
703
707
  - [kbrandwijk](https://github.com/kbrandwijk) - for providing [awesome tool](https://github.com/Paint-a-Farm/satmap_downloader) to download the satellite images from the Google Maps and giving a permission to modify it and create a Python Package.
704
708
  - [Maaslandmods](https://github.com/Maaslandmods) - for the awesome idea to edit the tree schema in UI, images and code snippets on how to do it.
705
709
  - [StrauntMaunt](https://gitlab.com/StrauntMaunt) - for developing procedural generation scripts, providing with the required updates for maps4fs and preparing the docs on how to use procedural generation.
706
-
@@ -1,27 +1,27 @@
1
1
  maps4fs/__init__.py,sha256=iq4GcHoQ-by-BK6CzDOfVcdDww6osyi9Wv6pKXub68U,1307
2
2
  maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
3
3
  maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
- maps4fs/generator/dem.py,sha256=RsIaL3LabYnZzow2fzmccsnzBo0m1YjcVVxUkJJGJMU,10735
4
+ maps4fs/generator/dem.py,sha256=oLN02bWNax73HzFsseRBOV47Azl_1L7qdrzuxHh4i_c,11886
5
5
  maps4fs/generator/game.py,sha256=YwtdzqwcBJm1FHtc1ehDd3WbfTTW_gBvxK-pzfU48zs,11015
6
6
  maps4fs/generator/map.py,sha256=c5GMhr5iTRdXvTXoBKyVYa2V1tocR3ZCc7D_hpU21k8,11523
7
7
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
8
8
  maps4fs/generator/satellite.py,sha256=t33EfaxWTCjcKYnqoppSlFdQQSekiBe7UFmLgTVhFQI,3650
9
- maps4fs/generator/settings.py,sha256=psZC3k95roptV6-YQMDz9ofoKw5dBNc1TZ66DEECmSc,6134
9
+ maps4fs/generator/settings.py,sha256=cFlN-gK8QcySqyPtcGm-2fLnxQnlmC3Y9kQufJxwI3Y,6270
10
10
  maps4fs/generator/texture.py,sha256=_IfqIuxH4934VJXKtdABHa6ToPWk3T0xknvlu-rZ5Uc,36570
11
11
  maps4fs/generator/component/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
12
- maps4fs/generator/component/background.py,sha256=pN1iMXm5Xg87OWh4XFml4bVIrueTJMdwVJvS3YWm0x0,18588
12
+ maps4fs/generator/component/background.py,sha256=cBBp-ULrJzuNG9wfE_MaxzAn_mR24neqtL7WjxXEb3I,18749
13
13
  maps4fs/generator/component/config.py,sha256=RitKgFDZPzjA1fi8GcEi1na75qqaueUvpcITHjBvCXc,3674
14
14
  maps4fs/generator/component/grle.py,sha256=aKMjVJNuKHJJ2gsXaH00bz10kWaIbbZXU_JbP-ZAGw4,18990
15
15
  maps4fs/generator/component/i3d.py,sha256=3x38yL-kSJ8ylBwICBb6wPYzRSky4gVj8XCk2jzYSeo,19861
16
16
  maps4fs/generator/component/base/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
17
17
  maps4fs/generator/component/base/component.py,sha256=U3XJae0GUvHN08psv2j24Y4PBAAytSkSK3AmD-VjjXE,21404
18
18
  maps4fs/generator/component/base/component_image.py,sha256=vuiiJqsrKZgYsnK8Qa5yL2Pejqa6nJSTtXNAcBPL29c,3099
19
- maps4fs/generator/component/base/component_mesh.py,sha256=-9kmMX5KUULAIScygcrzmq8Cg643hiZ2IfSZOKiFfRs,4634
19
+ maps4fs/generator/component/base/component_mesh.py,sha256=jtsEQZkqiCkycYpueq1M1eqgxdShL_bHaJT6Upu8PLI,8412
20
20
  maps4fs/generator/component/base/component_xml.py,sha256=6OO1dKoceO1ACk7-k1oGtnkfNud8ZN3u3ZNjdNMpTqw,3967
21
21
  maps4fs/generator/dtm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  maps4fs/generator/dtm/bavaria.py,sha256=7njrEvSCYAC8ZVyvS-_84iXHhWA0oHKrEqSzxdnZuGs,4293
23
23
  maps4fs/generator/dtm/canada.py,sha256=lYONwm6aNX5cjVggR3AiZZF9dlCDAWg0M8RMaObog8s,1288
24
- maps4fs/generator/dtm/dtm.py,sha256=Bu6-wHH2MXxLmfSEz_LYjfRdXmzsvnTBs18qHgdta0E,23191
24
+ maps4fs/generator/dtm/dtm.py,sha256=L824mwmd_cNTujwWBsRg7RZLq9KEXf0-xIzKx8M3y1Q,16872
25
25
  maps4fs/generator/dtm/england.py,sha256=YyCYwnNUJuBeeMNUozfKIj_yNjHpGeuH1Mz0NiAJL-U,1122
26
26
  maps4fs/generator/dtm/finland.py,sha256=Chi3-3sanLIYpipjtPpTu9tqnL3DYcnygSDCPm1s24c,1753
27
27
  maps4fs/generator/dtm/flanders.py,sha256=81pKkrM40SeOe1LSlcsTNXSmUNpofC4D454DG6WFSyA,1037
@@ -33,7 +33,7 @@ maps4fs/generator/dtm/norway.py,sha256=kSsNZGEqb3IQ3a82UCJ_Iw_0wqdCgEvuRJ4JpEK7Q
33
33
  maps4fs/generator/dtm/nrw.py,sha256=1zMa10O0NdQbiTwOa7XXGrx3NhdHUqRXI4yBn_Scb2M,958
34
34
  maps4fs/generator/dtm/scotland.py,sha256=Od4dDMuCM_iteewjGBbmZXJ26S0bDwrhRhXeV4HyyOA,4803
35
35
  maps4fs/generator/dtm/spain.py,sha256=HxN0MvrtRqqeTmhl47q60-1DGaMDb2wq6neMcDlDCl8,1005
36
- maps4fs/generator/dtm/srtm.py,sha256=abggmxrR1rC22llkJ6kFle9MYLqP-raMc76O9qm6qag,4411
36
+ maps4fs/generator/dtm/srtm.py,sha256=ob6AUuEn3G3G9kdqTA2VhT335N65RRBJsqAfHuw0gA8,4502
37
37
  maps4fs/generator/dtm/usgs.py,sha256=1XzLP5GJbe6xcqzkOrEBUtR2SPw7gm6rl1nw5YXmBP8,3253
38
38
  maps4fs/generator/dtm/utils.py,sha256=I-wUSA_J85Xbt8sZCZAVKHSIcrMj5Ng-0adtPVhVmk0,2315
39
39
  maps4fs/generator/dtm/base/wcs.py,sha256=UfkLNHqsYTr2ytjyZjWDocq2hFC_qyFCvl-_tnz6gHM,2510
@@ -42,8 +42,8 @@ maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,4
42
42
  maps4fs/toolbox/background.py,sha256=RclEqxEWLbMxuEkkegQP8jybzugwQ1_R3rdfDe0s21U,2104
43
43
  maps4fs/toolbox/custom_osm.py,sha256=X6ZlPqiOhNjkmdD_qVroIfdOl9Rb90cDwVSLDVYgx80,1892
44
44
  maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
45
- maps4fs-1.8.171.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
46
- maps4fs-1.8.171.dist-info/METADATA,sha256=-tS0TohOL4AcCk2sVK_XYAXWvKndrwtczFPm888wUic,43208
47
- maps4fs-1.8.171.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
48
- maps4fs-1.8.171.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
49
- maps4fs-1.8.171.dist-info/RECORD,,
45
+ maps4fs-1.8.173.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
46
+ maps4fs-1.8.173.dist-info/METADATA,sha256=juPm5cwfn3XLlaUemJjP43yHQbW-9NYEljrHscMlGN4,42937
47
+ maps4fs-1.8.173.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
48
+ maps4fs-1.8.173.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
49
+ maps4fs-1.8.173.dist-info/RECORD,,