From 8e3b76d758425446447e573dfbc0014ce94c9aa3 Mon Sep 17 00:00:00 2001 From: wisamuw Date: Mon, 13 Apr 2026 02:20:03 -0700 Subject: [PATCH] =?UTF-8?q?Fix=20OSM=E2=86=92OSW=20conversion=20when=20OSM?= =?UTF-8?q?=20Way=20contains=20consecutive=20duplicate=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/3286 **PR Title** Fix OSM→OSW conversion when OSM Way contains consecutive duplicate nodes **PR Summary** - Fix OSM→OSW conversion when OSM Way contains consecutive duplicate nodes (bug 3286). Instead of generating an invalid 0 length geometry, the segment is ignored. - Tested on file attached to dev board bug: osm.b8106236-b919-4e50-b956-99399635c0bb.xml - Added a new unit test --- CHANGELOG.md | 3 +++ .../serializer/osm/osm_graph.py | 10 ++++++++- src/osm_osw_reformatter/version.py | 2 +- tests/unit_tests/test_files/bug_3286.xml | 10 +++++++++ tests/unit_tests/test_osm2osw/test_osm2osw.py | 21 +++++++++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/test_files/bug_3286.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d4d417..adacb26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change log +### 0.3.4 +- Fix OSM→OSW conversion when OSM Way contains consecutive duplicate nodes (bug 3286). Instead of generating an invalid 0 length geometry, the segment is ignored. + ### 0.3.3 - Fix OSM→OSW export classification so canonical OSM tags are used for semantic recognition and `ext:*` tags are preserved as extensions instead of being treated as feature-defining tags. - Fix closed ext-only ways such as `ext:demolished:building=yes` to emit polygon output without falling through to point geometry construction. diff --git a/src/osm_osw_reformatter/serializer/osm/osm_graph.py b/src/osm_osw_reformatter/serializer/osm/osm_graph.py index 57617f2..ea5b965 100644 --- a/src/osm_osw_reformatter/serializer/osm/osm_graph.py +++ b/src/osm_osw_reformatter/serializer/osm/osm_graph.py @@ -34,6 +34,7 @@ def way(self, w) -> None: d2 = {**d, **OSWWayNormalizer(tags).normalize()} + segment_n = 0 for i in range(len(w.nodes) - 1): u = w.nodes[i] v = w.nodes[i + 1] @@ -49,12 +50,19 @@ def way(self, w) -> None: v_lon = float(v.lon) v_lat = float(v.lat) + # Skip consecutive duplicate nodes. They create zero-length segments. + if u_ref == v_ref: + del u + del v + continue + d3 = {**d2} - d3['segment'] = i + d3['segment'] = segment_n d3['ndref'] = [u_ref, v_ref] self.G.add_edges_from([(u_ref, v_ref, d3)]) self.G.add_node(u_ref, lon=u_lon, lat=u_lat) self.G.add_node(v_ref, lon=v_lon, lat=v_lat) + segment_n += 1 del u del v diff --git a/src/osm_osw_reformatter/version.py b/src/osm_osw_reformatter/version.py index 80eb7f9..bfeb9e7 100644 --- a/src/osm_osw_reformatter/version.py +++ b/src/osm_osw_reformatter/version.py @@ -1 +1 @@ -__version__ = '0.3.3' +__version__ = '0.3.4' diff --git a/tests/unit_tests/test_files/bug_3286.xml b/tests/unit_tests/test_files/bug_3286.xml new file mode 100644 index 0000000..2bcbff9 --- /dev/null +++ b/tests/unit_tests/test_files/bug_3286.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/tests/unit_tests/test_osm2osw/test_osm2osw.py b/tests/unit_tests/test_osm2osw/test_osm2osw.py index b22506d..3fb4d6c 100644 --- a/tests/unit_tests/test_osm2osw/test_osm2osw.py +++ b/tests/unit_tests/test_osm2osw/test_osm2osw.py @@ -15,6 +15,7 @@ TEST_INVALID_NODE_TAGS_FILE = os.path.join(ROOT_DIR, 'test_files/node_with_invalid_tags.xml') TEST_TREE_FILE = os.path.join(ROOT_DIR, 'test_files/tree-test.xml') TEST_BUG_3477_FILE = os.path.join(ROOT_DIR, 'test_files/bug_3477.xml') +TEST_BUG_3286_FILE = os.path.join(ROOT_DIR, 'test_files/bug_3286.xml') def is_valid_float(value): @@ -350,6 +351,26 @@ async def run_test(): asyncio.run(run_test()) + def test_bug_3286_consecutive_duplicate_nodes(self): + osm_file_path = TEST_BUG_3286_FILE + + async def run_test(): + osm2osw = OSM2OSW(osm_file=osm_file_path, workdir=OUTPUT_DIR, prefix='test') + result = await osm2osw.convert() + self.assertTrue(result.status) + self.assertEqual(len(result.generated_files), 2) + + for file_path in result.generated_files: + if file_path.endswith('edges.geojson'): + with open(file_path) as f: + geojson = json.load(f) + self.assertEqual(len(geojson.get("features", [])), 1) + + for file_path in result.generated_files: + os.remove(file_path) + + asyncio.run(run_test()) + if __name__ == '__main__': unittest.main()