跳过正文

ArcGIS 实现 DomLayer 兼容MapView/SceneView

·2727 字·13 分钟
Arcgis GIS Frontend
Yan Zhitao
作者
Yan Zhitao
A Web/GIS Developer, and a runner
目录

ArcGIS 有很多类型的图层,但是没有“DomLayer”,即根据地图点位,所需要的内容通过 DOM 元素渲染,通过 css 和框架的渲染能力,可以做出来美观、灵活的显示内容,如饼图、表格、动画等。实现难点在于地图在大比例尺、小比例尺下,能统一显示效果,在小比例尺下不至于挤成一片。核心方法使用rbush.js 的 R 树空间索引索算法, 扩展图层的LayerView,参考网上前辈们分享的代码,做了改造。

效果
#

效果图上添加了 2 个图层,一个为 DomLayer,另外一个橙色的为所有点位图层,方便对比实际数据与渲染出 Domlayer 的数据。

  • 在大比例尺下,屏幕范围点位多,domlayer 根据渲染出的效果不会有叠加遮挡
  • 小比例尺下,在没有遮挡的情况下,数据显示基本完整
  • 适配 2D、3D 图层 API,支持透明度调整
  • 其他地图框架实现思路也类似

2D MapView
#

3D SceneView
#

代码
#

index.ts

import getDomLayer from "./DomLayer";
import { LayerType } from "../../../../../../../types/arcgisType";

const domLayerCreator = async (
  options: LayerType
): Promise<__esri.Layer | undefined> => {
  const DomLayer = await getDomLayer();
  return new DomLayer(options);
};

export default domLayerCreator;

DomLayer.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
import requireArcgis from "../../../../../../../utils/requireArcgis";
import getDomLayerView2D from "./DomLayerView2D";
import getDomLayerView3D from "./DomLayerView3D";
import _ from "lodash";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
const getDomLayer = async function () {
  const [Layer, Collection] = await requireArcgis([
    "esri/layers/Layer",
    "esri/core/Collection",
  ]);
  const DomLayerView2D = await getDomLayerView2D();
  const DomLayerView3D = await getDomLayerView3D();
  const clazz = Layer.createSubclass({
    constructor: function (options: any) {
      options = options || {};
      this.graphics = new Collection();
      this.popupEnabled = false;
      this.legendEnabled = false;
      this.direction = options.direction;
      this.type = "domLayer";

      this.graphics.on("after-add", (param: any) => {
        this.emit("after-add", param);
      });

      this.graphics.on("after-remove", (param: any) => {
        this.emit("after-remove", param);
      });
    },

    DIRECTION: ["bottom-right", "top-mid", "center"],
    direction: "center",

    declaredClass: "caihm.DivLayer",

    createLayerView: function (view: any) {
      this.view = view;
      if (view.type === "3d") {
        // window.layerView && window.layerView.destroy();
        console.log("has performance issue on 3d");
        this.layerView = new DomLayerView3D({
          view: view,
          layer: this,
        });
        // window.layerView = this.layerView;
      } else {
        // window.layerView && window.layerView.destroy();
        this.layerView = new DomLayerView2D({
          view: view,
          layer: this,
        });
        // window.layerView = this.layerView;
      }
      // eslint-disable-next-line @typescript-eslint/no-empty-function
      this.layerView.hitTest = () => {};
      return this.layerView;
    },

    destroyLayerView: function () {
      this.graphics = null;
      if (this.view.type === "3d") {
        this.layerView.destroy();
      }
    },

    load: function () {
      return this;
    },

    addMany(arr: any) {
      this.graphics.addMany(arr);
    },

    add: function (graphic: any) {
      this.graphics.add(graphic);
    },

    removeMany(arr: any) {
      this.graphics.removeMany(arr);
    },
    remove(graphic: any) {
      this.graphics.remove(graphic);
    },

    removeAll() {
      this.graphics.removeAll();
    },
  });

  return clazz;
};

export default getDomLayer;

