/** * n00014p5 */ class DataExplorerView extends EICDomContent { options = { entity: { width: 140, height: 70, gap: 20, dragging: false, current: null }, 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 } filtersList = []; DOMContentLoaded(options) { for(let model in options.models) this[model] = options.models[model]; this.components = ui.eicfy(this.el); this.q = this.components.find(c => c.el.name == 'q'); this.q.onQuery = this.onQuery.bind(this); this.attributes = this.components.find(c => c.el.name == 'attributes'); this.attributes.activeEditor = DataExplorerFilter; this.attributes.onEditorOpened = this.fillFilters.bind(this); this.attributes.el.addEventListener( 'change' , this.onQuery.bind(this)); this.results = this.find('.results'); this.viewboxPosition = {x: 0, y: 0}; this.map = this.find('.results svg'); 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); // observe map viewport resizing new ResizeObserver(this.onMapResize.bind(this)).observe(this.find('.results ')) this.ml.filters().then(payload => { this.filtersList = payload; }) } clearMap() { this.map.innerHTML = ''; this.entities = []; } clearDetail() { this.find('.detail').innerHTML = ''; } fillFilters(editor) { let select = editor.el.querySelector('select'); for(let field of this.filtersList) { let option = ui.create(``) select.append(option) } } fillMap(data) { this.clearMap(); this.clearDetail(); this.viewboxSize = { x: this.find('.results').clientWidth, y: this.find('.results').clientHeight }; this.viewboxStartPosition = {x: 0, y: 0}; this.options.map.dragging = false; for(let entity of data.entities) this.addEntity(entity); for(let relation of data.relations) this.addRelation(relation); this.reorderMap(); } addEntity(entity) { const width = this.options.entity.width; const height = this.options.entity.height; const gap = this.options.entity.gap; let template = this.ml.getType(entity._entity_type); let index = this.entities.length; let content = ui.create( ` ${template.icon} ${entity._entity_label} ${template.label} ` ); content.addEventListener('click', this.onEntitySelect.bind(this)); content.addEventListener('mousedown', this.onEntityDragStart.bind(this)); content.addEventListener('mousemove', this.onEntityMove.bind(this)); this.entities.push({ el: content, data: entity, lines: [] }); this.map.append(content); } addRelation(relation) { let o1 = this.entities.find(item => item.data.id == relation.source); let o2 = this.entities.find(item => item.data.id == relation.target); if(!o1 || !o2) return; let line = ui.create( `` ).querySelector('line'); this.map.prepend(line); o1.lines.push(line) o2.lines.push(line) } reorderMap() { 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.viewboxSize.x / 2, y: this.viewboxSize.y / 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.el.setAttribute('x', pos.x) entity.el.setAttribute('y', pos.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)); } } i++; } if(this.entities.length > 0 && this.options.guides) { let guide = ui.create(``) this.map.prepend(guide.querySelector('circle')); } } setEntityDetail(entity) { let template = this.ml.getType(entity._entity_type); let el = ui.create(`

${entity._entity_label}

${template.label}

`); let footer = el.querySelector('footer'); let props = new DataGrid(el.querySelector('[eicdatagrid]'), { headers: [ {label: 'property', filter:'text'}, {label: 'value', filter:'text'}, ], height: '270px' }); this.setEntityProp(props, entity, 'id', null, entity._entity_type) for(let prop in entity.data) { //if(['_entity_type', '_entity_label'].indexOf(prop) == -1) { this.setEntityProp(props, entity.data, prop, null, entity._entity_type) //} } let lines = this.entities.find(item => item.data.id == entity.id).lines; if(lines.length > 0) { footer.append(ui.create(`
Relates locally to:`)) let ul = ui.create(``) footer.append(ul); for(let line of lines) { let other = this.entities.find(item => (item.data.id == line.getAttribute('data-target') && line.getAttribute('data-source') == entity.id) || (item.data.id == line.getAttribute('data-source') && line.getAttribute('data-target') == entity.id)) let template = this.ml.getType(other.data._entity_type); let li = ui.create(`
  • ${other.data._entity_label} (${template.label})
  • `); li.addEventListener('click', this.onEntitySelect.bind(this)) ul.append(li) } } this.clearDetail(); this.find('.detail').append(el); } setEntityProp(table, parent, prop, prefix, type) { if(typeof parent[prop] !== 'object') { let allowSearch = ['id', 'email', 'euLogin', 'pic'].indexOf(prop) != -1; let value = allowSearch ? `${parent[prop]}` : parent[prop]; let row = table.addRow('', [ (prefix ? prefix + '.': '') + prop, value ]); if(allowSearch) { row.querySelector('.cell:nth-child(3) a').addEventListener('click', this.onQueryEntity.bind(this)) } } else { for(let subprop in parent[prop]) this.setEntityProp(table,parent[prop], subprop, (prefix ? prefix + '.': '') + prop, type) } } onQuery(event) { if(this.q.value.trim() == '' && this.attributes.value.length == 0) return; let query = { freeText: this.q.value } this.attributes.value.map(val => { let parts = val.split('='); if(parts.length > 1) query[parts[0]] = parts[1]; }) this.ml.search(query).then( this.fillMap.bind(this)); } onQueryEntity(event) { event.stopPropagation(); event.preventDefault(); this.q.value = event.currentTarget.dataset.query; this.onQuery(event); } onEntityDragStart(event) { event.stopPropagation(); event.preventDefault(); this.options.entity.current = event.currentTarget this.mouseStartPosition = { x: event.pageX, y: event.pageY } this.entityStartPosition = { x: Number.parseFloat(this.options.entity.current.getAttribute("x")), y: Number.parseFloat(this.options.entity.current.getAttribute("y")) } window.addEventListener("mouseup", this.entityUpHandler); this.options.entity.dragging = true; } onEntityMove(event) { event.stopPropagation(); event.preventDefault(); this.mousePosition = { x: event.offsetX, y: event.offsetY }; if(this.options.entity.dragging) { this.options.entity.current.setAttribute("x", this.entityStartPosition.x - (this.mouseStartPosition.x - event.pageX) * this.options.scale.current) this.options.entity.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) { event.stopPropagation(); event.preventDefault(); let selected = this.entities.find(item => item.el.dataset.id == event.currentTarget.dataset.id); this.entities.forEach(item => { item.el.classList.remove('selected'); item.el.classList.remove('related'); item.lines.forEach(line => line.classList.remove('related')) }); selected.el.classList.add('selected'); selected.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.setEntityDetail(selected.data); } 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.viewboxPosition.x, y: (this.mousePosition.y * this.options.scale.current) + this.viewboxPosition.y } this.viewboxPosition.x = (this.viewboxPosition.x - pos.x) * scale + pos.x; this.viewboxPosition.y = (this.viewboxPosition.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.viewboxPosition.x, y: this.viewboxPosition.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.viewboxPosition.x = this.viewboxStartPosition.x + (this.mouseStartPosition.x - event.pageX) * this.options.scale.current; this.viewboxPosition.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.viewboxSize = { x: this.find('.results').clientWidth, y: this.find('.results').clientHeight }; this.setViewbox(); } setViewbox() { this.map.setAttribute("viewBox", this.viewboxPosition.x + " " + this.viewboxPosition.y + " " + (this.viewboxSize.x * this.options.scale.current) + " " + (this.viewboxSize.y * this.options.scale.current)); } } class DataExplorerFilter extends SelectEditor { _template = `
    `; open() { super.open(); this.input = this.components.find(c => c.el.nodeName == 'INPUT') this.select = this.components.find(c => c.el.nodeName == 'SELECT') this.select.clear(); } initEvents() { this.el.querySelector('input').addEventListener('keypress', this.onKeyPressed.bind(this)); this.el.querySelector('select').addEventListener('change', this.onFieldChange.bind(this) ); } onFieldChange(event) { this.input.disabled = this.select.value.length == 0; } get valid() { return super.valid && this.el.querySelector('select').value != ''; } get value() { return {value: this.el.querySelector('select').value + "=" + this.el.querySelector('input').value, label: this.el.querySelector('select').value + ' is "' + this.el.querySelector('input').value + '"' } } } app.registerClass('DataExplorerView', DataExplorerView);