const THREE = require('three');
const JSONLoader = require('../loaders/JSONLoader').JSONLoader;
/**
* This is a container of {@link Glyph} and their graphical properties
* including transformations, colors, number of time steps, duration of animations
* and group name. Please note that all glyphs in the glyphset share the same geometry
* however they may have different transformations.
*
* @class
* @author Alan Wu
* @return {Glyphset}
*/
const Glyphset = function () {
(require('./zincObject').ZincObject).call(this);
const glyphList = [];
let axis1s = undefined;
let axis2s = undefined;
let axis3s = undefined;
let positions = undefined;
let scales = undefined;
let colors = undefined;
let labels = undefined;
let numberOfTimeSteps = 0;
let numberOfVertices = 0;
let baseSize = [0, 0, 0];
let offset = [0, 0, 0];
let scaleFactors = [0, 0, 0];
let repeat_mode = "NONE";
this.ready = false;
let morphColours = false;
let morphVertices = false;
this.isGlyphset = true;
let _transformMatrix = new THREE.Matrix4();
const _bot_colour = new THREE.Color();
const _top_colour = new THREE.Color();
const _boundingBox1 = new THREE.Box3();
const _boundingBox2 = new THREE.Box3();
const _boundingBox3 = new THREE.Box3();
const _points = [];
const _current_positions = [];
const _current_axis1s = [];
const _current_axis2s = [];
const _current_axis3s = [];
const _current_scales = [];
const _current_colors = [];
const _glyph_axis_array = [];
for (let i = 0; i < 8; i++) {
_points[i] = new THREE.Vector3();
}
/**
* Copy glyphset data into this glyphset then load the glyph's geoemtry
* with the provided glyphURL. FinishCallback will be called once
* glyph is loaded.
*
* @param {Array} glyphsetData - contains the informations about the glyphs.
* @param {String} glyphURL - URL to the geometry which will be applied to all
* all the glyphs in the glyphset once loaded.
* @param {Function} finishCallback - User's function to be called once glyph's
* geometry is loaded.
*/
this.load = (glyphsetData, glyphURL, finishCallback, isInline, displayLabels) => {
axis1s = glyphsetData.axis1;
axis2s = glyphsetData.axis2;
axis3s = glyphsetData.axis3;
positions = glyphsetData.positions;
scales = glyphsetData.scale;
colors = glyphsetData.colors;
labels = glyphsetData.label;
morphColours = glyphsetData.metadata.MorphColours;
morphVertices = glyphsetData.metadata.MorphVertices;
numberOfTimeSteps = glyphsetData.metadata.number_of_time_steps;
repeat_mode = glyphsetData.metadata.repeat_mode;
numberOfVertices = glyphsetData.metadata.number_of_vertices;
if (repeat_mode == "AXES_2D" || repeat_mode == "MIRROR")
numberOfVertices = numberOfVertices * 2;
else if (repeat_mode == "AXES_3D")
numberOfVertices = numberOfVertices * 3;
baseSize = glyphsetData.metadata.base_size;
offset = glyphsetData.metadata.offset;
scaleFactors = glyphsetData.metadata.scale_factors;
const loader = new JSONLoader();
this.geometry = new THREE.BufferGeometry();
const instancedMesh = new THREE.InstancedMesh(this.geometry, undefined, numberOfVertices);
this.setMorph(instancedMesh);
if (isInline) {
var object = loader.parse(glyphURL);
(meshloader(finishCallback, displayLabels))(object.geometry, object.materials);
object.geometry.dispose();
} else {
loader.crossOrigin = "Anonymous";
loader.load(glyphURL, meshloader(finishCallback, displayLabels));
}
}
/**
* Calculate the actual transformation value that can be applied
* to the transformation matrix.
*
* @returns {Array}
*/
const resolve_glyph_axes = (point, axis1, axis2, axis3, scale, return_arrays) => {
if (repeat_mode == "NONE" || repeat_mode == "MIRROR") {
let axis_scale = [0.0, 0.0, 0.0];
let final_axis1 = [0.0, 0.0, 0.0];
let final_axis2 = [0.0, 0.0, 0.0];
let final_axis3 = [0.0, 0.0, 0.0];
let final_point = [0.0, 0.0, 0.0];
const mirrored_axis1 = [0.0, 0.0, 0.0];
const mirrored_axis2 = [0.0, 0.0, 0.0];
const mirrored_axis3 = [0.0, 0.0, 0.0];
const mirrored_point = [0.0, 0.0, 0.0];
for (var j = 0; j < 3; j++) {
var sign = (scale[j] < 0.0) ? -1.0 : 1.0;
axis_scale[j] = sign * baseSize[j] + scale[j] * scaleFactors[j];
}
for (var j = 0; j < 3; j++) {
final_axis1[j] = axis1[j] * axis_scale[0];
final_axis2[j] = axis2[j] * axis_scale[1];
final_axis3[j] = axis3[j] * axis_scale[2];
final_point[j] = point[j]
+ offset[0] * final_axis1[j]
+ offset[1] * final_axis2[j]
+ offset[2] * final_axis3[j];
if (repeat_mode == "MIRROR") {
mirrored_axis1[j] = -final_axis1[j];
mirrored_axis2[j] = -final_axis2[j];
mirrored_axis3[j] = -final_axis3[j];
mirrored_point[j] = final_point[j];
if (scale[0] < 0.0) {
// shift glyph origin to end of axis1
final_point[j] -= final_axis1[j];
mirrored_point[j] -= mirrored_axis1[j];
}
}
}
/* if required, reverse axis3 to maintain right-handed coordinate system */
if (0.0 > (
final_axis3[0] * (final_axis1[1] * final_axis2[2] -
final_axis1[2] * final_axis2[1]) +
final_axis3[1] * (final_axis1[2] * final_axis2[0] -
final_axis1[0] * final_axis2[2]) +
final_axis3[2] * (final_axis1[0] * final_axis2[1] -
final_axis1[1] * final_axis2[0]))) {
final_axis3[0] = -final_axis3[0];
final_axis3[1] = -final_axis3[1];
final_axis3[2] = -final_axis3[2];
}
return_arrays[0] = [final_point, final_axis1, final_axis2, final_axis3];
if (repeat_mode == "MIRROR") {
if (0.0 > (
mirrored_axis3[0] * (mirrored_axis1[1] * mirrored_axis2[2] -
mirrored_axis1[2] * mirrored_axis2[1]) +
mirrored_axis3[1] * (mirrored_axis1[2] * mirrored_axis2[0] -
mirrored_axis1[0] * mirrored_axis2[2]) +
mirrored_axis3[2] * (mirrored_axis1[0] * mirrored_axis2[1] -
mirrored_axis1[1] * mirrored_axis2[0]))) {
mirrored_axis3[0] = -mirrored_axis3[0];
mirrored_axis3[1] = -mirrored_axis3[1];
mirrored_axis3[2] = -mirrored_axis3[2];
}
return_arrays[1] = [mirrored_point, mirrored_axis1, mirrored_axis2, mirrored_axis3];
}
}
else if (repeat_mode == "AXES_2D" || repeat_mode == "AXES_3D") {
let axis_scale = [0.0, 0.0, 0.0];
let final_point = [0.0, 0.0, 0.0];
for (var j = 0; j < 3; j++) {
var sign = (scale[j] < 0.0) ? -1.0 : 1.0;
axis_scale[j] = sign * baseSize[0] + scale[j] * scaleFactors[0];
}
for (var j = 0; j < 3; j++) {
final_point[j] = point[j]
+ offset[0] * axis_scale[0] * axis1[j]
+ offset[1] * axis_scale[1] * axis2[j]
+ offset[2] * axis_scale[2] * axis3[j];
}
const number_of_glyphs = (glyph_repeat_mode == "AXES_2D") ? 2 : 3;
for (let k = 0; k < number_of_glyphs; k++) {
let use_axis1, use_axis2;
const use_scale = scale[k];
let final_axis1 = [0.0, 0.0, 0.0];
let final_axis2 = [0.0, 0.0, 0.0];
let final_axis3 = [0.0, 0.0, 0.0];
if (k == 0) {
use_axis1 = axis1;
use_axis2 = axis2;
}
else if (k == 1) {
use_axis1 = axis2;
use_axis2 = (glyph_repeat_mode == "AXES_2D") ? axis1 : axis3;
}
else // if (k == 2)
{
use_axis1 = axis3;
use_axis2 = axis1;
}
const final_scale1 = baseSize[0] + use_scale * scaleFactors[0];
final_axis1[0] = use_axis1[0] * final_scale1;
final_axis1[1] = use_axis1[1] * final_scale1;
final_axis1[2] = use_axis1[2] * final_scale1;
final_axis3[0] = final_axis1[1] * use_axis2[2] - use_axis2[1] * final_axis1[2];
final_axis3[1] = final_axis1[2] * use_axis2[0] - use_axis2[2] * final_axis1[0];
final_axis3[2] = final_axis1[0] * use_axis2[1] - final_axis1[1] * use_axis2[0];
let magnitude = Math.sqrt(final_axis3[0] * final_axis3[0] + final_axis3[1] * final_axis3[1] + final_axis3[2] * final_axis3[2]);
if (0.0 < magnitude) {
let scaling = (baseSize[2] + use_scale * scaleFactors[2]) / magnitude;
if ((repeat_mode == "AXES_2D") && (k > 0)) {
scaling *= -1.0;
}
final_axis3[0] *= scaling;
final_axis3[1] *= scaling;
final_axis3[2] *= scaling;
}
final_axis2[0] = final_axis3[1] * final_axis1[2] - final_axis1[1] * final_axis3[2];
final_axis2[1] = final_axis3[2] * final_axis1[0] - final_axis1[2] * final_axis3[0];
final_axis2[2] = final_axis3[0] * final_axis1[1] - final_axis3[1] * final_axis1[0];
magnitude = Math.sqrt(final_axis2[0] * final_axis2[0] + final_axis2[1] * final_axis2[1] + final_axis2[2] * final_axis2[2]);
if (0.0 < magnitude) {
var scaling = (baseSize[1] + use_scale * scaleFactors[1]) / magnitude;
final_axis2[0] *= scaling;
final_axis2[1] *= scaling;
final_axis2[2] *= scaling;
}
return_arrays[k] = [final_point, final_axis1, final_axis2, final_axis3];
}
}
return return_arrays;
};
/**
* Update transformation for each of the glyph in this glyphset.
*/
const updateGlyphsetTransformation = (
current_positions,
current_axis1s,
current_axis2s,
current_axis3s,
current_scales
) => {
let numberOfGlyphs = 1;
if (repeat_mode == "AXES_2D" || repeat_mode == "MIRROR")
numberOfGlyphs = 2;
else if (repeat_mode == "AXES_3D")
numberOfGlyphs = 3;
const numberOfPositions = current_positions.length / 3;
let current_glyph_index = 0;
_glyph_axis_array.length = numberOfGlyphs;
for (let i = 0; i < numberOfPositions; i++) {
const current_index = i * 3;
const current_position = [current_positions[current_index], current_positions[current_index + 1],
current_positions[current_index + 2]];
const current_axis1 = [current_axis1s[current_index], current_axis1s[current_index + 1],
current_axis1s[current_index + 2]];
const current_axis2 = [current_axis2s[current_index], current_axis2s[current_index + 1],
current_axis2s[current_index + 2]];
const current_axis3 = [current_axis3s[current_index], current_axis3s[current_index + 1],
current_axis3s[current_index + 2]];
const current_scale = [current_scales[current_index], current_scales[current_index + 1],
current_scales[current_index + 2]];
const arrays = resolve_glyph_axes(current_position, current_axis1, current_axis2,
current_axis3, current_scale, _glyph_axis_array);
if (arrays.length == numberOfGlyphs) {
for (let j = 0; j < numberOfGlyphs; j++) {
_transformMatrix.elements[0] = arrays[j][1][0];
_transformMatrix.elements[1] = arrays[j][1][1];
_transformMatrix.elements[2] = arrays[j][1][2];
_transformMatrix.elements[3] = 0.0;
_transformMatrix.elements[4] = arrays[j][2][0];
_transformMatrix.elements[5] = arrays[j][2][1];
_transformMatrix.elements[6] = arrays[j][2][2];
_transformMatrix.elements[7] = 0.0;
_transformMatrix.elements[8] = arrays[j][3][0];
_transformMatrix.elements[9] = arrays[j][3][1];
_transformMatrix.elements[10] = arrays[j][3][2];
_transformMatrix.elements[11] = 0.0;
_transformMatrix.elements[12] = arrays[j][0][0];
_transformMatrix.elements[13] = arrays[j][0][1];
_transformMatrix.elements[14] = arrays[j][0][2];
_transformMatrix.elements[15] = 1.0;
this.morph.setMatrixAt(current_glyph_index, _transformMatrix);
const glyph = glyphList[current_glyph_index];
if (glyph)
glyph.setTransformation(arrays[j][0], arrays[j][1],
arrays[j][2], arrays[j][3]);
current_glyph_index++;
}
}
}
this.morph.instanceMatrix.needsUpdate = true;
};
/**
* Update colour for each of the glyph in this glyphset.
*/
const updateGlyphsetHexColors = current_colors => {
let numberOfGlyphs = 1;
if (repeat_mode == "AXES_2D" || repeat_mode == "MIRROR")
numberOfGlyphs = 2;
else if (repeat_mode == "AXES_3D")
numberOfGlyphs = 3;
const numberOfColours = current_colors.length;
let current_glyph_index = 0;
for (let i = 0; i < numberOfColours; i++) {
const hex_values = current_colors[i];
for (let j = 0; j < numberOfGlyphs; j++) {
_bot_colour.setHex(hex_values)
this.morph.setColorAt(current_glyph_index, _bot_colour);
const glyph = glyphList[current_glyph_index];
if (glyph)
glyph.setColour(_bot_colour);
current_glyph_index++;
}
}
this.morph.instanceColor.needsUpdate = true;
};
/**
* Update the current states of the glyphs in this glyphset, this includes transformation and
* colour for each of them. This is called when glyphset and glyphs are initialised and whenever
* the internal time has been updated.
*/
const updateMorphGlyphsets = () => {
const current_positions = _current_positions;
const current_axis1s = _current_axis1s;
const current_axis2s = _current_axis2s;
const current_axis3s = _current_axis3s;
const current_scales = _current_scales;
const current_colors = _current_colors;
const current_time = this.inbuildTime / this.duration * (numberOfTimeSteps - 1);
const bottom_frame = Math.floor(current_time);
const proportion = 1 - (current_time - bottom_frame);
const top_frame = Math.ceil(current_time);
if (morphVertices) {
const bottom_positions = positions[bottom_frame.toString()];
const top_positions = positions[top_frame.toString()];
const bottom_axis1 = axis1s[bottom_frame.toString()];
const top_axis1 = axis1s[top_frame.toString()];
const bottom_axis2 = axis2s[bottom_frame.toString()];
const top_axis2 = axis2s[top_frame.toString()];
const bottom_axis3 = axis3s[bottom_frame.toString()];
const top_axis3 = axis3s[top_frame.toString()];
const bottom_scale = scales[bottom_frame.toString()];
const top_scale = scales[top_frame.toString()];
_current_positions.length = bottom_positions.length;
_current_axis1s.length = bottom_positions.length;
_current_axis2s.length = bottom_positions.length;
_current_axis3s.length = bottom_positions.length;
_current_scales.length = bottom_positions.length;
for (let i = 0; i < bottom_positions.length; i++) {
current_positions[i] = proportion * bottom_positions[i] + (1.0 - proportion) * top_positions[i];
current_axis1s[i] = proportion * bottom_axis1[i] + (1.0 - proportion) * top_axis1[i];
current_axis2s[i] = proportion * bottom_axis2[i] + (1.0 - proportion) * top_axis2[i];
current_axis3s[i] = proportion * bottom_axis3[i] + (1.0 - proportion) * top_axis3[i];
current_scales[i] = proportion * bottom_scale[i] + (1.0 - proportion) * top_scale[i];
}
} else {
current_positions = positions["0"];
current_axis1s = axis1s["0"];
current_axis2s = axis2s["0"];
current_axis3s = axis3s["0"];
current_scales = scales["0"];
}
updateGlyphsetTransformation(current_positions, current_axis1s, current_axis2s, current_axis3s,
current_scales);
this.boundingBoxUpdateRequired = true;
if (colors != undefined) {
if (morphColours) {
const bottom_colors = colors[bottom_frame.toString()];
const top_colors = colors[top_frame.toString()];
current_colors.length = bottom_colors.length;
for (let i = 0; i < bottom_colors.length; i++) {
_bot_colour.setHex(bottom_colors[i]);
_top_colour.setHex(top_colors[i]);
_bot_colour.setRGB(_bot_colour.r * proportion + _top_colour.r * (1 - proportion),
_bot_colour.g * proportion + _top_colour.g * (1 - proportion),
_bot_colour.b * proportion + _top_colour.b * (1 - proportion));
current_colors[i] = _bot_colour.getHex();
}
/*
for (var i = 0; i < bottom_colors.length; i++) {
current_colors.push(proportion * bottom_colors[i] + (1.0 - proportion) * top_colors[i]);
}
*/
} else {
current_colors = colors["0"];
}
updateGlyphsetHexColors(current_colors);
}
};
/**
* Display the label of the glyphs in the glyphset.
*/
this.showLabel = () => {
for (let i = 0; i < glyphList.length; i++) {
glyphList[i].showLabel(this.morph.material ? this.morph.material.color : undefined);
}
}
/**
* Create the glyphs in the glyphset.
*
* @param {Boolean} displayLabels -Flag to determine either the labels should be display or not.
*/
const createGlyphs = (displayLabels) => {
if ((labels != undefined) && displayLabels) {
for (let i = 0; i < numberOfVertices; i++) {
const glyph = new (require('./glyph').Glyph)(undefined, undefined, i, this);
if (labels != undefined && labels[i] != undefined) {
glyph.setLabel(labels[i]);
}
if (numberOfTimeSteps > 0) {
glyph.setFrustumCulled(false);
}
glyphList[i] = glyph;
this.morph.add(glyph.getGroup());
}
}
if ((labels != undefined) && displayLabels) {
this.showLabel(this.morph.material ? this.morph.material.color : undefined);
}
//Update the transformation of the glyphs.
updateGlyphsetTransformation(positions["0"], axis1s["0"],
axis2s["0"], axis3s["0"], scales["0"]);
//Update the color of the glyphs.
if (colors != undefined) {
updateGlyphsetHexColors(colors["0"]);
}
this.ready = true;
this.boundingBoxUpdateRequired = true;
};
/**
* Add a custom {@link Glyph} to this {@link Glyphset}.
*
* @param {Glyph} Glyph to be added.
*/
this.addCustomGlyph = glyph => {
if (glyph.isGlyph)
glyphList.push(glyph);
this.ready = true;
this.boundingBoxUpdateRequired = true;
}
/**
* Add a THREE.Mesh object to be displayed as glyph in this {@link Glyphset}.
*
* @param {THREE.Mesh} Mesh to be added.
* @param {Number} id of the mesh.
*/
this.addMeshAsGlyph = (mesh, id) => {
if (mesh.isMesh) {
const glyph = new (require('./glyph').Glyph)(undefined, undefined, id, this);
glyph.fromMesh(mesh);
glyphList.push(glyph);
this.morph.add(glyph.getGroup())
this.ready = true;
this.boundingBoxUpdateRequired = true;
return glyph;
}
return undefined;
}
/**
* A function which iterates through the list of glyphs and call the callback
* function with the glyph as the argument.
*
* @param {Function} callbackFunction - Callback function with the glyph
* as an argument.
*/
this.forEachGlyph = callbackFunction => {
for (let i = 0; i < glyphList.length; i++) {
callbackFunction(glyphList[i]);
}
}
var meshloader = (finishCallback, displayLabels) => {
return (geometry, materials) => {
const tempGeometry = geometry.toBufferGeometry();
this.geometry.copy(tempGeometry);
this.geometry.computeBoundingSphere();
this.geometry.computeBoundingBox();
tempGeometry.dispose();
if (materials && materials[0])
this.morph.material = materials[0];
createGlyphs(displayLabels);
this.morph.name = this.groupName;
this.morph.userData = this;
this.setMorph(this.morph);
geometry.dispose();
if (finishCallback != undefined && (typeof finishCallback == 'function'))
finishCallback(this);
};
}
/**
* Get the index of the closest vertex to centroid.
*/
this.getClosestVertexIndex = function () {
let closestIndex = -1;
if (this.morph && this.ready) {
this.getBoundingBox().getCenter(this._v1);
let current_positions = positions["0"];
const numberOfPositions = current_positions.length / 3;
let distance = -1;
let currentDistance = 0;
for (let i = 0; i < numberOfPositions; i++) {
const current_index = i * 3;
this._v2.set(current_positions[current_index],
current_positions[current_index + 1],
current_positions[current_index + 2]);
currentDistance = this._v1.distanceTo(this._v2);
if (distance == -1) {
distance = currentDistance;
closestIndex = i;
} else if (distance > currentDistance) {
distance = currentDistance;
closestIndex = i;
}
}
}
return closestIndex;
}
/**
* Get the closest vertex to centroid.
*/
this.getClosestVertex = function () {
if (this.closestVertexIndex == -1) {
this.closestVertexIndex = this.getClosestVertexIndex();
}
if (this.closestVertexIndex >= 0) {
/*
if (glyphList && glyphList[this.closestVertexIndex]) {
glyphList[this.closestVertexIndex].getBoundingBox().getCenter(position);
}
*/
if (this.morph) {
let position = new THREE.Vector3();
this.morph.getMatrixAt(this.closestVertexIndex, _transformMatrix);
position.setFromMatrixPosition(_transformMatrix);
return position;
}
}
return undefined;
}
/**
* Get the bounding box for the whole set of glyphs.
*
* @return {Three.Box3};
*/
this.getBoundingBox = () => {
if (this.morph && this.ready && this.morph.visible) {
if (this.boundingBoxUpdateRequired) {
_boundingBox1.setFromBufferAttribute(
this.morph.geometry.attributes.position);
for (let i = 0; i < numberOfVertices; i++) {
this.morph.getMatrixAt(i, _transformMatrix);
_boundingBox2.copy(_boundingBox1).applyMatrix4(_transformMatrix);
if (i == 0) {
_boundingBox3.copy(_boundingBox2);
} else {
_boundingBox3.union(_boundingBox2);
}
}
if (_boundingBox3) {
this.cachedBoundingBox.copy(_boundingBox3);
this.morph.updateWorldMatrix(true, true);
this.cachedBoundingBox.applyMatrix4(this.morph.matrixWorld);
this.boundingBoxUpdateRequired = false;
} else
return undefined;
}
return this.cachedBoundingBox;
}
return undefined;
}
/**
* Set the local time of this glyphset.
*
* @param {Number} time - Can be any value between 0 to duration.
*/
this.setMorphTime = time => {
if (time > this.duration)
this.inbuildTime = this.duration;
else if (0 > time)
this.inbuildTime = 0;
else
this.inbuildTime = time;
if (morphColours || morphVertices) {
updateMorphGlyphsets();
if (morphVertices)
this.markerUpdateRequired = true;
}
}
/**
* Check if the glyphset is time varying.
*
* @return {Boolean}
*/
this.isTimeVarying = () => {
if (((this.ready === false) || (numberOfTimeSteps > 0)) &&
(morphColours || morphVertices))
return true;
return false;
}
/**
* Get the current inbuild time of the
*
* @return {Number}
*/
this.getCurrentTime = () => {
return this.inbuildTime;
}
/**
* Clear this glyphset and its list of glyphs which will release them from the memory.
*/
this.dispose = () => {
for (let i = glyphList.length - 1; i >= 0; i--) {
glyphList[i].dispose();
}
if (this.geometry)
this.geometry.dispose();
if (this.morph)
this.morph.material.dispose();
axis1s = undefined;
axis2s = undefined;
axis3s = undefined;
positions = undefined;
scales = undefined;
colors = undefined;
this.ready = false;
this.groupName = undefined;
}
/**
* Update the glyphsets if required the render.
*/
this.render = (delta, playAnimation, options) => {
if (playAnimation == true) {
let targetTime = this.inbuildTime + delta;
if (targetTime > this.duration)
targetTime = targetTime - this.duration;
this.inbuildTime = targetTime;
if (morphColours || morphVertices) {
updateMorphGlyphsets();
}
}
this.updateMarker(playAnimation, options);
}
}
Glyphset.prototype = Object.create((require('./zincObject').ZincObject).prototype);
exports.Glyphset = Glyphset;