From 572806964381db2ede7b9ef108bc71215190bc37 Mon Sep 17 00:00:00 2001
From: Dmitrii Krasnov <dmitrii_krasnov@epam.com>
Date: Tue, 1 Feb 2022 17:31:08 +0300
Subject: [PATCH] Metabolic pathways visualisation (#731): pathway tree search

---
 .../ngbCytoscapePathway.component.js          |  2 +-
 .../ngbCytoscapePathway.controller.js         | 74 +++++++++++++++++++
 .../ngbCytoscapePathway.settings.js           | 11 ++-
 .../ngbInternalPathwaysResult.controller.js   | 11 ++-
 .../ngbInternalPathwaysResult.scss            | 62 +++-------------
 .../ngbInternalPathwaysResult.tpl.html        | 20 +++++
 .../ngbInternalPathwaysTable.service.js       |  3 +-
 7 files changed, 129 insertions(+), 54 deletions(-)

diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js
index 862000c39..8690e2003 100644
--- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js
+++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.component.js
@@ -7,7 +7,7 @@ export default  {
         tag: '@',
         onElementClick: '&',
         storageName: '@',
-        elementsOptions: '<'
+        searchParams: '<'
     },
     controller: ngbCytoscapePathwayController
 };
diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js
index 3fd89dd93..38d2e55f6 100644
--- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js
+++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.controller.js
@@ -6,6 +6,8 @@ const sbgnStylesheet = require('cytoscape-sbgn-stylesheet');
 const $ = require('jquery');
 
 const SCALE = 0.3;
