Source: primitives/markerCluster.js

const THREE = require('three');
const markerImage = new Image(128, 128);
markerImage.src = require("../assets/mapMarkerOrange.svg");
const texture = new THREE.Texture();
texture.image = markerImage;
texture.needsUpdate = true;
const size = [0.02, 0.03, 1];
const spriteMaterial = new THREE.SpriteMaterial({
  map: texture,
  alphaTest: 0.5,
  transparent: true,
  depthTest: false,
  depthWrite: false,
  sizeAttenuation: false
});
const createNewSpriteText = require('../utilities').createNewSpriteText;

/**
 * A special graphics type with a tear drop shape.
 * It forms a cluster a marker based on distance.
 * 
 * @class
 * @author Alan Wu
 * @return {Marker}
 */
const MarkerCluster = function(sceneIn) {
  (require('./zincObject').ZincObject).call(this);
  this.texture = texture;
  let sprite = undefined;
  let scene = sceneIn;
  this.morph = new THREE.Group();
  this.group = this.morph;
  this.isMarkerCluster = true;
  let enabled = true;
  let sprites = [];
  this.markers = {};
  let _v21 = new THREE.Vector2();
  let _v22 = new THREE.Vector2();
  let _radius = 0.1;
  let start = Date.now();

  /**
   * Set the size of the marker.
   * 
   * @param {Number} size - size to be set.
   */ 
  this.setSpriteSize = size => {
    sprite.scale.set(0.015, 0.02, 1);
    sprite.scale.multiplyScalar(size);
  }

  this.clear = () => {
    this.group.clear();
    this.markers = {};
  }

  /**
   * Clean up this object,
   */ 
  this.dispose = () => {
    this.clear();
    if (this.morph) {
      this.morph.clear();
    }
  }

  const createNewSprite = (index) => {
    //Group is needed to set the position after scaling
    //the sprite
    const localGroup = new THREE.Group();
    const sprite = new THREE.Sprite(spriteMaterial);
    sprite.clusterIndex = index;
    sprite.center.set(0.5, 0);
    sprite.position.set(0, 0, 0);
    sprite.renderOrder = 10000;
    sprite.scale.set(size[0], size[1], size[2]);
    sprite.userData = this;
    localGroup.add(sprite);
    this.group.add(localGroup);
    return {
      "group": localGroup,
      "marker": sprite,
      "label": undefined,
      "number": 0,
      "min": [0, 0, 0],
      "max": [1, 1, 1],
    };
  }

  const activateSpriteForCluster = (sprite, cluster, number) => {
    sprite.group.visible = true;
    sprite.group.position.set(
      cluster.coords[0], cluster.coords[1], cluster.coords[2]
    );
    if (sprite.label === undefined || (number !== sprite.number)) {
      if (sprite.label) {
        sprite.group.remove(sprite.label);
        sprite.label.material.map.dispose();
        sprite.label.material.dispose();
      }
      sprite.label = createNewSpriteText(number, 0.012, "black", "Asap", 50, 500);
      sprite.number = number;
      sprite.group.add(sprite.label);
    }
    sprite.min = cluster.min;
    sprite.max = cluster.max;
  }

  const drawClusters = (clusters) => {
    let currentIndex = 0;
    clusters.forEach((cluster) => {
      const length = cluster.members.length;
      let number = 0;
      if (length === 1) {
        cluster.members[0].setVisibility(true);
      } else {
        cluster.members.forEach((marker) => {
          number += marker.getNumber();
          marker.setVisibility(false);
        });
        if (!sprites[currentIndex]) {
          sprites.push(createNewSprite(currentIndex));
        }
        activateSpriteForCluster(sprites[currentIndex], cluster, number);
        currentIndex++;
      }
    });
    for (currentIndex; currentIndex < sprites.length; currentIndex++) {
      sprites[currentIndex].group.visible = false;
    }
  }

  //Get clusters based on the ndc coordinate for each cluster.
  const getCluster = (markersObj, clusters) => {
    let first = true;
    let newCluster = {members: [], coords: [0,0,0], min: [0, 0, 0], max: [1, 1, 1]};
    let dist = 0
    for (let prop in markersObj) {
      if (first) {
        _v21.set(markersObj[prop].ndc.x, markersObj[prop].ndc.y);
        this._b1.setFromPoints([markersObj[prop].morph.position]);
        first = false;
        newCluster.members.push(markersObj[prop]);
        newCluster.coords = [
          markersObj[prop].morph.position.x,
          markersObj[prop].morph.position.y,
          markersObj[prop].morph.position.z,
        ]
        clusters.push(newCluster);
        delete markersObj[prop];
      } else {
        _v22.set(markersObj[prop].ndc.x, markersObj[prop].ndc.y);
        dist = _v21.distanceTo(_v22);
        if (_radius > dist) {
          newCluster.members.push(markersObj[prop]);
          this._b1.expandByPoint(markersObj[prop].morph.position);
          delete markersObj[prop];
        }
      }
    }
    newCluster.min = [this._b1.min.x, this._b1.min.y, this._b1.min.z];
    newCluster.max = [this._b1.max.x, this._b1.max.y, this._b1.max.z];
    //this._b1.getCenter(this._v2);
    //newCluster.coords = [this._v2.x, this._v2.y, this._v2.z];
    //The following will not be called if there is object left and
    //thus finishing clustering
    if (first !== true) {
      getCluster(markersObj, clusters);
    }
  }

  this.calculate = () => {
    if (enabled) {
      const current = Date.now();
      if ((current - start) > 500) {
        let clusters = [];
        getCluster({...this.markers}, clusters);
        drawClusters(clusters);
        start = Date.now();
        this.markerUpdateRequired = false;
      }
    }
  }

  this.isEnabled = () => {
    return enabled;
  }

  /**
   * Enable and visualise the marker.
   */  
  this.enable = () => {
    enabled = true;
    this.morph.visible = true;
  }

  /**
   * Disable and hide the marker.
   */ 
  this.disable = () => {
    enabled = false;
    this.morph.visible = false;
    //turn all markers back on
    for (let prop in this.markers) {
      if (this.markers[prop]?.isMarker &&
        this.markers[prop].isEnabled()) {
        this.markers[prop].setVisibility(true);
      }
    }
  }

  this.zoomToCluster = (index) => {
    if (index !== undefined && index > -1) {
      this._v1.set(...sprites[index].min);
      this._v2.set(...sprites[index].max);
      this._b1.set(this._v1, this._v2);
      scene.translateBoundingBoxToCameraView(this._b1, 3, 300);
      this.markerUpdateRequired = true;
    }
  }

  this.clusterIsVisible = (index) => {
    if (index !== undefined && index > -1) {
      if (sprites[index]) {
        return sprites[index].group?.visible;
      }
    }
    return false;
  }
}

MarkerCluster.prototype = Object.create((require('./zincObject').ZincObject).prototype);
exports.MarkerCluster = MarkerCluster;