DomLayerView2D.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
import requireArcgis from "../../../../../../../utils/requireArcgis";
import { domStyle, domClass, domConstruct, lang } from "./domOpt";
import Rbush from "rbush";
import _ from "lodash";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
const getDomLayerView2D = async function () {
  const [geometryEngine, LayerView, reactiveUtils] = await requireArcgis([
    "esri/geometry/geometryEngine",
    "esri/views/layers/LayerView",
    "esri/core/reactiveUtils",
  ]);

  const clazz = LayerView.createSubclass({
    transformOffset: {
      x: 0,
      y: 0,
    },
    isChanged: false,

    constructor(param: any) {
      param.layer.on("after-add", (param: any) => {
        this._add(param.item);
      });

      param.layer.on("after-remove", (param: any) => {
        this._remove(param.item);
      });

      this.direction = param.layer.direction;
    },

    _remove(ele: any) {
      if (ele.node) {
        domConstruct.destroy(ele.node);
      }
    },
    viewChange() {
      //   console.log('viewChange', evt);

      const currentScreenTarget = this.view.toScreen(this.dragStartCenter);

      if (this.screenTargetGeoemtry) {
        const dx = currentScreenTarget.x - this.screenTargetGeoemtry.x;
        const dy = currentScreenTarget.y - this.screenTargetGeoemtry.y;
        const translate = "translate3d(" + dx + "px," + dy + "px,0px)";
        domStyle.set(this._displayDiv, "transform", translate);
      }
    },
    moveStart() {
      // console.log('moveStart', evt);
      domClass.add(this._displayDiv, "moving");
      this.clearViewpointWatchers();

      this.calcTransform();
      this.dragStartCenter = this.view.center.clone();
      this.screenTargetGeoemtry = this.view.toScreen(this.dragStartCenter);

      this.screenTargetGeoemtry = {
        x: this.screenTargetGeoemtry.x - this.transformOffset.x,
        y: this.screenTargetGeoemtry.y - this.transformOffset.y,
      };
    },

    moveEnd() {
      // console.log('moveEnd', evt);
      this.clearViewpointWatchers();
      domClass.remove(this._displayDiv, "moving");
      window.requestAnimationFrame(() => {
        domStyle.set(this._displayDiv, "opacity", 1);
        this.refresh();
      });
    },

    clearViewpointWatchers: function () {
      while (this.viewpointWatchers?.length) {
        let viewpointWatcher = this.viewpointWatchers.pop();
        if (viewpointWatcher) {
          viewpointWatcher.remove();
          viewpointWatcher = null;
        }
      }
    },

    _add: function (ele: any) {
      const initStyle =
        "position:absolute;will-change:transform;top:0;left:0;pointer-events: auto;";
      if (ele.node && ele.node.parentNode === this._displayDiv) {
        //todo
      } else {
        if (lang.isString(ele.dom)) {
          ele.html = ele.dom;
          ele.dom = domConstruct.toDom(ele.html);
        } else if (lang.isString(ele.html)) {
          document.getElementById(ele.id)?.remove();
          ele.dom = domConstruct.toDom(ele.html);
        }

        ele.node = domConstruct.create(
          "div",
          {
            style: initStyle,
          },
          this._displayDiv
        );
        domConstruct.place(ele.dom, ele.node);
      }

      if (ele.option) {
        if (window?.echarts) {
          const myEchart = window.echarts.init(document.getElementById(ele.id));
          myEchart.setOption(ele.option);
        }
      }

      this.reposition(ele);
    },

    bindEvents: function () {
      this.events = [];
      this.viewpointWatchers = [];

      if (this.view.type === "2d") {
        this.events.push(
          reactiveUtils.watch(
            () => this.view.interacting,
            (_interacting: boolean) => {
              // console.log('=>(DomLayerView2D.ts:137) interacting', interacting);
              // if(_interacting) {
              //
              // }
              this.refresh();
            }
          )
        );
        this.events.push(
          reactiveUtils.watch(
            () => this.view.animation,
            (_animation: boolean) => {
              // console.log('=>(DomLayerView2D.ts:137) animation', animation);
              this.refresh();
            }
          )
        );
        this.events.push(
          reactiveUtils.watch(
            () => this.view.viewpoint,
            (_viewpoint: any) => {
              this.refresh();
            }
          )
        );
        this.events.push(
          // this.layer.watch('opacity', (opacity: any) => {
          //     domStyle.set(this._displayDiv, 'opacity', opacity);
          // }),
          reactiveUtils.watch(
            () => this.layer.opacity,
            (opacity: any) => {
              // console.log('=>(DomLayerView2D.ts:148) opacity', opacity);
              domStyle.set(this._displayDiv, "opacity", opacity);
            }
          )
        );
        this.events.push(
          // this.layer.watch('visible', (visible: any) => {
          //     domStyle.set(
          //         this._displayDiv,
          //         'display',
          //         visible ? 'block' : 'none',
          //     );
          //     this.refresh();
          //     this.refresh();
          // }),
          reactiveUtils.watch(
            () => this.layer.visible,
            (visible: any) => {
              // console.log('=>(DomLayerView2D.ts:148) visible', visible);
              domStyle.set(
                this._displayDiv,
                "display",
                visible ? "block" : "none"
              );
              this.refresh();
            }
          )
        );
      }
    },

    initialize(evt: any) {
      console.log("attach", evt);
      this.divLayerClass = "div-layer2d";
      const display = this.layer.visible ? "block" : "none";
      this._displayDiv = domConstruct.create("div", {
        innerHTML: "",
        style: `width:100%;height:100%;position: absolute;top: 0px;right: 0px;left: 0px;bottom: 0px;display:${display}`,
        className: this.divLayerClass,
      });

      console.log(this._displayDiv);

      const surface = this.view.root.children[1];
      domConstruct.place(this._displayDiv, surface);
      this.bindEvents();
      //   debugger;

      this.refresh();
    },

    calcTransform() {
      const matrix = new window.WebKitCSSMatrix(
        window.getComputedStyle(this._displayDiv).webkitTransform
      );
      const x = matrix.e;
      const y = matrix.f;

      this.transformOffset = {
        x: x,
        y: y,
      };
      return this.transformOffset;
    },

    getDomBox(dom: any) {
      const styles = window?.getComputedStyle(dom);

      // getComputedStyle() should return values already in pixels, so using parseInt()
      //   is not as much as a hack as it seems to be.

      return {
        minX: parseInt(styles.left),
        minY: parseInt(styles.top),
        maxX: parseInt(styles.left) + parseInt(styles.width),
        maxY: parseInt(styles.top) + parseInt(styles.height),
        height: parseFloat(styles.height),
        width: parseFloat(styles.width),
      };
    },

    refresh: function () {
      if (!this.layer.visible) return;
      if (window.__fantasy_map_isScene === true) {
        // this.destroy();
        return;
      }

      if (this.layer.minScale > 0) {
        domStyle.set(
          this._displayDiv,
          "opacity",
          this.view.scale >= this.layer.minScale ? 0 : 1
        );
      }
      if (this.layer.maxScale > 0) {
        domStyle.set(
          this._displayDiv,
          "opacity",
          this.view.scale < this.layer.maxScale ? 0 : 1
        );
      }

      // this.layer.box = []
      const rbush = new Rbush();
      this.calcTransform();
      console.time("refresh-domLayer");
      // domConstruct.empty(this._displayDiv);
      let inExtenCount = 0;
      this.layer.graphics.forEach((v: any) => {
        if (this.isInExtent(v)) {
          inExtenCount++;

          let notAdded = true;

          if (v.box) {
            const newPosition = this.getPosition(v);

            const eleHeight = v.box.height;
            const eleWidth = v.box.width;
            v.box = {
              minX: parseInt(newPosition.left),
              minY: parseInt(newPosition.top),
              maxX: parseInt(newPosition.left) + parseInt(eleWidth),
              maxY: parseInt(newPosition.top) + parseInt(eleHeight),
              height: parseFloat(eleHeight),
              width: parseFloat(eleWidth),
            };
          } else {
            notAdded = false;
            this._add(v);
            const box = this.getDomBox(v.node);

            v.box = box;
            domStyle.set(v.node, "height", v.box.height + "px");
            domStyle.set(v.node, "width", v.box.width + "px");

            if (this.direction == "center") {
              domStyle.set(v.node, "margin-left", "-" + v.box.width / 2 + "px");
              domStyle.set(v.node, "margin-top", "-" + v.box.height / 2 + "px");
            } else {
              domStyle.set(v.node, "margin-top", "-" + v.box.height + "px");
            }

            if (v.grade) domStyle.set(v.node, "z-index", 5 - Number(v.grade));
          }

          if (v.grade == "1" || v.grade == "2" || v.grade == "3") {
            rbush.insert(v.box);
            if (notAdded) {
              this._add(v);
              domStyle.set(v.node, "height", v.box.height + "px");
              domStyle.set(v.node, "width", v.box.width + "px");
              if (v.grade) domStyle.set(v.node, "z-index", 5 - Number(v.grade));
            }
            return;
          }
          const find = rbush.search(v.box);
          if (find.length) {
            domConstruct.destroy(v.node);
            v.node = null;
          } else {
            // if(box.minX&&box)
            rbush.insert(v.box);
            // this.layer.box.push(v.box);
            if (notAdded) {
              this._add(v);
              domStyle.set(v.node, "height", v.box.height + "px");
              domStyle.set(v.node, "width", v.box.width + "px");
              if (v.grade) domStyle.set(v.node, "z-index", 5 - Number(v.grade));
            }

            // domStyle.set(v.node, 'display', 'block');
          }
          // if (v.node) {

          // }
        } else {
          if (v.node) {
            // console.log('out range, destroy', v.node);
            domConstruct.destroy(v.node);
            v.node = null;
          }
        }
      });
      console.log("inExtenCount", inExtenCount);

      // setTimeout(() => {
      //     window.layer.closeAll('loading');
      // }, 1500);
      // $('.div-layer canvas').css('pointer-events', 'auto');
      console.timeEnd("refresh-domLayer");
    },

    isInExtent: function (ele: any) {
      return geometryEngine.contains(this.view.extent, ele.geometry);
    },
    // items: [],
    _repositionForDirection: function (ele: any) {
      if (ele.box) {
        const newSP = {};
        // var computedStyle = window.getComputedStyle(ele.node);
        const hieght = ele.box.height;
        const width = ele.box.width;

        switch (this.direction) {
          case "top-mid": {
            break;
          }
          case "center": {
            domStyle.set(ele.node, "margin-left", -width / 2 + "px");
            domStyle.set(ele.node, "margin-top", -hieght / 2 + "px");
            break;
          }
          default: {
            domStyle.set(ele.node, "margin-top", "-" + hieght + "px");
            break;
          }
        }

        return newSP;
      }
    },
    getPosition(ele: any) {
      if (this.view) {
        let sp = this.view.toScreen(ele.geometry);
        sp = {
          x: sp.x - this.transformOffset.x,
          y: sp.y - this.transformOffset.y,
          // x: sp.x,
          // y: sp.y,
        };

        return {
          left: sp.x,
          top: sp.y,
        };
      }
    },

    reposition: function (ele: any) {
      const position = this.getPosition(ele);
      if (position) {
        if (ele.node) {
          domStyle.set(ele.node, {
            top: position.top + "px",
            left: position.left - 65 + "px",
            // transform:
            //   'translate3d(' + position.left + 'px,' + position.top + 'px,0px )'
          });
          this._repositionForDirection(ele);
        } else {
          console.log("no node", ele);
        }
      }
    },
    detach() {
      this.destroy();
    },

    destroy() {
      domConstruct.destroy(this._displayDiv);

      Array.prototype.forEach.call(this.events, function (event) {
        if (event.remove) {
          event.remove();
        }
      });
      // arrayUtil.forEach(this.events, function(event) {
      //   if (event.remove) {
      //     event.remove();
      //   }
      // });
    },
  });

  return clazz;
};

