From c9e7640a288f4dfecd16b70fc71eb2f3774fe6f2 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Tue, 18 Sep 2018 00:36:25 -0700 Subject: [PATCH] Add avatars to match intro and final score audience views. --- .gitignore | 1 + partner/tba.go | 48 ++++++++++++++++++++++++++++++++ static/css/audience_display.css | 22 +++++++++++++-- static/img/avatars/0.png | Bin 0 -> 78 bytes static/js/audience_display.js | 31 ++++++++++++++++++++- templates/audience_display.html | 22 +++++++++++---- web/setup_teams.go | 3 ++ 7 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 static/img/avatars/0.png diff --git a/.gitignore b/.gitignore index 85a0c44..616fe9b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ _testmain.go *.out .DS_Store static/logs +static/img/avatars diff --git a/partner/tba.go b/partner/tba.go index 36e097f..0bfe927 100644 --- a/partner/tba.go +++ b/partner/tba.go @@ -8,6 +8,7 @@ package partner import ( "bytes" "crypto/md5" + "encoding/base64" "encoding/json" "fmt" "github.com/Team254/cheesy-arena/game" @@ -20,6 +21,7 @@ import ( const ( tbaBaseUrl = "https://www.thebluealliance.com" tbaAuthKey = "MAApv9MCuKY9MSFkXLuzTSYBCdosboxDq8Q3ujUE2Mn8PD3Nmv64uczu5Lvy0NQ3" + avatarsDir = "static/img/avatars" ) type TbaClient struct { @@ -119,6 +121,11 @@ type TbaEvent struct { Name string `json:"name"` } +type TbaMediaItem struct { + Details map[string]interface{} `json:"details"` + Type string `json:"type"` +} + func NewTbaClient(eventCode, secretId, secret string) *TbaClient { return &TbaClient{BaseUrl: tbaBaseUrl, eventCode: eventCode, secretId: secretId, secret: secret, eventNamesCache: make(map[string]string)} @@ -204,6 +211,47 @@ func (client *TbaClient) GetTeamAwards(teamNumber int) ([]*TbaAward, error) { return awards, nil } +func (client *TbaClient) DownloadTeamAvatar(teamNumber, year int) error { + path := fmt.Sprintf("/api/v3/team/%s/media/%d", getTbaTeam(teamNumber), year) + resp, err := client.getRequest(path) + if err != nil { + return err + } + + // Get the response and handle errors + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + var mediaItems []*TbaMediaItem + err = json.Unmarshal(body, &mediaItems) + if err != nil { + return err + } + + for _, item := range mediaItems { + if item.Type == "avatar" { + base64String, ok := item.Details["base64Image"].(string) + if !ok { + return fmt.Errorf("Could not interpret avatar response from TBA: %v", item) + } + avatarBytes, err := base64.StdEncoding.DecodeString(base64String) + if err != nil { + return err + } + + // Store the avatar to disk as a PNG file. + avatarPath := fmt.Sprintf("%s/%d.png", avatarsDir, teamNumber) + ioutil.WriteFile(avatarPath, avatarBytes, 0644) + return nil + } + } + + return fmt.Errorf("No avatar found for team %d in year %d.", teamNumber, year) +} + // Uploads the event team list to The Blue Alliance. func (client *TbaClient) PublishTeams(database *model.Database) error { teams, err := database.GetAllTeams() diff --git a/static/css/audience_display.css b/static/css/audience_display.css index 43fad90..24ae5a1 100644 --- a/static/css/audience_display.css +++ b/static/css/audience_display.css @@ -33,6 +33,13 @@ html { display: table; font-family: "FuturaLT"; } +.avatars { + display: none; +} +.avatar { + height: 25px; + margin: 4px 10px 3px; +} .valign-cell { display: table-cell; vertical-align: middle; @@ -257,7 +264,7 @@ html { } #finalScore { position: fixed; - width: 800px; + width: 950px; height: 550px; top: 65px; bottom: 0; @@ -296,15 +303,24 @@ html { text-align: center; color: #fff; font-family: "FuturaLT"; - font-size: 35px; + font-size: 32px; } .final-teams span { - margin: 0px 20px; + margin: 0 10px; +} +.final-avatar { + height: 35px; + position: relative; + bottom: 5px; } #leftFinalTeams { + padding-right: 5%; clear: left; border-right: 2px solid #333; } +#rightFinalTeams { + padding-left: 5%; +} .final-breakdown { float: left; width: 33%; diff --git a/static/img/avatars/0.png b/static/img/avatars/0.png new file mode 100644 index 0000000000000000000000000000000000000000..2c78a6767a8c9250d39b4d3e772ce6065b325281 GIT binary patch literal 78 zcmeAS@N?(olHy`uVBq!ia0vp^Ahrev3y{o>U(5}pL_J*`Ln>}1ryO9CaAyTUPeB$2 Yw;l$~m{UEkfeIKrUHx3vIVCg!036m3i~s-t literal 0 HcmV?d00001 diff --git a/static/js/audience_display.js b/static/js/audience_display.js index d1bb9cb..58864e9 100644 --- a/static/js/audience_display.js +++ b/static/js/audience_display.js @@ -40,9 +40,15 @@ var handleMatchLoad = function(data) { $("#" + redSide + "Team1").text(data.Match.Red1); $("#" + redSide + "Team2").text(data.Match.Red2); $("#" + redSide + "Team3").text(data.Match.Red3); + $("#" + redSide + "Team1Avatar").attr("src", getAvatarUrl(data.Match.Red1)); + $("#" + redSide + "Team2Avatar").attr("src", getAvatarUrl(data.Match.Red2)); + $("#" + redSide + "Team3Avatar").attr("src", getAvatarUrl(data.Match.Red3)); $("#" + blueSide + "Team1").text(data.Match.Blue1); $("#" + blueSide + "Team2").text(data.Match.Blue2); $("#" + blueSide + "Team3").text(data.Match.Blue3); + $("#" + blueSide + "Team1Avatar").attr("src", getAvatarUrl(data.Match.Blue1)); + $("#" + blueSide + "Team2Avatar").attr("src", getAvatarUrl(data.Match.Blue2)); + $("#" + blueSide + "Team3Avatar").attr("src", getAvatarUrl(data.Match.Blue3)); $("#matchName").text(data.MatchType + " " + data.Match.DisplayName); }; @@ -100,6 +106,9 @@ var handleScorePosted = function(data) { $("#" + redSide + "FinalTeam1").text(data.Match.Red1); $("#" + redSide + "FinalTeam2").text(data.Match.Red2); $("#" + redSide + "FinalTeam3").text(data.Match.Red3); + $("#" + redSide + "FinalTeam1Avatar").attr("src", getAvatarUrl(data.Match.Red1)); + $("#" + redSide + "FinalTeam2Avatar").attr("src", getAvatarUrl(data.Match.Red2)); + $("#" + redSide + "FinalTeam3Avatar").attr("src", getAvatarUrl(data.Match.Red3)); $("#" + redSide + "FinalAutoRunPoints").text(data.RedScoreSummary.AutoRunPoints); $("#" + redSide + "FinalOwnershipPoints").text(data.RedScoreSummary.OwnershipPoints); $("#" + redSide + "FinalVaultPoints").text(data.RedScoreSummary.VaultPoints); @@ -113,6 +122,9 @@ var handleScorePosted = function(data) { $("#" + blueSide + "FinalTeam1").text(data.Match.Blue1); $("#" + blueSide + "FinalTeam2").text(data.Match.Blue2); $("#" + blueSide + "FinalTeam3").text(data.Match.Blue3); + $("#" + blueSide + "FinalTeam1Avatar").attr("src", getAvatarUrl(data.Match.Blue1)); + $("#" + blueSide + "FinalTeam2Avatar").attr("src", getAvatarUrl(data.Match.Blue2)); + $("#" + blueSide + "FinalTeam3Avatar").attr("src", getAvatarUrl(data.Match.Blue3)); $("#" + blueSide + "FinalAutoRunPoints").text(data.BlueScoreSummary.AutoRunPoints); $("#" + blueSide + "FinalOwnershipPoints").text(data.BlueScoreSummary.OwnershipPoints); $("#" + blueSide + "FinalVaultPoints").text(data.BlueScoreSummary.VaultPoints); @@ -163,6 +175,8 @@ var handleLowerThird = function(data) { }; var transitionBlankToIntro = function(callback) { + $(".avatars").show(); + $(".avatars").css("opacity", 1); $("#centering").transition({queue: false, bottom: "0px"}, 500, "ease", function() { $(".teams").transition({queue: false, width: "65px"}, 100, "linear", function() { $(".score").transition({queue: false, width: "120px"}, 500, "ease", function() { @@ -175,6 +189,9 @@ var transitionBlankToIntro = function(callback) { }; var transitionIntroToInMatch = function(callback) { + $(".avatars").transition({queue: false, opacity: 0}, 500, "ease", function() { + $(".avatars").hide(); + }); $("#logo").transition({queue: false, top: "10px"}, 500, "ease"); $(".score").transition({queue: false, width: "275px"}, 500, "ease", function() { $(".score-number").transition({queue: false, opacity: 1}, 750, "ease"); @@ -189,6 +206,8 @@ var transitionIntroToBlank = function(callback) { $("#eventMatchInfo").hide(); $(".score").transition({queue: false, width: "0px"}, 500, "ease"); $(".teams").transition({queue: false, width: "40px"}, 500, "ease", function() { + $(".avatars").css("opacity", 0); + $(".avatars").hide(); $("#centering").transition({queue: false, bottom: "-340px"}, 1000, "ease", callback); }); }); @@ -218,7 +237,10 @@ var transitionInMatchToIntro = function(callback) { $("#matchTime").transition({queue: false, opacity: 0}, 300, "linear", function() { $("#logo").transition({queue: false, top: "35px"}, 500, "ease"); $(".score").transition({queue: false, width: "120px"}, 500, "ease"); - $(".teams").transition({queue: false, width: "65px"}, 500, "ease", callback); + $(".teams").transition({queue: false, width: "65px"}, 500, "ease", function() { + $(".avatars").show(); + $(".avatars").transition({queue: false, opacity: 1}, 500, "ease", callback); + }); }); }; @@ -405,6 +427,10 @@ var initializeSponsorDisplay = function() { }); }; +var getAvatarUrl = function(teamId) { + return "/static/img/avatars/" + teamId + ".png"; +}; + $(function() { // Read the configuration for this display from the URL query string. var urlParams = new URLSearchParams(window.location.search); @@ -420,6 +446,9 @@ $(function() { $(".reversible-left").attr("data-reversed", reversed); $(".reversible-right").attr("data-reversed", reversed); + // Fall back to a blank avatar if one doesn't exist for the team. + $(".avatar, .final-avatar").attr("onerror", "this.src='" + getAvatarUrl(0) + "';"); + // Set up the websocket back to the server. websocket = new CheesyWebsocket("/displays/audience/websocket", { allianceSelection: function(event) { handleAllianceSelection(event.data); }, diff --git a/templates/audience_display.html b/templates/audience_display.html index a186ae7..2632cfd 100644 --- a/templates/audience_display.html +++ b/templates/audience_display.html @@ -24,6 +24,11 @@
+
+
+
+ +
F
@@ -44,6 +49,11 @@
 
+
+
+
+ +
F
@@ -97,14 +107,14 @@
- - - + + +
- - - + + +
diff --git a/web/setup_teams.go b/web/setup_teams.go index 83934a8..55d9abf 100644 --- a/web/setup_teams.go +++ b/web/setup_teams.go @@ -295,6 +295,9 @@ func (web *Web) getOfficialTeamInfo(teamId int) (*model.Team, error) { } } + // Download and store the team's avatar; if there isn't one, ignore the error. + web.arena.TbaClient.DownloadTeamAvatar(teamId, time.Now().Year()) + // Use those variables to make a team object team = model.Team{Id: teamId, Name: tbaTeam.Name, Nickname: tbaTeam.Nickname, City: tbaTeam.City, StateProv: tbaTeam.StateProv, Country: tbaTeam.Country, RookieYear: tbaTeam.RookieYear,