This commit is contained in:
Patrick Fairbank
2014-08-15 22:48:03 -07:00
24 changed files with 633 additions and 77 deletions

View File

@@ -12,8 +12,9 @@ import (
)
const (
arenaLoopPeriodMs = 10
dsPacketPeriodMs = 250
arenaLoopPeriodMs = 10
dsPacketPeriodMs = 250
matchEndScoreDwellSec = 3
)
// Progression of match states.
@@ -56,30 +57,34 @@ 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
scoringStatusNotifier *Notifier
realtimeScoreNotifier *Notifier
scorePostedNotifier *Notifier
audienceDisplayNotifier *Notifier
playSoundNotifier *Notifier
audienceDisplayScreen string
allianceStationDisplays map[string]string
lastMatchState int
lastMatchTimeSec float64
savedMatch *Match
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
scoringStatusNotifier *Notifier
realtimeScoreNotifier *Notifier
scorePostedNotifier *Notifier
audienceDisplayNotifier *Notifier
playSoundNotifier *Notifier
allianceStationDisplayNotifier *Notifier
allianceSelectionNotifier *Notifier
lowerThirdNotifier *Notifier
audienceDisplayScreen string
allianceStationDisplays map[string]string
allianceStationDisplayScreen string
lastMatchState int
lastMatchTimeSec float64
savedMatch *Match
savedMatchResult *MatchResult
}
var mainArena Arena // Named thusly to avoid polluting the global namespace with something more generic.
@@ -108,6 +113,9 @@ func (arena *Arena) Setup() {
arena.scorePostedNotifier = NewNotifier()
arena.audienceDisplayNotifier = NewNotifier()
arena.playSoundNotifier = NewNotifier()
arena.allianceStationDisplayNotifier = NewNotifier()
arena.allianceSelectionNotifier = NewNotifier()
arena.lowerThirdNotifier = NewNotifier()
// Load empty match as current.
arena.MatchState = PRE_MATCH
@@ -120,6 +128,7 @@ func (arena *Arena) Setup() {
arena.savedMatch = &Match{}
arena.savedMatchResult = &MatchResult{}
arena.allianceStationDisplays = make(map[string]string)
arena.allianceStationDisplayScreen = "blank"
}
// Loads a team into an alliance station, cleaning up the previous team there if there is one.
@@ -402,8 +411,12 @@ func (arena *Arena) Update() {
auto = false
enabled = false
sendDsPacket = true
arena.audienceDisplayScreen = "blank"
arena.audienceDisplayNotifier.Notify(nil)
go func() {
// Leave the scores on the screen briefly at the end of the match.
time.Sleep(time.Second * matchEndScoreDwellSec)
arena.audienceDisplayScreen = "blank"
arena.audienceDisplayNotifier.Notify(nil)
}()
arena.playSoundNotifier.Notify("match-end")
}
}

View File

@@ -23,6 +23,7 @@ type Database struct {
rankingMap *modl.DbMap
teamMap *modl.DbMap
allianceTeamMap *modl.DbMap
lowerThirdMap *modl.DbMap
}
// Opens the SQLite database at the given path, creating it if it doesn't exist, and runs any pending
@@ -75,4 +76,7 @@ func (database *Database) mapTables() {
database.allianceTeamMap = modl.NewDbMap(database.db, dialect)
database.allianceTeamMap.AddTableWithName(AllianceTeam{}, "alliance_teams").SetKeys(true, "Id")
database.lowerThirdMap = modl.NewDbMap(database.db, dialect)
database.lowerThirdMap.AddTableWithName(LowerThird{}, "lower_thirds").SetKeys(true, "Id")
}

View File

@@ -0,0 +1,9 @@
-- +goose Up
CREATE TABLE lower_thirds (
id INTEGER PRIMARY KEY,
toptext VARCHAR(255),
bottomtext VARCHAR(255)
);
-- +goose Down
DROP TABLE lower_thirds;

View File

