Skip to content

Commit 107041b

Browse files
iwatkotCopilot
andauthored
Output size feature: possibility to resize the map to the specified dimensions
* Output size feature. * WebUI update for Output size. * README update. * Message update Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 8d9f36d commit 107041b

16 files changed

+227
-61
lines changed

.pylintrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ disable=raw-checker-failed,
2727
unused-argument,
2828
consider-using-from-import,
2929
line-too-long,
30-
import-outside-toplevel
30+
import-outside-toplevel,
31+
too-many-branches

README.md

+45-6
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
<a href="docs/step_by_step.md">Create a map in 10 steps</a> •
1010
<a href="#How-To-Run">How-To-Run</a><br>
1111
<a href="docs/FAQ.md">FAQ</a> •
12-
<a href="docs/map_structure.md">Map Structure</a> •
13-
<a href="#Modder-Toolbox">Modder Toolbox</a><br>
12+
<a href="docs/map_structure.md">Map Structure</a>
13+
<a href="#Modder-Toolbox">Modder Toolbox</a> •
14+
<a href="#Main-Settings">Main Settings</a><br>
1415
<a href="#Supported-objects">Supported objects</a> •
1516
<a href="#Generation-info">Generation info</a> •
1617
<a href="#Texture-schema">Texture schema</a> •
@@ -42,15 +43,16 @@
4243
</div>
4344

