OpenLayers 6 實現百度echarts風格的“空氣質量”散點圖

百度的echarts(現在被Apache收編了)所包含的可視化樣式非常的豐富多彩,是衆多可視化項目必選的框架之一。

除了直方圖等各種圖標之外,echarts也有一些基於地圖(當然是百度地圖)的可視化功能,還有大神將OpenLayers和echarts結合起來做成了現成的組件供調用。但是在實際使用中,我發現echarts地圖應用的交互體驗其實並不是很好,圖形化數據在地圖層之上像是“掛”上去的,在拖動地圖的時候,會出現錯位:

 於是我萌生了一個自己實現這個散點圖動畫效果的想法。通過分析,最終大致上實現了這個散點圖(沒有做交互功能),並且性能還不錯(gif圖幀率有點低,實際還要流暢一些):

分析:

  • OpenLayers渲染點要素是很容易完成的,但是echarts這個動畫的效果不大容易做。
  • 通過觀察,發現每個帶動畫效果的點向外擴散的圈只有三個,總共有5*3=15個圓形渲染的點要素,所以感覺OpenLayers的性能應該跟得上。
  • OpenLayers官方實例有一個類似的動畫效果,是利用render機制實現的,可以拿來借鑑。

實現:

首先準備數據,可以在echarts網站上拷貝出來,這個就不說了。

然後把基本的地圖搭出來,數據讀取出來並做一下初步的處理。

import { Map, View } from 'ol';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import Point from 'ol/geom/Point';
import Feature from 'ol/Feature';
import { fromLonLat } from 'ol/proj';
import { getVectorContext } from 'ol/render';
import {Style, Stroke, Fill, Circle as CircleStyle} from 'ol/style';
import data from './data/scatter.json'
import { easeOut } from 'ol/easing'

let tileLayer = new TileLayer({
  source: new OSM()
})
let map = new Map({
  target: 'map',
  layers: [
    tileLayer
  ],
  view: new View({
    center: [11936406.337013, 3786384.633134],
    zoom: 5
  })
});

var poi = []
data.data.forEach((item, index) => {
  item.coord = data.coord[item.name]
  poi.push(new Feature(new Point(fromLonLat(item.coord))))
  poi[index].set('name', item.name)
  poi[index].set('value', item.value)
  var bdStyle = new Style({
    image: new CircleStyle({
      fill: new Fill({
        color: [128, 0, 128]
      }),
      radius: item.value / 20
    }),
  })
  poi[index].setStyle(bdStyle)
})
poi.sort(function (a, b) {
  return b.get('value') - a.get('value');
})

這個擴散圓圈的動畫不妨來分析一下:如圖所示,每一組動畫都有3-4個不同半徑和透明度的圓環組成,隨着時間動態改變半徑和透明度這兩個屬性,形成了“波動”的動畫。所以要實現這種效果,需要在同一個點位渲染3-4個這樣的圓環,並通過render控制實現關鍵幀(每一次渲染確定的半徑和透明度圓環暫時叫做一個關鍵幀)的不斷變化,最終形成動畫效果。

接着又要祭出render大法了。

首先定義幾個全局變量,用於控制動畫:

var duration = 2000;
var n=3
var flashGeom=new Array(5*n);

每次動畫週期設置爲2秒,然後擴散圓的數量爲3個一組,聲明一個5*n大小的數組,準備用於存放渲染擴散圓的要素。雖然很明顯同一組的3個擴散圓是同一個要素,但是爲了方便記錄關鍵幀每一輪的開始時間,每一個擴散圓都用一個要素來表示,通過要素的自定義屬性來記錄關鍵幀每一輪開始渲染的時間。