@@ -60,6 +60,10 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
defer close(scorePostedListener)
playSoundListener := mainArena.playSoundNotifier.Listen()
defer close(playSoundListener)
allianceSelectionListener := mainArena.allianceSelectionNotifier.Listen()
defer close(allianceSelectionListener)
lowerThirdListener := mainArena.lowerThirdNotifier.Listen()
defer close(lowerThirdListener)
// Send the various notifications immediately upon connection.
var data interface{}
@@ -113,6 +117,11 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("allianceSelection", cachedAlliances)
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() {
@@ -173,6 +182,18 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
}
messageType = "playSound"
message = sound
case _, ok := <-allianceSelectionListener:
if !ok {
return
}
messageType = "allianceSelection"
message = cachedAlliances
case lowerThird, ok := <-lowerThirdListener:
if !ok {
return
}
messageType = "lowerThird"
message = lowerThird
}
err = websocket.Write(messageType, message)
if err != nil {
@@ -772,9 +793,14 @@ func AllianceStationDisplayHandler(w http.ResponseWriter, r *http.Request) {
return
}
displayId := ""
if _, ok := r.URL.Query()["displayId"]; ok {
displayId = r.URL.Query()["displayId"][0]
}
data := struct {
*EventSettings
}{eventSettings}
DisplayId string
}{eventSettings, displayId}
err = template.ExecuteTemplate(w, "alliance_station_display.html", data)
if err != nil {
handleWebErr(w, err)
@@ -797,10 +823,13 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque
station = ""
mainArena.allianceStationDisplays[displayId] = station
}
defer delete(mainArena.allianceStationDisplays, displayId)
allianceStationDisplayListener := mainArena.allianceStationDisplayNotifier.Listen()
defer close(allianceStationDisplayListener)
matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen()
defer close(matchLoadTeamsListener)
robotStatusListener := mainArena.robotStatusNotifier.Listen()
defer close(robotStatusListener)
matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener)
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
@@ -808,6 +837,11 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque
// Send the various notifications immediately upon connection.
var data interface{}
err = websocket.Write("setAllianceStationDisplay", mainArena.allianceStationDisplayScreen)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("matchTiming", mainArena.matchTiming)
if err != nil {
log.Printf("Websocket error: %s", err)
@@ -851,6 +885,12 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque
var messageType string
var message interface{}
select {
case _, ok := <-allianceStationDisplayListener:
if !ok {
return
}
messageType = "setAllianceStationDisplay"
message = mainArena.allianceStationDisplayScreen
case _, ok := <-matchLoadTeamsListener:
if !ok {
return
@@ -864,6 +904,12 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque
"R2": mainArena.AllianceStations["R2"].team, "R3": mainArena.AllianceStations["R3"].team,
"B1": mainArena.AllianceStations["B1"].team, "B2": mainArena.AllianceStations["B2"].team,
"B3": mainArena.AllianceStations["B3"].team}}
case _, ok := <-robotStatusListener:
if !ok {
return
}
messageType = "status"
message = mainArena
case matchTimeSec, ok := <-matchTimeListener:
if !ok {
return

46
lower_third.go Normal file
View File

@@ -0,0 +1,46 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model and datastore CRUD methods for the text on a lower third slide.
package main
type LowerThird struct {
Id int
TopText string
BottomText string
}
func (database *Database) CreateLowerThird(lowerThird *LowerThird) error {
return database.lowerThirdMap.Insert(lowerThird)
}
func (database *Database) GetLowerThirdById(id int) (*LowerThird, error) {
lowerThird := new(LowerThird)
err := database.lowerThirdMap.Get(lowerThird, id)
if err != nil && err.Error() == "sql: no rows in result set" {
lowerThird = nil
err = nil
}
return lowerThird, err
}
func (database *Database) SaveLowerThird(lowerThird *LowerThird) error {
_, err := database.lowerThirdMap.Update(lowerThird)
return err
}
func (database *Database) DeleteLowerThird(lowerThird *LowerThird) error {
_, err := database.lowerThirdMap.Delete(lowerThird)
return err
}
func (database *Database) TruncateLowerThirds() error {
return database.lowerThirdMap.TruncateTables()
}
func (database *Database) GetAllLowerThirds() ([]LowerThird, error) {
var lowerThirds []LowerThird
err := database.teamMap.Select(&lowerThirds, "SELECT * FROM lower_thirds ORDER BY id")
return lowerThirds, err
}

View File

@@ -161,6 +161,8 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
defer close(audienceDisplayListener)
scoringStatusListener := mainArena.scoringStatusNotifier.Listen()
defer close(scoringStatusListener)
allianceStationDisplayListener := mainArena.allianceStationDisplayNotifier.Listen()
defer close(allianceStationDisplayListener)
// Send the various notifications immediately upon connection.
var data interface{}
@@ -196,6 +198,11 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("setAllianceStationDisplay", mainArena.allianceStationDisplayScreen)
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() {
@@ -232,6 +239,12 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
BlueScoreReady bool
}{mainArena.redRealtimeScore.FoulsCommitted && mainArena.blueRealtimeScore.FoulsCommitted,
mainArena.redRealtimeScore.TeleopCommitted, mainArena.blueRealtimeScore.TeleopCommitted}
case _, ok := <-allianceStationDisplayListener:
if !ok {
return
}
messageType = "setAllianceStationDisplay"
message = mainArena.allianceStationDisplayScreen
}
err = websocket.Write(messageType, message)
if err != nil {
@@ -340,6 +353,15 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
mainArena.audienceDisplayScreen = screen
mainArena.audienceDisplayNotifier.Notify(nil)
continue
case "setAllianceStationDisplay":
screen, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
mainArena.allianceStationDisplayScreen = screen
mainArena.allianceStationDisplayNotifier.Notify(nil)
continue
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
continue

View File

@@ -72,6 +72,8 @@ func AllianceSelectionPostHandler(w http.ResponseWriter, r *http.Request) {
}
}
}
mainArena.allianceSelectionNotifier.Notify(nil)
http.Redirect(w, r, "/setup/alliance_selection", 302)
}
@@ -109,6 +111,8 @@ func AllianceSelectionStartHandler(w http.ResponseWriter, r *http.Request) {
for i, ranking := range rankings {
cachedRankedTeams[i] = &RankedTeam{i + 1, ranking.TeamId, false}
}
mainArena.allianceSelectionNotifier.Notify(nil)
http.Redirect(w, r, "/setup/alliance_selection", 302)
}
@@ -121,6 +125,7 @@ func AllianceSelectionResetHandler(w http.ResponseWriter, r *http.Request) {
cachedAlliances = [][]*AllianceTeam{}
cachedRankedTeams = []*RankedTeam{}
mainArena.allianceSelectionNotifier.Notify(nil)
http.Redirect(w, r, "/setup/alliance_selection", 302)
}