export default getDomLayerView2D;

DomLayerView3D.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable no-debugger */
import requireArcgis from "../../../../../../../utils/requireArcgis";
import { domStyle, domClass, domConstruct, lang } from "./domOpt";
import Rbush from "rbush";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
const getDomLayerView3D = async function () {
  const [geometryEngine, LayerView, reactiveUtils] = await requireArcgis([
    "esri/geometry/geometryEngine",
    "esri/views/layers/LayerView",
    "esri/core/reactiveUtils",
  ]);
  const clazz = LayerView.createSubclass({
    transformOffset: {
      x: 0,
      y: 0,
    },
    isChanged: false,
    constructor(param: any) {
      this.supportsDraping = true;
      this.overlayUpdating = false;
      // console.log(LayerView);
      this.elevationInfo = null;
      param.layer.on("after-add", (param: any) => {
        this._add(param.item);
      });

      param.layer.on("after-remove", (param: any) => {
        this._remove(param.item);
      });
    },

    render(evt: any) {
      console.log(evt);
    },

    _remove(ele: any) {
      if (ele.node) {
        domConstruct.destroy(ele.node);
      }
    },
    viewChange() {
      //   console.log('viewChange', evt);

      const currentScreenTarget = this.view.toScreen(this.dragStartCenter);

      if (this.screenTargetGeoemtry) {
        const dx = currentScreenTarget.x - this.screenTargetGeoemtry.x;
        const dy = currentScreenTarget.y - this.screenTargetGeoemtry.y;
        const translate = "translate(" + dx + "px," + dy + "px)";

        domStyle.set(this._displayDiv, "transform", translate);
      }
    },
    moveStart() {
      // console.log('moveStart', evt);
      domClass.add(this._displayDiv, "moving");
      this.clearViewpointWatchers();

      this.calcTransform();
      this.dragStartCenter = this.view.center.clone();
      this.screenTargetGeoemtry = this.view.toScreen(this.dragStartCenter);

      this.screenTargetGeoemtry = {
        x: this.screenTargetGeoemtry.x - this.transformOffset.x,
        y: this.screenTargetGeoemtry.y - this.transformOffset.y,
      };
    },

    moveEnd() {
      // console.log('moveEnd', evt);
      this.clearViewpointWatchers();
      domClass.remove(this._displayDiv, "moving");
      window.requestAnimationFrame(() => {
        this.refresh();
        domStyle.set(this._displayDiv, "opacity", "1");
      });
    },

    clearViewpointWatchers: function () {
      while (this.viewpointWatchers.length) {
        let viewpointWatcher = this.viewpointWatchers.pop();
        if (viewpointWatcher) {
          viewpointWatcher.remove();
          viewpointWatcher = null;
        }
      }
    },

    _add: function (ele: any) {
      if (ele.node && ele.node.parentNode === this._displayDiv) {
        //todo
      } else {
        if (lang.isString(ele.dom)) {
          ele.html = ele.dom;
          ele.dom = domConstruct.toDom(ele.html);
        } else if (lang.isString(ele.html)) {
          document.getElementById(ele.id)?.remove();
          ele.dom = domConstruct.toDom(ele.html);
        }

        ele.node = domConstruct.create(
          "div",
          {
            style: "position:absolute;pointer-events: auto;",
          },
          this._displayDiv
        );

        domConstruct.place(ele.dom, ele.node);
      }
      if (ele.option) {
        if (window?.echarts) {
          const myEchart = window.echarts.init(ele.dom);
          myEchart.setOption(ele.option);
        }
      }
      this.reposition(ele);
      this._repositionForDirection(ele);
    },

    bindEvents: function () {
      this.events = [];
      this.viewpointWatchers = [];

      // this.events.push(
      //     this.view.watch('stationary', (stationary: any) => {
      //         if (stationary) {
      //             domStyle.set(this._displayDiv, 'opacity', 1);
      //             this.refresh();
      //         } else {
      //             domStyle.set(this._displayDiv, 'opacity', 0);
      //         }
      //     }),
      // );

      // this.view.watch('camera', function (evt) {});
      this.events.push(
        reactiveUtils.watch(
          () => this.view.interacting,
          (_interacting: boolean) => {
            // console.log('=>(DomLayerView2D.ts:137) interacting', interacting);
            // if(_interacting) {
            //
            // }
            this.refresh();
          }
        )
      );
      this.events.push(
        reactiveUtils.watch(
          () => this.view.animation,
          (_animation: boolean) => {
            // console.log('=>(DomLayerView2D.ts:137) animation', animation);
            this.refresh();
          }
        )
      );
      this.events.push(
        reactiveUtils.watch(
          () => this.view.viewpoint,
          (_viewpoint: any) => {
            this.refresh();
          }
        )
      );
      this.events.push(
        // this.layer.watch('opacity', (opacity: any) => {
        //     domStyle.set(this._displayDiv, 'opacity', opacity);
        // }),
        reactiveUtils.watch(
          () => this.layer.opacity,
          (opacity: any) => {
            // console.log('=>(DomLayerView2D.ts:148) opacity', opacity);
            domStyle.set(this._displayDiv, "opacity", opacity);
          }
        )
      );
      this.events.push(
        // this.layer.watch('visible', (visible: any) => {
        //     domStyle.set(
        //         this._displayDiv,
        //         'display',
        //         visible ? 'block' : 'none',
        //     );
        //     this.refresh();
        //     this.refresh();
        // }),
        reactiveUtils.watch(
          () => this.layer.visible,
          (visible: any) => {
            // console.log('=>(DomLayerView2D.ts:148) visible', visible);
            domStyle.set(
              this._displayDiv,
              "display",
              visible ? "block" : "none"
            );
            this.refresh();
          }
        )
      );
    },

    updateClippingExtent() {
      debugger;
    },
    setup() {
      debugger;
    },

    initialize() {
      console.log("initialize", this);
      this.divLayerClass = "div-layer3d";
      this._displayDiv = domConstruct.create("div", {
        innerHTML: "",
        style:
          "width:100%;height:100%;position: absolute;top: 0px;right: 0px;left: 0px;bottom: 0px;",
        className: this.divLayerClass,
      });

      console.log(this._displayDiv);

      const surface = this.view.root.children[1];
      domConstruct.place(this._displayDiv, surface);
      this.bindEvents();
      //   debugger;

      this.refresh();

      // this.on('after-changes', function (evt) {
      //     debugger;
      // });
    },

    doRefresh(evt: any) {
      console.log("doRefresh", evt);
    },

    calcTransform() {
      const matrix = new window.WebKitCSSMatrix(
        window?.getComputedStyle(this._displayDiv).webkitTransform
      );
      const x = matrix.e;
      const y = matrix.f;

      this.transformOffset = {
        x: x,
        y: y,
      };
      return this.transformOffset;
    },

    getDomBox(dom: any) {
      const styles = window?.getComputedStyle(dom);

      // getComputedStyle() should return values already in pixels, so using parseInt()
      //   is not as much as a hack as it seems to be.

      return {
        minX: parseInt(styles.left),
        minY: parseInt(styles.top),
        maxX: parseInt(styles.left) + parseInt(styles.width),
        maxY: parseInt(styles.top) + parseInt(styles.height),
        height: parseFloat(styles.height),
        width: parseFloat(styles.width),
      };
    },

    refresh: function () {
      if (!this.layer.visible) return;
      if (window.__fantasy_map_isScene === false) {
        // this.destroy();
        return;
      }

      const rbush = new Rbush();
      this.calcTransform();
      console.time("refresh-domLayer");
      // domConstruct.empty(this._displayDiv);
      this.layer.graphics.forEach((v: any) => {
        if (this.isInExtent(v)) {
          let notAdded = true;

          if (v.box) {
            const newPosition = this.getPosition(v);

            const eleHeight = v.box.height;
            const eleWidth = v.box.width;
            v.box = {
              minX: parseInt(newPosition.left),
              minY: parseInt(newPosition.top),
              maxX: parseInt(newPosition.left) + parseInt(eleWidth),
              maxY: parseInt(newPosition.top) + parseInt(eleHeight),
              height: parseFloat(eleHeight),
              width: parseFloat(eleWidth),
            };
          } else {
            notAdded = false;
            this._add(v);
            const box = this.getDomBox(v.node);

            v.box = box;
            domStyle.set(v.node, "height", v.box.height + "px");
            domStyle.set(v.node, "width", v.box.width + "px");

            if (this.direction == "center") {
              domStyle.set(v.node, "margin-left", "-" + v.box.width / 2 + "px");
              domStyle.set(v.node, "margin-top", "-" + v.box.height / 2 + "px");
            } else {
              domStyle.set(v.node, "margin-top", "-" + v.box.height + "px");
            }

            if (v.grade) domStyle.set(v.node, "z-index", 5 - Number(v.grade));
          }

          if (v.grade == "1" || v.grade == "2" || v.grade == "3") {
            rbush.insert(v.box);
            if (notAdded) {
              this._add(v);
              domStyle.set(v.node, "height", v.box.height + "px");
              domStyle.set(v.node, "width", v.box.width + "px");
              if (v.grade) domStyle.set(v.node, "z-index", 5 - Number(v.grade));
            }
            return;
          }

          const find = rbush.search(v.box);
          if (find.length) {
            domConstruct.destroy(v.node);
            v.node = null;
          } else {
            // if(box.minX&&box)
            rbush.insert(v.box);
            if (notAdded) {
              this._add(v);
              domStyle.set(v.node, "height", v.box.height + "px");
              domStyle.set(v.node, "width", v.box.width + "px");
              if (v.grade) domStyle.set(v.node, "z-index", 5 - Number(v.grade));
            }

            // domStyle.set(v.node, 'display', 'block');
          }
          // if (v.node) {

          // }
        } else {
          if (v.node) {
            domConstruct.destroy(v.node);
            v.node = null;
          }
        }
      });
      // setTimeout(() => {
      //     window.layer.closeAll('loading');
      // }, 1500);
      // $('.div-layer canvas').css('pointer-events','auto');
      console.timeEnd("refresh-domLayer");
    },

    isInExtent: function (ele: any) {
      return geometryEngine.contains(this.view.extent, ele.geometry);
    },
    // items: [],
    _repositionForDirection: function (ele: any) {
      if (ele.box) {
        const newSP = {};
        // var computedStyle = window.getComputedStyle(ele.node);
        const hieght = ele.box.height;
        const width = ele.box.width;

        switch (this.direction) {
          case "top-mid": {
            break;
          }
          case "center": {
            domStyle.set(ele.node, "margin-left", -width / 2 + "px");
            domStyle.set(ele.node, "margin-top", -hieght / 2 + "px");
            break;
          }
          default: {
            domStyle.set(ele.node, "margin-top", "-" + hieght + "px");
            break;
          }
        }

        return newSP;
      }
    },
    getPosition(ele: any) {
      if (this.view) {
        let sp = this.view.toScreen(ele.geometry);
        sp = {
          x: sp.x - this.transformOffset.x,
          y: sp.y - this.transformOffset.y,
        };

        return {
          left: sp.x,
          top: sp.y,
        };
      }
    },

    reposition: function (ele: any) {
      const position = this.getPosition(ele);
      if (position) {
        domStyle.set(ele.node, {
          top: position.top + "px",
          left: position.left + "px",
        });
      }
    },

    destroy() {
      domConstruct.destroy(this._displayDiv);

      Array.prototype.forEach.call(this.events, function (event) {
        if (event.remove) {
          event.remove();
        }
      });
      // arrayUtil.forEach(this.events, function(event) {
      //   if (event.remove) {
      //     event.remove();
      //   }
      // });
    },
  });

  return clazz;
};

