From 2e1df7b67d82ad724000c440eae391f5ecc22455 Mon Sep 17 00:00:00 2001
From: Julian Matschinske <ge93nar@mytum.de>
Date: Sat, 11 Apr 2020 09:12:11 +0200
Subject: [PATCH] Optimize selection and deselection of nodes

---
 src/app/analysis.service.ts                   |  57 ++++---
 .../analysis-window.component.ts              | 144 ++++++++++++------
 .../info-box/info-box.component.html          |   2 +-
 .../launch-analysis.component.ts              |   2 +-
 .../explorer-page.component.html              |   4 +-
 .../explorer-page/explorer-page.component.ts  |  42 +++--
 6 files changed, 166 insertions(+), 85 deletions(-)

diff --git a/src/app/analysis.service.ts b/src/app/analysis.service.ts
index 4b040e5c..8cb9f71f 100644
--- a/src/app/analysis.service.ts
+++ b/src/app/analysis.service.ts
@@ -37,7 +37,7 @@ export const MAX_TASKS = 3;
 export class AnalysisService {
 
   private selectedItems = new Map<string, Wrapper>();
-  private selectSubject = new Subject<{ item: Wrapper, selected: boolean }>();
+  private selectListSubject = new Subject<{ items: Wrapper[], selected: boolean | null }>();
 
   public tokens: string[] = [];
   public finishedTokens: string[] = [];
@@ -82,40 +82,62 @@ export class AnalysisService {
     });
   }
 
