Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Textures Schema Editor #126

Merged
merged 4 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,15 @@
</div>

🗺️ Supports 2x2, 4x4, 8x8, 16x16 and any custom size maps<br>
🔄 Support map rotation 🆕<br>
🔄 Support map rotation<br>
🌐 Supports custom [DTM Providers](#DTM-Providers) 🆕<br>
🌾 Automatically generates fields 🆕<br>
🌽 Automatically generates farmlands 🆕<br>
🌿 Automatically generates decorative foliage 🆕<br>
🌲 Automatically generates forests 🆕<br>
🌊 Automatically generates water planes 🆕<br>
🌾 Automatically generates fields<br>
🌽 Automatically generates farmlands<br>
🌿 Automatically generates decorative foliage<br>
🌲 Automatically generates forests<br>
🌊 Automatically generates water planes<br>
📈 Automatically generates splines 🆕<br>
🛰️ Automatically downloads high resolution satellite images 🆕<br>
🏔️ Allows to use multiple DTM providers for elevation models 🆕<br>
🌍 Based on real-world data from OpenStreetMap<br>
🗺️ Supports [custom OSM maps](/docs/custom_osm.md)<br>
🏞️ Generates height map using SRTM dataset<br>
Expand All @@ -73,10 +72,14 @@
🌿 Automatically generates decorative foliage.<br><br>
<img src="https://github.com/user-attachments/assets/27a5e541-a9f5-4504-b8d2-64aae9fb3e52"><br>
🌲 Automatically generates forests.<br><br>
<img src="https://github.com/user-attachments/assets/891911d7-081d-431e-a677-b4ae96870286"><br>
🌲 Allows to select trees for generation.<br><br>
<img src="https://github.com/user-attachments/assets/cce7d4e0-cba2-4dd2-b22d-03137fb2e860"><br>
🌊 Automatically generates water planes.<br><br>
<img src="https://github.com/user-attachments/assets/0b05b511-a595-48e7-a353-8298081314a4"><br>
📈 Automatically generates splines.<br><br>
<img src="https://github.com/user-attachments/assets/0957db9e-7b95-4951-969c-9d1edd9f073b"><br>
🖌️ Allows customization of the texture schema.<br><br>
<img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
🗒️ True-to-life blueprints for fast and precise modding.<br><br>
<img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
Expand Down Expand Up @@ -233,6 +236,8 @@ Tools are divided into categories, which are listed below.
#### For custom schemas
- **Tree Schema Editor** - allows you to view all the supported trees models and select the ones you need on your map. After it, you should click the Show updated schema button and copy the JSON schema to the clipboard. Then you can use it in the Expert settings to generate the map with the selected trees.

- **Texture Schema Editor** - allows you to view all the supported textures and edit their parameters, such as priority, OSM tags and so on. After editing, you should click the Show updated schema button and copy the JSON schema to the clipboard. Then you can use it in the Expert settings to generate the map with the updated textures.

#### For Textures and DEM
- **GeoTIFF windowing** - allows you to upload your GeoTIFF file and select the region of interest to extract it from the image. It's useful when you have high-resolution DEM data and want to create a height map using it.

Expand Down
75 changes: 35 additions & 40 deletions maps4fs/generator/texture.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,15 +340,8 @@ def _read_parameters(self) -> None:
- map dimensions in meters
- map coefficients (meters per pixel)
"""
north, south, east, west = self.get_bbox(project_utm=True)

# Parameters of the map in UTM format (meters).
self.minimum_x = min(west, east)
self.minimum_y = min(south, north)
self.maximum_x = max(west, east)
self.maximum_y = max(south, north)
self.logger.debug("Map minimum coordinates (XxY): %s x %s.", self.minimum_x, self.minimum_y)
self.logger.debug("Map maximum coordinates (XxY): %s x %s.", self.maximum_x, self.maximum_y)
bbox = ox.utils_geo.bbox_from_point(self.coordinates, dist=self.map_rotated_size / 2)
self.minimum_x, self.minimum_y, self.maximum_x, self.maximum_y = bbox

def info_sequence(self) -> dict[str, Any]:
"""Returns the JSON representation of the generation info for textures."""
Expand Down Expand Up @@ -576,29 +569,19 @@ def draw_base_layer(self, cumulative_image: np.ndarray) -> None:
cv2.imwrite(layer_path, img)
self.logger.debug("Base texture %s saved.", layer_path)

def get_relative_x(self, x: float) -> int:
"""Converts UTM X coordinate to relative X coordinate in map image.
def latlon_to_pixel(self, lat: float, lon: float) -> tuple[int, int]:
"""Converts latitude and longitude to pixel coordinates.

Arguments:
x (float): UTM X coordinate.
lat (float): Latitude.
lon (float): Longitude.

Returns:
int: Relative X coordinate in map image.
tuple[int, int]: Pixel coordinates.
"""
return int(self.map_rotated_size * (x - self.minimum_x) / (self.maximum_x - self.minimum_x))

def get_relative_y(self, y: float) -> int:
"""Converts UTM Y coordinate to relative Y coordinate in map image.

Arguments:
y (float): UTM Y coordinate.

Returns:
int: Relative Y coordinate in map image.
"""
return int(
self.map_rotated_size * (1 - (y - self.minimum_y) / (self.maximum_y - self.minimum_y))
)
x = int((lon - self.minimum_x) / (self.maximum_x - self.minimum_x) * self.map_rotated_size)
y = int((lat - self.maximum_y) / (self.minimum_y - self.maximum_y) * self.map_rotated_size)
return x, y

def np_to_polygon_points(self, np_array: np.ndarray) -> list[tuple[int, int]]:
"""Converts numpy array of polygon points to list of tuples.
Expand All @@ -623,11 +606,13 @@ def _to_np(self, geometry: shapely.geometry.polygon.Polygon, *args) -> np.ndarra
Returns:
np.ndarray: Numpy array of polygon points.
"""
xs, ys = geometry.exterior.coords.xy
xs = [int(self.get_relative_x(x)) for x in xs.tolist()]
ys = [int(self.get_relative_y(y)) for y in ys.tolist()]
pairs = list(zip(xs, ys))
return np.array(pairs, dtype=np.int32).reshape((-1, 1, 2))
coords = list(geometry.exterior.coords)
pts = np.array(
[self.latlon_to_pixel(coord[1], coord[0]) for coord in coords],
np.int32,
)
pts = pts.reshape((-1, 1, 2))
return pts

def _to_polygon(
self, obj: pd.core.series.Series, width: int | None
Expand Down Expand Up @@ -664,9 +649,20 @@ def _sequence(
Returns:
shapely.geometry.polygon.Polygon: Polygon geometry.
"""
polygon = geometry.buffer(width)
polygon = geometry.buffer(self.meters_to_degrees(width) if width else 0)
return polygon

def meters_to_degrees(self, meters: int) -> float:
"""Converts meters to degrees.

Arguments:
meters (int): Meters.

Returns:
float: Degrees.
"""
return meters / 111320

def _skip(
self, geometry: shapely.geometry.polygon.Polygon, *args, **kwargs
) -> shapely.geometry.polygon.Polygon:
Expand Down Expand Up @@ -724,12 +720,11 @@ def objects_generator(
except Exception as e: # pylint: disable=W0718
self.logger.debug("Error fetching objects for tags: %s. Error: %s.", tags, e)
return
objects_utm = ox.projection.project_gdf(objects, to_latlong=False)
self.logger.debug("Fetched %s elements for tags: %s.", len(objects_utm), tags)
self.logger.debug("Fetched %s elements for tags: %s.", len(objects), tags)

method = self.linestrings_generator if yield_linestrings else self.polygons_generator

yield from method(objects_utm, width, is_fieds)
yield from method(objects, width, is_fieds)

def linestrings_generator(
self, objects_utm: pd.core.frame.DataFrame, *args, **kwargs
Expand All @@ -745,9 +740,7 @@ def linestrings_generator(
for _, obj in objects_utm.iterrows():
geometry = obj["geometry"]
if isinstance(geometry, shapely.geometry.linestring.LineString):
points = [
(self.get_relative_x(x), self.get_relative_y(y)) for x, y in geometry.coords
]
points = [self.latlon_to_pixel(x, y) for y, x in geometry.coords]
yield points

def polygons_generator(
Expand All @@ -773,7 +766,9 @@ def polygons_generator(
continue

if is_fieds and self.map.texture_settings.fields_padding > 0:
padded_polygon = polygon.buffer(-self.map.texture_settings.fields_padding)
padded_polygon = polygon.buffer(
-self.meters_to_degrees(self.map.texture_settings.fields_padding)
)

if not isinstance(padded_polygon, shapely.geometry.polygon.Polygon):
self.logger.warning("The padding value is too high, field will not padded.")
Expand Down
3 changes: 2 additions & 1 deletion webui/tools/section.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from tools.background import ConvertImageToObj
from tools.dem import GeoTIFFWindowingTool
from tools.textures import TextureSchemaEditorTool
from tools.tool import Tool
from tools.trees import TreeSchemaEditorTool

Expand All @@ -24,7 +25,7 @@ def add(cls):
class Shemas(Section):
title = "📄 Schemas"
description = "Tools to work with different schemas."
tools = [TreeSchemaEditorTool]
tools = [TreeSchemaEditorTool, TextureSchemaEditorTool]


class TexturesAndDEM(Section):
Expand Down
74 changes: 74 additions & 0 deletions webui/tools/textures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import json
from typing import Any, NamedTuple

import streamlit as st
from config import FS25_TEXTURE_SCHEMA_PATH
from tools.textures_data import TEXTURE_URLS
from tools.tool import Tool

COLUMNS_PER_ROW = 5


class TextureInfo(NamedTuple):
name: str
data: dict[str, Any]
url: str


class TextureSchemaEditorTool(Tool):
title = "Texture Schema Editor"
description = "This tool allows you to edit the texture schema for the map generation. "
icon = "🎨"

def content(self):
with open(FS25_TEXTURE_SCHEMA_PATH, "r", encoding="utf-8") as f:
self.texture_schema = json.load(f)

texture_infos = []
for texture in self.texture_schema:
texture_name = texture["name"]
texture_url = TEXTURE_URLS.get(texture_name)
if not texture_url:
continue

texture_infos.append(TextureInfo(texture_name, texture, texture_url))

self.button_container = st.container()

# Create a grid of images using the number of columns per row
self.text_areas = {}
for i in range(0, len(texture_infos), COLUMNS_PER_ROW):
row = st.columns(COLUMNS_PER_ROW)
for j, texture_info in enumerate(texture_infos[i : i + COLUMNS_PER_ROW]):
with row[j]:
st.image(texture_info.url, use_container_width=True)
text_area = st.text_area(
texture_info.name,
value=json.dumps(texture_info.data, indent=2),
key=texture_info.name,
height=160,
)
self.text_areas[texture_info.name] = text_area

with self.button_container:
if st.button("Show updated schema", key="show_updated_texture_schema"):
texture_schema = self.read_schema()
st.success(
"Texture schema was generated, click the copy button to copy it to the "
"clipboard. \n"
"Then paste it into the texture schema input field in the generation tool."
)
st.json(texture_schema, expanded=False)

def read_schema(self) -> list[dict[str, str | int]]:
new_schema = []
for texture_name, texture_data in self.text_areas.items():
try:
data = json.loads(texture_data)
except json.JSONDecodeError:
st.error(f"Error reading schema for texture name: {texture_name}")
continue

new_schema.append(data)

return new_schema
45 changes: 45 additions & 0 deletions webui/tools/textures_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
TEXTURE_URLS = {
"asphalt": "https://github.com/user-attachments/assets/a1a5f095-9a14-4b9c-af42-a3399157e17e",
"asphaltCracks": "https://github.com/user-attachments/assets/4c124557-0c5e-45f3-906f-937c6a1f284a",
"asphaltDirt": "https://github.com/user-attachments/assets/89fb98aa-3bc4-4a45-b2cf-5a7aafe4aa07",
"asphaltDusty": "https://github.com/user-attachments/assets/cdbcda8c-5d4c-4f0a-9869-7b11f7cfbade",
"asphaltGravel": "https://github.com/user-attachments/assets/3754efbc-bbff-4845-884d-14a176d6b174",
"asphaltTwigs": "https://github.com/user-attachments/assets/513aa2a9-e8f5-4b34-858c-ff54d242e684",
"concrete": "https://github.com/user-attachments/assets/544737ff-0e83-4a37-9751-efc275f7510d",
"concreteGravelSand": "https://github.com/user-attachments/assets/24100801-55b7-45b2-93f1-fb53a07b1d61",
"concretePebbles": "https://github.com/user-attachments/assets/ff793ba6-9e54-4f3e-a21e-8e096bf63839",
"concreteShattered": "https://github.com/user-attachments/assets/9d0b22ed-4435-4bea-801b-1bda45cddb83",
"forestGrass": "https://github.com/user-attachments/assets/6b215423-2034-40fb-a678-eaa075373854",
"forestLeaves": "https://github.com/user-attachments/assets/b5bb051f-b647-4faa-b9a3-90ce4a77a48e",
"forestNeedels": "https://github.com/user-attachments/assets/91df605e-eda6-4221-a572-8fb84df5c987",
"forestRockRoots": "https://github.com/user-attachments/assets/7804a25a-1803-41e4-93d8-b1fcf8b701b1",
"grass": "https://github.com/user-attachments/assets/30469cf8-f984-4359-8c0c-8387b44ae519",
"grassClovers": "https://github.com/user-attachments/assets/03a7e881-4f8a-46dd-8e2a-672a7d82cf2f",
"grassCut": "https://github.com/user-attachments/assets/c54249e8-2deb-46e8-baf5-784d949dfd34",
"grassDirtPatchy": "https://github.com/user-attachments/assets/20168e61-585d-43bf-bc07-dca74d652d89",
"grassDirtPatchyDry": "https://github.com/user-attachments/assets/90eea824-7d03-4060-8e57-cce29f72ba64",
"grassDirtStones": "https://github.com/user-attachments/assets/3acc201a-ee21-4492-a746-150a6e4b310a",
"grassFreshMiddle": "https://github.com/user-attachments/assets/2aca54e6-68db-44d3-9c8f-998522ca18ba",
"grassFreshShort": "https://github.com/user-attachments/assets/103d6efd-032e-4672-b717-ba64d9f9cd9c",
"grassMoss": "https://github.com/user-attachments/assets/8e5a7dff-987c-4772-8531-e237c7aa0cee",
"gravel": "https://github.com/user-attachments/assets/d0c8de3f-191c-43a7-899d-902e63b10553",
"gravelDirtMoss": "https://github.com/user-attachments/assets/b618bba2-f4a4-4b7b-881f-6462c98fc666",
"gravelPebblesMoss": "https://github.com/user-attachments/assets/48332684-6878-4aab-b917-ddaea3338abe",
"gravelPebblesMossPatchy": "https://github.com/user-attachments/assets/5e1acd59-7674-470e-b5b0-307ea9b250e1",
"gravelSmall": "https://github.com/user-attachments/assets/acf725c6-d295-4c92-bb42-e761bdf2feb1",
"mudDark": "https://github.com/user-attachments/assets/c6ce48ab-13c1-4000-a75a-673ebd451149",
"mudDarkGrassPatchy": "https://github.com/user-attachments/assets/8c70e8a7-e561-4028-8f23-0e41fb0b4e76",
"mudDarkMossPatchy": "https://github.com/user-attachments/assets/e21b538f-c52e-465b-b88a-a93f87d01584",
"mudLeaves": "https://github.com/user-attachments/assets/540263bf-807d-4159-99f0-8c1493075834",
"mudLight": "https://github.com/user-attachments/assets/58aa1d41-15bd-4d03-8eed-1e20fcb39cf4",
"mudPebbles": "https://github.com/user-attachments/assets/e51cac8e-dd4b-4e76-8a8d-7320182fc652",
"mudPebblesLight": "https://github.com/user-attachments/assets/6973aaf7-2894-430d-9ba0-ece78e4b55d7",
"mudTracks": "https://github.com/user-attachments/assets/d4a222bb-6f2b-459d-8a36-38e1a31a46ba",
"pebblesForestGround": "https://github.com/user-attachments/assets/ffb1a80d-1338-4e9e-84db-aa157d894617",
"rock": "https://github.com/user-attachments/assets/a1a78285-b7c3-4a2a-b2ec-fbae4898f566",
"rockFloorTiles": "https://github.com/user-attachments/assets/7068057b-b2c8-4952-ab88-47afa700c488",
"rockFloorTilesPattern": "https://github.com/user-attachments/assets/6be8c2a3-d3c7-4951-82b5-8780068e20c5",
"rockForest": "https://github.com/user-attachments/assets/ef7ee19a-c1b9-4371-a5b2-3309118ef981",
"rockyForestGround": "https://github.com/user-attachments/assets/5e08392b-ec91-466a-8c46-2a632015c4bb",
"sand": "https://github.com/user-attachments/assets/4c79b020-cbc3-40ea-ae7f-04599309ef80",
}
2 changes: 1 addition & 1 deletion webui/tools/trees.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def content(self):
self.checkboxes[tree_info] = tree_checkbox

with self.button_container:
if st.button("Show updated schema"):
if st.button("Show updated schema", key="show_updated_tree_schema"):
tree_schema = self.read_schema()
st.success(
"Tree schema was generated, click the copy button to copy it to the "
Expand Down
Loading