export default getDomLayerView3D;

domOpt.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
export const domClass = {
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  add: function (dom: any, className: any) {
    dom?.classList.add(className);
  },
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  remove: function (dom: any, className: any) {
    dom?.classList.remove(className);
  },
};

export const domStyle = {
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  set: function (dom: any, styleKey: any, styleValue?: any) {
    if (typeof styleKey !== "string") {
      for (const key in styleKey) {
        if (key.includes("-")) {
          const arr = key.split("-");
          const newKey =
            arr[0] +
            arr[1].slice(0, 1).toUpperCase() +
            arr[1].slice(1).toLowerCase();
          dom.style[newKey] = styleKey[key];
        } else {
          dom.style[key] = styleKey[key];
        }
      }
    } else {
      if (styleKey.includes("-")) {
        const arr = styleKey.split("-");
        const newKey =
          arr[0] +
          arr[1].slice(0, 1).toUpperCase() +
          arr[1].slice(1).toLowerCase();
        dom.style[newKey] = styleValue;
      } else {
        dom.style[styleKey] = styleValue;
      }
    }
  },
};

export const domConstruct = {
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  create: function (tag: any, attrs: any, refNode?: any) {
    const el = window.document.createElement(tag);
    for (const attr in attrs) {
      if (attr == "style") {
        const arr = attrs["style"].split(";");
        // console.log(arr)
        for (const i in arr) {
          domStyle.set(el, arr[i].split(":")[0], arr[i].split(":")[1]);
        }
      } else {
        el[attr] = attrs[attr];
      }
    }
    if (refNode) {
      refNode.appendChild(el);
    }
    return el;
  },
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  place: function (subnode: any, node: any) {
    node.appendChild(subnode);
  },
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  destroy: function (node: any) {
    node?.parentNode?.removeChild(node);
  },
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  toDom: function (html: any) {
    const frag = document.createRange().createContextualFragment(html);
    return frag;
  },
  // create: _domConstruct.create,
  // place: _domConstruct.place,
  // destroy: _domConstruct.destroy,
  // toDom: _domConstruct.toDom
};

