OpenLayers 圖層(Layers) 詳解

聲明:文章爲本人原創,轉載或使用請註明作者和出處!!謝謝!

文中代碼可以在 https://github.com/QingyaFan/data-visualization 找到。


圖層分層示意

如果對現在的地圖技術瞭解的少,看到地圖,可能會覺得地圖就是一張圖片,這張圖片可以縮放、移動。這種看法不能說不對,但只是表面現象,實際上地圖是由一個或多個圖層組成的,不同的圖層存儲不同類型的地物,如線狀圖層存儲線狀地物:道路、河流;點狀圖層存儲POI信息:建築、店鋪等;面狀地物存儲諸如公園等有一定範圍的地物。當然,也有可能不按照地物本身的幾何特徵劃分圖層的,比如現在的展示擁堵情況的圖層,展示紅綠燈位置的圖層。

在 OpenLayers 中,圖層是使用 layer 對象表示的,主要有 WebGLPoints Layer熱度圖(HeatMap Layer)圖片圖層(Image Layer)切片圖層(Tile Layer)矢量圖層(Vector Layer) 五種類型,它們都是繼承 Layer 類的。

1、Layer 類


OpenLayers 初始化一幅地圖(map),至少需要一個可視區域(view),一個或多個圖層( layer), 和 一個地圖加載的目標 HTML 標籤(target),其中最重要的是圖層( layer)。

這裏 可以看到 layer 基類的定義,類的描述如下:

 * @classdesc
 * Base class from which all layer types are derived. This should only be instantiated
 * in the case where a custom layer is be added to the map with a custom `render` function.
 * Such a function can be specified in the `options` object, and is expected to return an HTML element.
 *
 * A visual representation of raster or vector map data.
 * Layers group together those properties that pertain to how the data is to be
 * displayed, irrespective of the source of that data.

layer 是各種圖層的基類,只用於讓子類型繼承和實現,一般自身不會實例化。主要功能是對矢量數據柵格數據的可視化。圖層渲染結果的樣式,主要與數據渲染方式有關,與數據源關係不大。

1.1 屬性

初始化時,所有圖層類型都具有的參數,如下:

  • source,指定了圖層的數據來源,圖層作用是以一定的樣式渲染數據,source則指定了數據;
  • className,圖層各個元素的樣式;
  • opacity,透明度,默認爲 1 ,即完全透明;
  • visible,是否可見;
  • zIndex,圖層的疊加次序,默認是0,最底層,如果使用setMap方法添加的圖層,zIndex值是Infinity,在最上層;
  • extent,圖層渲染的區域,即瀏覽器窗口中可見的地圖區域。extent 是一個矩形範圍,格式是[number, number, number, number] 分別代表 [left, bottom, right, top] 。爲了提升渲染效率和加載速度,extent範圍之外的瓦片是不會請求的,當然也不會渲染;
  • minResolution,圖層可見的最小分辨率;
  • maxResolution,圖層可見的最大分辨率;
  • minZoom,圖層可見的最小zoom level;
  • maxZoom,圖層可見的最大zoom level

source是一個非常重要的參數,圖層中渲染的數據來自於source參數指定的地址,可能是文件,可能是返回地理數據的網絡API,不同的source對象類型不一樣,source都有哪些可以參考這篇文章。zoom的邊界情況也需要注意:是 (minZoom, maxZoom],圖層可見的zoom level大於minZoom,小於等於maxZoom。這與resolution的情況剛好相反[minResolution, maxResolution)。

1.2 事件觸發

有的同學問:我想在圖層加載完成時,做一些事情,如何知道圖層加載完成呢?圖層初始化時,我們可以指定很多hook,用以當某些事件觸發時做出一定的動作,這些事件中有一個postrender,會在圖層渲染完成後觸發,我們可以對這個事件傳入回調。類似的事件還有prerender

包含的方法其實沒有什麼好說的,一般就是對屬性的getter和setter,詳細的列表可以參考 這裏

1.3 行爲

我們看到了每種圖層都有source可以讓我們指定數據來源,那數據是如何變成我們看到的效果的?這就涉及到renderer了,每種圖層類型都有一個隱式的屬性:renderer_,這個我們從Layer基類的屬性定義中可以看到:

/**
 * @private
 * @type {import("../renderer/Layer.js").default}
 */
this.renderer_ = null;

Layer基類還定義了相關方法:

/**
 * Getthe renderer for this layer.
 * @return {import("../renderer/Layer.js").default} The layer renderer.
 */
