Add 2023 scouting interface

Adds a basic, functional interface to submit data scouting for
the new 2023 game using our new 'actions architecture'.

Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
Change-Id: I2c7d65e21daa6c3b9f7a647e3a808874f7b3be8a
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 6209669..9aae0d1 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -11,17 +11,25 @@
 import {Builder, ByteBuffer} from 'flatbuffers';
 import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated';
 import {
-  ClimbLevel,
-  SubmitDataScouting,
-} from '../../webserver/requests/messages/submit_data_scouting_generated';
-import {SubmitDataScoutingResponse} from '../../webserver/requests/messages/submit_data_scouting_response_generated';
+  ObjectType,
+  ScoreLevel,
+  SubmitActions,
+  StartMatchAction,
+  PickupObjectAction,
+  PlaceObjectAction,
+  RobotDeathAction,
+  EndMatchAction,
+  ActionType,
+  Action,
+} from '../../webserver/requests/messages/submit_actions_generated';
 
 type Section =
   | 'Team Selection'
-  | 'Auto'
-  | 'TeleOp'
-  | 'Climb'
-  | 'Other'
+  | 'Init'
+  | 'Pickup'
+  | 'Place'
+  | 'Endgame'
+  | 'Dead'
   | 'Review and Submit'
   | 'Success';
 
@@ -38,22 +46,42 @@
   f: 'Finals',
 };
 
