经过多天的摸索,整理基于开源工具 graphhopper 做导航的一些浅显思路,这里做一个分享,抛砖引玉,希望能够获得正在研究相关领域大佬的一些建议。
如何运行graphhopper的一个导航服务这里就不再赘述了,感兴趣的可以去GitHub上按照教程学习。
这里分享的内容包括 OSM数据 和 graphhopper 开源的导航引擎的一些具体用法和代码层次的集成。
OSM
OSM(OpenStreetMap),是一款由网络大众共同打造的免费开源、可编辑的地图服务,但是在国内使用OpenStreetMap数据似乎并不能满足上述法律要求。
理解 OSM 的数据结构和相关概念:node 、way、relation、tag。
node(节点):存储经纬度,表示位置。但不存储节点在地图上的实际大小,比如说一个景点或者山峰,或一间商店或餐厅,或是最为路径的一部分。节点可依附于路径与关系。
way(路径):有序排列的节点,以折线的形式呈现,也能循环回起始节点形成封闭路径,可以循环路径或以多边形区域的方式呈现。这类原始资料可用于呈为线型资料,例如街道、河流等,或一个多边形区域,例如农田、公园、停车场、建筑物、校园或者是湖泊、森林。路径必须有节点才能显示在地图上,可依附于关系,路径资料可以计算出长度,或多边形的面积、周长。
relation(关系):有排序的节点、路径和关系(三类原始资料在这里统称为“成员” menber),在这里每个成员选择性拥有一个“角色” (role),以决定改成员于此关系中的性质。关系用来表示各个原始资料(节点、路径和关系)的关系,例如道路的转弯限制,由不同的路径所组成的边界,一条国道、省道或者铁路路线。或者一个区域中中间孔雀区域(例如被环形建筑物包围的中庭,或水体中的小岛)的多重多边形,这时“角色”就能用来形容他们之间的关系。
tag(标签):键值对(key- value pairs, 键值都是字符串),用来存储地图上物价的元数据(物件的类型,名字和物理特性),赋予OSM资料其意义,而能表示现实世界中存在的某件事物,与有关其事物的资讯。标签无法独立存在,他们必须依附在一个已存在的物件,也就是节点,路径或关系。一个原始资料称为一个物件,每个物件的同一键,只能设定一个值。例如建筑物是building=yes,至于住宅区街道,则为highway=residential,这是OSM中最频繁被使用的标签。
OSM 数据组织如下:
<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="CGImap 0.8.6 (1769772 spike-07.openstreetmap.org)" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
<bounds minlat="31.2264000" minlon="121.4598000" maxlat="31.2378000" maxlon="121.4832000"/>
<node id="63242769" visible="true" version="8" changeset="53519844" timestamp="2017-11-05T07:07:04Z" user="SteniLee" uid="3836466" lat="31.2333299" lon="121.4624429"/>
<node id="9127191400" visible="true" version="1" changeset="111778257" timestamp="2021-09-27T16:55:46Z" user="Herman Lee" uid="6921384" lat="31.2401334" lon="121.4749206">
<tag k="bus" v="yes"/>
<tag k="name" v="福建中路北京东路"/>
<tag k="public_transport" v="stop_position"/>
</node>
<way id="51317339" visible="true" version="12" changeset="114338570" timestamp="2021-11-29T06:41:25Z" user="wheelmap_visitor" uid="290680">
<nd ref="654577835"/>
<nd ref="1490098834"/>
<nd ref="6975896994"/>
<nd ref="1490098840"/>
<nd ref="6975896995"/>
<nd ref="654577837"/>
<nd ref="654577838"/>
<nd ref="654577839"/>
<nd ref="654577840"/>
<nd ref="654577841"/>
<nd ref="6975896993"/>
<nd ref="654577835"/>
<tag k="building" v="retail"/>
<tag k="building:levels" v="9"/>
<tag k="internet_access" v="wlan"/>
<tag k="name" v="新世界城"/>
<tag k="name:en" v="New World City"/>
<tag k="name:ko" v="신세계백화점"/>
<tag k="name:zh" v="新世界城"/>
<tag k="opening_hours" v="Mo-Su 08:00-22:00"/>
<tag k="shop" v="mall"/>
<tag k="wheelchair" v="no"/>
</way>
<way id="51317372" visible="true" version="10" changeset="94502123" timestamp="2020-11-20T12:53:01Z" user="wvdp" uid="436419">
<nd ref="654578187"/>
<nd ref="654578188"/>
<nd ref="654578442"/>
<nd ref="1728696583"/>
<nd ref="654578190"/>
<nd ref="654578187"/>
<tag k="building" v="yes"/>
<tag k="building:use" v="office"/>
<tag k="height" v="54"/>
<tag k="name" v="百联世茂国际广场"/>
<tag k="name:en" v="Bailian Shimao International Plaza"/>
<tag k="name:ko" v="브릴리안스 인터내셔널 플라자"/>
</way>
<relation id="5611326" visible="true" version="34" changeset="115631320" timestamp="2022-01-01T10:41:50Z" user="Lepuse" uid="9560399">
<member type="node" ref="3800386189" role="stop"/>
<member type="node" ref="3800385632" role="stop"/>
<member type="node" ref="3800385630" role="stop"/>
<member type="node" ref="3800386173" role="stop"/>
<member type="node" ref="3800385603" role="stop"/>
<member type="node" ref="3800386181" role="stop"/>
<member type="node" ref="3800385649" role="stop"/>
<member type="node" ref="3800386198" role="stop"/>
<member type="node" ref="3800385635" role="stop"/>
<member type="node" ref="3800385637" role="stop"/>
<member type="node" ref="3800386183" role="stop"/>
<member type="node" ref="3800386159" role="stop"/>
<member type="node" ref="3800385619" role="stop"/>
<member type="node" ref="3800385651" role="stop"/>
<member type="node" ref="3800385615" role="stop"/>
<member type="node" ref="3800385607" role="stop"/>
<member type="node" ref="3800386169" role="stop"/>
<member type="node" ref="3800385609" role="stop"/>
<member type="node" ref="5308572449" role="stop"/>
<member type="node" ref="3800386195" role="stop"/>
<member type="node" ref="3800385639" role="stop"/>
<member type="node" ref="3800385622" role="stop"/>
<member type="node" ref="3800386179" role="stop"/>
<member type="node" ref="3800385653" role="stop"/>
<member type="node" ref="3800385617" role="stop"/>
<member type="node" ref="3800385613" role="stop"/>
<member type="node" ref="3800385641" role="stop"/>
<member type="node" ref="3800386193" role="stop"/>
<member type="node" ref="3800385624" role="stop"/>
<member type="node" ref="3800386161" role="stop"/>
<member type="way" ref="376634457" role=""/>
<member type="way" ref="461893586" role=""/>
<member type="way" ref="461881036" role=""/>
<member type="way" ref="376634459" role=""/>
<member type="way" ref="120944643" role=""/>
<member type="way" ref="120944645" role=""/>
<member type="way" ref="376634461" role=""/>
<member type="way" ref="108731289" role=""/>
<member type="way" ref="108731285" role=""/>
<member type="way" ref="840501845" role=""/>
<member type="way" ref="230069191" role=""/>
<tag k="colour" v="#97D700"/>
<tag k="colour:ref" v="PANTONE 375 C"/>
<tag k="duration" v="01:31"/>
<tag k="from" v="徐泾东"/>
<tag k="from:en" v="East Xujing"/>
<tag k="name" v="2号线:徐泾东→浦东国际机场"/>
<tag k="name:en" v="Line 2: East Xujing → Pudong International Airport"/>
<tag k="name:zh" v="2号线:徐泾东→浦东国际机场"/>
<tag k="network" v="上海地铁"/>
<tag k="network:en" v="Shanghai Metro"/>
<tag k="network:wikidata" v="Q462201"/>
<tag k="network:wikipedia" v="zh:上海地铁"/>
<tag k="network:zh" v="上海地铁"/>
<tag k="operator" v="上海地铁第二运营有限公司"/>
<tag k="public_transport:version" v="2"/>
<tag k="ref" v="2"/>
<tag k="route" v="subway"/>
<tag k="to" v="浦东国际机场"/>
<tag k="to:en" v="Pudong International Airport"/>
<tag k="type" v="route"/>
<tag k="wikidata" v="Q1325437"/>
</relation>
</osm>
看完上边相关介绍,大致能理解OSM是如何组织地图上的点和路线以及相关关系。
目前在OSM中存在路线最大速度等约束,比如说:
<way id="40297715" visible="true" version="19" changeset="115611104" timestamp="2021-12-31T14:19:02Z" user="Herman Lee" uid="6921384">
<nd ref="152878617"/>
<nd ref="479290062"/>
<nd ref="479288912"/>
<nd ref="479286375"/>
<nd ref="1315785561"/>
<nd ref="385113975"/>
<tag k="bicycle" v="no"/>
<tag k="highway" v="secondary"/>
<tag k="lanes" v="2"/>
<tag k="maxspeed" v="30"/>
<tag k="maxspeed:type" v="sign"/>
<tag k="name" v="淮海东路"/>
</way>
那么我们所感兴趣的自定义导航比如说限行、限高等约束是否也可以用 relation 来定义呢?
答案是肯定的。
Grasshopper
Grasshopper 是一个开源的地图导航引擎,Java 语言开发,因此可以通过导入依赖很好的集成到Java项目中。
talk is cheap,show you the code!
首先,导入grasshopper的依赖。
<dependency>
<groupId>com.graphhopper</groupId>
<artifactId>graphhopper-core</artifactId>
<version>4.0</version>
</dependency>
有了依赖,我们便可以调用它的接口。
读取OSM数据,加载到内存中,加速后续计算。
static GraphHopper createGraphHopperInstance(String ghLoc) {
GraphHopper hopper = new GraphHopper();
// OSM 文件路径
hopper.setOSMFile(ghLoc);
// 读取完OSM数据之后会构建路线图,此处配置图的存储路径
hopper.setGraphHopperLocation("target/routing-graph-cache");
// 目前免费包仅支持car、bike、foot三种交通方式的导航
hopper.setProfiles(new Profile("car").setVehicle("car").setWeighting("fastest").setTurnCosts(false));
// this enables speed mode for the profile we called car
hopper.getCHPreparationHandler().setCHProfiles(new CHProfile("car"));
// now this can take minutes if it imports or a few seconds for loading of course this is dependent on the area you import
hopper.importOrLoad();
return hopper;
}
然后给定出发点和目的地的经纬度,调用 route(),便可以得到按照默认配置导航出来的结果。
public static void routing(GraphHopper hopper) {
// simple configuration of the request object
GHRequest req = new GHRequest(30.633627, 114.324787, 30.557590, 114.533759).
// note that we have to specify which profile we are using even when there is only one like here
setProfile("car").
// define the language for the turn instructions
setLocale(Locale.CHINA);
GHResponse rsp = hopper.route(req);
// handle errors
if (rsp.hasErrors())
throw new RuntimeException(rsp.getErrors().toString());
// use the best path, see the GHResponse class for more possibilities.
ResponsePath path = rsp.getBest();
// 导航结果点位集合
PointList pointList = path.getPoints();
// 总距离 m
double distance = path.getDistance();
// 总耗时 ms
long timeInMs = path.getTime();
System.out.println("路线点位: " + pointList);
System.out.println("总距离: " + distance + ", 总用时: " + timeInMs);
Translation tr = hopper.getTranslationMap().getWithFallBack(Locale.CHINA);
InstructionList il = path.getInstructions();
// iterate over all turn instructions
for (Instruction instruction : il) {
System.out.println("distance " + instruction.getDistance() + " for instruction: " + instruction.getTurnDescription(tr));
}
}
如果我们自己有一些定制化的需求,例如国道优先,不同道路类型不同的最大速度、限高或者限行等等约束,或者通过改变一些参数来满足不同车型的导航需求,那便需要我们对导航的模型做一些配置,代码层面的操作如下:
public static void customizableRouting(String ghLoc) {
GraphHopper hopper = new GraphHopper();
hopper.setOSMFile(ghLoc);
hopper.setGraphHopperLocation("target/routing-custom-graph-cache");
hopper.setProfiles(new CustomProfile("car_custom").setCustomModel(new CustomModel()).setVehicle("car"));
// The hybrid mode uses the "landmark algorithm" and is up to 15x faster than the flexible mode (Dijkstra).
// Still it is slower than the speed mode ("contraction hierarchies algorithm") ...
hopper.getLMPreparationHandler().setLMProfiles(new LMProfile("car_custom"));
hopper.importOrLoad();
// ... but for the hybrid mode we can customize the route calculation even at request time:
// 1. a request with default preferences
GHRequest req = new GHRequest().setProfile("car_custom").
addPoint(new GHPoint(30.633627, 114.324787)).addPoint(new GHPoint(30.557590, 114.533759));
// 2. now avoid primary roads and reduce maximum speed
CustomModel model = new CustomModel();
model.addToPriority(If("road_class == PRIMARY", MULTIPLY, 0.5));
// unconditional limit to 100km/h
model.addToPriority(If("true", LIMIT, 80));
req.setCustomModel(model);
GHResponse res = hopper.route(req);
if (res.hasErrors())
throw new RuntimeException(res.getErrors().toString());
ResponsePath path = res.getBest();
// points, distance in meters and time in millis of the full path
PointList pointList = path.getPoints();
double distance = path.getDistance();
long timeInMs = path.getTime();
System.out.println("路线点位: " + pointList);
System.out.println("总距离: " + distance + ", 总用时: " + timeInMs);
Translation tr = hopper.getTranslationMap().getWithFallBack(Locale.CHINA);
InstructionList il = path.getInstructions();
// iterate over all turn instructions
for (Instruction instruction : il) {
System.out.println("distance " + instruction.getDistance() + " for instruction: " + instruction.getTurnDescription(tr));
}
hopper.close();
}
关于自定义需求配置,下面详细说一下
CustomModel model = new CustomModel();
model.addToPriority(If("road_class == PRIMARY", MULTIPLY, 0.5));
model.addToPriority(If("true", LIMIT, 80));
If("road_class == PRIMARY", MULTIPLY, 0.5)
MULTIPLY 是设置优先级关键字。下边这句话的意思是 如果路线的类型是“PRIMARY”的话,优先级 * 0.5 ,也就是降低一半。
If("true", LIMIT, 80)
LIMIT 是设置约束的关键字。下边这句话的意思是,将速度无条件的设置为 80 km/h
上边提到的对不同的道路类型设置不同的速度这种复杂的约束是怎么实现的呢 ?
If("road_class == TERTIARY", MULTIPLY, 0.2), // 降低三级路线的优先级
ElseIf("road_class == SECONDARY", LIMIT, 25), // 二级路线限速25km/h
If("road_environment == TUNNEL", LIMIT, 60), // 隧道环境限速60km/h
ElseIf("road_environment == BRIDGE", LIMIT, 50), // 桥梁环境限速50km/h
If("max_width < 3", MULTIPLY, 0.6), // 最大路宽 < 3m ,设置优先级 此处就是设置导航的时候对于限宽的道路的优先选择的权重,如果车辆超过这个宽度,可以设置为0,避免这些道路。
Else(MULTIPLY, 0.8)
以上便是自定义航线约束的配置规则,我们感兴趣的限高、限行等约束同样可以根据上边的规则进行配置。
关于速度的限制,主要是用来计算总用时,至于导航路径的准确性还是依赖优先级的配置。
你可能会对 road_class == TERTIARY、road_environment == BRIDGE 等属性有些许的疑问
Q:这些属性是怎么来的?
A:没错,就是上边讲OSM的时候提到的路径(way)的标签 (tag)例如:
- 路线类型: <tag k="highway" v="tertiary"/>
- 路线环境:<tag k="bridge" v="yes"/>
Q:现在的数据已经有限高、现行等属性吗?
A:至于限行限高等属性是否存在,以我目前看到的数据来说是没有的(也可能是有,我没看到),如果要对这些条件进行限制导航,就需要我们对OSM进行编辑,或者编辑自己的约束数据,读取后在内存中与OSM数据进行合并。
以上便是最近对导航的一些浅显领悟,后续的问题便是如何将OSM数据中没有的一些约束数据合并进来,具体怎么处理,敬请期待下篇!