getRenderer() {
  if (!this.renderer_) {
    this.renderer_ = this.createRenderer();
  }
  return this.renderer_;
}

我們看到,如果圖層的renderer_還沒有得到初始化,會調用createRenderer方法初始化renderer_,在基類中是不做任何行爲的,把初始化的細節下放到了各個具體的圖層子類中實現,類似於Golang中的接口,C++中虛函數實現的多態。每種圖層類型對應的renderer如下:

  • Heatmap,爲了提升性能,使用了和WebGLPoints圖層一樣的渲染器:WebGLPointsLayerRenderer
  • WebGLPointsLayer,渲染器爲WebGLPointsLayerRenderer
  • ImageLayer,渲染器爲CanvasImageLayerRenderer
  • TileLayer,渲染器爲CanvasTileLayerRenderer
  • VectorLayer,渲染器爲CanvasVectorLayerRenderer
  • VectorImageLayer,渲染器爲CanvasVectorImageLayerRenderer
  • VectorTileLayer,渲染器爲CanvasVectorTileLayerRenderer

以上各類圖層使用的Renderer看來,openlayers當前(2020/04)主要使用H5的Canvas和WebGL進行渲染,目前來看,WebGL的比重會逐漸增加,從類似的mapboxgl.js或deck.gl可以看出來。

2、各種圖層類型


從渲染髮生的地方來看,openlayers的圖層主要分爲兩類:Vector(矢量)和Raster(柵格),矢量圖層是指在渲染髮生在瀏覽器的圖層,source返回的數據類型是矢量,如geojson的座標串;柵格圖層則是由服務器渲染,返回到瀏覽器的是一張張的瓦片圖片,柵格圖層主要是展示。

矢量圖層類型有:

  • Graticule,地圖上覆蓋的網格標尺圖層;
  • HeatMap,熱力圖;
  • Vector,矢量圖
  • VectorImage,單張的矢量圖層
  • VectorTile,矢量瓦片圖層
  • WebGLPoints,WebGL渲染的海量點圖層

柵格圖層類型較爲簡單,只有Tile圖層。

所有的圖層都繼承了 Layer 類,監聽和觸發的事件都在 ol.render.Event 中定義,共用的屬性和狀態都是在 layerbase 中定義的,它們除了從ol.layer.Layer 類繼承而來的參數外,還定義了自己的屬性和方法。下面我們來分別看看這幾個圖層類型。

注:不管使用什麼圖層類型,初始化 map 同時,如果不明確指定 control 對象,那麼就會默認包含 縮放鼠標拖拽 功能,關於這個 Control 對象,在後面的博客中會講到,現在認爲 Control 就是一個控制與地圖交互的工具就好。

2.1 WebGLPoint Layer

WebGLPoint Layer 是由 WebGL 作爲渲染引擎的點圖層,衆所周知,WebGL在渲染大量數據(>10k)效率明顯優於Canvas或SVG,所以對於有大數據量前端渲染需求的,WebGL作爲渲染引擎幾乎是唯一的選擇。以前openlayers一直沒有webgl作爲渲染引擎的圖層類型,雖然openlayer自從3.x重構以來就一直將支持三維作爲目標,但是進展較慢,對比隔壁mapboxgl.js,進度差的不是一點。嚴格來說,openlayers和leaflet是一個時代的產品,mapboxgl.js很早支持三維,且是leaflet的作者寫的“下一代”前端地圖可視化庫。

WebGLPoint Layer本質上是矢量圖層,在瀏覽器端渲染,然而,問題是:如果數據量較大,從服務器傳來瀏覽器將會耗費很長時間,雖然只需要傳輸一次,雖然渲染快,但是用戶感受到的是一直在等待。如果傳輸需要2分鐘,渲染只需10ms,用戶感知到的仍然是等了2分鐘渲染。所以以當前的網速來看,可能更適合內網應用。當然,如果5G時代來的足夠快,也可能真火了。

那麼有的同學會問,我在服務器端渲染不比webgl性能高,它不香嗎?當然香,但是服務器與客戶端是1對多的關係,每個客戶端都需要服務器渲染,併發量高了,服務器垮不垮?又有小夥伴說了,切片不就是解決這個問題的嗎?對,但現在需求往往是樣式隨時會變,緩存了切片,樣式一變,又要重新切,意義不大。矢量切片出現不就是這個問題的證據麼?

實例

