前言
很長一段時間,在我的認知裏,對地圖的理解都只是在百度和高德,直到我換了工作崗位後,才知道原來有個很有名的開源地圖庫叫作 Openlayer (與它同級別的還有Leaflet),因爲項目需要,所以開始學習這個庫,這篇文章帶大家走進 Openlayer ,記錄我踩的坑,並推薦給有同樣需求的人。
什麼是數據可視化
講 Openlayer 之前,先給大家說一下什麼時數據可視化,因爲地圖從某種程度上來講也是一種可視化工具。那數據可視化是用某Chart來堆圖嗎?餅圖、散點、柱狀、曲線圖?嚴格意義上講也算是一種,但是可視化遠不止這些圖那麼簡單,看一下百度百科的定義數據可視化 : 是關於數據視覺表現形式的科學技術研究。其中,這種數據的視覺表現形式被定義爲,一種以某種概要形式抽提出來的信息,包括相應信息單位的各種屬性和變量。
它是一個處於不斷演變之中的概念,其邊界在不斷地擴大。主要指的是技術上較爲高級的技術方法,而這些技術方法允許利用圖形、圖像處理、計算機視覺以及用戶界面,通過表達、建模以及對立體、表面、屬性以及動畫的顯示,對數據加以可視化解釋。與立體建模之類的特殊技術方法相比,數據可視化所涵蓋的技術方法要廣泛得多。
在大數據浪潮的今天,數據可視化是一種重要的數據分析和挖掘的手段。它的底層是一套可視化算法及技術實現手段。
什麼是Openlayer
Ok,現在來說一下今天的主角 Openlayer,它是一款可視化地圖開源庫,與它齊名的還有Leaflet,但是本人更傾向於Openlayer,因爲它的API更詳細點,針對初學者還有官方的示例,社區也不小。所以,如果你剛好也在用,或者想用Openlayer開發,請繼續往下看。
最前面
讓我們開始,我們假定現在項目使用的是Vue,下面都是基於在Vue框架下的開發(React其實也差不多)。
Step 1 安裝到項目
: npm i ol --save
我現在用的版本是 "ol": "^5.3.3"
,應該是最新版本了(更新:現在已經到6了,不過改動不大)。然後在項目的入口處引用它的樣式:
import 'ol/ol.css';
ok,這樣就可以在項目中使用了,但是開發一個項目應該會使用Openlayer的很多組件,很多功能,所以我那邊還是開發一個工具類來支持整個項目,像這樣:
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import Feature from 'ol/Feature.js';
import Overlay from 'ol/Overlay';
export default class OpenLayerHelper {
async fun(){
// TODO
}
}
哈哈,是的,其實就是一個類。這樣的好處是統一封裝,不會到處引用。
Step 2 初始化一個地圖
好了,大概思路有了,下面就開始畫一個地圖(利用Map類),有三個基本信息要告訴它,1、Layers,就是初始的層;2、Target,也就是目標Dom,告訴Map地圖需要在哪個頁面的元素上畫;3、設定一個View,它包括Zoom和中心點座標,還有其他參數,詳見View 的 API。具體代碼如下:
constructor(id) {
this.map = null;
this.raster = new TileLayer({
source: new OSM()
});
this.source = new VectorSource();
this.vector = new VectorLayer({
source: this.source,
style: new Style({
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)'
}),
stroke: new Stroke({
color: '#b40000',
width: 6
}),
image: new CircleStyle({
radius: 7,
fill: new Fill({
color: '#ffcc33'
})
})
})
});
this.id = id;
}
async init(){
this.map = new Map({
layers: [this.raster, this.vector],
target: this.id,
view: new View({
center: [22.53558, 113.960649],
zoom: 10
})
});
}
在類的Constructor方法中,初始化傳入了id(這個ID就是它的Target),然後利用 Tile 類 新建了一個層Layer,它其實是一個底圖基類(底圖基類可以替換的,後面會講到),還需要一個基本的交互層,用來進行對地圖的操作(比如,畫圖形,路徑和樣式等等),這個層就是 Vector ,在這裏我給它定義了 Source 和樣式。最後就是設置一個 View ,並且設置了基本的中心點和 Zoom。最後一個基本的地圖就生成了,效果如下圖:
嗯,地圖已經生成了,不過,好像有什麼不對,這個文字不認識,它是不同國家顯示不同國家的語言。那要是我要生成統一語言的地圖怎麼辦呢?
Step 3 切換中文底圖
文章開始說到,基本認知都只在百度和高德,那有沒有可能用它們的底圖呢 ?答案是肯定的。網上已經有很多關於集成百度底圖的教程,這裏我就把我用的貢獻給大家,邏輯都是一樣的:
createBaiduTile() {
let extent = [72.004, 0.8293, 137.8347, 55.8271];
let baiduMercator = new Projection({
code: 'baidu',
extent: applyTransform(extent, projzh.ll2bmerc),
units: 'm'
});
addProjection(baiduMercator);
addCoordinateTransforms('EPSG:4326', baiduMercator, projzh.ll2bmerc, projzh.bmerc2ll);
addCoordinateTransforms('EPSG:3857', baiduMercator, projzh.smerc2bmerc, projzh.bmerc2smerc);
let bmercResolutions = new Array(19);
for (let i = 0; i < 19; ++i) {
bmercResolutions[i] = Math.pow(2, 18 - i);
}
let urls = [0, 1, 2, 3, 4].map(function () {
return "http://online1.map.bdimg.com/onlinelabel/?qt=tile&x={x}&y={y}&z={z}&styles=pl&scaler=1&p=1";
});
let baidu = new TileLayer({
source: new XYZ({
projection: 'baidu',
maxZoom: 18,
tileUrlFunction: function (tileCoord) {
let x = tileCoord[1];
let y = tileCoord[2];
let z = tileCoord[0];
let hash = (x << z) + y;
let index = hash % urls.length;
index = index < 0 ? index + urls.length : index;
return urls[index].replace('{x}', x).replace('{y}', y).replace('{z}', z);
},
tileGrid: new TileGrid({
resolutions: bmercResolutions,
origin: [0, 0],
extent: applyTransform(extent, projzh.ll2bmerc),
tileSize: [256, 256]
})
})
});
return baidu;
}
然後,在地圖初始化那裏就把原來的底圖換成百度底圖就行,代碼如下:
async init(){
let baiduLayer = this.createBaiduTile();
this.map = new Map({
// layers: [this.raster, this.vector],
layers: [baiduLayer, this.vector],
target: this.id,
view: new View({
center: [22.53558, 113.960649],
zoom: 10
})
});
}
最後,看一下效果 ,可以看到,現在除了中國外,其他國家也都是中文顯示了。
它的基本邏輯就是替換掉 TileLayer ,內部是一個 XYZ 的Source。好了,現在地圖是中文的了,現在我想加個座標上去。你會發現,又不對了,明明輸入的是北京,去定位到其他地方???一臉黑…
Step 4 轉換座標
通常,國內開發軟件的話,基本上都是用百度,或者高德來選點。第3步中,已經把底圖換成中文的百度底圖了,那座標應該也可以轉換。在這之前,咱們先了解一下這個座標。 Openlayer 官網上有個例子,大家可以先看一下 EPSG:4326 ,這個EPSG是什麼呢?EPSP的英文全稱是European Petroleum Survey Group,中文名稱爲歐洲石油調查組織。這個組織成立於1986年,2005年併入IOGP(InternationalAssociation of Oil & Gas Producers),中文名稱爲國際油氣生產者協會。它爲每個地區都繪製了地圖,但是由於座標系不同,所以地圖也各不相同。1 有個專門的網站可以查看EPSG,epsg.io ,有興趣大家可以去搜一下,這裏咱們要做的就是把座標系轉換成正常的百度地圖用的格式就行。
這裏咱們要用到一個非常好用的庫:coordtransform
它在轉換EPSG之前,把座標先轉換成百度支持的格式,然後用transform轉換。比如百度地圖的就是要 “ EPSG:3857 ”,所以,問題就簡單了,在得到座標時,把它轉一下就行,具體如下:
import coordtransform from 'coordtransform';
let bd09togcj02 = coordtransform.bd09togcj02(local[0], local[1]);
let gcj02towgs84 = coordtransform.gcj02towgs84(bd09togcj02[0], bd09togcj02[1]);
let coordinate = transform(gcj02towgs84, 'EPSG:4326', 'EPSG:3857'); // 轉換座標 經緯
現在,地圖上的點就是準確的了,如下圖:
Step 5 畫畫
好了,現在有了精準定位的地圖,你肯定還想做點什麼 。是的, Openlayer 的功能可強大了,可以畫圖形,路徑等等,但是要做這些這前,首先要拿到一支可以畫畫的筆,就跟小朋友畫畫一樣,所以,我們要初始化畫筆。不多說,上代碼 :
addInteractions(type) {
if (this.draw !== null) {
this.draw.un('drawend');
}
this.draw = new Draw({
source: this.source,
type: type,
style: new Style({
fill: new Fill({
color: '#a4d9ff66'
}),
stroke: new Stroke({
color: '#1565c0',
width: 1
}),
})
});
this.draw.on('drawstart', (event) => { // set color after select
let s = new Style({
fill: new Fill({
color: '#a4d9ff66'
}),
stroke: new Stroke({
color: '#1565c0',
width: 1
}),
});
event.feature.setStyle(s);
event.feature.setId(this.curId);
});
this.map.addInteraction(this.draw);
this.snap = new Snap({ source: this.source });
this.map.addInteraction(this.snap);
return null;
}
這裏利用的就是 Draw類 定義一個Draw實例,然後監聽它的開始事件,並把它添加到Map實例下,還有一點,所以的畫的操作都是作爲 Interaction 相互作用 來添加到地圖實例的。Draw支持的Type有:‘Point’, ‘LineString’, ‘LinearRing’, ‘Polygon’, ‘MultiPoint’, ‘MultiLineString’, ‘MultiPolygon’, ‘GeometryCollection’, ‘Circle’ 9種,很豐富,本例中,我用的是’Polygon’ 多邊形,具體如下:
Step 6 特徵元素
在第5步中,我們添加了一個多邊形, Openlayer 把這個多邊形叫做 Feature,所有Draw支持的Type,畫出來都是一個 Feature 。所以,當我們想刪除掉已經畫的 Feature 時,可以像這樣:
redoDraw(id) {
this.polygonFeatures.forEach((item) => {
let source = this.vector.getSource();
if (id == item.id) {
item.feature.setStyle( // 爲了隱藏
new Style({
image: new CircleStyle({ opacity: 0 }),
})
);
let fid = item.feature.getId();
let back = source.getFeatureById(fid);
if (back != null) {
source.removeFeature(back);
}
}
});
return;
}
我們假定你之前添加的Feature都已經添加到 polygonFeatures 中,這個時候就可以遍歷得到那個 Feature 並根據ID刪除(注意我寫的方式,直接用你保存的特徵去刪除是刪除不了的,必須用ID去查找,再刪除)。同時這裏會有一個BUG,已經刪除的元素還顯示在地圖上,所以我加了一段代碼 ,添加一個沒用的Image並把它Opacity設置爲0,這樣就可以把已經刪除的 Feature 隱藏。
Step 7 保存特徵
回看第6步中,polygonFeatures 。特徵是怎麼被保存進去的呢?
其實,API中並沒有告訴你怎麼保存,只是提供了一些看似不相干的方法,需要你去實踐。 很慶幸的是,本人已經實踐過了。回到第5步,咱們設置了畫筆,這個時候,官方留了個回調函數,用於咱們畫筆畫完時。這個方法就是 drawend ,在這個回調函數中,組件給了你很多信息,其中就包括當前它畫完的Feature信息。利用 GeoJSON 可以把當前的特徵轉換成Json格式,然後你自然就可以存儲了。下面是我寫的部分代碼:
首先在設置畫筆的時候要監聽:
this.addDrawEndEventListener(this.draw);
然後是具體代碼:
addDrawEndEventListener(draw) {
draw.on('drawend', async (evt) => {
let GEOJSON_PARSER = new GeoJSON();
let currentZone = GEOJSON_PARSER.writeFeatureObject(evt.feature);// 得到區域
let res = this.polygonFeatures.findIndex(item => this.curId === item.id);
if (res !== -1) { // 當前id說明還在修改中,
this.polygonFeatures[res].feature = evt.feature;
this.polygonFeatures[res].geojson = currentZone;
} else {// 是新的特徵,重新push
this.polygonFeatures.push({ id: this.curId, feature: evt.feature, geojson: currentZone });
}
this.map.removeInteraction(this.draw);// A
this.map.removeInteraction(this.snap);// B
return true;
});
return null;
}
curId 是當前上下文id,可以根據實際情況替換,每次畫完之後可以根據實際情況執行A行和B行。
Step 8 畫路徑
前面幾節,咱們弄清楚了怎麼畫特徵,但是,有一個特殊的特徵必須要單獨拿出來說一下,因爲我相信有很多同學都會有這種需求,那就是畫路徑。其實,路徑雖爲特徵,但是畫法卻跟普通特徵不同。實際上它是把多個點連接成一條路徑,通過 LineString 來畫。但是這裏還需要涉及到一個轉碼的過程(在第4步中文章有寫),這裏咱們還是以百度爲例,假定咱們的路徑數據是下面這樣:
{
"path": "121.45728226951,31.057582190748;121.45780355619,31.056256219353;121.45819467841,31.05523208799;121.4582649259,31.055001382341;121.45853576502,31.054288457325;121.45867617017,31.054046921623;121.4588667906,31.053684810262;121.45935852303,31.052429165577;121.46004114539,31.050441059497;121.46019179122,31.050049311415;121.4603022828,31.049748054872;121.46047296085,31.049276019896;121.46054329817,31.049105242118;121.46056342021,31.049045067628;121.46072412707,31.048633126578;121.46110581708,31.047648281616;121.4612364307,31.047326675259;121.461296707,31.047176004082;121.46140719858,31.046904671603;121.46174882416,31.045990272299;121.46185931574,31.045718859057;121.46225124643,31.044783641471;121.46233164478,31.04458261218;121.46236182784,31.044512302103;121.46249253129,31.044190607696;121.46251265334,31.044130430068;121.46254301606,31.043770136986"
}
那路徑就可以這樣畫出來:
addLines(data) {
let locations = data.path.split(";");
locations = locations.map((item) => {// 轉換座標
let local = item.split(",");
let bd09togcj02 = coordtransform.bd09togcj02(parseFloat(local[0]), parseFloat(local[1]));
let gcj02towgs84 = coordtransform.gcj02towgs84(bd09togcj02[0], bd09togcj02[1]);
return gcj02towgs84;
});
let polyline = new LineString(locations);
polyline.transform('EPSG:4326', 'EPSG:3857');
let feature = new Feature({
geometry: polyline,
name: "diy",
});
feature.setStyle(new Style({
stroke: new Stroke({
color: '#FC2828',
width: 6
}),
}));
this.source.addFeature(feature);
}
記得給已經畫的路徑設置樣式。並把它添加到當前的Source上下文。這樣就會在這個Layer中顯示出來:
示例中只畫了一小段路徑,哈哈,實現就行(關於路徑 ,其實還有Hover高亮和顏色控制的問題,這裏就不再延伸了)。
Step 9 畫熱力圖
熱力圖官方有一個專門的例子,可以參考Earthquakes Heatmap ,我也不班門弄斧,只講相關的參數:
let HeatmapLayer = new Heatmap({
source: new VectorSource(),
radius: 18,
shadow: 500,
blur:45,
zIndex: 1
});
它的原理很簡單,就是把很多特徵量化,給它們加半徑和陰影,這樣在地圖上看的話,特徵的分佈就呈現熱力圖的效果,但是最爲關鍵的是這個Blur (模糊度),越模糊,特徵的邊界就越不明顯,熱力圖就更像是一個整體。
Step 10 生成Tooltips
好了,告訴我怎麼給地圖加ToolTips,這應該是一個很普遍的需求了。很簡單,就是生成一個Dom然後插入到指定位置,與地圖通過事件回調來聯動。可以參考下面代碼:
createToolTips(coordinate, name) {
let span = document.createElement('span');
span.className = "tooltips-diy";
document.body.append(span);
let overlay = new Overlay({
element: span,
offset: [-15, -45],
positioning: 'top'
});
this.map.addOverlay(overlay);
overlay.setPosition(coordinate);
span.innerHTML = name;
}
這裏要記住,下次添加的時候要把這個去掉,不然會重複,這個我本來想會有更官方的解決方案,但是我沒有找到,哈哈,不過這樣也行的通。
結語
純粹的技術博客,分享給那些跟我一樣被這類需求困擾的同學們,有問題歡迎私信或者留言。
ps: 需要源代碼請留言