4445
🗺️ Supports 2x2, 4x4, 8x8, 16x16 and any custom size maps<br>
46+
✂️ Supports map scaling 🆕<br>
4547
🔄 Support map rotation<br>
46-
🌐 Supports custom [DTM Providers](#DTM-Providers) 🆕<br>
48+
🌐 Supports custom [DTM Providers](#DTM-Providers)<br>
4749
🌾 Automatically generates fields<br>
4850
🌽 Automatically generates farmlands<br>
4951
🌿 Automatically generates decorative foliage<br>
5052
🌲 Automatically generates forests<br>
5153
🌊 Automatically generates water planes<br>
52-
📈 Automatically generates splines 🆕<br>
53-
🛰️ Automatically downloads high resolution satellite images 🆕<br>
54+
📈 Automatically generates splines<br>
55+
🛰️ Automatically downloads high resolution satellite images<br>
5456
🌍 Based on real-world data from OpenStreetMap<br>
5557
🗺️ Supports [custom OSM maps](/docs/custom_osm.md)<br>
5658
🏞️ Generates height map using SRTM dataset<br>
@@ -168,7 +170,8 @@ Don't know where to start? Don't worry, just follow this [step-by-step guide](do
168170

169171
🟢 Recommended for all users.
170172
🛠️ Don't need to install anything.
171-
🗺️ Supported map sizes: 2x2, 4x4, 8x8 km.
173+
🗺️ Supported map sizes: 2x2, 4x4.
174+
✂️ Map scaling: not supported.
172175
⚙️ Advanced settings: enabled.
173176
🖼️ Texture dissolving: enabled.
174177
Using the public version on [maps4fs.xyz](https://maps4fs.xyz) is the easiest way to generate a map template. Just open the link and follow the instructions.
@@ -181,6 +184,7 @@ Using it is easy and doesn't require any guides. Enjoy!
181184
🟠 Recommended for users who want bigger maps, fast generation, nice-looking textures, and advanced settings.
182185
🛠️ Launch with one single command.
183186
🗺️ Supported map sizes: 2x2, 4x4, 8x8, 16x16 km and any custom size.
187+
✂️ Map scaling: supported.
184188
⚙️ Advanced settings: enabled.
185189
🖼️ Texture dissolving: enabled.
186190
Check out the [Docker FAQ](docs/FAQ_docker.md) if you have any questions.<br>
@@ -212,6 +216,7 @@ Remember to replace `*.*.*` with the version you want to use, e.g. `iwatkot/maps
212216

213217
🔴 Recommended for developers.
214218
🗺️ Supported map sizes: 2x2, 4x4, 8x8, 16x16 km and any custom size.
219+
✂️ Map scaling: supported.
215220
⚙️ Advanced settings: enabled.
216221
🖼️ Texture dissolving: enabled.
217222
You can use the Python package to generate maps. Follow these steps:
@@ -316,6 +321,40 @@ Tools are divided into categories, which are listed below.
316321

317322
- **Convert image to obj model** - allows you to convert the image to the obj model. You can use this tool to create the background terrain for your map. It can be extremely useful if you have access to the sources of high-resolution DEM data and want to create the background terrain using it.
318323

324+
## Main Settings
325+
326+
### Game Selection
327+
The tool supports two games: Farming Simulator 22 and Farming Simulator 25. You can select the game you want to generate the map for in the `Game` dropdown list. The default value is `FS25`, but you can change it to `FS22` if you want to generate a map for Farming Simulator 22.<br>
328+
**NOTE:** Support for Farming Simulator 22 is discontinued. The tool will not be updated for this game anymore. Some features, such as forest generation, fields generation not implemented and not planned to be implemented. The tool will be updated only for Farming Simulator 25.<br>
329+
330+
### Latitude and Longitude
331+
These are the coordinates of the center of the map. The coordinates should be in decimal format, e.g. `45.28, 20.23`, any other format will not work.
332+
333+
### Map Size
334+
335+
#### Default sizes
336+
The tool supports all possible sizes of maps, but some of them only available in the [Docker version](#option-2-docker-version). <br>
337+
The sizes are:
338+
- 2x2 km
339+
- 4x4 km
340+
- 8x8 km
341+
- 16x16 km
342+
343+
**NOTE:** 16 km maps probably won't work for FS25 due to the limitations of the game engine. The map will be generated, but you may have issues trying to open it in the Giants Editor.
344+
345+
#### Custom size
346+
You can also specify any custom size of the map. Be aware that Giants Editor supports only square maps, which size is a power of 2 (2048, 4096 and so on). All other sizes will be generated, but if you try to open them in the Giants Editor, it will crash. If you want your map to cover other real-world region, use the [Output size](#output-size) option.
347+
348+
#### Output size
349+
This setting can be useful if you want add some scaling to your map. For example, you can select a region of 3000 meters in real world and set the output size to 2048 meters. In this case, the map will be generated with a size of 2048x2048 meters, but it will contain the region of 3000x3000 meters in real world.
350+
351+
### DTM Provider
352+
DTM Provider is a source of the height map data. will find the list of available providers in the [DTM Providers](#dtm-providers) section. The default provider is `SRTM30Provider` which is available all aroung the globe, but the resolution is not very high. If you want to use a different provider, you can select it in the dropdown list. You will only see the providers that are available for the selected region. It's better to use the provider that has the highest resolution for the selected region.
353+
**NOTE:** Some of the providers are community-developed and may not work properly. I do not provide any support for them. If you have any issues with them, please contact the provider's author.
354+
355+
### Map Rotation
356+
You can rotate the map by any angle. The rotation is applied to the map and the height map. The rotation is in degrees, so you can use any value from 0 to 360. The default value is `0`, which means that the map will be generated without rotation.
357+
319358
## Supported objects
320359

321360
The project is based on the [OpenStreetMap](https://www.openstreetmap.org/) data. So, refer to [this page](https://wiki.openstreetmap.org/wiki/Map_Features) to understand the list below.

demo.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@
2828
dtm_provider = mfs.dtm.SRTM30Provider
2929

3030
# 3️⃣ Define the coordinates of the central point of the map, size and rotation.
31-
lat, lon = 45.28, 20.23
31+
lat, lon = 45.2915463035936, 20.219764914521804
3232
coordinates = (lat, lon)
3333
size = 2048
34-
rotation = 0
34+
rotation = 15
35+
output_size = 1024
3536

3637
# 4️⃣ Define the output directory.
3738
map_directory = "map_directory"
@@ -44,9 +45,9 @@
4445

4546
# 6️⃣ Optional: advanced settings. You can use the default settings, but
4647
# it's recommended to change them according to your needs.
47-
dem_settings = mfs.settings.DEMSettings(multiplier=1, blur_radius=15, plateau=15, water_depth=10)
48+
dem_settings = mfs.settings.DEMSettings(multiplier=1, blur_radius=40, plateau=15, water_depth=10)
4849
background_settings = mfs.settings.BackgroundSettings(
49-
# generate_background=True,
50+
generate_background=True,
5051
generate_water=True,
5152
water_blurriness=50,
5253
resize_factor=8,
@@ -95,6 +96,7 @@
9596
satellite_settings=satellite_settings,
9697
# texture_custom_schema=texture_custom_schema,
9798
# tree_custom_schema=tree_custom_schema,
99+
output_size=output_size,
98100
)
99101

100102
# 9️⃣ Launch the generation process.

maps4fs/generator/component/background.py

+36-14
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,14 @@ def process(self) -> None:
8787

8888
if not self.map.custom_background_path:
8989
self.dem.process()
90-
self.validate_np_for_mesh(self.dem.dem_path, self.map_size)
91-
92-
shutil.copyfile(self.dem.dem_path, self.not_substracted_path)
93-
self.save_map_dem(self.dem.dem_path, save_path=self.not_resized_path)
90+
self.validate_np_for_mesh(self.output_path, self.map_size)
9491

9592
if self.map.dem_settings.water_depth:
9693
self.subtraction()
9794

98-
cutted_dem_path = self.save_map_dem(self.dem.dem_path)
95+
cutted_dem_path = self.save_map_dem(self.output_path)
96+
shutil.copyfile(self.output_path, self.not_substracted_path)
97+
self.save_map_dem(self.output_path, save_path=self.not_resized_path)
9998
if self.game.additional_dem_name is not None:
10099
self.make_copy(cutted_dem_path, self.game.additional_dem_name)
101100

@@ -213,19 +212,28 @@ def generate_obj_files(self) -> None:
213212
"""Iterates over all dems and generates 3D obj files based on DEM data.
214213
If at least one DEM file is missing, the generation will be stopped at all.
215214
"""
216-
if not os.path.isfile(self.dem.dem_path):
215+
if not os.path.isfile(self.output_path):
217216
self.logger.error(
218-
"DEM file not found, generation will be stopped: %s", self.dem.dem_path
217+
"DEM file not found, generation will be stopped: %s", self.output_path
219218
)
220219
return
221220

222-
self.logger.debug("DEM file for found: %s", self.dem.dem_path)
221+
self.logger.debug("DEM file for found: %s", self.output_path)
223222

224-
filename = os.path.splitext(os.path.basename(self.dem.dem_path))[0]
223+
filename = os.path.splitext(os.path.basename(self.output_path))[0]
225224
save_path = os.path.join(self.background_directory, f"{filename}.obj")
226225
self.logger.debug("Generating obj file in path: %s", save_path)
227226

228-
dem_data = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED)
227+
dem_data = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
228+
229+
if self.map.output_size is not None:
230+
scaled_background_size = int(self.background_size * self.map.size_scale)
231+
dem_data = cv2.resize(
232+
dem_data,
233+
(scaled_background_size, scaled_background_size),
234+
interpolation=cv2.INTER_NEAREST,
235+
)
236+
229237
self.plane_from_np(
230238
dem_data,
231239
save_path,
@@ -255,7 +263,7 @@ def save_map_dem(self, dem_path: str, save_path: str | None = None) -> str:
255263
if self.map.dem_settings.add_foundations:
256264
dem_data = self.create_foundations(dem_data)
257265

258-
output_size = self.map_size + 1
266+
output_size = self.scaled_size + 1
259267

260268
main_dem_path = self.game.dem_file_path(self.map_directory)
261269

@@ -265,7 +273,7 @@ def save_map_dem(self, dem_path: str, save_path: str | None = None) -> str:
265273
pass
266274

267275
resized_dem_data = cv2.resize(
268-
dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR
276+
dem_data, (output_size, output_size), interpolation=cv2.INTER_NEAREST
269277
)
270278

271279
cv2.imwrite(main_dem_path, resized_dem_data)
@@ -301,7 +309,7 @@ def plane_from_np(
301309
decimation_percent=self.map.background_settings.decimation_percent,
302310
decimation_agression=self.map.background_settings.decimation_agression,
303311
remove_center=remove_center,
304-
remove_size=self.map_size,
312+
remove_size=self.scaled_size,
305313
)
306314

307315
try:
@@ -348,7 +356,7 @@ def previews(self) -> list[str]:
348356
preview_paths = self.dem_previews(self.game.dem_file_path(self.map_directory))
349357

350358
background_dem_preview_path = os.path.join(self.previews_directory, "background_dem.png")
351-
background_dem_preview_image = cv2.imread(self.dem.dem_path, cv2.IMREAD_UNCHANGED)
359+
background_dem_preview_image = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
352360

353361
background_dem_preview_image = cv2.resize(
354362
background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
@@ -458,6 +466,7 @@ def create_background_textures(self) -> None:
458466
map_directory=self.map_directory,
459467
logger=self.logger,
460468
texture_custom_schema=background_layers, # type: ignore
469+
skip_scaling=True, # type: ignore
461470
)
462471

463472
self.background_texture.preprocess()
@@ -545,6 +554,19 @@ def generate_water_resources_obj(self) -> None:
545554
# Single channeled 16 bit DEM image of terrain.
546555
background_dem = cv2.imread(self.not_substracted_path, cv2.IMREAD_UNCHANGED)
547556

557+
if self.map.output_size is not None:
558+
scaled_background_size = int(self.background_size * self.map.size_scale)
559+
plane_water = cv2.resize(
560+
plane_water,
561+
(scaled_background_size, scaled_background_size),
562+
interpolation=cv2.INTER_NEAREST,
563+
)
564+
background_dem = cv2.resize(
565+
background_dem,
566+
(scaled_background_size, scaled_background_size),
567+
interpolation=cv2.INTER_NEAREST,
568+
)
569+
548570
if self.map.background_settings.water_blurriness:
549571
# Apply Gaussian blur to the background dem.
550572
blur_power = self._get_blur_power()

maps4fs/generator/component/base/component.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,8 @@ def top_left_coordinates_to_center(self, top_left: tuple[int, int]) -> tuple[int
339339
tuple[int, int]: The coordinates in the center system.
340340
"""
341341
x, y = top_left
342-
cs_x = x - self.map_size // 2
343-
cs_y = y - self.map_size // 2
342+
cs_x = x - self.scaled_size // 2
343+
cs_y = y - self.scaled_size // 2
344344

345345
return cs_x, cs_y
346346

@@ -368,23 +368,22 @@ def fit_object_into_bounds(
368368
raise ValueError("Either polygon or linestring points must be provided.")
369369

370370
min_x = min_y = 0 + border
371-
max_x = max_y = self.map_size - border
371+
max_x = max_y = self.scaled_size - border
372372

373373
object_type = Polygon if polygon_points else LineString
374374

375-
# polygon = Polygon(polygon_points)
376375
osm_object = object_type(polygon_points or linestring_points)
377376

378377
if angle:
379-
center_x = center_y = self.map_rotated_size // 2
378+
center_x = center_y = self.map_rotated_size * self.map.size_scale // 2
380379
self.logger.debug(
381380
"Rotating the osm_object by %s degrees with center at %sx%s",
382381
angle,
383382
center_x,
384383
center_y,
385384
)
386385
osm_object = rotate(osm_object, -angle, origin=(center_x, center_y))
387-
offset = (self.map_size / 2) - (self.map_rotated_size / 2)
386+
offset = int((self.map_size / 2) - (self.map_rotated_size / 2)) * self.map.size_scale
388387
self.logger.debug("Translating the osm_object by %s", offset)
389388
osm_object = translate(osm_object, xoff=offset, yoff=offset)
390389
self.logger.debug("Rotated and translated the osm_object.")
@@ -566,3 +565,12 @@ def get_z_scaling_factor(self, ignore_height_scale_multiplier: bool = False) ->
566565
scaling_factor *= 1 / self.map.shared_settings.mesh_z_scaling_factor
567566

568567
return scaling_factor
568+
569+
@property
570+
def scaled_size(self) -> int:
571+
"""Returns the output size of the map if it was set, otherwise returns the map size.
572+
573+
Returns:
574+
int: The output size of the map or the map size.
575+
"""
576+
return self.map_size if self.map.output_size is None else self.map.output_size

maps4fs/generator/component/base/component_image.py

+14
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,17 @@ def apply_blur(
161161
)
162162

163163
return blurred_data
164+
165+
def get_blur_radius(self) -> int:
166+
"""Get the blur radius from the DEM settings.
167+
168+
Returns:
169+
int: The blur radius.
170+
"""
171+
blur_radius = self.map.dem_settings.blur_radius
172+
if blur_radius is None or blur_radius <= 0:
173+
# We'll disable blur if the radius is 0 or negative.
174+
blur_radius = 0
175+
elif blur_radius % 2 == 0:
176+
blur_radius += 1
177+
return blur_radius

maps4fs/generator/component/config.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ def _set_map_size(self) -> None:
3636

3737
root = tree.getroot()
3838
data = {
39-
"width": str(self.map_size),
40-
"height": str(self.map_size),
39+
"width": str(self.scaled_size),
40+
"height": str(self.scaled_size),
4141
}
4242

4343
for element in root.iter("map"):

maps4fs/generator/component/dem.py

+5-18
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,6 @@ def preprocess(self) -> None:
4040
self.output_resolution = self.get_output_resolution()
4141
self.logger.debug("Output resolution for DEM data: %s.", self.output_resolution)
4242

43-
blur_radius = self.map.dem_settings.blur_radius
44-
if blur_radius is None or blur_radius <= 0:
45-
# We'll disable blur if the radius is 0 or negative.
46-
blur_radius = 0
47-
elif blur_radius % 2 == 0:
48-
blur_radius += 1
49-
self.blur_radius = blur_radius
50-
self.multiplier = self.map.dem_settings.multiplier
51-
self.logger.debug(
52-
"DEM value multiplier is %s, blur radius is %s.",
53-
self.multiplier,
54-
self.blur_radius,
55-
)
56-
5743
self.dtm_provider: DTMProvider = self.map.dtm_provider( # type: ignore
5844
coordinates=self.coordinates,
5945
user_settings=self.map.dtm_provider_settings,
@@ -200,7 +186,7 @@ def process(self) -> None:
200186
self.update_info("normalized", resampled_data)
201187

202188
# 6. Blur DEM data.
203-
resampled_data = self.apply_blur(resampled_data, blur_radius=self.blur_radius)
189+
resampled_data = self.apply_blur(resampled_data, blur_radius=self.get_blur_radius())
204190

205191
cv2.imwrite(self._dem_path, resampled_data)
206192
self.logger.debug("DEM data was saved to %s.", self._dem_path)
@@ -288,13 +274,14 @@ def apply_multiplier(self, data: np.ndarray) -> np.ndarray:
288274
Returns:
289275
np.ndarray: Multiplied DEM data.
290276
"""
291-
if not self.multiplier != 1:
277+
multiplier = self.map.dem_settings.multiplier
278+
if not multiplier != 1:
292279
return data
293280

294-
multiplied_data = data * self.multiplier
281+
multiplied_data = data * multiplier
295282
self.logger.debug(
296283
"DEM data was multiplied by %s. Min: %s, max: %s.",
297-
self.multiplier,
284+
multiplier,
298285
multiplied_data.min(),
299286
multiplied_data.max(),
300287
)

0 commit comments

Comments
 (0)