由於WebGL的優勢是大數據量下的渲染性能,所以隨意改變樣式重新渲染代價賊低,對於海量數據的個性化渲染也成爲了可能,WebGLPoint Layer的style也變成了一個非常實用的功能。先來個例子。

mounted() {
    this.map = new Map({
        layers: [
            new TileLayer({
                source: new OSM()
            })
        ],
        target: document.getElementById('map'),
        view: new View({
            center: [0, 0],
            zoom: 2
        })
    });

    let vectorSource = new Vector({
        url: 'https://openlayers.org/en/latest/examples/data/geojson/world-cities.geojson',
        format: new GeoJSON()
    });

    let pointLayer = new WebGLPointsLayer({
        source: vectorSource,
        style: {
            "symbol": {
                "symbolType": "circle",
                "size": [
                    "interpolate",
                    [
                        "linear"
                    ],
                    [
                        "get",
                        "population"
                    ],
                    40000,
                    8,
                    2000000,
                    28
                ],
                "color": "#006688",
                "rotateWithView": false,
                "offset": [
                    0,
                    0
                ],
                "opacity": [
                    "interpolate",
                    [
                        "linear"
                    ],
                    [
                        "get",
                        "population"
                    ],
                    40000,
                    0.6,
                    2000000,
                    0.92
                ]
            }
        }
    });

    this.map.addLayer(pointLayer);
}

圖層中指定的數據源world-cities.geojson包含了19321個點要素,Style中指定的是根據每個點要素包含的人口數量屬性決定要素的半徑大小和要素展示的透明度。如果使用Canvas,性能會查差一些,雖然差的不多,但是隨着數據量的繼續變大,WebGL還是可以輕鬆應對,Canvas就到極限了。最終的渲染結果是這樣的:

WebGLPoint Layer

2.2 Heatmap Layer

將矢量數據渲染成熱度圖的類,繼承了 ol.layer.Vector 類,ol.layer.Vector 繼承了ol.layer.Layer 類, 額外的參數是 olx.layer.HeatmapOptions ,其定義如下:

/**
 * @enum {string}
 */
ol.layer.HeatmapLayerProperty = {
  BLUR: 'blur',
  GRADIENT: 'gradient',
  RADIUS: 'radius'
};

Heatmap 圖層比起其它類型的圖層多三個屬性,常用的是 blur 和 radius,這兩個屬性什麼作用呢,我們可以調整一下看看效果:

熱度圖

沒錯,blur 控制圓點的邊緣,對邊緣做模糊化; radius 則規定了圓點的半徑。:並不是點,而是圓。

實例

首先創建一個 heatmaplayer 對象:

var vector = new ol.layer.Heatmap({
  source: new ol.source.KML({
    url: 'data/kml/2012_Earthquakes_Mag5.kml',
    projection: 'EPSG:3857',
    extractStyles: false
  }),
  blur: parseInt(blur.value, 10),
  radius: parseInt(radius.value, 10)
});

這裏 heatmap 使用KML格式,本地文件data/kml/2012_Earthquakes_Mag5.kml 作爲 heatmap 的來源,數據是2012年全球地震發生的位置和震級等簡單的描述信息,然後將 heatmap 圖層加到 map 中:

map = new ol.Map({  //初始化map
    target: 'map',
    layers: [
      new ol.layer.Tile({
        source: new ol.source.MapQuest({layer: 'sat'})
      }),
      heatmap
    ],
    view: new ol.View({
      center: ol.proj.transform([37.41, 8.82], 'EPSG:4326', 'EPSG:3857'),
      zoom: 4
    })
}); 

查看運行效果:

熱力圖

2.3 Image Layer

主要是指服務器端渲染的圖像,可能是已經渲染好的圖像,或者是每一次請求,都根據請求內容定製化地生成一幅圖片,該圖層類型支持任意的範圍和分辨率。

實例

首先實例化一幅圖片圖層:

/**
 * create an imageLayer 
 */
var extent = [0, 0, 3264, 2448];
var projection = new ol.proj.Projection({
            code: 'EPSG:4326',
            extent: extent
        }),
var imageLayer = new ol.layer.Image({
    source: new ol.source.ImageStatic({
        url: 'sample.jpg',
        projection: projection,
        imageExtent: extent
    })
})

