Added audience display.

This commit is contained in:
Patrick Fairbank
2014-08-02 19:43:45 -07:00
parent 04ade7ef1b
commit d0ec316bd9
22 changed files with 989 additions and 260 deletions

View File

@@ -46,6 +46,7 @@ type RealtimeScore struct {
CurrentScore Score
CurrentCycle Cycle
AutoPreloadedBalls int
AutoLeftoverBalls int
Fouls []Foul
AutoCommitted bool
TeleopCommitted bool
@@ -55,23 +56,27 @@ type RealtimeScore struct {
}
type Arena struct {
AllianceStations map[string]*AllianceStation
MatchState int
CanStartMatch bool
matchTiming MatchTiming
currentMatch *Match
redRealtimeScore *RealtimeScore
blueRealtimeScore *RealtimeScore
matchStartTime time.Time
lastDsPacketTime time.Time
matchStateNotifier *Notifier
matchTimeNotifier *Notifier
robotStatusNotifier *Notifier
matchLoadTeamsNotifier *Notifier
scorePostedNotifier *Notifier
lastMatchState int
lastMatchTimeSec float64
savedMatchResult *MatchResult
AllianceStations map[string]*AllianceStation
MatchState int
CanStartMatch bool
matchTiming MatchTiming
currentMatch *Match
redRealtimeScore *RealtimeScore
blueRealtimeScore *RealtimeScore
matchStartTime time.Time
lastDsPacketTime time.Time
matchStateNotifier *Notifier
matchTimeNotifier *Notifier
robotStatusNotifier *Notifier
matchLoadTeamsNotifier *Notifier
realtimeScoreNotifier *Notifier
scorePostedNotifier *Notifier
audienceDisplayNotifier *Notifier
audienceDisplayScreen string
lastMatchState int
lastMatchTimeSec float64
savedMatch *Match
savedMatchResult *MatchResult
}
var mainArena Arena // Named thusly to avoid polluting the global namespace with something more generic.
@@ -95,13 +100,20 @@ func (arena *Arena) Setup() {
arena.matchTimeNotifier = NewNotifier()
arena.robotStatusNotifier = NewNotifier()
arena.matchLoadTeamsNotifier = NewNotifier()
arena.realtimeScoreNotifier = NewNotifier()
arena.scorePostedNotifier = NewNotifier()
arena.audienceDisplayNotifier = NewNotifier()
// Load empty match as current.
arena.MatchState = PRE_MATCH
arena.LoadTestMatch()
arena.lastMatchState = -1
arena.lastMatchTimeSec = 0
// Initialize display parameters.
arena.audienceDisplayScreen = "blank"
arena.savedMatch = &Match{}
arena.savedMatchResult = &MatchResult{}
}
// Loads a team into an alliance station, cleaning up the previous team there if there is one.
@@ -184,6 +196,7 @@ func (arena *Arena) LoadMatch(match *Match) error {
arena.blueRealtimeScore = new(RealtimeScore)
arena.matchLoadTeamsNotifier.Notify(nil)
arena.realtimeScoreNotifier.Notify(nil)
return nil
}
@@ -274,6 +287,8 @@ func (arena *Arena) AbortMatch() error {
return fmt.Errorf("Cannot abort match when it is not in progress.")
}
arena.MatchState = POST_MATCH
arena.audienceDisplayScreen = "blank"
arena.audienceDisplayNotifier.Notify(nil)
return nil
}
@@ -322,6 +337,8 @@ func (arena *Arena) Update() {
auto = true
enabled = true
sendDsPacket = true
arena.audienceDisplayScreen = "match"
arena.audienceDisplayNotifier.Notify(nil)
case AUTO_PERIOD:
auto = true
enabled = true
@@ -357,6 +374,8 @@ func (arena *Arena) Update() {
auto = false
enabled = false
sendDsPacket = true
arena.audienceDisplayScreen = "blank"
arena.audienceDisplayNotifier.Notify(nil)
}
}
@@ -402,3 +421,14 @@ func (arena *Arena) sendDsPacket(auto bool, enabled bool) {
}
arena.lastDsPacketTime = time.Now()
}
func (realtimeScore *RealtimeScore) Score() int {
score := scoreSummary(&realtimeScore.CurrentScore, []Foul{}).Score
if realtimeScore.CurrentCycle.Truss {
score += 10
if realtimeScore.CurrentCycle.Catch {
score += 10
}
}
return score
}

View File

