pyosmium基于OSM关系构建GeoJSON徒步路线MultiLineString方案
报错原因说明
你遇到的RuntimeError: need at least two points for linestring属于OSM数据的正常情况:部分挂载到路径关系的way是编辑残留的无效数据,仅包含1个节点,本身无法构成线段,处理时直接过滤跳过即可,不会影响最终路径的完整性。
完整实现步骤
OSM的PBF文件不会按关系、way、节点的依赖顺序存储要素,因此标准处理流程是两次遍历PBF文件,不要尝试单次遍历同时收集关系和几何,会出现引用的way/节点还没加载的问题。
1. 第一次遍历:收集目标关系与关联Way ID
这一步和你已写的逻辑基本一致,只需要注意不要用dict、list这类Python内置关键字做变量名,避免污染内置命名空间:
import osmium import shapely.wkb as wkblib import shapely import json from collections import defaultdict wkbfab = osmium.geom.WKBFactory() class RelationCollector(osmium.SimpleHandler): def __init__(self): super().__init__() self.target_rels = {} # key: 关系ID,value: 关联的way ID集合 self.rel_tags = {} # 存储关系的原始标签,后续写入GeoJSON属性 def relation(self, r): # 筛选带sac_scale=hiking标签的徒步路径 if r.tags.get('sac_scale') != 'hiking': return # 测试阶段仅处理示例ID 104369,全量处理时删掉这行判断即可 if r.id != 104369: return way_ids = set() for mem in r.members: # 仅收集way类型成员,忽略节点、嵌套子关系 if mem.type == 'w': way_ids.add(mem.ref) self.target_rels[r.id] = way_ids self.rel_tags[r.id] = {k:v for k,v in r.tags} # 执行第一次遍历 rel_collector = RelationCollector() rel_collector.apply_file('../pbf/new-zealand.osm.pbf') # 汇总所有需要收集几何的way ID all_target_way_ids = set() for way_set in rel_collector.target_rels.values(): all_target_way_ids.update(way_set)
2. 第二次遍历:收集Way几何并过滤无效数据
这一步遍历所有way,匹配到目标ID时尝试生成LineString几何,自动过滤节点数不足2的无效线段,同时注意调用apply_file时必须加locations=True参数,pyosmium才会自动缓存way引用的节点坐标,否则无法生成几何:
class WayGeomCollector(osmium.SimpleHandler): def __init__(self, target_way_ids, rel_way_map): super().__init__() self.target_way_ids = target_way_ids self.rel_way_map = rel_way_map self.rel_geoms = defaultdict(list) # key:关系ID,value:对应LineString几何列表 def way(self, w): if w.id not in self.target_way_ids: return # 过滤节点数不足2的无效way if len(w.nodes) < 2: return try: wkb = wkbfab.create_linestring(w) line_geom = wkblib.loads(wkb, hex=True) # 将当前几何关联到所属的关系 for rel_id, way_set in self.rel_way_map.items(): if w.id in way_set: self.rel_geoms[rel_id].append(line_geom) except Exception as e: # 捕获节点缺失、几何无效等其他异常,避免程序崩溃 print(f"处理way {w.id} 时跳过,原因:{str(e)}") return # 执行第二次遍历,必须加locations=True参数缓存节点坐标 way_collector = WayGeomCollector(all_target_way_ids, rel_collector.target_rels) way_collector.apply_file('../pbf/new-zealand.osm.pbf', locations=True)
3. 组装为标准GeoJSON文件
拿到每个关系对应的LineString列表后,直接合并为MultiLineString类型,组装成标准GeoJSON结构写入文件即可,输出的文件可以直接在geojson.io正常加载渲染:
geojson_output = { "type": "FeatureCollection", "features": [] } for rel_id, line_list in way_collector.rel_geoms.items(): if not line_list: continue # 合并所有线段为单个MultiLineString multiline_geom = shapely.MultiLineString(line_list) feature = { "type": "Feature", "geometry": json.loads(shapely.to_geojson(multiline_geom)), "properties": rel_collector.rel_tags[rel_id] } # 补充OSM ID到属性 feature["properties"]["osm_id"] = f"r{rel_id}" geojson_output["features"].append(feature) # 写入本地文件 with open("hiking_trail_104369.geojson", "w", encoding="utf-8") as f: json.dump(geojson_output, f, ensure_ascii=False, indent=2)
OSM官网的路径渲染逻辑说明
OSM官网展示关系路径时,比上面的基础逻辑多做了两步处理,所以能呈现连续的完整路线:
- 按照关系成员的存储顺序,结合成员的
role值(forward/backward)判断线段方向,必要时反转LineString的坐标顺序 - 按线段端点的坐标匹配相邻线段,把首尾相连的线段拼接为连续的单条LineString,同时过滤和主路径不连通的孤立碎段
如果只需要正常展示路径,直接输出MultiLineString就足够,渲染效果和官网没有肉眼差异;如果需要得到连续的单条路线,可以在拿到LineString列表后补充端点匹配、方向反转的拼接逻辑即可。
补充说明
你提到的osmium export命令行工具,内部实现逻辑和上述流程完全一致,本质也是先筛选目标要素、再过滤无效几何、最后组装输出格式,不存在特殊的黑魔法处理。
处理大区域PBF文件时,两次遍历的内存占用极低,不需要全量加载所有节点和way数据,只需要缓存目标ID对应的要素即可,性能远高于一次性加载全量数据的方案。
内容的提问来源于stack exchange,提问作者Ben On Track

