Fix OSM multipolygon buildings
This commit is contained in:
@@ -7,11 +7,62 @@ import math
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _element_id(element):
|
||||
return element.attrib.get("id", "")
|
||||
|
||||
|
||||
def tags(element):
|
||||
return {t.attrib.get("k", ""): t.attrib.get("v", "")
|
||||
for t in element.findall("tag")}
|
||||
|
||||
|
||||
def _coords_from_refs(refs, nodes):
|
||||
return [nodes[r] for r in refs if r in nodes]
|
||||
|
||||
|
||||
def _open_ring(coords):
|
||||
if len(coords) > 1 and coords[0] == coords[-1]:
|
||||
return coords[:-1]
|
||||
return coords
|
||||
|
||||
|
||||
def _is_closed_ring(coords):
|
||||
return len(coords) >= 4 and coords[0] == coords[-1] and len(_open_ring(coords)) >= 3
|
||||
|
||||
|
||||
def _join_member_rings(members):
|
||||
"""Build closed rings from relation member coordinate runs.
|
||||
|
||||
Handles the common OSM multipolygon cases: members are either already
|
||||
closed ways, or open way fragments whose endpoints can be stitched together.
|
||||
Malformed leftovers are dropped rather than aborting the whole import.
|
||||
"""
|
||||
rings = []
|
||||
pending = [list(member) for member in members if len(member) >= 2]
|
||||
while pending:
|
||||
ring = pending.pop(0)
|
||||
changed = True
|
||||
while not _is_closed_ring(ring) and changed:
|
||||
changed = False
|
||||
for index, candidate in enumerate(pending):
|
||||
if ring[-1] == candidate[0]:
|
||||
ring.extend(candidate[1:])
|
||||
elif ring[-1] == candidate[-1]:
|
||||
ring.extend(reversed(candidate[:-1]))
|
||||
elif ring[0] == candidate[-1]:
|
||||
ring = candidate[:-1] + ring
|
||||
elif ring[0] == candidate[0]:
|
||||
ring = list(reversed(candidate[1:])) + ring
|
||||
else:
|
||||
continue
|
||||
pending.pop(index)
|
||||
changed = True
|
||||
break
|
||||
if _is_closed_ring(ring):
|
||||
rings.append(ring)
|
||||
return rings
|
||||
|
||||
|
||||
def parse_osm(path):
|
||||
root = ET.parse(path).getroot()
|
||||
bounds_node = root.find("bounds")
|
||||
@@ -37,6 +88,7 @@ def parse_osm(path):
|
||||
continue
|
||||
|
||||
ways = []
|
||||
way_coords_by_id = {}
|
||||
for way in root.findall("way"):
|
||||
if way.attrib.get("action") == "delete":
|
||||
continue
|
||||
@@ -46,17 +98,53 @@ def parse_osm(path):
|
||||
refs.append(int(ref.attrib["ref"]))
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
coords = [nodes[r] for r in refs if r in nodes]
|
||||
coords = _coords_from_refs(refs, nodes)
|
||||
way_id = _element_id(way)
|
||||
if len(coords) >= 2:
|
||||
ways.append({"id": way.attrib.get("id", ""),
|
||||
"coords": coords, "tags": tags(way)})
|
||||
way_coords_by_id[way_id] = coords
|
||||
way_tags = tags(way)
|
||||
if len(coords) >= 2:
|
||||
ways.append({"id": way_id, "coords": coords, "tags": way_tags})
|
||||
for relation in root.findall("relation"):
|
||||
if relation.attrib.get("action") == "delete":
|
||||
continue
|
||||
relation_tags = tags(relation)
|
||||
if relation_tags.get("type") != "multipolygon" or "building" not in relation_tags:
|
||||
continue
|
||||
outer_members = []
|
||||
inner_members = []
|
||||
for member in relation.findall("member"):
|
||||
if member.attrib.get("type") != "way":
|
||||
continue
|
||||
coords = way_coords_by_id.get(member.attrib.get("ref", ""))
|
||||
if not coords:
|
||||
continue
|
||||
role = member.attrib.get("role", "")
|
||||
if role == "inner":
|
||||
inner_members.append(coords)
|
||||
elif role in ("", "outer"):
|
||||
outer_members.append(coords)
|
||||
outer_rings = _join_member_rings(outer_members)
|
||||
if not outer_rings:
|
||||
continue
|
||||
inner_rings = _join_member_rings(inner_members)
|
||||
relation_id = _element_id(relation)
|
||||
for index, outer in enumerate(outer_rings):
|
||||
synthetic_id = relation_id if len(outer_rings) == 1 else f"{relation_id}:{index + 1}"
|
||||
ways.append({
|
||||
"id": synthetic_id,
|
||||
"coords": outer,
|
||||
"inner_coords": inner_rings,
|
||||
"tags": relation_tags,
|
||||
"source": "relation",
|
||||
})
|
||||
return bounds, ways, point_features
|
||||
|
||||
|
||||
def parse_height(feature_tags, default):
|
||||
try:
|
||||
return max(0.5, float(feature_tags.get("height", default)))
|
||||
except ValueError:
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user