Merge "Fix auto-aiming when turret range doesn't include [-pi, +pi]"
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 98cc788..d9bebc9 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -21,7 +21,10 @@
}
type Stats struct {
- TeamNumber, MatchNumber int32
+ TeamNumber, MatchNumber int32
+ StartingQuadrant int32
+ AutoBallPickedUp [5]bool
+ // TODO(phil): Re-order auto and teleop fields so auto comes first.
ShotsMissed, UpperGoalShots, LowerGoalShots int32
ShotsMissedAuto, UpperGoalAuto, LowerGoalAuto, PlayedDefense int32
Climbing int32
@@ -80,6 +83,12 @@
"id SERIAL PRIMARY KEY, " +
"TeamNumber INTEGER, " +
"MatchNumber INTEGER, " +
+ "StartingQuadrant INTEGER, " +
+ "AutoBall1PickedUp BOOLEAN, " +
+ "AutoBall2PickedUp BOOLEAN, " +
+ "AutoBall3PickedUp BOOLEAN, " +
+ "AutoBall4PickedUp BOOLEAN, " +
+ "AutoBall5PickedUp BOOLEAN, " +
"ShotsMissed INTEGER, " +
"UpperGoalShots INTEGER, " +
"LowerGoalShots INTEGER, " +
@@ -152,14 +161,20 @@
func (database *Database) AddToMatch(m Match) error {
statement, err := database.Prepare("INSERT INTO team_match_stats(" +
"TeamNumber, MatchNumber, " +
+ "StartingQuadrant, " +
+ "AutoBall1PickedUp, AutoBall2PickedUp, AutoBall3PickedUp, " +
+ "AutoBall4PickedUp, AutoBall5PickedUp, " +
"ShotsMissed, UpperGoalShots, LowerGoalShots, " +
"ShotsMissedAuto, UpperGoalAuto, LowerGoalAuto, " +
"PlayedDefense, Climbing, CollectedBy) " +
"VALUES (" +
"$1, $2, " +
- "$3, $4, $5, " +
- "$6, $7, $8, " +
- "$9, $10, $11) " +
+ "$3, " +
+ "$4, $5, $6, " +
+ "$7, $8, " +
+ "$9, $10, $11, " +
+ "$12, $13, $14, " +
+ "$15, $16, $17) " +
"RETURNING id")
if err != nil {
return errors.New(fmt.Sprint("Failed to prepare insertion into stats database: ", err))
@@ -168,7 +183,14 @@
var rowIds [6]int64
for i, TeamNumber := range []int32{m.R1, m.R2, m.R3, m.B1, m.B2, m.B3} {
- row := statement.QueryRow(TeamNumber, m.MatchNumber, 0, 0, 0, 0, 0, 0, 0, 0, "")
+ row := statement.QueryRow(
+ TeamNumber, m.MatchNumber,
+ 0,
+ false, false, false,
+ false, false,
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, "")
err = row.Scan(&rowIds[i])
if err != nil {
return errors.New(fmt.Sprint("Failed to insert stats: ", err))
@@ -200,16 +222,23 @@
func (database *Database) AddToStats(s Stats) error {
statement, err := database.Prepare("UPDATE team_match_stats SET " +
"TeamNumber = $1, MatchNumber = $2, " +
- "ShotsMissed = $3, UpperGoalShots = $4, LowerGoalShots = $5, " +
- "ShotsMissedAuto = $6, UpperGoalAuto = $7, LowerGoalAuto = $8, " +
- "PlayedDefense = $9, Climbing = $10, CollectedBy = $11 " +
- "WHERE MatchNumber = $12 AND TeamNumber = $13")
+ "StartingQuadrant = $3, " +
+ "AutoBall1PickedUp = $4, AutoBall2PickedUp = $5, AutoBall3PickedUp = $6, " +
+ "AutoBall4PickedUp = $7, AutoBall5PickedUp = $8, " +
+ "ShotsMissed = $9, UpperGoalShots = $10, LowerGoalShots = $11, " +
+ "ShotsMissedAuto = $12, UpperGoalAuto = $13, LowerGoalAuto = $14, " +
+ "PlayedDefense = $15, Climbing = $16, CollectedBy = $17 " +
+ "WHERE MatchNumber = $18 AND TeamNumber = $19")
if err != nil {
return errors.New(fmt.Sprint("Failed to prepare stats update statement: ", err))
}
defer statement.Close()
- result, err := statement.Exec(s.TeamNumber, s.MatchNumber,
+ result, err := statement.Exec(
+ s.TeamNumber, s.MatchNumber,
+ s.StartingQuadrant,
+ s.AutoBallPickedUp[0], s.AutoBallPickedUp[1], s.AutoBallPickedUp[2],
+ s.AutoBallPickedUp[3], s.AutoBallPickedUp[4],
s.ShotsMissed, s.UpperGoalShots, s.LowerGoalShots,
s.ShotsMissedAuto, s.UpperGoalAuto, s.LowerGoalAuto,
s.PlayedDefense, s.Climbing, s.CollectedBy,
@@ -263,7 +292,11 @@
for rows.Next() {
var team Stats
var id int
- err = rows.Scan(&id, &team.TeamNumber, &team.MatchNumber,
+ err = rows.Scan(&id,
+ &team.TeamNumber, &team.MatchNumber,
+ &team.StartingQuadrant,
+ &team.AutoBallPickedUp[0], &team.AutoBallPickedUp[1], &team.AutoBallPickedUp[2],
+ &team.AutoBallPickedUp[3], &team.AutoBallPickedUp[4],
&team.ShotsMissed, &team.UpperGoalShots, &team.LowerGoalShots,
&team.ShotsMissedAuto, &team.UpperGoalAuto, &team.LowerGoalAuto,
&team.PlayedDefense, &team.Climbing, &team.CollectedBy)
@@ -310,7 +343,11 @@
for rows.Next() {
var team Stats
var id int
- err = rows.Scan(&id, &team.TeamNumber, &team.MatchNumber,
+ err = rows.Scan(&id,
+ &team.TeamNumber, &team.MatchNumber,
+ &team.StartingQuadrant,
+ &team.AutoBallPickedUp[0], &team.AutoBallPickedUp[1], &team.AutoBallPickedUp[2],
+ &team.AutoBallPickedUp[3], &team.AutoBallPickedUp[4],
&team.ShotsMissed, &team.UpperGoalShots, &team.LowerGoalShots,
&team.ShotsMissedAuto, &team.UpperGoalAuto, &team.LowerGoalAuto,
&team.PlayedDefense, &team.Climbing, &team.CollectedBy)
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index b06706d..c5bea5e 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -89,42 +89,54 @@
correct := []Stats{
Stats{
TeamNumber: 1236, MatchNumber: 7,
- ShotsMissed: 9, UpperGoalShots: 5, LowerGoalShots: 4,
+ StartingQuadrant: 2,
+ AutoBallPickedUp: [5]bool{false, false, false, true, false},
+ ShotsMissed: 9, UpperGoalShots: 5, LowerGoalShots: 4,
ShotsMissedAuto: 3, UpperGoalAuto: 2, LowerGoalAuto: 1,
PlayedDefense: 2, Climbing: 3,
CollectedBy: "josh",
},
Stats{
TeamNumber: 1001, MatchNumber: 7,
- ShotsMissed: 6, UpperGoalShots: 9, LowerGoalShots: 9,
+ StartingQuadrant: 3,
+ AutoBallPickedUp: [5]bool{true, false, true, true, false},
+ ShotsMissed: 6, UpperGoalShots: 9, LowerGoalShots: 9,
ShotsMissedAuto: 0, UpperGoalAuto: 0, LowerGoalAuto: 0,
PlayedDefense: 0, Climbing: 0,
CollectedBy: "rupert",
},
Stats{
TeamNumber: 777, MatchNumber: 7,
- ShotsMissed: 5, UpperGoalShots: 7, LowerGoalShots: 12,
+ StartingQuadrant: 4,
+ AutoBallPickedUp: [5]bool{false, true, true, true, false},
+ ShotsMissed: 5, UpperGoalShots: 7, LowerGoalShots: 12,
ShotsMissedAuto: 0, UpperGoalAuto: 4, LowerGoalAuto: 0,
PlayedDefense: 0, Climbing: 0,
CollectedBy: "felix",
},
Stats{
TeamNumber: 1000, MatchNumber: 7,
- ShotsMissed: 12, UpperGoalShots: 6, LowerGoalShots: 10,
+ StartingQuadrant: 1,
+ AutoBallPickedUp: [5]bool{false, false, false, false, false},
+ ShotsMissed: 12, UpperGoalShots: 6, LowerGoalShots: 10,
ShotsMissedAuto: 0, UpperGoalAuto: 7, LowerGoalAuto: 0,
PlayedDefense: 0, Climbing: 0,
CollectedBy: "thea",
},
Stats{
TeamNumber: 4321, MatchNumber: 7,
- ShotsMissed: 14, UpperGoalShots: 12, LowerGoalShots: 3,
+ StartingQuadrant: 2,
+ AutoBallPickedUp: [5]bool{true, false, false, false, false},
+ ShotsMissed: 14, UpperGoalShots: 12, LowerGoalShots: 3,
ShotsMissedAuto: 0, UpperGoalAuto: 7, LowerGoalAuto: 0,
PlayedDefense: 0, Climbing: 0,
CollectedBy: "amy",
},
Stats{
TeamNumber: 1234, MatchNumber: 7,
- ShotsMissed: 3, UpperGoalShots: 4, LowerGoalShots: 0,
+ StartingQuadrant: 3,
+ AutoBallPickedUp: [5]bool{false, false, false, false, true},
+ ShotsMissed: 3, UpperGoalShots: 4, LowerGoalShots: 0,
ShotsMissedAuto: 0, UpperGoalAuto: 9, LowerGoalAuto: 0,
PlayedDefense: 0, Climbing: 0,
CollectedBy: "beth",
@@ -195,36 +207,48 @@
testDatabase := []Stats{
Stats{
TeamNumber: 1235, MatchNumber: 94,
- ShotsMissed: 2, UpperGoalShots: 2, LowerGoalShots: 2,
+ StartingQuadrant: 1,
+ AutoBallPickedUp: [5]bool{false, false, false, false, false},
+ ShotsMissed: 2, UpperGoalShots: 2, LowerGoalShots: 2,
ShotsMissedAuto: 2, UpperGoalAuto: 2, LowerGoalAuto: 2,
PlayedDefense: 2, Climbing: 2},
Stats{
TeamNumber: 1234, MatchNumber: 94,
- ShotsMissed: 4, UpperGoalShots: 4, LowerGoalShots: 4,
+ StartingQuadrant: 2,
+ AutoBallPickedUp: [5]bool{false, false, false, false, true},
+ ShotsMissed: 4, UpperGoalShots: 4, LowerGoalShots: 4,
ShotsMissedAuto: 4, UpperGoalAuto: 4, LowerGoalAuto: 4,
PlayedDefense: 7, Climbing: 2,
},
Stats{
TeamNumber: 1233, MatchNumber: 94,
- ShotsMissed: 3, UpperGoalShots: 3, LowerGoalShots: 3,
+ StartingQuadrant: 3,
+ AutoBallPickedUp: [5]bool{false, false, false, false, false},
+ ShotsMissed: 3, UpperGoalShots: 3, LowerGoalShots: 3,
ShotsMissedAuto: 3, UpperGoalAuto: 3, LowerGoalAuto: 3,
PlayedDefense: 3, Climbing: 3,
},
Stats{
TeamNumber: 1232, MatchNumber: 94,
- ShotsMissed: 5, UpperGoalShots: 5, LowerGoalShots: 5,
+ StartingQuadrant: 2,
+ AutoBallPickedUp: [5]bool{true, false, false, false, true},
+ ShotsMissed: 5, UpperGoalShots: 5, LowerGoalShots: 5,
ShotsMissedAuto: 5, UpperGoalAuto: 5, LowerGoalAuto: 5,
PlayedDefense: 7, Climbing: 1,
},
Stats{
TeamNumber: 1231, MatchNumber: 94,
- ShotsMissed: 6, UpperGoalShots: 6, LowerGoalShots: 6,
+ StartingQuadrant: 3,
+ AutoBallPickedUp: [5]bool{false, false, true, false, false},
+ ShotsMissed: 6, UpperGoalShots: 6, LowerGoalShots: 6,
ShotsMissedAuto: 6, UpperGoalAuto: 6, LowerGoalAuto: 6,
PlayedDefense: 7, Climbing: 1,
},
Stats{
TeamNumber: 1239, MatchNumber: 94,
- ShotsMissed: 7, UpperGoalShots: 7, LowerGoalShots: 7,
+ StartingQuadrant: 4,
+ AutoBallPickedUp: [5]bool{false, true, true, false, false},
+ ShotsMissed: 7, UpperGoalShots: 7, LowerGoalShots: 7,
ShotsMissedAuto: 7, UpperGoalAuto: 7, LowerGoalAuto: 3,
PlayedDefense: 7, Climbing: 1,
},
@@ -237,13 +261,15 @@
for i := 0; i < len(testDatabase); i++ {
err = fixture.db.AddToStats(testDatabase[i])
- check(t, err, fmt.Sprint("Failed to add stats", i))
+ check(t, err, fmt.Sprint("Failed to add stats ", i))
}
correct := []Stats{
Stats{
TeamNumber: 1235, MatchNumber: 94,
- ShotsMissed: 2, UpperGoalShots: 2, LowerGoalShots: 2,
+ StartingQuadrant: 1,
+ AutoBallPickedUp: [5]bool{false, false, false, false, false},
+ ShotsMissed: 2, UpperGoalShots: 2, LowerGoalShots: 2,
ShotsMissedAuto: 2, UpperGoalAuto: 2, LowerGoalAuto: 2,
PlayedDefense: 2, Climbing: 2,
},
@@ -309,37 +335,48 @@
correct := []Stats{
Stats{
TeamNumber: 1235, MatchNumber: 94,
- ShotsMissed: 2, UpperGoalShots: 2, LowerGoalShots: 2,
+ StartingQuadrant: 1,
+ AutoBallPickedUp: [5]bool{false, false, false, false, false},
+ ShotsMissed: 2, UpperGoalShots: 2, LowerGoalShots: 2,
ShotsMissedAuto: 2, UpperGoalAuto: 2, LowerGoalAuto: 2,
- PlayedDefense: 2, Climbing: 2,
- },
+ PlayedDefense: 2, Climbing: 2},
Stats{
TeamNumber: 1236, MatchNumber: 94,
- ShotsMissed: 4, UpperGoalShots: 4, LowerGoalShots: 4,
+ StartingQuadrant: 2,
+ AutoBallPickedUp: [5]bool{false, false, false, false, true},
+ ShotsMissed: 4, UpperGoalShots: 4, LowerGoalShots: 4,
ShotsMissedAuto: 4, UpperGoalAuto: 4, LowerGoalAuto: 4,
PlayedDefense: 7, Climbing: 2,
},
Stats{
TeamNumber: 1237, MatchNumber: 94,
- ShotsMissed: 3, UpperGoalShots: 3, LowerGoalShots: 3,
+ StartingQuadrant: 3,
+ AutoBallPickedUp: [5]bool{false, false, false, false, false},
+ ShotsMissed: 3, UpperGoalShots: 3, LowerGoalShots: 3,
ShotsMissedAuto: 3, UpperGoalAuto: 3, LowerGoalAuto: 3,
PlayedDefense: 3, Climbing: 3,
},
Stats{
TeamNumber: 1238, MatchNumber: 94,
- ShotsMissed: 5, UpperGoalShots: 5, LowerGoalShots: 5,
+ StartingQuadrant: 2,
+ AutoBallPickedUp: [5]bool{true, false, false, false, true},
+ ShotsMissed: 5, UpperGoalShots: 5, LowerGoalShots: 5,
ShotsMissedAuto: 5, UpperGoalAuto: 5, LowerGoalAuto: 5,
PlayedDefense: 7, Climbing: 1,
},
Stats{
TeamNumber: 1239, MatchNumber: 94,
- ShotsMissed: 6, UpperGoalShots: 6, LowerGoalShots: 6,
+ StartingQuadrant: 3,
+ AutoBallPickedUp: [5]bool{false, false, true, false, false},
+ ShotsMissed: 6, UpperGoalShots: 6, LowerGoalShots: 6,
ShotsMissedAuto: 6, UpperGoalAuto: 6, LowerGoalAuto: 6,
PlayedDefense: 7, Climbing: 1,
},
Stats{
TeamNumber: 1233, MatchNumber: 94,
- ShotsMissed: 7, UpperGoalShots: 7, LowerGoalShots: 7,
+ StartingQuadrant: 4,
+ AutoBallPickedUp: [5]bool{false, true, true, false, false},
+ ShotsMissed: 7, UpperGoalShots: 7, LowerGoalShots: 7,
ShotsMissedAuto: 7, UpperGoalAuto: 7, LowerGoalAuto: 3,
PlayedDefense: 7, Climbing: 1,
},
@@ -352,7 +389,7 @@
for i := 0; i < len(correct); i++ {
err = fixture.db.AddToStats(correct[i])
- check(t, err, fmt.Sprint("Failed to add stats", i))
+ check(t, err, fmt.Sprint("Failed to add stats ", i))
}
got, err := fixture.db.ReturnStats()
diff --git a/scouting/scouting_test.ts b/scouting/scouting_test.ts
index db0c9fd..84824ff 100644
--- a/scouting/scouting_test.ts
+++ b/scouting/scouting_test.ts
@@ -1,22 +1,39 @@
import {browser, by, element, protractor} from 'protractor';
+const EC = protractor.ExpectedConditions;
+
// Loads the page (or reloads it) and deals with the "Are you sure you want to
// leave this page" popup.
async function loadPage() {
- await browser.get(browser.baseUrl).catch(function () {
- return browser.switchTo().alert().then(function (alert) {
- alert.accept();
- return browser.get(browser.baseUrl);
- });
- });
+ await disableAlerts();
+ await browser.navigate().refresh();
+ expect((await browser.getTitle())).toEqual('FRC971 Scouting Application');
+ await disableAlerts();
}
+// Disables alert popups. They are extremely tedious to deal with in
+// Protractor since they're not angular elements. We achieve this by checking
+// an invisible checkbox that's off-screen.
+async function disableAlerts() {
+ await browser.executeAsyncScript(function(callback) {
+ let block_alerts =
+ document.getElementById('block_alerts') as HTMLInputElement;
+ block_alerts.checked = true;
+ callback();
+ });
+}
// Returns the contents of the header that displays the "Auto", "TeleOp", and
// "Climb" labels etc.
function getHeadingText() {
return element(by.css('.header')).getText();
}
+// Returns the currently displayed progress message on the screen. This only
+// exists on screens where the web page interacts with the web server.
+function getProgressMessage() {
+ return element(by.css('.progress_message')).getText();
+}
+
// Returns the currently displayed error message on the screen. This only
// exists on screens where the web page interacts with the web server.
function getErrorMessage() {
@@ -31,25 +48,79 @@
// Asserts that the n'th instance of a field on the "Submit and Review"
// screen has a specific value.
-async function expectNthReviewFieldToBe(fieldName: string, n: number, expectedValue: string) {
- expect(await element.all(by.cssContainingText('li', `${fieldName}:`)).get(n).getText())
+async function expectNthReviewFieldToBe(
+ fieldName: string, n: number, expectedValue: string) {
+ expect(await element.all(by.cssContainingText('li', `${fieldName}:`))
+ .get(n)
+ .getText())
.toEqual(`${fieldName}: ${expectedValue}`);
}
+// Sets a text field to the specified value.
+function setTextboxByIdTo(id: string, value: string) {
+ // Just sending "value" to the input fields is insufficient. We need to
+ // overwrite the text that is there. If we didn't hit CTRL-A to select all
+ // the text, we'd be appending to whatever is there already.
+ return element(by.id(id)).sendKeys(
+ protractor.Key.CONTROL, 'a', protractor.Key.NULL, value);
+}
+
describe('The scouting web page', () => {
+ beforeAll(async () => {
+ await browser.get(browser.baseUrl);
+ expect((await browser.getTitle())).toEqual('FRC971 Scouting Application');
+ await disableAlerts();
+
+ // Import the match list before running any tests. Ideally this should be
+ // run in beforeEach(), but it's not worth doing that at this time. Our
+ // tests are basic enough not to require this.
+ await element(by.cssContainingText('.nav-link', 'Import Match List'))
+ .click();
+ expect(await getHeadingText()).toEqual('Import Match List');
+ await setTextboxByIdTo('year', '2016');
+ await setTextboxByIdTo('event_code', 'nytr');
+ await element(by.buttonText('Import')).click();
+
+ await browser.wait(EC.textToBePresentInElement(
+ element(by.css('.progress_message')),
+ 'Successfully imported match list.'));
+ });
+
+ it('should: error on unknown match.', async () => {
+ await loadPage();
+
+ await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
+
+ // Pick a match that doesn't exist in the 2016nytr match list.
+ await setTextboxByIdTo('match_number', '3');
+ await setTextboxByIdTo('team_number', '971');
+
+ // Click Next until we get to the submit screen.
+ for (let i = 0; i < 5; i++) {
+ await element(by.buttonText('Next')).click();
+ }
+ expect(await getHeadingText()).toEqual('Review and Submit');
+
+ // Attempt to submit and validate the error.
+ await element(by.buttonText('Submit')).click();
+ expect(await getErrorMessage())
+ .toContain('Failed to find team 971 in match 3 in the schedule.');
+ });
+
+
it('should: review and submit correct data.', async () => {
await loadPage();
+ await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
+
+ // Submit scouting data for a random team that attended 2016nytr.
expect(await getHeadingText()).toEqual('Team Selection');
- // Just sending "971" to the input fields is insufficient. We need to
- // overwrite the text that is there. If we didn't hit CTRL-A to select all
- // the text, we'd be appending to whatever is there already.
- await element(by.id('team_number')).sendKeys(
- protractor.Key.CONTROL, 'a', protractor.Key.NULL,
- '971');
+ await setTextboxByIdTo('match_number', '2');
+ await setTextboxByIdTo('team_number', '5254');
await element(by.buttonText('Next')).click();
expect(await getHeadingText()).toEqual('Auto');
+ await element(by.id('quadrant3')).click();
await element(by.buttonText('Next')).click();
expect(await getHeadingText()).toEqual('TeleOp');
@@ -68,13 +139,14 @@
expect(await getErrorMessage()).toEqual('');
// Validate Team Selection.
- await expectReviewFieldToBe('Match number', '1');
- await expectReviewFieldToBe('Team number', '971');
+ await expectReviewFieldToBe('Match number', '2');
+ await expectReviewFieldToBe('Team number', '5254');
// Validate Auto.
await expectNthReviewFieldToBe('Upper Shots Made', 0, '0');
await expectNthReviewFieldToBe('Lower Shots Made', 0, '0');
await expectNthReviewFieldToBe('Missed Shots', 0, '0');
+ await expectReviewFieldToBe('Quadrant', '3');
// Validate TeleOp.
await expectNthReviewFieldToBe('Upper Shots Made', 1, '0');
@@ -92,31 +164,37 @@
await expectReviewFieldToBe('Battery died', 'false');
await expectReviewFieldToBe('Broke (mechanically)', 'true');
- // TODO(phil): Submit data and make sure it made its way to the database
- // correctly. Right now the /requests/submit/data_scouting endpoint is not
- // implemented.
+ await element(by.buttonText('Submit')).click();
+ await browser.wait(
+ EC.textToBePresentInElement(element(by.css('.header')), 'Success'));
+
+ // TODO(phil): Make sure the data made its way to the database correctly.
});
it('should: load all images successfully.', async () => {
await loadPage();
+ await element(by.cssContainingText('.nav-link', 'Data Entry')).click();
+
// Get to the Auto display with the field pictures.
expect(await getHeadingText()).toEqual('Team Selection');
await element(by.buttonText('Next')).click();
expect(await getHeadingText()).toEqual('Auto');
// We expect 2 fully loaded images.
- browser.executeAsyncScript(function (callback) {
- let images = document.getElementsByTagName('img');
- let numLoaded = 0;
- for (let i = 0; i < images.length; i += 1) {
- if (images[i].naturalWidth > 0) {
- numLoaded += 1;
- }
- }
- callback(numLoaded);
- }).then(function (numLoaded) {
- expect(numLoaded).toBe(2);
- });
+ browser
+ .executeAsyncScript(function(callback) {
+ let images = document.getElementsByTagName('img');
+ let numLoaded = 0;
+ for (let i = 0; i < images.length; i += 1) {
+ if (images[i].naturalWidth > 0) {
+ numLoaded += 1;
+ }
+ }
+ callback(numLoaded);
+ })
+ .then(function(numLoaded) {
+ expect(numLoaded).toBe(2);
+ });
});
});
diff --git a/scouting/webserver/requests/debug/cli/cli_test.py b/scouting/webserver/requests/debug/cli/cli_test.py
index b20703b..8bebc69 100644
--- a/scouting/webserver/requests/debug/cli/cli_test.py
+++ b/scouting/webserver/requests/debug/cli/cli_test.py
@@ -62,6 +62,12 @@
json_path = write_json_request({
"team": 100,
"match": 1,
+ "starting_quadrant": 3,
+ "auto_ball_1": True,
+ "auto_ball_2": False,
+ "auto_ball_3": False,
+ "auto_ball_4": False,
+ "auto_ball_5": True,
"missed_shots_auto": 10,
"upper_goal_auto": 11,
"lower_goal_auto": 12,
@@ -92,7 +98,13 @@
LowerGoalTele: (int32) 15,
DefenseRating: (int32) 3,
Climbing: (int32) 1,
- CollectedBy: (string) (len=9) "debug_cli"
+ CollectedBy: (string) (len=9) "debug_cli",
+ AutoBall1: (bool) true,
+ AutoBall2: (bool) false,
+ AutoBall3: (bool) false,
+ AutoBall4: (bool) false,
+ AutoBall5: (bool) true,
+ StartingQuadrant: (int32) 3
}"""), stdout)
def test_request_all_matches(self):
diff --git a/scouting/webserver/requests/messages/request_data_scouting_response.fbs b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
index 5703a07..7d91cb7 100644
--- a/scouting/webserver/requests/messages/request_data_scouting_response.fbs
+++ b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
@@ -3,6 +3,7 @@
table Stats {
team:int (id: 0);
match:int (id: 1);
+
missed_shots_auto:int (id: 2);
upper_goal_auto:int (id:3);
lower_goal_auto:int (id:4);
@@ -12,6 +13,15 @@
defense_rating:int (id:8);
climbing:int (id:9);
collected_by:string (id:10);
+
+ auto_ball_1:bool (id:11);
+ auto_ball_2:bool (id:12);
+ auto_ball_3:bool (id:13);
+ auto_ball_4:bool (id:14);
+ auto_ball_5:bool (id:15);
+ // The quadrant that the robot starts in for autonomous mode.
+ // Valid values are 1 through 4.
+ starting_quadrant: int (id:16);
}
table RequestDataScoutingResponse {
diff --git a/scouting/webserver/requests/messages/submit_data_scouting.fbs b/scouting/webserver/requests/messages/submit_data_scouting.fbs
index 755e70b..d3d87e2 100644
--- a/scouting/webserver/requests/messages/submit_data_scouting.fbs
+++ b/scouting/webserver/requests/messages/submit_data_scouting.fbs
@@ -21,6 +21,14 @@
// The rating that this robot gets for its climbing.
// TODO: Change into an enum to make the different values self-documenting.
climbing:int (id:9);
+ auto_ball_1:bool (id:11);
+ auto_ball_2:bool (id:12);
+ auto_ball_3:bool (id:13);
+ auto_ball_4:bool (id:14);
+ auto_ball_5:bool (id:15);
+ // The quadrant that the robot starts in for autonomous mode.
+ // Valid values are 1 through 4.
+ starting_quadrant: int (id:16);
}
root_type SubmitDataScouting;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 5a469b5..13d7396 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -142,8 +142,13 @@
}
stats := db.Stats{
- TeamNumber: request.Team(),
- MatchNumber: request.Match(),
+ TeamNumber: request.Team(),
+ MatchNumber: request.Match(),
+ StartingQuadrant: request.StartingQuadrant(),
+ AutoBallPickedUp: [5]bool{
+ request.AutoBall1(), request.AutoBall2(), request.AutoBall3(),
+ request.AutoBall4(), request.AutoBall5(),
+ },
ShotsMissedAuto: request.MissedShotsAuto(),
UpperGoalAuto: request.UpperGoalAuto(),
LowerGoalAuto: request.LowerGoalAuto(),
@@ -155,9 +160,17 @@
CollectedBy: username,
}
+ // Do some error checking.
+ if stats.StartingQuadrant < 1 || stats.StartingQuadrant > 4 {
+ respondWithError(w, http.StatusBadRequest, fmt.Sprint(
+ "Invalid starting_quadrant field value of ", stats.StartingQuadrant))
+ return
+ }
+
err = handler.db.AddToStats(stats)
if err != nil {
respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit datascouting data: ", err))
+ return
}
builder := flatbuffers.NewBuilder(50 * 1024)
@@ -316,17 +329,23 @@
var response RequestDataScoutingResponseT
for _, stat := range stats {
response.StatsList = append(response.StatsList, &request_data_scouting_response.StatsT{
- Team: stat.TeamNumber,
- Match: stat.MatchNumber,
- MissedShotsAuto: stat.ShotsMissedAuto,
- UpperGoalAuto: stat.UpperGoalAuto,
- LowerGoalAuto: stat.LowerGoalAuto,
- MissedShotsTele: stat.ShotsMissed,
- UpperGoalTele: stat.UpperGoalShots,
- LowerGoalTele: stat.LowerGoalShots,
- DefenseRating: stat.PlayedDefense,
- Climbing: stat.Climbing,
- CollectedBy: stat.CollectedBy,
+ Team: stat.TeamNumber,
+ Match: stat.MatchNumber,
+ StartingQuadrant: stat.StartingQuadrant,
+ AutoBall1: stat.AutoBallPickedUp[0],
+ AutoBall2: stat.AutoBallPickedUp[1],
+ AutoBall3: stat.AutoBallPickedUp[2],
+ AutoBall4: stat.AutoBallPickedUp[3],
+ AutoBall5: stat.AutoBallPickedUp[4],
+ MissedShotsAuto: stat.ShotsMissedAuto,
+ UpperGoalAuto: stat.UpperGoalAuto,
+ LowerGoalAuto: stat.LowerGoalAuto,
+ MissedShotsTele: stat.ShotsMissed,
+ UpperGoalTele: stat.UpperGoalShots,
+ LowerGoalTele: stat.LowerGoalShots,
+ DefenseRating: stat.PlayedDefense,
+ Climbing: stat.Climbing,
+ CollectedBy: stat.CollectedBy,
})
}
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index c716b44..12d918b 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -82,16 +82,22 @@
builder := flatbuffers.NewBuilder(1024)
builder.Finish((&submit_data_scouting.SubmitDataScoutingT{
- Team: 971,
- Match: 1,
- MissedShotsAuto: 9971,
- UpperGoalAuto: 9971,
- LowerGoalAuto: 9971,
- MissedShotsTele: 9971,
- UpperGoalTele: 9971,
- LowerGoalTele: 9971,
- DefenseRating: 9971,
- Climbing: 9971,
+ Team: 971,
+ Match: 1,
+ StartingQuadrant: 2,
+ AutoBall1: true,
+ AutoBall2: false,
+ AutoBall3: false,
+ AutoBall4: false,
+ AutoBall5: false,
+ MissedShotsAuto: 9971,
+ UpperGoalAuto: 9971,
+ LowerGoalAuto: 9971,
+ MissedShotsTele: 9971,
+ UpperGoalTele: 9971,
+ LowerGoalTele: 9971,
+ DefenseRating: 9971,
+ Climbing: 9971,
}).Pack(builder))
response, err := debug.SubmitDataScouting("http://localhost:8080", builder.FinishedBytes())
@@ -221,14 +227,18 @@
stats: []db.Stats{
{
TeamNumber: 971, MatchNumber: 1,
- ShotsMissed: 1, UpperGoalShots: 2, LowerGoalShots: 3,
+ StartingQuadrant: 1,
+ AutoBallPickedUp: [5]bool{true, false, false, false, true},
+ ShotsMissed: 1, UpperGoalShots: 2, LowerGoalShots: 3,
ShotsMissedAuto: 4, UpperGoalAuto: 5, LowerGoalAuto: 6,
PlayedDefense: 7, Climbing: 8,
CollectedBy: "john",
},
{
TeamNumber: 972, MatchNumber: 1,
- ShotsMissed: 2, UpperGoalShots: 3, LowerGoalShots: 4,
+ StartingQuadrant: 2,
+ AutoBallPickedUp: [5]bool{false, false, true, false, false},
+ ShotsMissed: 2, UpperGoalShots: 3, LowerGoalShots: 4,
ShotsMissedAuto: 5, UpperGoalAuto: 6, LowerGoalAuto: 7,
PlayedDefense: 8, Climbing: 9,
CollectedBy: "andrea",
@@ -255,12 +265,18 @@
// MissedShotsTele, UpperGoalTele, LowerGoalTele,
// DefenseRating, Climbing,
// CollectedBy,
+ // AutoBall1, AutoBall2, AutoBall3,
+ // AutoBall4, AutoBall5,
+ // StartingQuadrant,
{
971, 1,
4, 5, 6,
1, 2, 3,
7, 8,
"john",
+ true, false, false,
+ false, true,
+ 1,
},
{
972, 1,
@@ -268,6 +284,9 @@
2, 3, 4,
8, 9,
"andrea",
+ false, false, true,
+ false, false,
+ 2,
},
},
}
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index f0b91f6..f386f06 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -19,6 +19,7 @@
deps = [
"//scouting/www/entry",
"//scouting/www/import_match_list",
+ "//scouting/www/match_list",
"@npm//@angular/animations",
"@npm//@angular/common",
"@npm//@angular/core",
diff --git a/scouting/www/app.ng.html b/scouting/www/app.ng.html
index 2fafceb..fc21164 100644
--- a/scouting/www/app.ng.html
+++ b/scouting/www/app.ng.html
@@ -1,13 +1,22 @@
+<!-- Hidden element for protractor to disable alerts. -->
+<form class="visually-hidden">
+ <input type="checkbox" name="block_alerts" value="1" id="block_alerts" #block_alerts>
+</form>
+
<ul class="nav nav-tabs">
<li class="nav-item">
- <a class="nav-link" [class.active]="tabIs('Entry')" (click)="switchTabTo('Entry')">Data Entry</a>
+ <a class="nav-link" [class.active]="tabIs('MatchList')" (click)="switchTabToGuarded('MatchList')">Match List</a>
</li>
<li class="nav-item">
- <a class="nav-link" [class.active]="tabIs('ImportMatchList')" (click)="switchTabTo('ImportMatchList')">Import Match List</a>
+ <a class="nav-link" [class.active]="tabIs('Entry')" (click)="switchTabToGuarded('Entry')">Data Entry</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [class.active]="tabIs('ImportMatchList')" (click)="switchTabToGuarded('ImportMatchList')">Import Match List</a>
</li>
</ul>
<ng-container [ngSwitch]="tab">
- <app-entry *ngSwitchCase="'Entry'"></app-entry>
+ <app-match-list (selectedTeamEvent)="selectTeamInMatch($event)" *ngSwitchCase="'MatchList'"></app-match-list>
+ <app-entry (switchTabsEvent)="switchTabTo($event)" [teamNumber]="selectedTeamInMatch.teamNumber" [matchNumber]="selectedTeamInMatch.matchNumber" *ngSwitchCase="'Entry'"></app-entry>
<app-import-match-list *ngSwitchCase="'ImportMatchList'"></app-import-match-list>
</ng-container>
diff --git a/scouting/www/app.ts b/scouting/www/app.ts
index 79c1094..fbde74f 100644
--- a/scouting/www/app.ts
+++ b/scouting/www/app.ts
@@ -1,6 +1,11 @@
-import {Component} from '@angular/core';
+import {Component, ElementRef, ViewChild} from '@angular/core';
-type Tab = 'Entry'|'ImportMatchList';
+type Tab = 'MatchList'|'Entry'|'ImportMatchList';
+type TeamInMatch = {
+ teamNumber: number,
+ matchNumber: number,
+ compLevel: string
+};
@Component({
selector: 'my-app',
@@ -8,14 +13,21 @@
styleUrls: ['./common.css']
})
export class App {
- tab: Tab = 'Entry';
+ selectedTeamInMatch:
+ TeamInMatch = {teamNumber: 1, matchNumber: 1, compLevel: 'qm'};
+ tab: Tab = 'MatchList';
+
+ @ViewChild('block_alerts') block_alerts: ElementRef;
constructor() {
window.addEventListener('beforeunload', (e) => {
- // Based on https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
- // This combination ensures a dialog will be shown on most browsers.
- e.preventDefault();
- e.returnValue = '';
+ if (!this.block_alerts.nativeElement.checked) {
+ // Based on
+ // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
+ // This combination ensures a dialog will be shown on most browsers.
+ e.preventDefault();
+ e.returnValue = '';
+ }
});
}
@@ -23,13 +35,24 @@
return this.tab == tab;
}
- switchTabTo(tab: Tab) {
+ selectTeamInMatch(teamInMatch: TeamInMatch) {
+ this.selectedTeamInMatch = teamInMatch;
+ this.switchTabTo('Entry');
+ }
+
+ switchTabToGuarded(tab: Tab) {
let shouldSwitch = true;
- if (tab === 'ImportMatchList') {
- shouldSwitch = window.confirm('Leave data scouting page?');
+ if (this.tab !== tab) {
+ if (!this.block_alerts.nativeElement.checked) {
+ shouldSwitch = window.confirm('Leave current page?');
+ }
}
if (shouldSwitch) {
- this.tab = tab;
+ this.switchTabTo(tab);
}
}
+
+ private switchTabTo(tab: Tab) {
+ this.tab = tab;
+ }
}
diff --git a/scouting/www/app_module.ts b/scouting/www/app_module.ts
index 2a514f2..e560bfc 100644
--- a/scouting/www/app_module.ts
+++ b/scouting/www/app_module.ts
@@ -3,6 +3,7 @@
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {EntryModule} from './entry/entry.module';
import {ImportMatchListModule} from './import_match_list/import_match_list.module';
+import {MatchListModule} from './match_list/match_list.module';
import {App} from './app';
@@ -13,6 +14,7 @@
BrowserAnimationsModule,
EntryModule,
ImportMatchListModule,
+ MatchListModule,
],
exports: [App],
bootstrap: [App],
diff --git a/scouting/www/common.css b/scouting/www/common.css
index 35e943f..43f117e 100644
--- a/scouting/www/common.css
+++ b/scouting/www/common.css
@@ -8,3 +8,10 @@
* I.e. It would be nice to keep the error message and the progress message
* aligned when they are non-empty. */
}
+
+/* Hidden from the user, but interactable from protractor. */
+.visually-hidden {
+ position: absolute !important;
+ top: -9999px !important;
+ left: -9999px !important;
+}
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index aaf23dd..d35818d 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -1,119 +1,142 @@
-import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-
+import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {FormsModule} from '@angular/forms';
import * as flatbuffer_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
import * as error_response from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
-import * as submit_data_scouting_response from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_response_generated';
import * as submit_data_scouting from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_generated';
+import * as submit_data_scouting_response from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_response_generated';
+
import SubmitDataScouting = submit_data_scouting.scouting.webserver.requests.SubmitDataScouting;
import SubmitDataScoutingResponse = submit_data_scouting_response.scouting.webserver.requests.SubmitDataScoutingResponse;
import ErrorResponse = error_response.scouting.webserver.requests.ErrorResponse;
-type Section = 'Team Selection'|'Auto'|'TeleOp'|'Climb'|'Other'|'Review and Submit'|'Home'
-type Level = 'NoAttempt'|'Failed'|'FailedWithPlentyOfTime'|'Low'|'Medium'|'High'|'Transversal'
+type Section = 'Team Selection'|'Auto'|'TeleOp'|'Climb'|'Other'|
+ 'Review and Submit'|'Success';
+type Level = 'NoAttempt'|'Failed'|'FailedWithPlentyOfTime'|'Low'|'Medium'|
+ 'High'|'Transversal';
@Component({
- selector: 'app-entry',
- templateUrl: './entry.ng.html',
- styleUrls: ['../common.css', './entry.component.css']
+ selector: 'app-entry',
+ templateUrl: './entry.ng.html',
+ styleUrls: ['../common.css', './entry.component.css']
})
export class EntryComponent {
- section: Section = 'Team Selection';
- matchNumber: number = 1
- teamNumber: number = 1
- 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: Level = 'NoAttempt';
- errorMessage: string = '';
- noShow: boolean = false;
- neverMoved: boolean = false;
- batteryDied: boolean = false;
- mechanicallyBroke: boolean = false;
- lostComs: boolean = false;
+ section: Section = 'Team Selection';
+ @Output() switchTabsEvent = new EventEmitter<string>();
+ @Input() matchNumber: number = 1;
+ @Input() teamNumber: number = 1;
+ 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: Level = 'NoAttempt';
+ ball1: boolean = false;
+ ball2: boolean = false;
+ ball3: boolean = false;
+ ball4: boolean = false;
+ ball5: boolean = false;
+ quadrant: number = 1;
+ errorMessage: string = '';
+ noShow: boolean = false;
+ neverMoved: boolean = false;
+ batteryDied: boolean = false;
+ mechanicallyBroke: boolean = false;
+ lostComs: boolean = false;
- @ViewChild("header") header: ElementRef;
+ @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;
- }
- // 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();
+ 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();
+ 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();
+ }
- private scrollToTop() {
- this.header.nativeElement.scrollIntoView();
+ private scrollToTop() {
+ this.header.nativeElement.scrollIntoView();
+ }
+
+ async submitDataScouting() {
+ this.errorMessage = '';
+
+ const builder =
+ new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
+ SubmitDataScouting.startSubmitDataScouting(builder);
+ SubmitDataScouting.addTeam(builder, this.teamNumber);
+ SubmitDataScouting.addMatch(builder, this.matchNumber);
+ 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.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);
+
+ // TODO(phil): Add support for defensePlayedOnScore.
+ // TODO(phil): Fix the Climbing score.
+ SubmitDataScouting.addClimbing(builder, 1);
+ builder.finish(SubmitDataScouting.endSubmitDataScouting(builder));
+
+ const buffer = builder.asUint8Array();
+ const res = await fetch(
+ '/requests/submit/data_scouting', {method: 'POST', body: buffer});
+
+ if (res.ok) {
+ // We successfully submitted the data. Report success.
+ this.section = 'Success';
+ } else {
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(
+ fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+ const errorMessage = parsedResponse.errorMessage();
+ this.errorMessage =
+ `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
}
-
- async submitDataScouting() {
- const builder = new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
- SubmitDataScouting.startSubmitDataScouting(builder);
- SubmitDataScouting.addTeam(builder, this.teamNumber);
- SubmitDataScouting.addMatch(builder, this.matchNumber);
- 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);
- // TODO(phil): Add support for defensePlayedOnScore.
- // TODO(phil): Fix the Climbing score.
- SubmitDataScouting.addClimbing(builder, 1);
- builder.finish(SubmitDataScouting.endSubmitDataScouting(builder));
-
- const buffer = builder.asUint8Array();
- const res = await fetch(
- '/requests/submit/data_scouting', {method: 'POST', body: buffer});
-
- if (res.ok) {
- // We successfully submitted the data. Go back to Home.
- this.section = 'Home';
- } else {
- const resBuffer = await res.arrayBuffer();
- const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
- const parsedResponse = ErrorResponse.getRootAsErrorResponse(
- fbBuffer as unknown as flatbuffers.ByteBuffer);
-
- const errorMessage = parsedResponse.errorMessage();
- this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
- }
- }
+ }
}
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index a8feaec..5849f3b 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -21,27 +21,27 @@
<div *ngSwitchCase="'Auto'" id="auto" class="container-fluid">
<div class="row">
- <img src="/pictures/field/balls.jpeg" alt="Image">
+ <img src="/pictures/field/quadrants.jpeg" alt="Quadrants Image">
<form>
- <!--Choice for each ball location-->
- <input type="checkbox" name="balls" value="1" id="ball-1"><label for="ball-1">Ball 1</label>
- <input type="checkbox" name="balls" value="2" id="ball-2"><label for="ball-2">Ball 2</label><br>
- <input type="checkbox" name="balls" value="3" id="ball-3"><label for="ball-3">Ball 3</label>
- <input type="checkbox" name="balls" value="4" id="ball-4"><label for="ball-4">Ball 4</label><br>
- <input type="checkbox" name="balls" value="5" id="ball-5"><label for="ball-5">Ball 5</label>
+ <input type="radio" [(ngModel)]="quadrant" name="quadrant" id="quadrant1" [value]="1">
+ <label for="quadrant1">Quadrant 1</label>
+ <input type="radio" [(ngModel)]="quadrant" name="quadrant" id="quadrant2" [value]="2">
+ <label for="quadrant2">Quadrant 2</label><br>
+ <input type="radio" [(ngModel)]="quadrant" name="quadrant" id="quadrant3" [value]="3">
+ <label for="quadrant3">Quadrant 3</label>
+ <input type="radio" [(ngModel)]="quadrant" name="quadrant" id="quadrant4" [value]="4">
+ <label for="quadrant4">Quadrant 4</label>
</form>
</div>
<div class="row">
- <img src="/pictures/field/quadrants.jpeg" alt="Image">
+ <img src="/pictures/field/balls.jpeg" alt="Image">
<form>
- <input type="radio" name="quadrant" id="first" value="Quadrant 1">
- <label for="first">Quadrant 1</label>
- <input type="radio" name="quadrant" id="second" value="Quadrant 2">
- <label for="second">Quadrant 2</label><br>
- <input type="radio" name="quadrant" id="third" value="Quadrant 3">
- <label for="third">Quadrant 3</label>
- <input type="radio" name="quadrant" id="fourth" value="Quadrant 4">
- <label for="fourth">Quadrant 4</label>
+ <!--Choice for each ball location-->
+ <input [(ngModel)]=ball1 type="checkbox" name="1ball" value="1" id="ball-1"><label for="ball-1">Ball 1</label>
+ <input [(ngModel)]=ball2 type="checkbox" name="2ball" value="2" id="ball-2"><label for="ball-2">Ball 2</label><br>
+ <input [(ngModel)]=ball3 type="checkbox" name="3ball" value="3" id="ball-3"><label for="ball-3">Ball 3</label>
+ <input [(ngModel)]=ball4 type="checkbox" name="4ball" value="4" id="ball-4"><label for="ball-4">Ball 4</label><br>
+ <input [(ngModel)]=ball5 type="checkbox" name="5ball" value="5" id="ball-5"><label for="ball-5">Ball 5</label>
</form>
</div>
<div class="row justify-content-center">
@@ -157,6 +157,12 @@
<h4>Auto</h4>
<ul>
+ <li>Quadrant: {{quadrant}}</li>
+ <li>Collected Ball 1: {{ball1}}</li>
+ <li>Collected Ball 2: {{ball2}}</li>
+ <li>Collected Ball 3: {{ball3}}</li>
+ <li>Collected Ball 4: {{ball4}}</li>
+ <li>Collected Ball 5: {{ball5}}</li>
<li>Upper Shots Made: {{autoUpperShotsMade}}</li>
<li>Lower Shots Made: {{autoLowerShotsMade}}</li>
<li>Missed Shots: {{autoShotsMissed}}</li>
@@ -187,9 +193,16 @@
<span class="error_message">{{ errorMessage }}</span>
- <div class="buttons">
- <button class="btn btn-primary" (click)="prevSection()">Back</button>
+ <div class="buttons justify-content-end">
<button class="btn btn-primary" (click)="nextSection()">Submit</button>
</div>
</div>
+
+ <div *ngSwitchCase="'Success'" id="success" class="container-fluid">
+ <span>Successfully submitted scouting data.</span>
+ <div class="buttons justify-content-end">
+ <button class="btn btn-primary" (click)="nextSection()">Continue</button>
+ </div>
+
+ </div>
</ng-container>
diff --git a/scouting/www/import_match_list/import_match_list.component.ts b/scouting/www/import_match_list/import_match_list.component.ts
index 7ad4ab6..bb7e73e 100644
--- a/scouting/www/import_match_list/import_match_list.component.ts
+++ b/scouting/www/import_match_list/import_match_list.component.ts
@@ -21,8 +21,12 @@
errorMessage: string = '';
async importMatchList() {
- if (!window.confirm('Actually import new matches?')) {
- return;
+ const block_alerts = document.getElementById('block_alerts') as HTMLInputElement;
+ console.log(block_alerts.checked);
+ if (!block_alerts.checked) {
+ if (!window.confirm('Actually import new matches?')) {
+ return;
+ }
}
this.errorMessage = '';
diff --git a/scouting/www/index.html b/scouting/www/index.html
index 3a09dfd..b5c33e6 100644
--- a/scouting/www/index.html
+++ b/scouting/www/index.html
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
+ <title>FRC971 Scouting Application</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="/">
<script src="./npm/node_modules/zone.js/dist/zone.min.js"></script>
diff --git a/scouting/www/match_list/BUILD b/scouting/www/match_list/BUILD
new file mode 100644
index 0000000..a33caf8
--- /dev/null
+++ b/scouting/www/match_list/BUILD
@@ -0,0 +1,27 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+
+ts_library(
+ name = "match_list",
+ srcs = [
+ "match_list.component.ts",
+ "match_list.module.ts",
+ ],
+ angular_assets = [
+ "match_list.component.css",
+ "match_list.ng.html",
+ "//scouting/www:common_css",
+ ],
+ compiler = "//tools:tsc_wrapped_with_angular",
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ use_angular_plugin = True,
+ visibility = ["//visibility:public"],
+ deps = [
+ "//scouting/webserver/requests/messages:error_response_ts_fbs",
+ "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
+ "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
+ "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+ "@npm//@angular/common",
+ "@npm//@angular/core",
+ "@npm//@angular/forms",
+ ],
+)
diff --git a/scouting/www/match_list/match_list.component.css b/scouting/www/match_list/match_list.component.css
new file mode 100644
index 0000000..a2c1676
--- /dev/null
+++ b/scouting/www/match_list/match_list.component.css
@@ -0,0 +1,11 @@
+* {
+ padding: 10px;
+}
+
+.red {
+ background-color: #dc3545;
+}
+
+.blue {
+ background-color: #0d6efd;
+}
diff --git a/scouting/www/match_list/match_list.component.ts b/scouting/www/match_list/match_list.component.ts
new file mode 100644
index 0000000..0085b7b
--- /dev/null
+++ b/scouting/www/match_list/match_list.component.ts
@@ -0,0 +1,122 @@
+import {Component, EventEmitter, OnInit, Output} from '@angular/core';
+import * as flatbuffer_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
+import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
+import * as error_response from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
+import * as request_all_matches from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_generated';
+import * as request_all_matches_response from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_response_generated';
+
+import RequestAllMatches = request_all_matches.scouting.webserver.requests.RequestAllMatches;
+import RequestAllMatchesResponse = request_all_matches_response.scouting.webserver.requests.RequestAllMatchesResponse;
+import Match = request_all_matches_response.scouting.webserver.requests.Match;
+import ErrorResponse = error_response.scouting.webserver.requests.ErrorResponse;
+
+type TeamInMatch = {
+ teamNumber: number,
+ matchNumber: number,
+ compLevel: string
+};
+
+@Component({
+ selector: 'app-match-list',
+ templateUrl: './match_list.ng.html',
+ styleUrls: ['../common.css', './match_list.component.css']
+})
+export class MatchListComponent implements OnInit {
+ @Output() selectedTeamEvent = new EventEmitter<TeamInMatch>();
+ teamInMatch: TeamInMatch = {teamNumber: 1, matchNumber: 1, compLevel: 'qm'};
+ progressMessage: string = '';
+ errorMessage: string = '';
+ matchList: Match[] = [];
+
+ setTeamInMatch(teamInMatch: TeamInMatch) {
+ this.teamInMatch = teamInMatch;
+ this.selectedTeamEvent.emit(teamInMatch);
+ }
+
+ teamsInMatch(match: Match): {number: number, color: 'red'|'blue'}[] {
+ return [
+ {number: match.r1(), color: 'red'},
+ {number: match.r2(), color: 'red'},
+ {number: match.r3(), color: 'red'},
+ {number: match.b1(), color: 'blue'},
+ {number: match.b2(), color: 'blue'},
+ {number: match.b3(), color: 'blue'},
+ ];
+ }
+
+ matchType(match: Match): string|null {
+ switch (match.compLevel()) {
+ case 'qm':
+ return 'Quals';
+ case 'ef':
+ return 'Eighth Final';
+ case 'qf':
+ return 'Quarter Final';
+ case 'sf':
+ return 'Semi Final';
+ case 'f':
+ return 'Final';
+ default:
+ return null;
+ }
+ }
+
+ displayMatchNumber(match: Match): string {
+ return `${this.matchType(match)} ${match.matchNumber()}`;
+ }
+
+ ngOnInit() {
+ this.importMatchList();
+ }
+
+ async importMatchList() {
+ this.errorMessage = '';
+
+ const builder =
+ new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
+ RequestAllMatches.startRequestAllMatches(builder);
+ builder.finish(RequestAllMatches.endRequestAllMatches(builder));
+
+ this.progressMessage = 'Fetching match list. Please be patient.';
+
+ const buffer = builder.asUint8Array();
+ const res = await fetch(
+ '/requests/request/all_matches', {method: 'POST', body: buffer});
+
+ if (res.ok) {
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse =
+ RequestAllMatchesResponse.getRootAsRequestAllMatchesResponse(
+ fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+ this.matchList = [];
+ for (let i = 0; i < parsedResponse.matchListLength(); i++) {
+ this.matchList.push(parsedResponse.matchList(i));
+ }
+ this.matchList.sort((a, b) => {
+ let aString = this.displayMatchNumber(a);
+ let bString = this.displayMatchNumber(b);
+ if (aString < bString) {
+ return -1;
+ }
+ if (aString > bString) {
+ return 1;
+ }
+ return 0;
+ });
+
+ this.progressMessage = 'Successfully fetched match list.';
+ } else {
+ this.progressMessage = '';
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(
+ fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+ const errorMessage = parsedResponse.errorMessage();
+ this.errorMessage =
+ `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ }
+ }
+}
diff --git a/scouting/www/match_list/match_list.module.ts b/scouting/www/match_list/match_list.module.ts
new file mode 100644
index 0000000..dbec410
--- /dev/null
+++ b/scouting/www/match_list/match_list.module.ts
@@ -0,0 +1,13 @@
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+
+import {MatchListComponent} from './match_list.component';
+
+@NgModule({
+ declarations: [MatchListComponent],
+ exports: [MatchListComponent],
+ imports: [CommonModule, FormsModule],
+})
+export class MatchListModule {
+}
diff --git a/scouting/www/match_list/match_list.ng.html b/scouting/www/match_list/match_list.ng.html
new file mode 100644
index 0000000..39d4578
--- /dev/null
+++ b/scouting/www/match_list/match_list.ng.html
@@ -0,0 +1,30 @@
+<div class="header">
+ <h2>Matches</h2>
+</div>
+
+<div class="container-fluid">
+
+ <div class="row">
+
+ <div *ngFor="let match of matchList; index as i">
+ <span class="badge bg-secondary rounded-left">{{ displayMatchNumber(match) }}</span>
+ <div class="list-group list-group-horizontal-sm">
+ <button
+ *ngFor="let team of teamsInMatch(match);"
+ (click)="setTeamInMatch({
+ teamNumber: team.number,
+ matchNumber: match.matchNumber(),
+ compLevel: match.compLevel()
+ })"
+ class="text-center text-white fw-bold
+ list-group-item list-group-item-action"
+ [ngClass]="team.color">
+ {{ team.number }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <span class="progress_message" role="alert">{{ progressMessage }}</span>
+ <span class="error_message" role="alert">{{ errorMessage }}</span>
+</div>
diff --git a/third_party/y2022/field/balls.jpeg b/third_party/y2022/field/balls.jpeg
index 9c7cc7f..8e0fba3 100644
--- a/third_party/y2022/field/balls.jpeg
+++ b/third_party/y2022/field/balls.jpeg
Binary files differ