简单的室内导航,就是在没有传感器或者说外部硬件设施辅助(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看吧,nav(https://github.com/shihuc/nav)项目。可以fork,可以star,欢迎欢迎,关注我的博客,随时评论。
接下来,看看效果图:
A。 空的界面
B。打点,选择出关键分叉路口点(这个思路有很大的好处,就是室内规划有变的时候,只需要在关键分叉口添加或者节点,局部调整一下路径连接)
C。绘制任意两点之间可以通行的路径
D。选择导航的起点(因为这里没有任何传感器设备,起点只能人为选择)
E。选择终点(就是要到达的目的地)
F。开始游戏(基于选择的起点和终点,选出最短的路径。说明下:绘制路径的时候,其实已经将两点之间的距离,即基于像素算出来的欧氏距离已经入库了)
到此,一个简单的室内导航的应用方案,就完成了,有什么更好的创意,可以随时与我交流,关注博客,欢迎留言。
注意:转载请写明出处。