Source: SynchronizedOverlay.js

/**
 * @module
 */
import olBase from 'ol';
import olOverlay from 'ol/Overlay';
import olProj from 'ol/proj';
import olDom from 'ol/dom';
import olObservable from 'ol/Observable';

const exports = function(options) {
  /**
   * @private
   * @type {?Function}
   */
  this.scenePostRenderListenerRemover_ = null;

  /**
   * @private
   * @type {!Cesium.Scene}
   */
  this.scene_ = options.scene;

  /**
   * @private
   * @type {!olcs.OverlaySynchronizer}
   */
  this.synchronizer_ = options.synchronizer;

  /**
   * @private
   * @type {!ol.Overlay}
   */
  this.parent_ = options.parent;

  /**
   * @private
   * @type {ol.Coordinate|undefined}
   */
  this.positionWGS84_ = undefined;

  /**
   * @private
   * @type {MutationObserver}
   */
  this.observer_ = new MutationObserver(this.handleElementChanged.bind(this));
  /**
   * @private
   * @type {Array.<MutationObserver>}
   */
  this.attributeObserver_ = [];

  /**
   * @private
   * @type {Array<ol.EventsKey>}
   */
  this.listenerKeys_ = [];
  // synchronize our Overlay with the parent Overlay
  this.listenerKeys_.push(this.parent_.on('change:position', this.setPropertyFromEvent_.bind(this)));
  this.listenerKeys_.push(this.parent_.on('change:element', this.setPropertyFromEvent_.bind(this)));
  this.listenerKeys_.push(this.parent_.on('change:offset', this.setPropertyFromEvent_.bind(this)));
  this.listenerKeys_.push(this.parent_.on('change:position', this.setPropertyFromEvent_.bind(this)));
  this.listenerKeys_.push(this.parent_.on('change:positioning', this.setPropertyFromEvent_.bind(this)));

  olOverlay.call(this, this.parent_.getOptions());
  this.setProperties(this.parent_.getProperties());

  this.handleMapChanged();
};

olBase.inherits(exports, olOverlay);

/**
 * @param {Node} target
 * @private
 */
exports.prototype.observeTarget_ = function(target) {
  this.observer_.disconnect();
  this.observer_.observe(target, {
    attributes: false,
    childList: true,
    characterData: true,
    subtree: true
  });
  this.attributeObserver_.forEach((observer) => {
    observer.disconnect();
  });
  this.attributeObserver_.length = 0;
  for (let i = 0; i < target.childNodes.length; i++) {
    const node = target.childNodes[i];
    if (node.nodeType === 1) {
      const observer = new MutationObserver(this.handleElementChanged.bind(this));
      observer.observe(node, {
        attributes: true,
        subtree: true
      });
      this.attributeObserver_.push(observer);
    }
  }
};

/**
 *
 * @param {ol.Object.Event} event
 * @private
 */
exports.prototype.setPropertyFromEvent_ = function(event) {
  if (event.target && event.key) {
    this.set(event.key, event.target.get(event.key));
  }
};
/**
 * Get the scene associated with this overlay.
 * @see ol.Overlay.prototype.getMap
 * @return {!Cesium.Scene} The scene that the overlay is part of.
 * @api
 */
exports.prototype.getScene = function() {
  return this.scene_;
};



/**
 * @override
 */
exports.prototype.handleMapChanged = function() {
  if (this.scenePostRenderListenerRemover_) {
    this.scenePostRenderListenerRemover_();
    olDom.removeNode(this.element);
  }
  this.scenePostRenderListenerRemover_ = null;
  const scene = this.getScene();
  if (scene) {
    this.scenePostRenderListenerRemover_ = scene.postRender.addEventListener(this.updatePixelPosition.bind(this));
    this.updatePixelPosition();
    const container = this.stopEvent ?
      this.synchronizer_.getOverlayContainerStopEvent() : this.synchronizer_.getOverlayContainer();
    if (this.insertFirst) {
      container.insertBefore(this.element, container.childNodes[0] || null);
    } else {
      container.appendChild(this.element);
    }
  }
};

/**
 * @override
 */
exports.prototype.handlePositionChanged = function() {
  // transform position to WGS84
  const position = this.getPosition();
  if (position) {
    const sourceProjection = this.parent_.getMap().getView().getProjection();
    this.positionWGS84_ = olProj.transform(position, sourceProjection, 'EPSG:4326');
  } else {
    this.positionWGS84_ = undefined;
  }
  this.updatePixelPosition();
};

/**
 * @override
 */
exports.prototype.handleElementChanged = function() {
  function cloneNode(node, parent) {
    const clone = node.cloneNode();
    if (parent) {
      parent.appendChild(clone);
    }
    if (node.nodeType != Node.TEXT_NODE) {
      clone.addEventListener('click', (event) => {
        node.dispatchEvent(new MouseEvent('click', event));
        event.stopPropagation();
      });
    }
    const nodes = node.childNodes;
    for (let i = 0; i < nodes.length; i++) {
      if (!nodes[i]) {
        continue;
      }
      cloneNode(nodes[i], clone);
    }
    return clone;
  }
  olDom.removeChildren(this.element);
  const element = this.getElement();
  if (element) {
    if (element.parentNode && element.parentNode.childNodes) {
      for (const node of element.parentNode.childNodes) {
        const clonedNode = cloneNode(node, null);
        this.element.appendChild(clonedNode);
      }
    }
  }
  if (element.parentNode) {
    // set new Observer
    this.observeTarget_(element.parentNode);
  }
};


/**
 * @override
 */
exports.prototype.updatePixelPosition = function() {
  const position = this.positionWGS84_;
  if (!this.scene_ || !position) {
    this.setVisible(false);
    return;
  }
  let cartesian;
  if (position.length === 2) {
    cartesian = Cesium.Cartesian3.fromDegreesArray(position)[0];
  } else {
    cartesian = Cesium.Cartesian3.fromDegreesArrayHeights(position)[0];
  }
  const camera = this.scene_.camera;
  const ellipsoidBoundingSphere = new Cesium.BoundingSphere(new Cesium.Cartesian3(), 6356752);
  const occluder = new Cesium.Occluder(ellipsoidBoundingSphere, camera.position);
  // check if overlay position is behind the horizon
  if (!occluder.isPointVisible(cartesian)) {
    this.setVisible(false);
    return;
  }
  const cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  // check if overlay position is visible from the camera
  if (cullingVolume.computeVisibility(new Cesium.BoundingSphere(cartesian)) !== 1) {
    this.setVisible(false);
    return;
  }
  this.setVisible(true);

  const pixelCartesian = this.scene_.cartesianToCanvasCoordinates(cartesian);
  const pixel = [pixelCartesian.x, pixelCartesian.y];
  const mapSize = [this.scene_.canvas.width, this.scene_.canvas.height];
  this.updateRenderedPosition(pixel, mapSize);
};

/**
 * Destroys the overlay, removing all its listeners and elements
 * @api
 */
exports.prototype.destroy = function() {
  if (this.scenePostRenderListenerRemover_) {
    this.scenePostRenderListenerRemover_();
  }
  if (this.observer_) {
    this.observer_.disconnect();
  }
  olObservable.unByKey(this.listenerKeys_);
  this.listenerKeys_.splice(0);
  if (this.element.removeNode) {
    this.element.removeNode(true);
  } else {
    this.element.remove();
  }
  this.element = null;
};
export default exports;