STSParabola.js

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

export class STSParabola extends STShape {
  /**
   *
   * @param {STLayer} stLayer The STLayer instance used to create the shape
   * @param {object} options Options of the shape
   * @param {number} options.distAxisX Distance from control point on X axis
   * @param {number} options.distAxisY Distance from control point on Y axis
   * @param {number} options.height Height at which the parabola is drawn
   */
  constructor(stLayer, options = {}) {
    super(stLayer);

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

    /** @type {THREE.Line} */
    this.dashedLine = null;
  }

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

    const view = this.stLayer.view;
    const rootObject3D = this.stLayer.rootObject3D;
    rootObject3D.position.z += this.height;

    const dashedLineGeom = new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(0, 0, -this.height),
      new THREE.Vector3(0, 0, 0),
    ]);
    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;

    rootObject3D.add(dashedLine);
    dashedLine.updateMatrixWorld();

    const path = new THREE.Path();
    path.moveTo(-this.distAxisX, this.distAxisY);
    const controlPoint1 = new THREE.Vector2(-this.distAxisX / 2, 0);
    path.quadraticCurveTo(controlPoint1.x, controlPoint1.y, 0, 0);
    const controlPoint2 = new THREE.Vector2(this.distAxisX / 2, 0);
    path.quadraticCurveTo(
      controlPoint2.x,
      controlPoint2.y,
      this.distAxisX,
      this.distAxisY
    );

    const numberOfDivisions = 50;
    const middleIndex = numberOfDivisions / 2;

    let points = path.getSpacedPoints(numberOfDivisions);

    const length = this.stLayer.versions.length;
    const middleVersion = this.stLayer.versions.find(
      (v) => v.date == this.middleDate
    );

    let yearDelta;
    let minIndex;
    let maxIndex;
    switch (displayMode) {
      case DISPLAY_MODE.SEQUENTIAL: {
        const nLeft = this.stLayer.versions.indexOf(middleVersion);
        const nRight = length - 1 - nLeft;
        yearDelta = Math.round(middleIndex / (length - 1));
        minIndex = middleIndex - yearDelta * nLeft;
        maxIndex = middleIndex + yearDelta * nRight;
        break;
      }
      case DISPLAY_MODE.CHRONOLOGICAL: {
        yearDelta = numberOfDivisions / 2 / this.stLayer.dateInterval;
        minIndex =
          middleIndex -
          (this.middleDate - this.stLayer.versions[0].date) * yearDelta;
        maxIndex =
          middleIndex +
          (this.stLayer.versions[length - 1].date - this.middleDate) *
            yearDelta;
        break;
      }
    }

    points = points.slice(
      Math.max(Math.round(minIndex), 0),
      Math.min(Math.round(maxIndex) + 1, points.length)
    );

    const geometryDisplayed = new THREE.BufferGeometry().setFromPoints(points);

    const materialDisplayed = new THREE.LineBasicMaterial({ color: 0x0000ff });
    const parabolaLine = new THREE.Line(geometryDisplayed, materialDisplayed);

    rootObject3D.add(parabolaLine);

    this.stLayer.versions.forEach((version) => {
      const objectCopy = new THREE.Object3D().copy(
        version.c3DTLayer.root,
        true
      );
      rootObject3D.add(objectCopy);

      version.c3DTLayer.visible = false;

      let curveIndex;
      switch (displayMode) {
        case DISPLAY_MODE.SEQUENTIAL: {
          curveIndex = Math.max(
            0,
            Math.min(
              points.length - 1,
              yearDelta * this.stLayer.versions.indexOf(version)
            )
          );
          break;
        }
        case DISPLAY_MODE.CHRONOLOGICAL: {
          const interval = version.date - this.stLayer.versions[0].date;
          curveIndex = yearDelta * interval;
          break;
        }
      }
      const curvePoint = points[Math.round(curveIndex)];
      const newPosition = new THREE.Vector3(curvePoint.x, curvePoint.y, 0);

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

      // position C3DTLayer
      objectCopy.position.copy(newPosition);
      if (version == middleVersion)
        objectCopy.position.copy(new THREE.Vector3(0, 0, -this.height));
      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();
  }

  dispose() {
    super.dispose();
  }

  /**
   *
   * @param {STLayer} stLayer The STLayer instance used to create the shape
   */
  setSTLayer(stLayer) {
    super.setSTLayer(stLayer);

    if (stLayer)
      /** @type {number} */
      this.middleDate =
        this.stLayer.versions[
          Math.round((this.stLayer.versions.length - 1) / 2)
        ].date;
  }
}