以下內容轉載自多多洛愛學習的文章《JSAPI-在地圖上添加自定義覆蓋物》
作者:多多洛愛學習
鏈接:https://juejin.im/post/5ee5f80d51882542e2695874
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
地圖上的覆蓋物
在地圖上添加覆蓋物有兩種方式,一是在canvas畫布上渲染,比如JSAPI GL繪制MultiMarker/MultiPolygon
等矢量圖形覆蓋物就是通過編寫對應圖形的數據解析及渲染程序,直接繪制在底圖上層。這樣的渲染方式下視角變換時圖形也可以實現3D形變。另一種方式是通過CSS布局將其他DOM元素疊加到地圖容器之上,這種方式下視角變換時DOM元素需重新計算布局,比如JSAPI v2的Marker/Polygon
等覆蓋物,以及JSAPI GL的InfoWindow
信息窗,這些都屬於DOM覆蓋物。
如果你需要疊加一個自定義的復雜元素,第一種方式的話需要實現對應的數據解析和着色器程序,需要了解WebGL的渲染原理,成本很高,且不易變通。而DOM是每個前端工程師都非常熟悉的,簡單幾個標簽加CSS就能實現高度定制的DOM元素。但是如何將一個DOM元素正確地安置在地圖上,並且隨着地圖平移、旋轉、縮放實時調整自己的位置呢?
這就要使用到DOMOverlay
了。它並不是一個具體的DOM覆蓋物,而是所有DOM覆蓋物的抽象基類,InfoWindow
就繼承自它。DOMOverlay
抽象出了DOM覆蓋物的生命周期,公共屬性及方法,實現了地圖事件的監聽綁定及解綁,你只需要關注DOM節點的創建和位置計算方法即可。
DOMOverlay 接口設計
先來看看DOMOverlay
的類關系圖,這里結合了官網示例DOMOverlay中定義的Donut
類作為DOMOverlay
的實現:
公共屬性及方法
事件監聽及觸發
從上圖可見,DOMOverlay
繼承自Node.js的EventEmitter
類,所以它已經實現了事件監聽、觸發等功能的封裝,不太熟悉的同學可以看看Node.js EventEmitter | 菜鳥教程。
地圖綁定與解綁
DOMOverlay
有一個公共屬性map
,其值為該覆蓋物綁定的地圖實例,同時提供了setMap(map: Map)
和getMap()
方法作為map
參數的訪問器。
要將自定義覆蓋物顯示在地圖上,首先得明確具體的地圖實例,有兩種辦法,一是在初始化參數中定義map
屬性,二是通過setMap
進行動態設置,可以綁定到另一個地圖實例上,或者解綁。setMap
做了什么呢?綁定時一方面主要是將createDOM()
返回的DOM元素加入到特定的節點下,使其覆蓋在地圖上方且可以進行相對定位;另一方面是監聽地圖變換執行updateDOM()
,使DOM元素可以跟隨地圖更新定位或內容。解綁時則是將其從父節點下去除,同時刪除對地圖事件的監聽。
DOM元素
DOMOverlay的公共屬性dom指向的是該覆蓋物的具體元素,可以是HTMLElement或者SVGElement,該元素的創建由子類進行實現,綁定地圖后會掛載到覆蓋在canvas畫布上層的一個div容器中。
銷毀
當覆蓋物不再被使用時應適時進行銷毀操作,以防內存泄漏。destroy
方法封裝了銷毀時應執行的操作,一方面將地圖解綁,另一方面刪除對象上注冊的所有監聽器。
抽象方法
DOMOverlay
提供了4個抽象方法,在生命周期的不同階段進行調用。
onInit
在初始化階段調用,並透傳了構造函數的參數options
,用於參數注入createDOM
在初始階段調用,用於創建DOM元素並將其返回,作為dom屬性的值,並加入到特定的父節點下updateDOM
在地圖發生平移、縮放、旋轉時調用,用於更新DOM元素定位onDestroy
在銷毀階段調用,可在此函數中對自定義的對象和事件監聽進行刪除
具體的生命周期如下:
基於DOMOverlay
實現自定義覆蓋物
舉個🌰:自定義環形餅圖
以官網示例中的Donut
為例,創建自定義環形餅圖。官網示例中使用了原生JS語法實現繼承,這里我們改用ES6語法實現下:
const SVG_NS = 'http://www.w3.org/2000/svg';
// 自定義環狀餅圖 - 繼承DOMOverlay
class Donut extends TMap.DOMOverlay {
constructor(options) {
super(options);
}
// 初始化:獲取配置參數
onInit({
position,
data,
minRadius = 0,
maxRadius = 50,
} = {}) {
Object.assign(this, {
position,
data,
minRadius,
maxRadius,
});
}
// 創建DOM元素,返回一個Element,使用this.dom可以獲取到這個元素
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元素,在地圖移動/縮放后執行
updateDOM() {
if (!this.map) {
return;
}
// 經緯度坐標轉容器像素坐標
let pixel = this.map.projectToContainer(this.position);
// 使餅圖中心點對齊經緯度坐標點
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})`;
}
// 銷毀時
onDestroy() {}
}
其中createDonut
是根據數據和半徑創建對應的SVG圖形,這里先不過多關注。
如何進行元素定位?
這里重點說明下updateDOM
的實現,如何進行定位更新。
首先,我們在初始化階段給position
屬性賦值,position
是一個經緯度對象,可以通過map.projectToContainer
方法轉為地圖容器內的像素坐標,記為pixel
。地圖容器坐標系是以地圖容器左上角為原點,向右為x正方向,向下為y正方向的坐標系。
另外,我們在createDOM
方法中對生成的svg元素設置了CSS樣式position:absolute;top:0px;left:0px;
,所以元素實際定位是與地圖容器左上角對齊。
我們需要讓環形餅圖的中心與pixel
位置對齊,首先可以通過clientWidth/clientHeight
獲取元素寬高,然后計算得到元素左上角的像素坐標為(left
、top
),最后通過transform: translate(${left}, ${top})
設置平移偏移量,將元素移動到對應位置。
為什么不使用top: ${top}; left: ${left}
進行定位呢?
因為transform
比top/left
性能好很多。top/left
是在CPU上進行計算,會引起周圍區域的重繪;而transform是利用GPU計算能力,且是在獨立的圖層中進行變換,不會引起重繪。具體可以參考創建前端平移動畫為何translate()優於top/right/bottom/left。
如何實現click監聽?
有的同學發現創建了自定義覆蓋物之后就不能像MultiMarker
那樣通過on('click')
監聽到點擊事件了,這是為什么呢?因為你沒有觸發事件啊😂 首先你需要監聽DOM元素的點擊事件,可以在createDOM
中實現:
// 創建DOM元素,返回一個Element,使用this.dom可以獲取到這個元素
createDOM() {
...
// click事件回調
this.onClick = () => {
// DOMOverlay繼承自EventEmitter,可以使用emit觸發事件
this.emit('click');
};
// 使用addEventListener實現DOM元素的click監聽
svg.addEventListener('click', this.onClick);
return svg;
}
在clic
k事件回調中可以直接執行你想要的操作,或者調用emit
觸發事件,就可以觸發通過on
掛載的監聽器了,如下:
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(`環形圖被點擊,位置為${donut.position}`);
});
需要注意的是,在銷毀時應該將事件監聽刪除,所以onDestroy
應相應修改為:
// 銷毀時需解綁事件監聽
onDestroy() {
if (this.onClick) {
this.dom.removeEventListener(this.onClick);
}
}
類似的,你可以監聽mousedown
、mouseup
以及移動端的touchstart
、touchend
等事件,因為是自定義元素,所以控制權在你自己手上哦。
為什么出現偏移?
有的同學在實現自定義覆蓋物之后,發現創建多個元素會發生向下偏移,且逐個的偏移量越來越多,這是為什么?
或許你可以檢查下DOM元素是不是沒有設置position:absolute;top:0px;left:0px;
,如果沒有設置絕對定位以及坐標為(0, 0)的話,則transform
是在元素原本的定位上進行偏移,且元素沒有脫離文檔流,后加入的元素會依次下移。
其他應用
DOMOverlay
可以應用在各種圖文結合、不易繪制的元素上。 比如使用點聚合接口時,如果想要使用自定義樣式,而且需要顯示簇大小,就可以使用自定義DOM元素來表達聚合簇。
再比如編輯器中,繪制和編輯圖形時圖形需要實時變化,使用矢量圖形圖層需要不斷重構數據,有較大開銷,所以也是結合DOM覆蓋物,通過SVG渲染單個圖形。
另外,有的同學還問到,JSAPI v2中的marker跳動動畫在GL里怎么實現呢?其實也可以使用自定義覆蓋物來實現,官網也提供了marker動畫示例。
什么情況下不適合使用DOMOverlay
?
需要注意的是,當你需要繪制大量(>1000)的覆蓋物時是不適合使用DOMOverlay
的,因為每個DOM元素都是單獨進行定位更新的計算,會帶來非常大的開銷,在地圖變化時會非常卡頓。
海量覆蓋物的渲染還是推薦使用MultiMarker/MultiPolygon
等矢量圖形圖層,或者位置數據可視化API,提供了散點圖、弧線圖、軌跡圖、區域圖等可視化類型。