Add pit image submission test

Add submission test and add pit images to view tab.

Signed-off-by: Emily Markova <emily.markova@gmail.com>
Change-Id: I01e227f19c0622fcca09186662ab25ab34599045
diff --git a/scouting/www/pit_scouting/pit_scouting.component.ts b/scouting/www/pit_scouting/pit_scouting.component.ts
index fe14090..58e7647 100644
--- a/scouting/www/pit_scouting/pit_scouting.component.ts
+++ b/scouting/www/pit_scouting/pit_scouting.component.ts
@@ -1,4 +1,10 @@
-import {Component} from '@angular/core';
+import {
+  Component,
+  ElementRef,
+  QueryList,
+  ViewChild,
+  ViewChildren,
+} from '@angular/core';
 import {Builder, ByteBuffer} from 'flatbuffers';
 import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated';
 import {SubmitPitImage} from '../../webserver/requests/messages/submit_pit_image_generated';
@@ -16,10 +22,11 @@
   styleUrls: ['../app/common.css', './pit_scouting.component.css'],
 })
 export class PitScoutingComponent {
+  @ViewChild('preview') preview: ElementRef<HTMLImageElement>;
   section: Section = 'Data';
 
   errorMessage = '';
-  teamNumber: string = '971';
+  teamNumber: string = '1';
   pitImage: string = '';
 
   async readFile(file): Promise<ArrayBuffer> {
@@ -39,10 +46,19 @@
     return new Uint8Array(await this.readFile(selectedFile));
   }
 
+  async setPitImage(event) {
+    const src = URL.createObjectURL(event.target.files[0]);
+    const previewElement = this.preview.nativeElement;
+    previewElement.src = src;
+    previewElement.style.display = 'block';
+  }
+
   async submitData() {
     const builder = new Builder();
     const teamNumber = builder.createString(this.teamNumber);
-    const pitImage = builder.createString(this.pitImage.toString());
+    const pitImage = builder.createString(
+      this.pitImage.toString().replace(/^.*[\\\/]/, '')
+    );
     const imageData = SubmitPitImage.createImageDataVector(
       builder,
       await this.getImageData()
@@ -68,8 +84,14 @@
 
       const errorMessage = parsedResponse.errorMessage();
       this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+      return;
     }
-    this.section = 'TeamSelection';
+
+    // Reset Data.
+    this.section = 'Data';
+    this.errorMessage = '';
     this.pitImage = '';
+    const previewElement = this.preview.nativeElement;
+    previewElement.src = '';
   }
 }
diff --git a/scouting/www/pit_scouting/pit_scouting.ng.html b/scouting/www/pit_scouting/pit_scouting.ng.html
index 9e90d2d..94de4f0 100644
--- a/scouting/www/pit_scouting/pit_scouting.ng.html
+++ b/scouting/www/pit_scouting/pit_scouting.ng.html
@@ -5,6 +5,7 @@
   <div *ngSwitchCase="'Data'" id="pitImageSelection" class="container-fluid">
     <label for="teamNumber">Team Number</label>
     <input [(ngModel)]="teamNumber" type="text" id="teamNumber" />
+    <img id="preview" #preview style="max-height: 200px; max-width: 200px" />
     <form action="setPitImage()">
       <label for="pitImage">Select pit image:</label>
       <br />
@@ -12,6 +13,7 @@
         id="pitImage"
         [(ngModel)]="pitImage"
         type="file"
+        (change)="setPitImage($event)"
         accept="image/*"
       />
     </form>
diff --git a/scouting/www/rpc/BUILD b/scouting/www/rpc/BUILD
index 113c747..ab28581 100644
--- a/scouting/www/rpc/BUILD
+++ b/scouting/www/rpc/BUILD
@@ -19,6 +19,8 @@
         "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_notes_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_notes_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_pit_images_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_pit_images_ts_fbs",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
     ],
 )