export const lang = {
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  isString: function (data: any) {
    return typeof data === "string";
  },
};

用法
#

dom是自定义的 dom 元素,id需要唯一。

const graphics = features.map((v, i: number) => {
  return {
    geometry: webMercatorUtils.geographicToWebMercator({
      type: "point",
      x: v.geometry.coordinate[0][0],
      y: v.geometry.coordinate[0][1],
      spatialReference: new SpatialReference(4326),
    }),
    dom: `<div id="domlayer-dom-${i}" class="infowindow ${
      setPptnColor(v.properties.ACCP)[0]
    } rain-condition-stpptnr" code="${v.properties.STCD}" sitename="${
      v.properties.STNM
    }"><div class="window">
              <span class="regin">${v.properties.ADNM}</span>
              <div class="title"><span>${
                v.properties.STNM
              }</span><span class="type">&nbsp;&nbsp;当日雨量</span><span class="num" rel="毫米">${
      v.properties.ACCP == null ? "--" : v.properties.ACCP
    }</span></div>
              <span class="no">${v.properties.STCD}</span>
              <div class="footer"></div></div></div>`,
    grade: setPptnColor(v.properties.ACCP)[1],
    id: `domlayer-dom-${i}`,
  };
});
layersFeature.add([
  {
    id: "111",
    title: "DomLayer",
    layerType: "DomLayer",
    layerName: "f",
    spatialReference: { wkid: 3857 },
    graphics,
  },
]);
layersFeature.add([
  {
    id: "pnts",
    title: "实际点位置",
    layerType: "GeoJSONLayer",
    url: "assets/domLayerExampleDataGeoJSON.json",
  },
]);