Source: region.js

const { Group, Matrix4 } = require('three');
const Pointset = require('./primitives/pointset').Pointset;
const Lines = require('./primitives/lines').Lines;
const Lines2 = require('./primitives/lines2').Lines2;
const Geometry = require('./primitives/geometry').Geometry;
const THREE = require('three');
let uniqueiId = 0;

const getUniqueId = function () {
  return "re" + uniqueiId++;
}

/**
 * Provides a hierachical structure to objects, Each region
 * may contain multiple child regions and {@link ZincObject}.
 * 
 * @class
 * @author Alan Wu
 * @return {Region}
 */
let Region = function (parentIn, sceneIn) {
  let parent = parentIn;
  let group = new Group();
  group.matrixAutoUpdate = false;
  group.userData = this;
  let children = [];
  let name = "";
  let zincObjects = [];
  let scene = sceneIn;
  const tMatrix = new Matrix4();
  let duration = 3000;
  tMatrix.set(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
  this.pickableUpdateRequired = true;
  this.isRegion = true;
  this.uuid = getUniqueId();
  

  /**
   * Hide all primitives belong to this region.
   */
  this.hideAllPrimitives = () => {
    children.forEach(child => child.hideAllPrimitives());
    zincObjects.forEach(zincObject => zincObject.setVisibility(false));
  }

  /**
   * Show all primitives belong to this region.
   */
  this.showAllPrimitives = () => {
    children.forEach(child => child.showAllPrimitives());
    zincObjects.forEach(zincObject => zincObject.setVisibility(true));
  }

  /**
   * Set the visibility and propagate it down the hierarchies
   * depending on the flag.
   * 
   * @param {Boolean} flag - A flag indicating either the visibilty to be on/off.
   */
  this.setVisibility = (flag) => {
    if (flag != group.visible) {
      group.visible = flag;
      this.pickableUpdateRequired = true;
    }
  }

  /**
   * Get the visibility of the region and its children.
   * 
   * @return {Boolean}
   */
  this.getVisibility = () => {
    return group.visible;
  }

  /**
   * Get the {THREE.Group} containing all child regions and their
   * primitives.
   * 
   * @return {THREE.Group}
   */
  this.getGroup = () => {
    return group;
  }

  /**
   * Set the transformation with a {THREE.Matrix4} matrix, this will affect
   * all primitives in this and its child regions
   * 
   * @param {THREE.Matrix4} transformation - The transformation matrix
   * used for the transformation.
   */
  this.setTransformation = transformation => {
    tMatrix.set(...transformation);
    group.matrix.copy(tMatrix);
    group.updateMatrixWorld();
  }

  /**
   * Set the name of this region.
   * 
   * @param {String} nameIn - Name to be set for this region. It must be defined
   * and non-empty.
   */
  this.setName = (nameIn) => {
    if (nameIn && nameIn !== "") {
      name = nameIn;
    }
  }

  /**
   * Get the name of this region.
   * 
   * @return {String}
   */
  this.getName = () => {
    return name;
  }

  /**
   * Get the parent region.
   * 
   * @return {Region}
   */
  this.getParent = () => {
    return parent;
  }

  /**
   * Get the array of each hierarachy from the root region to this region.
   * 
   * @return {Array}
   */
  this.getFullSeparatedPath = () => {
    const paths = [];
    if (name !== "") {
      paths.push(name);
      for (let p = parent; p !== undefined;) {
        const parentName = p.getName();
        if (parentName !== "") {
          paths.unshift(parentName);
        }
        p = p.getParent();
      }
    }
    return paths;
  }

  /**
   * Get the full paths from the root region to this region.
   * 
   * @return {String}
   */
  this.getFullPath = () => {
    const paths = this.getFullSeparatedPath();
    if (paths.length > 0) {
      let fullPath = paths.shift();
      paths.forEach(path => {
        fullPath = fullPath.concat("/", path);
      });
      return fullPath;
    }
    return "";
  }

  /**
   * Create a new child region with the provided name.
   * @param {String} nameIn - Name to be set for the new child region.
   * 
   * @return {Region}
   */
  this.createChild = (nameIn) => {
    let childRegion = new Region(this, scene);
    childRegion.setName(nameIn);
    children.push(childRegion);
    group.add(childRegion.getGroup());
    return childRegion;
  }

  /**
   * Get the child region with matching childName.
   * @param {String} childName - Name to be matched.
   * 
   * @return {Region}
   */
  this.getChildWithName = childName => {
    if (childName) {
      const lowerChildName = childName.toLowerCase();
      for (let i = 0; i < children.length; i++) {
        if (children[i].getName().toLowerCase() === lowerChildName)
          return children[i];
      }
    }
    return undefined;
  }

  /**
   * Find a child region using the path array.
   * @param {Array} pathArray - Array containing regions' name at each
   * hierarchy to match.
   * 
   * @return {Region}
   */
  this.findChildFromSeparatedPath = pathArray => {
    if (pathArray && pathArray.length > 0) {
      if (pathArray[0] === "") {
        pathArray.shift();
      }
    }
    if (pathArray && pathArray.length > 0) {
      const childRegion = this.getChildWithName(pathArray[0]);
      if (childRegion) {
        pathArray.shift();
        return childRegion.findChildFromSeparatedPath(pathArray);
      } else {
        return undefined;
      }
    }
    return this;
  }

  /**
   * Find the region using the provided relative path.
   * 
   * @param {String} path - Relative paths from this region
   * to the child region.
   * 
   * @return {Region}
   */
  this.findChildFromPath = (path) => {
    const pathArray = path.split("/");
    return this.findChildFromSeparatedPath(pathArray);
  }

  /**
   * Create a new child using the path array. All required new regions
   * down the path will be created.
   * 
   * @param {Array} pathArray - Array containing regions' name, new regions
   * will be created along the path if not found.
   * 
   * @return {Region}
   */
  this.createChildFromSeparatedPath = pathArray => {
    if (pathArray.length > 0) {
      if (pathArray[0] === "") {
        pathArray.shift();
      }
    }
    if (pathArray.length > 0) {
      let childRegion = this.getChildWithName(pathArray[0]);
      if (!childRegion) {
        childRegion = this.createChild(pathArray[0]);
      }
      pathArray.shift();
      return childRegion.createChildFromSeparatedPath(pathArray);
    }
    return this;
  }

  /**
   * Create a new child using the path. All required new regions
   * down the path will be created.
   * 
   * @param {String} path - Relative paths from the region
   * to the child region.
   * 
   * @return {Region}
   */
  this.createChildFromPath = (path) => {
    const pathArray = path.split("/");
    return this.createChildFromSeparatedPath(pathArray);
  }


  /**
   * Return existing region if it exists, otherwise, create a new
   * region with the provided path.
   * 
   * @param {String} path - Relative paths from the region
   * to the child region.
   * 
   * @return {Region}
   */
  this.findOrCreateChildFromPath = (path) => {
    let childRegion = this.findChildFromPath(path);
    if (!childRegion) {
      childRegion = this.createChildFromPath(path);
    }
    return childRegion;
  }

  /**
   * Add a zinc object into this region, the morph will be added
   * to the group.
   * 
   * @param {ZincObject} zincObject - Zinc object to be added into
   * this region.
   */
  this.addZincObject = zincObject => {
    if (zincObject) {
      zincObject.setRegion(this);
      group.add(zincObject.getGroup());
      zincObjects.push(zincObject);
      this.pickableUpdateRequired = true;
      if (scene) {
        scene.triggerObjectAddedCallback(zincObject);
      }
    }
  }


  /**
   * Remove a ZincObject from this region if it presents. This will eventually
   * destroy the object and free up the memory.
   * 
   * @param {ZincObject} zincObject - object to be removed from this region.
   */
  this.removeZincObject = zincObject => {
    for (let i = 0; i < zincObjects.length; i++) {
      if (zincObject === zincObjects[i]) {
        group.remove(zincObject.getGroup());
        zincObjects.splice(i, 1);
        if (scene) {
          scene.triggerObjectRemovedCallback(zincObject);
        }
        zincObject.dispose();
        this.pickableUpdateRequired = true;
        return;
      }
    }
  }

  /**
   * Return true if pickable objects require an update.
   * 
   * @param {Boolean} transverse - Check child regions as well
   * if this is set to true.
   * 
   * @return {Boolean}
   */
  this.checkPickableUpdateRequred = (transverse) => {
    if (this.pickableUpdateRequired) return true;
    if (transverse) {
      let flag = false;
      for (let i = 0; i < children.length; i++) {
         flag = children[i].checkPickableUpdateRequred(transverse);
         if (flag) return true;
      }
    }
    return false;
  }

  /**
   * Get all pickable objects.
   */
  this.getPickableThreeJSObjects = (objectsList,  transverse) => {
    if (group.visible) {
      zincObjects.forEach(zincObject => {
        if (zincObject.getGroup() && zincObject.getGroup().visible) {
          let marker = zincObject.marker;
          if (marker && marker.isEnabled()) {
            objectsList.push(marker.getMorph());
          }
          objectsList.push(zincObject.getGroup());
        }
      });
      if (transverse) {
        children.forEach(childRegion => {
          childRegion.getPickableThreeJSObjects(objectsList, transverse);
        });
      }
      this.pickableUpdateRequired = false;
    }
    return objectsList;
  }

  /**
   * Set the default duration value for all zinc objects
   * that are to be loaded into this region.
   * 
   * @param {Number} durationIn - duration of the scene.
   */
  this.setDuration = durationIn => {
    duration = durationIn;
    zincObjects.forEach(zincObject => zincObject.setDuration(durationIn));
    children.forEach(childRegion => childRegion.setDuration(durationIn));
  }

  /**
   * Get the default duration value.
   * returns {Number}
   */
  this.getDuration = () => {
    return duration;
  }

  /**
   * Get the bounding box of all the object in this and child regions only.
   * Do not include the matrix transformation here, it is done at the primitives
   * level.
   * 
   * @returns {THREE.Box3} 
   */
  this.getBoundingBox = transverse => {
    let boundingBox1 = undefined, boundingBox2 = undefined;
    zincObjects.forEach(zincObject => {
      boundingBox2 = zincObject.getBoundingBox();
      if (boundingBox2) {
        if (boundingBox1 == undefined) {
          boundingBox1 = boundingBox2.clone();
        } else {
          boundingBox1.union(boundingBox2);
        }
      }
    });
    if (transverse) {
      children.forEach(childRegion => {
        boundingBox2 = childRegion.getBoundingBox(transverse);
        if (boundingBox2) {
          if (boundingBox1 == undefined) {
            boundingBox1 = boundingBox2.clone();
          } else {
            boundingBox1.union(boundingBox2);
          }
        }
      });
    }
    return boundingBox1;
  }

  /**
   * Clear and dispose all objects belong to this region.
   * 
   * @param {Boolean} transverse - Clear and dispose child regions as well
   * if this is set to true.
   */
  this.clear = transverse => {
    if (transverse) {
      children.forEach(childRegion => childRegion.clear(transverse));
    }
    zincObjects.forEach(zincObject => {
      group.remove(zincObject.getGroup());
      zincObject.dispose();
    });
    children = [];
    zincObjects = [];
  }

  /**
   * Check if a zincObject is a member of this region.
   * 
   * @param {ZincObject} zincObject - The ZincObject to be checked.
   * @param {Boolean} transverse - Also check the child regions.
   * 
   * @return {Boolean}
   */
  this.objectIsInRegion = (zincObject, transverse) => {
    for (let i = 0; i < zincObjects.length; i++) {
      if (zincObject === zincObjects[i]) {
        return true;
      }
    }
    if (transverse) {
      for (let i = 0; i < children.length; i++) {
        if (children[i].objectIsInRegion(zincObject, transverse))
          return true;
      }
    }

    return false;
  }

  /**
   * A function which iterates through the list of geometries and call the callback
   * function with the geometries as the argument.
   * 
   * @param {Function} callbackFunction - Callback function with the geometry
   * as an argument.
   * @param {Boolean} transverse - Also perform the same callback function for
   * all child regions if this is set to be true.
   */
  this.forEachGeometry = (callbackFunction, transverse) => {
    zincObjects.forEach(zincObject => {
      if (zincObject.isGeometry)
        callbackFunction(zincObject);
    });
    if (transverse)
      children.forEach(childRegion => childRegion.forEachGeometry(
        callbackFunction, transverse));
  }

  /**
   * A function which iterates through the list of glyphsets and call the callback
   * function with the glyphset as the argument.
   * 
   * @param {Function} callbackFunction - Callback function with the glyphset
   * as an argument.
   * @param {Boolean} transverse - Also perform the same callback function for
   * all child regions if this is set to be true.
   */
  this.forEachGlyphset = (callbackFunction, transverse) => {
    zincObjects.forEach(zincObject => {
      if (zincObject.isGlyphset)
        callbackFunction(zincObject);
    });
    if (transverse)
      children.forEach(childRegion => childRegion.forEachGlyphset(
        callbackFunction, transverse));
  }

  /**
   * A function which iterates through the list of pointsets and call the callback
   * function with the pointset as the argument.
   * 
   * @param {Function} callbackFunction - Callback function with the pointset
   * as an argument.
   * @param {Boolean} transverse - Also perform the same callback function for
   * all child regions if this is set to be true.
   */
  this.forEachPointset = (callbackFunction, transverse) => {
    zincObjects.forEach(zincObject => {
      if (zincObject.isPointset)
        callbackFunction(zincObject);
    });
    if (transverse)
      children.forEach(childRegion => childRegion.forEachPointset(
        callbackFunction, transverse));
  }

  /**
  * A function which iterates through the list of lines and call the callback
  * function with the lines as the argument.
  * 
  * @param {Function} callbackFunction - Callback function with the lines
  * as an argument.
   * @param {Boolean} transverse - Also perform the same callback function for
   * all child regions if this is set to be true.
  */
  this.forEachLine = (callbackFunction, transverse) => {
    zincObjects.forEach(zincObject => {
      if (zincObject.isLines)
        callbackFunction(zincObject);
    });
    if (transverse)
      children.forEach(childRegion => childRegion.forEachLine(
        callbackFunction, transverse));
  }

  this.findObjectsWithAnatomicalId = (anatomicalId, transverse) => {
    zincObjects.forEach(zincObject => {
      if (zincObject.anatomicalId === anatomicalId)
        objectsArray.push(zincObject);
    });
    if (transverse) {
      children.forEach(childRegion => {
        let childObjects = childRegion.findObjectsWithAnatomicalId(anatomicalId, transverse);
        objectsArray.push(...childObjects);
      });
    }

    return objectsArray;
  }

  /** 
   * Find and return all zinc objects in this and child regions with 
   * the matching GroupName.
   * 
   * @param {String} groupName - Groupname to match with.
   * @param {Boolean} transverse - Also look for the object with groupName
   * in child regions if set to true.
   * @returns {Array}
   */
  this.findObjectsWithGroupName = (groupName, transverse) => {
    const objectsArray = [];
    zincObjects.forEach(zincObject => {
      const lowerObjectName = zincObject.groupName ? zincObject.groupName.toLowerCase() : zincObject.groupName;
      const lowerGroupName = groupName ? groupName.toLowerCase() : groupName;
      if (lowerObjectName === lowerGroupName)
        objectsArray.push(zincObject);
    });
    if (transverse) {
      children.forEach(childRegion => {
        let childObjects = childRegion.findObjectsWithGroupName(groupName, transverse);
        objectsArray.push(...childObjects);
      });
    }
    return objectsArray;
  }

  /** 
   * Find and return all geometries in this and child regions with 
   * the matching GroupName.
   * 
   * @param {String} groupName - Groupname to match with.
   * @param {Boolean} transverse - Also look for the object with groupName
   * in child regions if set to true.
   * @returns {Array}
   */
  this.findGeometriesWithGroupName = (groupName, transverse) => {
    const primitivesArray = this.findObjectsWithGroupName(groupName, transverse);
    const geometriesArray = primitivesArray.filter(primitive => primitive.isGeometry);
    return geometriesArray;
  }

  /** 
   * Find and return all pointsets in this and child regions with
   * the matching groupName.
   * 
   * @param {String} groupName - Groupname to match with.
   * @param {Boolean} transverse - Also look for the object with groupName
   * in child regions if set to true.
   * @returns {Array}
   */
  this.findPointsetsWithGroupName = (groupName, transverse) => {
    const primitivesArray = this.findObjectsWithGroupName(groupName, transverse);
    const pointsetsArray = primitivesArray.filter(primitive => primitive.isPointset);
    return pointsetsArray;
  }

  /** 
   * Find and return all glyphsets in this and child regions with
   * the matching groupName.
   * 
   * @param {String} groupName - Groupname to match with.
   * @param {Boolean} transverse - Also look for the object with groupName
   * in child regions if set to true.
   * @returns {Array}
   */
  this.findGlyphsetsWithGroupName = (groupName, transverse) => {
    const primitivesArray = this.findObjectsWithGroupName(groupName, transverse);
    const glyphsetsArray = primitivesArray.filter(primitive => primitive.isGlyphset);
    return glyphsetsArray;
  }

  /** 
   * Find and return all lines in this and child regions with
   * the matching groupName.
   * 
   * @param {String} groupName - Groupname to match with.
   * @param {Boolean} transverse - Also look for the object with groupName
   * in child regions if set to true.
   * @returns {Array}
   */
  this.findLinesWithGroupName = (groupName, transverse) => {
    const primitivesArray = this.findObjectsWithGroupName(groupName, transverse);
    const linesArray = primitivesArray.filter(primitive => primitive.isLines);
    return linesArray;
  }

  /** 
   * Get all zinc objects in this region.
   * 
   * @param {Boolean} transverse - Include zinc objects in child regions if this is
   * set to true.
   * @returns {Array}
   */
  this.getAllObjects = transverse => {
    const objectsArray = [...zincObjects];
    if (transverse) {
      children.forEach(childRegion => {
        let childObjects = childRegion.getAllObjects(transverse);
        objectsArray.push(...childObjects);
      });
    }
    return objectsArray;
  }

  /** 
   * Get all child regions.
   * 
   * @param {Boolean} transverse - Include all regions which are descendants of 
   * this reigon when this is set to true.
   * @returns {Array}
   */
   this.getChildRegions = transverse => {
    const objectsArray = [...children];
    if (transverse) {
      children.forEach(childRegion => {
        const childObjects = childRegion.getChildRegions(transverse);
        objectsArray.push(...childObjects);
      });
    }
    return objectsArray;
  }

  /**
   * Get the current time of the region.
   * Return -1 if no graphics in the region.
   * 
   * @return {Number}
   */
  this.getCurrentTime = () => {
    if (zincObjects[0] != undefined) {
      return zincObjects[0].getCurrentTime();
    } else {
      for (let i = 0; i < children.length; i++) {
        const time = children[i].getCurrentTime();
        if (time !== -1)
          return time;
      }
    }
    return -1;
  }

  /**
   * Set the current time of all the objects of this region.
   * 
   * @param {Number} time  - Value to set the time to.
   * @param {Boolean} transverse - Set the time for chidl regions if
   * this is set to true.
   */
  this.setMorphTime = (time, transverse) => {
    zincObjects.forEach(zincObject => {
      zincObject.setMorphTime(time);
    });
    if (transverse) {
      children.forEach(childRegion => {
        childRegion.setMorphTime(time);
      });
    }
  }

  /**
   * Check if any object in this region is time varying.
   * 
   * @return {Boolean}
   */
  this.isTimeVarying = () => {
    for (let i = 0; i < zincObjects.length; i++) {
      if (zincObjects[i].isTimeVarying()) {
        return true;
      }
    }
    for (let i = 0; i < children.length; i++) {
      if (children[i].isTimeVarying()) {
        return true;
      }
    }

    return false;
  }

  /**
   * Update geometries and glyphsets based on the calculated time.
   * @private
   */
  this.renderGeometries = (playRate, delta, playAnimation, cameraControls, options, transverse) => {
    // Let video dictates the progress if one is present
    const allObjects = this.getAllObjects(transverse);
    allObjects.forEach(zincObject => {
      zincObject.render(playRate * delta, playAnimation, cameraControls, options);
    });
    //process markers visibility and size, as long as there are more than
    //one entry in markersList is greater than 1, markers have been enabled.
    if (options && (playAnimation === false) &&
      options.markerCluster?.markerUpdateRequired) {
      /** 
        const markerDepths = Object.values(options.markersList)
          .map((marker) => marker.ndc.z);
        if (markerDepths.length > 1) {
          const min = Math.min(...markerDepths);
          const max = Math.max(...markerDepths);
          allObjects.forEach(zincObject => {
            zincObject.processMarkerVisual(min, max);
          });
        }
      */
      options.markerCluster.calculate();
    }
  }

  /**
   * Update geometries and glyphsets based on the calculated time.
   */
  this.createPoints = ( groupName, coords, labels, colour ) => {
    let isNew = false;
    const zincObjects = this.findObjectsWithGroupName(groupName, false);
    const index = zincObjects.findIndex((zincObject) => zincObject.isPointset);
    const pointset = index > -1 ? zincObjects[index] : new Pointset();
    pointset.addPoints(coords, labels, colour);
    if (index === -1) {
      pointset.setName(groupName);
      this.addZincObject(pointset);
      isNew = true;
    } else {
      this.pickableUpdateRequired = true;
    }
    return { zincObject: pointset, isNew };
  }

  /**
   * Update geometries and glyphsets based on the calculated time.
   */
  this.createLines = ( groupName, coords, colour ) => {
    let isNew = false;
    const zincObjects = this.findObjectsWithGroupName(groupName, false);
    const index = zincObjects.findIndex((zincObject) => zincObject.isLines);
    const lines = index > -1 ? zincObjects[index] : new Lines2();
    lines.addLines(coords, colour);
    if (index === -1) {
      lines.setName(groupName);
      this.addZincObject(lines);
      isNew = true;
    } else {
      this.pickableUpdateRequired = true;
    }
    return { zincObject: lines, isNew };
  }

  /**
   * Add a new geometry
   */
  this.createGeometryFromThreeJSGeometry = (
    groupName, geometry, colour, opacity, visibility, renderOrder) => {
    const zincGeometry = new Geometry();
    const material = new THREE.MeshPhongMaterial({
      color : colour,
      morphTargets : false,
      morphNormals : false,
      transparent : true,
      opacity : opacity,
      side : THREE.DoubleSide
    });
    zincGeometry.createMesh(
      geometry,
      material,
      {localTimeEnabled: false, localMorphColour: false,},
    );
    if (zincGeometry.getMorph()) {
      zincGeometry.setVisibility(false);
      zincGeometry.setName(groupName);
      zincGeometry.setRenderOrder(renderOrder);
      this.addZincObject(zincGeometry);
      return zincGeometry;
    }
    return undefined;
  }
}

exports.Region = Region;