Arcgis JS api4.18简单实现leaflet的divOverlay

arcgis js api 简单实现leaflet的divOverlay

介绍

在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()

效果

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值