diff --git a/components/VFBMain.js b/components/VFBMain.js index 09abb64ed..8c8a2458b 100644 --- a/components/VFBMain.js +++ b/components/VFBMain.js @@ -788,7 +788,7 @@ export default class VFBMain extends React.Component { instance={this.instanceOnFocus} size={{ height: _height, width: _width }} ref={ref => this.treeBrowserReference = ref} - selectionHandler={this.addVfbId}/> + selectionHandler={this.addVfbId} /> ); } } diff --git a/components/configuration/buttonBarConfiguration.js b/components/configuration/buttonBarConfiguration.js index 2fdeef005..b834aa903 100644 --- a/components/configuration/buttonBarConfiguration.js +++ b/components/configuration/buttonBarConfiguration.js @@ -7,7 +7,7 @@ var buttonBarConfig = { "queryBuilderVisible": { "icon": "fa fa-quora", "label": "", - "tooltip": "Open Query" + "tooltip": "Query Results" }, "controlPanelVisible": { "icon": "fa fa-list", diff --git a/components/configuration/queryBuilderConfiguration.js b/components/configuration/queryBuilderConfiguration.js index 96b2f3853..77e0653eb 100644 --- a/components/configuration/queryBuilderConfiguration.js +++ b/components/configuration/queryBuilderConfiguration.js @@ -76,12 +76,26 @@ var queryResultsColMeta = [ "locked": false, "visible": true, "displayName": "Stage", - "cssClassName": "query-results-stage-column" + "cssClassName": "query-results-stage-column", + "sortDirectionCycle": ['asc', 'desc', null] }, { - "columnName": "controls", + "columnName": "license", "order": 8, "locked": false, + "visible": true, + "customComponent": QueryLinkComponent, + "actions": "window.addVfbId('$entity$');", + "entityIndex": 1, + "entityDelimiter": "----", + "displayName": "License", + "cssClassName": "query-results-license-column", + "sortDirectionCycle": ['asc', 'desc', null] + }, + { + "columnName": "controls", + "order": 9, + "locked": false, "visible": false, "customComponent": QueryResultsControlsComponent, "displayName": "Controls", @@ -91,7 +105,7 @@ var queryResultsColMeta = [ }, { "columnName": "images", - "order": 9, + "order": 10, "locked": false, "visible": true, "customComponent": SlideshowImageComponent, @@ -102,17 +116,26 @@ var queryResultsColMeta = [ }, { "columnName": "score", - "order": 10, + "order": 11, "locked": false, "visible": true, "displayName": "Score", "cssClassName": "query-results-score-column", "sortDirectionCycle": ['desc', 'asc', null] + }, + { + "columnName": "image_count", + "order": 12, + "locked": false, + "visible": true, + "displayName": "Image_count", + "cssClassName": "query-results-image_count-column", + "sortDirectionCycle": ['desc', 'asc', null] } ]; // which columns to display in the results -var queryResultsColumns = ['name', 'expressed_in', 'description', 'reference', 'type', 'stage', 'images', 'score']; +var queryResultsColumns = ['name', 'expressed_in', 'description', 'reference', 'type', 'stage', 'license', 'images', 'score','image_count']; var queryResultsControlConfig = { "Common": { @@ -140,7 +163,7 @@ var queryResultsControlConfig = { var queryBuilderDatasourceConfig = { VFB: { - url: "https://solr.virtualflybrain.org/solr/ontology/select?fl=short_form,label,synonym,id,type,has_narrow_synonym_annotation,has_broad_synonym_annotation&start=0&fq=ontology_name:(vfb)&fq=shortform_autosuggest:VFB*%20OR%20shortform_autosuggest:FB*%20OR%20is_defining_ontology:true&rows=100&bq=is_obsolete:false%5E100.0%20shortform_autosuggest:VFB*%5E110.0%20shortform_autosuggest:FBbt*%5E100.0%20is_defining_ontology:true%5E100.0%20label_s:%22%22%5E2%20synonym_s:%22%22%20in_subset_annotation:BRAINNAME%5E3%20short_form:FBbt_00003982%5E2&q=$SEARCH_TERM$%20OR%20$SEARCH_TERM$*%20OR%20*$SEARCH_TERM$*&defType=edismax&qf=label%20synonym%20label_autosuggest_ws%20label_autosuggest_e%20label_autosuggest%20synonym_autosuggest_ws%20synonym_autosuggest_e%20synonym_autosuggest%20shortform_autosuggest%20has_narrow_synonym_annotation%20has_broad_synonym_annotation&wt=json&indent=true", + url: "https://solr.virtualflybrain.org/solr/ontology/select?fl=short_form,label,synonym,id,type,has_narrow_synonym_annotation,has_broad_synonym_annotation&start=0&fq=ontology_name:(vfb)&fq=shortform_autosuggest:VFB*%20OR%20shortform_autosuggest:FB*%20OR%20is_defining_ontology:true&rows=100&bq=is_obsolete:false%5E100.0%20shortform_autosuggest:VFB*%5E110.0%20shortform_autosuggest:FBbt*%5E100.0%20is_defining_ontology:true%5E100.0%20label_s:%22%22%5E2%20synonym_s:%22%22%20in_subset_annotation:BRAINNAME%5E3%20short_form:FBbt_00003982%5E2&q=$SEARCH_TERM$%20OR%20$SEARCH_TERM$*%20OR%20*$SEARCH_TERM$*&defType=edismax&qf=label%20synonym%20label_autosuggest_ws%20label_autosuggest_e%20label_autosuggest%20synonym_autosuggest_ws%20synonym_autosuggest_e%20synonym_autosuggest%20shortform_autosuggest%20has_narrow_synonym_annotation%20has_broad_synonym_annotation&wt=json&indent=true", crossDomain: true, id: "short_form", label: { field: "label", formatting: "$VALUE$" }, @@ -280,6 +303,10 @@ var sorterColumns = [ column: "score", order: "DESC" }, + { + column: "image_count", + order: "DESC" + }, { column: "images", order: "DESC" diff --git a/components/configuration/treeWidgetConfiguration.js b/components/configuration/treeWidgetConfiguration.js index 37a299831..f088829c2 100644 --- a/components/configuration/treeWidgetConfiguration.js +++ b/components/configuration/treeWidgetConfiguration.js @@ -3,14 +3,30 @@ var restPostConfig = { contentType: "application/json" }; +/* + * var treeCypherQuery = instance => ({ + * "statements": [ + * { + * "statement": "MATCH (root:Class)<-[:INSTANCEOF]-(t:Individual {short_form:'" + instance + "'})" + * + "<-[:depicts]-(tc:Individual)<-[ie:in_register_with]-(c:Individual)-[:depicts]->(image:" + * + "Individual)-[r:INSTANCEOF]->(anat:Class) WHERE has(ie.index) WITH root, anat,r,image" + * + " MATCH p=allShortestPaths((root)<-[:SUBCLASSOF|part_of*..]-(anat:Class)) RETURN p,r,image", + * "resultDataContents": ["graph"] + * } + * ] + * }); + */ + var treeCypherQuery = instance => ({ "statements": [ { "statement": "MATCH (root:Class)<-[:INSTANCEOF]-(t:Individual {short_form:'" + instance + "'})" + "<-[:depicts]-(tc:Individual)<-[ie:in_register_with]-(c:Individual)-[:depicts]->(image:" + "Individual)-[r:INSTANCEOF]->(anat:Class) WHERE has(ie.index) WITH root, anat,r,image" - + " MATCH p=allShortestPaths((root)<-[:SUBCLASSOF|part_of*..]-(anat:Class)) RETURN p,r,image", - "resultDataContents": ["graph"] + + " MATCH p=allShortestPaths((root)<-[:SUBCLASSOF|part_of*..]-(anat)) " + + "RETURN collect(distinct { node_id: id(anat), short_form: anat.short_form, image: image.short_form })" + + " AS image_nodes, id(root) AS root, collect(p)", + "resultDataContents": ["row", "graph"] } ] }); @@ -19,4 +35,4 @@ var treeCypherQuery = instance => ({ module.exports = { restPostConfig, treeCypherQuery -}; \ No newline at end of file +}; diff --git a/components/interface/ErrorCatcher.js b/components/interface/ErrorCatcher.js index cf4f5d7b5..89cc74d6c 100644 --- a/components/interface/ErrorCatcher.js +++ b/components/interface/ErrorCatcher.js @@ -47,7 +47,7 @@ class ErrorCatcher extends React.Component { handleClose = () => { var customMessage = "Steps to reproduce the problem: \n\nPlease fill the below with the necessary steps to reproduce the problem\n\n\n\nError Information:\n\n" - var url = "https://github.com/VirtualFlyBrain/VFB2/issues/new?body=" + customMessage + this.state.error.message + "\n\n" + this.state.error.stack.replace("#",escape("#")) + "\n\n```diff\n" + window.console.logs.slice(-10).join('\n').replace("#",escape("#")) + "\n```\n"; + var url = "https://github.com/VirtualFlyBrain/VFB2/issues/new?body=" + customMessage + this.state.error.message + "\n\n" + this.state.error.stack.replace("#",escape("#")) + "\n\n```diff\n" + window.console.logs.slice(-8).join('\n').replace("#",escape("#")) + "\n```\n"; var win = window.open(encodeURI(url), '_blank'); win.focus(); }; diff --git a/components/interface/FocusTerm.js b/components/interface/FocusTerm.js index df1a78606..465600785 100644 --- a/components/interface/FocusTerm.js +++ b/components/interface/FocusTerm.js @@ -498,14 +498,14 @@ export default class FocusTerm extends React.Component { }} /> + title="Query results"> { this.props.UIUpdateManager("queryBuilderVisible"); }} /> + title="Layers"> { this.props.UIUpdateManager("controlPanelVisible"); diff --git a/components/interface/TreeWidget.js b/components/interface/TreeWidget.js index 7e1eb7068..814abbbac 100644 --- a/components/interface/TreeWidget.js +++ b/components/interface/TreeWidget.js @@ -21,10 +21,11 @@ export default class TreeWidget extends React.Component { super(props); this.state = { - instance: undefined, + errors: undefined, dataTree: undefined, root: undefined, loading: false, + edges: undefined, nodes: undefined, nodeSelected: undefined, displayColorPicker: false, @@ -33,21 +34,26 @@ export default class TreeWidget extends React.Component { this.initTree = this.initTree.bind(this); this.getNodes = this.getNodes.bind(this); - this.sortData = this.sortData.bind(this); this.restPost = this.restPost.bind(this); this.nodeClick = this.nodeClick.bind(this); this.updateTree = this.updateTree.bind(this); this.getButtons = this.getButtons.bind(this); this.selectNode = this.selectNode.bind(this); - this.convertEdges = this.convertEdges.bind(this); - this.convertNodes = this.convertNodes.bind(this); this.findChildren = this.findChildren.bind(this); - this.searchChildren = this.searchChildren.bind(this); this.insertChildren = this.insertChildren.bind(this); + this.updateSubtitle = this.updateSubtitle.bind(this); this.monitorMouseClick = this.monitorMouseClick.bind(this); - this.defaultComparator = this.defaultComparator.bind(this); this.convertDataForTree = this.convertDataForTree.bind(this); - this.parseGraphResultData = this.parseGraphResultData.bind(this); + + this.isNumber = require('./VFBTree/helper').isNumber; + this.sortData = require('./VFBTree/helper').sortData; + this.findRoot = require('./VFBTree/helper').findRoot; + this.convertEdges = require('./VFBTree/helper').convertEdges; + this.convertNodes = require('./VFBTree/helper').convertNodes; + this.searchChildren = require('./VFBTree/helper').searchChildren; + this.defaultComparator = require('./VFBTree/helper').defaultComparator; + this.parseGraphResultData = require('./VFBTree/helper').parseGraphResultData; + this.buildDictClassToIndividual = require('./VFBTree/helper').buildDictClassToIndividual; this.theme = createMuiTheme({ overrides: { MuiTooltip: { tooltip: { fontSize: "12px" } } } }); this.AUTHORIZATION = "Basic " + btoa("neo4j:vfb"); @@ -65,13 +71,6 @@ export default class TreeWidget extends React.Component { this.nodeWithColorPicker = undefined; } - isNumber (variable) { - if (isNaN(variable)) { - return variable; - } else { - return parseFloat(variable); - } - } restPost (data) { var strData = JSON.stringify(data); return $.ajax({ @@ -87,175 +86,6 @@ export default class TreeWidget extends React.Component { }); } - defaultComparator (a, b, key) { - if (this.isNumber(a[key]) < this.isNumber(b[key])) { - return -1; - } - if (this.isNumber(a[key]) > this.isNumber(b[key])) { - return 1; - } - return 0; - } - - sortData (unsortedArray, key, comparator) { - // Create a sortable array to return. - const sortedArray = [ ...unsortedArray ]; - - // Recursively sort sub-arrays. - const recursiveSort = (start, end) => { - - // If this sub-array is empty, it's sorted. - if (end - start < 1) { - return; - } - - const pivotValue = sortedArray[end]; - let splitIndex = start; - for (let i = start; i < end; i++) { - const sort = comparator(sortedArray[i], pivotValue, key); - - // This value is less than the pivot value. - if (sort === -1) { - - /* - * If the element just to the right of the split index, - * isn't this element, swap them. - */ - if (splitIndex !== i) { - const temp = sortedArray[splitIndex]; - sortedArray[splitIndex] = sortedArray[i]; - sortedArray[i] = temp; - } - - /* - * Move the split index to the right by one, - * denoting an increase in the less-than sub-array size. - */ - splitIndex++; - } - - /* - * Leave values that are greater than or equal to - * the pivot value where they are. - */ - } - - // Move the pivot value to between the split. - sortedArray[end] = sortedArray[splitIndex]; - sortedArray[splitIndex] = pivotValue; - - // Recursively sort the less-than and greater-than arrays. - recursiveSort(start, splitIndex - 1); - recursiveSort(splitIndex + 1, end); - }; - - // Sort the entire array. - recursiveSort(0, unsortedArray.length - 1); - return sortedArray; - } - - convertEdges (edges) { - var convertedEdges = []; - edges.forEach(function (edge) { - var relatType = edge.type.replace("_"," "); - if (relatType.indexOf("Related") > -1){ - relatType = edge.properties['label'].replace("_"," "); - } - if (convertedEdges.length > 0) { - - } else { - convertedEdges.push({ - from: edge.endNode, - to: edge.startNode, - label: relatType - }); - } - convertedEdges.push({ - from: edge.endNode, - to: edge.startNode, - label: relatType - }); - }); - return convertedEdges; - } - - convertNodes (nodes) { - var convertedNodes = []; - nodes.forEach(function (node) { - var nodeLabel = node.properties['short_form']; - var displayedLabel = node.properties['label']; - var description = node.properties['description'] - convertedNodes.push({ - title: displayedLabel, - subtitle: nodeLabel, - instanceId: nodeLabel, - info: description, - id: node.id, - }) - }); - return convertedNodes; - } - - parseGraphResultData (data) { - var nodes = {}, edges = {}; - data.results[0].data.forEach(function (row) { - row.graph.nodes.forEach(function (n) { - if (!nodes.hasOwnProperty(n.id)) { - nodes[n.id] = n; - } - }); - row.graph.relationships.forEach(function (r) { - if (!edges.hasOwnProperty(r.id)) { - edges[r.id] = r; - } - }); - }); - var nodesArray = [], edgesArray = []; - for (var p in nodes) { - if (nodes.hasOwnProperty(p)) { - nodesArray.push(nodes[p]); - } - } - for (var q in edges) { - if (edges.hasOwnProperty(q)) { - edgesArray.push(edges[q]) - } - } - return { nodes: nodesArray, edges: edgesArray }; - } - - searchChildren (array, key, target, label){ - // Define Start and End Index - let startIndex = 0; - let endIndex = array.length - 1; - - // While Start Index is less than or equal to End Index - while (startIndex <= endIndex) { - // Define Middle Index (This will change when comparing ) - let middleIndex = Math.floor((startIndex + endIndex) / 2); - // Compare Middle Index with Target for match - if (this.isNumber(array[middleIndex][key]) === this.isNumber(target[key])) { - // check for target relationship (label) - if (array[middleIndex].label === label){ - return middleIndex; - } else { - // move on if not matching target relationship (label) - startIndex = middleIndex + 1; - } - } - // Search Right Side Of Array - if (this.isNumber(target[key]) > this.isNumber(array[middleIndex][key])) { - startIndex = middleIndex + 1; - } - // Search Left Side Of Array - if (this.isNumber(target[key]) < this.isNumber(array[middleIndex][key])) { - endIndex = middleIndex - 1; - } - } - // If Target Is Not Found - return undefined; - } - findChildren (parent, key, familyList, label) { var childrenList = []; var childKey = this.searchChildren(familyList, key, parent, label); @@ -275,7 +105,7 @@ export default class TreeWidget extends React.Component { return childrenList; } - insertChildren (nodes, edges, child) { + insertChildren (nodes, edges, child, imagesMap) { var childrenList = this.findChildren({ from: child.id }, "from", edges, "part of"); // child.images = this.findChildren({ from: child.id }, "from", edges, "INSTANCEOF"); var nodesList = []; @@ -284,45 +114,33 @@ export default class TreeWidget extends React.Component { } var uniqNodes = [...new Set(nodesList)]; - for (var j = uniqNodes.length - 1; j >= 0 ; j--) { - var node = nodes[this.findChildren({ id: uniqNodes[j] }, "id", nodes)[0]]; - if (node.instanceId.indexOf("VFB_") > -1) { - child.instanceId = node.instanceId; - node.subtitle = child.subtitle; - // child.subtitle = child.subtitle + " " + node.instanceId; - uniqNodes.splice(j, 1); - } - } - for ( var j = 0; j < uniqNodes.length; j++) { var node = nodes[this.findChildren({ id: uniqNodes[j] }, "id", nodes)[0]]; - if (node.instanceId.indexOf("VFB_") > -1) { - child.instanceId = node.instanceId; - node.subtitle = child.subtitle; - } else { - child.children.push({ - title: node.title, - subtitle: node.instanceId, - description: node.info, - instanceId: node.instanceId, - id: node.id, - showColorPicker: false, - children: [] - }); - this.insertChildren(nodes, edges, child.children[j]) - } + let imageId = node.instanceId; + child.children.push({ + title: node.title, + subtitle: node.classId, + description: node.info, + classId: node.classId, + instanceId: node.instanceId, + id: node.id, + showColorPicker: false, + children: [] + }); + this.insertChildren(nodes, edges, child.children[j], imagesMap) } } - convertDataForTree (nodes, edges, vertix) { + convertDataForTree (nodes, edges, vertix, imagesMap) { // This will create the data structure for the react-sortable-tree library, starting from the vertix node. var refinedDataTree = []; for ( var i = 0; i < nodes.length; i++ ) { if (vertix === nodes[i].id) { refinedDataTree.push({ title: nodes[i].title, - subtitle: nodes[i].instanceId, + subtitle: nodes[i].classId, description: nodes[i].info, + classId: nodes[i].classId, instanceId: nodes[i].instanceId, id: nodes[i].id, showColorPicker: false, @@ -333,12 +151,31 @@ export default class TreeWidget extends React.Component { } var child = refinedDataTree[0]; // Once the vertix has been established we call insertChildren recursively on each child. - this.insertChildren(nodes, edges, child); + this.insertChildren(nodes, edges, child, imagesMap); return refinedDataTree; } + updateSubtitle (tree, idSelected) { + var node = undefined; + if (tree.length !== undefined) { + node = tree[0]; + } else { + node = tree; + } + if (node.instanceId === idSelected || node.classId === idSelected) { + node.subtitle = idSelected; + } + for (let i = 0; i < node.children.length; i++) { + this.updateSubtitle(node.children[i], idSelected); + } + } + selectNode (instance) { if (this.state.nodeSelected !== undefined && this.state.nodeSelected.instanceId !== instance.instanceId) { + /* + * var treeData = this.state.dataTree; + * this.updateSubtitle(treeData, instance.instanceId); + */ this.setState({ nodeSelected: instance }); } } @@ -354,9 +191,12 @@ export default class TreeWidget extends React.Component { } else { innerInstance = instance; } + var idToSearch = innerInstance.getId(); - if (this.state.instance !== undefined && innerInstance.id !== this.state.instance.id) { - if (innerInstance.id === window.templateID) { + if (this.state.nodeSelected !== undefined + && idToSearch !== this.state.nodeSelected.instanceId + && idToSearch !== this.state.nodeSelected.classId) { + if (idToSearch === window.templateID) { this.selectNode(this.state.dataTree[0]) return; } @@ -367,8 +207,7 @@ export default class TreeWidget extends React.Component { * in the searchQuery in the render to move the tree focus on this node */ while (this.state.nodes.length > i) { - var idToSearch = innerInstance.getId(); - if (idToSearch === this.state.nodes[i]["instanceId"]) { + if (idToSearch === this.state.nodes[i]["instanceId"] || idToSearch === this.state.nodes[i]["classId"]) { node.push(i); break; } @@ -382,23 +221,31 @@ export default class TreeWidget extends React.Component { initTree (instance) { // This function is the core and starting point of the component itself + var that = this; this.setState({ loading: true }); this.restPost(treeCypherQuery(instance)).done(data => { /* * we take the data provided by the cypher query and consume the until we obtain the treeData that can be given * to the react-sortable-tree since it understands this data structure */ + if (data.errors.length > 0) { + console.log("-- ERROR TREE COMPONENT --"); + console.log(data.errors); + this.setState({ errors: "Error retrieving the data - check the console for additional information" }); + } + if (data.results[0].data.length > 0) { var dataTree = this.parseGraphResultData(data); - var vertix = this.findRoot(data.results[0].data[0].graph.nodes); - var nodes = this.sortData(this.convertNodes(dataTree.nodes), "id", this.defaultComparator); + var vertix = this.findRoot(data); + var imagesMap = this.buildDictClassToIndividual(data); + var nodes = this.sortData(this.convertNodes(dataTree.nodes, imagesMap), "id", this.defaultComparator); var edges = this.sortData(this.convertEdges(dataTree.edges), "from", this.defaultComparator); - var treeData = this.convertDataForTree(nodes, edges, vertix); + var treeData = this.convertDataForTree(nodes, edges, vertix, imagesMap); this.setState({ - instance: { id: instance }, + loading: false, dataTree: treeData, root: vertix, - loading: false, + edges: edges, nodes: nodes, nodeSelected: (this.props.instance === undefined ? treeData[0] @@ -413,7 +260,6 @@ export default class TreeWidget extends React.Component { children: [] }]; this.setState({ - instance: { id: instance }, dataTree: treeData, root: undefined, loading: false @@ -422,29 +268,37 @@ export default class TreeWidget extends React.Component { }); } - findRoot (nodes) { - let min = nodes[0].id; - for ( let i = 1; i < nodes.length; i++) { - if (nodes[i].id < min) { - min = nodes[i].id; - } - } - return min; - } - nodeClick (event, rowInfo) { - this.selectNode(rowInfo.node); + if (event.target.getAttribute("type") !== "button" && (event.target.getAttribute("aria-label") !== "Collapse" || event.target.getAttribute("aria-label") !== "Expand")) { + this.selectNode(rowInfo.node); + } } monitorMouseClick (e) { - // event handler to monitor when we click outside the color picker and close it. - if (!(this.colorPickerContainer !== undefined && this.colorPickerContainer !== null && this.colorPickerContainer.contains(e.target))) { + const clickCoord = { + INSIDE: 'inside', + OUTSIDE: 'outside', + PICKER_PRESENT: 'picker_present', + NODE_PRESENT: 'node_present' + }; + + let clickCondition = undefined; + if (this.colorPickerContainer !== undefined && this.colorPickerContainer !== null) { + clickCondition = clickCoord.PICKER_PRESENT; + if (!this.colorPickerContainer.contains(e.target)) { + clickCondition = clickCoord.OUTSIDE; + } + } + + switch (clickCondition) { + case clickCoord.OUTSIDE: if (this.nodeWithColorPicker !== undefined) { this.nodeWithColorPicker.showColorPicker = false; this.nodeWithColorPicker = undefined; } this.colorPickerContainer = undefined; this.setState({ displayColorPicker: false }); + break; } } @@ -452,9 +306,16 @@ export default class TreeWidget extends React.Component { // As per name, provided by the react-sortable-tree api, we use this to attach to each node custom buttons var buttons = []; var fillCondition = "unknown"; + var instanceLoaded = false; if (rowInfo.node.instanceId.indexOf("VFB_") > -1) { fillCondition = "3dAvailable"; - if (Instances[rowInfo.node.instanceId] === undefined) { + for (var i = 1; i < Instances.length; i++) { + if (Instances[i].id !== undefined && Instances[i].id === rowInfo.node.instanceId) { + instanceLoaded = true; + break; + } + } + if (!instanceLoaded) { fillCondition = "3dToLoad"; } else { if ((typeof Instances[rowInfo.node.instanceId].isVisible !== "undefined") && (Instances[rowInfo.node.instanceId].isVisible())) { @@ -471,7 +332,7 @@ export default class TreeWidget extends React.Component { aria-hidden="true" onClick={ e => { e.stopPropagation(); - rowInfo.node.subtitle = rowInfo.node.instanceId; + // rowInfo.node.subtitle = rowInfo.node.instanceId; this.props.selectionHandler(rowInfo.node.instanceId); this.setState({ nodeSelected: rowInfo.node }); }} />); @@ -560,13 +421,24 @@ export default class TreeWidget extends React.Component {
{rowInfo.node.description}
)}>
{ e.stopPropagation(); this.colorPickerContainer = undefined; - this.props.selectionHandler(rowInfo.node.subtitle); + let instanceFound = false; + for (let i = 0; i < Instances.length; i++) { + if (Instances[i].getId() === rowInfo.node.instanceId) { + instanceFound = true; + break; + } + } + if (instanceFound && typeof Instances[rowInfo.node.instanceId].isVisible === "function") { + this.props.selectionHandler(rowInfo.node.instanceId); + } else { + this.props.selectionHandler(rowInfo.node.classId); + } this.setState({ nodeSelected: rowInfo.node }); }}> {rowInfo.node.title} @@ -590,7 +462,18 @@ export default class TreeWidget extends React.Component { componentDidMount () { var that = this; document.addEventListener('mousedown', this.monitorMouseClick, false); + GEPPETTO.on(GEPPETTO.Events.Select, function (instance) { + that.updateTree(instance); + }); + + GEPPETTO.on(GEPPETTO.Events.Instance_deleted, function (parameters) { + if (Instances[parameters] !== undefined ) { + that.setState({ nodeSelected: undefined }); + } + }); + + GEPPETTO.on(GEPPETTO.Events.Instances_created, function () { that.setState({ displayColorPicker: false }); }); } diff --git a/components/interface/VFBToolBar.js b/components/interface/VFBToolBar.js index 2196eb0e7..cf236e62c 100644 --- a/components/interface/VFBToolBar.js +++ b/components/interface/VFBToolBar.js @@ -117,19 +117,19 @@ export default class VFBToolBar extends React.Component { } else if ((verOffset = nAgt.indexOf("MSIE")) != -1) { // In MSIE, the true version is after "MSIE" in userAgent browserName = "Microsoft Internet Explorer"; fullVersion = nAgt.substring(verOffset + 5); - } else if ((verOffset = nAgt.indexOf("Chrome")) != -1) { // In Chrome, the true version is after "Chrome" + } else if ((verOffset = nAgt.indexOf("Chrome")) != -1) { // In Chrome, the true version is after "Chrome" browserName = "Chrome"; fullVersion = nAgt.substring(verOffset + 7); - } else if ((verOffset = nAgt.indexOf("Safari")) != -1) { // In Safari, the true version is after "Safari" or after "Version" + } else if ((verOffset = nAgt.indexOf("Safari")) != -1) { // In Safari, the true version is after "Safari" or after "Version" browserName = "Safari"; fullVersion = nAgt.substring(verOffset + 7); if ((verOffset = nAgt.indexOf("Version")) != -1) { fullVersion = nAgt.substring(verOffset + 8); } - } else if ((verOffset = nAgt.indexOf("Firefox")) != -1) { // In Firefox, the true version is after "Firefox" + } else if ((verOffset = nAgt.indexOf("Firefox")) != -1) { // In Firefox, the true version is after "Firefox" browserName = "Firefox"; fullVersion = nAgt.substring(verOffset + 8); - } else if ( (nameOffset = nAgt.lastIndexOf(' ') + 1) < (verOffset = nAgt.lastIndexOf('/')) ) { // In most other browsers, "name/version" is at the end of userAgent + } else if ( (nameOffset = nAgt.lastIndexOf(' ') + 1) < (verOffset = nAgt.lastIndexOf('/')) ) { // In most other browsers, "name/version" is at the end of userAgent browserName = nAgt.substring(nameOffset,verOffset); fullVersion = nAgt.substring(verOffset + 1); if (browserName.toLowerCase() == browserName.toUpperCase()) { @@ -145,10 +145,32 @@ export default class VFBToolBar extends React.Component { } majorVersion = parseInt('' + fullVersion,10); if (isNaN(majorVersion)) { - fullVersion = '' + parseFloat(navigator.appVersion); + fullVersion = '' + parseFloat(navigator.appVersion); majorVersion = parseInt(navigator.appVersion,10); } - + // return as much of the log up to the last 10 events < 1000 characters: + var logLength = -10; + var limitedLog = window.console.logs.slice(logLength).join('%0A').replace( + /\&/g,escape('&') + ).replace( + /\#/g,escape('#') + ).replace( + /\-/g,'%2D' + ).replace( + /\+/g,'%2B' + ); + while (limitedLog.length > 1000 && logLength < 0) { + logLength += 1; + limitedLog = window.console.logs.slice(logLength).join('%0A').replace( + /\&/g,escape('&') + ).replace( + /\#/g,escape('#') + ).replace( + /\-/g,'%2D' + ).replace( + /\+/g,'%2B' + ); + } this.props.htmlOutputHandler( htmlContent.replace( /\$URL\$/g,window.location.href.replace( @@ -165,15 +187,7 @@ export default class VFBToolBar extends React.Component { ).replace( /\$SCREEN\$/g, window.innerWidth + ',' + window.innerHeight ).replace( - /\$LOG\$/g, window.console.logs.slice(-10).join('%0A').replace( - /\&/g,escape('&') - ).replace( - /\#/g,escape('#') - ).replace( - /\-/g,'%2D' - ).replace( - /\+/g,'%2B' - ) + /\$LOG\$/g, limitedLog ).replace( /\$COLOURLOG\$/g, window.console.logs.join('
').replace( /\&/g,escape('&') diff --git a/components/interface/VFBTree/helper.js b/components/interface/VFBTree/helper.js new file mode 100644 index 000000000..084051717 --- /dev/null +++ b/components/interface/VFBTree/helper.js @@ -0,0 +1,206 @@ +/* eslint-disable no-prototype-builtins */ + +const parseGraphResultData = data => { + var nodes = {}, edges = {}; + data.results[0].data.forEach(function (row) { + row.graph.nodes.forEach(function (n) { + if (!nodes.hasOwnProperty(n.id)) { + nodes[n.id] = n; + } + }); + row.graph.relationships.forEach(function (r) { + if (!edges.hasOwnProperty(r.id)) { + edges[r.id] = r; + } + }); + }); + var nodesArray = [], edgesArray = []; + for (var p in nodes) { + if (nodes.hasOwnProperty(p)) { + nodesArray.push(nodes[p]); + } + } + for (var q in edges) { + if (edges.hasOwnProperty(q)) { + edgesArray.push(edges[q]) + } + } + return { nodes: nodesArray, edges: edgesArray }; +} + +const findRoot = data => { + let columns = data.results[0].columns; + let rootIndex = columns.indexOf("root"); + return data.results[0].data[0].row[rootIndex].toString(); +} + +const buildDictClassToIndividual = data => { + var dictionaryIndividuals = {}; + let columns = data.results[0].columns; + let imagesIndex = columns.indexOf("image_nodes"); + let nodes = data.results[0].data[0].row[imagesIndex]; + for (let i = 0; i < nodes.length; i++) { + dictionaryIndividuals[nodes[i].short_form] = { + id: nodes[i].node_id, + image: nodes[i].image + } + } + return dictionaryIndividuals; +}; + +const searchChildren = (array, key, target, label) => { + // Define Start and End Index + let startIndex = 0; + let endIndex = array.length - 1; + + // While Start Index is less than or equal to End Index + while (startIndex <= endIndex) { + // Define Middle Index (This will change when comparing ) + let middleIndex = Math.floor((startIndex + endIndex) / 2); + // Compare Middle Index with Target for match + if (isNumber(array[middleIndex][key]) === isNumber(target[key])) { + // check for target relationship (label) + if (array[middleIndex].label === label){ + return middleIndex; + } else { + // move on if not matching target relationship (label) + startIndex = middleIndex + 1; + } + } + // Search Right Side Of Array + if (isNumber(target[key]) > isNumber(array[middleIndex][key])) { + startIndex = middleIndex + 1; + } + // Search Left Side Of Array + if (isNumber(target[key]) < isNumber(array[middleIndex][key])) { + endIndex = middleIndex - 1; + } + } + // If Target Is Not Found + return undefined; +} + +const sortData = (unsortedArray, key, comparator) => { + // Create a sortable array to return. + const sortedArray = [ ...unsortedArray ]; + // Recursively sort sub-arrays. + const recursiveSort = (start, end) => { + // If this sub-array is empty, it's sorted. + if (end - start < 1) { + return; + } + const pivotValue = sortedArray[end]; + let splitIndex = start; + for (let i = start; i < end; i++) { + const sort = comparator(sortedArray[i], pivotValue, key); + // This value is less than the pivot value. + if (sort === -1) { + /* + * If the element just to the right of the split index, + * isn't this element, swap them. + */ + if (splitIndex !== i) { + const temp = sortedArray[splitIndex]; + sortedArray[splitIndex] = sortedArray[i]; + sortedArray[i] = temp; + } + /* + * Move the split index to the right by one, + * denoting an increase in the less-than sub-array size. + */ + splitIndex++; + } + /* + * Leave values that are greater than or equal to + * the pivot value where they are. + */ + } + // Move the pivot value to between the split. + sortedArray[end] = sortedArray[splitIndex]; + sortedArray[splitIndex] = pivotValue; + // Recursively sort the less-than and greater-than arrays. + recursiveSort(start, splitIndex - 1); + recursiveSort(splitIndex + 1, end); + }; + // Sort the entire array. + recursiveSort(0, unsortedArray.length - 1); + return sortedArray; +} + + +const convertEdges = edges => { + var convertedEdges = []; + edges.forEach(function (edge) { + var relatType = edge.type.replace("_"," "); + if (relatType.indexOf("Related") > -1){ + relatType = edge.properties['label'].replace("_"," "); + } + if (convertedEdges.length > 0) { + + } else { + convertedEdges.push({ + from: edge.endNode, + to: edge.startNode, + label: relatType + }); + } + convertedEdges.push({ + from: edge.endNode, + to: edge.startNode, + label: relatType + }); + }); + return convertedEdges; +} + +const convertNodes = (nodes, imagesMap) => { + var convertedNodes = []; + nodes.forEach(function (node) { + var nodeLabel = node.properties['short_form']; + var nodeImage = nodeLabel; + if (imagesMap[nodeLabel] !== undefined && imagesMap[nodeLabel].id.toString() === node.id.toString()) { + nodeImage = imagesMap[nodeLabel].image; + } + var displayedLabel = node.properties['label']; + var description = node.properties['description'] + convertedNodes.push({ + title: displayedLabel, + subtitle: nodeLabel, + instanceId: nodeImage, + classId: nodeLabel, + info: description, + id: node.id, + }) + }); + return convertedNodes; +} + +const defaultComparator = (a, b, key) => { + if (isNumber(a[key]) < isNumber(b[key])) { + return -1; + } + if (isNumber(a[key]) > isNumber(b[key])) { + return 1; + } + return 0; +} + +const isNumber = variable => { + if (isNaN(variable)) { + return variable; + } else { + return parseFloat(variable); + } +} + +module.exports = { + isNumber, + findRoot, + sortData, + convertNodes, + convertEdges, + searchChildren, + defaultComparator, + parseGraphResultData, + buildDictClassToIndividual +}; diff --git a/css/VFBTermInfo.less b/css/VFBTermInfo.less index 979414556..ccfaa0493 100644 --- a/css/VFBTermInfo.less +++ b/css/VFBTermInfo.less @@ -380,6 +380,10 @@ background-color:#534700 !important; } +.label.types>.label.label-Split { + background-color:#e012e3 !important; +} + .button-bar-vfbHistoryLinks-back { position:absolute; left:10px; diff --git a/model/vfb.xmi b/model/vfb.xmi index 90c64d59a..de6d449ef 100644 --- a/model/vfb.xmi +++ b/model/vfb.xmi @@ -129,6 +129,9 @@ + + name="Query for anatomy from expression " + description="Get JSON for anat_2_ep query"> + query=""statement": "MATCH (ep:Expression_pattern:Class)<-[ar:overlaps|part_of]-(anoni:Individual)-[:INSTANCEOF]->(anat:Class) WHERE ep.short_form in [{ID}] WITH anoni, anat, ar OPTIONAL MATCH (p:pub { short_form: ar.pub}) WITH anat, anoni, { core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p) } , PubMed: coalesce(p.PMID, ''), FlyBase: coalesce(p.FlyBase, ''), DOI: coalesce(p.DOI, '') } AS pub OPTIONAL MATCH (anoni)-[r:Related]->(o:FBdv) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.uri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o) } }) END AS stages ,anoni,anat,pub OPTIONAL MATCH (anat:Synaptic_neuropil)<-[:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(template_anat:Individual) WITH template, channel, template_anat, irw, anoni, anat, pub, stages , i OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i) } , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel) } , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique) } ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template) } , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat) } ,image_folder: irw.folder, index: coalesce(irw.index, []) + [] }} }) END AS anatomy_channel_image ,anoni,anat,pub,stages RETURN { short_form: anat.short_form, label: coalesce(anat.label,''), iri: anat.iri, types: labels(anat) } AS anatomy, 'Get JSON for ep_2_anat query' AS query, 'ca9ab19' AS version , pub, stages, anatomy_channel_image", "parameters" : { "ID" : "$ID" }" + countQuery=""statement": "MATCH (ep:Class:Expression_pattern)<-[ar:overlaps|part_of]-(anoni:Individual)-[:INSTANCEOF]->(anat:Class) WHERE ep.short_form in [{ID}] RETURN count(anat) as count", "parameters" : { "ID" : "$ID" }"/> + + + + + + + + + + + + + + @@ -424,9 +487,9 @@ @@ -444,9 +507,9 @@ @@ -454,9 +517,9 @@ @@ -881,4 +944,22 @@ + + + + + +