78
setup_lower_thirds.go Normal file
View File

@@ -0,0 +1,78 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web routes for managing lower thirds.
package main
import (
"html/template"
"net/http"
"strconv"
)
// Shows the lower third configuration page.
func LowerThirdsGetHandler(w http.ResponseWriter, r *http.Request) {
template, err := template.ParseFiles("templates/lower_thirds.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
return
}
lowerThirds, err := db.GetAllLowerThirds()
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
LowerThirds []LowerThird
}{eventSettings, lowerThirds}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// Saves the new or modified lower third to the database and triggers showing it on the audience display.
func LowerThirdsPostHandler(w http.ResponseWriter, r *http.Request) {
lowerThirdId, _ := strconv.Atoi(r.PostFormValue("id"))
lowerThird, err := db.GetLowerThirdById(lowerThirdId)
if err != nil {
handleWebErr(w, err)
return
}
if r.PostFormValue("action") == "delete" {
err := db.DeleteLowerThird(lowerThird)
if err != nil {
handleWebErr(w, err)
return
}
} else {
// Save the lower third even if the show or hide buttons were clicked.
if lowerThird == nil {
lowerThird = &LowerThird{TopText: r.PostFormValue("topText"),
BottomText: r.PostFormValue("bottomText")}
err = db.CreateLowerThird(lowerThird)
} else {
lowerThird.TopText = r.PostFormValue("topText")
lowerThird.BottomText = r.PostFormValue("bottomText")
err = db.SaveLowerThird(lowerThird)
}
if err != nil {
handleWebErr(w, err)
return
}
if r.PostFormValue("action") == "show" {
mainArena.lowerThirdNotifier.Notify(lowerThird)
mainArena.audienceDisplayScreen = "lowerThird"
mainArena.audienceDisplayNotifier.Notify(nil)
} else if r.PostFormValue("action") == "hide" {
mainArena.audienceDisplayScreen = "blank"
mainArena.audienceDisplayNotifier.Notify(nil)
}
}
http.Redirect(w, r, "/setup/lower_thirds", 302)
}