-  public addItem(wrapper: Wrapper) {
-    if (!this.inSelection(wrapper)) {
-      this.selectedItems.set(wrapper.nodeId, wrapper);
-      this.selectSubject.next({item: wrapper, selected: true});
+  public addItems(wrappers: Wrapper[]) {
+    const addedWrappers: Wrapper[] = [];
+    for (const wrapper of wrappers) {
+      if (!this.inSelection(wrapper)) {
+        addedWrappers.push(wrapper);
+        this.selectedItems.set(wrapper.nodeId, wrapper);
+      }
+    }
+    this.selectListSubject.next({items: addedWrappers, selected: true});
+  }
+
+  public removeItems(wrappers: Wrapper[]) {
+    const removedWrappers: Wrapper[] = [];
+    for (const wrapper of wrappers) {
+      if (this.selectedItems.delete(wrapper.nodeId)) {
+        removedWrappers.push(wrapper);
+      }
     }
+    this.selectListSubject.next({items: removedWrappers, selected: false});
   }
 
   public addAllHostProteins(nodes, proteins) {
+    const items: Wrapper[] = [];
     const visibleIds = new Set<string>(nodes.getIds());
     for (const protein of proteins) {
       const wrapper = getWrapperFromProtein(protein);
       const found = visibleIds.has(wrapper.nodeId);
       if (found && !this.inSelection(wrapper)) {
-        this.addItem(wrapper);
+        items.push(wrapper);
+        this.selectedItems.set(wrapper.nodeId, wrapper);
       }
     }
+    this.selectListSubject.next({items, selected: true});
   }
 
   public addAllViralProteins(nodes, viralProteins) {
+    const items: Wrapper[] = [];
     const visibleIds = new Set<string>(nodes.getIds());
     for (const viralProtein of viralProteins) {
       const wrapper = getWrapperFromViralProtein(viralProtein);
       const found = visibleIds.has(wrapper.nodeId);
       if (found && !this.inSelection(wrapper)) {
-        this.addItem(wrapper);
+        items.push(wrapper);
+        this.selectedItems.set(wrapper.nodeId, wrapper);
       }
     }
+    this.selectListSubject.next({items, selected: true});
   }
 
   resetSelection() {
-    const oldSelection = this.selectedItems.values();
-    for (const item of oldSelection) {
-      this.removeItem(item);
-    }
+    this.selectListSubject.next({items: [], selected: null});
+    this.selectedItems.clear();
+  }
+
+  idInSelection(nodeId: string): boolean {
+    return this.selectedItems.has(nodeId);
   }
 
   inSelection(wrapper: Wrapper): boolean {
@@ -130,13 +152,6 @@ export class AnalysisService {
     return this.inSelection(getWrapperFromViralProtein(viralProtein));
   }
 
-  removeItem(wrapper: Wrapper) {
-    const item = this.selectedItems.get(wrapper.nodeId);
-    if (this.selectedItems.delete(wrapper.nodeId)) {
-      this.selectSubject.next({item, selected: false});
-    }
-  }
-
   getSelection(): Wrapper[] {
     return Array.from(this.selectedItems.values());
   }
@@ -145,9 +160,9 @@ export class AnalysisService {
     return this.selectedItems.size;
   }
 
-  subscribe(cb: (item: Wrapper, selected: boolean) => void) {
-    this.selectSubject.subscribe((event) => {
-      cb(event.item, event.selected);
+  subscribeList(cb: (items: Array<Wrapper>, selected: boolean | null) => void) {
+    this.selectListSubject.subscribe((event) => {
+      cb(event.items, event.selected);
     });
   }
 
diff --git a/src/app/components/analysis-window/analysis-window.component.ts b/src/app/components/analysis-window/analysis-window.component.ts
index e167fc61..9f4d4088 100644
--- a/src/app/components/analysis-window/analysis-window.component.ts
+++ b/src/app/components/analysis-window/analysis-window.component.ts
@@ -154,10 +154,10 @@ export class AnalysisWindowComponent implements OnInit, OnChanges {
             }
             const wrapper = node.wrapper;
             if (this.analysis.inSelection(wrapper)) {
-              this.analysis.removeItem(wrapper);
+              this.analysis.removeItems([wrapper]);
               this.analysis.getCount();
             } else {
-              this.analysis.addItem(wrapper);
+              this.analysis.addItems([wrapper]);
               this.analysis.getCount();
             }
           }
@@ -174,40 +174,78 @@ export class AnalysisWindowComponent implements OnInit, OnChanges {
           }
         });
 
-        this.analysis.subscribe((item, selected) => {
-          if (item.type === 'host') {
-            // TODO: Refactor!
-            const found = this.tableSelectedProteins.findIndex((i) => getProteinNodeId(i) === item.nodeId);
-            const tableItem = this.tableProteins.find((i) => getProteinNodeId(i) === item.nodeId);
-            if (selected && found === -1 && tableItem) {
-              this.tableSelectedProteins.push(tableItem);
-            }
-            if (!selected && found !== -1 && tableItem) {
-              this.tableSelectedProteins.splice(found, 1);
+        this.analysis.subscribeList((items, selected) => {
+          if (selected !== null) {
+            const updatedNodes = [];
+            for (const item of items) {
+              const node = this.nodeData.nodes.get(item.nodeId);
+              if (!node) {
+                continue;
+              }
+              const pos = this.network.getPositions([item.nodeId]);
+              node.x = pos[item.nodeId].x;
+              node.y = pos[item.nodeId].y;
+              Object.assign(node, NetworkSettings.getNodeStyle(node.wrapper.type, node.isSeed, selected));
+              updatedNodes.push(node);
             }
-            this.tableSelectedProteins = [...this.tableSelectedProteins];
-          } else if (item.type === 'virus') {
-            // TODO: Refactor!
-            const found = this.tableSelectedViralProteins.findIndex((i) => getViralProteinNodeId(i) === item.nodeId);
-            const tableItem = this.tableViralProteins.find((i) => getViralProteinNodeId(i) === item.nodeId);
-            if (selected && found === -1 && tableItem) {
-              this.tableSelectedViralProteins.push(tableItem);
+            this.nodeData.nodes.update(updatedNodes);
+
+            const proteinSelection = this.tableSelectedProteins;
+            const viralProteinSelection = this.tableSelectedViralProteins;
+            for (const item of items) {
+              if (item.type === 'host') {
+                // TODO: Refactor!
+                const found = proteinSelection.findIndex((i) => getProteinNodeId(i) === item.nodeId);
+                const tableItem = this.tableProteins.find((i) => getProteinNodeId(i) === item.nodeId);
+                if (selected && found === -1 && tableItem) {
+                  proteinSelection.push(tableItem);
+                }
+                if (!selected && found !== -1 && tableItem) {
+                  proteinSelection.splice(found, 1);
+                }
+              } else if (item.type === 'virus') {
+                // TODO: Refactor!
+                const found = viralProteinSelection.findIndex((i) => getViralProteinNodeId(i) === item.nodeId);
+                const tableItem = this.tableViralProteins.find((i) => getViralProteinNodeId(i) === item.nodeId);
+                if (selected && found === -1 && tableItem) {
+                  viralProteinSelection.push(tableItem);
+                }
+                if (!selected && found !== -1 && tableItem) {
+                  viralProteinSelection.splice(found, 1);
+                }
+              }
             }
-            if (!selected && found !== -1 && tableItem) {
-              this.tableSelectedViralProteins.splice(found, 1);
+            this.tableSelectedProteins = [...proteinSelection];
+            this.tableSelectedViralProteins = [...viralProteinSelection];
+          } else {
+            const updatedNodes = [];
+            this.nodeData.nodes.forEach((node) => {
+              const nodeSelected = this.analysis.idInSelection(node.id);
+              if (selected !== nodeSelected) {
+                Object.assign(node, NetworkSettings.getNodeStyle(node.wrapper.type, true, selected));
+                updatedNodes.push(node);
+              }
+            });
+            this.nodeData.nodes.update(updatedNodes);
+
+            const proteinSelection = [];
+            const viralProteinSelection = [];
+            for (const item of items) {
+              if (item.type === 'host') {
+                const tableItem = this.tableProteins.find((i) => getProteinNodeId(i) === item.nodeId);
+                if (tableItem) {
+                  proteinSelection.push(tableItem);
+                }
+              } else if (item.type === 'virus') {
+                const tableItem = this.tableViralProteins.find((i) => getViralProteinNodeId(i) === item.nodeId);
+                if (tableItem) {
+                  viralProteinSelection.push(tableItem);
+                }
+              }
             }
-            this.tableSelectedViralProteins = [...this.tableSelectedViralProteins];
-          }
-
-          const node = this.nodeData.nodes.get(item.nodeId);
-          if (!node) {
-            return;
+            this.tableSelectedProteins = [...proteinSelection];
+            this.tableSelectedViralProteins = [...viralProteinSelection];
           }
-          const pos = this.network.getPositions([item.nodeId]);
-          node.x = pos[item.nodeId].x;
-          node.y = pos[item.nodeId].y;
-          Object.assign(node, NetworkSettings.getNodeStyle(node.wrapper.type, node.isSeed, selected));
-          this.nodeData.nodes.update(node);
         });
       }
     }
@@ -390,20 +428,20 @@ export class AnalysisWindowComponent implements OnInit, OnChanges {
     if (this.showDrugs) {
       const result = await this.http.get<any>(
         `${environment.backend}drug_interactions/?token=${this.token}`).toPromise().catch(
-          (err: HttpErrorResponse) => {
-        // simple logging, but you can do a lot more, see below
-        toast({
-          message: 'An error occured while fetching the drugs.',
-          duration: 5000,
-          dismissible: true,
-          pauseOnHover: true,
-          type: 'is-danger',
-          position: 'top-center',
-          animate: {in: 'fadeIn', out: 'fadeOut'}
+        (err: HttpErrorResponse) => {
+          // simple logging, but you can do a lot more, see below
+          toast({
+            message: 'An error occured while fetching the drugs.',
+            duration: 5000,
+            dismissible: true,
+            pauseOnHover: true,
+            type: 'is-danger',
+            position: 'top-center',
+            animate: {in: 'fadeIn', out: 'fadeOut'}
+          });
+          this.showDrugs = false;
+          return;
         });
-        this.showDrugs = false;
-        return;
-      });
 
       const drugs = result.drugs;
       const edges = result.edges;
@@ -458,35 +496,43 @@ export class AnalysisWindowComponent implements OnInit, OnChanges {
   public tableProteinSelection(e) {
     const oldSelection = [...this.tableSelectedProteins];
     this.tableSelectedProteins = e;
+    const addItems = [];
+    const removeItems = [];
     for (const i of this.tableSelectedProteins) {
       const wrapper = getWrapperFromProtein(i);
       if (oldSelection.indexOf(i) === -1) {
-        this.analysis.addItem(wrapper);
+        addItems.push(wrapper);
       }
     }
     for (const i of oldSelection) {
       const wrapper = getWrapperFromProtein(i);
       if (this.tableSelectedProteins.indexOf(i) === -1) {
-        this.analysis.removeItem(wrapper);
+        removeItems.push(wrapper);
       }
     }
+    this.analysis.addItems(addItems);
+    this.analysis.removeItems(removeItems);
   }
 
   public tableViralProteinSelection(e) {
     const oldSelection = [...this.tableSelectedViralProteins];
     this.tableSelectedViralProteins = e;
+    const addItems = [];
+    const removeItems = [];
     for (const i of this.tableSelectedViralProteins) {
       const wrapper = getWrapperFromViralProtein(i);
       if (oldSelection.indexOf(i) === -1) {
-        this.analysis.addItem(wrapper);
+        addItems.push(wrapper);
       }
     }
     for (const i of oldSelection) {
       const wrapper = getWrapperFromViralProtein(i);
       if (this.tableSelectedViralProteins.indexOf(i) === -1) {
-        this.analysis.removeItem(wrapper);
+        removeItems.push(wrapper);
       }
     }
+    this.analysis.addItems(addItems);
+    this.analysis.removeItems(removeItems);
   }
 
 }
diff --git a/src/app/components/info-box/info-box.component.html b/src/app/components/info-box/info-box.component.html
index bc4f40ee..57e89bc8 100644
--- a/src/app/components/info-box/info-box.component.html
+++ b/src/app/components/info-box/info-box.component.html
@@ -44,7 +44,7 @@
 
   <div class="field has-addons add-remove-toggle" *ngIf="wrapper.type !== 'drug'">
     <app-toggle [value]="analysis.inSelection(wrapper)"
-                (valueChange)="$event ? analysis.addItem(wrapper) : analysis.removeItem(wrapper)" textOn="Selected"
+                (valueChange)="$event ? analysis.addItems([wrapper]) : analysis.removeItems([wrapper])" textOn="Selected"
                 textOff="Deselected" tooltipOn="Add protein to selection." tooltipOff="Remove protein from selection."
                 icon="fa-plus"></app-toggle>
   </div>
diff --git a/src/app/components/launch-analysis/launch-analysis.component.ts b/src/app/components/launch-analysis/launch-analysis.component.ts
index adee8d47..63dc23fe 100644
--- a/src/app/components/launch-analysis/launch-analysis.component.ts
+++ b/src/app/components/launch-analysis/launch-analysis.component.ts
@@ -57,7 +57,7 @@ export class LaunchAnalysisComponent implements OnInit, OnChanges {
 
   constructor(public analysis: AnalysisService) {
     this.hasBaits = !!analysis.getSelection().find((i) => i.type === 'virus');
-    analysis.subscribe(() => {
+    analysis.subscribeList(() => {
       this.hasBaits = !!analysis.getSelection().find((i) => i.type === 'virus');
     });
   }
diff --git a/src/app/pages/explorer-page/explorer-page.component.html b/src/app/pages/explorer-page/explorer-page.component.html
index 71de0c63..6d693342 100644
--- a/src/app/pages/explorer-page/explorer-page.component.html
+++ b/src/app/pages/explorer-page/explorer-page.component.html
@@ -26,7 +26,6 @@
             </a>
           </header>
           <div *ngIf="collapseData">
-
             <div class="card-content">
               <app-select-dataset [datasetItems]="datasetItems" [selectedDataset]="selectedDataset"
                                   (selectedDatasetChange)="selectedDataset = $event; createNetwork($event.data)">
@@ -371,7 +370,6 @@
       </div>
     </div>
 
-
     <div class="card bar-large">
       <header class="card-header">
         <p class="card-header-title">
@@ -408,7 +406,7 @@
               <td *ngIf="p.type == 'virus'">{{p.data.effectName}}</td>
               <td *ngIf="p.type == 'host'">{{p.data.name}}</td>
               <td>
-                <button (click)="analysis.removeItem(p)" class="button is-small is-danger is-outlined has-tooltip"
+                <button (click)="analysis.removeItems([p])" class="button is-small is-danger is-outlined has-tooltip"
                         data-tooltip="Remove from selection.">
                   <i class="fa fa-trash"></i>
                 </button>
diff --git a/src/app/pages/explorer-page/explorer-page.component.ts b/src/app/pages/explorer-page/explorer-page.component.ts
index c83e9db4..3611926b 100644
--- a/src/app/pages/explorer-page/explorer-page.component.ts
+++ b/src/app/pages/explorer-page/explorer-page.component.ts
@@ -114,16 +114,38 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
 
     this.showDetails = false;
 
-    this.analysis.subscribe((item, selected) => {
-      const node = this.nodeData.nodes.get(item.nodeId);
-      if (!node) {
+    this.analysis.subscribeList((items, selected) => {
+      if (!this.nodeData.nodes) {
         return;
       }
-      const pos = this.network.getPositions([item.nodeId]);
-      node.x = pos[item.nodeId].x;
-      node.y = pos[item.nodeId].y;
-      Object.assign(node, NetworkSettings.getNodeStyle(node.wrapper.type, true, selected));
-      this.nodeData.nodes.update(node);
+      if (selected !== null) {
+        if (items.length === 0) {
+          return;
+        }
+        const updatedNodes = [];
+        for (const item of items) {
+          const node = this.nodeData.nodes.get(item.nodeId);
+          if (!node) {
+            continue;
+          }
+          const pos = this.network.getPositions([item.nodeId]);
+          node.x = pos[item.nodeId].x;
+          node.y = pos[item.nodeId].y;
+          Object.assign(node, NetworkSettings.getNodeStyle(node.wrapper.type, true, selected));
+          updatedNodes.push(node);
+        }
+        this.nodeData.nodes.update(updatedNodes);
+      } else {
+        const updatedNodes = [];
+        this.nodeData.nodes.forEach((node) => {
+          const nodeSelected = this.analysis.idInSelection(node.id);
+          if (selected !== nodeSelected) {
+            Object.assign(node, NetworkSettings.getNodeStyle(node.wrapper.type, true, selected));
+            updatedNodes.push(node);
+          }
+        });
+        this.nodeData.nodes.update(updatedNodes);
+      }
     });
   }
 
@@ -234,9 +256,9 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
         const node = this.nodeData.nodes.get(nodeId);
         const wrapper = node.wrapper;
         if (this.analysis.inSelection(wrapper)) {
-          this.analysis.removeItem(wrapper);
+          this.analysis.removeItems([wrapper]);
         } else {
-          this.analysis.addItem(wrapper);
+          this.analysis.addItems([wrapper]);
         }
       }
     });
-- 
GitLab