diff --git a/scouting/www/rpc/view_data_requestor.ts b/scouting/www/rpc/view_data_requestor.ts
index 24d10b6..f1f2b79 100644
--- a/scouting/www/rpc/view_data_requestor.ts
+++ b/scouting/www/rpc/view_data_requestor.ts
@@ -13,6 +13,11 @@
 } from '../../webserver/requests/messages/request_all_driver_rankings_response_generated';
 import {Request2023DataScouting} from '../../webserver/requests/messages/request_2023_data_scouting_generated';
 import {
+  PitImage,
+  RequestAllPitImagesResponse,
+} from '../../webserver/requests/messages/request_all_pit_images_response_generated';
+import {RequestAllPitImages} from '../../webserver/requests/messages/request_all_pit_images_generated';
+import {
   Stats2023,
   Request2023DataScoutingResponse,
 } from '../../webserver/requests/messages/request_2023_data_scouting_response_generated';
@@ -96,4 +101,25 @@
     }
     return statList;
   }
+
+  // Returns all pit image entries from the database.
+  async fetchPitImagesList(): Promise<PitImage[]> {
+    let fbBuffer = await this.fetchFromServer(
+      RequestAllPitImages.startRequestAllPitImages,
+      RequestAllPitImages.endRequestAllPitImages,
+      '/requests/request/all_pit_images'
+    );
+
+    const parsedResponse =
+      RequestAllPitImagesResponse.getRootAsRequestAllPitImagesResponse(
+        fbBuffer
+      );
+
+    // Convert the flatbuffer list into an array. That's more useful.
+    const pitImageList = [];
+    for (let i = 0; i < parsedResponse.pitImageListLength(); i++) {
+      pitImageList.push(parsedResponse.pitImageList(i));
+    }
+    return pitImageList;
+  }
 }
diff --git a/scouting/www/view/view.component.css b/scouting/www/view/view.component.css
index e220645..7811d2d 100644
--- a/scouting/www/view/view.component.css
+++ b/scouting/www/view/view.component.css
@@ -1,3 +1,23 @@
 * {
   padding: 10px;
 }
+
+.collapse-border {
+  border-collapse: collapse;
+  border-color: transparent;
+  border-width: 0;
+  overflow: auto;
+}
+
+.align-images {
+  word-wrap: break-word;
+  display: flex;
+  margin: 5px;
+  justify-content: center;
+  align-items: center;
+}
+
+.align-text {
+  text-align: center;
+  vertical-align: middle;
+}
diff --git a/scouting/www/view/view.component.ts b/scouting/www/view/view.component.ts
index 26e816b..64b0680 100644
--- a/scouting/www/view/view.component.ts
+++ b/scouting/www/view/view.component.ts
@@ -9,6 +9,12 @@
   Stats2023,
   Request2023DataScoutingResponse,
 } from '../../webserver/requests/messages/request_2023_data_scouting_response_generated';
+
+import {
+  PitImage,
+  RequestAllPitImagesResponse,
+} from '../../webserver/requests/messages/request_all_pit_images_response_generated';
+
 import {
   Note,
   RequestAllNotesResponse,
@@ -18,7 +24,7 @@
 
 import {ViewDataRequestor} from '../rpc';
 
-type Source = 'Notes' | 'Stats2023' | 'DriverRanking';
+type Source = 'Notes' | 'Stats2023' | 'PitImages' | 'DriverRanking';
 
 //TODO(Filip): Deduplicate
 const COMP_LEVEL_LABELS = {
@@ -56,6 +62,7 @@
   // Stores the corresponding data.
   noteList: Note[] = [];
   driverRankingList: Ranking[] = [];
+  pitImageList: PitImage[][] = [];
   statList: Stats2023[] = [];
 
   // Fetch notes on initialization.
@@ -74,13 +81,23 @@
           .team()
           .localeCompare(a[0].team(), undefined, {numeric: true});
       });
+      this.pitImageList.sort(function (a, b) {
+        return b[0]
+          .teamNumber()
+          .localeCompare(a[0].teamNumber(), undefined, {numeric: true});
+      });
       this.statList.sort((a, b) => b.matchNumber() - a.matchNumber());
     } else {
       this.driverRankingList.sort((a, b) => a.matchNumber() - b.matchNumber());
       this.noteList.sort(function (a, b) {
-        return a[0]
+        return b[0]
           .team()
-          .localeCompare(b[0].team(), undefined, {numeric: true});
+          .localeCompare(a[0].team(), undefined, {numeric: true});
+      });
+      this.pitImageList.sort(function (a, b) {
+        return a[0]
+          .teamNumber()
+          .localeCompare(b[0].teamNumber(), undefined, {numeric: true});
       });
       this.statList.sort((a, b) => a.matchNumber() - b.matchNumber());
     }