+const searchedColor = '#00cc00';
+let defaultNodeStyle = {};
 
 export default class ngbCytoscapePathwayController {
     constructor($element, $scope, $compile, $window, $timeout, dispatcher, cytoscapeSettings) {
@@ -57,6 +59,28 @@ export default class ngbCytoscapePathwayController {
             (changes.elements.previousValue.id !== changes.elements.currentValue.id)) {
             this.reloadCytoscape(true);
         }
+        if (!!changes.searchParams &&
+            !!changes.searchParams.previousValue &&
+            !!changes.searchParams.currentValue) {
+            if (changes.searchParams.currentValue.search
+                && changes.searchParams.previousValue.search !== changes.searchParams.currentValue.search) {
+                this.searchNode(
+                    changes.searchParams.currentValue.search,
+                    node => {
+                        node.style({
+                            'color': searchedColor,
+                            'border-color': searchedColor
+                        });
+                    },
+                    node => {
+                        node.style({
+                            'color': defaultNodeStyle.color,
+                            'border-color': defaultNodeStyle['border-color']
+                        });
+                    }
+                );
+            }
+        }
     }
 
     reloadCytoscape(active) {
@@ -67,6 +91,10 @@ export default class ngbCytoscapePathwayController {
             }
             this.$timeout(() => {
                 const sbgnStyle = sbgnStylesheet(Cytoscape);
+                defaultNodeStyle = {
+                    ...this.settings.style.node,
+                    ...this.getNodeStyle(sbgnStyle)
+                };
                 const savedState = JSON.parse(localStorage.getItem(this.storageName) || '{}');
                 const savedLayout = savedState.layout ? savedState.layout[this.elements.id] : undefined;
                 let elements;
@@ -210,4 +238,50 @@ export default class ngbCytoscapePathwayController {
         });
         return nodes;
     }
+
+    searchNode(term, onSatisfy, onDeny) {
+        if (!this.viewer) {
+            return;
+        }
+        const roots = this.viewer.nodes().roots();
+        this.viewer.nodes().dfs({
+            root: roots,
+            visit: node => {
+                if (this.deepSearch(node.data(), term)) {
+                    onSatisfy(node);
+                } else {
+                    onDeny(node);
+                }
+            }
+        });
+    }
+
+    deepSearch(obj, term) {
+        let result = false;
+        for (const key in obj) {
+            if (!obj.hasOwnProperty(key) || !obj[key]) continue;
+            if (obj[key] instanceof Object || obj[key] instanceof Array) {
+                result = this.deepSearch(obj[key], term);
+            } else {
+                result = obj[key].toString().toLocaleLowerCase()
+                    .includes(term.toLocaleLowerCase());
+            }
+            if (result) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    getNodeStyle(style) {
+        const result = {};
+        Object.keys(style).forEach(key => {
+            if (style[key].selector === 'node') {
+                Object.keys(style[key].properties).forEach(propKey => {
+                    result[style[key].properties[propKey].name] = style[key].properties[propKey].value;
+                });
+            }
+        });
+        return result;
+    }
 }
diff --git a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js
index 2cc400569..4846ebd6d 100644
--- a/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js
+++ b/client/client/app/components/ngbPathways/ngbCytoscapePathway/ngbCytoscapePathway.settings.js
@@ -1,6 +1,15 @@
 export default {
     viewer: {},
-    style: {},
+    style: {
+        node: {
+            'text-opacity': 1,
+            'text-valign': 'center',
+            'text-halign': 'center',
+            'shape': 'rectangle',
+            'color': '#000',
+            'border-width': 1
+        }
+    },
     defaultLayout: {
         name: 'dagre',
         rankDir: 'TB'
diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js
index d9eaecb4a..533a3747f 100644
--- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js
+++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.controller.js
@@ -2,7 +2,10 @@ import baseController from '../../../shared/baseController';
 
 export default class ngbInternalPathwaysResultController extends baseController {
     selectedTree = null;
-    selectedTreeName = null;
+    treeSearchParams = {
+        search: null
+    };
+    treeSearch = null;
     loading = true;
     treeError = false;
 
@@ -61,6 +64,12 @@ export default class ngbInternalPathwaysResultController extends baseController
         this.$timeout(() => this.$scope.$apply());
     }
 
+    searchInTree() {
+        this.treeSearchParams = {
+            search: this.treeSearch
+        };
+    }
+
     activePanelChanged(o) {
         const isActive = o === this.appLayout.Panels.pathways.panel;
         this.dispatcher.emit('cytoscape:panel:active', isActive);
diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss
index bf544dfca..98eb23aba 100644
--- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss
+++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.scss
@@ -31,7 +31,7 @@
 .tree-container {
   position: relative;
 
-  .element-description-container {
+  .pathway-search-panel {
     width: 50%;
     max-width: 300px;
     position: absolute;
@@ -51,70 +51,32 @@
       padding: 10px;
       box-shadow: 2px 2px 8px 2px #aaa;
 
-      .close {
-        cursor: pointer;
-        position: absolute;
-        top: 5px;
-        right: 5px;
-        fill: #777;
-      }
-
-      .close:hover {
-        fill: #333333;
-      }
-
-      .element-description-body {
+      .pathway-search-panel-body {
         display: table;
         padding-right: 16px;
 
-        .element-description-row {
+        .pathway-search-panel-row {
           display: flex;
           flex-wrap: wrap;
           align-items: baseline;
           border-spacing: 5px;
           word-break: break-all;
           padding: 5px 0;
-
-          .element-description-title {
-            margin-right: 5px;
-          }
-
-          .element-description-navigation {
-            font-weight: bold;
-            color: #2c4f9e;
-            text-decoration: underline;
-            cursor: pointer;
-          }
-
-          .sequenced {
-            font-size: smaller;
-            font-style: italic;
-            white-space: nowrap;
-          }
         }
 
-        .element-description-attributes {
-          display: flex;
-          flex-direction: column;
-          align-items: flex-start;
-
-          .element-description-attribute {
-            margin: 2px 0;
-            padding: 2px 4px;
-            background: #f9f9f9;
-            border: 1px solid #e9e9e9;
-            border-radius: 3px;
-            font-size: smaller;
-            text-transform: uppercase;
-          }
+        .pathway-tree-search-input {
+          font-size: 14px;
+          margin: 5px 0 10px 6px;
         }
 
-        .element-description-row:not(:last-child) {
-          border-bottom: 1px solid #eee;
+        .pathway-tree-search-input .md-errors-spacer {
+          min-height: 0;
         }
 
-        .element-description-empty {
-          color: #999;
+        .pathway-tree-search-button {
+          line-height: 32px;
+          min-height: 32px;
+          height: 32px;
         }
       }
     }
diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html
index 170a8ea8b..1c210b733 100644
--- a/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html
+++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysResult/ngbInternalPathwaysResult.tpl.html
@@ -16,6 +16,25 @@
         <span class="blast-search-result-title">{{$ctrl.selectedTree.name}}</span>
     </div>
     <div class="u-height__full tree-container" flex layout="column">
+        <div class="pathway-search-panel">
+            <div class="md-content">
+                <div class="pathway-search-panel-body" layout="column">
+                    <div class="pathway-search-panel-row">
+                        <form layout="row" ng-submit="$ctrl.searchInTree()">
+                            <md-input-container class="pathway-tree-search-input" flex>
+                                <input id="pathwayTreeSearchKeyword" name="pathway_tree_search_keyword"
+                                       ng-model="$ctrl.treeSearch" type="text">
+                            </md-input-container>
+                            <md-button aria-label="search node"
+                                       class="md-raised pathway-tree-search-button"
+                                       ng-click="$ctrl.searchInTree()">
+                                <ng-md-icon icon="search"></ng-md-icon>
+                            </md-button>
+                        </form>
+                    </div>
+                </div>
+            </div>
+        </div>
         <md-content flex layout="column">
             <div class="md-padding ngb-pathway-cytoscape-container">
                 <div class="internal-pathway-container-error" ng-if="$ctrl.treeError">
@@ -26,6 +45,7 @@
                         ng-if="!$ctrl.treeError"
                         storage-name="{{$ctrl.ngbInternalPathwaysResultService.localStorageKey}}"
                         tag="ngb-internal-pathway-node"
+                        search-params="$ctrl.treeSearchParams"
                 ></ngb-cytoscape-pathway>
             </div>
         </md-content>
diff --git a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js
index 2bb558d67..2e98f366c 100644
--- a/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js
+++ b/client/client/app/components/ngbPathways/ngbInternalPathwaysTable/ngbInternalPathwaysTable.service.js
@@ -67,7 +67,8 @@ export default class ngbInternalPathwaysTableService extends ClientPaginationSer
                 pageSize: this.pageSize,
                 pageNum: this.currentPage
             },
-            sortInfos: this.orderBy
+            sortInfos: this.orderBy,
+            term: currentSearch
         };
         const data = await this.genomeDataService.getInternalPathwaysLoad(filter);
         if (data.error) {