-const IMAGES_ARRAY = [
-  {
-    id: 'field_quadrants_image',
-    original_image:
-      '/sha256/cbb99a057a2504e80af526dae7a0a04121aed84c56a6f4889e9576fe1c20c61e/pictures/field/quadrants.jpeg',
-    reversed_image:
-      '/sha256/ee4d24cf6b850158aa64e2b301c31411cb28f88a247a8916abb97214bb251eb5/pictures/field/reversed_quadrants.jpeg',
-  },
-  {
-    id: 'field_balls_image',
-    original_image:
-      '/sha256/e095cc8a75d804b0e2070e0a941fab37154176756d4c1a775e53cc48c3a732b9/pictures/field/balls.jpeg',
-    reversed_image:
-      '/sha256/fe4a4605c03598611c583d4dcdf28e06a056a17302ae91f5c527568966d95f3a/pictures/field/reversed_balls.jpeg',
-  },
-];
+type ActionT =
+  | {
+      type: 'startMatchAction';
+      timestamp?: number;
+      position: number;
+    }
+  | {
+      type: 'pickupObjectAction';
+      timestamp?: number;
+      objectType: ObjectType;
+      auto?: boolean;
+    }
+  | {
+      type: 'placeObjectAction';
+      timestamp?: number;
+      objectType?: ObjectType;
+      scoreLevel: ScoreLevel;
+      auto?: boolean;
+    }
+  | {
+      type: 'robotDeathAction';
+      timestamp?: number;
+      robotOn: boolean;
+    }
+  | {
+      type: 'endMatchAction';
+      docked: boolean;
+      engaged: boolean;
+      timestamp?: number;
+    }
+  | {
+      // This is not a action that is submitted,
+      // It is used for undoing purposes.
+      type: 'endAutoPhase';
+      timestamp?: number;
+    };
 
 @Component({
   selector: 'app-entry',
@@ -63,9 +91,10 @@
 export class EntryComponent {
   // Re-export the type here so that we can use it in the `[value]` attribute
   // of radio buttons.
-  readonly ClimbLevel = ClimbLevel;
   readonly COMP_LEVELS = COMP_LEVELS;
   readonly COMP_LEVEL_LABELS = COMP_LEVEL_LABELS;
+  readonly ObjectType = ObjectType;
+  readonly ScoreLevel = ScoreLevel;
 
   section: Section = 'Team Selection';
   @Output() switchTabsEvent = new EventEmitter<string>();
@@ -73,118 +102,169 @@
   @Input() teamNumber: number = 1;
   @Input() setNumber: number = 1;
   @Input() compLevel: CompLevel = 'qm';
-  autoUpperShotsMade: number = 0;
-  autoLowerShotsMade: number = 0;
-  autoShotsMissed: number = 0;
-  teleUpperShotsMade: number = 0;
-  teleLowerShotsMade: number = 0;
-  teleShotsMissed: number = 0;
-  defensePlayedOnScore: number = 0;
-  defensePlayedScore: number = 0;
-  level: ClimbLevel = ClimbLevel.NoAttempt;
-  ball1: boolean = false;
-  ball2: boolean = false;
-  ball3: boolean = false;
-  ball4: boolean = false;
-  ball5: boolean = false;
-  quadrant: number = 1;
+
+  actionList: ActionT[] = [];
   errorMessage: string = '';
-  noShow: boolean = false;
-  neverMoved: boolean = false;
-  batteryDied: boolean = false;
-  mechanicallyBroke: boolean = false;
-  lostComs: boolean = false;
-  comment: string = '';
+  autoPhase: boolean = true;
+  lastObject: ObjectType = null;
+
+  matchStartTimestamp: number = 0;
+
+  addAction(action: ActionT): void {
+    action.timestamp = Math.floor(Date.now() / 1000);
+    if (action.type == 'startMatchAction') {
+      // Unix nanosecond timestamp.
+      this.matchStartTimestamp = Date.now() * 1e6;
+      action.timestamp = 0;
+    } else {
+      // Unix nanosecond timestamp relative to match start.
+      action.timestamp = Date.now() * 1e6 - this.matchStartTimestamp;
+    }
+
+    if (
+      action.type == 'pickupObjectAction' ||
+      action.type == 'placeObjectAction'
+    ) {
+      action.auto = this.autoPhase;
+      if (action.type == 'pickupObjectAction') {
+        this.lastObject = action.objectType;
+      } else if (action.type == 'placeObjectAction') {
+        action.objectType = this.lastObject;
+      }
+    }
+    this.actionList.push(action);
+  }
+
+  undoLastAction() {
+    if (this.actionList.length > 0) {
+      let lastAction = this.actionList.pop();
+      switch (lastAction?.type) {
+        case 'endAutoPhase':
+          this.autoPhase = true;
+        case 'pickupObjectAction':
+          this.section = 'Pickup';
+          break;
+        case 'placeObjectAction':
+          this.section = 'Place';
+          break;
+        case 'endMatchAction':
+          this.section = 'Pickup';
+          break;
+        default:
+          break;
+      }
+    }
+  }
+
+  changeSectionTo(target: Section) {
+    this.section = target;
+  }
 
   @ViewChild('header') header: ElementRef;
 
-  nextSection() {
-    if (this.section === 'Team Selection') {
-      this.section = 'Auto';
-    } else if (this.section === 'Auto') {
-      this.section = 'TeleOp';
-    } else if (this.section === 'TeleOp') {
-      this.section = 'Climb';
-    } else if (this.section === 'Climb') {
-      this.section = 'Other';
-    } else if (this.section === 'Other') {
-      this.section = 'Review and Submit';
-    } else if (this.section === 'Review and Submit') {
-      this.submitDataScouting();
-      return;
-    } else if (this.section === 'Success') {
-      this.switchTabsEvent.emit('MatchList');
-      return;
-    }
-    // Scroll back to the top so that we can be sure the user sees the
-    // entire next screen. Otherwise it's easy to overlook input fields.
-    this.scrollToTop();
-  }
-
-  prevSection() {
-    if (this.section === 'Auto') {
-      this.section = 'Team Selection';
-    } else if (this.section === 'TeleOp') {
-      this.section = 'Auto';
-    } else if (this.section === 'Climb') {
-      this.section = 'TeleOp';
-    } else if (this.section === 'Other') {
-      this.section = 'Climb';
-    } else if (this.section === 'Review and Submit') {
-      this.section = 'Other';
-    }
-    // Scroll back to the top so that we can be sure the user sees the
-    // entire previous screen. Otherwise it's easy to overlook input
-    // fields.
-    this.scrollToTop();
-  }
-
-  flipImages() {
-    for (let obj of IMAGES_ARRAY) {
-      let img = document.getElementById(obj.id) as HTMLImageElement;
-      img.src = img.src.endsWith(obj.original_image)
-        ? obj.reversed_image
-        : obj.original_image;
-    }
-  }
   private scrollToTop() {
     this.header.nativeElement.scrollIntoView();
   }
 
-  async submitDataScouting() {
-    this.errorMessage = '';
-
+  async submitActions() {
     const builder = new Builder();
-    const compLevel = builder.createString(this.compLevel);
-    const comment = builder.createString(this.comment);
-    SubmitDataScouting.startSubmitDataScouting(builder);
-    SubmitDataScouting.addTeam(builder, this.teamNumber);
-    SubmitDataScouting.addMatch(builder, this.matchNumber);
-    SubmitDataScouting.addSetNumber(builder, this.setNumber);
-    SubmitDataScouting.addCompLevel(builder, compLevel);
-    SubmitDataScouting.addMissedShotsAuto(builder, this.autoShotsMissed);
-    SubmitDataScouting.addUpperGoalAuto(builder, this.autoUpperShotsMade);
-    SubmitDataScouting.addLowerGoalAuto(builder, this.autoLowerShotsMade);
-    SubmitDataScouting.addMissedShotsTele(builder, this.teleShotsMissed);
-    SubmitDataScouting.addUpperGoalTele(builder, this.teleUpperShotsMade);
-    SubmitDataScouting.addLowerGoalTele(builder, this.teleLowerShotsMade);
-    SubmitDataScouting.addDefenseRating(builder, this.defensePlayedScore);
-    SubmitDataScouting.addDefenseReceivedRating(
+    const actionOffsets: number[] = [];
+
+    for (const action of this.actionList) {
+      let actionOffset: number | undefined;
+      console.log(action.type);
+
+      switch (action.type) {
+        case 'startMatchAction':
+          const startMatchActionOffset =
+            StartMatchAction.createStartMatchAction(builder, action.position);
+          actionOffset = Action.createAction(
+            builder,
+            action.timestamp || 0,
+            ActionType.StartMatchAction,
+            startMatchActionOffset
+          );
+          break;
+
+        case 'pickupObjectAction':
+          const pickupObjectActionOffset =
+            PickupObjectAction.createPickupObjectAction(
+              builder,
+              action.objectType,
+              action.auto || false
+            );
+          actionOffset = Action.createAction(
+            builder,
+            action.timestamp || 0,
+            ActionType.PickupObjectAction,
+            pickupObjectActionOffset
+          );
+          break;
+
+        case 'placeObjectAction':
+          const placeObjectActionOffset =
+            PlaceObjectAction.createPlaceObjectAction(
+              builder,
+              action.objectType,
+              action.scoreLevel,
+              action.auto || false
+            );
+          actionOffset = Action.createAction(
+            builder,
+            action.timestamp || 0,
+            ActionType.PlaceObjectAction,
+            placeObjectActionOffset
+          );
+          break;
+
+        case 'robotDeathAction':
+          const robotDeathActionOffset =
+            RobotDeathAction.createRobotDeathAction(builder, action.robotOn);
+          actionOffset = Action.createAction(
+            builder,
+            action.timestamp || 0,
+            ActionType.RobotDeathAction,
+            robotDeathActionOffset
+          );
+          break;
+
+        case 'endMatchAction':
+          const endMatchActionOffset = EndMatchAction.createEndMatchAction(
+            builder,
+            action.docked,
+            action.engaged
+          );
+          actionOffset = Action.createAction(
+            builder,
+            action.timestamp || 0,
+            ActionType.EndMatchAction,
+            endMatchActionOffset
+          );
+          break;
+
+        case 'endAutoPhase':
+          // Not important action.
+          break;
+
+        default:
+          throw new Error(`Unknown action type`);
+      }
+
+      if (actionOffset !== undefined) {
+        actionOffsets.push(actionOffset);
+      }
+    }
+
+    const actionsVector = SubmitActions.createActionsListVector(
       builder,
-      this.defensePlayedOnScore
+      actionOffsets
     );
-    SubmitDataScouting.addAutoBall1(builder, this.ball1);
-    SubmitDataScouting.addAutoBall2(builder, this.ball2);
-    SubmitDataScouting.addAutoBall3(builder, this.ball3);
-    SubmitDataScouting.addAutoBall4(builder, this.ball4);
-    SubmitDataScouting.addAutoBall5(builder, this.ball5);
-    SubmitDataScouting.addStartingQuadrant(builder, this.quadrant);
-    SubmitDataScouting.addClimbLevel(builder, this.level);
-    SubmitDataScouting.addComment(builder, comment);
-    builder.finish(SubmitDataScouting.endSubmitDataScouting(builder));
+    SubmitActions.startSubmitActions(builder);
+    SubmitActions.addActionsList(builder, actionsVector);
+    builder.finish(SubmitActions.endSubmitActions(builder));
 
     const buffer = builder.asUint8Array();
-    const res = await fetch('/requests/submit/data_scouting', {
+    const res = await fetch('/requests/submit/actions', {
       method: 'POST',
       body: buffer,
     });
@@ -192,6 +272,7 @@
     if (res.ok) {
       // We successfully submitted the data. Report success.
       this.section = 'Success';
+      this.actionList = [];
     } else {
       const resBuffer = await res.arrayBuffer();
       const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));