UNPKG

aframe-babia-components

Version:

A data visualization set of components for A-Frame.

1,406 lines (1,280 loc) 54.1 kB
/* global AFRAME */ if (typeof AFRAME === 'undefined') { throw new Error('Component attempted to register before AFRAME was available.'); } let rootCodecityEntity /** * CodeCity component for A-Frame. */ AFRAME.registerComponent('babiaxr-codecity', { schema: { // Absolute size (width and depth will be used for proportions) absolute: { type: 'boolean', default: false }, width: { type: 'number', default: 20 }, depth: { type: 'number', default: 20 }, // Algoritm to split rectangle in buildings: naive, pivot split: { type: 'string', default: 'naive' }, // Data to visualize data: { type: 'string', default: JSON.stringify({ id: "CodeCity", area: 1, height: 1 }) }, // Field in data items to represent as area farea: { type: 'string', default: 'area' }, // Field in data items to represent as max_area fmaxarea: { type: 'string', default: 'max_area' }, // Field in data items to represent as area fheight: { type: 'string', default: 'height' }, // Titles on top of the buildings when hovering titles: { type: 'boolean', default: true }, // Base: color building_color: { type: 'color', default: '#E6B9A1' }, building_model: { type: 'string', default: null }, // Base (build it or not) base: { type: 'boolean', default: true }, // Base: thickness base_thick: { type: 'number', default: 0.2 }, // Base: color base_color: { type: 'color', default: '#98e690' }, // Size of border around buildings (streets are built on it) border: { type: 'number', default: 1 }, // Extra factor for total area with respect to built area extra: { type: 'number', default: 1.4 }, // Zone: elevation for each "depth" of quarters, over the previous one zone_elevation: { type: 'number', default: 1 }, // Unique color for each zone unicolor: { type: 'color', default: false }, // Show materials as wireframe wireframe: { type: 'boolean', default: false }, colormap: { type: 'array', default: ['blue', 'green', 'yellow', 'brown', 'orange', 'magenta', 'grey', 'cyan', 'azure', 'beige', 'blueviolet', 'coral', 'crimson', 'darkblue', 'darkgrey', 'orchid', 'olive', 'navy', 'palegreen'] }, // Time evolution time changing between snapshots time_evolution_delta: { type: 'number', default: 8000 }, // Time evolution time changing between snapshots time_evolution_init: { type: 'string', default: 'data_0' }, // Time evolution direction time_evolution_past_present: { type: 'boolean', default: false }, time_evolution_animation: { type: 'boolean', default: true }, time_evolution_color: { type: 'boolean', default: false }, // ui navbar UD ui_navbar: { type: 'string', default: "" } }, /** * Set if component needs multiple instancing. */ multiple: false, /** * Called once when component is attached. Generally for initial setup. */ init: function () { }, /** * Called when component is attached and when component data changes. * Generally modifies the entity based on the data. */ update: function (oldData) { this.loader = new THREE.FileLoader(); let data = this.data; let el = this.el; currentColor = data.building_color; rootCodecityEntity = el; if (typeof data.data == 'string') { if (data.data.endsWith('json')) { raw_items = requestJSONDataFromURL(data); } else { raw_items = JSON.parse(data.data); } } else { raw_items = data.data; }; el.emit('babiaxr-dataLoaded', { data: raw_items, codecity: true }) deltaTimeEvolution = data.time_evolution_delta this.zone_data = raw_items; let zone = new Zone({ data: this.zone_data, extra: function (area) { return area * data.extra; }, farea: data.farea, fheight: data.fheight, fmaxarea: data.fmaxarea }); let width, depth; if (data.absolute == true) { width = Math.sqrt(zone.areas.canvas) * data.width / data.depth; depth = zone.areas.canvas / width; } else { width = data.width; depth = data.depth }; // New levels are entities relative (children of the previous level) or not let relative = false; let canvas = new Rectangle({ width: width, depth: depth, x: 0, z: 0 }); zone.add_rects({ rect: canvas, split: data.split, relative: relative }); let base = document.createElement('a-entity'); this.base = base; let visible = true; zone.draw_rects({ ground: canvas, el: base, base: data.base, level: 0, elevation: 0, relative: relative, base_thick: data.base_thick, wireframe: data.wireframe, building_color: data.building_color, base_color: data.base_color, model: data.building_model, visible: visible, titles: data.titles }); el.appendChild(base); // Time Evolution starts if (time_evolution) { time_evolution_past_present = data.time_evolution_past_present time_evolution_animation = data.time_evolution_animation time_evolution_color = data.time_evolution_color dateBar(data) time_evol() } }, /** * Called when a component is removed (e.g., via removeAttribute). * Generally undoes all modifications to the entity. */ remove: function () { }, /** * Called on each scene tick. */ // tick: function (t) { }, /** * Called when entity pauses. * Use to stop or remove any dynamic or background behavior such as events. */ // pause: function () { }, /** * Called when entity resumes. * Use to continue or add any dynamic or background behavior such as events. */ play: function () { } }); /* * Class for storing zone, with all its subzones and items, to show as buildings */ let Zone = class { /* * Constructor, based on a tree. * * Each node of the tree must include 'id' and 'children', * except if it is a leaf, in wihc case must include 'id' * and fields for computing area and height. * The tree can also come as a JSON-encoded string. * * @constructor * @param {object} data Tree with data to store in the object * @param {function} extra Function to compute extra area for canvas, based on area * @param {string} farea Field to consider as area in leaf items * @param {string} fheight Field to consider as height in leaf items */ constructor({ data, extra = function (area) { return area; }, farea = 'area', fmaxarea = 'max_area', fheight = 'height' }) { this.data = data; this.id = this.data.id; this.extra = extra; this.farea = farea; this.fmaxarea = fmaxarea; this.fheight = fheight; this.areas = this.areas_tree(); // Root element (a-entity) of the codecity for this Zone this.el = null; // Number of rectangles to be drawn as buildings, but still not in the scene this.pending_rects = 0; } /* * Compute areas for each node of the subree at node * * Annotates each node with: * .area: accumulated area of all children * .inner: area of the inner rectangle (acc. canvas of all children) * .canvas: area of the canvas for this node */ areas_tree({ data = this.data, level = 0 } = {}) { let data_node = data; let node = { data: data_node }; if ('children' in data_node) { node.inner = 0; node.area = 0; node.children = []; for (const data_child of data_node.children) { let child = this.areas_tree({ data: data_child, level: level + 1 }); node.inner += child.canvas; node.area += child.area; node.children.push(child); }; } else { // Leaf node node.area = data_node[this.farea]; node.max_area = data_node[this.fmaxarea] node.inner = node.max_area; node.inner_real = node.area; }; node.canvas = this.extra(node.inner, level); return node; } /** * Add rectangles to a canvas rectangle, according to info in an areas subtree * * @param {Rectangle} rect Rectangle acting as canvas for the next level * @param {Object} area Node of an areas tree, as it was composed by areas_tree() */ add_rects({ rect, area = this.areas, relative = true, split = 'naive' } = {}) { // Make this the rectangle for the area, and compute its inner dimensions area.rect = rect; area.rect.inner(area.canvas, area.inner, area.inner_real); if ('children' in area) { let child_areas = new Values(area.children.map(child => child.canvas), area.inner); let child_rect; if (split === 'naive') { child_rect = area.rect.split(child_areas, relative); // console.log("Naive split"); } else if (split === 'pivot') { child_rect = area.rect.split_pivot(child_areas, relative); // console.log("Pivot split"); } else { throw new Error("CodeCity: Unknwon split method"); }; for (const i in area.children) { this.add_rects({ rect: child_rect[i], area: area.children[i], relative: relative, split: split }); }; }; } /** * Draw all rectangles for an area tree * * @param {Rectangle} ground Rectangle for the ground * @param {DOMElement} el DOM element that will be parent * @param {boolean} visible Draw elements with visible meshes * @return {number} Number of rectangles drawn */ draw_rects({ ground, el, area = this.areas, level = 0, elevation = 0, relative = true, base_thick = .2, wireframe = false, building_color = "red", base_color = "green", model = null, visible = true, titles = true }) { if (level === 0) { this.el = el; }; let pending_rects = this.pending_rects; if ('children' in area) { // Create base for this area, and go recursively to the next level let base = area.rect.box({ elevation: elevation, height: base_thick, color: base_color, inner: false, wireframe: wireframe, visible: visible, id: area.data['id'], rawarea: 0 }); // Titles on quarters if (titles) { let legend; let transparentBox; base.addEventListener('click', function () { if (legend) { rootCodecityEntity.removeChild(transparentBox) rootCodecityEntity.removeChild(legend) legend = undefined transparentBox = undefined } else { transparentBox = document.createElement('a-entity'); let oldGeometry = base.getAttribute('geometry') let boxPosition = base.getAttribute("position") transparentBox.setAttribute('geometry', { height: oldGeometry.height + 10, depth: oldGeometry.depth, width: oldGeometry.width }); transparentBox.setAttribute('position', boxPosition) transparentBox.setAttribute('material', { 'visible': true, 'opacity': 0.4 }); legend = generateLegend(this.getAttribute("id"), oldGeometry.height + 10, boxPosition, null, rootCodecityEntity); rootCodecityEntity.appendChild(legend) rootCodecityEntity.appendChild(transparentBox) } }) } base.setAttribute('class', 'babiaxraycasterclass'); el.appendChild(base); let root_el = base; if (!relative) { root_el = el }; for (const child of area.children) { let next_elevation = base_thick / 2; if (!relative) { next_elevation = elevation + base_thick }; this.draw_rects({ ground: area.rect, el: root_el, area: child, level: level + 1, elevation: next_elevation, relative: relative, building_color: building_color, base_color: base_color, model: model, base_thick: base_thick, wireframe: wireframe, visible: visible, titles: titles }); }; } else { // Leaf node, create the building let height = area.data[this.fheight]; let box = area.rect.box({ height: area.data[this.fheight], elevation: elevation, wireframe: wireframe, color: building_color, model: model, visible: visible, id: area.data['id'], rawarea: area.data[this.farea], inner_real: true }); box.setAttribute('class', 'babiaxraycasterclass'); el.appendChild(box); // Titles if (titles) { let legend; let legendBox; let alreadyActive = false; box.addEventListener('click', function () { if (alreadyActive) { rootCodecityEntity.removeChild(legend) rootCodecityEntity.removeChild(legendBox) legend = undefined legendBox = undefined alreadyActive = false } else { alreadyActive = true } }) box.addEventListener('mouseenter', function () { if (!alreadyActive) { legendBox = document.createElement('a-entity'); let oldGeometry = box.getAttribute('geometry') let boxPosition = box.getAttribute("position") legendBox.setAttribute('position', boxPosition) legendBox.setAttribute('material', { 'visible': true }); legendBox.setAttribute('geometry', { height: oldGeometry.height + 0.1, depth: oldGeometry.depth + 0.1, width: oldGeometry.width + 0.1 }); legend = generateLegend(this.getAttribute("id"), oldGeometry.height + 0.1, boxPosition, null, rootCodecityEntity); rootCodecityEntity.appendChild(legend) rootCodecityEntity.appendChild(legendBox) } }) box.addEventListener('mouseleave', function () { if (!alreadyActive && legend) { rootCodecityEntity.removeChild(legend) rootCodecityEntity.removeChild(legendBox) legend = undefined legendBox = undefined } }) } }; }; }; /** * Class for lists (arrays) of values */ let Values = class { /* * @param {Array} values Array with values (Number) */ constructor(values, total) { this.items = values; if (typeof (total) !== 'undefined') { this.total = total; } else { this.total = values.reduce((acc, a) => acc + a, 0); }; } imax() { let largest = this.items[0]; let largest_i = 0; for (let i = 0; i < this.items.length; i++) { if (largest < this.items[i]) { largest = this.items[i]; largest_i = i; }; }; return largest_i; } static range(start, length) { var indexes = []; for (let i = start; i < start + length; i++) { indexes.push(i); }; return indexes; } /* * Return the scaled area, for a rectangle area, of item i * * @param {Number} area Total area of the rectangle * @param {Integer} item Item number (starting in 0) */ scaled_area(area, item) { return this.items[item] * area / this.total; } /* * Produce a Values object for items in positions * * @param {array} positions Positions of items to produce the new Values object */ values_i(positions) { let values = []; for (const position of positions) { values.push(this.items[position]) }; return new Values(values); } /** * Produce pivot and three regions * * The array of values will be split in an element (pivot) and * three arrays (a1, a2, a3). The function will return the * index in the array of values for each of its items in the * pivot and the three regions. * This function assumes there are at least three items in the object. * It also assumes that the rectangle is laying. * * @return {Object} Pivot and regions, as properties of the object */ pivot_regions(width, depth) { if (this.items.length < 3) { throw new Error("CodeCity - Values.pivot_regions: less than three items"); }; if (width < depth) { throw new Error("Codecity - Values.pivot_regions: rectangle should be laying"); }; let a1_len, a2_len, a3_len; let pivot_i = this.imax(); if (this.items.lenght == pivot_i + 1) { // No items to the right of pivot. a2, a3 empty return { pivot: pivot_i, a1: Values.range(0, pivot_i), a2: [], a3: [] }; }; if (this.items.length == pivot_i + 2) { // Only one item to the right of pivot. It is a2. a3 is empty. return { pivot: pivot_i, a1: Values.range(0, pivot_i), a2: [pivot_i + 1], a3: [] }; }; // More than one item to the right of pivot. // Compute a2 so that pivot can be as square as possible let area = width * depth; let pivot_area = this.scaled_area(area, pivot_i); let a2_width_ideal = Math.sqrt(pivot_area); let a2_area_ideal = a2_width_ideal * depth - pivot_area; let a2_area = 0; let a2_area_last = a2_area; let i = pivot_i + 1; while (a2_area < a2_area_ideal && i < this.items.length) { a2_area_last = a2_area; a2_area += this.scaled_area(area, i); i++; }; // There are two candidates to be the area closest to the ideal area: // the last area computed (long), and the one that was conputed before it (short), // provided the last computed one is not the next to the pivot (in that case, // the last computed is the next to the pivot, and therefore it needs to be the // first in a3. let a3_first = i; if ((i - 1 > pivot_i) && (Math.abs(a2_area - a2_area_ideal) > Math.abs(a2_area_last - a2_area_ideal))) { a3_first = i - 1; }; a2_len = a3_first - pivot_i - 1; a3_len = this.items.length - a3_first; return { pivot: pivot_i, a1: Values.range(0, pivot_i), a2: Values.range(pivot_i + 1, a2_len), a3: Values.range(pivot_i + 1 + a2_len, a3_len) }; } /* * Compute the width for a region, for a rectangle of given width * (region is a rectangles with rectangle depth as depth) * * @param {array} values Position of values belonging to region * @param {number} width Width of rectangle */ pivot_region_width(values, width) { let region_total = 0; for (const i of values) { region_total += this.items[i] }; return (region_total / this.total) * width; } }; /* * Rectangles, using AFrame coordinates */ let Rectangle = class { /* * Build a rectangle, given its parameters * * @constructor * @param {number} width Width (side parallel to X axis) * @param {number} depth Depth (side parallel to Z axis) * @param {number} x X coordinate * @param {number} z Z coordinate * @param {boolean} revolved Was the rectangle revolved? */ constructor({ width, depth, x = 0, z = 0 }) { this.width = width; this.depth = depth; this.x = x; this.z = z; } /* * Is the rectangle laying, inner dimensions? * (is width the longest side?) * * @return {boolean} True if width is the longest side. */ is_ilaying() { let longest = Math.max(this.width, this.depth); return (longest == this.width); } /* * Add the inner area rectangle, assuming this is the canvas * Note: canvas and area are not the real area of canvas and * area, but the numbers used to compute the proportion * If there si no acanvas, it is assumed that inner is equal to canvas * * @param {number} canvas Value for area of canvas * @param {number} area Value for area of inner */ inner(acanvas, ainner, ainner_real) { if (acanvas < ainner) { throw "Rectangle.inner: Area for inner rectangle larger than my area" }; if (typeof acanvas !== 'undefined') { let ratio = Math.sqrt(ainner / acanvas); this.iwidth = ratio * this.width; this.idepth = ratio * this.depth; if (ainner_real) { // The area to print may be less than the max area (of the past) let ratio_real = Math.sqrt(ainner_real / acanvas); this.iwidth_real = ratio_real * this.width; this.idepth_real = ratio_real * this.depth; } } else { this.iwidth = this.width; this.idepth = this.depth; }; } /* * Reflect (change horizontal for vertical dimensions) * Only for width, depth, x, y */ reflect() { [this.width, this.depth] = [this.depth, this.width]; [this.x, this.z] = [this.z, this.x]; } /* * Return inner dimensions (plus position) as if rectangle was laying. * * Check if rectangle is laying. If it is not, return dimensions as if * reflected (but not reflect it). Last element in the resturned array * is a boolean indicating if values were reflected or not. * * @return {Array} Inner values: [iwidth, idepth, x, y, reflected] */ idims_as_laying() { if (this.is_ilaying()) { return [this.iwidth, this.idepth, this.x, this.z, false]; } else { return [this.idepth, this.iwidth, this.z, this.x, true]; }; } /* * Split according to data in values (array) * * Split is of the inner rectangle. * If relative is true, the coordinates of the resulting rectangle * consider the center of the canvas rectangle as 0,0. * If relative is false, the coordinates of the resulting rectangle * consider the center of the canvas as x,z (coordinates of the * rectangle to split. * * @param {Values} values Values to be used to split the rectangle * @param {boolean} relative Result is in relative (center in 0,0) or not */ split(values, relative = true) { // Always split on width, as if the rectangle was laying. // Use local variables to point to the rigth real dimensions let [width, depth, x, z, reflected] = this.idims_as_laying(); // Ratio to convert a size in a split (part of total) let ratio = width / values.total; let current_x = -width / 2; let current_z = 0; if (!relative) { current_x += x; current_z = z; }; let rects = []; // Value of fields scaled to fit total canvas for (const value of values.items) { let sub_width = value * ratio; let rect = new Rectangle({ width: sub_width, depth: depth, x: current_x + sub_width / 2, z: current_z }); if (reflected) { // Dimensions were reflected, reflect back rect.reflect(); }; rects.push(rect); current_x += sub_width; }; return rects; } /* * Split according to data in values (array), with the pivot algorithm * * Split is of the inner rectangle */ split_pivot(values, relative = true) { // Always split on width, as if the rectangle was laying. // Use local variables to point to the rgith real dimensions if (values.items.length <= 2) { // Only one or two values, we cannot apply pivot, apply naive return this.split(values, relative); }; let [width, depth, x, z, reflected] = this.idims_as_laying(); if (relative) { x = 0; z = 0; }; let { pivot, a1, a2, a3 } = values.pivot_regions(width, depth); // Dimensions for areas (a1, a2, a3) let width_a1 = values.pivot_region_width(a1, width); let width_a2 = values.pivot_region_width(a2.concat(pivot), width); let width_a3 = values.pivot_region_width(a3, width); let x_a1 = x - width / 2 + width_a1 / 2; let x_a2 = x - width / 2 + width_a1 + width_a2 / 2; let x_a3 = x - width / 2 + width_a1 + width_a2 + width_a3 / 2; let rects = []; // Pivot rectangle let depth_pivot = values.scaled_area(width * depth, pivot) / width_a2; rects[pivot] = new Rectangle({ width: width_a2, depth: depth_pivot, x: x_a2, z: z + depth / 2 - depth_pivot / 2 }); // Dimensions for each area (and corresponding rectangle) let dim_areas = [ [a1, width_a1, depth, x_a1, z], [a2, width_a2, depth - depth_pivot, x_a2, z - depth_pivot / 2], [a3, width_a3, depth, x_a3, z]]; for (const [values_i, width_i, depth_i, x_i, z_i] of dim_areas) { if (values_i.length > 0) { let subrect = new Rectangle({ width: width_i, depth: depth_i, x: x_i, z: z_i }); subrect.inner(); // Ensure we add rectangles in the right places let subvalues = values.values_i(values_i); // Further splits should always be absolute, wrt my coordinates let rects_i = subrect.split_pivot(subvalues, false); let counter = 0; for (const i of values_i) { rects[i] = rects_i[counter]; counter++; }; }; }; if (reflected) { // Dimensions were reflected, reflect back for (const rect of rects) { rect.reflect(); } }; return rects; } /* * Produce a A-Frame building for the rectangle * * The building is positioned right above the y=0 level. * If a model is specified, the corresponding glTF model will be used, * scaled to the "box" that would be used. If not, a box will be used. * * @param {Number} height Height of the box * @param {Color} color Color of the box * @param {string} model Link to the glTF model */ box({ height, elevation = 0, color = 'red', model = null, inner = true, wireframe = false, visible = true, id = "", rawarea = 0, inner_real = false }) { let depth, width; if (inner_real) { [depth, width] = [this.idepth_real, this.iwidth_real]; } else if (inner) { [depth, width] = [this.idepth, this.iwidth]; } else { [depth, width] = [this.depth, this.width]; }; let box = document.createElement('a-entity'); box.setAttribute('geometry', { primitive: 'box', skipCache: true, depth: depth, width: width, height: height, }); box.setAttribute('material', { 'color': color }); box.setAttribute('position', { x: this.x, y: elevation + height / 2, z: this.z }); box.setAttribute('id', id); box.setAttribute('babiaxr-rawarea', rawarea); return box; } }; /* * Auxiliary function: produce a random data tree for codecity */ let rnd_producer = function (levels = 2, number = 3, area = 20, height = 30) { if (levels == 1) { return { "id": "A", "area": Math.random() * area, "height": Math.random() * height }; } else if (levels > 1) { let children = Array.from({ length: number }, function () { return rnd_producer(levels - 1, number, area, height); }); return { id: "BlockAA", children: children }; }; }; if (typeof module !== 'undefined') { module.exports = { Values, Rectangle, Zone }; }; /** * Request a JSON url */ let requestJSONDataFromURL = (data) => { let items = data.data let raw_items // Create a new request object let request = new XMLHttpRequest(); // Initialize a request request.open('get', items, false) // Send it request.onload = function () { if (this.status >= 200 && this.status < 300) { ////// console.log("data OK in request.response", request.response) // Save data if (typeof request.response === 'string' || request.response instanceof String) { raw_items = JSON.parse(request.response) } else { raw_items = request.response } } else { reject({ status: this.status, statusText: xhr.statusText }); } }; request.onerror = function () { reject({ status: this.status, statusText: xhr.statusText }); }; request.send(); // If time evolution if (raw_items.time_evolution) { main_json = raw_items time_evolution = true time_evolution_commit_by_commit = raw_items.time_evolution_commit_by_commit // Get first tree let first_tree = raw_items.data_files.find(o => o.key_tree === "data_0_tree"); raw_items = first_tree["data_0_tree"] initItems = first_tree["data_0"] // Items of the time evolution let navbarData = [] for (let i = 0; i < main_json.data_files.length; i++) { let array_of_tree_to_retrieve = "data_" + i timeEvolutionItems[array_of_tree_to_retrieve] = main_json.data_files[i] navbarData.push({ date: new Date(main_json.data_files[i].date * 1000).toLocaleDateString(), commit: main_json.data_files[i].commit_sha, data: i }) } // Navbar if defined if (data.ui_navbar) { if (data.time_evolution_past_present) { last_uinavbar = parseInt(data.time_evolution_init.split("_")[1]) } else { last_uinavbar = main_json.data_files.length - 1 } ui_navbar = data.ui_navbar document.getElementById(ui_navbar).setAttribute("babiaxr-navigation-bar", "commits", JSON.stringify(navbarData.reverse())) } // Change init tree if needed if (data.time_evolution_init !== "data_0") { let key if (timeEvolutionItems[data.time_evolution_init][data.time_evolution_init + "_allfiles"]) { key = data.time_evolution_init + "_allfiles" } else { key = data.time_evolution_init } let leafEntities = findLeafs(raw_items['children'], {}) timeEvolutionItems[data.time_evolution_init][key].forEach((item) => { if (leafEntities[item.id]) { leafEntities[item.id].height = item.height leafEntities[item.id].area = item.area leafEntities[item.id].max_area = item.max_area } //changeBuildingLayout(item) }) index = parseInt(data.time_evolution_init.split("_")[1]) } } return raw_items } let time_evolution = false let time_evolution_animation = true let time_evolution_color = false let time_evolution_commit_by_commit = false let ui_navbar = undefined let last_uinavbar = undefined let timeEvolutionItems = {} let dateBarEntity let deltaTimeEvolution = 8000 let main_json = {} let initItems = undefined let changedItems = [] let index = 0 let timeEvolTimeout = undefined /** * This function generate a plane with date of files */ let dateBar = (data) => { // get entity codecity let component if (document.getElementById('scene')) { component = document.getElementById('scene') } else { component = document.getElementsByTagName('a-scene') // Others let entities = document.getElementsByTagName('a-entity') /*for (let i in entities) { if (entities[i].attributes && entities[i].attributes['geocodecity']){ component = entities[i] } }*/ } let entity = document.createElement('a-entity') entity.classList.add('babiaxrDateBar') entity.setAttribute('position', { x: -13, y: 10, z: -3 }) entity.setAttribute('rotation', { x: 0, y: 0, z: 0 }) entity.setAttribute('material', { color: 'black' }) entity.setAttribute('height', 0.5) entity.setAttribute('width', 2) entity.setAttribute('scale', { x: 1, y: 1, z: 1 }) let text = "Date: " + new Date(timeEvolutionItems[data.time_evolution_init].date * 1000).toLocaleDateString() if (timeEvolutionItems[data.time_evolution_init].commit_sha) { text += "\n\nCommit: " + timeEvolutionItems[data.time_evolution_init].commit_sha } entity.setAttribute('text-geometry', { value: text, }); dateBarEntity = entity component.appendChild(entity) } /** * This function generate a plane at the top of the building with the desired text */ let generateLegend = (text, heightItem, boxPosition, model, rootCodecityEntity) => { let width = 2; if (text.length > 16) width = text.length / 8; let height = heightItem let entity = document.createElement('a-plane'); entity.setAttribute('babia-lookat', "[camera]") entity.setAttribute('position', { x: boxPosition.x, y: boxPosition.y + height / 2 + 1, z: boxPosition.z }); entity.setAttribute('rotation', { x: 0, y: 0, z: 0 }); entity.setAttribute('height', '1'); entity.setAttribute('width', width); entity.setAttribute('color', 'white'); entity.setAttribute('material', { 'side': 'double' }); entity.setAttribute('text', { 'value': text, 'align': 'center', 'width': 6, 'color': 'black', }); // Check scale let scaleParent = rootCodecityEntity.getAttribute("scale") if (scaleParent && (scaleParent.x !== scaleParent.y || scaleParent.x !== scaleParent.z)) { entity.setAttribute('scale', { x: 1 / scaleParent.x, y: 1 / scaleParent.y, z: 1 / scaleParent.z }); } return entity; } function time_evol() { const maxFiles = Object.keys(timeEvolutionItems).length let i = 0 if (ui_navbar) { // Events from the navbar document.addEventListener('babiaxrToPast', function () { time_evolution_past_present = false }) document.addEventListener('babiaxrToPresent', function () { time_evolution_past_present = true }) document.addEventListener('babiaxrStop', function () { clearInterval(timeEvolTimeout) }) document.addEventListener('babiaxrContinue', function () { timeEvolTimeout = setTimeout(function () { loopLogic(maxFiles, i) if (i < maxFiles - 1) { loop(); } }, deltaTimeEvolution); }) document.addEventListener('babiaxrSkipNext', function () { time_evolution_past_present = false clearInterval(timeEvolTimeout) showLegendUiNavBar(maxFiles - index - 1) last_uinavbar = maxFiles - index - 1 i-- index-- changeCity() }) document.addEventListener('babiaxrSkipPrev', function () { time_evolution_past_present = true clearInterval(timeEvolTimeout) showLegendUiNavBar(maxFiles - index - 3) last_uinavbar = maxFiles - index - 3 i++ index++ changeCity() }) document.addEventListener('babiaxrShow', function (event) { clearInterval(timeEvolTimeout) eventData = event.detail.data i = eventData.data showLegendUiNavBar(maxFiles - i - 1) last_uinavbar = maxFiles - i - 1 index = i - 1 changeCity(true) }) } let loop = () => { timeEvolTimeout = setTimeout(function () { if (ui_navbar) { showLegendUiNavBar(maxFiles - index - 2) } last_uinavbar = maxFiles - index - 2 loopLogic(maxFiles, i) if (i < maxFiles - 1) { loop(); } }, deltaTimeEvolution); } let doIt = () => { loop(); } doIt(); } let loopLogic = (maxFiles, i) => { console.log("Loop number", i) changeCity() if (time_evolution_past_present) { index-- if (index == 0) { console.log("finished") } i-- } else { index++ i++; if (index > maxFiles - 1) { index = 0 } } } let changeCity = (bigStepCommitByCommit) => { let key = "data_" + (index + 1) //key2 only for commit by commit analysis let key2 if (bigStepCommitByCommit) { key2 = "data_" + (index + 1) + "_allfiles" } else { if (time_evolution_past_present) { key2 = "data_reverse_" + (index + 1) } else { key2 = "data_" + (index + 1) } } // Change Date let text = "Date: " + new Date(timeEvolutionItems[key].date * 1000).toLocaleDateString() if (timeEvolutionItems[key].commit_sha) { text += "\n\nCommit: " + timeEvolutionItems[key].commit_sha } dateBarEntity.setAttribute('text-geometry', 'value', text) changedItems = [] // Change color by date if (time_evolution_color) { // Change color currentColorPercentage += 5 if (currentColorPercentage !== 80) { currentColor = getNewBrightnessColor(currentColor, currentColorPercentage) } else { colorEvolutionArrayStartingPoint++ if (colorEvolutionArrayStartingPoint > colorEvolutionArray.length - 1) { colorEvolutionArrayStartingPoint = 0 } currentColor = colorEvolutionArray[colorEvolutionArrayStartingPoint] currentColorPercentage = 20 } } // Check if commit by commit or time snapshots (time snapshots = same key) if (timeEvolutionItems[key][key2]) { timeEvolutionItems[key][key2].forEach((item) => { changeBuildingLayout(item) }) } else { timeEvolutionItems[key][key].forEach((item) => { changeBuildingLayout(item) }) } // Put height 0 those that not exists if (!time_evolution_commit_by_commit) { initItems.forEach((item) => { if (!changedItems.includes(item.id)) { // Put it to opacity 0.3 and black color dissapearBuildingAnimation(item.id) } }) } updateCity() } let currentColorPercentage = 20 let currentColor let colorEvolutionArray = ["#000066", "#006600", "#666600", "#660000"] let colorEvolutionArrayStartingPoint = 0 let changeBuildingLayout = (item) => { if (document.getElementById(item.id) != undefined && item.area != 0.0) { // Add to changed items changedItems.push(item.id) // Get old data in order to do the math let prevPos = document.getElementById(item.id).getAttribute("position") let oldX = document.getElementById(item.id).getAttribute("position").x let oldY = document.getElementById(item.id).getAttribute("position").y let oldZ = document.getElementById(item.id).getAttribute("position").z let prevWidth = document.getElementById(item.id).getAttribute("geometry").width let prevDepth = document.getElementById(item.id).getAttribute("geometry").depth let prevHeight = document.getElementById(item.id).getAttribute("geometry").height let oldRawArea = parseFloat(document.getElementById(item.id).getAttribute("babiaxr-rawarea")) if (prevHeight.toFixed(6) !== item.height.toFixed(6) || oldRawArea.toFixed(6) !== item.area.toFixed(6)) { //Change color if (time_evolution_color) { document.getElementById(item.id).setAttribute('material', { 'color': currentColor }); } // Calculate Aspect Ratio let reverseWidthDepth = false let AR = prevWidth / prevDepth if (AR < 1) { reverseWidthDepth = true AR = prevDepth / prevWidth } // New area that depends on the city let newAreaDep = (item.area * (prevDepth * prevWidth)) / oldRawArea // New size for the building based on the AR and the Area depend let newWidth = Math.sqrt(newAreaDep * AR) let newDepth = Math.sqrt(newAreaDep / AR) if (reverseWidthDepth) { newDepth = Math.sqrt(newAreaDep * AR) newWidth = Math.sqrt(newAreaDep / AR) } // Write the new values document.getElementById(item.id).setAttribute("babiaxr-rawarea", item.area) if (time_evolution_animation) { // Change area with animation let duration = 500 if (newWidth > prevWidth || newDepth > prevDepth) { let incrementWidth = 20 * (newWidth - prevWidth) / duration let incrementDepth = 20 * (newDepth - prevDepth) / duration let sizeWidth = prevWidth let sizeDepth = prevDepth let idIncA = setInterval(function () { animationAreaIncrease() }, 1); function animationAreaIncrease() { if (sizeWidth >= newWidth || sizeDepth >= newDepth) { document.getElementById(item.id).setAttribute("geometry", "width", newWidth) document.getElementById(item.id).setAttribute("geometry", "depth", newDepth) clearInterval(idIncA); } else { sizeWidth += incrementWidth; sizeDepth += incrementDepth document.getElementById(item.id).setAttribute("geometry", "width", sizeWidth) document.getElementById(item.id).setAttribute("geometry", "depth", sizeDepth) } } } else if (newWidth < prevWidth || newDepth < prevDepth) { let incrementWidth = 20 * (prevWidth - newWidth) / duration let incrementDepth = 20 * (prevDepth - newDepth) / duration let sizeWidth = prevWidth let sizeDepth = prevDepth let idDecA = setInterval(function () { animationAreaDecrease() }, 1); function animationAreaDecrease() { if (sizeWidth <= newWidth || sizeDepth <= newDepth) { document.getElementById(item.id).setAttribute("geometry", "width", newWidth) document.getElementById(item.id).setAttribute("geometry", "depth", newDepth) clearInterval(idDecA); } else { sizeWidth -= incrementWidth; sizeDepth -= incrementDepth document.getElementById(item.id).setAttribute("geometry", "width", sizeWidth) document.getElementById(item.id).setAttribute("geometry", "depth", sizeDepth) } } } // Change height with animation if (item.height < 0) { // Has to dissapear dissapearBuildingAnimation(item.id) } else if (item.height > prevHeight) { let increment = 20 * (item.height - prevHeight) / duration let size = prevHeight let idIncH = setInterval(function () { animationHeightIncrease() }, 1); function animationHeightIncrease() { if (size >= item.height) { document.getElementById(item.id).setAttribute("position", { x: oldX, y: (oldY - prevHeight / 2) + (item.height / 2), z: oldZ }) clearInterval(idIncH); } else { size += increment; document.getElementById(item.id).setAttribute("geometry", 'height', size); document.getElementById(item.id).setAttribute("position", { x: oldX, y: (oldY - prevHeight / 2) + (size / 2), z: oldZ }) } } } else if (item.height < prevHeight) { let increment = 20 * (prevHeight - item.height) / duration let size = prevHeight let idDecH = setInterval(function () { animationHeightDecrease() }, 1); function animationHeightDecrease() { if (size <= item.height) { document.getElementById(item.id).setAttribute("position", { x: oldX, y: (oldY - prevHeight / 2) + (item.height / 2), z: oldZ }) clearInterval(idDecH); } else { size -= increment; document.getElementById(item.id).setAttribute("geometry", 'height', size); document.getElementById(item.id).setAttribute("position", { x: oldX, y: (oldY - prevHeight / 2) + (size / 2), z: oldZ }) } } } } else { document.getElementById(item.id).setAttribute("geometry", "width", newWidth) document.getElementById(item.id).setAttribute("geometry", "depth", newDepth) document.getElementById(item.id).setAttribute("geometry", "height", item.height) document.getElementById(item.id).setAttribute("position", { x: prevPos.x, y: (prevPos.y - prevHeight / 2) + (item.height / 2), z: prevPos.z }) } } } } let dissapearBuildingAnimation = (itemId) => { // Put it to opacity 0.3 and black color let oldColor = document.getElementById(itemId).getAttribute('material').color document.getElementById(itemId).setAttribute('material', { 'color': 'black' }); document.getElementById(itemId).setAttribute('material', { 'opacity':