View File

@@ -8,28 +8,60 @@ body {
background-color: #000;
font-family: "FuturaLTBold";
}
#match {
position: absolute;
width: 100%;
height: 100%;
}
#displayId {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto auto;
height: 1px;
line-height: 1px;
text-align: center;
color: #ff0;
font-size: 500px;
}
#teamId {
position: absolute;
top: 7%;
left: 0;
right: 0;
margin: 0 auto;
font-size: 500px;
line-height: 500px;
text-align: center;
margin: 50px 0px;
color: #fff;
}
#teamName {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
height: 200px;
line-height: 200px;
text-align: center;
font-family: "FuturaLT";
font-size: 120px;
}
[data-alliance=R] {
color: #f00;
}
[data-alliance=B] {
color: #00f;
}
.match-info {
color: #fff;
}
#matchInfo {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
height: 200px;
line-height: 200px;
text-align: center;
font-size: 190px;
color: #fff;
}
#redScore {
color: #f00;
@@ -37,3 +69,21 @@ body {
#blueScore {
color: #00f;
}
#logo {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 90%;
margin: auto auto;
}
[data-alliance=R], [data-status=R] {
background-color: #f00;
}
[data-alliance=B], [data-status=B] {
background-color: #00f;
}
[data-status=bypass] {
background-color: #999;
}

View File

@@ -5,7 +5,8 @@ html {
overflow: hidden;
}
#centering {
position: absolute; left: 50%;
position: absolute;
left: 50%;
bottom: -340px;
}
#matchOverlay {
@@ -275,3 +276,61 @@ html {
font-size: 28px;
color: #fff;
}
#allianceSelectionCentering {
position: absolute;
left: 50%;
height: 100%;
display: table;
}
#allianceSelection {
position: relative;
left: -50%;
display: table-cell;
vertical-align: middle;
}
#allianceSelectionTable {
background-color: #006;
border-radius: 20px;
text-align: center;
font-family: "FuturaLTBold";
font-size: 70px;
}
.alliance-cell {
padding: 0px 40px;
font-family: "FuturaLT";
color: #999;
}
.selection-cell {
width: 250px;
color: #fff;
}
#lowerThird {
display: none;
position: absolute;
left: -1000px;
bottom: 100px;
background-color: #fff;
border: 1px solid #000;
color: #000;
font-size: 30px;
width: 800px;
height: 87px;
}
#lowerThirdLogo {
margin: 20px;
height: 45px;
float: left;
}
#lowerThirdTop {
display: none;
font-family: "FuturaLTBold";
}
#lowerThirdBottom {
display: none;
font-family: "FuturaLT";
}
#lowerThirdSingle {
display: none;
font-family: "FuturaLTBold";
line-height: 87px;
}

View File

@@ -88,3 +88,6 @@
.scoring-message {
color: #f00;
}
.btn-lower-third {
width: 80px;
}

View File

