UNPKG

aframe-babia-components

Version:

A data visualization set of components for A-Frame.

1,196 lines (1,066 loc) 92.9 kB
let findProdComponent = require('../others/common').findProdComponent; let updateFunction = require('../others/common').updateFunction; const colorsArray = require('../others/common').colors.palettes['categoric']; const NotiBuffer = require("../../common/noti-buffer").NotiBuffer; /* global AFRAME */ if (typeof AFRAME === 'undefined') { throw new Error('Component attempted to register before AFRAME was available.'); } /** * A-Charts component for A-Frame. */ AFRAME.registerComponent('babia-boats', { schema: { data: { type: 'asset' }, from: { type: 'string' }, border: { type: 'number', default: 0.5 }, width: { type: 'string', default: 'width' }, depth: { type: 'string', default: 'depth' }, area: { type: 'string' }, color: { type: 'string' }, height: { type: 'string', default: 'height' }, // Maximum height for a building, in VR units (meters) maxBuildingHeight: { type: 'number', default: 2 }, // Minimum height for a building, in VR units (meters) minBuildingHeight: { type: 'number', default: 0.03 }, // Differential heights for buildings (the height of // buildings is calculated relative to the difference between // highest and lowest value) diffBuildingHeight: { type: 'boolean', default: false }, zone_elevation: { type: 'number', default: 0.01 }, separation: { type: 'number', default: 0.1 }, extra: { type: 'number', default: 1.0 }, levels: { type: 'number' }, building_color: { type: 'string', default: "#E6B9A1" }, buildingAlpha: { type: 'number', default: 1 }, gradientBaseColor: { type: 'boolean', default: false }, base_color: { type: 'color', default: '#98e690' }, baseAlpha: { type: 'number', default: 1 }, // To add into the doc height_quarter_legend_box: { type: 'number', default: 11 }, height_quarter_legend_title: { type: 'number', default: 12 }, height_building_legend: { type: 'number', default: 0 }, legend_scale: { type: 'number', default: 1 }, legend_lookat: { type: 'string', default: "[camera]" }, legend_text: { type: 'string', default: '{name}\n{fheight} (height): {height}\n{farea} (area): {area}\n{fwidth} (width): {width}\n{fdepth} (depth): {depth}\n{fcolor} (color): {color}' }, legendsAsChildren: { type: 'boolean', default: false }, legendsAsChildrenHeight: { type: 'number', default: 3 }, metricsInfoId: { type: 'string', default: "" }, highlightQuarter: { type: 'boolean', default: false }, hideQuarterBoxLegend: { type: 'boolean', default: false }, highlightQuarterByClick: { type: 'boolean', default: false }, field: { type: 'string', default: 'uid' }, // Numeric color legend entity to hide/show numericColorLegendId: { type: 'string' }, // Highlight building by field highlightBuildingByField: { type: 'string' }, highlightBuildingByFieldColor: { type: 'string', default: 'white' }, // Wireframe & Transparency by repeated IDs wireframeByRepeatedField: { type: 'string' }, transparent80ByRepeatedField: { type: 'string' }, transparent20ByRepeatedField: { type: 'string' }, // Autoscale when animating or starting autoscale: { type: 'boolean', default: false }, autoscaleSizeX: { type: 'number', default: 3 }, autoscaleSizeZ: { type: 'number', default: 3 }, //autoscaleSizeY: { type: 'number', default: 2 } // New layout treeLayout: { type: 'boolean', default: false }, treeQuartersLevelHeight: { type: 'number', default: 0.2 }, treeFixQuarterHeight: { type: 'boolean', default: false }, treeHideOneSonQuarters: { type: 'boolean', default: false } }, /** * Entities with legend activated */ entitiesWithLegend: [], legendsActive: [], /** * Querier component target */ dataComponent: undefined, /** * Where the data is gonna be stored */ newData: undefined, /** * Where the metaddata is gonna be stored */ babiaMetadata: { id: 0 }, /** * List of visualization properties */ visProperties: ['height', 'area', 'width', 'depth', 'color'], /** * Set if component needs multiple instancing. */ multiple: false, /** * */ duration: 2000, figures_del: [], figures_in: [], animation: false, /** * Called once when component is attached. Generally for initial setup. */ init: function () { this.notiBuffer = new NotiBuffer(); }, /** * Called when component is attached and when component data changes. * Generally modifies the entity based on the data. */ update: function (oldData) { updateFunction(this, oldData) }, /** * Already autoscaled */ alreadyAutoscaled: false, autoscaleBoats: function () { const self = this // 2 for X and 2 for Y let bbox = new THREE.Box3().setFromObject(this.el.object3D) let finalSizeX = bbox.max.x - bbox.min.x let finalSizeY = bbox.max.y - bbox.min.y let finalSizeZ = bbox.max.z - bbox.min.z let currentScale = this.el.getAttribute("scale") if (!currentScale) { currentScale = { x: 1, y: 1, z: 1 } } this.el.setAttribute("scale", { x: eval(this.data.autoscaleSizeX / finalSizeX) * currentScale.x, y: currentScale.y, z: eval(this.data.autoscaleSizeZ / finalSizeZ) * currentScale.z }) }, /** * Called on each scene tick. */ tick: function (t, delta) { let self = this; // First time to autoscale if (this.data.autoscale && !this.alreadyAutoscaled) { let bbox = new THREE.Box3().setFromObject(this.el.object3D) if (bbox.min.x !== Infinity) { this.autoscaleBoats() this.alreadyAutoscaled = true } } if (this.animation) { let t = { x: 0, y: 0, z: 0 }; if ((Date.now() - this.start_time) > this.duration) { this.animation = false; this.setFigures(this.figures, t); // Animation finished, set autoscale if activated if (this.data.autoscale) { this.autoscaleBoats() } //Reactivate legends, check PERFORMANCE self.entitiesWithLegend = self.entitiesWithLegend.filter(item => document.getElementById(item.entity.id)) self.entitiesWithLegend.forEach(item => { let entity = item.entity let figure = item.figure if (figure.children) { // Quarter entity.legend = generateLegend(entity.babiaRawData || {}, { name: figure.name, scale: self.data.legend_scale, lookat: self.data.legend_lookat, template: self.data.legend_text || '{name}', colorPlane: 'black', colorText: 'white', isQuarter: true, fields: {} }); let worldPos = new THREE.Vector3(); let coordinates = worldPos.setFromMatrixPosition(entity.object3D.matrixWorld); let coordinatesFinal = { x: coordinates.x, y: self.data.height_quarter_legend_title, z: coordinates.z } // If legend is a child of the building/quarter if (self.data.legendsAsChildren) { entity.legend.setAttribute('position', { x: 0, y: self.data.legendsAsChildrenHeight, z: 0 }) entity.legend.setAttribute('visible', true); entity.appendChild(entity.legend) } else { entity.legend.setAttribute('position', coordinatesFinal) entity.legend.setAttribute('visible', true); self.el.parentElement.appendChild(entity.legend) } self.legendsActive.push(entity.legend) entity.alreadyActive = true } else { // Building entity.legend = entity.legend = generateLegend(entity.babiaRawData, { name: figure.name, scale: self.data.legend_scale, lookat: self.data.legend_lookat, template: self.data.legend_text, colorPlane: 'white', colorText: 'black', isQuarter: false, fields: { height: self.data.height, area: self.data.area, width: self.data.width, depth: self.data.depth, color: self.data.color, } }); let worldPos = new THREE.Vector3(); let coordinates = worldPos.setFromMatrixPosition(entity.object3D.matrixWorld); let height_real = new THREE.Box3().setFromObject(entity.object3D) let coordinatesFinal = { x: coordinates.x, y: height_real.max.y + 1 + self.data.height_building_legend, z: coordinates.z } entity.legend.setAttribute('position', coordinatesFinal) entity.legend.setAttribute('visible', true); self.el.parentElement.appendChild(entity.legend); self.legendsActive.push(entity.legend) entity.alreadyActive = true } }); } else { this.Animation(this.el, this.figures, this.figures_old, delta, t, t); } } }, updateChart: function (items) { //console.log('Data Loaded.'); let t = { x: 0, y: 0, z: 0 }; this.idsToNotRepeatWireframe = [] this.idsToNotRepeat20Transparent = [] this.idsToNotRepeat80Transparent = [] // when animation not finished, delete figures and opa 1 to inserted if (this.animation) { this.figures_del.forEach(figure => { let entity = document.getElementById(figure.id); if (entity) { entity.remove(); } }) this.figures_in.forEach(figure => { let entity = document.getElementById(figure.id); if (entity) { setOpacity(entity, 1.0); } }) this.setFigures(this.figures, t); this.animation = false; this.figures_del = []; this.figures_in = []; } this.figures_old = this.figures; this.figures = []; let el = this.el; let elements = items // Calculate Increment let increment; if (this.data.levels) { increment = this.data.border * this.data.extra * this.data.levels; } else { // Find last level let levels = getLevels(elements, 0); //console.log("Levels:" + levels); increment = this.data.border * this.data.extra * (levels + 1); } // Register all figures before drawing [x, y, t, this.figures] = this.generateElements(elements, this.figures, t, increment); // Draw figures t.x = 0; t.z = 0; if (this.figures_old.length == 0) { // First, delete all elements of the component while (this.el.firstChild) this.el.firstChild.remove(); this.drawElements(el, this.figures, t); } else { if (this.figures_old !== this.figures) { this.animation = true; this.start_time = Date.now(); } } }, /** * For the categoric colors */ categoricColorIndex: 0, categoricColorMaps: {}, /** * generateElements * * Generate width, depth and translation coords for the center, * and list of figures to visualize, from a tree of data elements and * a partial list of figures. * * Each element is a data element, obtained from the data to visualize, * which may include children elements (so, in fact, it may also be the root * of a tree of elements). * * Each figure includes all data needed to visualize it (which will be done * later, in another function). * * @param {*} elements Array of elements * @param {*} figures Array of figures * @param {*} translate Translation from the center * @param {*} inc Increment * @returns [width, depth, translate, figures] */ generateElements: function (elements, figures, translate, inc) { const self = this; const data = this.data; var increment = inc; // Vertical limits (of all figures up to now) var limit_up = 0; var limit_down = 0; // Horizontal limits (of all figures up to now) var limit_right = 0; var limit_left = 0; //Position of the center of current figure var posX = 0; var posY = 0; // Aux variables to update limits // Save max limits to update limits in the next step var max_right = 0; var max_left = 0; var max_down = 0; var max_up = 0; // Control points var current_vertical = 0; var current_horizontal = 0; // Controllers var up = false; var down = false; var left = false; var right = true; /** * Get each element and set its position respectly * Then save all data in figures array */ for (let i = 0; i < elements.length; i++) { // To do not overwrite newData let element = {} if (data.width) { element.width = elements[i][data.width] || 0.5 }; if (data.depth) { element.depth = elements[i][data.depth] || 0.5 }; if (data.height) { let height_min = 0; if (data.diffBuildingHeight) { height_min = self.babiaMetadata['heightMin']; }; element.height = self.normalizeValues(height_min, self.babiaMetadata['heightMax'], self.data.minBuildingHeight, self.data.maxBuildingHeight, elements[i][data.height]) || data.minBuildingHeight }; if (data.area) { element.area = elements[i][data.area] || 0.5; element.width = element.depth = Math.sqrt(element.area); } if (elements[i].children) { // There are elements hanging from this element // so, it is a quarter. Let's run this function recursively element.children = elements[i].children var children = []; var translate_matrix; // Save Zone's parameters // Check scale because it should be abosulte, not relative let scale = self.el.getAttribute("scale") if (!scale) { scale = { x: 1, y: 1, z: 1 } } element.height = data.zone_elevation / scale.y; increment -= data.border * data.extra; // Tree layout if (data.treeLayout) { element.children.forEach(el => { if (!el.children) { element.treeMetaphorHeight = el[data.height] return } }); } [element.width, element.depth, translate_matrix, children] = this.generateElements(element.children, children, translate_matrix, increment); translate_matrix.y = element.height; increment = inc; }; if (i == 0) { // This is the first element to place if (data.area && !elements[i].children) { limit_up += Math.sqrt(element.area) / 2; limit_down -= Math.sqrt(element.area) / 2; limit_right += Math.sqrt(element.area) / 2; limit_left -= Math.sqrt(element.area) / 2; } else { limit_up += element.depth / 2; limit_down -= element.depth / 2; limit_right += element.width / 2; limit_left -= element.width / 2; } //console.log("==== RIGHT SIDE ===="); current_horizontal = limit_up } else if (element.height > 0) { console.log("Element: ", i); console.log("Directions (up, right, down, left):", up, right, down, left); console.log("Current (vertical, horizontal):", current_vertical, current_horizontal); console.log("Limits (up, right, down, left):", limit_up, limit_right, limit_down, limit_left); console.log("Max (up, right, down, left):", max_up, max_right, max_down, max_left); console.log("Position (x, y):", posX, posY); if (up) { [current_vertical, posX, posY, max_up] = this.UpSide(element, limit_up, current_vertical, max_up); if (current_vertical > limit_right) { max_right = current_vertical; up = false; right = true; if (max_left < limit_left) { limit_left = max_left; } current_horizontal = limit_up; } } else if (right) { [current_horizontal, posX, posY, max_right] = this.RightSide(element, limit_right, current_horizontal, max_right); console.log("RIGHT", current_horizontal, posX, posY, max_right); if (current_horizontal < limit_down) { max_down = current_horizontal; right = false; down = true; if (max_up > limit_up) { limit_up = max_up; } current_vertical = limit_right; } console.log("RIGHT2", current_horizontal, current_vertical, posX, posY, max_right); } else if (down) { console.log("DOWN1 (element, limit_down, current_vertical, max_down):", element, limit_down, current_vertical, max_down); [current_vertical, posX, posY, max_down] = this.DownSide(element, limit_down, current_vertical, max_down); console.log("DOWN2", current_vertical, posX, posY, max_down); if (current_vertical < limit_left) { max_left = current_vertical; down = false; left = true; if (max_right > limit_right) { limit_right = max_right; } current_horizontal = limit_down; } } else if (left) { [current_horizontal, posX, posY, max_left] = this.LeftSide(element, limit_left, current_horizontal, max_left); if (current_horizontal > limit_up) { max_up = current_horizontal; left = false; up = true; if (max_down < limit_down) { limit_down = max_down; } current_vertical = limit_left; } } } // Save information about the figure // First, fill in common properties, then add those specific for // bases or buildings let figure = { id: "boat-" + elements[i][data.field], posX: posX, posY: posY, height: element.height, translate_matrix: translate_matrix } if (elements[i].children) { // This is the base for a quarter figure.name = elements[i][this.data.field] || '', figure.width = element.width - this.data.separation, figure.depth = element.depth - this.data.separation, figure.children = children, figure.alpha = self.data.baseAlpha, figure.translate_matrix = translate_matrix // Tree layout if (data.treeLayout) { figure.treeMetaphorHeight = element.treeMetaphorHeight; } // Hierarchy level let hierarchyLevel = figure.name.split("/").length - 1; figure.hierarchyLevel = hierarchyLevel; figure.renderOrder = hierarchyLevel; // Gradient color figure.color = self.getColorBase(hierarchyLevel); } else { // This is a building figure.name = (elements[i].name) ? elements[i].name : elements[i][this.data.field], figure.alpha = self.data.buildingAlpha figure.width = element.width - data.separation; figure.depth = element.depth - data.separation; if (typeof elements[i][data.color] === 'number') { figure.color = heatMapColorforValue(elements[i][data.color], self.babiaMetadata['color_max'], self.babiaMetadata['color_min']) } else if (typeof elements[i][data.color] === 'string') { // Categoric color if (elements[i][data.color] in self.categoricColorMaps) { figure.color = self.categoricColorMaps[elements[i][data.color]] } else { self.categoricColorMaps[elements[i][data.color]] = colorsArray[self.categoricColorIndex] figure.color = colorsArray[self.categoricColorIndex] self.categoricColorIndex = self.categoricColorIndex + 1 if (self.categoricColorIndex >= colorsArray.length) { self.categoricColorIndex = 0 } } } } figure.rawData = elements[i] // Set transparency and wireframe, if appropriate self.setTransparency(figure); figure.wireframe = this.getWireframe(figure, data.wireframeByRepeatedField); figures.push(figure); } // Check and update last limits if (max_down < limit_down) { limit_down = max_down; } if (max_left < limit_left) { limit_left = max_left; } if (max_up > limit_up) { limit_up = max_up; } if (max_right > limit_right) { limit_right = max_right; } // Calculate translation of the center, width and depth of the zone var width = Math.abs(limit_left) + Math.abs(limit_right); var depth = Math.abs(limit_down) + Math.abs(limit_up); width += 2 * increment; depth += 2 * increment; var translate_x = limit_left + width / 2 - increment; var translate_z = limit_down + depth / 2 - increment; translate = { x: translate_x, y: 0, z: translate_z, }; return [width, depth, translate, figures]; }, // If wireframerepeated activated idsToNotRepeatWireframe: [], idsToNotRepeat80Transparent: [], idsToNotRepeat20Transparent: [], drawElements: function (element, figures, translate) { const self = this for (let i in figures) { let height = figures[i].height; let x = figures[i].posX; let y = figures[i].posY; let position = { x: x - translate.x, y: (height / 2 + translate.y / 2), z: -y + translate.z } // Tree layout if (self.data.treeLayout) { // Hide quarters with no children if (self.data.treeHideOneSonQuarters && (figures[i].children && figures[i].children.length === 1)) { figures[i].alphaTest = 1 figures[i].dontAddEvents = true figures[i].children[0].dontAddLine = true } if (self.data.treeFixQuarterHeight) { // Fix position y for quarters // Check if not undefined because if it is 0, it returns false (thx JS) if (figures[i].treeMetaphorHeight !== undefined) { position.y = (height / 2 + translate.y / 2) + self.data.treeQuartersLevelHeight } else { position.y = ((height / 2 + translate.y / 2)) - self.data.treeQuartersLevelHeight - self.data.zone_elevation if (figures[i].height < self.data.treeQuartersLevelHeight) { // If flag to hide activated and the parent has only one son, not needed to draw the line if (!(self.data.treeHideOneSonQuarters && figures[i].dontAddLine)) { // First get top let topBuildingY = position.y // Get where the quarter is let quarterPosY = position.y + (self.data.treeQuartersLevelHeight - (height / 2)) // Draw the line in the space let line = document.createElement('a-entity') line.setAttribute('class', 'babiaboatstreelines') line.setAttribute('line', { start: { x: position.x, y: topBuildingY, z: position.z }, end: { x: position.x, y: quarterPosY, z: position.z }, color: 'yellow' }) element.appendChild(line) } } } } else { // Quarters on top of the buildings if (figures[i].treeMetaphorHeight !== undefined) { let height_min = 0; if (self.data.diffBuildingHeight) { height_min = self.babiaMetadata['heightMin']; }; position.y = (self.normalizeValues(height_min, self.babiaMetadata['heightMax'], self.data.minBuildingHeight, self.data.maxBuildingHeight, figures[i].treeMetaphorHeight)) } else { position.y = ((height / 2 + translate.y / 2)) - height - 0.001 } } } let entity = this.createElement(figures[i], position); this.addEvents(entity, figures[i]); element.appendChild(entity); } }, RightSide: function (element, limit_right, current_horizontal, max_right) { const data = this.data; let width, depth; if (data.area && !element.children) { width = Math.sqrt(element.area); depth = Math.sqrt(element.area); } else { width = parseFloat(element.width); depth = parseFloat(element.depth); } // Calculate position let posX = limit_right + (width / 2); let posY = current_horizontal - (depth / 2); // Calculate states current_horizontal -= depth; let total_x = limit_right + width; if (total_x > max_right) { max_right = total_x; } return [current_horizontal, posX, posY, max_right]; }, DownSide: function (element, limit_down, current_vertical, max_down) { const data = this.data; let width, depth; if (data.area && !element.children) { width = Math.sqrt(element.area); depth = Math.sqrt(element.area); } else { width = parseFloat(element.width); depth = parseFloat(element.depth); } let posX = current_vertical - (width / 2); let posY = limit_down - (depth / 2); current_vertical -= width; let total_y = limit_down - depth; if (total_y < max_down) { max_down = total_y; } return [current_vertical, posX, posY, max_down]; }, LeftSide: function (element, limit_left, current_horizontal, max_left) { const data = this.data; let width, depth; if (data.area && !element.children) { width = Math.sqrt(element.area); depth = Math.sqrt(element.area); } else { width = parseFloat(element.width); depth = parseFloat(element.depth); } let posX = limit_left - (width / 2); let posY = current_horizontal + (depth / 2); current_horizontal += depth; let total_x = limit_left - width; if (total_x < max_left) { max_left = total_x; } return [current_horizontal, posX, posY, max_left]; }, UpSide: function (element, limit_up, current_vertical, max_up) { const data = this.data; let width, depth; if (data.area && !element.children) { width = Math.sqrt(element.area); depth = Math.sqrt(element.area); } else { width = parseFloat(element.width); depth = parseFloat(element.depth); } let posX = current_vertical + (width / 2); let posY = limit_up + (depth / 2); current_vertical += width; let total_y = limit_up + depth; if (total_y > max_up) { max_up = total_y; } return [current_vertical, posX, posY, max_up]; }, /** * getColor * * Calculate the color of the element based on its value in color_field. * If the value is a number, it will calculate the color based on the * heat map from color_min to color_max. * If the value is a string, it will assign a color from colorsArray in * order of appearence. * * @param {Object} element - The element to calculate the color for * @param {String} color_field - The field in the element to calculate the color from * * @return {String} - The color for the element */ getColor: function (element, color_field) { const self = this; let color_value = element[color_field]; let color; if (typeof color_value === 'number') { color = heatMapColorforValue(color_value, self.babiaMetadata['color_max'], self.babiaMetadata['color_min']); } else if (typeof color_value === 'string') { // Categoric color if (color_value in self.categoricColorMaps) { color = self.categoricColorMaps[color_value]; } else { self.categoricColorMaps[color_value] = colorsArray[self.categoricColorIndex] color = colorsArray[self.categoricColorIndex] self.categoricColorIndex = self.categoricColorIndex + 1 if (self.categoricColorIndex >= colorsArray.length) { self.categoricColorIndex = 0 } } } return color; }, /** * getColorBase * * Determines the color for the base in a given level, depending on whether * gradientBaseColor is enabled. If enabled, it calculates a color * using the greenColorsurface function based on normalized level * values. If not enabled, it uses the default base color. * * @param {number} level - The level for which to determine the color. * * @return {string} - The calculated base color. */ getColorBase: function (level) { let color; const self = this; if (self.data.gradientBaseColor) { let max_levels = self.babiaMetadata['maxLevels']; color = greenColorsurface(Math.round( self.normalizeValues(1, max_levels, 15, 100, level) )); } else { color = self.data.base_color; }; return color; }, /** * setTransparency * * If "transparency by a field" on buildings is selected * set transparency to 80% or 20% to those buildings that share * same value for the selected field. * @param {Object} figure - The figure to set transparency */ setTransparency: function (figure) { const self = this; const data = this.data; // If transparency by a field on buildings // Put transparent 80% to those buildings that share same value for a field selected const t80_field = data.transparent80ByRepeatedField; const t20_field = data.transparent20ByRepeatedField; if (t80_field && (!figure.children && self.idsToNotRepeat80Transparent.includes(figure.rawData[t80_field]))) { figure.alpha = 0.8 figure.renderOrder = 2000 } else { self.idsToNotRepeat80Transparent.push(figure.rawData[t80_field]) } // Put transparent 20% to those buildings that share same value for a field selected if (t20_field && (!figure.children && self.idsToNotRepeat20Transparent.includes(figure.rawData[t20_field]))) { figure.alpha = 0.45 figure.renderOrder = 2000 } else { self.idsToNotRepeat20Transparent.push(figure.rawData[t20_field]) } }, /** * getWireframe * * Get boolean to decide if wireframe will be put to the elements * that share the same value of the selected field. * @param {Object} figure - The figure to set wireframe * @param {String} wireframe_field - The field in the element to decide the wireframe * * @return {Boolean} - True if the element will be visualized as wireframe */ getWireframe: function (figure, wireframe_field) { const self = this; let wireframe; // Put wireframe to those buildings that share same value for a field selected if (wireframe_field && (!figure.children && self.idsToNotRepeatWireframe.includes(figure.rawData[wireframe_field]))) { wireframe = true; } else { wireframe = false; self.idsToNotRepeatWireframe.push(figure.rawData[wireframe_field]); } return wireframe; }, Animation: function (element, figures, figures_old, delta, translate, translate_old) { let self = this let new_time = Date.now(); let entity; // First, remove all the legends that are active self.legendsActive.forEach(legend => { legend.remove() }); self.legendsActive = [] for (let i in figures) { if (document.getElementById(figures[i].id)) { // If exists entity = document.getElementById(figures[i].id); // Creating... (next ticks) if (figures[i].inserted) { //TODO: This code increments the opacity in the animation part, but it adds several performance issues //Increment opacity // let opa_inc = delta / this.duration; // let opacity = parseFloat(entity.getAttribute('material').opacity); // if (opacity + opa_inc < 1) { // opacity += opa_inc; // } else { // opacity = 1.0; // figures[i].inserted = false; // } //setOpacity(entity, opacity); // If animation stops before finish let cond = 'id=' + figures[i].id if (findIndex(self.figures_in, cond) < 0) { self.figures_in.push(figures[i]) } } else { // find index in old_figures let cond = 'id=' + figures[i].id let index = findIndex(figures_old, cond) // Update rawData entity.babiaRawData = figures[i].rawData if (index < 0) { // encontrar el elemento que no esta en la escena con id y darle opacidad // y cambiar sus propiedades a la nueva si es necesario figures[i].inserted = true; if (entity.getAttribute('width') != figures[i].width) { entity.setAttribute('width', figures[i].width); } if (entity.getAttribute('height') != figures[i].height) { entity.setAttribute('height', figures[i].height); } if (entity.getAttribute('depth') != figures[i].depth) { entity.setAttribute('depth', figures[i].depth); } if (entity.getAttribute('color') != figures[i].color) { entity.setAttribute('color', figures[i].color); } if (entity.getAttribute('material').wireframe != figures[i].wireframe) { entity.setAttribute('material', 'wireframe', figures[i].wireframe); entity.setAttribute('material', 'wireframeLinewidth', 0.1); } } else { // RESIZE this.resize(entity, new_time, delta, figures[i], figures_old[index]); // TRASLATE this.traslate(entity, new_time, delta, figures[i], figures_old[index], translate, translate_old); // COLOR if (entity.getAttribute('color') != figures[i].color && !self.inEntitiesWithLegend(entity)) { entity.setAttribute('color', figures[i].color); } if (entity.getAttribute('material').wireframe != figures[i].wireframe) { entity.setAttribute('material', 'wireframe', figures[i].wireframe); entity.setAttribute('material', 'wireframeLinewidth', 0.1); } if (entity.getAttribute('material').opacity != figures[i].alpha) { entity.setAttribute('material', 'opacity', figures[i].alpha); entity.object3D.renderOrder = figures[i].renderOrder } if (figures[i].children) { this.Animation(entity, figures[i].children, figures_old[index].children, delta, figures[i].translate_matrix, figures_old[index].translate_matrix); } else { // Building color if changed, force to the new one if (figures[i].rawData[self.data.color] !== figures_old[index].rawData[self.data.color]) { // Color numeric or categoric let color if (typeof figures[i].rawData[self.data.color] === 'number') { color = heatMapColorforValue(figures[i].rawData[self.data.color], self.babiaMetadata['color_max'], self.babiaMetadata['color_min']) } else if (typeof figures[i].rawData[self.data.color] === 'string') { // Categoric color if (figures[i].rawData[self.data.color] in self.categoricColorMaps) { color = self.categoricColorMaps[figures[i].rawData[self.data.color]] } else { self.categoricColorMaps[figures[i].rawData[self.data.color]] = colorsArray[self.categoricColorIndex] color = colorsArray[self.categoricColorIndex] self.categoricColorIndex = self.categoricColorIndex + 1 if (self.categoricColorIndex >= colorsArray.length) { self.categoricColorIndex = 0 } } } let oldColor = entity.getAttribute('color') if (color !== oldColor) { entity.setAttribute('color', color) } } } } } } else { // CREATE NEW position = { x: figures[i].posX - translate.x, y: (figures[i].height / 2 + translate.y / 2), z: -figures[i].posY + translate.z } let new_entity = this.createElement(figures[i], position); this.addEvents(new_entity, figures[i]); //TODO: bad perfomance when Opacity 0 at the beginning //setOpacity(new_entity, 0); // New building and quarter, it appears with 0.5 opacity setOpacity(new_entity, 0.5); element.appendChild(new_entity); figures[i].inserted = true; } } // Delete figures let opa_dec = delta / this.duration; for (let i in figures_old) { let deleted = false; let cond = 'id=' + figures_old[i].id let index = findIndex(figures, cond) if (index < 0) { if (findIndex(self.figures_del, cond) < 0) { self.figures_del.push(figures_old[i]) } let entity_del = document.getElementById(figures_old[i].id); if (entity_del) { //TODO: Bad perfomance issue when changing dinamically the opacity, better to just make it non vissible entity_del.object3D.visible = false entity_del.remove(); // let opacity = parseFloat(entity_del.components.material.opacity); // if (opacity - opa_dec > 0) { // opacity -= opa_dec; // } else { // opacity = 0.0; // deleted = true; // self.figures_del.pop(figures_old[i]); // } // setOpacity(entity_del, opacity); // if (deleted) { // entity_del.remove(); // } } } } }, inEntitiesWithLegend(obj) { let i; for (i = 0; i < this.entitiesWithLegend.length; i++) { if (this.entitiesWithLegend[i].entity === obj) { return true; } } return false; }, setFigures: function (figures, translate) { const self = this figures.forEach(figure => { let entity = document.getElementById(figure.id); if (entity) { if (entity.getAttribute('width') != figure.width) { entity.setAttribute('width', figure.width); } if (entity.getAttribute('height') != figure.height) { entity.setAttribute('height', figure.height); } if (entity.getAttribute('depth') != figure.depth) { entity.setAttribute('depth', figure.depth); } // If legend active, don't force change color, put it as the color before if (self.inEntitiesWithLegend(entity)) { entity.setAttribute('babiaxrFirstColor', figure.color) } else { if (entity.getAttribute('color') != figure.color) { entity.setAttribute('color', figure.color); } } //TODO: Full opacity because it was in 0.5 if new building/quarter if (entity.components.material.data.opacity < 1) { setOpacity(entity, 1) } // Quarters to alpha if tree fix position if (entity.getAttribute('material').opacity != figure.alpha) { entity.setAttribute('material', 'opacity', figure.alpha); } // TEST TREE if (self.data.treeLayout) { // Hide quarters that has not children if (self.data.treeHideOneSonQuarters && (figure.children && figure.children.length === 1)) { figure.alphaTest = 1 figure.dontAddEvents = true figure.children[0].dontAddLine = true } if (self.data.treeFixQuarterHeight) { // Fix position y for quarters if (figure.treeMetaphorHeight !== undefined) { entity.object3D.position.set( figure.posX - translate.x, (figure.height / 2 + translate.y / 2) + self.data.treeQuartersLevelHeight, (- figure.posY + translate.z), ) } else { let positionYFixed = ((figure.height / 2 + translate.y / 2)) - self.data.treeQuartersLevelHeight - self.data.zone_elevation let positionX = figure.posX - translate.x let positionZ = (- figure.posY + translate.z) entity.object3D.position.set( positionX, positionYFixed, positionZ, ) // DRAW LINE WHEN DOES NOT ACHIEVE THE QUARTER (TOO LOW) if (figure.height < self.data.treeQuartersLevelHeight) { // If flag to hide activated and the parent has only one son, not needed to draw the line if (!(self.data.treeHideOneSonQuarters && figure.dontAddLine)) { // First get top let topBuildingY = positionYFixed // Get where the quarter is let quarterPosY = positionYFixed + (self.treeQuartersLevelHeight - figure.height / 2) // Draw the line in the space let line = document.createElement('a-entity') line.setAttribute('class', 'babiaboatstreelines') line.setAttribute('line', { start: { x: positionX, y: topBuildingY, z: positionZ }, end: { x: positionX, y: quarterPosY, z: positionZ }, color: 'yellow' }) entity.parentElement.appendChild(line) } } } } else { if (figure.treeMetaphorHeight !== undefined) { let height_min = 0; if (self.data.diffBuildingHeight) {