geoTools的A*(A star)算法计算最优路径

1,929 阅读6分钟

概述

寻路算法有很多种,A*寻路算法被公认为最好的寻路算法。

对A*算法的原理想要详细了解的,推荐以下两篇文章

blog.csdn.net/qq_36946274…

blog.csdn.net/denghecsdn/…


本文主要内容不会对A*算法的详细介绍,而是通过已经实现的A*算法的geotools工具来寻找最优路径。


核心依赖包

开发语言java

<dependency>   
    <groupId>org.geotools</groupId>    
    <artifactId>gt-graph</artifactId>   
    <version>18.0</version>
</dependency>

Geotools提供了一个Graph的扩展包,这个扩展包不只是对A*算法进行了实现,也对算法Dijkstra进行了实现,两种算法在Geotools中的用法基本一致。


基本概念介绍


Graph(图)

可以理解为是一个将要建模的对象的容器,图里面存放的是很多的点(node)和边(edge)。

Node(点)

Node(点)是将要建模的对象,它可以指的是实际地图中的一个坐标点,也可以指的是任意坐标系中的一个点,也可以指的是拓扑图里面的一个节点。构建好的Node全部都存放在Graph中。

Edge(边)

边是指两个Node(点)的关联,一个边下面会有两个点,在图中的边是认为两个点之间可到达,边下面两个点分别表示NodeA和NodeB,节点方向为NodeA->NodeB,


第一步:构图

既然是路径搜索,那必然是在图上进行搜索,无论是实际的地图,平面图还是流程图或者是自己构想的概念图,首先需要构建出一张图,然后才能在给定的图中根据条件去搜索最短路径。图是由很多的点和边构成,所以构图的同时要构建点和边,放到图中。

构图可以有多种方式:

1、通过导入shp文件构建

该文件格式已经成为了地理信息软件界的一个开放标准

需要引入依赖

<dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-shapefile</artifactId>
    <version>18.4</version>
</dependency>

    public static void main(String[] args) throws IOException {
        String filePath = "F:/binjiang.shp";
        File shapeFile = new File(filePath);

        FileDataStore dataStore = FileDataStoreFinder.getDataStore(shapeFile);
        SimpleFeatureSource featureSource = dataStore.getFeatureSource();

        SimpleFeatureCollection simpleFeatureCollection = featureSource.getFeatures();
        Graph graph = buildGraph(simpleFeatureCollection);
    }

    private static Graph buildGraph(FeatureCollection fc) {
        LineStringGraphGenerator lineStringGen = new LineStringGraphGenerator();
        FeatureGraphGenerator featureGen = new FeatureGraphGenerator(lineStringGen);
        FeatureIterator iter = fc.features();

        while (iter.hasNext()) {
            Feature next = iter.next();
            featureGen.add(next);
        }

        iter.close();

        return featureGen.getGraph();
    }

2、LineString

和上面的方式差不多,传入LineString的数据格式,通过LineStringGraphGenerator构建图,生成器会将lineString转换成图中的点和边放入图中

LineStringGraphGenerator lineStringGraphGenerator = new LineStringGraphGenerator();
lineStringGraphGenerator.add(lineString);
BasicDirectedGraph graph = (BasicDirectedGraph)generator.getGraph();

3、手动构建

通过GraphGenerator手动构建Node(点)和Edge(边)来构造图。这种方式虽然需要手动去绘制点和边,但灵活性最高,可以构建流程图或是构想的概念图。

简单介绍构图的辅助类有向图生成器BasicDirectedGraphGenerator

该生成器继承GraphGenerator,内部使用了一个有向图构造器BasicDirectedGraphBuilder来构造点、边和图。该构造器继承了BasicGraphBuilder,如下图源码


可以发现,构建器在构建(Node)点和(Edge)边时,是用了HashSet来作为底层数据结构存储,所以这里需要注意一个问题:当用这个方法手动构建时,需要用到多线程进行批量建模的时候,要注意线程安全的问题。


构造Node(点)

Node node = graphGenerator.getGraphBuilder().buildNode();
//将构造出来的点添加到生成器中
graphGenerator.getGraphBuilder().addNode(node);

构造点的时候,实际是通过GraphGenerator创建了一个Node对象。如果是有向图的点,点在被构建时,会初始化Node中的两个连接点列表


