百度地圖Canvas實現十萬CAD資料秒級載入
背景
前段時間工作室接到一個與地圖相關的專案,我作為專案組成員主要負責地圖方面的設計和開發。由於地圖部分主要涉及的是前端頁面的顯示,作為一名Java後端的小白,第一次寫了這麼多HTML和JavaScript。
專案大概是需要將一張CAD的圖(匯出大概三十萬條資料)疊加在地圖上,在接Canvas之前考慮了很多種方案,最後都否定了。首先我們想利用百度地圖原生的JavaScript API實現線和點的載入,但是經過測試,當資料達到2000左右,載入時間就已經達到了數十秒,後來直接測試了一萬條資料,瀏覽器直接卡死了,這種方案很快就被否定了。然後我們又準備採用分割靜態圖的方法,將整個CAD圖分割成地圖瓦片,作為覆蓋圖層疊加在原來地圖之上,在這一步中,由於CAD圖的資訊涉及到整個城市,資訊量非常巨大,幾乎沒有找到合適軟體能夠匯出一張這麼大的圖。後來仔細研究需求文件後又發現需要針對圖的資訊做操作,然後這種方案將近完成的時候被否定了。最後,偶然在Github上看到:https://github.com/lcosmos/map-canvas 這個實現颱風軌跡,這個資料量非常龐大,當時開啟時,看到這麼多資料載入很快,感到有點震驚,然後自己研究了一番,發現作者採用的是Canvas作為百度的自定義覆蓋層,說幹就幹,自己嘗試寫出了第一個版本,寫上Ajax請求,效果十分震撼,將近秒級載入,加了計時器測試了一番效能,發現繪畫只花了1ms左右,主要延時都在請求延時(90M左右的資料),記憶體佔用也非常少,這下放心多了,專案基本也可以完成了,然後當然是搗鼓傳輸延時,GZip等都用上了,最後也有了極大優化,客戶看後覺得還十分不錯。
還準備繼續更新CAD資料提取、UTM座標轉WGS84、CAD校準等系列分享
效果圖
(由於專案涉及國家機密,部分細節和圖片不便於展示,但已經可以完成標題需求)
具體實現
(由於專案涉及國家機密,部分細節和圖片不便於展示,但已經可以完成標題需求)
HTML測試頁:
<!DOCTYPE html> <html> <head> <title>百度地圖Canvas海量折線</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"> <link rel="stylesheet" href="css/style.css"> <script type="text/javascript" src="https://api.map.baidu.com/api?v=2.0&ak=nuWah68S1WieW2AEwiT8T3Ro&s=1"></script> <script type="text/javascript" src="js/jquery.min.js"></script> </head> <body> <div id="map"></div> <script type="text/javascript" src="pointLine.js"></script> <script type="text/javascript"> var map = new BMap.Map('map', { minZoom: 5 }); map.centerAndZoom(new BMap.Point(112.954699, 27.851256), 13); map.enableScrollWheelZoom(true); map.setMapStyle({ styleJson: styleJson }); $.getJSON('line.json', function (result) { var pointLine = new PointLine(map, { //線條寬度 lineWidth: 2, //線條顏色 lineStyle: '#F9815C', //資料來源 data: result, //事件 methods: { click: function (e, name) { console.log('你當前點選的是' + name); }, // mousemove: function (e, name) { //console.log('你當前點選的是' + name); // } } }); }) </script> </body> </html>
核心JavaScript:pointLine.js
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.PointLine = factory()); }(this, (function () { 'use strict'; function CanvasLayer(options) { this.options = options || {}; this.paneName = this.options.paneName || 'labelPane'; this.zIndex = this.options.zIndex || 0; this._map = options.map; this._lastDrawTime = null; this.show(); } CanvasLayer.prototype = new BMap.Overlay(); CanvasLayer.prototype.initialize = function (map) { this._map = map; var canvas = this.canvas = document.createElement('canvas'); var ctx = this.ctx = this.canvas.getContext('2d'); canvas.style.cssText = 'position:absolute;' + 'left:0;' + 'top:0;' + 'z-index:' + this.zIndex + ';'; this.adjustSize(); this.adjustRatio(ctx); map.getPanes()[this.paneName].appendChild(canvas); var that = this; map.addEventListener('resize', function () { that.adjustSize(); that._draw(); }); return this.canvas; }; CanvasLayer.prototype.adjustSize = function () { var size = this._map.getSize(); var canvas = this.canvas; canvas.width = size.width; canvas.height = size.height; canvas.style.width = canvas.width + 'px'; canvas.style.height = canvas.height + 'px'; }; CanvasLayer.prototype.adjustRatio = function (ctx) { var backingStore = ctx.backingStorePixelRatio || ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; var pixelRatio = (window.devicePixelRatio || 1) / backingStore; var canvasWidth = ctx.canvas.width; var canvasHeight = ctx.canvas.height; ctx.canvas.width = canvasWidth * pixelRatio; ctx.canvas.height = canvasHeight * pixelRatio; ctx.canvas.style.width = canvasWidth + 'px'; ctx.canvas.style.height = canvasHeight + 'px'; // console.log(ctx.canvas.height, canvasHeight); ctx.scale(pixelRatio, pixelRatio); }; CanvasLayer.prototype.draw = function () { var self = this; var args = arguments; clearTimeout(self.timeoutID); self.timeoutID = setTimeout(function () { self._draw(); }, 15); }; CanvasLayer.prototype._draw = function () { var map = this._map; var size = map.getSize(); var center = map.getCenter(); if (center) { var pixel = map.pointToOverlayPixel(center); this.canvas.style.left = pixel.x - size.width / 2 + 'px'; this.canvas.style.top = pixel.y - size.height / 2 + 'px'; this.dispatchEvent('draw'); this.options.update && this.options.update.call(this); } }; CanvasLayer.prototype.getContainer = function () { return this.canvas; }; CanvasLayer.prototype.show = function () { if (!this.canvas) { this._map.addOverlay(this); } this.canvas.style.display = 'block'; }; CanvasLayer.prototype.hide = function () { this.canvas.style.display = 'none'; //this._map.removeOverlay(this); }; CanvasLayer.prototype.setZIndex = function (zIndex) { this.canvas.style.zIndex = zIndex; }; CanvasLayer.prototype.getZIndex = function () { return this.zIndex; }; var tool = { merge: function merge(settings, defaults) { Object.keys(settings).forEach(function (key) { defaults[key] = settings[key]; }); }, //計算兩點間距離 getDistance: function getDistance(p1, p2) { return Math.sqrt((p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1])); }, //判斷點是否線上段上 containStroke: function containStroke(x0, y0, x1, y1, lineWidth, x, y) { if (lineWidth === 0) { return false; } var _l = lineWidth; var _a = 0; var _b = x0; // Quick reject if (y > y0 + _l && y > y1 + _l || y < y0 - _l && y < y1 - _l || x > x0 + _l && x > x1 + _l || x < x0 - _l && x < x1 - _l) { return false; } if (x0 !== x1) { _a = (y0 - y1) / (x0 - x1); _b = (x0 * y1 - x1 * y0) / (x0 - x1); } else { return Math.abs(x - x0) <= _l / 2; } var tmp = _a * x - y + _b; var _s = tmp * tmp / (_a * _a + 1); return _s <= _l / 2 * _l / 2; } }; var PointLine = function PointLine(map, userOptions) { var self = this; self.map = map; self.lines = []; self.pixelList = []; //預設引數 var options = { //線條寬度 lineWidth: 1, //線條顏色 lineStyle: '#F9815C' }; //全域性變數 var baseLayer = null, width = map.getSize().width, height = map.getSize().height; function Line(opts) { this.name = opts.name; this.path = opts.path; } Line.prototype.getPointList = function () { var points = [], path = this.path; if (path && path.length > 0) { path.forEach(function (p) { points.push({ name: p.name, pixel: map.pointToPixel(p.location) }); }); } return points; }; Line.prototype.draw = function (context) { var pointList = this.pixelList || this.getPointList(); context.save(); context.beginPath(); context.lineWidth = options.lineWidth; context.strokeStyle = options.lineStyle; context.moveTo(pointList[0].pixel.x, pointList[0].pixel.y); for (var i = 0, len = pointList.length; i < len; i++) { context.lineTo(pointList[i].pixel.x, pointList[i].pixel.y); } context.stroke(); context.closePath(); context.restore(); }; //底層canvas渲染,標註,線條 var brush = function brush() { var baseCtx = baseLayer.canvas.getContext('2d'); if (!baseCtx) { return; } addLine(); baseCtx.clearRect(0, 0, width, height); self.pixelList = []; self.lines.forEach(function (line) { self.pixelList.push({ name: line.name, data: line.getPointList() }); line.draw(baseCtx); }); }; var addLine = function addLine() { if (self.lines && self.lines.length > 0) return; var dataset = options.data; dataset.forEach(function (l, i) { var line = new Line({ name: l.name, path: [] }); l.data.forEach(function (p, j) { line.path.push({ name: p.name, location: new BMap.Point(p.Longitude, p.Latitude) }); }); self.lines.push(line); }); }; self.init(userOptions, options); baseLayer = new CanvasLayer({ map: map, update: brush }); this.clickEvent = this.clickEvent.bind(this); this.bindEvent(); }; PointLine.prototype.init = function (settings, defaults) { //合併引數 tool.merge(settings, defaults); this.options = defaults; }; PointLine.prototype.bindEvent = function (e) { var map = this.map; if (this.options.methods) { if (this.options.methods.click) { map.setDefaultCursor("default"); map.addEventListener('click', this.clickEvent); } if (this.options.methods.mousemove) { map.setDefaultCursor("default"); map.addEventListener('mousemove', this.clickEvent); } } }; PointLine.prototype.clickEvent = function (e) { var self = this, lines = self.pixelList; if (lines.length > 0) { lines.forEach(function (line, i) { for (var j = 0; j < line.data.length; j++) { var beginPt = line.data[j].pixel; if (line.data[j + 1] == undefined) { return; } var endPt = line.data[j + 1].pixel; var curPt = e.pixel; var isOnLine = tool.containStroke(beginPt.x, beginPt.y, endPt.x, endPt.y, self.options.lineWidth, curPt.x, curPt.y); if (isOnLine) { self.options.methods.click(e, line.name); return; } } }); } }; return PointLine; })));
測試資料:line.json
ofollow,noindex">https://files.cnblogs.com/files/lcosmos/line.json.zip