@@ -95,6 +112,7 @@
     this.noteList = [];
     this.driverRankingList = [];
     this.statList = [];
+    this.pitImageList = [];
     this.fetchCurrentSource();
   }
 
@@ -109,6 +127,10 @@
         this.fetchStats2023();
       }
 
+      case 'PitImages': {
+        this.fetchPitImages();
+      }
+
       case 'DriverRanking': {
         this.fetchDriverRanking();
       }
@@ -211,6 +233,42 @@
     }
   }
 
+  // Fetch all pit image data and store in pitImageList.
+  async fetchPitImages() {
+    this.progressMessage = 'Fetching pit image list. Please be patient.';
+    this.errorMessage = '';
+
+    try {
+      const initialPitImageList =
+        await this.viewDataRequestor.fetchPitImagesList();
+      let officialPitImageList = [];
+      // Use iteration to make an array of arrays containing pit image data for individual teams.
+      // Ex. [ [ {971PitImageData} , {971PitImage2Data} ], [ {432PitImageData} ] , [ {213PitImageData} ] ]
+      for (let pitImage of initialPitImageList) {
+        let found = false;
+        for (let arr of officialPitImageList) {
+          if (arr[0].teamNumber() == pitImage.teamNumber()) {
+            arr.push(pitImage);
+            found = true;
+          }
+        }
+        if (!found) {
+          officialPitImageList.push([pitImage]);
+        }
+      }
+      // Sort the arrays based on image file names so their order is predictable.
+      for (let arr of officialPitImageList) {
+        arr.sort((a, b) => (a.imagePath() > b.imagePath() ? 1 : -1));
+      }
+      this.pitImageList = officialPitImageList;
+      this.sortData();
+      this.progressMessage = 'Successfully fetched pit image list.';
+    } catch (e) {
+      this.errorMessage = e;
+      this.progressMessage = '';
+    }
+  }
+
   // Fetch all data scouting (stats) data and store in statList.
   async fetchStats2023() {
     this.progressMessage = 'Fetching stats list. Please be patient.';
diff --git a/scouting/www/view/view.ng.html b/scouting/www/view/view.ng.html
index ee2c62f..239548c 100644
--- a/scouting/www/view/view.ng.html
+++ b/scouting/www/view/view.ng.html
@@ -34,6 +34,16 @@
       <a
         class="dropdown-item"
         href="#"
+        (click)="switchDataSource('PitImages')"
+        id="pit_images_source_dropdown"
+      >
+        PitImages
+      </a>
+    </li>
+    <li>
+      <a
+        class="dropdown-item"
+        href="#"
         (click)="switchDataSource('DriverRanking')"
         id="driver_ranking_source_dropdown"
       >
@@ -123,6 +133,42 @@
       </tbody>
     </table>
   </div>
+  <!-- Pit Images Data Display. -->
+  <div *ngSwitchCase="'PitImages'">
+    <table class="table collapse-border">
+      <thead>
+        <tr>
+          <th scope="col" class="d-flex flex-row">
+            <div class="align-self-center">Team</div>
+            <div class="align-self-center" *ngIf="ascendingSort">
+              <i (click)="sortData()" class="bi bi-caret-up"></i>
+            </div>
+            <div class="align-self-center" *ngIf="!ascendingSort">
+              <i (click)="sortData()" class="bi bi-caret-down"></i>
+            </div>
+          </th>
+          <th scope="col" style="max-width: 200px; max-height: 200px">
+            Image(s)
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr *ngFor="let pitImageArr of pitImageList">
+          <th scope="row" class="align-text">
+            {{pitImageArr[0].teamNumber()}}
+          </th>
+          <td style="display: flex">
+            <div *ngFor="let pitImage of pitImageArr" class="align-images">
+              <img
+                src="/sha256/{{ pitImage.checkSum() }}/{{ pitImage.imagePath() }}"
+                style="max-width: 50px; max-height: 50px; padding: 0; margin: 0"
+              />
+            </div>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
   <!-- Driver Ranking Data Display. -->
   <div *ngSwitchCase="'DriverRanking'">
     <table class="table">