m_In和m_out存放的是当前Node有关系的边,m_In表示的是方向指向自身的Edge,m_out表示的是从自身出去的Edge。

例如:NodeA->NodeB可达  NodeC -> NodeA 可达

则在NodeA中的m_in会有NodeC -> NodeA,在m_out中会有NodeA->NodeB

当然这部分是到了构造边的时候才会添加入值,因为有边才表示两个点之间可达。


另外,Node继承的BasicGraphable中维护了一个Object类型的字段,我们可以在这个字段中放置一些标识该Node特性的信息。比如坐标,或者节点名称等。


node.setObject(new Coordinate(26.061743,119.169153));

构造Edge(边)

Edge表示的就是图里面Node与Node之间的关系,所以一个边是包含了两个点。有边也认为两个点之间是可直达的。


Edge edge = graphGenerator.getGraphBuilder().buildEdge(nodeA, nodeB);
Long distance = getDistance(nodeA, nodeB)
edge.setObject(distance);
graphGenerator.getGraphBuilder().addEdge(edge);

同样我们可以在源码中看到,构建边的时候,会分别往边下面的两个Node中的m_in和m_out列表中去添加值


获取图(Graph)

当完成点和边的构造之后,图也就构造完了,直接从生成器中getGraph()即可得到图

Graph graph = graphGenerator.getGraph();


第二步:设值权重,编写消耗函数和启发函数

这部分是寻找最短路径中的核心,不同权重的设值,对于最后计算出的结果息息相关

A*算法的遍历中,f(x) = g(x) + h(x)。g(x)对应的是两个点之间移动的消耗规则, h(x)是引导函数,引导路径朝着终点的方向进行。

AStarIterator.AStarFunctions asFunction = new AStarIterator.AStarFunctions(destination) {

@Override
public double cost(AStarIterator.AStarNode aStarNode, AStarIterator.AStarNode aStarNode1) {
    Edge edge;
    double cost = Integer.MAX_VALUE;
    edge = ((DirectedNode)aStarNode.getNode()).getOutEdge((DirectedNode) aStarNode1.getNode());
    
    if(edge != null){
        cost = (double) edge.getObject();

    }

    return cost;
}

@Override
public double h(Node node) {
    double h = 0d;
    Coordinate destCoor = (Coordinate) destination.getObject();    Coordinate nodeCoor = (Coordinate) node.getObject();
    distance = destCoor.distance(nodeCoor);

    return distance;
}
};



第三步:寻找最短路径

传入构建好的graph,起点Node,终点Node和权重函数。通过AstarShortestPathFinder中的calculate()进行计算并最终会返回路径

    public Path findAStarShortestPath(Graph graph, Node source, Node destination, AStarIterator.AStarFunctions asFunction) throws Exception {
        Path shortestPath;

        // 求解最短路径
        AStarShortestPathFinder pf = new AStarShortestPathFinder(graph, source, destination, asFunction);
        // geotools 20.x 以上的版本才支持有向图的查找,其他版本会将有向图视作无向图
        pf.calculate();
        shortestPath = pf.getPath();
        return shortestPath;
    }

返回的path中是路径中包含的所有Node,可以将路径遍历出来

Iterator it = path.iterator();
String result = "";
while (it.hasNext()) {
      Node node = (Node) it.next();
      result = result + node.getObject().toString(); 
}

Path中遍历出来的路径是从终点到起点的,例如实际最短路径是NodeA-> NodeD -> NodeB ->NodeC,从path中遍历出来的结果是NodeC、NodeB、NodeD、NodeA

tips

这里还有一个点需要说明,由于寻找最短路径时,我们需要给定start Node和destination Node,但是通常情况下我们一般给定的是两个坐标点,或者两个流程节点的标识。

如果是在坐标地图中,我们可以通过graph的queryNodes()来找到条件范围内的所有点(通过重写其visit方法,可以设定在给定点的多少范围内认为可选,返回PASS_AND_CONTINUE状态,否则返回FAIL_QUERY状态),从而去找到起点和终点相应的Node。

如果是流程图或者概念图,可以在内存中放一个Map<String, Node> nodeMap去存储图中的所有Node,这样我们可以通过每个node的特性快速找到对应的Node。