From 2c5decb171c222c064a84bf898e884ce3cda35de Mon Sep 17 00:00:00 2001 From: sujata-m Date: Mon, 8 Jun 2026 21:05:31 +0530 Subject: [PATCH 1/3] Fix OSW to OSM duplicate node ID geometry corruption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Dev Board Ticket - https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/3726 ## Changes - Fixed OSW→OSM ID remapping so ways preserve references to the exact `ogr2osm` node objects instead of resolving by duplicate temporary numeric IDs. - Prevented generated point-node IDs from being overwritten by OSW `_id` values, avoiding temporary node ID collisions before output. - Added BUG-3726 regression fixtures with real San Diego coordinates. - Added regression coverage to verify converted OSM ways keep local geometry and do not create cross-map spider lines. - Bumped package version to `0.3.6` and updated the changelog. ## Testing - `python -m unittest tests.unit_tests.test_osw2osm.test_osw2osm` - `python -m unittest discover tests/unit_tests` --- CHANGELOG.md | 4 + .../serializer/osm/osm_normalizer.py | 52 ++++++++++--- src/osm_osw_reformatter/version.py | 2 +- .../test_files/bug_3726/edges.geojson | 78 +++++++++++++++++++ .../test_files/bug_3726/nodes.geojson | 31 ++++++++ tests/unit_tests/test_osw2osm/test_osw2osm.py | 39 ++++++++++ .../test_serializer/test_osm_normalizer.py | 11 ++- .../test_osm_osm_normalizer.py | 13 ++++ 8 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 tests/unit_tests/test_files/bug_3726/edges.geojson create mode 100644 tests/unit_tests/test_files/bug_3726/nodes.geojson diff --git a/CHANGELOG.md b/CHANGELOG.md index f781e04..305ac91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change log +### 0.3.6 +- [BUG-3726](https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/3726/) - Fix OSW→OSM conversion so duplicate source/generated node IDs do not corrupt way geometry. Way and relation references now preserve the exact `ogr2osm` node/way/relation objects during sequential ID remapping, preventing QGIS spider-line artifacts from ways snapping to unrelated duplicate node IDs. +- Add regression coverage for overlapping OSW node IDs and generated IDs, asserting converted OSM ways keep short local segments instead of cross-map jumps. + ### 0.3.5 - [BUG-3665](https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/3665) - Fix OSM→OSW export so zone boundary nodes are retained in `nodes.geojson` and zone `_w_id` references resolve to remapped sequential `_id`s, restoring OSW validation compliance for pedestrian-area geometries. - Add regression coverage that converts a `highway=pedestrian` plaza fixture and asserts `python-osw-validation` reports zero issues. diff --git a/src/osm_osw_reformatter/serializer/osm/osm_normalizer.py b/src/osm_osw_reformatter/serializer/osm/osm_normalizer.py index cb39aef..c3850a6 100644 --- a/src/osm_osw_reformatter/serializer/osm/osm_normalizer.py +++ b/src/osm_osw_reformatter/serializer/osm/osm_normalizer.py @@ -177,7 +177,13 @@ def _as_int(val): elif '_id' in osmgeometry.tags and osmgeometry.tags['_id'][0]: osm_id = _as_int(osmgeometry.tags['_id'][0]) - if osm_id is not None: + is_generated_node = ( + hasattr(osmgeometry, "x") + and hasattr(osmgeometry, "y") + and not hasattr(osmgeometry, "nodes") + and not hasattr(osmgeometry, "members") + ) + if osm_id is not None and not is_generated_node: osmgeometry.id = osm_id elevation = self._extract_elevation(ogrgeometry) if elevation is not None: @@ -231,8 +237,11 @@ def process_output(self, osmnodes, osmways, osmrelations): references accordingly. """ # Capture original IDs for mapping + node_identity_map = {} node_id_map = {} + way_identity_map = {} way_id_map = {} + rel_identity_map = {} rel_id_map = {} next_node_id = 1 next_way_id = 1 @@ -279,7 +288,8 @@ def _member_type(member): continue new_id = next_node_id next_node_id += 1 - # Keep first mapping for refs that still point to source IDs. + node_identity_map[id(node)] = new_id + # Keep first mapping only as a fallback for raw integer refs. node_id_map.setdefault(old_id, new_id) node.id = new_id _set_id_tag(node, new_id) @@ -290,6 +300,7 @@ def _member_type(member): if old_id is not None: new_id = next_way_id next_way_id += 1 + way_identity_map[id(way)] = new_id way_id_map.setdefault(old_id, new_id) way.id = new_id _set_id_tag(way, new_id) @@ -305,11 +316,16 @@ def _member_type(member): node_id_map[ref] = new_id new_refs.append(node_id_map.get(ref, ref)) elif hasattr(ref, "id"): - if ref.id not in node_id_map: + ref_identity = id(ref) + if ref_identity in node_identity_map: + ref.id = node_identity_map[ref_identity] + elif ref.id not in node_id_map: new_id = next_node_id next_node_id += 1 node_id_map[ref.id] = new_id - ref.id = node_id_map.get(ref.id, ref.id) + ref.id = new_id + else: + ref.id = node_id_map.get(ref.id, ref.id) new_refs.append(ref) else: new_refs.append(ref) @@ -329,6 +345,7 @@ def _member_type(member): if old_id is not None: new_id = next_rel_id next_rel_id += 1 + rel_identity_map[id(rel)] = new_id rel_id_map.setdefault(old_id, new_id) rel.id = new_id _set_id_tag(rel, new_id) @@ -365,24 +382,39 @@ def _member_type(member): rel_id_map[ref] = new_id member.ref = node_id_map.get(ref, way_id_map.get(ref, rel_id_map.get(ref, ref))) elif hasattr(ref, "id"): + ref_identity = id(ref) if m_type == "node": - if ref.id not in node_id_map: + if ref_identity in node_identity_map: + ref.id = node_identity_map[ref_identity] + elif ref.id not in node_id_map: new_id = next_node_id next_node_id += 1 node_id_map[ref.id] = new_id - ref.id = node_id_map.get(ref.id, ref.id) + ref.id = new_id + else: + ref.id = node_id_map.get(ref.id, ref.id) elif m_type == "way": - if ref.id not in way_id_map: + ref_identity = id(ref) + if ref_identity in way_identity_map: + ref.id = way_identity_map[ref_identity] + elif ref.id not in way_id_map: new_id = next_way_id next_way_id += 1 way_id_map[ref.id] = new_id - ref.id = way_id_map.get(ref.id, ref.id) + ref.id = new_id + else: + ref.id = way_id_map.get(ref.id, ref.id) elif m_type == "relation": - if ref.id not in rel_id_map: + ref_identity = id(ref) + if ref_identity in rel_identity_map: + ref.id = rel_identity_map[ref_identity] + elif ref.id not in rel_id_map: new_id = next_rel_id next_rel_id += 1 rel_id_map[ref.id] = new_id - ref.id = rel_id_map.get(ref.id, ref.id) + ref.id = new_id + else: + ref.id = rel_id_map.get(ref.id, ref.id) else: if ref.id not in node_id_map and ref.id not in way_id_map and ref.id not in rel_id_map: new_id = next_rel_id diff --git a/src/osm_osw_reformatter/version.py b/src/osm_osw_reformatter/version.py index 40ed83d..4596d03 100644 --- a/src/osm_osw_reformatter/version.py +++ b/src/osm_osw_reformatter/version.py @@ -1 +1 @@ -__version__ = '0.3.5' +__version__ = '0.3.6' diff --git a/tests/unit_tests/test_files/bug_3726/edges.geojson b/tests/unit_tests/test_files/bug_3726/edges.geojson new file mode 100644 index 0000000..5ef8989 --- /dev/null +++ b/tests/unit_tests/test_files/bug_3726/edges.geojson @@ -0,0 +1,78 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -117.1589921, + 32.7043326 + ], + [ + -117.1590819, + 32.7043175 + ], + [ + -117.1591807, + 32.7042797 + ], + [ + -117.1592436, + 32.7042419 + ], + [ + -117.1593784, + 32.7042041 + ], + [ + -117.1595581, + 32.7041663 + ], + [ + -117.1596749, + 32.7041512 + ], + [ + -117.1597826, + 32.7041512 + ], + [ + -117.1599443, + 32.7041587 + ], + [ + -117.1601958, + 32.7041814 + ], + [ + -117.160636, + 32.7044762 + ], + [ + -117.1607708, + 32.7045821 + ], + [ + -117.1608786, + 32.7046501 + ], + [ + -117.1609504, + 32.7047181 + ] + ] + }, + "properties": { + "_id": "359", + "_u_id": "612", + "_v_id": "613", + "highway": "footway", + "footway": "sidewalk", + "surface": "concrete", + "surface:score": 0.94 + } + } + ] +} diff --git a/tests/unit_tests/test_files/bug_3726/nodes.geojson b/tests/unit_tests/test_files/bug_3726/nodes.geojson new file mode 100644 index 0000000..cb13f0d --- /dev/null +++ b/tests/unit_tests/test_files/bug_3726/nodes.geojson @@ -0,0 +1,31 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -117.1589921, + 32.7043326 + ] + }, + "properties": { + "_id": "612" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -117.1609504, + 32.7047181 + ] + }, + "properties": { + "_id": "613" + } + } + ] +} diff --git a/tests/unit_tests/test_osw2osm/test_osw2osm.py b/tests/unit_tests/test_osw2osm/test_osw2osm.py index bb05a8b..f643bd5 100644 --- a/tests/unit_tests/test_osw2osm/test_osw2osm.py +++ b/tests/unit_tests/test_osw2osm/test_osw2osm.py @@ -1,4 +1,5 @@ import json +import math import os from pathlib import Path import tempfile @@ -14,6 +15,8 @@ TEST_DATA_WITH_INCLINE_ZIP_FILE = os.path.join(ROOT_DIR, 'test_files/dataset_with_incline.zip') TEST_EDGES_WITH_INVALID_INCLINE_FILE = os.path.join(ROOT_DIR, 'test_files/edges_invalid_incline.geojson') TEST_NODES_WITH_INVALID_INCLINE_FILE = os.path.join(ROOT_DIR, 'test_files/nodes_invalid_incline.geojson') +TEST_BUG_3726_NODES_FILE = os.path.join(ROOT_DIR, 'test_files/bug_3726/nodes.geojson') +TEST_BUG_3726_EDGES_FILE = os.path.join(ROOT_DIR, 'test_files/bug_3726/edges.geojson') def _create_invalid_incline_zip(zip_path: str) -> str: @@ -91,6 +94,15 @@ def _create_3d_node_zip(zip_path: str, z_value: float) -> str: class TestOSW2OSM(unittest.IsolatedAsyncioTestCase): + @staticmethod + def _distance_meters(first, second): + lon1, lat1 = first + lon2, lat2 = second + return math.hypot( + (lon1 - lon2) * 111320 * math.cos(math.radians((lat1 + lat2) / 2)), + (lat1 - lat2) * 110540, + ) + def test_convert_successful(self): zip_file = TEST_ZIP_FILE osw2osm = OSW2OSM(zip_file_path=zip_file, workdir=OUTPUT_DIR, prefix='test') @@ -366,6 +378,33 @@ def test_remap_ids_rewrites_refs(self): rel_tag_ids = [tag.get("v") for tag in root.findall(".//relation/tag[@k='_id']")] self.assertEqual(rel_tag_ids, ["1"]) + def test_convert_preserves_way_geometry_when_osw_node_ids_overlap_generated_ids(self): + with tempfile.TemporaryDirectory() as tmpdir: + zip_path = Path(tmpdir, "overlapping_ids.zip") + with zipfile.ZipFile(zip_path, "w") as zf: + zf.write(TEST_BUG_3726_NODES_FILE, arcname="nodes.geojson") + zf.write(TEST_BUG_3726_EDGES_FILE, arcname="edges.geojson") + + result = OSW2OSM(zip_file_path=str(zip_path), workdir=tmpdir, prefix="overlapping").convert() + self.assertTrue(result.status, msg=getattr(result, "error", "Conversion failed")) + + root = ET.parse(result.generated_files).getroot() + nodes = { + node.get("id"): (float(node.get("lon")), float(node.get("lat"))) + for node in root.findall(".//node") + } + node_ids = [node.get("id") for node in root.findall(".//node")] + self.assertEqual(len(node_ids), len(set(node_ids))) + + for way in root.findall(".//way"): + refs = [nd.get("ref") for nd in way.findall("nd")] + distances = [ + self._distance_meters(nodes[start_ref], nodes[end_ref]) + for start_ref, end_ref in zip(refs, refs[1:]) + ] + self.assertTrue(distances) + self.assertLess(max(distances), 200) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit_tests/test_serializer/test_osm_normalizer.py b/tests/unit_tests/test_serializer/test_osm_normalizer.py index 24646f3..84d58e5 100644 --- a/tests/unit_tests/test_serializer/test_osm_normalizer.py +++ b/tests/unit_tests/test_serializer/test_osm_normalizer.py @@ -133,6 +133,15 @@ def test_falls_back_to_internal_id_tag(self): self.assertEqual(geometry.id, 67890) + def test_does_not_override_generated_node_id_from_internal_id_tag(self): + geometry = self.DummyGeometry({"_id": ["67890"]}, osm_id=12) + geometry.x = 1.0 + geometry.y = 2.0 + + self.normalizer.process_feature_post(geometry, ogrfeature=None, ogrgeometry=None) + + self.assertEqual(geometry.id, 12) + def test_keeps_existing_id_when_no_tags_present(self): geometry = self.DummyGeometry({}, osm_id=555) @@ -149,4 +158,4 @@ def test_ignores_empty_ext_osm_id_values(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/unit_tests/test_serializer/test_osm_osm_normalizer.py b/tests/unit_tests/test_serializer/test_osm_osm_normalizer.py index 0cc3a05..29976a1 100644 --- a/tests/unit_tests/test_serializer/test_osm_osm_normalizer.py +++ b/tests/unit_tests/test_serializer/test_osm_osm_normalizer.py @@ -228,6 +228,19 @@ def test_process_output_assigns_unique_ids_when_source_ids_repeat(self): for idx, way in enumerate(sorted(ways, key=lambda w: w.id), start=1): self.assertEqual(way.tags.get('_id'), [str(idx)]) + def test_process_output_preserves_object_refs_when_source_node_ids_repeat(self): + first_node = DummyOsmGeometry(tags={'_id': ['10']}, osm_id=10) + second_node = DummyOsmGeometry(tags={'_id': ['10']}, osm_id=10) + way = DummyOsmGeometry(tags={'_id': ['20']}, osm_id=20) + way.nodes = [second_node] + + self.normalizer.process_output([first_node, second_node], [way], []) + + self.assertEqual(first_node.id, 1) + self.assertEqual(second_node.id, 2) + self.assertIs(way.nodes[0], second_node) + self.assertEqual(way.nodes[0].id, 2) + def test_process_output_relation_member_type_uses_correct_id_map(self): node = DummyOsmGeometry(tags={'_id': ['7']}, osm_id=7) way = DummyOsmGeometry(tags={'_id': ['7']}, osm_id=7) From bf416b98bc716a9409608fdca7a99ca67bbc1a75 Mon Sep 17 00:00:00 2001 From: sujata-m Date: Mon, 8 Jun 2026 21:16:19 +0530 Subject: [PATCH 2/3] Fixed files --- .../test_files/bug_3726/nodes.geojson | 13 ++ .../test_files/bug_3726/qgis_preview.geojson | 118 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 tests/unit_tests/test_files/bug_3726/qgis_preview.geojson diff --git a/tests/unit_tests/test_files/bug_3726/nodes.geojson b/tests/unit_tests/test_files/bug_3726/nodes.geojson index cb13f0d..500433e 100644 --- a/tests/unit_tests/test_files/bug_3726/nodes.geojson +++ b/tests/unit_tests/test_files/bug_3726/nodes.geojson @@ -26,6 +26,19 @@ "properties": { "_id": "613" } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -117.1631527, + 32.7320171 + ] + }, + "properties": { + "_id": "1" + } } ] } diff --git a/tests/unit_tests/test_files/bug_3726/qgis_preview.geojson b/tests/unit_tests/test_files/bug_3726/qgis_preview.geojson new file mode 100644 index 0000000..2ed1eb7 --- /dev/null +++ b/tests/unit_tests/test_files/bug_3726/qgis_preview.geojson @@ -0,0 +1,118 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -117.1589921, + 32.7043326 + ], + [ + -117.1590819, + 32.7043175 + ], + [ + -117.1591807, + 32.7042797 + ], + [ + -117.1592436, + 32.7042419 + ], + [ + -117.1593784, + 32.7042041 + ], + [ + -117.1595581, + 32.7041663 + ], + [ + -117.1596749, + 32.7041512 + ], + [ + -117.1597826, + 32.7041512 + ], + [ + -117.1599443, + 32.7041587 + ], + [ + -117.1601958, + 32.7041814 + ], + [ + -117.160636, + 32.7044762 + ], + [ + -117.1607708, + 32.7045821 + ], + [ + -117.1608786, + 32.7046501 + ], + [ + -117.1609504, + 32.7047181 + ] + ] + }, + "properties": { + "_id": "359", + "feature_type": "edge", + "highway": "footway", + "footway": "sidewalk", + "surface": "concrete" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -117.1631527, + 32.7320171 + ] + }, + "properties": { + "_id": "1", + "feature_type": "collision_node" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -117.1589921, + 32.7043326 + ] + }, + "properties": { + "_id": "612", + "feature_type": "edge_endpoint" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -117.1609504, + 32.7047181 + ] + }, + "properties": { + "_id": "613", + "feature_type": "edge_endpoint" + } + } + ] +} From 21b75e1ddf7774a39f228bdb738a12749df4a404 Mon Sep 17 00:00:00 2001 From: sujata-m Date: Wed, 10 Jun 2026 13:48:30 +0530 Subject: [PATCH 3/3] - PyPI version: osm-osw-reformatter - Unit Tests workflow - Coverage badge from the repo badges branch --- .github/workflows/unit_tests.yml | 38 ++++-- CHANGELOG.md | 1 + README.md | 4 +- .../test_files/bug_3726/qgis_preview.geojson | 118 ------------------ 4 files changed, 34 insertions(+), 127 deletions(-) delete mode 100644 tests/unit_tests/test_files/bug_3726/qgis_preview.geojson diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 9e4cb27..c8e79a5 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -60,19 +60,41 @@ jobs: python -m coverage run --source=src/osm_osw_reformatter -m unittest discover -v tests/unit_tests >> $log_file 2>&1 echo -e "\nCoverage Report\n" >> $log_file coverage report >> $log_file - coverage xml - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml - fail_ci_if_error: false - name: Check coverage run: | coverage report --fail-under=85 + - name: Generate coverage badge + run: | + pip install coverage-badge + mkdir -p badge-out + coverage-badge -f -o badge-out/coverage.svg + + - name: Publish coverage badge to badges branch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -e + cp badge-out/coverage.svg /tmp/coverage.svg + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git fetch origin badges || true + if git show-ref --verify --quiet refs/remotes/origin/badges; then + git checkout badges + else + git checkout --orphan badges + git rm -rf . >/dev/null 2>&1 || true + fi + cp /tmp/coverage.svg coverage.svg + git add coverage.svg + if ! git diff --cached --quiet; then + git commit -m "chore: update coverage badge" + git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:badges + else + echo "No badge changes to commit." + fi + - name: Upload report to Azure uses: LanceMcCarthy/Action-AzureBlobUpload@v2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 305ac91..63a371f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### 0.3.6 - [BUG-3726](https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/3726/) - Fix OSW→OSM conversion so duplicate source/generated node IDs do not corrupt way geometry. Way and relation references now preserve the exact `ogr2osm` node/way/relation objects during sequential ID remapping, preventing QGIS spider-line artifacts from ways snapping to unrelated duplicate node IDs. - Add regression coverage for overlapping OSW node IDs and generated IDs, asserting converted OSM ways keep short local segments instead of cross-map jumps. +- Update README badges for PyPI package version, unit test workflow status, and coverage with short cache timing. ### 0.3.5 - [BUG-3665](https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/3665) - Fix OSM→OSW export so zone boundary nodes are retained in `nodes.geojson` and zone `_w_id` references resolve to remapped sequential `_id`s, restoring OSW validation compliance for pedestrian-area geometries. diff --git a/README.md b/README.md index bd62adf..7fc6250 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ # TDEI python lib formatter library -[![codecov](https://codecov.io/gh/TaskarCenterAtUW/TDEI-python-lib-osw-formatter/branch/main/graph/badge.svg)](https://codecov.io/gh/TaskarCenterAtUW/TDEI-python-lib-osw-formatter) +[![osm-osw-reformatter](https://img.shields.io/pypi/v/osm-osw-reformatter?label=osm-osw-reformatter&cacheSeconds=60&t=1)](https://pypi.org/project/osm-osw-reformatter/) +[![Unit Tests](https://github.com/TaskarCenterAtUW/TDEI-python-lib-osw-formatter/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/TaskarCenterAtUW/TDEI-python-lib-osw-formatter/actions/workflows/unit_tests.yml) +![Coverage](https://raw.githubusercontent.com/TaskarCenterAtUW/TDEI-python-lib-osw-formatter/badges/coverage.svg?cacheSeconds=60&t=1) This python package designed to convert geospatial data from one format to another. In this case, it converts data from OpenStreetMap (OSM) format to OpenSideWalks (OSW) format and OpenSideWalks (OSW) format to OpenStreetMap (OSM) format. Let's break down the key components and processes involved in this converter: diff --git a/tests/unit_tests/test_files/bug_3726/qgis_preview.geojson b/tests/unit_tests/test_files/bug_3726/qgis_preview.geojson deleted file mode 100644 index 2ed1eb7..0000000 --- a/tests/unit_tests/test_files/bug_3726/qgis_preview.geojson +++ /dev/null @@ -1,118 +0,0 @@ -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [ - -117.1589921, - 32.7043326 - ], - [ - -117.1590819, - 32.7043175 - ], - [ - -117.1591807, - 32.7042797 - ], - [ - -117.1592436, - 32.7042419 - ], - [ - -117.1593784, - 32.7042041 - ], - [ - -117.1595581, - 32.7041663 - ], - [ - -117.1596749, - 32.7041512 - ], - [ - -117.1597826, - 32.7041512 - ], - [ - -117.1599443, - 32.7041587 - ], - [ - -117.1601958, - 32.7041814 - ], - [ - -117.160636, - 32.7044762 - ], - [ - -117.1607708, - 32.7045821 - ], - [ - -117.1608786, - 32.7046501 - ], - [ - -117.1609504, - 32.7047181 - ] - ] - }, - "properties": { - "_id": "359", - "feature_type": "edge", - "highway": "footway", - "footway": "sidewalk", - "surface": "concrete" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -117.1631527, - 32.7320171 - ] - }, - "properties": { - "_id": "1", - "feature_type": "collision_node" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -117.1589921, - 32.7043326 - ] - }, - "properties": { - "_id": "612", - "feature_type": "edge_endpoint" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -117.1609504, - 32.7047181 - ] - }, - "properties": { - "_id": "613", - "feature_type": "edge_endpoint" - } - } - ] -}