與 heatmap 一樣,首先需要傳入 URL 參數,即圖片地址,這裏可以是網絡圖片的地址,或者是本地的文件地址;然後需要傳入參考座標系 projection,code 是一個標識,可以是任何字符串,如果是EPSG:4326 或者是 EPSG:3857 ,那麼就會使用這兩個座標系,如果不是,就使用默認的座標系,extent 是一個矩形範圍,上面已經提到;imageLayer 的第三個參數是 imageExtent 表示圖片的尺寸,這裏我們不能改變圖片的原來的比例,圖片只會根據原來的比例放大或縮小。

最後將 imageLayer 加到地圖中:

map = new ol.Map({  //初始化map
    target: 'map',
    layers: [ imageLayer ],
    view: new ol.View({
      projection: projection,
      center: ol.extent.getCenter(extent),
      zoom: 2
    })
}); 

效果如下:

image layer

放大之後感覺很像必應搜索的界面的感覺,有木有 _|:

image layer

2.4 Tile Layer

切片地圖是比較常用的圖層類型,切片的概念,就是利用網格將一幅地圖切成大小相等的小正方形,如圖:

tile layer

這樣就明白我們使用百度地圖等地圖時爲什麼網速慢時候,會一塊一塊的加載的原因了吧!對,因爲是切片。當請求地圖的時候,會請求視口(也就是瀏覽器可見的區域)可見的區域內包含的切片,其餘的切片不會請求,這樣就節省了網絡帶寬,而且一般這些切片都是預先切好的,且分爲不同的縮放級別,根據不同的縮放級別分成不同的目錄。如果將這些切片地圖放到緩存中,那訪問速度會更快。

繼承了 ol.layer.Layer ,額外的參數是 olx.layer.TileOptions ,其定義如下:

/**
 * @typedef {{brightness: (number|undefined),
 *     contrast: (number|undefined),
 *     hue: (number|undefined),
 *     opacity: (number|undefined),
 *     preload: (number|undefined),
 *     saturation: (number|undefined),
 *     source: (ol.source.Tile|undefined),
 *     visible: (boolean|undefined),
 *     extent: (ol.Extent|undefined),
 *     minResolution: (number|undefined),
 *     maxResolution: (number|undefined),
 *     useInterimTilesOnError: (boolean|undefined)}}
 * @api
 */

可以看出,多出了 preload 和 useInterimTilesOnError 兩個參數,preload 是在還沒有將相應分辨率的渲染出來的時候,將低分辨率的切片先放大到當前分辨率(可能會有模糊),填充到相應位置,默認是 0,現在也就明白了當網速慢時,爲什麼地圖會先是模糊的,然後再變清晰了吧,就是這個過程!useInterimTilesOnError是指當加載切片發生錯誤時,是否用一個臨時的切片代替,默認值是 true

實例

其實在 加載地圖的例子 中,我們就是請求 MapQuest 的切片地圖:

map = new ol.Map({  //初始化map
    target: 'map',
    layers: [
      new ol.layer.Tile({
        source: new ol.source.MapQuest({layer: 'sat'})
      })
    ],
    view: new ol.View({
      center: ol.proj.transform([37.41, 8.82], 'EPSG:4326', 'EPSG:3857'),
      zoom: 2
    })
}); 

其中的 ol.layer.Tile 就是切片圖層類型,來源是 MapQuest ,layer
是請求的圖層的類型, MapQuest 有三種類型的圖層:osm, sathybosm 就是 OpenStreetMap 的縮寫,是其提供的數據, sat 是衛星圖,hyb 是兩種類型的混合圖層。

我們可以查看一下瀏覽器的網絡請求內容:

baidu map

這裏是 Firefox 瀏覽器的 Firebug 網絡請求面板,可見其請求的圖片,是一塊塊的,且是基於一定的編號規則進行編號的。

2.5 Vector Layer

OpenLayers之使用Vector Layer 中曾經使用過,即矢量圖層,矢量圖層實際上是在客戶端渲染的圖層類型,服務器返回的數據或者文件會通過 OpenLayers 進行渲染,得到相應的矢量圖層。

在客戶端渲染的矢量數據圖層,繼承了 ol.layer.Layer ,額外的參數是 olx.layer.VectorOptions ,其定義如下:

