From cf57bb5e552f50a9e3283b66dc7d7355e87aece9 Mon Sep 17 00:00:00 2001 From: Stan Soldatov Date: Sun, 12 Jan 2025 11:57:21 +0100 Subject: [PATCH] New tool: Fix custom OSM file. --- .vscode/launch.json | 2 +- maps4fs/toolbox/custom_osm.py | 67 ++++++++++++++++++++++++++++++ webui/tools/custom_osm.py | 76 +++++++++++++++++++++++++++++++++++ webui/tools/section.py | 3 +- 4 files changed, 146 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/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):