STSCircle.js

import * as THREE from 'three';
import { MAIN_LOOP_EVENTS } from 'itowns';
import { createSpriteFromString } from '@ud-viz/utils_browser/src/THREEUtil';
import { STLayer } from './STLayer';
import { DISPLAY_MODE, STShape } from './STShape';

export class STSCircle extends STShape {
  /**
   *
   * @param {STLayer} stLayer The STLayer instance used to create the shape
   * @param {object} options Options of the shape
   * @param {number} options.radius Radius of the helix
   * @param {number} options.height Height at which the circle is drawn
   */
  constructor(stLayer, options = {}) {
    super(stLayer);

    /** @type {number} */
    this.radius = isNaN(options.radius) ? 1000 : options.radius;
    /** @type {number} */
    this.height = isNaN(options.height) ? 550 : options.height;

    this.frameRequester = null;

    /** @type {Array<THREE.Object3D>} */
    this.objectCopies = null;
    /** @type {THREE.Line} */
    this.circleLine = null;
    /** @type {THREE.Line} */
    this.dashedLine = null;

    /** @type {number} */
    this.selectedDate = null;
    /** @type {boolean} */
    this.pause = false;
  }

  display(displayMode = DISPLAY_MODE.SEQUENTIAL) {
    super.display();

    const view = this.stLayer.view;
    const rootObject3D = this.stLayer.rootObject3D;

    rootObject3D.position.z += this.height;
    // Init circle line
    const pointsDisplayed = [];
    for (let i = 90; i <= 360; i += 10) {
      const angle = (i * Math.PI) / 180;
      pointsDisplayed.push(
        new THREE.Vector3(
          this.radius * Math.cos(angle),
          this.radius * Math.sin(angle),
          0
        )
      );
    }
    const geometryDisplayed = new THREE.BufferGeometry().setFromPoints(
      pointsDisplayed
    );
    const materialDisplayed = new THREE.LineBasicMaterial({ color: 0x0000ff });
    const circleLine = new THREE.Line(geometryDisplayed, materialDisplayed);
    this.circleLine = circleLine;

    rootObject3D.add(circleLine);
    circleLine.updateMatrixWorld();

    const dashedLineGeom = new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(0, 0, 0),
      new THREE.Vector3(0, 0, this.height),
    ]);
    const dashedLineMaterial = new THREE.LineDashedMaterial({
      color: 0x0000ff,
      linewidth: 1,
      scale: 1,
      dashSize: 3,
      gapSize: 3,
    });
    const dashedLine = new THREE.Line(dashedLineGeom, dashedLineMaterial);
    dashedLine.computeLineDistances();
    this.dashedLine = dashedLine;

    dashedLine.updateMatrixWorld();

    // Place versions cdtlayers + labels on the circle
    this.objectCopies = new Map();

    let yearDelta;
    let interval;

    const firstDate = this.stLayer.versions[0].date;

    this.stLayer.versions.forEach((version) => {
      const objectCopy = new THREE.Object3D().copy(
        version.c3DTLayer.root,
        true
      );
      this.objectCopies[version.date] = objectCopy;
      rootObject3D.add(objectCopy);
      switch (displayMode) {
        case DISPLAY_MODE.SEQUENTIAL: {
          interval = this.stLayer.versions.indexOf(version);
          yearDelta = 270 / (this.stLayer.versions.length - 1);
          break;
        }
        case DISPLAY_MODE.CHRONOLOGICAL: {
          interval = version.date - firstDate;
          yearDelta = 270 / this.stLayer.dateInterval;
          break;
        }
      }

      const angleDeg = yearDelta * -interval;
      const angleRad = (angleDeg * Math.PI) / 180;

      const point = new THREE.Vector3(
        this.radius * Math.cos(angleRad),
        this.radius * Math.sin(angleRad),
        0
      );

      version.c3DTLayer.visible = false;

      const dateSprite = createSpriteFromString(version.date.toString());

      const newPosition = new THREE.Vector3(
        circleLine.position.x + point.x,
        circleLine.position.y + point.y,
        circleLine.position.z
      );

      // position C3DTLayer
      objectCopy.position.copy(newPosition);
      for (let i = 0; i < objectCopy.children.length; i++) {
        const child = objectCopy.children[i];
        const tileId = version.c3DTLayer.root.children[i].tileId;
        const tile = version.c3DTLayer.tileset.tiles[tileId];
        const tileTransform = tile.transform.elements;
        const tilePosition = new THREE.Vector3(
          tileTransform[12],
          tileTransform[13],
          tileTransform[14]
        );
        child.position.copy(tilePosition.sub(this.layerCentroid));
      }

      // Date label sprite
      dateSprite.position.z += 40;
      dateSprite.scale.multiplyScalar(0.02);
      objectCopy.add(dateSprite);
    });
    rootObject3D.updateMatrixWorld();

    view.notifyChange();

    if (!this.frameRequester) {
      this.frameRequester = this.update.bind(this);

      view.addFrameRequester(
        MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE,
        this.frameRequester
      );
    }
    if (this.selectedDate) this.selectVersion(this.selectedDate);
  }

  selectVersion(date) {
    this.selectedDate = date;
    const object3dCopySelected = this.objectCopies[date];
    const offset = object3dCopySelected.position.clone();

    this.stLayer.rootObject3D.children.forEach((object) => {
      object.position.sub(offset);
      object.position.z = 0;
    });

    object3dCopySelected.position.z = -this.height;
    object3dCopySelected.add(this.dashedLine);

    this.stLayer.rootObject3D.updateMatrixWorld(true);
    this.stLayer.view.notifyChange();
  }

  update() {
    if (this.pause) return;
    // Compute the angle between camera and the base layer.
    if (!this.stLayer.rootObject3D.children.length) return;

    const dirToCamera = new THREE.Vector2(
      this.layerCentroid.x - this.stLayer.view.camera.camera3D.position.x,
      this.layerCentroid.y - this.stLayer.view.camera.camera3D.position.y
    ).normalize();
    let dirObject = this.circleLine.position.clone().normalize();
    dirObject = new THREE.Vector2(dirObject.x, dirObject.y);

    let angle = dirObject.angleTo(dirToCamera);
    const orientation =
      dirToCamera.x * dirObject.y - dirToCamera.y * dirObject.x;
    if (orientation > 0) angle = 2 * Math.PI - angle;

    // Update position of the circle
    if (!this.stLayer.rootObject3D) return;

    this.stLayer.rootObject3D.rotation.set(0, 0, angle);

    for (const date in this.objectCopies) {
      this.objectCopies[date].rotation.set(0, 0, -angle);
    }

    this.stLayer.rootObject3D.updateMatrixWorld();
  }

  dispose() {
    super.dispose();
    this.objectCopies = null;
    if (this.frameRequester != null)
      this.stLayer.view.removeFrameRequester(
        MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE,
        this.frameRequester
      );
    this.frameRequester = null;
  }
}