騰訊地圖JSAPI教程-在地圖上添加自定義覆蓋物

以下內(nèi)容轉(zhuǎn)載自多多洛愛學(xué)習(xí)的文章《JSAPI-在地圖上添加自定義覆蓋物》

作者:多多洛愛學(xué)習(xí)

鏈接:https://juejin.im/post/5ee5f80d51882542e2695874

來源:掘金

著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

地圖上的覆蓋物

在地圖上添加覆蓋物有兩種方式,一是在canvas畫布上渲染,比如JSAPI GL繪制MultiMarker/MultiPolygon等矢量圖形覆蓋物就是通過編寫對(duì)應(yīng)圖形的數(shù)據(jù)解析及渲染程序,直接繪制在底圖上層。這樣的渲染方式下視角變換時(shí)圖形也可以實(shí)現(xiàn)3D形變。另一種方式是通過CSS布局將其他DOM元素疊加到地圖容器之上,這種方式下視角變換時(shí)DOM元素需重新計(jì)算布局,比如JSAPI v2的Marker/Polygon等覆蓋物,以及JSAPI GL的InfoWindow信息窗,這些都屬于DOM覆蓋物。

image

如果你需要疊加一個(gè)自定義的復(fù)雜元素,第一種方式的話需要實(shí)現(xiàn)對(duì)應(yīng)的數(shù)據(jù)解析和著色器程序,需要了解WebGL的渲染原理,成本很高,且不易變通。而DOM是每個(gè)前端工程師都非常熟悉的,簡(jiǎn)單幾個(gè)標(biāo)簽加CSS就能實(shí)現(xiàn)高度定制的DOM元素。但是如何將一個(gè)DOM元素正確地安置在地圖上,并且隨著地圖平移、旋轉(zhuǎn)、縮放實(shí)時(shí)調(diào)整自己的位置呢?

這就要使用到DOMOverlay了。它并不是一個(gè)具體的DOM覆蓋物,而是所有DOM覆蓋物的抽象基類,InfoWindow就繼承自它。DOMOverlay抽象出了DOM覆蓋物的生命周期,公共屬性及方法,實(shí)現(xiàn)了地圖事件的監(jiān)聽綁定及解綁,你只需要關(guān)注DOM節(jié)點(diǎn)的創(chuàng)建和位置計(jì)算方法即可。

DOMOverlay 接口設(shè)計(jì)

先來看看DOMOverlay的類關(guān)系圖,這里結(jié)合了官網(wǎng)示例DOMOverlay中定義的Donut類作為DOMOverlay的實(shí)現(xiàn):

image

公共屬性及方法

事件監(jiān)聽及觸發(fā)

從上圖可見,DOMOverlay繼承自Node.js的EventEmitter類,所以它已經(jīng)實(shí)現(xiàn)了事件監(jiān)聽、觸發(fā)等功能的封裝,不太熟悉的同學(xué)可以看看Node.js EventEmitter | 菜鳥教程

地圖綁定與解綁

DOMOverlay有一個(gè)公共屬性map,其值為該覆蓋物綁定的地圖實(shí)例,同時(shí)提供了setMap(map: Map)getMap()方法作為map參數(shù)的訪問器。
要將自定義覆蓋物顯示在地圖上,首先得明確具體的地圖實(shí)例,有兩種辦法,一是在初始化參數(shù)中定義map屬性,二是通過setMap進(jìn)行動(dòng)態(tài)設(shè)置,可以綁定到另一個(gè)地圖實(shí)例上,或者解綁。setMap做了什么呢?綁定時(shí)一方面主要是將createDOM()返回的DOM元素加入到特定的節(jié)點(diǎn)下,使其覆蓋在地圖上方且可以進(jìn)行相對(duì)定位;另一方面是監(jiān)聽地圖變換執(zhí)行updateDOM(),使DOM元素可以跟隨地圖更新定位或內(nèi)容。解綁時(shí)則是將其從父節(jié)點(diǎn)下去除,同時(shí)刪除對(duì)地圖事件的監(jiān)聽。

DOM元素

DOMOverlay的公共屬性dom指向的是該覆蓋物的具體元素,可以是HTMLElement或者SVGElement,該元素的創(chuàng)建由子類進(jìn)行實(shí)現(xiàn),綁定地圖后會(huì)掛載到覆蓋在canvas畫布上層的一個(gè)div容器中。

銷毀

當(dāng)覆蓋物不再被使用時(shí)應(yīng)適時(shí)進(jìn)行銷毀操作,以防內(nèi)存泄漏。destroy方法封裝了銷毀時(shí)應(yīng)執(zhí)行的操作,一方面將地圖解綁,另一方面刪除對(duì)象上注冊(cè)的所有監(jiān)聽器。

