Openlayer 數據可視化開發筆記

前言

很長一段時間,在我的認知裏,對地圖的理解都只是在百度和高德,直到我換了工作崗位後,才知道原來有個很有名的開源地圖庫叫作 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: 需要源代碼請留言


  1. 這一段引用的是卡哥的文章 EPSG是什麼? ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章