SHIHUC

好记性不如烂笔头,还可以分享给别人看看! 专注基础算法,互联网架构,人工智能领域的技术实现和应用。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

室内导航简单应用方案【Dijkstra+zrender】

Posted on 2019-12-31 11:05  shihuc  阅读(1906)  评论(0编辑  收藏  举报

简单的室内导航,就是在没有传感器或者说外部硬件设施辅助(WIFI或者蓝牙组网点整)的情况下,基于相对位置实现。

 

思路很简单,在室内地图上,将能走的路上关键点(能够产生分叉的路口)上打点,然后将能走通的点间,用线连接起来(这个线就是相邻两个点之间的路径,路径的长度由打出来的点的坐标,依据勾股定理计算出来),这样,就可以构建出一个限定地图(楼层)范围内的路线网络,这个打点连线的过程,就有点类似百度地图或者高德地图之类的,绘制地图中的道路的过程,只是我这里,相对来说,比较简单而已,但是,核心的思想其实大同小异。即:导航前,必须有一个地图,关键就是有一个路线网络。

 

接下来,当有人需要用导航的时候,就需要选择自己在那个门口,然后选择自己要去那个地方,这套方案就可以给选定出一个最短路线。后台计算最短路径的算法,就是基于dijkstra算法,思路简单清晰。

 

也就是说,这里的室内简单导航方案,主要是前端绘图,然后,后端基于客户请求,算出最短路径所经过的点,将这些点以及边的信息,告知前端,前端绘制出这个最短的路径,用户就可以基于自己所在的起点,沿着这个路线,找到自己所要去的目的地。这里之所以说是个简单的方案,原因在于,用户离开起点后,在行进的过程中,失去了自己当前所在位置信息,即没有了参考。当然,结合硬件设备,即可将用户实时的位置信息反映到地图上,就解决了实时位置参考信息。

 

前端的打点和绘图工作,主要依据zrender.js这个插件实现(是个非常不错的绘图工具),后台数据处理,主要基于springboot+mysql完成。

 

这里不做过多的介绍,直接上代码:

1. 前端HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>DijMap</title>
</head>
<style>
#container{
    height: 700px;
    border: 3px dashed #ccc;
    margin: 0 auto;
}
#clearBtn, span{
    margin-left: 12px;
}
</style>
<body>
<h1>找最短路径游戏</h1>
<span>前端技术参考资料:https://ecomfe.github.io/zrender-doc/public/api.html</span><br/>
<span style="color: #44bb99;">说明:1)左键单击创建节点,左键按下拖动到终点实现划线;2)右键单击删除节点/边;3)选择起点/终点状态后,中键选择起点/终点</span><br/>
<button id="clearBtn">清除所有点</button>
<label><input name="demo" type="radio" value="st"/>选起点</label>
<label><input name="demo" type="radio" value="ed"/>选终点</label>
<button id="nearest">开始游戏</button>
<button id="restgame">重新游戏</button>
<div id="container"></div>
<script src="../js/jquery-2.1.1.min.js"></script>
<script src="../js/zrender.min.js"></script>
<script src="../js/inner-map.js"></script>
</body>
</html>

 

2.前端inner-map.js

