var DBG = DBG || false; var DBG1 = true; var DBG_INIT_SELECTED = []; // [ "BI_audit_ENERGA_RUM_KONTRAHENCI.18661", "BI_audit_KRS.389967" ]; if ('undefined' === typeof HTML_ID_REF_GRAPH) throw "Missing HTML_ID_REF_GRAPH"; if (!RAPORT_ID) throw "Missing RAPORT_ID"; if (!API_URL) throw "Missing API_URL"; if (!global.p5VendorJs) throw "Missing p5VendorJs"; if (!global.vis) throw "Missing vis"; if (!global.p5VendorJs.Typeahead) throw "Missing Typeahead"; var createReactClass = global.p5VendorJs.createReactClass; var h = global.p5VendorJs.React.createElement; var React = global.p5VendorJs.React; var ReactDOM = global.p5VendorJs.ReactDOM; var Redux = global.p5VendorJs.Redux; var ReduxThunk = global.p5VendorJs.ReduxThunk; var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js var Typeahead = global.p5VendorJs.Typeahead; var mapStatsNodeToGraphNode = function (node) { return { id: node['@object'] + '.' + node['@primaryKey'], label: Utils.nodeLabelShort(node['@label']), color: Utils.nodeColorByObject(node['@object']), rawLabel: node['@label'], value: 1, level: 0, } }; var mapStatsEdgeToGraphEdge = function (edge) { return { id: Utils.makeUniqueEdgeId(edge['source'], edge['target']), from: edge['source'], to: edge['target'], pathId: edge['pathId'], listPathId: [ edge['pathId'] ], } }; //// DBG: Utils: // var from, to; // from = 'aa.11'; to = 'bb.22'; console.log('DBG: Utils.makeUniqueEdgeId('+from+', '+to+')', Utils.makeUniqueEdgeId(from, to), { cache: Utils.getCacheAllEdgeId().join(',') }) // from = 'bb.22'; to = 'aa.11'; console.log('DBG: Utils.makeUniqueEdgeId('+from+', '+to+')', Utils.makeUniqueEdgeId(from, to), { cache: Utils.getCacheAllEdgeId().join(',') }) // from = 'cc.22'; to = 'cc.11'; console.log('DBG: Utils.makeUniqueEdgeId('+from+', '+to+')', Utils.makeUniqueEdgeId(from, to), { cache: Utils.getCacheAllEdgeId().join(',') }) var Utils = (function __makeUtils() { var Utils = {}; var _cacheListFeatureNames = []; Utils.fidSplit = function (fid) { var dotPos = fid.indexOf('.'); return [ fid.substr(0, dotPos), fid.substr(dotPos + 1) ]; } Utils.makeUniqueEdgeId = function (from, to) { var listFidArr = [ from, to ].sort().map(Utils.fidSplit); var listIdx = listFidArr.map(function (fidArr) { var cacheIdx = _cacheListFeatureNames.indexOf(fidArr[0]); if (-1 === cacheIdx) { cacheIdx = _cacheListFeatureNames.length; _cacheListFeatureNames.push(fidArr[0]); } return cacheIdx; }) return listIdx.map(function (cacheIdx, fidIdx) { return cacheIdx + '.' + listFidArr[fidIdx][1]; }).join('-') } Utils.getCacheAllEdgeId = function () { return [].concat(_cacheListFeatureNames); } Utils.nodeLabelShort = function (label) { return label.replace('SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ', 'Sp. z o.o.') } Utils.bsColors = { 'base': "#777", 'blue': "#337ab7", 'green': "#5cb85c", 'lightblue': "#5bc0de", 'orange': "#f0ad4e", 'red': "#d9534f", }; Utils.bsLightColors = { 'base': "#f5f5f5", 'blue': "#d9edf7", 'green': "#dff0d8", 'lightblue': "#d9edf7", 'orange': "#fcf8e3", 'red': "#f2dede", }; Utils.colors = { person: Utils.bsLightColors['orange'], company: Utils.bsLightColors['red'], location: Utils.bsLightColors['base'], }; Utils.nodeColorByObject = function (objectName) { switch (objectName) { case 'BI_audit_KRS_person': return Utils.colors['person']; case 'BI_audit_MSIG_person': return Utils.colors['person']; case 'BI_audit_MSIG': return Utils.colors['company']; case 'BI_audit_KRS': return Utils.colors['company']; case 'BI_audit_ENERGA_RUM_KONTRAHENCI': return Utils.colors['company']; case 'TERYT_adresy': return Utils.colors['location']; case 'BI_audit_MSIG_address': return Utils.colors['location']; case 'BI_audit_KRS_company': return Utils.colors['company']; case 'BI_audit_MSIG_company': return Utils.colors['company']; case 'BI_audit_CEIDG': return Utils.colors['company']; default: return '#666'; } } return Utils; })(); { // TODO: RaportOutputPanel ? var RaportOutputPanel = {}; RaportOutputPanel.initialState = function () { var nodes = (STATS && STATS.nodes) ? STATS.nodes : []; var edges = (STATS && STATS.edges) ? STATS.edges : []; var paths = (STATS && STATS.path_list) ? STATS.path_list : []; DBG && console.log('DBG: STATS', STATS); return { nodes: nodes.map(mapStatsNodeToGraphNode), edges: edges.map(mapStatsEdgeToGraphEdge), paths: paths, isLoading: false, sentRequestId: 0, receivedRequestId: 0, } }; RaportOutputPanel.store = function (state, action) { var prevState = state || RaportOutputPanel.initialState(); DBG && console.log('DBG: store', { prevState, action, actionType: action.type }); switch (action.type) { case 'SET_SENT_REQUEST_ID': return Object.assign(prevState, { sentRequestId: action.sentRequestId, isLoading: true }); case 'SET_RESPONSE': return Object.assign(prevState, { receivedRequestId: action.requestId, nodes: action.body.nodes.map(mapStatsNodeToGraphNode), edges: action.body.edges, isLoading: (prevState.sentRequestId > action.requestId) ? true : false, }); default: return prevState; } }; RaportOutputPanel.createActions = function () { var setSentRequestId = function (sentRequestId) { return { type: 'SET_SENT_REQUEST_ID', sentRequestId: sentRequestId }; } var setResponse = function (response) { return Object.assign(response, { type: 'SET_RESPONSE' }); } var fetchData = function (raportId, params) { return function (dispatch, getState) { var state = getState(); var this__requestId = state.sentRequestId + 1; dispatch(setSentRequestId(this__requestId)); var reqPromise = window.fetch(API_URL, { method: 'GET', credentials: 'same-origin', }).then(function (response) { return response.json(); }); return reqPromise.then(function (response) { var items = response; var state = getState(); if (state.receivedRequestId > this__requestId) { DBG && console.log('DBG: skipped response', { 'state.receivedRequestId': state.receivedRequestId, this__requestId }); return; } dispatch( setResponse({ requestId: this__requestId, msg: "Pobrano dane", body: response.body }) ); }).catch(function (e) { p5UI__notifyAjaxCallback({type: 'error', msg: 'Wystąpił błąd #GS1: ' + e}); // dispatch( setErrorMsg(e) ); // TODO: show error with msg and refresh button }); } } return { fetchData: fetchData, } }; } { // TODO: NetworkGraph - mv to another file var NetworkGraph = {}; NetworkGraph.initialState = function () { var nodes = (STATS && STATS.nodes) ? STATS.nodes : []; var edges = (STATS && STATS.edges) ? STATS.edges : []; var paths = (STATS && STATS.path_list) ? STATS.path_list : []; DBG && console.log('DBG: STATS', STATS); var allNodesSet = new vis.DataSet(); allNodesSet.update( nodes.map(mapStatsNodeToGraphNode) ); var allEdgesSet = new vis.DataSet(); allEdgesSet.update( edges.map(mapStatsEdgeToGraphEdge) ); var visibleNodesSet = new vis.DataSet(); var selectedNodeIds = []; return { allNodesSet: allNodesSet, allEdgesSet: allEdgesSet, visibleNodesSet: visibleNodesSet, // nodes: nodes.map(mapStatsNodeToGraphNode), // TODO: RMME // edges: edges.map(mapStatsEdgeToGraphEdge), paths: paths, isLoading: false, sentRequestId: 0, receivedRequestId: 0, } }; NetworkGraph.store = function (state, action) { var prevState = state || NetworkGraph.initialState(); DBG && console.log('DBG: store', { prevState, action, actionType: action.type }); switch (action.type) { case 'SET_SENT_REQUEST_ID': return Object.assign(prevState, { sentRequestId: action.sentRequestId, isLoading: true }); case 'SET_RESPONSE': return Object.assign(prevState, { receivedRequestId: action.requestId, nodes: action.body.nodes.map(mapStatsNodeToGraphNode), edges: action.body.edges, isLoading: (prevState.sentRequestId > action.requestId) ? true : false, }); case 'SET_SELECTED': { // TODO: mv from updateSelected var selected = action.selected || []; var idsSelected = selected.map( function (node) { return node.id } ) DBG && console.log('DBG:NetworkGraph.SET_SELECTED ', { idsSelected, selected }) var foundPaths = prevState.paths.filter(function (path) { for (var i = 0, totalFids = path.fids.length, fid = null; i < totalFids; i++) { fid = path.fids[i]; DBG && console.log('DBG:foundPaths ', {fid: ''+fid, path}); if ( -1 !== idsSelected.indexOf(fid) ) return true; } return false; }) var addFidsToGraph = Array.from(new Set(foundPaths.reduce(function (ret, path) { return ret.concat(path.fids.filter(function (fid) { return ( -1 === idsSelected.indexOf(fid) ); })) }, []))); function markColorSelected(node) { return Object.assign(node, { color: '#95c1fe' }); } prevState.visibleNodesSet.clear(); prevState.visibleNodesSet.add(selected.map(markColorSelected)); addFidsToGraph.forEach(function (fid) { var node = prevState.allNodesSet.get(fid) prevState.visibleNodesSet.update(node); }) DBG && console.log('DBG:NetworkGraph.SET_SELECTED new state ', { state: Object.assign(prevState, { selected: selected }) }) return Object.assign(prevState, { selected: selected }); } case 'TEST_ADD_DATA': { if (action.nodes) { action.nodes.forEach(function (node) { prevState.allNodesSet.update(node); prevState.visibleNodesSet.update(node); }) } if (action.edges) { action.edges.forEach(function (edge) { prevState.allEdgesSet.update(edge); }) } DBG && console.log('DBG:NetworkGraph.TEST_ADD_DATA new state ', { state: Object.assign(prevState, {}) }) return Object.assign(prevState, {}) } case 'RELOAD_NODES': { var nodes = prevState.visibleNodesSet.map(function(e) { return e; }); // DataSet to Array prevState.visibleNodesSet.clear(); var animAddNodes = (function (this__nodes, nodes, wait) { return function (callback) { DBG && console.log('DBG:animAddNodes', { nodes }); if (nodes.length > 0) { var node = nodes.pop(); this__nodes.add(node); setTimeout(function () { animAddNodes(callback) }, wait); } } })(prevState.visibleNodesSet, nodes, 200); animAddNodes(animAddNodes); } default: return prevState; } }; NetworkGraph.createActions = function () { var setSentRequestId = function (sentRequestId) { return { type: 'SET_SENT_REQUEST_ID', sentRequestId: sentRequestId }; } var setResponse = function (response) { return Object.assign(response, { type: 'SET_RESPONSE' }); } var fetchData = function (raportId, params) { return function (dispatch, getState) { var state = getState(); var this__requestId = state.sentRequestId + 1; dispatch(setSentRequestId(this__requestId)); var reqPromise = window.fetch(API_URL, { method: 'GET', credentials: 'same-origin', }).then(function (response) { return response.json(); }); return reqPromise.then(function (response) { var items = response; var state = getState(); if (state.receivedRequestId > this__requestId) { DBG && console.log('DBG: skipped response', { 'state.receivedRequestId': state.receivedRequestId, this__requestId }); return; } dispatch( setResponse({ requestId: this__requestId, msg: "Pobrano dane", body: response.body }) ); }).catch(function (e) { p5UI__notifyAjaxCallback({type: 'error', msg: 'Wystąpił błąd #GS1: ' + e}); // dispatch( setErrorMsg(e) ); // TODO: show error with msg and refresh button }); } } var setSelected = function (selected) { return { type: 'SET_SELECTED', selected: selected }; } return { fetchData: fetchData, setSelected: setSelected, } }; } var defaultNetworkGraphOptions = { nodes: { shape: 'dot', scaling: { min: 8, max: 30, label: { min: 8, max: 16, drawThreshold: 6, maxVisible: 20 } }, font: { color: "#666", size: 10, face: 'Helvetica Neue, Helvetica, Arial' } // font: "8px Helvetica Neue, Helvetica, Arial" }, interaction: { hover: true, hoverConnectedEdges: false, selectConnectedEdges: true, }, physics: { barnesHut: { avoidOverlap: 0.2 } }, }; var TMP_COUNTER = 1; var p5UI__NetworkGraph = createReactClass({ _network: null, _visOutputRef: React.createRef(), _allNodes: null, // new vis.DataSet(), _allEdges: null, // new vis.DataSet(), _nodes: new vis.DataSet(), _edges: new vis.DataSet(), getStateFromStore: function () { var state = this.props.store.getState(); return { visibleNodesSet: state.visibleNodesSet, allEdgesSet: state.allEdgesSet, isLoading: state.isLoading, receivedRequestId: state.receivedRequestId, // to force render after udpate nodes, edges } }, getInitialState: function () { return Object.assign({ initialized: false, isLoading: false, receivedRequestId: null, }, this.getStateFromStore()); }, setOutputRef: function (elem) { this._visOutputRef = elem; }, componentDidMount: function () { // var data = { nodes: this._nodes, edges: this._edges }; var data = { nodes: this.state.visibleNodesSet, edges: this.state.allEdgesSet }; DBG && console.log('DBG:componentDidMount data', { data }); this._network = new vis.Network(this._visOutputRef, data, defaultNetworkGraphOptions); this._network.on('stabilized', this.handleNetworkStabilized); if (this.props.onZoom) this._network.on('zoom', this.props.onZoom) this.setState({ initialized: true }); this._unsubscribe = this.props.store.subscribe(this._storeUpdated) }, handleNetworkStabilized: function (event) { // _network.getBoundingBox( nodeId ) // @return { top, left, bottom, right } var _nodes = this.state.visibleNodesSet; var _network = this._network; if (_nodes) { this.fitToContainer(); // > 1. using getBoundingBox // var nodeCoords = _nodes.getIds().map( function (nodeId) { // var coords = _network.getBoundingBox( nodeId ); // // // // // // 1.0 1.0 // // 1.0 1.0 // // // // // var envelope = { // upperCorner: _network.canvasToDOM({ x: coords.left, y: coords.top }), // lowerCorner: _network.canvasToDOM({ x: coords.right, y: coords.bottom }), // }; // DBG_drawBox(envelope, _nodes.get(nodeId).label); // return Object.assign( { nodeId: nodeId }, _nodes.get(nodeId), envelope ) // } ); // > 2. using getPositions // var nodeCoords = _network.getPositions() // > 3. using storePositions _network.storePositions() // updates x,y in _nodes // NOTE: trigger stabilisation, so may be stabilized event again var nodeCoords = _nodes.getIds().map( function (nodeId) { var node = _nodes.get(nodeId) var domPosition = _network.canvasToDOM({ x: node.x, y: node.y }) var canvasPos = _network.body.container.getBoundingClientRect() var envelope = { upperCorner: { x: canvasPos.x + domPosition.x - 10, y: canvasPos.y + domPosition.y - 10 }, lowerCorner: { x: canvasPos.x + domPosition.x + 10, y: canvasPos.y + domPosition.y + 10 }, }; DBG && DBG_drawBox(envelope, _nodes.get(nodeId).label); return Object.assign( { nodeId: nodeId }, node, { position: domPosition, envelope: envelope } ) } ) DBG && console.log("DBG:stabilized nodeCoords", { nodeCoords }); } DBG && console.log("DBG:stabilized", { event, network: this._network, nodes: this.state.visibleNodesSet, edges: this.state.allEdgesSet }); }, _storeUpdated: function () { this.setState( this.getStateFromStore() ) }, fitToContainer: function () { if (this._network) { this._network.fit() } }, incScale: function () { if (this._network) { var scale = this._network.getScale(); this._network.moveTo({ scale: scale + 0.2 }) } }, decScale: function () { if (this._network) { var scale = this._network.getScale(); this._network.moveTo({ scale: (scale > 0.3) ? scale - 0.2 : scale }) } }, reloadGraph: function () { this.props.store.dispatch({ type: 'RELOAD_NODES' }) }, testOnClick1: function () { TMP_COUNTER++; this._nodes.add({ id: TMP_COUNTER, label: "Node " + TMP_COUNTER, value: TMP_COUNTER, level: 0 }); }, testOnClick2: function () { TMP_COUNTER++; var ids = this._nodes.getIds(); var totalNodes = ids.length if (totalNodes < 3) return; var fromIdx = Math.floor(Math.random() * totalNodes); var toIdx = Math.floor(Math.random() * totalNodes); this._edges.add({ from: ids[fromIdx], to: ids[toIdx] }); }, testOnClick3: function () { this.props.store.dispatch( this.props.storeActions.fetchData( this.props.raportId ) ); }, testOnClick4: function () { DBG && console.log('DBG:Utils cache ', { cache: Utils.getCacheAllEdgeId() }) }, testOnClick5: function () { var edges = []; var nodes = [ { id: 'TERYT_adresy.123', label: "TER.123" }, { id: 'TERYT_adresy.666', label: "TER.666" } ]; DBG && console.log('DBG:testOnClick5 add custom node + edges ', { nodes: nodes, edges: edges }) this.props.store.dispatch({ type: 'TEST_ADD_DATA', nodes: nodes, edges: edges }) }, shouldComponentUpdate: function (nextProps, nextState) { // if (this.state.receivedRequestId !== nextState.receivedRequestId) { // add missing nodes // var state = this.props.store.getState(); // DBG && console.warn("TODO: update nodes, edges", { state }); // // this._edges.add( edge_or_edges_array ) // if (state.nodes && state.nodes.length) { // var __nodes = this._nodes; // state.nodes.forEach(function (node) { // __nodes.add(node); // }) // } // // edge: { from: page, to: subpageID, color:getEdgeColor(level), level: level, selectionWidth:2, hoverWidth:0 } // if (state.edges && state.edges.length) { // var __edges = this._edges; // // state.edges.forEach(function (edge) { // state.edges.slice(0, 30).forEach(function (edge) { // __edges.add({ // from: edge.source, // to: edge.target, // }); // }) // } // } return false; }, render: function () { return h('div', { style: { 'position': "relative" } }, [ h('div', { ref: this.setOutputRef, style: { 'min-height': 600, 'height': 600, 'border-radius': "6px", 'border': "1px solid #ddd", }, }), h('div', { className: "", style: { position: "absolute", right: 10, top: 10 } }, [ h('div', { className: "btn-group-vertical", title: "Powiększenie" }, [ h('button', { onClick: this.incScale, className: "btn btn-xs btn-default" }, [ h('i', { className: "glyphicon glyphicon-plus" }) ]), h('button', { onClick: this.fitToContainer, className: "btn btn-xs btn-default" }, [ "100%" ]), h('button', { onClick: this.decScale, className: "btn btn-xs btn-default" }, [ h('i', { className: "glyphicon glyphicon-minus" }) ]), ]), h('div', { style: { clear: "both", margin: 4 } }), h('div', { className: "btn-group-vertical", style: { width: "100%" } }, [ h('button', { onClick: this.reloadGraph, className: "btn btn-xs btn-default", title: "Odśwież" }, [ h('i', { className: "glyphicon glyphicon-random" }) ]), ]), ]), // h('div', {}, [ // h('button', { onClick: this.testOnClick1 }, [ "TEST 1 ", h('small', [], "(+ node)") ]), // h('button', { onClick: this.testOnClick2 }, [ "TEST 2 ", h('small', [], "(+ edge)") ]), // h('button', { onClick: this.testOnClick3 }, [ "TEST 3 ", h('small', [], "(+ fetch)") ]), // h('button', { onClick: this.testOnClick4 }, [ "TEST 4 ", h('small', [], "(+ log Utils.cache)") ]), // h('button', { onClick: this.testOnClick5 }, [ "TEST 5 ", h('small', [], "(+ data)") ]), // ]), ]); } }); /** * props.store: networkGraphStore * - used: state.nodes */ var p5UI__RaportOutputPanel = createReactClass({ getInitialState: function () { var initSelected = []; if (DBG_INIT_SELECTED) { var state = this.props.store.getState() initSelected = DBG_INIT_SELECTED.map(function (fid) { return state.allNodesSet.get(fid) }) this.props.store.dispatch( this.props.storeActions.setSelected(initSelected) ) } return { selected: initSelected, } }, handleSelectFeatures: function (selected) { DBG && console.log('DBG:typeahead selected:', { selected }) this.setState({ selected: selected }); // TODO: read from this.props.store.getState() this.props.store.dispatch( this.props.storeActions.setSelected( selected ) ); }, _onZoom: function (event) { // { // direction: '+'/'-', // scale: Number, // pointer: {x:pointer_x, y:pointer_y} // } DBG && console.log('DBG:onZoom', { scale: event.scale, event }) // var this__handleZoomUpdate = this.handleZoomUpdate; }, // handleZoomUpdate: function () { // DBG && console.warn('DBG:handleZoomUpdate', { event }) // }, render: function () { var state = this.props.store.getState() DBG && console.log('DBG:render - state', state); var nodes = state.allNodesSet.map(function (node) { return node; }); return h(React.Fragment, {}, [ h(Typeahead, { labelKey: "label", multiple: true, options: nodes, placeholder: "Wybierz", bsSize: 'large', selected: this.state.selected, onChange: this.handleSelectFeatures, }), h(p5UI__NetworkGraph, { raportId: this.props.raportId, store: this.props.store, storeActions: this.props.storeActions, selected: this.state.selected, onZoom: window._.throttle(this._onZoom, 500), }) ]) } }) function DBG_drawBox(envelope, label) { // envelope: { upperCorner, lowerCorner } var box = document.createElement('div') box.style.position = "absolute" box.style.backgroundColor = "red" box.style.opacity = 0.3 box.title = label box.style.top = '' + envelope.upperCorner.y + 'px' box.style.left = '' + envelope.upperCorner.x + 'px' box.style.height = '' + envelope.lowerCorner.y - envelope.upperCorner.y + 'px' box.style.width = '' + envelope.lowerCorner.x - envelope.upperCorner.x + 'px' document.body.appendChild(box) } ReactDOM.render( h(p5UI__RaportOutputPanel, { raportId: RAPORT_ID, store: createStoreWithThunkMiddleware(NetworkGraph.store), storeActions: NetworkGraph.createActions(), }), document.getElementById(HTML_ID_REF_GRAPH) );