@@ -20,6 +20,157 @@ var rules = []string{"G3", "G5", "G10", "G11", "G12", "G14", "G15", "G16", "G17"
"G23", "G24", "G25", "G26", "G26-1", "G27", "G28", "G29", "G30", "G31", "G32", "G34", "G35", "G36", "G37",
"G38", "G39", "G40", "G41", "G42"}
// Renders the audience display to be chroma keyed over the video feed.
func AudienceDisplayHandler(w http.ResponseWriter, r *http.Request) {
template := template.New("").Funcs(templateHelpers)
_, err := template.ParseFiles("templates/audience_display.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
}{eventSettings}
err = template.ExecuteTemplate(w, "audience_display.html", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// The websocket endpoint for the audience display client to receive status updates.
func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
websocket, err := NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer websocket.Close()
audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen()
defer close(audienceDisplayListener)
matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen()
defer close(matchLoadTeamsListener)
matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener)
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
defer close(realtimeScoreListener)
scorePostedListener := mainArena.scorePostedNotifier.Listen()
defer close(scorePostedListener)
// Send the various notifications immediately upon connection.
var data interface{}
err = websocket.Write("matchTiming", mainArena.matchTiming)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)})
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("setAudienceDisplay", mainArena.audienceDisplayScreen)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
data = struct {
Match *Match
MatchName string
}{mainArena.currentMatch, mainArena.currentMatch.CapitalizedType()}
err = websocket.Write("setMatch", data)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
data = struct {
RedScore int
RedCycle Cycle
BlueScore int
BlueCycle Cycle
}{mainArena.redRealtimeScore.Score(), mainArena.redRealtimeScore.CurrentCycle,
mainArena.blueRealtimeScore.Score(), mainArena.blueRealtimeScore.CurrentCycle}
err = websocket.Write("realtimeScore", data)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
data = struct {
Match *Match
MatchName string
RedScore *ScoreSummary
BlueScore *ScoreSummary
}{mainArena.savedMatch, mainArena.savedMatch.CapitalizedType(),
mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary()}
err = websocket.Write("setFinalScore", data)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
go func() {
for {
var messageType string
var message interface{}
select {
case _, ok := <-audienceDisplayListener:
if !ok {
return
}
messageType = "setAudienceDisplay"
message = mainArena.audienceDisplayScreen
case _, ok := <-matchLoadTeamsListener:
if !ok {
return
}
messageType = "setMatch"
message = struct {
Match *Match
MatchName string
}{mainArena.currentMatch, mainArena.currentMatch.CapitalizedType()}
case matchTimeSec, ok := <-matchTimeListener:
if !ok {
return
}
messageType = "matchTime"
message = MatchTimeMessage{mainArena.MatchState, matchTimeSec.(int)}
case _, ok := <-realtimeScoreListener:
if !ok {
return
}
messageType = "realtimeScore"
message = struct {
RedScore int
RedCycle Cycle
BlueScore int
BlueCycle Cycle
}{mainArena.redRealtimeScore.Score(), mainArena.redRealtimeScore.CurrentCycle,
mainArena.blueRealtimeScore.Score(), mainArena.blueRealtimeScore.CurrentCycle}
case _, ok := <-scorePostedListener:
if !ok {
return
}
messageType = "setFinalScore"
message = struct {
Match *Match
MatchName string
RedScore *ScoreSummary
BlueScore *ScoreSummary
}{mainArena.savedMatch, mainArena.savedMatch.CapitalizedType(),
mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary()}
}
err = websocket.Write(messageType, message)
if err != nil {
// The client has probably closed the connection; nothing to do here.
return
}
}
}()
}
// Renders the pit display which shows scrolling rankings.
func PitDisplayHandler(w http.ResponseWriter, r *http.Request) {
template, err := template.ParseFiles("templates/pit_display.html")
@@ -58,17 +209,10 @@ func AnnouncerDisplayHandler(w http.ResponseWriter, r *http.Request) {
// Assemble info about the saved match result.
var redScoreSummary, blueScoreSummary *ScoreSummary
var savedMatchType, savedMatchDisplayName string
if mainArena.savedMatchResult != nil {
redScoreSummary = mainArena.savedMatchResult.RedScoreSummary()
blueScoreSummary = mainArena.savedMatchResult.BlueScoreSummary()
match, err := db.GetMatchById(mainArena.savedMatchResult.MatchId)
if err != nil {
handleWebErr(w, err)
return
}
savedMatchType = match.CapitalizedType()
savedMatchDisplayName = match.DisplayName
}
savedMatchType = mainArena.savedMatch.CapitalizedType()
savedMatchDisplayName = mainArena.savedMatch.DisplayName
redScoreSummary = mainArena.savedMatchResult.RedScoreSummary()
blueScoreSummary = mainArena.savedMatchResult.BlueScoreSummary()
data := struct {
*EventSettings
MatchType string
@@ -115,8 +259,7 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Websocket error: %s", err)
return
}
data := MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)}
err = websocket.Write("matchTime", data)
err = websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)})
if err != nil {
log.Printf("Websocket error: %s", err)
return
@@ -157,7 +300,7 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
// Loop, waiting for commands and responding to them, until the client closes the connection.
for {
messageType, _, err := websocket.Read()
messageType, data, err := websocket.Read()
if err != nil {
if err == io.EOF {
// Client has closed the connection; nothing to do here.
@@ -168,9 +311,16 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
}
switch messageType {
case "setAudienceDisplay":
screen, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
mainArena.audienceDisplayScreen = screen
mainArena.audienceDisplayNotifier.Notify(nil)
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
continue
}
}
}
@@ -316,17 +466,20 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
(*score).CurrentCycle.DeadBall = false
}
case "assist":
if !(*score).TeleopCommitted && (*score).CurrentCycle.Assists < 3 {
if (*score).AutoCommitted && !(*score).TeleopCommitted && (*score).AutoLeftoverBalls == 0 &&
(*score).CurrentCycle.Assists < 3 {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.Assists += 1
}
case "truss":
if !(*score).TeleopCommitted && !(*score).CurrentCycle.Truss {
if (*score).AutoCommitted && !(*score).TeleopCommitted && (*score).AutoLeftoverBalls == 0 &&
!(*score).CurrentCycle.Truss {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.Truss = true
}
case "catch":
if !(*score).TeleopCommitted && !(*score).CurrentCycle.Catch && (*score).CurrentCycle.Truss {
if (*score).AutoCommitted && !(*score).TeleopCommitted && (*score).AutoLeftoverBalls == 0 &&
!(*score).CurrentCycle.Catch && (*score).CurrentCycle.Truss {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.Catch = true
}
@@ -339,15 +492,15 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
}
case "commit":
if !(*score).AutoCommitted {
(*score).AutoLeftoverBalls = (*score).AutoPreloadedBalls - (*score).CurrentScore.AutoHighHot -
(*score).CurrentScore.AutoHigh - (*score).CurrentScore.AutoLowHot -
(*score).CurrentScore.AutoLow
(*score).AutoCommitted = true
} else if !(*score).TeleopCommitted {
if (*score).CurrentCycle.ScoredHigh || (*score).CurrentCycle.ScoredLow ||
(*score).CurrentCycle.DeadBall {
// Check whether this is a leftover ball from autonomous.
if ((*score).AutoPreloadedBalls - (*score).CurrentScore.AutoHighHot -
(*score).CurrentScore.AutoHigh - (*score).CurrentScore.AutoLowHot -
(*score).CurrentScore.AutoLow - (*score).CurrentScore.AutoClearHigh -
(*score).CurrentScore.AutoClearLow - (*score).CurrentScore.AutoClearDead) > 0 {
if (*score).AutoLeftoverBalls > 0 {
if (*score).CurrentCycle.ScoredHigh {
(*score).CurrentScore.AutoClearHigh += 1
} else if (*score).CurrentCycle.ScoredLow {
@@ -355,6 +508,7 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
} else {
(*score).CurrentScore.AutoClearDead += 1
}
(*score).AutoLeftoverBalls -= 1
} else {
(*score).CurrentScore.Cycles = append((*score).CurrentScore.Cycles, (*score).CurrentCycle)
}
@@ -382,6 +536,8 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
continue
}
mainArena.realtimeScoreNotifier.Notify(nil)
// Send out the score again after handling the command, as it most likely changed as a result.
err = websocket.Write("score", *score)
if err != nil {

View File

@@ -119,7 +119,16 @@ func MatchPlayLoadHandler(w http.ResponseWriter, r *http.Request) {
func MatchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
matchId, _ := strconv.Atoi(vars["matchId"])
matchResult, err := db.GetMatchResultForMatch(matchId)
match, err := db.GetMatchById(matchId)
if err != nil {
handleWebErr(w, err)
return
}
if match == nil {
handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId))
return
}
matchResult, err := db.GetMatchResultForMatch(match.Id)
if err != nil {
handleWebErr(w, err)
return
@@ -128,6 +137,7 @@ func MatchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) {
handleWebErr(w, fmt.Errorf("No result found for match ID %d.", matchId))
return
}
mainArena.savedMatch = match
mainArena.savedMatchResult = matchResult
mainArena.scorePostedNotifier.Notify(nil)
@@ -147,6 +157,8 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
defer close(matchTimeListener)
robotStatusListener := mainArena.robotStatusNotifier.Listen()
defer close(robotStatusListener)
audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen()
defer close(audienceDisplayListener)
// Send the various notifications immediately upon connection.
err = websocket.Write("status", mainArena)
@@ -165,6 +177,11 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("setAudienceDisplay", mainArena.audienceDisplayScreen)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
go func() {
@@ -184,6 +201,12 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
}
messageType = "status"
message = mainArena
case _, ok := <-audienceDisplayListener:
if !ok {
return
}
messageType = "setAudienceDisplay"
message = mainArena.audienceDisplayScreen
}
err = websocket.Write(messageType, message)
if err != nil {
@@ -284,6 +307,15 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
return
}
continue // Skip sending the status update, as the client is about to terminate and reload.
case "setAudienceDisplay":
screen, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
mainArena.audienceDisplayScreen = screen
mainArena.audienceDisplayNotifier.Notify(nil)
continue
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
continue

View File

@@ -172,6 +172,7 @@ func TestMatchPlayWebsocketCommands(t *testing.T) {
readWebsocketType(t, ws, "status")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "setAudienceDisplay")
// Test that a server-side error is communicated to the client.
ws.Write("nonexistenttype", nil)
@@ -219,6 +220,7 @@ func TestMatchPlayWebsocketCommands(t *testing.T) {
assert.Contains(t, readWebsocketError(t, ws), "Cannot reset match")
ws.Write("abortMatch", nil)
readWebsocketType(t, ws, "status")
readWebsocketType(t, ws, "setAudienceDisplay")
assert.Equal(t, POST_MATCH, mainArena.MatchState)
ws.Write("commitResults", nil)
readWebsocketType(t, ws, "reload")
@@ -249,6 +251,7 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
readWebsocketType(t, ws, "status")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "setAudienceDisplay")
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
@@ -258,10 +261,13 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
mainArena.AllianceStations["B3"].Bypass = true
mainArena.StartMatch()
mainArena.Update()
statusReceived, matchTime := readWebsocketStatusMatchTime(t, ws)
messages := readWebsocketMultiple(t, ws, 3)
statusReceived, matchTime := getStatusMatchTime(t, messages)
assert.Equal(t, true, statusReceived)
assert.Equal(t, 2, matchTime.MatchState)
assert.Equal(t, 0, matchTime.MatchTimeSec)
_, ok := messages["setAudienceDisplay"]
assert.True(t, ok)
// Should get a tick notification when an integer second threshold is crossed.
mainArena.matchStartTime = time.Now().Add(-time.Second + 10*time.Millisecond) // Not crossed yet
@@ -290,22 +296,18 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
assert.Equal(t, mainArena.matchTiming.AutoDurationSec, matchTime.MatchTimeSec)
}
// Handles the status and matchTime messaegs arriving in either order.
// Handles the status and matchTime messages arriving in either order.
func readWebsocketStatusMatchTime(t *testing.T, ws *Websocket) (bool, MatchTimeMessage) {
statusReceived := false
return getStatusMatchTime(t, readWebsocketMultiple(t, ws, 2))
}
func getStatusMatchTime(t *testing.T, messages map[string]interface{}) (bool, MatchTimeMessage) {
_, statusReceived := messages["status"]
message, ok := messages["matchTime"]
var matchTime MatchTimeMessage
for i := 0; i < 2; i++ {
messageType, message, err := ws.Read()
if assert.Nil(t, err) {
if messageType == "status" {
statusReceived = true
} else {
if assert.Equal(t, "matchTime", messageType) {
err = mapstructure.Decode(message, &matchTime)
assert.Nil(t, err)
}
}
}
if assert.True(t, ok) {
err := mapstructure.Decode(message, &matchTime)
assert.Nil(t, err)
}
return statusReceived, matchTime
}

View File

@@ -1,42 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!--
Copyright 2014 Team 254. All Rights Reserved.
Author: nick@team254.com (Nick Eyre)
-->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Cheesy Arena - Audience Screen</title>
<meta name="generator" content="Cheesy Arena" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/css/fonts/roboto-regular/stylesheet.css" type="text/css" charset="utf-8" />
<link rel="stylesheet" href="/static/css/audience.css"/>
</head>
<body>
<div id='topcontainer'></div>
<div class='controlpanel'>
<h3>Control Panel</h3>
<button onclick="openScreen('logo');">Logo</button><br />
<button onclick="openScreen('blank');">None</button><br />
</button>
<script src="/static/lib/jquery.min.js"></script>
<script src="/static/lib/jquery.transit.min.js"></script>
<script src="/static/js/audience.js"></script>
<script type='text/x-template' class='template' id='blank'></script>
<script type='text/x-template' class='template' id='logo'>
<div class='blinds right background'>&nbsp;</div>
<div class='blinds right center blank'>&nbsp;</div>
<div class='blinds left background'>&nbsp;</div>
<div class='blinds left center'>&nbsp;</div>
<div class='blinds left center blank'>&nbsp;</div>
</script>
</body>
</html>

View File

@@ -1,63 +0,0 @@
/*
Copyright 2014 Team 254. All Rights Reserved.
Author: nick@team254.com (Nick Eyre)
*/
/* Control Panel (Temporary) */
.controlpanel {
position: fixed;
right: 20px;
top: 20px;
z-index: 99;
background-color: white;
padding: 20px;
border: 1px solid black;
}
/* Top Level Container */
html,body {
height: 100%;
margin: 0;
width: 100%;
}
#topcontainer {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
/* Design Element: Blinds */
.blinds {
position: fixed;
top: 0;
background-size: 200%;
height: 100%;
width: 50%;
}
.blinds.left {
background-position: left;
left: -50%;
}
.blinds.right {
background-position: right;
right: -50%;
}
.blinds.full {
width: 100%;
background-position: center;
background-size: 100%;
}
/* Screen: Logo */
#logo .blinds.background {
background-image: url('/static/img/endofmatch-bg.png');
}
#logo .blinds.center {
background-image: url('/static/img/endofmatch-center.png');
-webkit-backface-visibility: hidden;
}
#logo .blinds.center.blank {
background-image: url('/static/img/endofmatch-center-blank.png');
}

