/** * @augments EicComponent * @category EICUI/AdvancedComponents */ class NodeMap extends EicComponent { entities = []; relations = []; constructor(el, options) { let defaultOptions = { resizable: true, allowDrag: true, orientation: 'linear', entity: { width: 200, height: 50, gap: 40 }, map: { position: { x: 0, y: 0 }, size: { width: 0, height: 0 }, dragging: false, }, scale: { current: 1.0, factor: 0.085, min: 0.5, max: 4 }, guides: true } super(el, {...defaultOptions, ...(options || {})}); if(this._constructed) return this; this.el.setAttribute('eicnodemap',''); if(this.options.resizable) this.el.setAttribute('resizable',''); else this.el.removeAttribute('resizable'); this.el.innerHTML = ''; this.map = ui.create(``) this.el.append(this.map); this.viewbox = { x: 0, y: 0, w: this.el.clientWidth, h: this.el.clientHeight }; if(this.options.allowDrag) { this.map.addEventListener('wheel', this.onMapScroll.bind(this)); this.map.addEventListener('mousedown', this.onMapDragStart.bind(this)); this.map.addEventListener('mousemove', this.onMapDragMove.bind(this)); this.mouseUpHandler = this.onMapDragStop.bind(this); this.entityUpHandler = this.onEntityDragStop.bind(this); } } set loading(state) { if(state == true) this.el.setAttribute('loading',''); else this.el.removeAttribute('loading'); } get loading() { return this.el.hasAttribute('loading') } set data(value) { this.clear(); if(value.entities) for(let entity of value.entities) this.addEntity(entity); if(value.relations) for(let relation of value.relations) this.addRelation(relation); this.reorder(); } clear() { this.map.innerHTML = ''; this.entities = []; } reorder() { switch(this.options.orientation) { case 'linear': this.reorderLinear(); break; case 'radial': default: this.reorderRadial(); } } reorderLinear() { // 1. prepare geometries and identify starters and enders for(let entity of this.entities) { entity.geometry = { starter: false, ender: false, depth: null, siblings: null, index: null }; if(!entity.relations.find(r => r.target.id == entity.id)) { entity.geometry.starter = true; entity.geometry.depth = 0 } entity.geometry.ender = !entity.relations.find(r => r.source.id == entity.id); } // 2. dig paths using starters let starters = this.entities.filter(o => o.geometry.starter == true) for(let entity of starters) { this.digLinearPath(entity) } // 3. resolve siblings let maxSiblings = 0; for(let entity of this.entities) { entity.geometry.siblings = this.entities.filter(o => o.geometry.depth == entity.geometry.depth).length - 1 maxSiblings = Math.max(maxSiblings, entity.geometry.siblings); let i = 0; for(let relation of entity.relations) { if(relation.target.id != entity.id && !relation.target.geometry.index) { relation.target.geometry.index = i; i++; } } } // 4. reposition let maxHeight = 3 * maxSiblings * (this.options.entity.height + (this.options.entity.gap)) for(let entity of this.entities) { entity.x = this.options.entity.gap + (this.options.entity.width + this.options.entity.gap) * entity.geometry.depth; entity.y = maxHeight * (entity.geometry.index + 1) / (entity.geometry.siblings + 2); for(let relation of entity.relations) { relation.redraw(); } } this.center(); } digLinearPath(entity) { for(let relation of entity.relations) { if(relation.target.id != entity.id) { if(!relation.target.geometry.depth) { relation.target.geometry.depth = entity.geometry.depth + 1; this.digLinearPath(relation.target) } } } } center() { let box = this.map.getBBox(); this.options.scale.current = Math.max( (box.height + this.options.entity.gap) / (this.viewbox.h ), (box.width + this.options.entity.gap) / (this.viewbox.w ) ) this.viewbox.y = box.y - (this.options.entity.gap / 2); this.viewbox.x = 20; this.setViewbox(); } reorderRadial() { let a = (Math.PI * 2) / this.entities.length; let r = (this.options.entity.width) * Math.log(this.entities.length); let i = 0; let center = { x: this.viewbox.w / 2, y: this.viewbox.h / 2 } for(let entity of this.entities) { let pos = { x: center.x + (Math.cos(a * i - Math.PI * .5) * r) - (this.options.entity.width / 2), y: center.y + (Math.sin(a * i - Math.PI * .5) * r) - (this.options.entity.height / 2) } entity.x = pos.x; entity.y = pos.y; for(let relation of entity.relations) relation.redraw(); i++; } } getEntityById(id) { return this.entities.find(entity => entity.id == id); } addEntity(entity) { let index = this.entities.length; let options = { ...entity, ...this.options.entity }; let item = new NodeMapNode(null, options); item.x = this.options.entity.gap + (this.options.entity.width + this.options.entity.gap) * index; item.y = 30; item.el.addEventListener('selected', this.onEntitySelect.bind(this)); this.entities.push(item); this.map.append(item.el); } removeEntity(entity) { let parents = entity.relations.filter(r => r.target.id == entity.id); let childs = entity.relations.filter(r => r.source.id == entity.id); for(let relation of parents) { let parent = relation.source; parent.relations = parent.relations.filter(r => r.target.id != entity.id); for(let relation of childs) { let child = relation.target; child.relations = child.relations.filter(r => r.source.id != entity.id); this.addRelation({source: parent.id, target: child.id}); } } entity.relations.forEach(r => r.el.remove()) entity.el.remove(); this.entities = this.entities.filter(e => e.id != entity.id) this.reorder(); } addRelation(relation) { let source = this.entities.find(entity => entity.id == relation.source); let target = this.entities.find(entity => entity.id == relation.target); let item = new NodeMapRelation(source, target); source.relations.push(item); target.relations.push(item); this.map.prepend(item.el); } onEntityDragStart(event) { event.stopPropagation(); event.preventDefault(); this.current = event.currentTarget this.mouseStartPosition = { x: event.pageX, y: event.pageY } this.entityStartPosition = { x: Number.parseFloat(this.current.getAttribute("x")), y: Number.parseFloat(this.current.getAttribute("y")) } window.addEventListener("mouseup", this.entityUpHandler); this.entityDrag = true; } onEntityMove(event) { event.stopPropagation(); event.preventDefault(); this.mousePosition = { x: event.offsetX, y: event.offsetY }; if(this.entityDrag) { this.current.setAttribute("x", this.entityStartPosition.x - (this.mouseStartPosition.x - event.pageX) * this.options.scale.current) this.current.setAttribute("y", this.entityStartPosition.y - (this.mouseStartPosition.y - event.pageY) * this.options.scale.current) let entity = this.entities.find(item => item.data.id == (this.options.entity.current.getAttribute("data-id"))) let pos = { x: Number.parseFloat(this.options.entity.current.getAttribute("x")), y: Number.parseFloat(this.options.entity.current.getAttribute("y")) } /* for(let line of entity.lines) { if(line.getAttribute("data-source") == entity.data.id) { line.setAttribute("x1", pos.x + (this.options.entity.width / 2)); line.setAttribute("y1", pos.y + (this.options.entity.height / 2)); } else { line.setAttribute("x2", pos.x + (this.options.entity.width / 2)); line.setAttribute("y2", pos.y + (this.options.entity.height / 2)); } } */ } } onEntityDragStop(event) { event.stopPropagation(); event.preventDefault(); window.removeEventListener("mouseup", this.entityUpHandler); this.options.entity.dragging = false; } onEntitySelect(event) { let entity = event.detail; this.entities.forEach(item => { item.el.classList.remove('selected'); item.el.classList.remove('related'); //item.lines.forEach(line => line.classList.remove('related')) }); entity.el.classList.add('selected'); /* entity.lines.forEach(line => { line.classList.add('related'); let other = this.entities.find(item => (item.data.id == line.getAttribute('data-target') && line.getAttribute('data-source') == selected.data.id) || (item.data.id == line.getAttribute('data-source') && line.getAttribute('data-target') == selected.data.id)); other.el.classList.add('related'); }); */ this.click(entity); } onMapScroll(event) { event.stopPropagation(); event.preventDefault(); let scale = 1. + (event.deltaY < 0 ? - this.options.scale.factor : this.options.scale.factor); if((this.options.scale.current * scale < this.options.scale.max) && (this.options.scale.current * scale > this.options.scale.min)) { let pos = { x: (this.mousePosition.x * this.options.scale.current) + this.viewbox.x, y: (this.mousePosition.y * this.options.scale.current) + this.viewbox.y } this.viewbox.x = (this.viewbox.x - pos.x) * scale + pos.x; this.viewbox.y = (this.viewbox.y - pos.y) * scale + pos.y; this.options.scale.current *= scale; this.setViewbox(); } } onMapDragStart(event) { this.mouseStartPosition = { x: event.pageX, y: event.pageY } this.viewboxStartPosition = { x: this.viewbox.x, y: this.viewbox.y } this.options.map.dragging = true; window.addEventListener("mouseup", this.mouseUpHandler); } onMapDragMove(event) { this.mousePosition = { x: event.offsetX, y: event.offsetY }; if(this.options.map.dragging) { this.viewbox.x = this.viewboxStartPosition.x + (this.mouseStartPosition.x - event.pageX) * this.options.scale.current; this.viewbox.y = this.viewboxStartPosition.y + (this.mouseStartPosition.y - event.pageY) * this.options.scale.current; this.setViewbox(); } } onMapDragStop(event) { window.removeEventListener("mouseup", this.mouseUpHandler); this.options.map.dragging = false; } onMapResize(event) { this.viewbox.w = this.find('.results').clientWidth; //this.viewbox.w = this.map.viewBox.baseVal.width; this.viewbox.h = this.find('.results').clientHeight; //this.viewbox.h = this.map.viewBox.baseVal.height; this.setViewbox(); } setViewbox() { this.map.setAttribute("viewBox", this.viewbox.x + " " + this.viewbox.y + " " + (this.viewbox.w * this.options.scale.current) + " " + (this.viewbox.h * this.options.scale.current)); } click() {} } /** * @augments EicComponent * @category EICUI/AdvancedComponents */ class NodeMapNode extends EicComponent { /** * */ relations = []; /** * */ id = ""; /** * */ options = { badge: { size: 'large', severity: 'danger' } } constructor(el, options) { super(el, options); this.setOptions(options); if(!el) { this.el = ui.create( ` ${options.title} ${options.subtitle} ` ); if(options.badge) this.badge = options.badge; } if(options.data) { for(let property in options.data) { this.el.setAttribute('data-' + property, options.data[property]); if(property == "id") this.id = options.data[property]; } } this.el.addEventListener('click', this.onClick.bind(this)); } /** * */ set x(value) { this.el.setAttribute('x', value); } get x() { return parseInt(this.el.getAttribute('x')); } /** * */ set y(value) { this.el.setAttribute('y', value); } get y() { return parseInt(this.el.getAttribute('y')); } /** * */ set disabled(state) { state ? this.el.setAttribute('disabled',''): this.el.removeAttribute('disabled'); } get disabled() { return this.el.hasAttribute('disabled'); } /** * */ get width() { return parseInt(this.el.querySelector('rect').getAttribute('width')); } get height() { return parseInt(this.el.querySelector('rect').getAttribute('height')); } /** * */ set title(str) { this.el.querySelector('.title').innerHTML = str; } set subtitle(str) { this.el.querySelector('.subtitle').innerHTML = str; } /** * */ set badge(value) { if(!this._badge && value != 0) { let r = 14; switch(this.options.badge.size) { case 'xxsmall': r = 8; break; case 'xsmall': r = 10; break; case 'small': r = 12; break; case 'large': r = 16; break; case 'xlarge': r = 18; break; case 'xxlarge': r = 20; break; case 'xxxlarge': r = 24; break; } this._badge = ui.create( ` ` ); this.el.append(this._badge) } this._badge.querySelector('text').textContent = value; } /** * */ set data(value) { } onClick(event) { event.stopPropagation(); event.preventDefault(); this.el.dispatchEvent(new CustomEvent('selected', {detail: this})) } } /** * @augments EicComponent * @category EICUI/AdvancedComponents */ class NodeMapRelation extends EicComponent { source = null; target = null; constructor(source, target, options) { super(null, options); this.setOptions(options); this.source = source; this.target = target; //this.el = ui.create( `` ); this.el = document.createElementNS('http://www.w3.org/2000/svg','path'); this.el.setAttribute('class', 'relation'); this.el.dataset.source = source.id; this.el.dataset.target = target.id; this.redraw(); } redraw() { let o = { x: this.source.x + this.source.width / 2, y: this.source.y + this.source.height / 2 } let d = { x: o.x + Math.abs((this.target.y + this.target.height / 2) - o.y), y: this.target.y + this.target.height / 2 } let p1 = { x: o.x, y: d.y } let f = { x: this.target.x + this.target.width / 2, y: this.target.y + this.target.height / 2 } let path = `M ${o.x} ${o.y} C ${p1.x} ${p1.y} ${p1.x} ${p1.y} ${d.x} ${d.y} L ${f.x} ${f.y}` this.el.setAttribute('d', path) } }