From 727407151228e58f7c0bf1d85960d5a7e01f9ad6 Mon Sep 17 00:00:00 2001 From: Stan Soldatov <118521851+iwatkot@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:08:38 +0100 Subject: [PATCH] New tool: Fix custom OSM file * New tool: Fix custom OSM file. * README update. --- .vscode/launch.json | 2 +- README.md | 3 ++ maps4fs/toolbox/custom_osm.py | 67 ++++++++++++++++++++++++++++++ webui/tools/custom_osm.py | 76 +++++++++++++++++++++++++++++++++++ webui/tools/section.py | 3 +- 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 maps4fs/toolbox/custom_osm.py create mode 100644 webui/tools/custom_osm.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 0f101ce..77006a3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,7 +24,7 @@ "console": "integratedTerminal", "justMyCode": true, "env": { - "PYTHONPATH": "${workspaceFolder}:${PYTHONPATH}" + "PYTHONPATH": "${workspaceFolder}" } } ] diff --git a/README.md b/README.md index fccac53..d2d47ba 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,9 @@ Tools are divided into categories, which are listed below. - **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 + +- **Fix custom OSM file** - this tool fixes the most common errors in the custom OSM file, but it can not guarantee that the file will be fixed completely if some non-common errors are present. + - **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. #### For Background terrain diff --git a/maps4fs/toolbox/custom_osm.py b/maps4fs/toolbox/custom_osm.py new file mode 100644 index 0000000..a1836d6 --- /dev/null +++ b/maps4fs/toolbox/custom_osm.py @@ -0,0 +1,67 @@ +"""This module contains functions to work with custom OSM files.""" + +import json +from xml.etree import ElementTree as ET + +import osmnx as ox +from osmnx._errors import InsufficientResponseError + +from maps4fs.generator.game import FS25 + + +def check_osm_file(file_path: str) -> bool: + """Tries to read the OSM file using OSMnx and returns True if the file is valid, + False otherwise. + + Arguments: + file_path (str): Path to the OSM file. + + Returns: + bool: True if the file is valid, False otherwise. + """ + with open(FS25().texture_schema, encoding="utf-8") as f: + schema = json.load(f) + + tags = [] + for element in schema: + element_tags = element.get("tags") + if element_tags: + tags.append(element_tags) + + for tag in tags: + try: + ox.features_from_xml(file_path, tags=tag) + except InsufficientResponseError: + continue + except Exception: # pylint: disable=W0718 + return False + return True + + +def fix_osm_file(input_file_path: str, output_file_path: str) -> tuple[bool, int]: + """Fixes the OSM file by removing all the nodes and all the nodes with + action='delete'. + + Arguments: + input_file_path (str): Path to the input OSM file. + output_file_path (str): Path to the output OSM file. + + Returns: + tuple[bool, int]: A tuple containing the result of the check_osm_file function + and the number of fixed errors. + """ + broken_entries = ["relation", ".//*[@action='delete']"] + + tree = ET.parse(input_file_path) + root = tree.getroot() + + fixed_errors = 0 + for entry in broken_entries: + for element in root.findall(entry): + root.remove(element) + fixed_errors += 1 + + tree.write(output_file_path) + result = check_osm_file(output_file_path) + + return result, fixed_errors diff --git a/webui/tools/custom_osm.py b/webui/tools/custom_osm.py new file mode 100644 index 0000000..498acf0 --- /dev/null +++ b/webui/tools/custom_osm.py @@ -0,0 +1,76 @@ +import os + +import streamlit as st +from config import INPUT_DIRECTORY +from tools.tool import Tool + +from maps4fs.toolbox.custom_osm import fix_osm_file + + +class FixCustomOsmFile(Tool): + title = "Fix a custom OSM file" + description = ( + "This tool tries to fix a custom OSM file by removing all incorrect entries. " + "It does not guarantee that the file will be fixed, if some specific errors are " + "present in the file, since the tool works with a common set of errors." + ) + icon = "🛠️" + + save_path = None + download_path = None + + def content(self): + if "fixed_osm" not in st.session_state: + st.session_state.fixed_osm = False + uploaded_file = st.file_uploader( + "Upload a custom OSM file", type=["osm"], key="osm_uploader" + ) + + if uploaded_file is not None: + self.save_path = self.get_save_path(uploaded_file.name) + with open(self.save_path, "wb") as f: + f.write(uploaded_file.read()) + + base_name = os.path.basename(self.save_path).split(".")[0] + output_name = f"{base_name}_fixed.osm" + self.download_path = self.get_save_path(output_name) + + if st.button("Fix the file", icon="▶️"): + try: + result, number_of_errors = fix_osm_file(self.save_path, self.download_path) + except Exception as e: + st.error( + f"The file is completely broken it's even impossible to read it. Error: {e}" + ) + return + + st.success(f"Fixed the file with {number_of_errors} errors.") + if result: + st.success("The file was read successfully.") + else: + st.error("Even after fixing, the file could not be read.") + + st.session_state.fixed_osm = True + + if st.session_state.fixed_osm: + with open(self.download_path, "rb") as f: + st.download_button( + label="Download", + data=f, + file_name=f"{self.download_path.split('/')[-1]}", + mime="application/zip", + icon="📥", + ) + + st.session_state.fixed_osm = False + + def get_save_path(self, file_name: str) -> str: + """Get the path to save the file in the input directory. + + Arguments: + file_name {str} -- The name of the file. + + Returns: + str -- The path to save the file in the input directory. + """ + return os.path.join(INPUT_DIRECTORY, file_name) diff --git a/webui/tools/section.py b/webui/tools/section.py index 1728840..82c77ae 100644 --- a/webui/tools/section.py +++ b/webui/tools/section.py @@ -1,6 +1,7 @@ from typing import Type from tools.background import ConvertImageToObj +from tools.custom_osm import FixCustomOsmFile from tools.dem import GeoTIFFWindowingTool from tools.textures import TextureSchemaEditorTool from tools.tool import Tool @@ -31,7 +32,7 @@ class Shemas(Section): class TexturesAndDEM(Section): title = "🖼️ Textures and DEM" description = "Tools to work with textures and digital elevation models." - tools = [GeoTIFFWindowingTool] + tools = [FixCustomOsmFile, GeoTIFFWindowingTool] class Background(Section):