View File

@@ -0,0 +1,277 @@
html {
cursor: none;
-webkit-user-select: none;
-moz-user-select: none;
overflow: hidden;
}
#centering {
position: absolute; left: 50%;
bottom: -340px;
}
#matchOverlay {
position: relative;
left: -50%;
bottom: 70px;
margin: 0 auto;
height: 104px;
background-color: #fff;
border: 1px solid #000;
color: #000;
font-size: 22px;
}
.teams {
width: 40px;
height: 100%;
line-height: 26px;
text-align: center;
display: table;
font-family: "FuturaLT";
}
.valign-cell {
display: table-cell;
vertical-align: middle;
}
#redTeams {
float: left;
border-right: 1px solid #000;
}
#blueTeams {
float: right;
border-left: 1px solid #000;
}
.score {
width: 0px;
height: 100%;
float: left;
}
#redScore {
float: left;
background-color: #ff4444;
}
#blueScore {
float: left;
background-color: #2080ff;
}
#eventMatchInfo {
display: none;
position: absolute;
width: 100%;
height: 30px;
bottom: 0px;
line-height: 30px;
background-color: #444;
border: 1px solid #000;
padding: 0px 5px;
font-family: "FuturaLT";
font-size: 15px;
color: #fff;
z-index: -1;
}
#matchCircle {
position: absolute;
left: -75px;
bottom: 50px;
margin: 0 auto;
border-radius: 50%;
width: 150px;
height: 150px;
background-color: #fff;
border: 1px solid #000;
}
.score-number {
width: 60%;
margin: 0px 5px;
text-align: center;
font-family: "FuturaLTBold";
font-size: 55px;
line-height: 80px;
color: #fff;
opacity: 0;
}
.score-fields {
padding: 0px 15px;
margin: -10px 0px;
opacity: 0;
}
.assist {
float: left;
width: 22px;
height: 12px;
margin: 10px 4px;
background-color: #fff;
opacity: 0.3;
}
.trussCatch {
float: left;
font-family: "FuturaLTBold";
margin: 0px 5px;
color: #fff;
opacity: 0.3;
}
[data-on=true] {
opacity: 1;
}
#logo {
position: relative;
top: 45px;
height: 60px;
}
#matchTime {
position: relative;
top: 30px;
height: 60px;
color: #000;
font-family: "FuturaLTBold";
font-size: 32px;
opacity: 0;
}
#blindsContainer {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.blinds {
position: fixed;
top: 0;
background-size: 200%;
height: 100%;
width: 50%;
}
.blinds.left {
background-position: left;
left: -50%;
}
.blinds.right {
background-position: right;
right: -50%;
}
.blinds.full {
width: 100%;
background-position: center;
background-size: 100%;
}
.blinds.background {
background-image: url("/static/img/endofmatch-bg.png");
}
.blinds.center {
position: fixed;
background-image: url("/static/img/endofmatch-center.png");
}
.blinds.center-blank {
background-image: url("/static/img/endofmatch-center-blank.png");
-webkit-backface-visibility: hidden;
z-index: 3;
}
#blindsCenter {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto auto;
border-radius: 50%;
width: 310px;
height: 310px;
background-color: #fff;
border: 1px solid #333;
box-shadow: 0 0 5px #666;
text-align: center;
-webkit-backface-visibility: hidden;
z-index: 2;
transform: rotateY(-180deg);
}
#blindsLogo {
position: relative;
top: 85px;
height: 140px;
}
#finalScore {
position: fixed;
width: 800px;
height: 450px;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto auto;
border: 2px solid #333;
z-index: 0;
opacity: 0;
background-color: #444;
}
.final-score {
float: left;
width: 50%;
height: 44%;
line-height: 200px;
border-bottom: 2px solid #333;
color: #fff;
font-family: "FuturaLTBold";
font-size: 100px;
text-align: center;
text-shadow: 0 0 3px #333;
}
#redFinalScore {
background-color: #ff4444;
padding-right: 150px;
}
#blueFinalScore {
clear: right;
background-color: #2080ff;
padding-left: 150px;
}
.final-teams {
float: left;
width: 50%;
height: 11%;
line-height: 50px;
text-align: center;
color: #fff;
font-family: "FuturaLT";
font-size: 35px;
}
.final-teams span {
margin: 0px 20px;
}
#redFinalTeams {
clear: left;
border-right: 2px solid #333;
}
#blueFinalTeam {
}
.final-breakdown {
float: left;
width: 33%;
height: 34%;
padding: 0px 20px;
display: table;
text-align: center;
background-color: #fff;
color: #000;
font-family: "FuturaLT";
font-size: 30px;
}
#redFinalBreakdown {
clear: left;
text-align: right;
}
#blueFinalBreakdown {
text-align: left;
}
#centerFinalBreakdown {
width: 34%;
border-left: 2px solid #333;
border-right: 2px solid #333;
}
#finalEventMatchInfo {
clear: both;
width: 100%;
height: 11%;
line-height: 50px;
padding: 0px 25px;
font-family: "FuturaLT";
font-size: 28px;
color: #fff;
}