抽象方法

DOMOverlay提供了4個(gè)抽象方法,在生命周期的不同階段進(jìn)行調(diào)用。

  • onInit在初始化階段調(diào)用,并透?jìng)髁藰?gòu)造函數(shù)的參數(shù)options,用于參數(shù)注入
  • createDOM在初始階段調(diào)用,用于創(chuàng)建DOM元素并將其返回,作為dom屬性的值,并加入到特定的父節(jié)點(diǎn)下
  • updateDOM在地圖發(fā)生平移、縮放、旋轉(zhuǎn)時(shí)調(diào)用,用于更新DOM元素定位
  • onDestroy在銷毀階段調(diào)用,可在此函數(shù)中對(duì)自定義的對(duì)象和事件監(jiān)聽進(jìn)行刪除

具體的生命周期如下:

image

基于DOMOverlay實(shí)現(xiàn)自定義覆蓋物

舉個(gè)??:自定義環(huán)形餅圖

[圖片上傳失敗...(image-3e8639-1593402335630)]

以官網(wǎng)示例中的Donut為例,創(chuàng)建自定義環(huán)形餅圖。官網(wǎng)示例中使用了原生JS語法實(shí)現(xiàn)繼承,這里我們改用ES6語法實(shí)現(xiàn)下:

const SVG_NS = 'http://www.w3.org/2000/svg';

// 自定義環(huán)狀餅圖 - 繼承DOMOverlay
class Donut extends TMap.DOMOverlay {
    constructor(options) {
        super(options);
    }

    // 初始化:獲取配置參數(shù)
    onInit({
        position,
        data,
        minRadius = 0,
        maxRadius = 50,
    } = {}) {
        Object.assign(this, {
            position,
            data,
            minRadius,
            maxRadius,
        });
    }

    // 創(chuàng)建DOM元素,返回一個(gè)Element,使用this.dom可以獲取到這個(gè)元素
    createDOM() {
        let svg = document.createElementNS(SVG_NS, 'svg');
        svg.setAttribute('version', '1.1');
        svg.setAttribute('baseProfile', 'full');

        let r = this.maxRadius;
        svg.setAttribute('viewBox', [-r, -r, r * 2, r * 2].join(' '));
        svg.setAttribute('width', r * 2);
        svg.setAttribute('height', r * 2);
        svg.style.cssText = 'position:absolute;top:0px;left:0px;';

        let donut = createDonut(this.data, this.minRadius, this.maxRadius);
        svg.appendChild(donut);

        return svg;
    }

    // 更新DOM元素,在地圖移動(dòng)/縮放后執(zhí)行
    updateDOM() {
        if (!this.map) {
            return;
        }

        // 經(jīng)緯度坐標(biāo)轉(zhuǎn)容器像素坐標(biāo)
        let pixel = this.map.projectToContainer(this.position);

        // 使餅圖中心點(diǎn)對(duì)齊經(jīng)緯度坐標(biāo)點(diǎn)
        let left = pixel.getX() - this.dom.clientWidth / 2 + 'px';
        let top = pixel.getY() - this.dom.clientHeight / 2 + 'px';
        this.dom.style.transform = `translate(${left}, ${top})`;
    }

    // 銷毀時(shí)
    onDestroy() {}
}

其中createDonut是根據(jù)數(shù)據(jù)和半徑創(chuàng)建對(duì)應(yīng)的SVG圖形,這里先不過多關(guān)注。

如何進(jìn)行元素定位?

這里重點(diǎn)說明下updateDOM的實(shí)現(xiàn),如何進(jìn)行定位更新。
首先,我們?cè)诔跏蓟A段給position屬性賦值,position是一個(gè)經(jīng)緯度對(duì)象,可以通過map.projectToContainer方法轉(zhuǎn)為地圖容器內(nèi)的像素坐標(biāo),記為pixel。地圖容器坐標(biāo)系是以地圖容器左上角為原點(diǎn),向右為x正方向,向下為y正方向的坐標(biāo)系。
另外,我們?cè)?code>createDOM方法中對(duì)生成的svg元素設(shè)置了CSS樣式position:absolute;top:0px;left:0px;,所以元素實(shí)際定位是與地圖容器左上角對(duì)齊。
我們需要讓環(huán)形餅圖的中心與pixel位置對(duì)齊,首先可以通過clientWidth/clientHeight獲取元素寬高,然后計(jì)算得到元素左上角的像素坐標(biāo)為(lefttop),最后通過transform: translate(${left}, ${top})設(shè)置平移偏移量,將元素移動(dòng)到對(duì)應(yīng)位置。

