在leaflet里有一个非常好用divOverylay类,可以使用html+css构建元素直接叠加到地图,并随地图拖动实时改变位置,用于表现复杂的弹窗气泡或标注都很实用,但在arcgis js api 里没有相关的实现方式,这里参考leaflet的实现方式基于arcgis api实现类似的功能。
// divOverlay.ts 类
const supportClass = ['esri.layers.FeatureLayer', 'esri.layers.GraphicsLayer'];
const getGeometryCenter = (geometry: __esri.Geometry) => {
if (!geometry) {
window.console.log(new Error('map:getGeometryCenter:几何对象为空'));
return null
}
const { type } = geometry;
// 获取图形中心点
let center: __esri.Geometry | any = null;
switch (type) {
case 'point':
center = geometry
break;
case 'polyline':
center = geometry.extent.center;
break;
case 'polygon':
// center = geometry.centroid;
center = geometry.extent.center;
break;
case 'extent':
center = geometry.center;
break;
default:
break;
}
return center;
}
const setClass = (el: HTMLElement, name: string) => {
if (!el.className['baseVal']) {
el.className = name;
} else {
// in case of SVG element
el.className['baseVal'] = name;
}
};
const getClass = (el: HTMLElement) => {
if (el['correspondingElement']) {
el = el['correspondingElement'];
}
return el.className['baseVal'] === undefined ? el.className : el.className['baseVal'];
};
const hasClass = (el: HTMLElement, name: string) => {
if (el.classList !== undefined) {
return el.classList.contains(name);
}
const className = getClass(el);
return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className);
};
function splitWords(str: string) {
return trim(str).split(/\s+/);
}
const addClass = (el, name) => {
if (el.classList !== undefined) {
const classes = splitWords(name);
for (let i = 0, len = classes.length; i < len; i++) {
el.classList.add(classes[i]);
}
} else if (!hasClass(el, name)) {
const className = getClass(el);
setClass(el, (className ? className + ' ' : '') + name);
}
};
export default class DivOverlay{
declaredClass: string = 'divoverlay';
uiKey: string = 'div-overlay';
map: __esri.WebMap | __esri.WebScene;
mapView: __esri.MapView | __esri.SceneView;
source: __esri.Graphic[] | __esri.GraphicsLayerView | __esri.FeatureLayerView;
options: any = {};
container: any;
data: any = [];
domNodes: any = {};
displayField: string = 'name';
currentAlignment: string = 'top-center'; // top-center\top-left\top-right\bottom-center\bottom-left\bottom-right\left-center\right-center
_pointerOffsetInPx: number = 0;
hasArrow: boolean | undefined;
constructor(options) {
this.map = options.map;
this.mapView = options.mapView;
this.source = options.source || [];
this.domNodes = {};
this.displayField = options.displayField || 'name';
this.hasArrow = options.hasOwnProperty('hasArrow') ? options.hasArrow : true;
this._pointerOffsetInPx = (options.offset !== undefined) ? options.offset : 2;
this.currentAlignment = options.alignment || 'top-center';
}
addTo() {
this.renderer();
if (supportClass.indexOf(this.source?.declaredClass) > -1) {
this.source.on('layerview-destroy', () => {
this.destroy();
});
}
}
renderer() {
if (!Array.isArray(this.source) && supportClass.indexOf(this.source?.declaredClass) < 0) {
window.console.log(new Error('数据源类型不对'));
}
this.createContainer();
this.update();
this.mapView.watch(['size', 'padding', 'extent'], () => {
this.update();
});
if (!Array.isArray(this.source)) {
this.source.on('draw-complete', () => {
this.update();
});
}
}
update() {
if (!Array.isArray(this.source) && this.source?.declaredClass === 'esri.layers.FeatureLayer') {
this.queryDataFromFeatureLayer(this.source);
}
if (!Array.isArray(this.source) && this.source?.declaredClass === 'esri.layers.GraphicsLayer') {
this.queryDataFromGraphicsLayer(this.source);
}
if (Array.isArray(this.source)) {
this.reposition(this.source);
}
}
createContainer() {
this.container = document.createElement('div');
addClass(this.container, this.uiKey);
addClass(this.container, 'esri-divoverlay-container');
this.container.id = this.uiKey;
this.mapView.ui.add(this.container, {
key: this.uiKey,
position: "manual",
})
}
createDivDom(content, id) {
const container = document.createElement('div');
addClass(container, 'esri-divoverlay');
// 内容容器
const wrapper = document.createElement('div');
addClass(wrapper, 'esri-divoverlay-content-wrapper');
const contentDom = document.createElement('div');
addClass(contentDom, 'esri-divoverlay-content');
wrapper.append(contentDom);
container.append(wrapper);
// 箭头标注
if (this.hasArrow) {
const tipContainer = document.createElement('div');
addClass(tipContainer, 'esri-divoverlay-tip-container');
addClass(tipContainer, `esri-divoverlay-tip-container-${this.currentAlignment}`);
const tip = document.createElement('div');
addClass(tip, 'esri-divoverlay-tip');
addClass(tip, `esri-divoverlay-tip-${this.currentAlignment}`);
tipContainer.append(tip);
container.append(tipContainer);
}
// 关闭按钮
const closeBtn = document.createElement('a');
addClass(closeBtn, 'esri-divoverlay-close-button');
closeBtn.innerText = '×';
closeBtn.setAttribute('id', id);
closeBtn.addEventListener('click', this._onCloseButtonClick.bind(this), false);
container.append(closeBtn);
contentDom.innerText = content;
return container;
}
getContent(graphic: any, field: string) {
const { attributes } = graphic;
const content = attributes?.hasOwnProperty(field) ? attributes[field] : '';
return content;
}
reposition(data: any[]) {
this.data = data;
const ids = [];
data?.forEach(graphic => {
if (graphic.declaredClass !== 'esri.Graphic') return null;
const { geometry, visible, attributes } = graphic;
const uid = attributes?.id.toString() || '';
ids.push(uid);
if (!visible) return null;
if (geometry === undefined || geometry === null) return null;
let target = this.domNodes[uid] || null;
if (!target) {
const container = this.createDivDom(this.getContent(graphic, this.displayField), uid);
this?.container?.append(container);
target = { container, attributes };
this.domNodes[uid] = target;
} else {
const content = this.getContent(graphic, this.displayField);
this.updateContent(target.container, content);
}
// 获取图形中心点
const center = getGeometryCenter(geometry);
if (!center) return null;
this._positionContainer(target.container, center);
});
Object.keys(this.domNodes).forEach(key => {
if (ids.indexOf(key) < 0) {
this.domNodes[key]?.container?.remove();
delete this.domNodes[key];
}
});
}
updateContent(container: any, content: string) {
const target = container?.querySelector('.esri-divoverlay-content');
if (target && target.innerHTML !== content) {
target.innerHTML = content;
}
}
_positionContainer = (container: any, center: __esri.Geometry) => {
let screenPoint = this.mapView.toScreen(center);
const { width, height } = container?.getBoundingClientRect();
screenPoint = this._calculatePositionStyle(screenPoint, width, height);
if (!screenPoint) return;
container.style.top = screenPoint.top;
container.style.left = screenPoint.left;
container.style.bottom = screenPoint.bottom;
container.style.right = screenPoint.right;
}
_calculatePositionStyle = (screenPoint: any, width: number, height: number) => {
if (this.mapView && screenPoint && width && height) {
const values = this._calculateFullWidth(width, height);
width = values.width;
height = values.height;
screenPoint = this._calculateAlignmentPosition(screenPoint.x, screenPoint.y, width, height);
if (!screenPoint) return null;
return {
top: void 0 !== screenPoint.top ? `${screenPoint.top}px` : "auto",
left: void 0 !== screenPoint.left ? `${screenPoint.left}px` : "auto",
bottom: void 0 !== screenPoint.bottom ? `${screenPoint.bottom}px` : "auto",
right: void 0 !== screenPoint.right ? `${screenPoint.right}px` : "auto",
};
}
return null;
}
_calculateFullWidth = (width: number, height: number) => {
const { currentAlignment, _pointerOffsetInPx } = this;
width = (currentAlignment === "top-left" ||
currentAlignment === "bottom-left" ||
currentAlignment === "top-right" ||
currentAlignment === "bottom-right") ? width + _pointerOffsetInPx : width;
height = (currentAlignment === 'left-top' ||
currentAlignment === 'left-bottom' ||
currentAlignment === 'right-top' ||
currentAlignment === 'right-bottom') ? height + _pointerOffsetInPx : height;
return { width, height };
}
_calculateAlignmentPosition = (screenPointX: number, screenPointY: number, width: number, height: number) => {
const { currentAlignment, _pointerOffsetInPx } = this;
width /= 2;
height /= 2;
const q = this.mapView.height - screenPointY;
const b = this.mapView.width - screenPointX;
const { padding } = this.mapView;
if (currentAlignment === "bottom-center")
return { top: screenPointY + _pointerOffsetInPx - padding.top, left: screenPointX - width - padding.left };
if (currentAlignment === "top-left")
return { bottom: q + _pointerOffsetInPx - padding.bottom, right: b + _pointerOffsetInPx - padding.right };
if (currentAlignment === "bottom-left")
return { top: screenPointY + _pointerOffsetInPx - padding.top, right: b + _pointerOffsetInPx - padding.right };
if (currentAlignment === "top-right")
return { bottom: q + _pointerOffsetInPx - padding.bottom, left: screenPointX + _pointerOffsetInPx - padding.left };
if (currentAlignment === "bottom-right")
return { top: screenPointY + _pointerOffsetInPx - padding.top, left: screenPointX + _pointerOffsetInPx - padding.left };
if (currentAlignment === "top-center")
return { bottom: q + _pointerOffsetInPx - padding.bottom, left: screenPointX - width - padding.left };
if (currentAlignment === "left-center")
return { bottom: q - padding.bottom - height, left: screenPointX - width * 2 - padding.left - _pointerOffsetInPx };
if (currentAlignment === "right-center")
return { bottom: q - padding.bottom - height, left: screenPointX - padding.left + _pointerOffsetInPx };
return null;
};
_onCloseButtonClick = (ags) => {
const target = ags.target || ags.currentTarget;
const uid = target?.getAttribute('id');
if (!uid) {
window.console.log(new Error('map:关闭overlay失败'));
return;
}
const domTarget = this.domNodes[uid];
const graphics = this.source.filter(item => {
if (item?.uid?.toString() === uid) return true;
return false;
});
if (graphics.length > 0) graphics[0].visible = false;
domTarget.container.remove();
delete this.domNodes[uid];
}
private queryDataFromFeatureLayer(layer: __esri.FeatureLayer) {
this.mapView?.whenLayerView(layer).then((layerView: __esri.FeatureLayerView) => {
const waitLayerViewUpdated = setInterval(() => {
if (!layerView.updating) {
clearInterval(waitLayerViewUpdated);
}
layerView.queryFeatures().then(featureSet => {
const { features } = featureSet;
this.reposition(features);
});
}, 200);
});
}
private queryDataFromGraphicsLayer(layer: __esri.GraphicsLayer) {
this.mapView?.whenLayerView(layer).then((layerView: __esri.GraphicsLayerView) => {
layerView.queryGraphics().then((results: any) => {
this.reposition(results);
});
});
}
destroy() {
this.source = null;
this.mapView.ui.remove(this.uiKey);
}
}
const divOverlay = new DivOverlay({
map: map,
mapView: mapView,
source: layer, // FeatureLayer、GraphicsLayer
displayField: 'name',
offset: 0,
alignment: 'right-center'
});
divOverlay.addTo()