View File

@@ -8,6 +8,14 @@
}
/* New styles. */
@font-face {
font-family: "FuturaLTBold";
src: url("fonts/futura-lt-bold.otf") format("opentype");
}
@font-face {
font-family: "FuturaLT";
src: url("fonts/futura-lt.otf") format("opentype");
}
.red-text {
color: #f00;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

16
static/img/logo-min.svg Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="1276.184px" height="982.562px" viewBox="0 0 1276.184 982.562" enable-background="new 0 0 1276.184 982.562"
xml:space="preserve">
<path fill="#231F20" d="M507.973,863.748c-39.596-39.314-71.906-85.14-96.543-135.638
c-95.381-38.679-163.341-134.439-163.341-255.029c0-96.801,68.419-187.122,164.313-224.21
c27.115-50.321,63.396-96.359,108.284-136.231c41.487-36.849,89.22-67.072,140.599-89.991C611.907,7.935,559.958,0,507.134,0
C243.231,0,0,194.585,0,471.857C0,768.6,212.818,977.776,507.134,977.776c49.08,0,95.815-5.959,139.75-17.021
C595.167,936.198,548.224,903.724,507.973,863.748z"/>
<path fill="#0070FF" d="M910.72,751.506c-142.264,0-259.015-115.534-259.015-273.641c0-126.474,116.751-241.995,259.015-241.995
c73.258,0,139.664,30.651,186.944,77.602l162.968-178.936C1167.11,52.621,1041.488,4.803,910.72,4.803
c-263.872,0-507.116,194.579-507.116,471.851c0,296.73,212.83,505.908,507.116,505.908c148.021,0,275.382-52.934,365.464-143.273
l-181.566-166.412C1047.551,722.176,982.388,751.506,910.72,751.506z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -8,9 +8,6 @@ var blinkTimeout;
var handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
console.log(matchState);
console.log(matchStateText);
console.log(countdownSec);
$("#matchState").text(matchStateText);
$("#matchTime").text(getCountdown(data.MatchState, data.MatchTimeSec));
if (matchState == "PRE_MATCH" || matchState == "POST_MATCH") {
@@ -22,8 +19,7 @@ var handleMatchTime = function(data) {
var postMatchResult = function(data) {
clearTimeout(blinkTimeout);
$("#savedMatchResult").attr("data-blink", false);
// TODO(patrick): Signal audience display.
websocket.send("setAudienceDisplay", "score");
}
$(function() {

View File

@@ -1,79 +0,0 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: nick@team254.com (Nick Eyre)
var screens = {
blank: {
init: function(cb){callback(cb);},
open: function(cb){callback(cb);},
close: function(cb){callback(cb);}
},
logo: {
init: function(cb){
$('.blinds.center:not(.blank)').css({rotateY: '-180deg'});
callback(cb);
},
open: function(cb){
closeBlinds(function(){
setTimeout(function(){
$('.blinds.center.blank').transition({rotateY: '180deg'});
$('.blinds.center:not(.blank)').transition({rotateY: '0deg'}, function(){
callback(cb);
});
}, 400);
});
},
close: function(cb){
$('.blinds.center.blank').transition({rotateY: '360deg'});
$('.blinds.center:not(.blank)').transition({rotateY: '180deg'}, function(){
openBlinds(callback);
});
}
}
};
var currentScreen = 'blank';
function openScreen(screen){
// If Screen Exists
if(typeof(screens[screen]) == 'object' && $('.template#'+screen).length > 0 && currentScreen != screen){
// Initialize New Screen
$('#topcontainer').append("<div class='container' id='"+screen+"'>"+$('.template#'+screen).html()+"</div>");
screens[screen].init(function(){
// Close Current Screen
screens[currentScreen].close(function(){
// Open New Screen
currentScreen = screen;
screens[screen].open();
});
});
}
}
function callback(cb){
if(typeof(cb) == 'function')
cb();
}
function closeBlinds(cb){
$('.blinds.right').transition({right: 0});
$('.blinds.left').transition({left: 0}, function(){
$(this).addClass('full');
callback(cb);
});
}
function openBlinds(cb){
$('.blinds.right').show();
$('.blinds.left').removeClass('full');
$('.blinds.right').show().transition({right: '-50%'});
$('.blinds.left').transition({left: '-50%'}, function(){
callback(cb);
});
}

View File

@@ -0,0 +1,239 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Client-side methods for the audience display.
var transitionMap;
var currentScreen = "blank";
var handleSetAudienceDisplay = function(targetScreen) {
if (targetScreen == currentScreen) {
return;
}
transitions = transitionMap[currentScreen][targetScreen];
if (transitions == null) {
// There is no direct transition defined; need to go to the blank screen first.
transitions = function() {
transitionMap[currentScreen]["blank"](transitionMap["blank"][targetScreen]);
};
}
transitions();
currentScreen = targetScreen;
};
var handleSetMatch = function(data) {
$("#redTeam1").text(data.Match.Red1)
$("#redTeam2").text(data.Match.Red2)
$("#redTeam3").text(data.Match.Red3)
$("#blueTeam1").text(data.Match.Blue1)
$("#blueTeam2").text(data.Match.Blue2)
$("#blueTeam3").text(data.Match.Blue3)
$("#matchName").text(data.MatchName + " " + data.Match.DisplayName);
};
var handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
var countdownString = String(countdownSec % 60);
if (countdownString.length == 1) {
countdownString = "0" + countdownString;
}
countdownString = Math.floor(countdownSec / 60) + ":" + countdownString;
$("#matchTime").text(countdownString);
});
};
var handleRealtimeScore = function(data) {
$("#redScoreNumber").text(data.RedScore);
$("#redAssist1").attr("data-on", data.RedCycle.Assists >= 1);
$("#redAssist2").attr("data-on", data.RedCycle.Assists >= 2);
$("#redAssist3").attr("data-on", data.RedCycle.Assists >= 3);
$("#redTruss").attr("data-on", data.RedCycle.Truss);
$("#redCatch").attr("data-on", data.RedCycle.Catch);
$("#blueScoreNumber").text(data.BlueScore);
$("#blueAssist1").attr("data-on", data.BlueCycle.Assists >= 1);
$("#blueAssist2").attr("data-on", data.BlueCycle.Assists >= 2);
$("#blueAssist3").attr("data-on", data.BlueCycle.Assists >= 3);
$("#blueTruss").attr("data-on", data.BlueCycle.Truss);
$("#blueCatch").attr("data-on", data.BlueCycle.Catch);
};
var handleSetFinalScore = function(data) {
$("#redFinalScore").text(data.RedScore.Score);
$("#redFinalTeam1").text(data.Match.Red1);
$("#redFinalTeam2").text(data.Match.Red2);
$("#redFinalTeam3").text(data.Match.Red3);
$("#redFinalAuto").text(data.RedScore.AutoPoints);
$("#redFinalTeleop").text(data.RedScore.TeleopPoints);
$("#redFinalFoul").text(data.RedScore.FoulPoints);
$("#blueFinalScore").text(data.BlueScore.Score);
$("#blueFinalTeam1").text(data.Match.Blue1);
$("#blueFinalTeam2").text(data.Match.Blue2);
$("#blueFinalTeam3").text(data.Match.Blue3);
$("#blueFinalAuto").text(data.BlueScore.AutoPoints);
$("#blueFinalTeleop").text(data.BlueScore.TeleopPoints);
$("#blueFinalFoul").text(data.BlueScore.FoulPoints);
$("#finalMatchName").text(data.MatchName + " " + data.Match.DisplayName);
};
var transitionBlankToIntro = function(callback) {
$("#centering").transition({queue: false, bottom: "0px"}, 500, "ease", function() {
$(".teams").transition({queue: false, width: "75px"}, 100, "linear", function() {
$(".score").transition({queue: false, width: "120px"}, 500, "ease", function() {
$("#eventMatchInfo").show();
var height = -$("#eventMatchInfo").height();
$("#eventMatchInfo").transition({queue: false, bottom: height + "px"}, 500, "ease", callback);
});
});
});
};
var transitionIntroToInMatch = function(callback) {
$("#logo").transition({queue: false, top: "25px"}, 500, "ease");
$(".score").transition({queue: false, width: "230px"}, 500, "ease", function() {
$(".score-number").transition({queue: false, opacity: 1}, 750, "ease");
$(".score-fields").transition({queue: false, opacity: 1}, 750, "ease");
$("#matchTime").transition({queue: false, opacity: 1}, 750, "ease", callback);
});
};
var transitionIntroToBlank = function(callback) {
$("#eventMatchInfo").transition({queue: false, bottom: "0px"}, 500, "ease", function() {
$("#eventMatchInfo").hide();
$(".score").transition({queue: false, width: "0px"}, 500, "ease");
$(".teams").transition({queue: false, width: "40px"}, 500, "ease", function() {
$("#centering").transition({queue: false, bottom: "-340px"}, 1000, "ease", callback);
});
});
};
var transitionBlankToInMatch = function(callback) {
$("#centering").transition({queue: false, bottom: "0px"}, 500, "ease", function() {
$(".teams").transition({queue: false, width: "75px"}, 100, "linear", function() {
$("#logo").transition({queue: false, top: "25px"}, 500, "ease");
$(".score").transition({queue: false, width: "230px"}, 500, "ease", function() {
$("#eventMatchInfo").show();
$(".score-number").transition({queue: false, opacity: 1}, 750, "ease");
$(".score-fields").transition({queue: false, opacity: 1}, 750, "ease");
$("#matchTime").transition({queue: false, opacity: 1}, 750, "ease", callback);
var height = -$("#eventMatchInfo").height();
$("#eventMatchInfo").transition({queue: false, bottom: height + "px"}, 500, "ease", callback);
});
});
});
}
var transitionInMatchToIntro = function(callback) {
$(".score-number").transition({queue: false, opacity: 0}, 300, "linear");
$(".score-fields").transition({queue: false, opacity: 0}, 300, "linear");
$("#matchTime").transition({queue: false, opacity: 0}, 300, "linear", function() {
$("#logo").transition({queue: false, top: "45px"}, 500, "ease");
$(".score").transition({queue: false, width: "120px"}, 500, "ease");
$(".teams").transition({queue: false, width: "75px"}, 500, "ease", callback);
});
};
var transitionInMatchToBlank = function(callback) {
$("#eventMatchInfo").transition({queue: false, bottom: "0px"}, 500, "ease");
$("#matchTime").transition({queue: false, opacity: 0}, 300, "linear");
$(".score-number").transition({queue: false, opacity: 0}, 300, "linear");
$(".score-fields").transition({queue: false, opacity: 0}, 300, "linear", function() {
$("#eventMatchInfo").hide();
$("#logo").transition({queue: false, top: "45px"}, 500, "ease");
$(".score").transition({queue: false, width: "0px"}, 500, "ease");
$(".teams").transition({queue: false, width: "40px"}, 500, "ease", function() {
$("#centering").transition({queue: false, bottom: "-340px"}, 1000, "ease", callback);
});
});
};
var transitionBlankToLogo = function(callback) {
$(".blinds.right").transition({queue: false, right: 0}, 1000, "ease");
$(".blinds.left").transition({queue: false, left: 0}, 1000, "ease", function() {
$(".blinds.left").addClass("full");
$(".blinds.right").hide();
$(".blinds.center-blank").css({rotateY: "0deg"});
setTimeout(function() {
$(".blinds.center-blank").transition({queue: false, rotateY: "180deg"}, 500, "ease");
$("#blindsCenter").transition({queue: false, rotateY: "0deg"}, 500, "ease", callback);
}, 200);
});
};
var transitionLogoToBlank = function(callback) {
$(".blinds.center-blank").transition({queue: false, rotateY: "360deg"}, 500, "ease");
$("#blindsCenter").transition({queue: false, rotateY: "180deg"}, 500, "ease", function() {
setTimeout(function() {
$(".blinds.left").removeClass("full");
$(".blinds.right").show();
$(".blinds.right").transition({queue: false, right: "-50%"}, 1000, "ease");
$(".blinds.left").transition({queue: false, left: "-50%"}, 1000, "ease", callback);
}, 200);
});
};
var transitionLogoToScore = function(callback) {
$("#blindsCenter").transition({queue: false, top: "-350px"}, 750, "ease", function () {
$("#finalScore").show();
$("#finalScore").transition({queue: false, opacity: 1}, 1000, "ease", callback);
});
};
var transitionBlankToScore = function(callback) {
transitionBlankToLogo(function() {
setTimeout(function() { transitionLogoToScore(callback); }, 100);
});
};
var transitionScoreToLogo = function(callback) {
$("#finalScore").transition({queue: false, opacity: 0}, 500, "linear", function() {
$("#finalScore").hide();
$("#blindsCenter").transition({queue: false, top: 0}, 750, "ease", callback);
});
};
var transitionScoreToBlank = function(callback) {
transitionScoreToLogo(function() {
transitionLogoToBlank(callback);
});
}
$(function() {
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/audience/websocket", {
setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); },
setMatch: function(event) { handleSetMatch(event.data); },
matchTiming: function(event) { handleMatchTiming(event.data); },
matchTime: function(event) { handleMatchTime(event.data); },
realtimeScore: function(event) { handleRealtimeScore(event.data); },
setFinalScore: function(event) { handleSetFinalScore(event.data); }
});
// Map how to transition from one screen to another. Missing links between screens indicate that first we
// must transition to the blank screen and then to the target screen.
transitionMap = {
blank: {
intro: transitionBlankToIntro,
match: transitionBlankToInMatch,
score: transitionBlankToScore,
logo: transitionBlankToLogo
},
intro: {
blank: transitionIntroToBlank,
match: transitionIntroToInMatch
},
match: {
blank: transitionInMatchToBlank,
intro: transitionInMatchToIntro
},
score: {
blank: transitionScoreToBlank,
logo: transitionScoreToLogo
},
logo: {
blank: transitionLogoToBlank,
score: transitionLogoToScore
}
}
});