@@ -15,7 +15,7 @@ body {
background: -webkit-linear-gradient(top, #003375 1%, #3C679D 100%); /* Chrome10+,Safari5.1+ */
background: -webkit-linear-gradient(top, #003375 1%, #3C679D 100%); /* Chrome10+,Safari5.1+ */
background-repeat: no-repeat;
font-family: sans-serif;
font-family: "FuturaLT";
}
#column {
width: 80%;
@@ -26,7 +26,7 @@ body {
padding: 20px 0px;
line-height: 50px;
font-size: 40px;
font-weight: bold;
font-family: "FuturaLTBold";
color: #fff;
}
#logo {

BIN
static/img/logo-black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
static/img/logo-colored.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -3,11 +3,27 @@
//
// Client-side methods for the alliance station display.
// A unique id to differentiate this station's display from its peers.
var displayId;
var allianceStation = "";
var blinkInterval;
var websocket;
var handleSetAllianceStationDisplay = function(targetScreen) {
switch (targetScreen) {
case "logo":
$("#match").hide();
$("#logo").show();
break;
case "blank":
$("#match").hide();
$("#logo").hide();
break;
case "match":
$("#match").show();
$("#logo").hide();
break;
}
};
var handleSetMatch = function(data) {
if (allianceStation != "" && data.AllianceStation == "") {
// The client knows better what display this should be; let the server know.
@@ -22,22 +38,49 @@ var handleSetMatch = function(data) {
if (team == null) {
$("#teamId").text("");
$("#teamName").text("");
$("#teamName").attr("data-alliance", "");
} else {
$("#teamId").attr("data-alliance", allianceStation[0]);
$("#teamName").attr("data-alliance", allianceStation[0]);
$("#teamId").text(data.Teams[allianceStation].Id);
$("#teamName").text(data.Teams[allianceStation].Nickname);
}
$("#displayIdRow").hide();
$("#teamIdRow").show();
$("#teamNameRow").show();
$("#displayId").hide();
$("#teamId").show();
$("#teamName").show();
} else {
// Show the display ID so that someone can assign it to a station from the configuration interface.
$("#teamId").text("");
$("#teamName").text("");
$("#displayIdRow").show();
$("#teamIdRow").hide();
$("#teamNameRow").hide();
$("#displayId").show();
$("#teamId").hide();
$("#teamName").hide();
}
};
var handleStatus = function(data) {
stationStatus = data.AllianceStations[allianceStation];
var blink = false;
if (stationStatus.Bypass) {
$("#match").attr("data-status", "bypass");
} else if (stationStatus.DsConn) {
if (!stationStatus.DsConn.DriverStationStatus.DsLinked) {
$("#match").attr("data-status", allianceStation[0]);
} else if (!stationStatus.DsConn.DriverStationStatus.RobotLinked) {
blink = true;
if (!blinkInterval) {
blinkInterval = setInterval(function() {
var status = $("#match").attr("data-status");
$("#match").attr("data-status", (status == "") ? allianceStation[0] : "");
}, 250);
}
}
} else {
$("#match").attr("data-status", "");
}
if (!blink && blinkInterval) {
clearInterval(blinkInterval);
blinkInterval = null;
}
};
@@ -51,11 +94,11 @@ var handleMatchTime = function(data) {
$("#matchTime").text(countdownString);
if (matchState == "PRE_MATCH" || matchState == "POST_MATCH") {
$("#teamNameRow").show();
$("#matchInfoRow").hide();
$("#teamName").show();
$("#matchInfo").hide();
} else {
$("#teamNameRow").hide();
$("#matchInfoRow").show();
$("#teamName").hide();
$("#matchInfo").show();
}
});
};
@@ -66,12 +109,17 @@ var handleRealtimeScore = function(data) {
};
$(function() {
displayId = Math.floor(Math.random() * 10000);
if (displayId == "") {
displayId = Math.floor(Math.random() * 10000);
window.location = "/displays/alliance_station?displayId=" + displayId;
}
$("#displayId").text(displayId);
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/alliance_station/websocket?displayId=" + displayId, {
setAllianceStationDisplay: function(event) { handleSetAllianceStationDisplay(event.data); },
setMatch: function(event) { handleSetMatch(event.data); },
status: function(event) { handleStatus(event.data); },
matchTiming: function(event) { handleMatchTiming(event.data); },
matchTime: function(event) { handleMatchTime(event.data); },
realtimeScore: function(event) { handleRealtimeScore(event.data); }

View File

@@ -6,6 +6,7 @@
var websocket;
var transitionMap;
var currentScreen = "blank";
var allianceSelectionTemplate = Handlebars.compile($("#allianceSelectionTemplate").html());
var handleSetAudienceDisplay = function(targetScreen) {
if (targetScreen == currentScreen) {
@@ -87,6 +88,30 @@ var handlePlaySound = function(sound) {
$("#" + sound)[0].play();
};
var handleAllianceSelection = function(alliances) {
if (alliances) {
$.each(alliances, function(k, v) {
v.Index = k + 1;
});
$("#allianceSelection").html(allianceSelectionTemplate(alliances));
}
};
var handleLowerThird = function(data) {
if (data.BottomText == "") {
$("#lowerThirdTop").hide();
$("#lowerThirdBottom").hide();
$("#lowerThirdSingle").text(data.TopText);
$("#lowerThirdSingle").show();
} else {
$("#lowerThirdSingle").hide();
$("#lowerThirdTop").text(data.TopText);
$("#lowerThirdBottom").text(data.BottomText);
$("#lowerThirdTop").show();
$("#lowerThirdBottom").show();
}
};
var transitionBlankToIntro = function(callback) {
$("#centering").transition({queue: false, bottom: "0px"}, 500, "ease", function() {
$(".teams").transition({queue: false, width: "75px"}, 100, "linear", function() {
@@ -209,6 +234,34 @@ var transitionScoreToBlank = function(callback) {
});
}
var transitionBlankToAllianceSelection = function(callback) {
$("#allianceSelectionCentering").show();
if (callback) {
callback();
}
};
var transitionAllianceSelectionToBlank = function(callback) {
$("#allianceSelectionCentering").hide();
if (callback) {
callback();
}
};
var transitionBlankToLowerThird = function(callback) {
$("#lowerThird").show();
$("#lowerThird").transition({queue: false, left: "150px"}, 750, "ease", callback);
};
var transitionLowerThirdToBlank = function(callback) {
$("#lowerThird").transition({queue: false, left: "-1000px"}, 1000, "ease", function() {
$("#lowerThird").hide();
if (callback) {
callback();
}
});
};
$(function() {
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/audience/websocket", {
@@ -218,7 +271,9 @@ $(function() {
matchTime: function(event) { handleMatchTime(event.data); },
realtimeScore: function(event) { handleRealtimeScore(event.data); },
setFinalScore: function(event) { handleSetFinalScore(event.data); },
playSound: function(event) { handlePlaySound(event.data); }
playSound: function(event) { handlePlaySound(event.data); },
allianceSelection: function(event) { handleAllianceSelection(event.data); },
lowerThird: function(event) { handleLowerThird(event.data); }
});
// Map how to transition from one screen to another. Missing links between screens indicate that first we
@@ -228,7 +283,9 @@ $(function() {
intro: transitionBlankToIntro,
match: transitionBlankToInMatch,
score: transitionBlankToScore,
logo: transitionBlankToLogo
logo: transitionBlankToLogo,
allianceSelection: transitionBlankToAllianceSelection,
lowerThird: transitionBlankToLowerThird
},
intro: {
blank: transitionIntroToBlank,
@@ -245,6 +302,12 @@ $(function() {
logo: {
blank: transitionLogoToBlank,
score: transitionLogoToScore
},
allianceSelection: {
blank: transitionAllianceSelectionToBlank
},
lowerThird: {
blank: transitionLowerThirdToBlank
}
}
});

View File

@@ -34,6 +34,10 @@ var setAudienceDisplay = function() {
websocket.send("setAudienceDisplay", $("input[name=audienceDisplay]:checked").val());
};
var setAllianceStationDisplay = function() {
websocket.send("setAllianceStationDisplay", $("input[name=allianceStationDisplay]:checked").val());
};
var confirmCommit = function(isReplay) {
if (isReplay || !scoreIsReady) {
// Show the appropriate message(s) in the confirmation dialog.
@@ -108,6 +112,7 @@ var handleMatchTime = function(data) {
};
var handleSetAudienceDisplay = function(data) {
$("input[name=audienceDisplay]:checked").prop("checked", false);
$("input[name=audienceDisplay][value=" + data + "]").prop("checked", true);
};
@@ -118,6 +123,11 @@ var handleScoringStatus = function(data) {
$("#blueScoreStatus").attr("data-ready", data.BlueScoreReady);
};
var handleSetAllianceStationDisplay = function(data) {
$("input[name=allianceStationDisplay]:checked").prop("checked", false);
$("input[name=allianceStationDisplay][value=" + data + "]").prop("checked", true);
};
$(function() {
// Activate tooltips above the status headers.
$("[data-toggle=tooltip]").tooltip({"placement": "top"});
@@ -128,6 +138,7 @@ $(function() {
matchTiming: function(event) { handleMatchTiming(event.data); },
matchTime: function(event) { handleMatchTime(event.data); },
setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); },
scoringStatus: function(event) { handleScoringStatus(event.data); }
scoringStatus: function(event) { handleScoringStatus(event.data); },
setAllianceStationDisplay: function(event) { handleSetAllianceStationDisplay(event.data); }
});
});

View File

@@ -8,20 +8,21 @@
<link rel="stylesheet" href="/static/css/alliance_station_display.css" />
</head>
<body>
<div class="row" id="displayIdRow" style="display: none;">>
<div class="col-lg-12 text-center" id="displayId">1718</div>
</div>
<div class="row" id="teamIdRow" style="display: none;">>
<div class="col-lg-12 text-center" id="teamId">254</div>
</div>
<div class="row" id="teamNameRow" style="display: none;">
<div class="col-lg-12 text-center" id="teamName">Buchanan Bird Brains</div>
</div>
<div class="row" id="matchInfoRow" style="display: none;">
<div class="col-lg-4 text-center match-info" id="redScore"></div>
<div class="col-lg-4 text-center match-info" id="matchTime"></div>
<div class="col-lg-4 text-center match-info" id="blueScore"></div>
<div id="displayId" style="display: none;"></div>
<div id="match" style="display: none;">
<div id="teamId" style="display: none;"></div>
<div id="teamName" style="display: none;"></div>
<div class="row" id="matchInfo" style="display: none;">
<div class="col-lg-4 text-center" id="redScore"></div>
<div class="col-lg-4 text-center" id="matchTime"></div>
<div class="col-lg-4 text-center" id="blueScore"></div>
</div>
</div>
<img id="logo" src="/static/img/logo-black.png" style="display: none;"/>
<script>
// A unique id to differentiate this station's display from its peers.
var displayId = "{{.DisplayId}}";
</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>

View File

@@ -50,7 +50,7 @@
</div>
</div>
<div class="text-center" id="matchCircle">
<img id="logo" src="/static/img/logo-min.svg"</img>
<img id="logo" src="/static/img/logo-min.svg" />
<div id="matchTime"></div>
</div>
</div>
@@ -59,7 +59,7 @@
<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>
<img id="blindsLogo" src="/static/img/logo-min.svg" />
</div>
<div class="blinds left center-blank"></div>
<div id="finalScore">
@@ -98,6 +98,27 @@
</div>
</div>
</div>
<div id="allianceSelectionCentering" style="display: none;">
<div id="allianceSelection"></div>
</div>
<div id="lowerThird">
<img id="lowerThirdLogo" src="/static/img/logo-min.svg" />
<div id="lowerThirdTop"></div>
<div id="lowerThirdBottom"></div>
<div id="lowerThirdSingle"></div>
</div>
<script id="allianceSelectionTemplate" type="text/x-handlebars-template">
<table id="allianceSelectionTable">
{{"{{#each this}}"}}
<tr>
<td class="alliance-cell">{{"{{Index}}"}}</td>
{{"{{#each this}}"}}
<td class="selection-cell">{{"{{#if TeamId}}"}}{{"{{TeamId}}"}}{{"{{/if}}"}}</td>
{{"{{/each}}"}}
</tr>
{{"{{/each}}"}}
</table>
</script>
<audio id="match-start" src="/static/audio/match_start.wav" type="audio/wav" preload="auto" />
<audio id="match-end" src="/static/audio/match_end.wav" type="audio/wav" preload="auto" />
<audio id="match-abort" src="/static/audio/match_abort.mp3" type="audio/mp3" preload="auto" />
@@ -107,6 +128,7 @@
<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/lib/handlebars-1.3.0.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>

View File

@@ -25,6 +25,7 @@
<li><a href="/setup/teams">Team List</a></li>
<li><a href="/setup/schedule">Match Scheduling</a></li>
<li><a href="/setup/alliance_selection">Alliance Selection</a></li>
<li><a href="/setup/lower_thirds">Lower Thirds</a></li>
</ul>
</li>
<li class="dropdown">

View File

@@ -0,0 +1,43 @@
{{define "title"}}Lower Thirds{{end}}
{{define "body"}}
<div class="row">
<div class="col-lg-6 col-lg-offset-3">
<div class="well">
<legend>Lower Thirds</legend>
{{range $lowerThird := .LowerThirds}}
<form class="form-horizontal" action="/setup/lower_thirds" method="POST">
<div class="form-group">
<div class="col-lg-7">
<input type="hidden" name="id" value="{{$lowerThird.Id}}" />
<input type="text" class="form-control" name="topText" value="{{$lowerThird.TopText}}"
placeholder="Top Text"/>
<input type="text" class="form-control" name="bottomText" value="{{$lowerThird.BottomText}}"
placeholder="Bottom Text"/>
</div>
<div class="col-lg-5">
<button type="submit" class="btn btn-info btn-lower-third" name="action" value="save">Save</button>
<button type="submit" class="btn btn-success btn-lower-third" name="action" value="show">Show</button>
<br />
<button type="submit" class="btn btn-primary btn-lower-third" name="action" value="delete">Delete</button>
<button type="submit" class="btn btn-default btn-lower-third" name="action" value="hide">Hide</button>
</div>
</div>
</form>
{{end}}
<form class="form-horizontal" action="/setup/lower_thirds" method="POST">
<div class="form-group">
<div class="col-lg-7">
<input type="text" class="form-control" name="topText" placeholder="Top or Solo Text" />
<input type="text" class="form-control" name="bottomText" placeholder="Bottom Text" />
</div>
<div class="col-lg-5">
<button type="submit" class="btn btn-info btn-lower-third" name="action" value="save">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
{{end}}
{{define "script"}}
{{end}}

View File

@@ -97,8 +97,8 @@
</div>
<br />
<div class="row">
<div class="col-lg-6 well">
<div class="col-lg-6">
<div class="col-lg-9 well">
<div class="col-lg-4">
Audience Display
<div class="form-group">
<div class="radio">
@@ -126,14 +126,43 @@
<input type="radio" name="audienceDisplay" value="logo" onclick="setAudienceDisplay();">Logo
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="audienceDisplay" value="allianceSelection"
onclick="setAudienceDisplay();">Alliance Selection
</label>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="col-lg-4">
<p>Scoring Status</p>
<p><span class="label label-scoring" id="refereeScoreStatus">Referee</span><br />
<span class="label label-scoring" id="redScoreStatus">Red Scoring</span><br />
<span class="label label-scoring" id="blueScoreStatus">Blue Scoring</span></p>
</div>
<div class="col-lg-4">
Alliance Station Display
<div class="form-group">
<div class="radio">
<label>
<input type="radio" name="allianceStationDisplay" value="blank"
onclick="setAllianceStationDisplay();">Blank
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="allianceStationDisplay" value="match"
onclick="setAllianceStationDisplay();">Match
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="allianceStationDisplay" value="logo"
onclick="setAllianceStationDisplay();">Logo
</label>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -9,13 +9,14 @@
<title>Pit 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/pit_display.css" />
</head>
<body>
<div id="column">
<div id="titlebar">
<div class="pull-left">TEAM STANDINGS</div>
<div class="pull-right"><img id="logo" src="/static/img/logo.png"></div>
<div class="pull-right"><img id="logo" src="/static/img/logo-colored.png"></div>
<div>&nbsp</div>
</div>
<div id="standings">

2
web.go
View File

@@ -123,6 +123,8 @@ func newHandler() http.Handler {
router.HandleFunc("/setup/alliance_selection/finalize", AllianceSelectionFinalizeHandler).Methods("POST")
router.HandleFunc("/setup/field", FieldGetHandler).Methods("GET")
router.HandleFunc("/setup/field", FieldPostHandler).Methods("POST")
router.HandleFunc("/setup/lower_thirds", LowerThirdsGetHandler).Methods("GET")
router.HandleFunc("/setup/lower_thirds", LowerThirdsPostHandler).Methods("POST")
router.HandleFunc("/match_play", MatchPlayHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/load", MatchPlayLoadHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/show_result", MatchPlayShowResultHandler).Methods("GET")