Skip to content

Commit

Permalink
Settings refactoring
Browse files Browse the repository at this point in the history
* Settings refactoring.

* Advanced settings.

* UI updates.

* Tests updates.

* Kwargs update.

* Tests updates.
  • Loading branch information
iwatkot authored Dec 25, 2024
1 parent 0856fd8 commit dbfd066
Show file tree
Hide file tree
Showing 15 changed files with 307 additions and 205 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
run: pylint maps4fs

- name: Run pytest with coverage
run: pytest --cov=maps4fs --cov-report xml
run: pytest --cov=maps4fs/generator --cov-report xml

- name: Download Code Climate test-reporter
run: |
Expand Down
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,7 @@
## Quick Start
There are several ways to use the tool. You obviously need the **first one**, but you can choose any of the others depending on your needs.<br>
### 🚜 For most users
**Option 1:** Open the [maps4fs](https://maps4fs.streamlit.app) on StreamLit and generate a map template in a few clicks.<br>
<i>Note, that StreamLit community hosting has some limitations, such as: <br>
1. Maximum map size is 4096x4096 meters. <br>
2. Advanced settings are disabled. <br>
3. Texture dissolving is disabled (they will look worse). </i><br>

If you run the application locally, you won't have any of these limitations and will be able to generate maps of any size with any settings you want and nice looking textures.<br>
So, jump to [Docker version](#option-2-docker-version) to launch the tool with one command and get the full experience.<br>
**Option 1:** Open the [maps4fs](https://maps4fs.xyz) and generate a map template in a few clicks.<br>

![Basic WebUI](https://github.com/user-attachments/assets/52f499cc-f28a-4da3-abef-0e818abe8dbe)

Expand Down Expand Up @@ -140,13 +133,13 @@ Don't know where to start? Don't worry, just follow this [step-by-step guide](do

## How-To-Run

### Option 1: StreamLit
🟢 Recommended for all users.
### Option 1: Public version
🟢 Recommended for all users.
🛠️ Don't need to install anything.
🗺️ Supported map sizes: 2x2, 4x4 km.
⚙️ Advanced settings: disabled.
🖼️ Texture dissolving: disabled.
Using the [StreamLit](https://maps4fs.streamlit.app) version of the tool is the easiest way to generate a map template. Just open the link and follow the instructions.
🗺️ Supported map sizes: 2x2, 4x4, 8x8 km.
⚙️ Advanced settings: enabled.
🖼️ Texture dissolving: enabled.
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.
Note: due to CPU and RAM limitations of the hosting, the generation may take some time. If you need faster processing, use the [Docker version](#option-2-docker-version).<br>

Using it is easy and doesn't require any guides. Enjoy!
Expand Down Expand Up @@ -463,6 +456,8 @@ You can also apply some advanced settings to the map generation process. Note th

- Fields padding - this value (in meters) will be applied to each field, making it smaller. It's useful when the fields are too close to each other and you want to make them smaller. By default, it's set to 0.

- Texture dissolving - if enabled, the values from one layer will be splitted between different layers of texture, making it look more natural. By default, it's set to True. Can be turned of for faster processing.

### Farmlands Advanced settings

- Farmlands margin - this value (in meters) will be applied to each farmland, making it bigger. You can use the value to adjust how much the farmland should be bigger than the actual field. By default, it's set to 3.
Expand All @@ -473,6 +468,12 @@ You can also apply some advanced settings to the map generation process. Note th

- Random plants - when adding decorative foliage, enabling this option will add different species of plants to the map. If unchecked only basic grass (smallDenseMix) will be added. Defaults to True.

### Background terrain Advanced settings

- Generate background - if enabled, the obj files for the background terrain will be generated. You can turn it off if you already have those files or don't need them. By default, it's set to True.

- Generate water - if enabled, the water planes obj files will be generated. You can turn it off if you already have those files or don't need them. By default, it's set to True.

## Resources
In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
To create a basic map, you only need the Giants Editor. But if you want to create a background terrain - the world around the map, so it won't look like it's floating in the void - you also need Blender and the Blender Exporter Plugins. To create realistic textures for the background terrain, the QGIS is required to obtain high-resolution satellite images.<br>
Expand Down
9 changes: 8 additions & 1 deletion maps4fs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# pylint: disable=missing-module-docstring
from maps4fs.generator.game import Game
from maps4fs.generator.map import Map
from maps4fs.generator.map import (
BackgroundSettings,
DEMSettings,
GRLESettings,
I3DSettings,
Map,
TextureSettings,
)
from maps4fs.logger import Logger
28 changes: 9 additions & 19 deletions maps4fs/generator/background.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@
import trimesh # type: ignore

from maps4fs.generator.component import Component
from maps4fs.generator.dem import (
DEFAULT_BLUR_RADIUS,
DEFAULT_MULTIPLIER,
DEFAULT_PLATEAU,
DEM,
)
from maps4fs.generator.dem import DEM
from maps4fs.generator.texture import Texture

DEFAULT_DISTANCE = 2048
Expand Down Expand Up @@ -46,8 +41,6 @@ class Background(Component):
# pylint: disable=R0801
def preprocess(self) -> None:
"""Registers the DEMs for the background terrain."""
self.light_version = self.kwargs.get("light_version", False)
self.water_depth = self.kwargs.get("water_depth", 0)
self.stl_preview_path: str | None = None
self.water_resources_path: str | None = None

Expand All @@ -65,7 +58,7 @@ def preprocess(self) -> None:
os.makedirs(self.background_directory, exist_ok=True)
os.makedirs(self.water_directory, exist_ok=True)

autoprocesses = [self.kwargs.get("auto_process", False), False]
autoprocesses = [self.map.dem_settings.auto_process, False]
self.output_paths = [
os.path.join(self.background_directory, f"{name}.png") for name in ELEMENTS
]
Expand All @@ -83,11 +76,8 @@ def preprocess(self) -> None:
self.rotation,
self.map_directory,
self.logger,
auto_process=autoprocess,
blur_radius=self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS),
multiplier=self.kwargs.get("multiplier", DEFAULT_MULTIPLIER),
plateau=self.kwargs.get("plateau", DEFAULT_PLATEAU),
)
dem.auto_process = autoprocess
dem.preprocess()
dem.is_preview = self.is_preview(name) # type: ignore
dem.set_output_resolution((self.rotated_size, self.rotated_size))
Expand Down Expand Up @@ -118,7 +108,7 @@ def process(self) -> None:
if not dem.is_preview: # type: ignore
shutil.copyfile(dem.dem_path, self.not_substracted_path)

if self.water_depth:
if self.map.dem_settings.water_depth:
self.subtraction()

for dem in self.dems:
Expand All @@ -127,8 +117,9 @@ def process(self) -> None:
if self.game.additional_dem_name is not None:
self.make_copy(cutted_dem_path, self.game.additional_dem_name)

if not self.light_version:
if self.map.background_settings.generate_background:
self.generate_obj_files()
if self.map.background_settings.generate_water:
self.generate_water_resources_obj()
else:
self.logger.info("Light version is enabled, obj files will not be generated.")
Expand Down Expand Up @@ -325,7 +316,7 @@ def plane_from_np(
self.mesh_to_stl(mesh)
else:
if not include_zeros:
multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
multiplier = self.map.dem_settings.multiplier
if multiplier != 1:
z_scaling_factor = 1 / multiplier
else:
Expand Down Expand Up @@ -485,8 +476,7 @@ def create_background_textures(self) -> None:
rotation=self.rotation,
map_directory=self.map_directory,
logger=self.logger,
light_version=self.light_version,
custom_schema=background_layers,
custom_schema=background_layers, # type: ignore
)

self.background_texture.preprocess()
Expand Down Expand Up @@ -534,7 +524,7 @@ def subtraction(self) -> None:

# Create a mask where water_resources_image is 255 (or not 0)
# Subtract water_depth from dem_image where mask is True
dem_image[mask] = dem_image[mask] - self.water_depth
dem_image[mask] = dem_image[mask] - self.map.dem_settings.water_depth

# Save the modified dem_image back to the output path
cv2.imwrite(output_path, dem_image)
Expand Down
11 changes: 9 additions & 2 deletions maps4fs/generator/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from maps4fs.generator.map import Map


# pylint: disable=R0801, R0903, R0902, R0904
# pylint: disable=R0801, R0903, R0902, R0904, R0913, R0917
class Component:
"""Base class for all map generation components.
Expand All @@ -46,7 +46,7 @@ def __init__(
rotation: int,
map_directory: str,
logger: Any = None,
**kwargs, # pylint: disable=W0613, R0913, R0917
**kwargs: dict[str, Any],
):
self.game = game
self.map = map
Expand All @@ -58,6 +58,13 @@ def __init__(
self.logger = logger
self.kwargs = kwargs

self.logger.info(
"Component %s initialized. Map size: %s, map rotated size: %s", # type: ignore
self.__class__.__name__,
self.map_size,
self.map_rotated_size,
)

os.makedirs(self.previews_directory, exist_ok=True)
os.makedirs(self.scripts_directory, exist_ok=True)
os.makedirs(self.info_layers_directory, exist_ok=True)
Expand Down
25 changes: 11 additions & 14 deletions maps4fs/generator/dem.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
from maps4fs.generator.component import Component

SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
DEFAULT_MULTIPLIER = 1
DEFAULT_BLUR_RADIUS = 35
DEFAULT_PLATEAU = 0


# pylint: disable=R0903, R0902
Expand Down Expand Up @@ -50,20 +47,20 @@ def preprocess(self) -> None:
self.output_resolution = self.get_output_resolution()
self.logger.debug("Output resolution for DEM data: %s.", self.output_resolution)

self.multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
blur_radius = self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS)
blur_radius = self.map.dem_settings.blur_radius
if blur_radius is None or blur_radius <= 0:
# We'll disable blur if the radius is 0 or negative.
blur_radius = 0
elif blur_radius % 2 == 0:
blur_radius += 1
self.blur_radius = blur_radius
self.logger.debug(
"DEM value multiplier is %s, blur radius is %s.", self.multiplier, self.blur_radius
"DEM value multiplier is %s, blur radius is %s.",
self.map.dem_settings.multiplier,
self.blur_radius,
)

self.auto_process = self.kwargs.get("auto_process", False)
self.plateau = self.kwargs.get("plateau", False)
self.auto_process = self.map.dem_settings.auto_process

@property
def dem_path(self) -> str:
Expand Down Expand Up @@ -191,11 +188,11 @@ def process(self) -> None:
resampled_data = self._normalize_dem(resampled_data)
else:
self.logger.debug("Auto processing is disabled, DEM data will not be normalized.")
resampled_data = resampled_data * self.multiplier
resampled_data = resampled_data * self.map.dem_settings.multiplier

self.logger.debug(
"DEM data was multiplied by %s. Min: %s, max: %s. Data type: %s.",
self.multiplier,
self.map.dem_settings.multiplier,
resampled_data.min(),
resampled_data.max(),
resampled_data.dtype,
Expand All @@ -210,7 +207,7 @@ def process(self) -> None:
self.logger.debug(
"DEM data was multiplied by %s and clipped to 16-bit unsigned integer range. "
"Min: %s, max: %s.",
self.multiplier,
self.map.dem_settings.multiplier,
resampled_data.min(),
resampled_data.max(),
)
Expand Down Expand Up @@ -240,18 +237,18 @@ def process(self) -> None:
resampled_data.max(),
)

if self.plateau:
if self.map.dem_settings.plateau:
# Plateau is a flat area with a constant height.
# So we just add this value to each pixel of the DEM.
# And also need to ensure that there will be no values with height greater than
# it's allowed in 16-bit unsigned integer.

resampled_data += self.plateau
resampled_data += self.map.dem_settings.plateau
resampled_data = np.clip(resampled_data, 0, 65535)

self.logger.debug(
"Plateau with height %s was added to DEM data. Min: %s, max: %s.",
self.plateau,
self.map.dem_settings.plateau,
resampled_data.min(),
resampled_data.max(),
)
Expand Down
7 changes: 2 additions & 5 deletions maps4fs/generator/grle.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ def preprocess(self) -> None:
"""Gets the path to the map I3D file from the game instance and saves it to the instance
attribute. If the game does not support I3D files, the attribute is set to None."""

self.farmland_margin = self.kwargs.get("farmland_margin", 0)
self.randomize_plants = self.kwargs.get("randomize_plants", True)

try:
grle_schema_path = self.game.grle_schema
except ValueError:
Expand Down Expand Up @@ -148,7 +145,7 @@ def _add_farmlands(self) -> None:
for field in fields:
try:
fitted_field = self.fit_polygon_into_bounds(
field, self.farmland_margin, angle=self.rotation
field, self.map.grle_settings.farmland_margin, angle=self.rotation
)
except ValueError as e:
self.logger.warning(
Expand Down Expand Up @@ -358,7 +355,7 @@ def get_rounded_polygon(
# Add islands of plants to the base image.
island_count = self.map_size
self.logger.info("Adding %s islands of plants to the base image.", island_count)
if self.randomize_plants:
if self.map.grle_settings.random_plants:
grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
self.logger.debug("Islands of plants added to the base image.")

Expand Down
13 changes: 5 additions & 8 deletions maps4fs/generator/i3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,13 @@ class I3d(Component):
def preprocess(self) -> None:
"""Gets the path to the map I3D file from the game instance and saves it to the instance
attribute. If the game does not support I3D files, the attribute is set to None."""
self.auto_process = self.kwargs.get("auto_process", False)

try:
self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
except NotImplementedError:
self.logger.info("I3D file processing is not implemented for this game.")
self._map_i3d_path = None

self.forest_density = self.kwargs.get("forest_density", DEFAULT_FOREST_DENSITY)
self.logger.info("Forest density: %s.", self.forest_density)

def process(self) -> None:
"""Updates the map I3D file with the default settings."""
self._update_i3d_file()
Expand Down Expand Up @@ -84,7 +79,7 @@ def _update_i3d_file(self) -> None:
root = tree.getroot()
for map_elem in root.iter("Scene"):
for terrain_elem in map_elem.iter("TerrainTransformGroup"):
if self.auto_process:
if self.map.dem_settings.auto_process:
terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
self.logger.debug(
"heightScale attribute set to %s in TerrainTransformGroup element.",
Expand Down Expand Up @@ -395,12 +390,14 @@ def _add_forests(self) -> None:
forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)

tree_count = 0
for x, y in self.non_empty_pixels(forest_image, step=self.forest_density):
for x, y in self.non_empty_pixels(forest_image, step=self.map.i3d_settings.forest_density):
xcs, ycs = self.top_left_coordinates_to_center((x, y))
node_id += 1

rotation = randint(-180, 180)
xcs, ycs = self.randomize_coordinates((xcs, ycs), self.forest_density) # type: ignore
xcs, ycs = self.randomize_coordinates( # type: ignore
(xcs, ycs), self.map.i3d_settings.forest_density
)

random_tree = choice(tree_schema)
tree_name = random_tree["name"]
Expand Down
Loading

0 comments on commit dbfd066

Please sign in to comment.