File diff suppressed because one or more lines are too long

View File

@@ -29,6 +29,10 @@ var discardResults = function() {
websocket.send("discardResults");
};
var setAudienceDisplay = function() {
websocket.send("setAudienceDisplay", $("input[name=audienceDisplay]:checked").val());
};
var handleStatus = function(data) {
// Update the team status view.
$.each(data.AllianceStations, function(station, stationStatus) {
@@ -91,6 +95,10 @@ var handleMatchTime = function(data) {
});
};
var handleSetAudienceDisplay = function(data) {
$("input[name=audienceDisplay][value=" + data + "]").prop("checked", true);
};
$(function() {
// Activate tooltips above the status headers.
$("[data-toggle=tooltip]").tooltip({"placement": "top"});
@@ -99,6 +107,7 @@ $(function() {
websocket = new CheesyWebsocket("/match_play/websocket", {
status: function(event) { handleStatus(event.data); },
matchTiming: function(event) { handleMatchTiming(event.data); },
matchTime: function(event) { handleMatchTime(event.data); }
matchTime: function(event) { handleMatchTime(event.data); },
setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); }
});
});

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<title>Audience Display - {{.EventSettings.Name}} - Cheesy Arena </title>
<link rel="shortcut icon" href="/static/img/favicon32.png">
<link rel="stylesheet" href="/static/css/lib/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/cheesy-arena.css" />
<link rel="stylesheet" href="/static/css/audience_display.css" />
</head>
<body style="background-color: {{.EventSettings.DisplayBackgroundColor}};">
<div id="centering">
<div id="matchOverlay">
<div class="teams" id="redTeams">
<span class="valign-cell">
<span id="redTeam1"></span><br />
<span id="redTeam2"></span><br />
<span id="redTeam3"></span>
</span>
</div>
<div class="score" id="redScore">
<div class="score-number" id="redScoreNumber">&nbsp;</div>
<div class="score-fields">
<div class="assist" id="redAssist1"></div>
<div class="assist" id="redAssist2"></div>
<div class="assist" id="redAssist3"></div>
<div class="trussCatch" id="redTruss">T</div>
<div class="trussCatch" id="redCatch">C</div>
</div>
</div>
<div class="score" id="blueScore">
<div class="score-number pull-right" id="blueScoreNumber">&nbsp;</div>
<div class="score-fields pull-right">
<div class="trussCatch" id="blueTruss">T</div>
<div class="trussCatch" id="blueCatch">C</div>
<div class="assist" id="blueAssist3"></div>
<div class="assist" id="blueAssist2"></div>
<div class="assist" id="blueAssist1"></div>
</div>
</div>
<div class="teams" id="blueTeams">
<span class="valign-cell">
<span id="blueTeam1"></span><br />
<span id="blueTeam2"></span><br />
<span id="blueTeam3"></span>
</span>
</div>
<div id="eventMatchInfo">
<span>{{.EventSettings.Name}} 2014</span>
<span class="pull-right" id="matchName"></span>
</div>
</div>
<div class="text-center" id="matchCircle">
<img id="logo" src="/static/img/logo-min.svg"</img>
<div id="matchTime"></div>
</div>
</div>
<div id="blindsContainer">
<div class="blinds right background"></div>
<div class="blinds right center-blank"></div>
<div class="blinds left background"></div>
<div id="blindsCenter">
<img id="blindsLogo" src="/static/img/logo-min.svg"</img>
</div>
<div class="blinds left center-blank"></div>
<div id="finalScore">
<div class="final-score" id="redFinalScore"></div>
<div class="final-score" id="blueFinalScore"></div>
<div class="final-teams" id="redFinalTeams">
<span id="redFinalTeam1"></span>
<span id="redFinalTeam2"></span>
<span id="redFinalTeam3"></span>
</div>
<div class="final-teams" id="blueFinalTeams">
<span id="blueFinalTeam1"></span>
<span id="blueFinalTeam2"></span>
<span id="blueFinalTeam3"></span>
</div>
<div class="final-breakdown" id="redFinalBreakdown">
<span class="valign-cell">
<span id="redFinalAuto"></span><br />
<span id="redFinalTeleop"></span><br />
<span id="redFinalFoul"></span>
</span>
</div>
<div class="final-breakdown" id="centerFinalBreakdown">
<span class="valign-cell">Autonomous</br>Teleoperated</br>Foul</span>
</div>
<div class="final-breakdown" id="blueFinalBreakdown">
<span class="valign-cell">
<span id="blueFinalAuto"></span><br />
<span id="blueFinalTeleop"></span><br />
<span id="blueFinalFoul"></span>
</span>
</div>
<div id="finalEventMatchInfo">
<span>{{.EventSettings.Name}} 2014</span>
<span class="pull-right" id="finalMatchName"></span>
</div>
</div>
</div>
<script src="/static/js/lib/handlebars-1.3.0.js"></script>
<script src="/static/js/lib/jquery.min.js"></script>
<script src="/static/js/lib/jquery.json-2.4.min.js"></script>
<script src="/static/js/lib/jquery.websocket-0.0.1.js"></script>
<script src="/static/js/lib/jquery.transit.min.js"></script>
<script src="/static/js/cheesy-websocket.js"></script>
<script src="/static/js/match_timing.js"></script>
<script src="/static/js/audience_display.js"></script>
</body>
</html>

View File

@@ -55,8 +55,8 @@
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Display</a>
<ul class="dropdown-menu">
<li class="disabled"><a href="#">Audience</a></li>
<li><a target="_blank" href="/displays/pit">Pit</a></li>
<li><a href="/displays/audience">Audience</a></li>
<li><a href="/displays/pit">Pit</a></li>
<li><a href="/displays/announcer">Announcer</a></li>
<li><a href="/displays/referee">Referee</a></li>
<li><a href="/displays/scoring/red">Scoring &ndash; Red</a></li>

View File

@@ -46,12 +46,12 @@
{{end}}
</div>
</div>
<div class="col-lg-8 text-center">
<div class="row">
<div class="col-lg-8">
<div class="row text-center">
<div id="matchState" class="col-lg-2 col-lg-offset-4 well well-sm">&nbsp;</div>
<div id="matchTime" class="col-lg-2 well well-sm">&nbsp;</div>
</div>
<div class="row">
<div class="row text-center">
<div class="col-lg-6 well well-darkblue">
<div class="row form-group">
<div class="col-lg-4">Blue Teams</div>
@@ -77,7 +77,7 @@
{{template "matchPlayTeam" dict "team" .Match.Red1 "color" "R" "position" 1 "data" .}}
</div>
</div>
<div>
<div class="row text-center">
<button type="button" id="startMatch" class="btn btn-success btn-lg btn-match-play"
onclick="startMatch();" disabled>
Start Match
@@ -96,6 +96,39 @@
Discard Results
</button>
</div>
<br />
<div class="row">
<div class="col-lg-3 well">
Audience Display
<div class="form-group">
<div class="radio">
<label>
<input type="radio" name="audienceDisplay" value="blank" onclick="setAudienceDisplay();">Blank
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="audienceDisplay" value="intro" onclick="setAudienceDisplay();">Intro
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="audienceDisplay" value="match" onclick="setAudienceDisplay();">Match
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="audienceDisplay" value="score" onclick="setAudienceDisplay();">Final Score
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="audienceDisplay" value="logo" onclick="setAudienceDisplay();">Logo
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="confirmCommitResults" class="modal" style="top: 20%;">

2
web.go
View File

@@ -133,6 +133,8 @@ func newHandler() http.Handler {
router.HandleFunc("/reports/pdf/schedule/{type}", SchedulePdfReportHandler).Methods("GET")
router.HandleFunc("/reports/csv/teams", TeamsCsvReportHandler).Methods("GET")
router.HandleFunc("/reports/pdf/teams", TeamsPdfReportHandler).Methods("GET")
router.HandleFunc("/displays/audience", AudienceDisplayHandler).Methods("GET")
router.HandleFunc("/displays/audience/websocket", AudienceDisplayWebsocketHandler).Methods("GET")
router.HandleFunc("/displays/pit", PitDisplayHandler).Methods("GET")
router.HandleFunc("/displays/announcer", AnnouncerDisplayHandler).Methods("GET")
router.HandleFunc("/displays/announcer/websocket", AnnouncerDisplayWebsocketHandler).Methods("GET")

View File

@@ -63,3 +63,14 @@ func readWebsocketType(t *testing.T, ws *Websocket, expectedMessageType string)
}
return message
}
func readWebsocketMultiple(t *testing.T, ws *Websocket, count int) map[string]interface{} {
messages := make(map[string]interface{})
for i := 0; i < count; i++ {
messageType, message, err := ws.Read()
if assert.Nil(t, err) {
messages[messageType] = message
}
}
return messages
}