tileLayer.on('postrender', evt => {
  var vc = getVectorContext(evt);
  var frameState = evt.frameState;
  poi.forEach((item, index) => {
    vc.drawFeature(item, item.getStyle())
  })

監聽tileLayer的postrender事件,獲取VectorContex對象,獲取到當前幀的狀態;然後迭代要素數組的元素,將數據中的靜態城市點渲染上去。

for (var i = 0; i < 5; i++) {
    for (var j = 0; j < n; j++) {
      if(flashGeom[j+i*n] ==undefined)flashGeom[j+i*n] = poi[i].clone()
      if (flashGeom[j+i*n].get('start')==undefined) flashGeom[j+i*n].set('start',(new Date().getTime())+600*j) ;
      
      var elapsed = frameState.time - flashGeom[j+i*n].get('start')
      if(elapsed >= duration){
        flashGeom[j+i*n].set('start',flashGeom[j+i*n].get('start')+duration);
        elapsed=0
      }

 對Top5的城市開始動態渲染過程:

首先克隆n個要素,作爲擴散圓的點要素,然後設置每個擴散圓的一次循環(循環一次指擴散圓半徑從0向外擴散到消失)的開始時間,然後計算已逝時間;如果此時的已逝時間超過了單次循環的時間duration,則將循環起始時間更新,向後平移已逝時間的長度,同時已逝時間設置爲0。

接下來的事情就順理成章了:

      var elapsedRatio = elapsed / duration ;
      elapsedRatio = elapsedRatio > 0 ? elapsedRatio : 0
      elapsedRatio= elapsedRatio > 1 ? elapsedRatio-1 : elapsedRatio;

      var radius = easeOut(elapsedRatio) * flashGeom[j+i*n].get('value') / 7;
      radius = radius > 0 ? radius : 0;
      var opacity = easeOut(1-elapsedRatio*1.3);
      var style = new Style({
        image: new CircleStyle({
          radius: radius,
          stroke: new Stroke({
            color: 'rgba(128, 0, 128, ' + opacity + ')',
            width: 0.1 + opacity
          })
        })
      });
      vc.drawFeature(flashGeom[j+i*n],style);
    }
  }
  map.render()
})

計算已逝比率,根據這個比率和要素的value,也就是污染值,計算得到圓環的大小。此處的參數都是可以調整的,怎樣美觀怎樣來。然後計算透明度,最後根據這個半徑和透明度製作樣式,並使用這個樣式將要素畫到canvas上。

最後的最後,顯式調用一下render(),進行下一幀的繪製。

完整代碼:

import { Map, View } from 'ol';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import Point from 'ol/geom/Point';
import Feature from 'ol/Feature';
import { fromLonLat } from 'ol/proj';
import { getVectorContext } from 'ol/render';
import {Style, Stroke, Fill, Circle as CircleStyle} from 'ol/style';
import data from './data/scatter.json'
import { easeOut } from 'ol/easing'

let tileLayer = new TileLayer({
  source: new OSM()
})
let map = new Map({
  target: 'map',
  layers: [
    tileLayer
  ],
  view: new View({
    center: [11936406.337013, 3786384.633134],
    zoom: 5
  })
});

var poi = []
data.data.forEach((item, index) => {
  item.coord = data.coord[item.name]
  poi.push(new Feature(new Point(fromLonLat(item.coord))))
  poi[index].set('name', item.name)
  poi[index].set('value', item.value)
  var bdStyle = new Style({
    image: new CircleStyle({
      fill: new Fill({
        color: [128, 0, 128]
      }),
      radius: item.value / 20
    }),
  })
  poi[index].setStyle(bdStyle)
})
poi.sort(function (a, b) {
  return b.get('value') - a.get('value');
})

var duration = 2000;
var n=3
var flashGeom=new Array(5*n);

tileLayer.on('postrender', evt => {
  var vc = getVectorContext(evt);
  var frameState = evt.frameState;
  poi.forEach((item, index) => {
    vc.drawFeature(item, item.getStyle())
  })
  for (var i = 0; i < 5; i++) {
    for (var j = 0; j < n; j++) {
      if(flashGeom[j+i*n] ==undefined)flashGeom[j+i*n] = poi[i].clone()
      if (flashGeom[j+i*n].get('start')==undefined) flashGeom[j+i*n].set('start',(new Date().getTime())+600*j) ;
      
      var elapsed = frameState.time - flashGeom[j+i*n].get('start')
      if(elapsed >= duration){
        flashGeom[j+i*n].set('start',flashGeom[j+i*n].get('start')+duration);
        elapsed=0
      }
      
      var elapsedRatio = elapsed / duration ;
      elapsedRatio = elapsedRatio > 0 ? elapsedRatio : 0
      elapsedRatio= elapsedRatio > 1 ? elapsedRatio-1 : elapsedRatio;

      var radius = easeOut(elapsedRatio) * flashGeom[j+i*n].get('value') / 7;
      radius = radius > 0 ? radius : 0;
      var opacity = easeOut(1-elapsedRatio*1.3);
      var style = new Style({
        image: new CircleStyle({
          radius: radius,
          stroke: new Stroke({
            color: 'rgba(128, 0, 128, ' + opacity + ')',
            width: 0.1 + opacity
          })
        })
      });
      vc.drawFeature(flashGeom[j+i*n],style);
    }
  }
  map.render()
})

render機制多用於製作動畫,項目中常用到動畫的朋友有必要學習一下,便於製作一些深度定製的動畫效果。畢竟人家造好的輪子不一定適合自己。


我在企鵝家的課堂和CSDN學院都開通了《OpenLayers實例詳解》課程,歡迎報名學習。搜索關鍵字OpenLayers就能看到。

 

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