function setPanel() {
    var width = $(document.body).width();
    var height = $(document.body).height();
    $('#container').height(height - 100);
    $('#container').width(width - 30);
    $('canvas').attr("height",height - 100);
    $('canvas').attr("width", width - 30);
}
function savePoint(zr, pos, cycle) {
    $.post("./point/save", pos, function(data){
        cycle.pointId = data.info;
        pos.id = data.info;
        createText(zr, pos);
        savePointToLocal(pos.id, {"x": pos.pointx, "y":pos.pointy});
    }, "json");
}
function getAllPoints(zr) {
    $.get("./point/getAll", function(data){
        var jp = data;
        for(var i=0; i<jp.length; i++){
            console.log("id: " + jp[i].id + ", x: " + jp[i].pointx + ", y: " + jp[i].pointy);
            createPoint(zr, jp[i]);
            createText(zr, jp[i]);
            savePointToLocal(jp[i].id, {"x": jp[i].pointx, "y":jp[i].pointy})
        }
    }, "json");
}
function getPaths(zr, src, dst) {
    $.get("./go", {"srcId": src, "dstId": dst}, function(data){
        var jp = data;
        for(var i=0; i<jp.length; i++){
            console.log("id: " + jp[i].id + ", x: " + jp[i].pointx + ", y: " + jp[i].pointy);
        }
        showPath(zr, jp);
    }, "json");
}
function showPath(zr, jp) {
    //jp的长度一定是大于等于2的,否则不可能行程一条路径
    if(jp.length <= 1){
        console.log("不是一个合法的路径");
        return;
    }
    for(var i = 0; i<jp.length-1; i++){
        var fp = jp[i];
        var tp = jp[i+1];
        var path = new zrender.Line({
            shape: {
                x1:fp.pointx,
                y1:fp.pointy,
                x2:tp.pointx,
                y2:tp.pointy
            },
            style: {
                stroke:'green',
                lineWidth: 3
            }
        });
        zr.add(path);
        showedPath.push(path);
    }
}
function savePointToLocal(idx, pos) {
    var spos = JSON.stringify(pos);
    sessionStorage.setItem(idx, spos);
}
function getPointFromLocal(idx) {
    var res = sessionStorage.getItem(idx);
    var pos = JSON.parse(res);
    return pos;
}
function saveEdge(line) {
    if(line.len <= 10){
        console.log("距离太近,不予考虑...");
        return;
    }
    $.post("./edge/save", {"from":line.from, "to": line.to, "len": line.len}, function(data){
        line.lineId = data.info;
    }, "json");
}
function getAllEdges(zr) {
    $.get("./edge/getAll", function(data){
        var je = data;
        for(var i=0; i<je.length; i++){
            console.log("id: " + je[i].id + ", point: " + je[i].point + ", neighbor: " + je[i].neighbor + ", weight: " + je[i].weight);
            createEdge(zr, je[i]);
        }
    }, "json");
}
function delPoint(zr, circle) {
    $.post("./point/del", {"id":circle.pointId}, function(data){
        zr.remove(circle);
        zr.remove(textMap[circle.pointId]);
        for(var i = 0; i<data.length; i++){
            var edgeId = data[i];
            var dline = edgeMap[edgeId];
            zr.remove(dline);
            delete(edgeMap[edgeId])
        }
    }, "json");
}
function delEdge(zr, line) {
    $.post("./edge/del", {"id":line.lineId}, function(data){
        zr.remove(line);
        delete(edgeMap[line.lineId]);
    }, "json");
}
function createEdge(zr, je) {
    var fp = je.point;
    var tp = je.neighbor;
    fpoint = getPointFromLocal(fp);
    tpoint = getPointFromLocal(tp);
    var line = new zrender.Line({
        shape: {
            x1:fpoint.x,
            y1:fpoint.y,
            x2:tpoint.x,
            y2:tpoint.y
        },
        style: {
            stroke:'black'
        }
    }).on("mousedown", function(ev){
        if(ev.which == 3) { //右键
            delEdge(zr, line);
        }
    });
    line.from = fp;
    line.to = tp;
    line.len = je.weight;
    line.lineId = je.id;
    zr.add(line);
    edgeMap[je.id] = line;
}
function calcLen(fpoint, tpoint) {
    var xx = (fpoint.x - tpoint.x) * (fpoint.x - tpoint.x);
    var yy = (fpoint.y - tpoint.y) * (fpoint.y - tpoint.y);
    var edge = Math.sqrt(xx + yy);
    return Math.round(edge);
}
var fpoint = {"x":0, "y":0};
var tpoint = {"x":0, "y":0};
var fcycle = null;
var srcId = null;
var dstId = null;
var step = null;
var edgeMap = {};
var textMap = {};
var showedPath = [];
function createPoint(zr, pos) {
    var circle = new zrender.Circle({
        shape: {
            cx: 0,
            cy: 0,
            r: 10
        },
        position: [
            pos.pointx,
            pos.pointy
        ],
        style: {
            stroke: 'green',
            fill: 'red'
        }
    }).on('mouseover', function(){
        this.animateTo({
           shape: {
               r: 20
           },
           style: {
               stroke: 'green',
               fill: 'blue'
           }
        }, 300)
    }).on('mouseout', function() {
        this.animateTo({
            shape: {
                r: 10
            },
            style: {
                stroke: 'green',
                fill: 'red'
            }
        }, 300)
    }).on("mousedown", function(ev){
        if(ev.which == 1){ //左键
            fpoint = {"id": circle.pointId, "x": pos.pointx, "y": pos.pointy};
        }else if(ev.which == 3){//右轮
            delPoint(zr, circle);
        }else if(ev.which == 2){//中键
             //var step = $('input:radio:checked').val();
             if(step === 'st'){
                srcId = circle.pointId;
             }
             if(step === 'ed'){
                dstId = circle.pointId;
             }
             console.log("step: " + step + ", src: " + srcId + ", dst: " + dstId);
         }
    }).on("mouseup", function(ev){
        if(ev.which == 1){ //左键
            tpoint = {"id": circle.pointId, "x": pos.pointx, "y": pos.pointy};
            var line = new zrender.Line({
                shape: {
                    x1:fpoint.x,
                    y1:fpoint.y,
                    x2:tpoint.x,
                    y2:tpoint.y
                },
                style: {
                    stroke:'black'
                }
            }).on("mousedown", function(ev){
                if(ev.which == 3){ //左键
                    delEdge(zr, line);
                }
            });
            var len = calcLen(fpoint, tpoint);
            line.from = fpoint.id;
            line.to = tpoint.id;
            line.len = len;
            saveEdge(line);
            zr.add(line);
            edgeMap[line.lineId] = line;
        }else if(ev.which == 3){//右轮

        }
    })
    if(pos.id != null && pos.id != undefined){
        circle.pointId = pos.id;
    }
    zr.add(circle);
    return circle;
}
function createText(zr, pos) {
    var posText = new zrender.Text({
        style: {
            stroke: 'blue',
            text: "[" + pos.id + "] (" + pos.pointx + "," + pos.pointy + ")",
            fontSize: '11',
            textAlign:'center'
        },
        position: [pos.pointx, pos.pointy + 13]
    });
    zr.add(posText);
    textMap[pos.id] = posText;
}

