index.js

import { MAIN_LOOP_EVENTS } from 'itowns';
import * as THREE from 'three';

export class CameraPositioner extends THREE.EventDispatcher {
  /**
   * Creates a CameraPositioner
   *
   * @param {import('itowns').PlanarView} itownsView - the itowns view object
   */
  constructor(itownsView) {
    super();

    // start dom creation

    this.domElement = document.createElement('div');

    this.sectionTitle = document.createElement('h3');
    this.sectionTitle.innerText = 'Coordinates';
    this.domElement.appendChild(this.sectionTitle);

    this.form = document.createElement('form');
    this.domElement.appendChild(this.form);

    const createInputAndAddToParent = (labelText, name, parent) => {
      const label = document.createElement('label');
      label.innerText = labelText;

      const uuid = THREE.MathUtils.generateUUID();

      label.setAttribute('for', uuid);

      const input = document.createElement('input');
      input.setAttribute('type', 'text');
      input.setAttribute('id', uuid);
      input.setAttribute('name', name);

      parent.appendChild(label);
      parent.appendChild(input);

      return input;
    };

    const fieldsetPosition = document.createElement('fieldset');
    this.form.appendChild(fieldsetPosition);

    const legendPosition = document.createElement('legend');
    legendPosition.innerText = 'Position';
    fieldsetPosition.appendChild(legendPosition);

    this.positionXElement = createInputAndAddToParent(
      'X',
      'positionX',
      fieldsetPosition
    );
    this.positionYElement = createInputAndAddToParent(
      'Y',
      'positionY',
      fieldsetPosition
    );
    this.positionZElement = createInputAndAddToParent(
      'Z',
      'positionZ',
      fieldsetPosition
    );

    const fieldsetQuaternion = document.createElement('fieldset');
    this.form.appendChild(fieldsetQuaternion);

    const legendQuaternion = document.createElement('legend');
    legendQuaternion.innerText = 'Quaternion';
    fieldsetQuaternion.appendChild(legendQuaternion);

    this.quaternionXElement = createInputAndAddToParent(
      'X',
      'quaternionX',
      fieldsetQuaternion
    );
    this.quaternionYElement = createInputAndAddToParent(
      'Y',
      'quaternionY',
      fieldsetQuaternion
    );
    this.quaternionZElement = createInputAndAddToParent(
      'Z',
      'quaternionZ',
      fieldsetQuaternion
    );
    this.quaternionWElement = createInputAndAddToParent(
      'W',
      'quaternionW',
      fieldsetQuaternion
    );

    this.buttonValidate = document.createElement('button');
    this.buttonValidate.setAttribute('type', 'button');
    this.buttonValidate.innerText = 'Validate';
    this.form.appendChild(this.buttonValidate);

    this.buttonTravel = document.createElement('input');
    this.buttonTravel.setAttribute('type', 'submit');
    this.buttonTravel.setAttribute('value', 'Travel');
    this.form.appendChild(this.buttonTravel);

    // end dom creation

    this.itownsView = itownsView;

    // Request update every active frame
    this.itownsView.addFrameRequester(
      MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE,
      () => this._updateFieldsFromCamera()
    );

    // callbacks
    this.form.onsubmit = () => {
      this._travel();
      return false;
    };

    this.buttonValidate.onclick = () => {
      this._validate();
    };
  }

  // ///////////////////////
  // /// POSITION MANAGEMENT

  /**
   * Updates the form fields from the camera position.
   */
  _updateFieldsFromCamera() {
    if (this.domElement.parentElement) {
      // html is present in DOM update fields
      const camera = this.itownsView.camera.camera3D;
      const position = camera.position;
      const quaternion = camera.quaternion;
      this.positionXElement.value = position.x;
      this.positionYElement.value = position.y;
      this.positionZElement.value = position.z;
      this.quaternionXElement.value = quaternion.x;
      this.quaternionYElement.value = quaternion.y;
      this.quaternionZElement.value = quaternion.z;
      this.quaternionWElement.value = quaternion.w;
    }
  }

  /**
   * Retrieve the current camera position from the form fields.
   *
   * @returns {{position: THREE.Vector3, quaternion: THREE.Quaternion}} Returns a position and a rotation
   */
  _getCameraPosition() {
    const data = new FormData(this.form);
    const position = new THREE.Vector3();
    const quaternion = new THREE.Quaternion();

    position.x = Number(data.get('positionX'));
    position.y = Number(data.get('positionY'));
    position.z = Number(data.get('positionZ'));

    quaternion.x = Number(data.get('quaternionX'));
    quaternion.y = Number(data.get('quaternionY'));
    quaternion.z = Number(data.get('quaternionZ'));
    quaternion.w = Number(data.get('quaternionW'));

    return {
      position: position,
      quaternion: quaternion,
    };
  }

  /**
   * Make the camera travel to the position and orientation in the form fields.
   */
  async _travel() {
    const camera = this._getCameraPosition();
    this.itownsView.controls.initiateTravel(
      camera.position,
      'auto',
      camera.quaternion,
      true
    );
  }

  /**
   * Sends the position submitted event with the current camera position and
   * orientation.
   */
  _validate() {
    const camera = this._getCameraPosition();
    this.dispatchEvent({
      type: CameraPositioner.EVENT_POSITION_SUBMITTED,
      message: camera,
    });
  }

  // //////////
  // /// EVENTS

  static get EVENT_POSITION_SUBMITTED() {
    return 'EVENT_POSITION_SUBMITTED';
  }
}