> /**
 * @typedef {{brightness: (number|undefined),
 *     contrast: (number|undefined),
 *     renderOrder: (function(ol.Feature, ol.Feature):number|null|undefined),
 *     hue: (number|undefined),
 *     minResolution: (number|undefined),
 *     maxResolution: (number|undefined),
 *     opacity: (number|undefined),
 *     renderBuffer: (number|undefined),
 *     saturation: (number|undefined),
 *     source: (ol.source.Vector|undefined),
 *     style: (ol.style.Style|Array.<ol.style.Style>|ol.style.StyleFunction|undefined),
 *     updateWhileAnimating: (boolean|undefined),
 *     updateWhileInteracting: (boolean|undefined),
 *     visible: (boolean|undefined)}}
 * @api
 */

相對於一般的圖層,多出了 renderOrder、renderBuffer、style、updateWhileAnimating 和 updateWhileInteracting 五個參數。renderOrder 是指渲染地理要素時的順序,一般情況下,在渲染之前,要素是基於一定規則排序的,而渲染就是根據這個順序進行依次渲染的,這個參數便指定了這個排序規則,如果賦值爲 null ,那麼就不會對地理要素進行排序,渲染也不會有一定的順序;renderBuffer 表示地圖的視口區域的緩衝區;style 規定了矢量圖層的樣式,就是配色和形狀等等;updateWhileAnimating 表示當有動畫特效時,地理要素是否被重新創建,默認是 false,當設置爲 true 時,可能會對性能有所影響;updateWhileInteracting 表示當 地理要素 交互時,是否會被重新渲染。

實例

首先創建一個 矢量圖層:

vectorLayer = new ol.layer.Vector({ //初始化矢量圖層
  source: new ol.source.GeoJSON({
    projection: 'EPSG:3857',
    url: 'data/geojson/countries.geojson'   //從文件加載邊界等地理信息
  }),
  style: function(feature, resolution) {
    style.getText().setText(resolution < 5000 ? feature.get('name') : '');  //當放大到1:5000分辨率時,顯示國家名字
    return [style];
  }
});

服務器返回的 GeoJSON 格式的文件 data/geojson/countries.geojson 包含國家的邊界數據,屬於多邊形類型,經過 OpenLayers 渲染之後得到結果如下:

vector layer

可以看到藍色的線爲各個國家的邊界,當鼠標在某個國家上方時,相應的區塊會變紅色,這是添加的事件,我們可以改變其樣式,注意到 vectorlayer 相對於其他類型的圖層,還包含了一個 style 參數,這個參數便是控制矢量圖層的外觀樣式的,其定義如下:

/**
 * 定義矢量圖層
 * 其中style是矢量圖層的顯示樣式 
 */
var style = new ol.style.Style({
  fill: new ol.style.Fill({ //矢量圖層填充顏色,以及透明度
    color: 'rgba(255, 255, 255, 0.6)'
  }),
  stroke: new ol.style.Stroke({ //邊界樣式
    color: '#319FD3',
    width: 1
  }),
  text: new ol.style.Text({ //文本樣式
    font: '12px Calibri,sans-serif',
    fill: new ol.style.Fill({
      color: '#000'
    }),
    stroke: new ol.style.Stroke({
      color: '#fff',
      width: 3
    })
  })
});

style 是一個 ol.style.Style 類型,矢量圖層是可以調節透明度的,如下:

fill: new ol.style.Fill({ //矢量圖層填充顏色,以及透明度
    color: 'rgba(255, 255, 255, 0.6)'
  })

rgba 的最後一個變量就是控制透明度的變量,範圍是 0~1,0 表示不透明,1 代表完全透明。因爲這裏主要講 Layer,所以關於 ol.style.Style 其它的內容,這裏就不多說了。

3、討論


百度地圖或高德地圖提供的是什麼layer類型呢?我們來分別看看它們在 Firefox 看到的網絡請求。

百度地圖:
baidu tile layer

高德地圖:

gaode map

通過上面的討論,我們可以得出結論,它們都是提供的網絡切片圖層類型,而一些加載的地理要素,如酒店等,便是加載在一個矢量圖層中的,所以說,它們是混雜着切片圖層和矢量圖層的。

4、總結


其實圖層可以按照渲染的位置分爲兩類,一類是在服務器端渲染好,以圖片形式返回瀏覽器的, imagelayer 和 tilelayer 都是屬於這種類型;另一類是在瀏覽器渲染的圖層類型,vectorlayer 和 heatmaplayer 就是這種類型。

OK,終於寫完了,好累好累!

參考

  1. https://www.omnisci.com/technical-glossary/cpu-vs-gpu
  2. https://openlayers.org/en/latest/examples/webgl-points-layer.html

聲明:文章爲本人原創,轉載或使用請註明作者和出處!!謝謝!

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