$(document).ready(function() {            
    document.oncontextmenu = function(){
      return false;
    }

    var container = document.getElementById('container');
    var zr = zrender.init(container);
    
    setPanel();
    //注意,一定是先加载点,然后再加载边
    getAllPoints(zr);
    getAllEdges(zr);
    zr.on('click', function(e) {
        var pos = {"id": 0, "pointx": e.offsetX, "pointy": e.offsetY};
        var point = createPoint(zr, pos)
        savePoint(zr, pos, point);
    })
    //删除所有的节点
    $('#clearBtn').on('click', function(e) {
        zr.clear()
    })
    //选择起点和终点
    $("input[type=radio]").on("click", function(){
        step = $('input:radio:checked').val();
    });
    //开始绘制最短路径
    $('#nearest').on('click', function(e) {
        getPaths(zr, srcId, dstId);
    });
    //删除生成的最短路径,将上次的起始和结束点复位
    $('#restgame').on('click', function(e) {
        var len = showedPath.length;
        for(var i=0; i<len; i++){
            zr.remove(showedPath[i]);
        }
        showedPath.splice(0, len);
        srcId = null;
        dstId = null;
    })
});

 

3. Dijkstra最短路径

package com.shihuc.up.nav.path.util;

import org.springframework.data.mongodb.core.aggregation.ArrayOperators;

import java.util.List;
import java.util.Queue;
import java.util.Stack;

/**
 * @Author: chengsh05
 * @Date: 2019/12/9 10:25
 */
public class DJMatrix {

    private static int INF = Integer.MAX_VALUE;