為什么不使用top: ${top}; left: ${left}進(jìn)行定位呢?
因?yàn)?code>transform比top/left性能好很多。top/left是在CPU上進(jìn)行計(jì)算,會(huì)引起周圍區(qū)域的重繪;而transform是利用GPU計(jì)算能力,且是在獨(dú)立的圖層中進(jìn)行變換,不會(huì)引起重繪。具體可以參考創(chuàng)建前端平移動(dòng)畫為何translate()優(yōu)于top/right/bottom/left

如何實(shí)現(xiàn)click監(jiān)聽?

有的同學(xué)發(fā)現(xiàn)創(chuàng)建了自定義覆蓋物之后就不能像MultiMarker那樣通過on('click')監(jiān)聽到點(diǎn)擊事件了,這是為什么呢?因?yàn)槟銢]有觸發(fā)事件啊:joy: 首先你需要監(jiān)聽DOM元素的點(diǎn)擊事件,可以在createDOM中實(shí)現(xiàn):

    // 創(chuàng)建DOM元素,返回一個(gè)Element,使用this.dom可以獲取到這個(gè)元素
    createDOM() {
        ...

        // click事件回調(diào)
        this.onClick = () => {
            // DOMOverlay繼承自EventEmitter,可以使用emit觸發(fā)事件
            this.emit('click');
        };
        
        // 使用addEventListener實(shí)現(xiàn)DOM元素的click監(jiān)聽
        svg.addEventListener('click', this.onClick);
        return svg;
    }

click事件回調(diào)中可以直接執(zhí)行你想要的操作,或者調(diào)用emit觸發(fā)事件,就可以觸發(fā)通過on掛載的監(jiān)聽器了,如下:

let donut = new Donut({
    map,
    position: new TMap.LatLng(40.02906301748584, 116.25499991104516),
    data: [18, 41, 50],
    minRadius: 20,
    maxRadius: 28
})
donut.on('click', () => {
    console.log(`環(huán)形圖被點(diǎn)擊,位置為${donut.position}`);
});

需要注意的是,在銷毀時(shí)應(yīng)該將事件監(jiān)聽刪除,所以onDestroy應(yīng)相應(yīng)修改為:

    // 銷毀時(shí)需解綁事件監(jiān)聽
    onDestroy() {
        if (this.onClick) {
            this.dom.removeEventListener(this.onClick);
        }
    }

類似的,你可以監(jiān)聽mousedownmouseup以及移動(dòng)端的touchstarttouchend等事件,因?yàn)槭亲远x元素,所以控制權(quán)在你自己手上哦。

為什么出現(xiàn)偏移?

有的同學(xué)在實(shí)現(xiàn)自定義覆蓋物之后,發(fā)現(xiàn)創(chuàng)建多個(gè)元素會(huì)發(fā)生向下偏移,且逐個(gè)的偏移量越來越多,這是為什么?
或許你可以檢查下DOM元素是不是沒有設(shè)置position:absolute;top:0px;left:0px;,如果沒有設(shè)置絕對(duì)定位以及坐標(biāo)為(0, 0)的話,則transform是在元素原本的定位上進(jìn)行偏移,且元素沒有脫離文檔流,后加入的元素會(huì)依次下移。

其他應(yīng)用

DOMOverlay可以應(yīng)用在各種圖文結(jié)合、不易繪制的元素上。 比如使用點(diǎn)聚合接口時(shí),如果想要使用自定義樣式,而且需要顯示簇大小,就可以使用自定義DOM元素來表達(dá)聚合簇。

image

再比如編輯器中,繪制和編輯圖形時(shí)圖形需要實(shí)時(shí)變化,使用矢量圖形圖層需要不斷重構(gòu)數(shù)據(jù),有較大開銷,所以也是結(jié)合DOM覆蓋物,通過SVG渲染單個(gè)圖形。

image

另外,有的同學(xué)還問到,JSAPI v2中的marker跳動(dòng)動(dòng)畫在GL里怎么實(shí)現(xiàn)呢?其實(shí)也可以使用自定義覆蓋物來實(shí)現(xiàn),官網(wǎng)也提供了marker動(dòng)畫示例

什么情況下不適合使用DOMOverlay
需要注意的是,當(dāng)你需要繪制大量(>1000)的覆蓋物時(shí)是不適合使用DOMOverlay的,因?yàn)槊總€(gè)DOM元素都是單獨(dú)進(jìn)行定位更新的計(jì)算,會(huì)帶來非常大的開銷,在地圖變化時(shí)會(huì)非常卡頓。
海量覆蓋物的渲染還是推薦使用MultiMarker/MultiPolygon等矢量圖形圖層,或者位置數(shù)據(jù)可視化API,提供了散點(diǎn)圖、弧線圖、軌跡圖、區(qū)域圖等可視化類型。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容