From b0b3f1ab6f7e4607cdc30ac6fd8f17a819a6f81d Mon Sep 17 00:00:00 2001
From: Julian Matschinske <ge93nar@mytum.de>
Date: Wed, 15 Apr 2020 17:04:19 +0200
Subject: [PATCH] Add custom proteins

---
 src/app/analysis.service.ts                   | 20 ++++-
 src/app/app.module.ts                         |  2 +
 .../custom-proteins.component.html            | 44 +++++++++++
 .../custom-proteins.component.scss            | 10 +++
 .../custom-proteins.component.spec.ts         | 27 +++++++
 .../custom-proteins.component.ts              | 75 +++++++++++++++++++
 .../explorer-page.component.html              | 44 +++++++++--
 .../explorer-page.component.scss              |  9 +++
 .../explorer-page/explorer-page.component.ts  |  2 +
 9 files changed, 226 insertions(+), 7 deletions(-)
 create mode 100644 src/app/components/custom-proteins/custom-proteins.component.html
 create mode 100644 src/app/components/custom-proteins/custom-proteins.component.scss
 create mode 100644 src/app/components/custom-proteins/custom-proteins.component.spec.ts
 create mode 100644 src/app/components/custom-proteins/custom-proteins.component.ts

diff --git a/src/app/analysis.service.ts b/src/app/analysis.service.ts
index 0866109c..bccdf162 100644
--- a/src/app/analysis.service.ts
+++ b/src/app/analysis.service.ts
@@ -103,7 +103,7 @@ export class AnalysisService {
     this.selectListSubject.next({items: removedWrappers, selected: false});
   }
 
