經過結合 HTML5 和 OpenLayers 能夠組合成很是棒的一個電信地圖網絡拓撲圖的應用,造成的效果能夠用來做爲電信資源管理系統,美食定位分享軟件,片區找房,繪製鐵軌線路等等,各個領域都可以涉及的一款應用。雖然這個 Demo 是結合 OpenLayers3 的,其實還可推廣到與 ArcGIS、百度地圖以及 GoogleMap 等衆多 GIS 地圖引擎融合。css
http://www.hightopo.com/demo/openlayers/html
OpenLayers 是一個用於開發 WebGIS 客戶端的 JavaScript 包。OpenLayers 支持的地圖來源包括 Google Maps、Yahoo、 Map、微軟 Virtual Earth 等多種離線在線地圖,這裏用到的是比較大衆化的谷歌地圖 Google Map 的在線地圖,使用 OpenLayers 前只須要引入相關的類庫以及 css 文件:node
<link rel="stylesheet" href="css/ol.css" type="text/css"> <script src="lib/ol.js"></script>
初始化地圖的操做則是將 Map 放進一個 div 元素中,初始化一個 ol.Map 地圖類,這在整個電信資源管理系統中必不可少,而後設置這個類中的各個參數:json
var mapDiv = document.getElementById('mapDiv'); map = new ol.Map({ target: 'mapDiv',// 地圖容器 controls: ol.control.defaults().extend([ graphViewControl,// 自定義拓撲控件 new ol.control.OverviewMap(),// 地圖全局視圖控件 new ol.control.ScaleLine(),// 比例尺控件 new ol.control.ZoomSlider(),// 縮放刻度控件 new ol.control.ZoomToExtent()// 縮放到全局控件 ]), layers: [// 圖層 new ol.layer.Tile({ source: new ol.source.XYZ({// 谷歌地圖 url:'http://www.google.cn/maps/vt/pb=!1m4!1m3!1i{z}!2i{x}!3i{y}!2m3!1e0!2sm!3i345013117!3m8!2szh-CN!3scn!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0' }) }) ], view: new ol.View({// 地圖視圖 projection: 'EPSG:3857',// 投影 center: ol.proj.fromLonLat([106, 35]),// 視圖的初始中心 中心的座標系由projection選項指定 zoom: 4// 縮放級別 用於計算視圖的初始分辨率 }) });
上面的代碼根據每行的代碼註釋加上官方 API 解釋應該沒有什麼難度。細心的朋友可能注意到了一個非官方的控件:graphViewControl 控件,這個控件是我自定義出來,用來在這個控件上繪製拓撲圖形的,聲明和定義部分在 GraphViewControl.js 文件中。canvas
自定義 OpenLayers 的控件,無非就是將某個類繼承於 ol.control.Control 類,而後針對不一樣的需求重寫父類方法或者增長方法。數組
我在聲明類的時候傳了一個 options 參數,經過在定義類的時候設置控件的容器元素而且將控件渲染到 GIS 地圖的 viewport 以外:網絡
var view = graphView.getView();// 獲取拓撲組件的 div ol.control.Control.call(this, { element: view,// 控件的容器元素 target: options.target// 將控件渲染到地圖的視口以外 });
上面的 graphView 是經過 GraphViewControl 在父類方法上新添加的一個方法而且初始化值爲 ht.graph.GraphView,HT 的拓撲圖形組件:ide
// 獲取GraphView對象 GraphViewControl.prototype.getGraphView = function() { return this._graphView; }; var graphView = this._graphView = new ht.graph.GraphView();// 拓撲圖組件
我在控件中還給 graphView 拓撲組件添加了一些事件的監聽,因爲 OpenLayers 和 HT 是兩款不一樣的 js 庫,有着各自的交互系統和座標系,首先咱們將某些咱們須要獲取在 HT 上作的交互事件並中止事件傳播到 OpenLayers 上:函數
// 拖拽 node 時不移動地圖 var stopGraphPropagation = function(e) { var data = graphView.getDataAt(e);// 獲取 graphView 事件下的節點 var interaction = graphView.getEditInteractor();// 獲取編輯交互器 if (data || e.metaKey || e.ctrlKey || interaction && interaction.gvEditing) { e.stopPropagation();// 再也不派發事件 該方法將中止事件的傳播,阻止它被分派到其餘 Document 節點 } } /** pointerdown 當指針變爲活動事件 * 對於鼠標,當設備從按下的按鈕轉換到至少一個按鈕被按下時,它會被觸發。 * 對於觸摸,當與數字化儀進行物理接觸時會被觸發。 * 對於筆,當觸筆與數字化儀進行物理接觸時會被觸發。 **/ view.addEventListener('pointerdown', stopGraphPropagation, false); view.addEventListener('touchstart', stopGraphPropagation, false);// 當觸摸點被放置在觸控面板上事件 view.addEventListener('mousedown', stopGraphPropagation, false);// 鼠標點下事件
GraphViewControl 類定義部分還添加了一些關於移動和編輯節點的交互事件,主要是將節點的像素座標轉爲 OpenLayers 的 ol.Cordinate 地圖視圖投影中的座標並存儲到節點的業務屬性(HT 的一個能夠存儲任意值的對象)中,這樣咱們只須要經過獲取或設置節點的業務屬性 coord 就能夠自由獲取和設置節點在 map 上的像素座標。工具
var position = data.getPosition(),// 獲取選中節點的座標 x = position.x + graphView.tx(),// 節點橫座標+graphView水平平移值 y = position.y + graphView.ty();// 節點縱座標+graphView垂直平移值 var coord = map.getCoordinateFromPixel([x, y]);// 根據座標的像素獲取地圖視圖投影中的座標 data.a('coord', coord);
這裏我就提一些基礎的功能,其餘的就不做解釋了,只是一些擴展。
值得注意的一點是,咱們在上面對節點在電信 GIS 地圖視圖投影中的座標進行了數據存儲,可是這個方法對於 Shape 類型的節點來講不太合適,由於地圖上通常都是用點圍成區域面,勾勒出某個國家或者某個城市的輪廓,縮放的時候並不實時保持大小,而是根據地圖的縮放來縮放,實時保持在電信 GIS 地圖的某個位置,因此我對 Shape 類型的節點中全部的點遍歷了一遍,都設置了業務屬性 pointCoord,獲取地圖視圖投影中的座標:
// 給 shape 類型的節點的每一個點位置都設置爲經緯度 if (e.kind === 'endEditPoint' || e.kind === 'endEditPoints' || e.kind === 'endEditResize' || e.kind === 'endMove') { if (data instanceof ht.Shape) {// Shape 類型的節點 data.getPoints().forEach(function(point, index) { var pointCoord = map.getCoordinateFromPixel([point.x, point.y]);// 獲取給定像素的座標 data.a('pointCoord['+index+']', pointCoord); }); } }
OpenLayers 的結構比較複雜,而 HT 相對來講簡單不少,因此我將 HT 疊加到 OpenLayers Map 的 viewport 中。這裏我在子類 GraphViewControl 中重載了父類 ol.control.Control 的 setMap 方法,在此方法中將 HT 的拓撲組件 graphView 添加到 OpenLayers 的視圖 viewport 中,咱們知道,HT 的組件通常都是絕對定位的,因此咱們要設置 css 中的位置和寬高屬性:
var graphView = self._graphView;// = GraphViewControl.getGraphView() var view = graphView.getView();// 獲取 graphView 組件的 div var dataModel = graphView.getDataModel();// 獲取 graphView 的數據容器 view.style.top = '0'; view.style.left = '0'; view.style.width = '100%'; view.style.height = '100%'; map.getViewport().insertBefore(view, map.getViewport().firstChild);// getViewPort 獲取用做地圖視口的元素 insertBefore 在指定的已有子節點(參數二)以前插入新的子節點(參數一)
並對數據容器增刪變化事件進行監聽,經過監聽當前加入數據容器的節點類型,將當前節點的像素座標轉爲地圖視圖投影中的座標存儲在節點的業務屬性 coord 上:
dataModel.addDataModelChangeListener(function(e) {// 數據容器增刪改查變化監聽 if (e.kind === 'add' && !(e.data instanceof ht.Edge)) {// 添加事件&&事件對象不是 ht.Edge 類型 if (e.data instanceof ht.Node) { var position = e.data.getPosition(); var coordPosition = map.getCoordinateFromPixel([position.x, position.y]);// 獲取給定像素的座標 e.data.a('coord', coordPosition); } if (e.data instanceof ht.Shape) {// 給 shape 類型的節點上的每一個點都設置經緯度 e.data.getPoints().forEach(function(point, index) {// 對 shape 類型的節點則將全部點的座標都轉爲經緯度 var pointCoord = map.getCoordinateFromPixel([point.x, point.y]);// 獲取給定像素的座標 e.data.a('pointCoord['+index+']', pointCoord); }); } } });
最後監聽地圖更新事件,重設拓撲:
map.on('postrender', function() { self.resetGraphView(); });
重設拓撲在這邊的意思就是將拓撲圖中節點座標從咱們一開始設置在 HT 中的像素座標從新經過地圖的縮放或者移動將地圖視圖投影中的座標轉爲像素座標設置到節點上,這時候前面存儲的業務屬性 coord 就派上用場了,記住,Shape 類型的節點是例外的,仍是要對其中的每一個點都從新設置座標:
GraphViewControl.prototype.resetGraphView = function() {// 重置 graphView 組件的狀態 var graphView = this._graphView; graphView.tx(0);// grpahView 水平平移值 graphView.ty(0);// graphView 垂直平移值 graphView.dm().each(function(data) {// 遍歷 graphView 中的數據容器 var coord = data.a('coord');// 獲取節點的業務屬性 coord if (coord) { var position = map.getPixelFromCoordinate(coord);// 獲取給定座標的像素 data.setPosition(position[0], position[1]);// 從新給節點設置像素座標 } if (data instanceof ht.Shape) { var points = data.toPoints();// 構建一個新的Shape點集合並返回 data.getPoints().clear();// 清空點集合 data._points = new ht.List(); points.forEach(function(point, index) {// 給 shape 從新設置每個點的像素座標 point.x = map.getPixelFromCoordinate(data.a('pointCoord['+ index +']'))[0]; point.y = map.getPixelFromCoordinate(data.a('pointCoord['+ index +']'))[1]; data._points.add(point); }); data.setPoints(data._points); } }); graphView.validate();//刷新拓撲組件 }
OpenLayers 的 Map 部分作好了,接下來就是將它放進場景中了~可是從上面的截圖中能看到,除了地圖,頂部有工具條(可是我是用 formPane 表單組件作的),左側有一個可供拖拽的 Palette 面板組件,經過 HT 的 borderPane 邊框面板組件將整個場景佈局好:
raphViewControl = new GraphViewControl();// 自定義控件,做爲 openlayers 地圖上自定義控件 graphView = graphViewControl.getGraphView();// 獲取拓撲圖組件 dm = graphView.getDataModel();// 獲取拓撲圖中的數據容器 palette = new ht.widget.Palette();// 建立一個組件面板 formPane = createFormPane();// 工具條的 form 表單 borderPane = new ht.widget.BorderPane();// 邊框面板組件 borderPane.setTopView(formPane);// 設置頂部組件爲 formPane borderPane.setLeftView(palette, 260);// 設置左邊組件爲 palette 參數二爲設置 該view的寬度 borderPane.setCenterView(mapDiv);// 設置中間組件爲 mapDiv borderPane.addToDOM();// 將面板組件添加到 body 中
這樣整個場景的佈局和顯示就完成了,很是輕鬆~
自己 HT 有自帶的工具條,可是由於 form 表單在排布以及樣式上面能夠更靈活,因此採用這個。
var fp = new ht.widget.FormPane(); fp.setVGap(0);// 設置表單組件水平間距 默認值爲6 fp.setHGap(0);// 設置表單的行垂直間距 默認值爲6 fp.setHPadding(4);// 設置表單左邊和右邊與組件內容的間距,默認值爲8 fp.setVPadding(4);// 設置表單頂部和頂部與組件內容的間距,默認值爲8 fp.setHeight(40);// 設置表單高度 var btBgColor = '#fff', btnIconColor = 'rgb(159, 159, 159)', btnSelectColor = 'rgb(231, 231, 231)'; fp.addRow([// 添加行 首尾各加了一個'',而且佔的寬度均爲相對值0.1,就會將中間部分居中 '', { id: 'select',// id 惟一標示屬性,可經過 formPane.getItemById(id) 獲取添加到對應的 item 對象 button: {// ht.widget.Button 爲按鈕類 background: btBgColor,// 設置背景顏色 icon: './symbols/icon/select.json',// 設置圖標 iconColor: btnIconColor,// 設置圖標顏色 selectBackground: btnSelectColor,// 設置選中背景顏色 togglable: true,// 設置按鈕是否處於開關狀態 groupId: 't',// 設置組編號,屬於同組的togglable按鈕具備互斥功能 toolTip: '編輯',// 設置文字提示,可經過 enableToolTip() 和 disableToolTip() 啓動和關閉文字提示 onClicked: function() {// 按鈕點擊觸發函數 editableFunc(); } } }, { id: 'pointLine', button: { background: btBgColor, icon: './symbols/icon/line.json', iconColor: btnIconColor, selectBackground: btnSelectColor, togglable: true, groupId: 't', toolTip: '連線', onClicked: function () { /** 經過 setInteractors 組合交互器 * DefaultInteractor實現Group、Edge和SubGraph圖元的默認雙擊響應,手抓圖平移,滾輪縮放,鍵盤響應等功能 * TouchInteractor實現移動設備上的Touch交互功能 * CreateEdgeInteractor 爲 CreateEdgeInteractor.js 文件中自定義的連線交互器 * CreateShapeInteractor 爲 CreateShapeInteractor.js 文件中自定義的多邊形交互器 **/ graphView.setInteractors([new ht.graph.DefaultInteractor(graphView), new ht.graph.TouchInteractor(graphView, { selectable: false }), new CreateEdgeInteractor(graphView)]); } } },'' ], [0.1, 36, 36, 0.1]);
上面的 form 表單中添加行我只列出了兩個功能,一個編輯的功能,另外一個繪製連線的功能。formPane.addRow 爲添加一行元素,參數一爲元素數組,元素可爲字符串、json 格式描述的組件參數信息、html 元素或者爲 null 的空,參數二爲爲每一個元素寬度信息數組,寬度值大於1表明固定絕對值,小於等於1表明相對值,也可爲 80+0.3 的組合。
爲了讓我想顯示的部分顯示在工具欄的正中央,因此我在第一項和最後一項都設置了一個空,佔 0.1 的相對寬度,而且比例相同,因此中間的部分纔會顯示在正中央。
上面代碼經過 setInteractors 組合咱們所須要的交互器。DefaultInteractor 實現 Group、Edge 和 SubGraph 圖元的默認雙擊響應,手抓圖平移,滾輪縮放,鍵盤響應等功能;TouchInteractor 實現移動設備上的 Touch 交互功能。至於最後面的 CreateEdgeInteractor 則是繼承於 ht.graph.Interactor 交互器的建立連線的交互器。這裏細細地分析一下這個部分,之後就能夠修改或者自定義新的交互器。
咱們經過 ht.Default.def(className, superClass, methods) 定義類,並在 methods 對象中對方法和變量進行聲明。
setUp 方法在對象被建立的時候被調用,根據需求在這裏設置一些功能,我設置的是清除全部的選中的節點:
setUp: function () {// CreateEdgeInteractor 對象被建立的時候調用的函數 CreateEdgeInteractor.superClass.setUp.call(this);this._graphView.sm().cs();// 清除全部選中 }
tearDown 方法在對象結束調用的時候被調用,繪製連線的時候,若是未結束繪製怎麼辦?下一次繪製不可能連着上一次繼續繪製,因此咱們得在結束調用這個類的時候將以前的繪製的點都清除:
tearDown: function () {// CreateEdgeInteractor 對象結束調用的時候調用的函數 CreateEdgeInteractor.superClass.tearDown.call(this); // 清除連線起點、終點以及連線中間的各個點 this._source = null; this._target = null; this._logicalPoint = null; }
關於鼠標事件以及 touch 事件,我但願這二者在操做上相同,因此直接在鼠標事件中調用的 touch 事件的方法。
繪製連線須要鼠標左鍵先選中一個節點,而後拖動鼠標左鍵不放,移動鼠標到連線的終點節點上,此時一條連線建立完畢。
首先是 touchstart 選中一個節點:
handle_mousedown: function (e) {// 鼠標點下事件 this.handle_touchstart(e); }, handle_touchstart: function (e) {// 開始 touch this._sourceNode = this.getNodeAt(e);// 獲取事件下的節點 if (this._sourceNode) { this._targetNode = null;// 初始化 targetNode this.startDragging(e); this._graphView.addTopPainter(this);// 增長頂層Painter 使用Canvas的畫筆對象自由繪製任意形狀,頂層Painter繪製在拓撲最上面 this._graphView.sm().ss(this._sourceNode);// 設置選中 } }, getNodeAt: function(e){// 獲取事件下的節點 if (ht.Default.isLeftButton(e) && ht.Default.getTouchCount(e) === 1) {// 鼠標左鍵被按下 && 當前Touch手指個數爲1 var data = this._graphView.getDataAt(e);// 獲取事件下的節點 if(data instanceof ht.Node) return data;// 爲 ht.Node 類型的節點 } return null; }
而後手指滑動 touchmove :
handleWindowMouseMove: function (e) { this.handleWindowTouchMove(e); }, handleWindowTouchMove: function (e) {// 手指滑動 var graphView = this._graphView;// 拓撲組件 this.redraw();// 若是不從新繪製矩形區域,那麼容易形成髒矩形 this._logicalPoint = graphView.getLogicalPoint(e);// 獲取事件下的邏輯座標 this._targetNode = this.getNodeAt(e);// 獲取事件下的 edge 的終點 if (this._targetNode) graphView.sm().ss([this._sourceNode, this._targetNode]);// 設置起始和終止節點都被選中 else graphView.sm().ss([this._sourceNode]);// 只選中起始節點 }, redraw: function () { var p1 = this._sourceNode.getPosition(),// 獲取連線起始端的節點的座標 p2 = this._logicalPoint; if (p1 && p2) { var rect = ht.Default.unionPoint(p1, p2);// 將點組合成矩形 ht.Default.grow(rect, 1);// 改變rect大小,上下左右分別擴展 extend 的大小 this._graphView.redraw(rect);// 重繪拓撲,rect參數爲空時重繪拓撲中的全部圖元,不然重繪矩形範圍內的圖元 } }
最後 touchend 建立連線:
handleWindowMouseUp: function (e) { this.handleWindowTouchEnd(e); }, handleWindowTouchEnd: function (e) { if (this._targetNode) { var edge = new ht.Edge(this._sourceNode, this._targetNode);// 建立新的連線節點 if (this._edgeType) edge.s('edge.type', this._edgeType);// 設置連線的類型 this._graphView.dm().add(edge);// 將節點添加進數據容器 this._graphView.sm().ss(edge);// 設置選中您當前連線 } editableFunc();// 繪製結束後 工具條選中「編輯」項 this._graphView.removeTopPainter(this);// 移除頂層畫筆 }
至於還未建立連線以前(也就是說爲選中終止節點),鼠標在拖動的過程當中會建立一條連線,這裏是直接用 canvas 繪製的:
draw: function (g) {// 繪製起點與鼠標移動位置的連線 var p1 = this._sourceNode.getPosition(), p2 = this._logicalPoint; if(p1 && p2){ g.lineWidth = 1; g.strokeStyle = '#1ABC9C'; g.beginPath(); g.moveTo(p1.x, p1.y); g.lineTo(p2.x, p2.y); g.stroke(); } }
這樣,自定義連線類結束!
左側面板組件 ht.widget.Palette 支持自定義樣式及單選、拖拽操做,由 ht.DataModel 驅動,用 ht.Group 展現分組,ht.Node 展現按鈕元素。
展現分組,首先得建立分組和組中的按鈕元素:
function initPalette(palette) {// 加載palette面板組件中的圖元 var nodeArray = ['city', 'equipment']; var nameArray = ['城市', '大型'];// arrNode中的index與nameArr中的一一對應 for (var i = 0; i < nodeArray.length; i++) { var name = nameArray[i]; nodeArray[i] = new ht.Group();// palette面板是將圖元都分在「組」裏面,而後向「組」中添加圖元便可 palette.dm().add(nodeArray[i]);// 向palette面板組件中添加group圖元 nodeArray[i].setExpanded(true);// 設置分組爲打開的狀態 nodeArray[i].setName(name);// 設置組的名字 var imageArray = []; switch(i){ case 0: imageArray = ['symbols/5.json', 'symbols/6.json', 'symbols/叉車.json', 'symbols/公交車.json', 'symbols/人1.json', 'symbols/人2.json', 'symbols/人3.json', 'symbols/樹.json', 'symbols/樹2.json']; break; case 1: imageArray = ['symbols/飛機.json', 'symbols/吊機.json', 'symbols/卡車.json', 'symbols/貨輪.json', 'symbols/龍門吊.json', 'symbols/公園.json']; break; default: break; } setPaletteNode(imageArray, nodeArray[i], palette); } } function setPaletteNode(imageArray, array, palette) {// 建立 palette 上 節點及設置名稱、顯示圖片、父子關係 for (var i = 0; i < imageArray.length; i++) { var imageName = imageArray[i], name = imageName.slice(imageName.lastIndexOf('/')+1, imageName.lastIndexOf('.'));// 獲取最後一個 / 和最後一個.中間的文本,做爲節點的 name createNode(imageName, name, array, palette);// 建立節點,顯示在 palette 面板上 } } function createNode(image, name, parent, palette) {// 建立palette面板組件上的節點 var node = new ht.Node(); palette.dm().add(node);// 將節點添加進 palette 的數據容器中 node.setImage(image);// 設置節點的圖片 node.setName(name);// 設置節點名稱 node.setParent(parent);// 設置節點的父親 node.s({// 設置節點的屬性 'draggable': true,// 若是Node的draggable設爲true,Palette能夠自動處理dragstart,可是dragover和drop事件須要咱們處理 'image.stretch': 'centerUniform',// 圖片的繪製方式爲非失真方式 }); return node; }
建立完後,咱們就要啓用模擬的拖拽事件 handleDragAndDrop(e, state):
palette = new ht.widget.Palette();// 建立一個組件面板 var data; palette.handleDragAndDrop = function(e, state) {// 左側面板組件拖拽功能 if ( state === 'prepare' ) data = palette.getDataAt(e); else if( state === 'begin' || state === 'between' ) {} else { if (!ht.Default.containedInView(e, graphView)) return; // 判斷交互事件所處位置是否在graphView組件之上 var node = new ht.Node();// 拖拽到graphView中就建立一個新的節點顯示在graphView上 node.setImage(data.getImage());// 設置節點上貼圖 node.setName(data.getName());// 設置名稱(爲了顯示在屬性欄中) node.s('label', '');// 在graphView中節點下方不會出現setName中的值,label優先級高於name node.p(graphView.lp(e));// 將節點的位置設置爲graphView事件下的拓撲圖中的邏輯座標,即設置鼠標點下的位置爲節點座標 graphView.dm().add(node);// 將節點添加進graphView中 graphView.sm().ss(node);// 默認選中節點 graphView.setFocus(node);// 設置將焦點彙集在該節點上 editableFunc();// 設置節點爲可編輯狀態而且選中導航欄中的「編輯」 } }
好了,先在你就能夠直接從左側的 palette 面板組件上直接拖拽節點到右側的地圖上的 graphView 拓撲圖。
咱們能夠在 graphView 上進行繪製節點的編輯、繪製連線、繪製直角連線以及繪製多邊形。
在上面基於 GIS 的電信資源管理系統的基礎上我嘗試了增長切換地圖的功能,同時還在導航欄上添加了「地鐵線路圖」,這個地鐵線路圖實現起來也是很是厲害的,下次我會再針對這個地鐵線路圖進行一次詳解,這裏就很少作解釋,來看看我添加後的最終結果:
http://www.hightopo.com/demo/openlayers/
若是有什麼建議或者意見,歡迎留言或者私信我,也能夠直接去 HT for Web(https://hightopo.com/) 官網查閱相關資料。
原文出處:https://www.cnblogs.com/htdaydayup/p/11666654.html