簡單的室內導航,就是在沒有傳感器或者說外部硬件設施輔助(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。開始游戲(基於選擇的起點和終點,選出最短的路徑。說明下:繪制路徑的時候,其實已經將兩點之間的距離,即基於像素算出來的歐氏距離已經入庫了)
到此,一個簡單的室內導航的應用方案,就完成了,有什么更好的創意,可以隨時與我交流,關注博客,歡迎留言。
注意:轉載請寫明出處。