Skip to content

Commit

Permalink
Custom map sizes
Browse files Browse the repository at this point in the history
* Multiplier and blur radius.

* WebUI update.

* Custom map sizes.

* Version update.

* README update.

* Logging update.

* Typing updates.

* Tests update.

* README update.

* webUI update.
  • Loading branch information
iwatkot authored Nov 18, 2024
1 parent 55f100f commit 2acec72
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 82 deletions.
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<a href="#How-To-Run">How-To-Run</a> •
<a href="#Features">Features</a> •
<a href="#Supported-objects">Supported objects</a> •
<a href="Settings">Settings</a> •
<a href="#Advanced Settings">Advanced Settings</a> •
<a href="#Bugs-and-feature-requests">Bugs and feature requests</a>
</p>

Expand Down Expand Up @@ -136,7 +136,8 @@ import maps4fs as mfs
map = mfs.Map(
game,
(52.5200, 13.4050), # Latitude and longitude of the map center.
distance=1024, # The DISTANCE from the center to the edge of the map in meters. The map will be 2048x2048 meters.
height=1024, # The height of the map in meters.
width=1024, # The width of the map in meters.
map_directory="path/to/your/map/directory", # The directory where the map will be saved.
)
```
Expand Down Expand Up @@ -172,18 +173,28 @@ The list will be updated as the project develops.
The script will also generate the `generation_info.json` file in the `output` folder. It contains the following keys: <br>
`"coordinates"` - the coordinates of the map center which you entered,<br>
`"bbox"` - the bounding box of the map in lat and lon,<br>
`"distance"` - the size of the map in meters,<br>
`"map_height"` - the height of the map in meters (this one is from the user input, e.g. 2048 and so on),<br>
`"map_width"` - the width of the map in meters (same as above),<br>
`"minimum_x"` - the minimum x coordinate of the map (UTM projection),<br>
`"minimum_y"` - the minimum y coordinate of the map (UTM projection),<br>
`"maximum_x"` - the maximum x coordinate of the map (UTM projection),<br>
`"maximum_y"` - the maximum y coordinate of the map (UTM projection),<br>
`"height"` - the height of the map in meters (it won't be equal to the distance since the Earth is not flat, sorry flat-earthers),<br>
`"width"` - the width of the map in meters,<br>
`"height"` - the height of the map in meters (it won't be equal to the parameters above since the Earth is not flat, sorry flat-earthers),<br>
`"width"` - the width of the map in meters (same as above),<br>
`"height_coef"` - since we need a texture of exact size, the height of the map is multiplied by this coefficient,<br>
`"width_coef"` - same as above but for the width,<br>
`"tile_name"` - the name of the SRTM tile which was used to generate the height map, e.g. "N52E013"<br>

You can use this information to adjust some other sources of data to the map, e.g. textures, height maps, etc.

## Advanced Settings
You can also apply some advanced settings to the map generation process. Note that they're ADVANCED, so you don't need to use them if you're not sure what they do.<br>

Here's the list of the advanced settings:

- DEM 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 avaiable value there is 65535, while the actual difference between the deepest and the highest point on Earth is about 20 km. So, by default this value is set to 3. Just note that this setting mostly does not matter, because you can always adjust it in the Giants Editor, learn more about the [heightScale](https://www.farming-simulator.org/19/terrain-heightscale.php) parameter on the [PMC Farming Simulator](https://www.farming-simulator.org/) website.

- DEM 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 as a Minecraft-like map.

## Bugs and feature requests
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).<br>
Binary file modified data/fs22-map-template.zip
Binary file not shown.
44 changes: 40 additions & 4 deletions maps4fs/generator/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@

from typing import TYPE_CHECKING, Any

import osmnx as ox # type: ignore

if TYPE_CHECKING:
from maps4fs.generator.game import Game


# pylint: disable=R0801, R0903
# pylint: disable=R0801, R0903, R0902
class Component:
"""Base class for all map generation components.
Args:
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
distance (int): The distance from the center to the edge of the map.
map_height (int): The height of the map in pixels.
map_width (int): The width of the map in pixels.
map_directory (str): The directory where the map files are stored.
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
info, warning. If not provided, default logging will be used.
Expand All @@ -24,18 +27,21 @@ def __init__(
self,
game: Game,
coordinates: tuple[float, float],
distance: int,
map_height: int,
map_width: int,
map_directory: str,
logger: Any = None,
**kwargs, # pylint: disable=W0613, R0913, R0917
):
self.game = game
self.coordinates = coordinates
self.distance = distance
self.map_height = map_height
self.map_width = map_width
self.map_directory = map_directory
self.logger = logger
self.kwargs = kwargs

self.save_bbox()
self.preprocess()

def preprocess(self) -> None:
Expand All @@ -61,3 +67,33 @@ def previews(self) -> list[str]:
NotImplementedError: If the method is not implemented in the child class.
"""
raise NotImplementedError

def get_bbox(self, project_utm: bool = False) -> tuple[int, int, int, int]:
"""Calculates the bounding box of the map from the coordinates and the height and
width of the map.
Args:
project_utm (bool, optional): Whether to project the bounding box to UTM.
Returns:
tuple[int, int, int, int]: The bounding box of the map.
"""
north, south, _, _ = ox.utils_geo.bbox_from_point(
self.coordinates, dist=self.map_height / 2, project_utm=project_utm
)
_, _, east, west = ox.utils_geo.bbox_from_point(
self.coordinates, dist=self.map_width / 2, project_utm=project_utm
)
bbox = north, south, east, west
self.logger.debug(
f"Calculated bounding box for component: {self.__class__.__name__}: {bbox}, "
f"project_utm: {project_utm}"
)
return bbox

def save_bbox(self) -> None:
"""Saves the bounding box of the map to the component instance from the coordinates and the
height and width of the map.
"""
self.bbox = self.get_bbox(project_utm=False)
self.logger.debug(f"Saved bounding box: {self.bbox}")
14 changes: 8 additions & 6 deletions maps4fs/generator/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ class Config(Component):
Args:
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
distance (int): The distance from the center to the edge of the map.
map_height (int): The height of the map in pixels.
map_width (int): The width of the map in pixels.
map_directory (str): The directory where the map files are stored.
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
info, warning. If not provided, default logging will be used.
"""

def preprocess(self) -> None:
self._map_xml_path = self.game.map_xml_path(self.map_directory)
self.logger.debug(f"Map XML path: {self._map_xml_path}")
self.logger.debug("Map XML path: %s.", self._map_xml_path)

def process(self):
self._set_map_size()
Expand All @@ -36,10 +37,11 @@ def _set_map_size(self):
self.logger.debug("Map XML file loaded from: %s.", self._map_xml_path)
root = tree.getroot()
for map_elem in root.iter("map"):
width = height = str(self.distance * 2)
map_elem.set("width", width)
map_elem.set("height", height)
self.logger.debug("Map size set to %sx%s in Map XML file.", width, height)
map_elem.set("width", str(self.map_width))
map_elem.set("height", str(self.map_height))
self.logger.debug(
"Map size set to %sx%s in Map XML file.", self.map_width, self.map_height
)
tree.write(self._map_xml_path)
self.logger.debug("Map XML file saved to: %s.", self._map_xml_path)

Expand Down
46 changes: 34 additions & 12 deletions maps4fs/generator/dem.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@

import cv2
import numpy as np
import osmnx as ox # type: ignore
import rasterio # type: ignore
import requests

from maps4fs.generator.component import Component

SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
DEFAULT_MULTIPLIER = 3
DEFAULT_BLUR_RADIUS = 21


# pylint: disable=R0903
Expand All @@ -22,7 +23,8 @@ class DEM(Component):
Args:
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
distance (int): The distance from the center to the edge of the map.
map_height (int): The height of the map in pixels.
map_width (int): The width of the map in pixels.
map_directory (str): The directory where the map files are stored.
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
info, warning. If not provided, default logging will be used.
Expand All @@ -36,24 +38,27 @@ def preprocess(self) -> None:
os.makedirs(self.hgt_dir, exist_ok=True)
os.makedirs(self.gz_dir, exist_ok=True)

self.multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
self.blur_radius = self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS)
self.logger.debug(
"DEM multiplier is %s, blur radius is %s.", self.multiplier, self.blur_radius
)

# pylint: disable=no-member
def process(self) -> None:
"""Reads SRTM file, crops it to map size, normalizes and blurs it,
saves to map directory."""
north, south, east, west = ox.utils_geo.bbox_from_point( # pylint: disable=W0632
self.coordinates, dist=self.distance
)
self.logger.debug(
f"Processing DEM. North: {north}, south: {south}, east: {east}, west: {west}."
)
north, south, east, west = self.bbox

dem_output_size = self.distance * self.game.dem_multipliyer + 1
dem_height = self.map_height * self.game.dem_multipliyer + 1
dem_width = self.map_width * self.game.dem_multipliyer + 1
self.logger.debug(
"DEM multiplier is %s, DEM output size is %s.",
"DEM multiplier is %s, DEM height is %s, DEM width is %s.",
self.game.dem_multipliyer,
dem_output_size,
dem_height,
dem_width,
)
dem_output_resolution = (dem_output_size, dem_output_size)
dem_output_resolution = (dem_width, dem_height)
self.logger.debug("DEM output resolution: %s.", dem_output_resolution)

tile_path = self._srtm_tile()
Expand Down Expand Up @@ -88,12 +93,29 @@ def process(self) -> None:
data, dem_output_resolution, interpolation=cv2.INTER_LINEAR
).astype("uint16")

self.logger.debug(
f"Maximum value in resampled data: {resampled_data.max()}, "
f"minimum value: {resampled_data.min()}."
)

resampled_data = resampled_data * self.multiplier
self.logger.debug(
f"DEM data multiplied by {self.multiplier}. Shape: {resampled_data.shape}, "
f"dtype: {resampled_data.dtype}. "
f"Min: {resampled_data.min()}, max: {resampled_data.max()}."
)

self.logger.debug(
f"DEM data was resampled. Shape: {resampled_data.shape}, "
f"dtype: {resampled_data.dtype}. "
f"Min: {resampled_data.min()}, max: {resampled_data.max()}."
)

resampled_data = cv2.GaussianBlur(resampled_data, (self.blur_radius, self.blur_radius), 0)
self.logger.debug(
f"Gaussion blur applied to DEM data with kernel size {self.blur_radius}. "
)

cv2.imwrite(self._dem_path, resampled_data)
self.logger.debug("DEM data was saved to %s.", self._dem_path)

Expand Down
20 changes: 12 additions & 8 deletions maps4fs/generator/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class Map:
Args:
game (Type[Game]): Game for which the map is generated.
coordinates (tuple[float, float]): Coordinates of the center of the map.
distance (int): Distance from the center of the map.
height (int): Height of the map in pixels.
width (int): Width of the map in pixels.
map_directory (str): Path to the directory where map files will be stored.
logger (Any): Logger instance
"""
Expand All @@ -27,21 +28,26 @@ def __init__( # pylint: disable=R0917
self,
game: Game,
coordinates: tuple[float, float],
distance: int,
height: int,
width: int,
map_directory: str,
logger: Any = None,
**kwargs,
):
self.game = game
self.components: list[Component] = []
self.coordinates = coordinates
self.distance = distance
self.height = height
self.width = width
self.map_directory = map_directory

if not logger:
logger = Logger(__name__, to_stdout=True, to_file=False)
self.logger = logger
self.logger.debug("Game was set to %s", game.code)

self.kwargs = kwargs

os.makedirs(self.map_directory, exist_ok=True)
self.logger.debug("Map directory created: %s", self.map_directory)

Expand All @@ -58,9 +64,11 @@ def generate(self) -> None:
component = game_component(
self.game,
self.coordinates,
self.distance,
self.height,
self.width,
self.map_directory,
self.logger,
**self.kwargs,
)
try:
component.process()
Expand All @@ -71,7 +79,6 @@ def generate(self) -> None:
e,
)
raise e
# setattr(self, game_component.__name__.lower(), component)
self.components.append(component)

pbar.update(1)
Expand All @@ -82,9 +89,6 @@ def previews(self) -> list[str]:
Returns:
list[str]: List of preview images.
"""
# texture_previews = self.texture.previews() # type: ignore # pylint: disable=no-member
# dem_previews = self.dem.previews() # type: ignore # pylint: disable=no-member
# return texture_previews + dem_previews
previews = []
for component in self.components:
previews.extend(component.previews())
Expand Down
Loading

0 comments on commit 2acec72

Please sign in to comment.