-  public addAllHostProteins(nodes, proteins) {
+  public addVisibleHostProteins(nodes, proteins) {
     const items: Wrapper[] = [];
     const visibleIds = new Set<string>(nodes.getIds());
     for (const protein of proteins) {
@@ -117,7 +117,7 @@ export class AnalysisService {
     this.selectListSubject.next({items, selected: true});
   }
 
-  public addAllViralProteins(nodes, viralProteins) {
+  public addVisibleViralProteins(nodes, viralProteins) {
     const items: Wrapper[] = [];
     const visibleIds = new Set<string>(nodes.getIds());
     for (const viralProtein of viralProteins) {
@@ -131,6 +131,22 @@ export class AnalysisService {
     this.selectListSubject.next({items, selected: true});
   }
 
+  public removeAllHostProteins() {
+    const items: Wrapper[] = Array.from(this.selectedItems.values()).filter(p => p.type === 'host');
+    for (const wrapper of items) {
+      this.selectedItems.delete(wrapper.nodeId);
+    }
+    this.selectListSubject.next({items, selected: false});
+  }
+
+  public removeAllViralProteins() {
+    const items: Wrapper[] = Array.from(this.selectedItems.values()).filter(p => p.type === 'virus');
+    for (const wrapper of items) {
+      this.selectedItems.delete(wrapper.nodeId);
+    }
+    this.selectListSubject.next({items, selected: false});
+  }
+
   resetSelection() {
     this.selectListSubject.next({items: [], selected: null});
     this.selectedItems.clear();
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 7b184586..37fdc628 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -19,6 +19,7 @@ import {AnalysisWindowComponent} from './components/analysis-window/analysis-win
 import {TaskListComponent} from './components/task-list/task-list.component';
 import {ToggleComponent} from './components/toggle/toggle.component';
 import {InfoBoxComponent} from './components/info-box/info-box.component';
+import {CustomProteinsComponent} from './components/custom-proteins/custom-proteins.component';
 
 import {AnalysisService} from './analysis.service';
 
@@ -36,6 +37,7 @@ import {AnalysisService} from './analysis.service';
     TaskListComponent,
     ToggleComponent,
     InfoBoxComponent,
+    CustomProteinsComponent,
   ],
   imports: [
     BrowserModule,
diff --git a/src/app/components/custom-proteins/custom-proteins.component.html b/src/app/components/custom-proteins/custom-proteins.component.html
new file mode 100644
index 00000000..90736648
--- /dev/null
+++ b/src/app/components/custom-proteins/custom-proteins.component.html
@@ -0,0 +1,44 @@
+<div class="modal" [class.is-active]="show">
+  <div class="modal-background"></div>
+  <div class="modal-card">
+    <header class="modal-card-head">
+      <p class="modal-card-title">
+        <span class="icon"><i class="fa fa-dna"></i></span>
+        Add Custom Proteins
+      </p>
+      <button (click)="close()" class="delete" aria-label="close"></button>
+    </header>
+    <section class="modal-card-body">
+      <div class="notification is-success" *ngIf="itemsAdded.length > 0">
+        {{itemsAdded.length}} host proteins have been added to the selection.
+      </div>
+      <div class="notification is-warning" *ngIf="notFound.length > 0">
+        The following {{notFound.length}} Uniprot IDs could not be found and have been ignored:
+        <ul><li class="not-found" *ngFor="let nf of notFound">{{nf}}</li></ul>
+      </div>
+      <div class="field">
+        <label class="label" for="protein-list">List of Uniprot IDs</label>
+        <div class="control">
+          <textarea class="input" [ngModel]="textList" (ngModelChange)="changeTextList($event)" id="protein-list"></textarea>
+        </div>
+      </div>
+      <p *ngIf="proteins">
+        Proteins parsed: {{proteins.length}}
+      </p>
+    </section>
+    <footer class="modal-card-foot">
+      <button (click)="addProteins();" class="button is-success is-rounded has-tooltip"
+              data-tooltip="Add list of proteins to the selection."
+              [disabled]="proteins.length === 0">
+        <span class="icon">
+          <i class="fa fa-plus"></i>
+        </span>
+        <span>
+          Add
+        </span>
+      </button>
+      <button (click)="close()" class="button is-rounded has-tooltip" data-tooltip="Close the current window.">Close
+      </button>
+    </footer>
+  </div>
+</div>
diff --git a/src/app/components/custom-proteins/custom-proteins.component.scss b/src/app/components/custom-proteins/custom-proteins.component.scss
new file mode 100644
index 00000000..a403b397
--- /dev/null
+++ b/src/app/components/custom-proteins/custom-proteins.component.scss
@@ -0,0 +1,10 @@
+#protein-list {
+  height: 150px;
+}
+
+.not-found {
+  padding: 4px;
+  margin: 4px;
+  border-radius: 3px;
+  background-color: rgba(255, 255, 255, 0.33);
+}
diff --git a/src/app/components/custom-proteins/custom-proteins.component.spec.ts b/src/app/components/custom-proteins/custom-proteins.component.spec.ts
new file mode 100644
index 00000000..2c5e7741
--- /dev/null
+++ b/src/app/components/custom-proteins/custom-proteins.component.spec.ts
@@ -0,0 +1,27 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+
+import {CustomProteinsComponent} from './custom-proteins.component';
+import {HttpClientModule} from '@angular/common/http';
+
+describe('CustomProteinsComponent', () => {
+  let component: CustomProteinsComponent;
+  let fixture: ComponentFixture<CustomProteinsComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [CustomProteinsComponent],
+      imports: [HttpClientModule],
+    })
+      .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CustomProteinsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/components/custom-proteins/custom-proteins.component.ts b/src/app/components/custom-proteins/custom-proteins.component.ts
new file mode 100644
index 00000000..483ce2d9
--- /dev/null
+++ b/src/app/components/custom-proteins/custom-proteins.component.ts
@@ -0,0 +1,75 @@
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+import {environment} from '../../../environments/environment';
+import {getWrapperFromProtein, Wrapper} from '../../interfaces';
+import {AnalysisService} from '../../analysis.service';
+
+@Component({
+  selector: 'app-custom-proteins',
+  templateUrl: './custom-proteins.component.html',
+  styleUrls: ['./custom-proteins.component.scss']
+})
+export class CustomProteinsComponent implements OnInit {
+
+  @Input()
+  public show = false;
+  @Output()
+  public showChange = new EventEmitter<boolean>();
+
+  public textList = '';
+  public proteins: Array<string> = [];
+  public notFound: Array<string> = [];
+  public itemsAdded: Array<Wrapper> = [];
+
+  constructor(private http: HttpClient, private analysis: AnalysisService) { }
+
+  ngOnInit(): void {
+  }
+
+  public close() {
+    this.show = false;
+    this.showChange.emit(this.show);
+  }
+
+  public async addProteins() {
+    this.notFound = [];
+    this.itemsAdded = [];
+    const proteins = this.proteins;
+    this.changeTextList('');
+    const result = await this.http.post<any>(`${environment.backend}query_proteins/`, proteins).toPromise();
+    this.notFound = result.notFound;
+    const details = result.details;
+    const items = [];
+    for (const detail of details) {
+      items.push(getWrapperFromProtein(detail));
+    }
+    this.itemsAdded = items;
+    this.analysis.addItems(items);
+  }
+
+  public changeTextList(textList) {
+    this.textList = textList;
+    if (!textList) {
+      this.proteins = [];
+      return;
+    }
+
+    const separators = ['\n', ',', ';', ' '];
+
+    let proteins;
+    for (const sep of separators) {
+      if (textList.indexOf(sep) === -1) {
+        continue;
+      }
+      proteins = textList.split(sep).map(ac => ac.trim()).filter(ac => !!ac);
+      break;
+    }
+
+    if (!proteins) {
+      proteins = [textList];
+    }
+
+    this.proteins = proteins;
+  }
+
+}
diff --git a/src/app/pages/explorer-page/explorer-page.component.html b/src/app/pages/explorer-page/explorer-page.component.html
index 78343dc7..ad18cfdc 100644
--- a/src/app/pages/explorer-page/explorer-page.component.html
+++ b/src/app/pages/explorer-page/explorer-page.component.html
@@ -4,6 +4,9 @@
                        [dataset]="selectedDataset.backendId">
   </app-launch-analysis>
 
+  <app-custom-proteins [(show)]="showCustomProteinsDialog">
+  </app-custom-proteins>
+
   <div class="covex explorer">
 
   <div class="covex left-window">
@@ -387,7 +390,7 @@
           </span>
         </a>
       </header>
-      <div *ngIf="collapseSelection">
+      <div *ngIf="collapseSelection" class="seed-selection">
         <div class="card-content overflow">
           <table class="table selection-table" *ngIf="analysis.getCount() > 0">
             <thead>
@@ -421,25 +424,56 @@
           </i>
         </div>
         <footer class="card-footer">
-          <a (click)="analysis.addAllHostProteins(currentViewNodes, currentViewProteins)"
+          <a (click)="analysis.addVisibleHostProteins(currentViewNodes, currentViewProteins)"
              class="card-footer-item has-text-success" data-tooltip="Add all visible host proteins.">
             <span class="icon">
-              <i class="fa fa-plus"></i>
+              <i class="fa fa-eye"></i>
             </span>
             <span>
               Host Proteins
             </span>
           </a>
-          <a (click)="analysis.addAllViralProteins(currentViewNodes, currentViewEffects)"
+          <a (click)="analysis.addVisibleViralProteins(currentViewNodes, currentViewEffects)"
              class="card-footer-item has-text-success" data-tooltip="Add all visible viral proteins.">
             <span class="icon">
-              <i class="fa fa-plus"></i>
+              <i class="fa fa-eye"></i>
+            </span>
+            <span>
+              Viral Proteins
+            </span>
+          </a>
+        </footer>
+        <footer class="card-footer">
+          <a (click)="analysis.removeAllHostProteins()"
+             class="card-footer-item has-text-danger" data-tooltip="Remove all host proteins.">
+            <span class="icon">
+              <i class="fa fa-minus"></i>
+            </span>
+            <span>
+              Host Proteins
+            </span>
+          </a>
+          <a (click)="analysis.removeAllViralProteins()"
+             class="card-footer-item has-text-danger" data-tooltip="Remove all viral proteins.">
+            <span class="icon">
+              <i class="fa fa-minus"></i>
             </span>
             <span>
               Viral Proteins
             </span>
           </a>
         </footer>
+        <footer class="card-footer">
+          <a (click)="showCustomProteinsDialog = true" class="card-footer-item has-text-primary"
+             data-tooltip="Add a custom list of proteins.">
+            <span class="icon">
+              <i class="fa fa-upload"></i>
+            </span>
+            <span>
+              Custom Proteins
+            </span>
+          </a>
+        </footer>
         <footer class="card-footer">
           <a (click)="analysis.resetSelection()" class="card-footer-item has-text-danger"
              data-tooltip="Remove all entries of the selection.">
diff --git a/src/app/pages/explorer-page/explorer-page.component.scss b/src/app/pages/explorer-page/explorer-page.component.scss
index 6042c453..5d4f9d9e 100644
--- a/src/app/pages/explorer-page/explorer-page.component.scss
+++ b/src/app/pages/explorer-page/explorer-page.component.scss
@@ -41,3 +41,12 @@
   padding: 15px;
   font-weight: bold;
 }
+
+.seed-selection {
+  .card-footer {
+    font-size: 12px;
+    a {
+      padding: 3px;
+    }
+  }
+}
diff --git a/src/app/pages/explorer-page/explorer-page.component.ts b/src/app/pages/explorer-page/explorer-page.component.ts
index db0d9275..bca79bba 100644
--- a/src/app/pages/explorer-page/explorer-page.component.ts
+++ b/src/app/pages/explorer-page/explorer-page.component.ts
@@ -60,6 +60,8 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
   public showAnalysisDialog = false;
   public analysisDialogTarget: 'drug' | 'drug-target';
 
+  public showCustomProteinsDialog = false;
+
   public selectedAnalysisToken: string | null = null;
 
   public currentDataset = [];
-- 
GitLab