    public static void dijkstra(int vs, int mMatrix[][], int[] prev, int[] dist) {
        // flag[i]=true表示"顶点vs"到"顶点i"的最短路径已成功获取
        boolean[] flag = new boolean[mMatrix.length];

        // 初始化
        for (int i = 0; i < mMatrix.length; i++) {
            // 顶点i的最短路径还没获取到。
            flag[i] = false;
            // 顶点i的前驱顶点为0,此数组的价值在于计算出最终具体路径信息。
            prev[i] = 0;
            // 顶点i的最短路径为"顶点vs"到"顶点i"的权。
            dist[i] = mMatrix[vs][i];
        }

        // 对"顶点vs"自身进行初始化
        flag[vs] = true;
        dist[vs] = 0;

        // 遍历所有顶点;每次找出一个顶点的最短路径。
        int k=0;
        for (int i = 1; i < mMatrix.length; i++) {
            // 寻找当前最小的路径, 即,在未获取最短路径的顶点中,找到离vs最近的顶点(k)。
            int min = INF;
            for (int j = 0; j < mMatrix.length; j++) {
                if (flag[j]==false && dist[j]<min) {
                    min = dist[j];
                    k = j;
                }
            }
            // 标记"顶点k"为已经获取到最短路径
            flag[k] = true;

            // 修正当前最短路径和前驱顶点
            // 即,当已经求出"顶点k的最短路径"之后,更新"未获取最短路径的顶点的最短路径和前驱顶点"。
            for (int j = 0; j < mMatrix.length; j++) {
                int tmp = (mMatrix[k][j]==INF ? INF : (min + mMatrix[k][j]));
                if (flag[j]==false && (tmp<dist[j]) ) {
                    dist[j] = tmp;
                    prev[j] = k;
                }
            }
        }
    }

    public static String calcPath(int vs, int ve, int prev[], Stack<Integer> pathOut) {
        String path = "" + ve;
        pathOut.push(ve);
        int vep = prev[ve];
        while (vep != 0 && vs != vep) {
            path = vep + "->" + path;
            pathOut.push(vep);
            vep = prev[vep];
        }
        pathOut.push(vs);
        return vs + "->" + path;
    }

    public static void main(String []args) {
        int stops[][] = new int [][] {
                {0,   12,INF,INF,INF,16,14},
                {12,   0,10,INF,INF, 7,INF},
                {INF, 10, 0, 3, 5, 6,INF},
                {INF,INF, 3, 0, 4,INF,INF},
                {INF,INF, 5, 4, 0, 2, 8},
                {16,   7, 6, INF, 2, 0, 9},
                {14, INF,INF,INF, 8, 9, 0}
        };

        int vs = 0;
        int prev[] = new int[stops.length];
        int dist[] = new int[stops.length];
        dijkstra(vs, stops, prev, dist);
    }
}

 

4. 数据库表结构

A.点表(记录的是关键分叉路口的位置,是像素点坐标)

CREATE TABLE `dij_point` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pointx` int(11) NOT NULL COMMENT '点的X坐标',
  `pointy` int(11) NOT NULL COMMENT '点的Y坐标',
  PRIMARY KEY (`id`),
  UNIQUE KEY `POINT_XY_IDX` (`pointx`,`pointy`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4;

 

B.边表(记录可以通行的两点之间的边,边代表路径,是无方向的,边的长度用两个点之间的像素距离表示)

CREATE TABLE `dij_edge` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `point` int(11) NOT NULL COMMENT 'point表的主键ID',
  `neighbor` int(11) NOT NULL COMMENT '指定点的邻居节点在point表的主键ID',
  `weight` int(11) NOT NULL COMMENT '边的权重,这里主要是像素距离',
  PRIMARY KEY (`id`),
  UNIQUE KEY `POINT_NEIG_IDX` (`point`,`neighbor`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;

 

 

5. 效果展示

其他的代码,这里就不做过多的贴出来,有兴趣的,可以去我的github看吧,navhttps://github.com/shihuc/nav)项目。可以fork,可以star,欢迎欢迎,关注我的博客,随时评论。

 

接下来,看看效果图:

A。 空的界面

 

B。打点,选择出关键分叉路口点(这个思路有很大的好处,就是室内规划有变的时候,只需要在关键分叉口添加或者节点,局部调整一下路径连接)

 

C。绘制任意两点之间可以通行的路径

 

D。选择导航的起点(因为这里没有任何传感器设备,起点只能人为选择)

 

E。选择终点(就是要到达的目的地)

 

F。开始游戏(基于选择的起点和终点,选出最短的路径。说明下:绘制路径的时候,其实已经将两点之间的距离,即基于像素算出来的欧氏距离已经入库了)

 

到此,一个简单的室内导航的应用方案,就完成了,有什么更好的创意,可以随时与我交流,关注博客,欢迎留言。

注意:转载请写明出处。