diff --git a/db/migrations/20140524160241_CreateEventSettings.sql b/db/migrations/20140524160241_CreateEventSettings.sql index fa1248f..cbfa345 100644 --- a/db/migrations/20140524160241_CreateEventSettings.sql +++ b/db/migrations/20140524160241_CreateEventSettings.sql @@ -2,7 +2,6 @@ CREATE TABLE event_settings ( id INTEGER PRIMARY KEY, name VARCHAR(255), - displaybackgroundcolor VARCHAR(16), numelimalliances int, selectionround2order VARCHAR(1), selectionround3order VARCHAR(1), diff --git a/field/arena.go b/field/arena.go index 9b5a6ca..545ba22 100644 --- a/field/arena.go +++ b/field/arena.go @@ -47,6 +47,7 @@ type Arena struct { TbaClient *partner.TbaClient StemTvClient *partner.StemTvClient AllianceStations map[string]*AllianceStation + Displays map[string]*Display ArenaNotifiers MatchState lastMatchState MatchState @@ -61,7 +62,6 @@ type Arena struct { AudienceDisplayMode string SavedMatch *model.Match SavedMatchResult *model.MatchResult - AllianceStationDisplays map[string]string AllianceStationDisplayMode string MuteMatchSounds bool matchAborted bool @@ -110,6 +110,8 @@ func NewArena(dbPath string) (*Arena, error) { arena.AllianceStations["B2"] = new(AllianceStation) arena.AllianceStations["B3"] = new(AllianceStation) + arena.Displays = make(map[string]*Display) + arena.configureNotifiers() // Load empty match as current. @@ -122,7 +124,6 @@ func NewArena(dbPath string) (*Arena, error) { arena.AudienceDisplayMode = "blank" arena.SavedMatch = &model.Match{} arena.SavedMatchResult = model.NewMatchResult() - arena.AllianceStationDisplays = make(map[string]string) arena.AllianceStationDisplayMode = "match" return arena, nil diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index facbeac..a4ec9d3 100644 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -20,6 +20,7 @@ type ArenaNotifiers struct { AllianceStationDisplayModeNotifier *websocket.Notifier ArenaStatusNotifier *websocket.Notifier AudienceDisplayModeNotifier *websocket.Notifier + DisplayConfigurationNotifier *websocket.Notifier LedModeNotifier *websocket.Notifier LowerThirdNotifier *websocket.Notifier MatchLoadNotifier *websocket.Notifier @@ -31,6 +32,11 @@ type ArenaNotifiers struct { ScoringStatusNotifier *websocket.Notifier } +type DisplayConfigurationMessage struct { + Displays map[string]*Display + DisplayUrls map[string]string +} + type LedModeMessage struct { LedMode led.Mode VaultLedMode vaultled.Mode @@ -58,6 +64,8 @@ func (arena *Arena) configureNotifiers() { arena.ArenaStatusNotifier = websocket.NewNotifier("arenaStatus", arena.generateArenaStatusMessage) arena.AudienceDisplayModeNotifier = websocket.NewNotifier("audienceDisplayMode", arena.generateAudienceDisplayModeMessage) + arena.DisplayConfigurationNotifier = websocket.NewNotifier("displayConfiguration", + arena.generateDisplayConfigurationMessage) arena.LedModeNotifier = websocket.NewNotifier("ledMode", arena.generateLedModeMessage) arena.LowerThirdNotifier = websocket.NewNotifier("lowerThird", nil) arena.MatchLoadNotifier = websocket.NewNotifier("matchLoad", arena.generateMatchLoadMessage) @@ -89,6 +97,14 @@ func (arena *Arena) generateAudienceDisplayModeMessage() interface{} { return arena.AudienceDisplayMode } +func (arena *Arena) generateDisplayConfigurationMessage() interface{} { + displayUrls := make(map[string]string) + for displayId, display := range arena.Displays { + displayUrls[displayId] = display.ToUrl() + } + return &DisplayConfigurationMessage{arena.Displays, displayUrls} +} + func (arena *Arena) generateLedModeMessage() interface{} { return &LedModeMessage{arena.ScaleLeds.GetCurrentMode(), arena.RedVaultLeds.CurrentForceMode} } diff --git a/field/display.go b/field/display.go new file mode 100644 index 0000000..d5fde94 --- /dev/null +++ b/field/display.go @@ -0,0 +1,178 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Model representing and methods for controlling a remote web display. + +package field + +import ( + "fmt" + "math/rand" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "sync" +) + +const ( + minDisplayId = 100 + maxDisplayId = 999 +) + +type DisplayType int + +const ( + InvalidDisplay DisplayType = iota + PlaceholderDisplay + AllianceStationDisplay + AnnouncerDisplay + AudienceDisplay + FieldMonitorDisplay + PitDisplay +) + +var DisplayTypeNames = map[DisplayType]string{ + PlaceholderDisplay: "Placeholder", + AllianceStationDisplay: "Alliance Station", + AnnouncerDisplay: "Announcer", + AudienceDisplay: "Audience", + FieldMonitorDisplay: "Field Monitor", + PitDisplay: "Pit", +} + +var displayTypePaths = map[DisplayType]string{ + PlaceholderDisplay: "/display", + AllianceStationDisplay: "/displays/alliance_station", + AnnouncerDisplay: "/displays/announcer", + AudienceDisplay: "/displays/audience", + FieldMonitorDisplay: "/displays/fta", + PitDisplay: "/displays/pit", +} + +var displayRegistryMutex sync.Mutex + +type Display struct { + Id string + Nickname string + Type DisplayType + Configuration map[string]string + ConnectionCount int +} + +// Parses the given display URL path and query string to extract the configuration. +func DisplayFromUrl(path string, query map[string][]string) (*Display, error) { + if _, ok := query["displayId"]; !ok { + return nil, fmt.Errorf("Display ID not present in request.") + } + + var display Display + display.Id = query["displayId"][0] + if nickname, ok := query["nickname"]; ok { + display.Nickname, _ = url.QueryUnescape(nickname[0]) + } + + // Determine type from the websocket connection URL. This way of doing it isn't super efficient, but it's not really + // a concern since it should happen relatively infrequently. + for displayType, displayPath := range displayTypePaths { + if path == displayPath+"/websocket" { + display.Type = displayType + break + } + } + if display.Type == InvalidDisplay { + return nil, fmt.Errorf("Could not determine display type from path %s.", path) + } + + // Put any remaining query parameters into the per-type configuration map. + display.Configuration = make(map[string]string) + for key, value := range query { + if key != "displayId" && key != "nickname" { + display.Configuration[key], _ = url.QueryUnescape(value[0]) + } + } + + return &display, nil +} + +// Returns the URL string for the given display that includes all of its configuration parameters. +func (display *Display) ToUrl() string { + var builder strings.Builder + builder.WriteString(displayTypePaths[display.Type]) + builder.WriteString("?displayId=") + builder.WriteString(url.QueryEscape(display.Id)) + if display.Nickname != "" { + builder.WriteString("&nickname=") + builder.WriteString(url.QueryEscape(display.Nickname)) + } + + // Sort the keys so that the URL generated is deterministic. + var keys []string + for key := range display.Configuration { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + builder.WriteString("&") + builder.WriteString(url.QueryEscape(key)) + builder.WriteString("=") + builder.WriteString(url.QueryEscape(display.Configuration[key])) + } + return builder.String() +} + +// Returns an unused ID that can be used for a new display. +func (arena *Arena) NextDisplayId() string { + // Loop until we get an ID that isn't already used. This is inefficient if there is a large number of displays, but + // that should never be the case. + for { + candidateId := strconv.Itoa(rand.Intn(maxDisplayId+1-minDisplayId) + minDisplayId) + if _, ok := arena.Displays[candidateId]; !ok { + return candidateId + } + } +} + +// Adds the given display to the arena registry and triggers a notification. +func (arena *Arena) RegisterDisplay(display *Display) { + displayRegistryMutex.Lock() + defer displayRegistryMutex.Unlock() + + existingDisplay, ok := arena.Displays[display.Id] + if ok { + display.ConnectionCount = existingDisplay.ConnectionCount + 1 + } else { + display.ConnectionCount = 1 + } + arena.Displays[display.Id] = display + arena.DisplayConfigurationNotifier.Notify() +} + +// Updates the given display in the arena registry. Triggers a notification if the display configuration changed. +func (arena *Arena) UpdateDisplay(display *Display) error { + displayRegistryMutex.Lock() + defer displayRegistryMutex.Unlock() + + existingDisplay, ok := arena.Displays[display.Id] + if !ok { + return fmt.Errorf("Display %s doesn't exist.", display.Id) + } + display.ConnectionCount = existingDisplay.ConnectionCount + if !reflect.DeepEqual(existingDisplay, display) { + arena.Displays[display.Id] = display + arena.DisplayConfigurationNotifier.Notify() + } + return nil +} + +// Marks the given display as having disconnected in the arena registry and triggers a notification. +func (arena *Arena) MarkDisplayDisconnected(display *Display) { + displayRegistryMutex.Lock() + defer displayRegistryMutex.Unlock() + + if display, ok := arena.Displays[display.Id]; ok { + display.ConnectionCount -= 1 + arena.DisplayConfigurationNotifier.Notify() + } +} diff --git a/field/display_test.go b/field/display_test.go new file mode 100644 index 0000000..6eaa8df --- /dev/null +++ b/field/display_test.go @@ -0,0 +1,126 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) + +package field + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDisplayFromUrl(t *testing.T) { + query := map[string][]string{} + display, err := DisplayFromUrl("/display", query) + assert.Nil(t, display) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "ID not present") + } + + // Test the various types. + query["displayId"] = []string{"123"} + display, err = DisplayFromUrl("/blorpy", query) + assert.Nil(t, display) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "Could not determine display type") + } + display, _ = DisplayFromUrl("/display/websocket", query) + assert.Equal(t, PlaceholderDisplay, display.Type) + display, _ = DisplayFromUrl("/displays/alliance_station/websocket", query) + assert.Equal(t, AllianceStationDisplay, display.Type) + display, _ = DisplayFromUrl("/displays/announcer/websocket", query) + assert.Equal(t, AnnouncerDisplay, display.Type) + display, _ = DisplayFromUrl("/displays/audience/websocket", query) + assert.Equal(t, AudienceDisplay, display.Type) + display, _ = DisplayFromUrl("/displays/fta/websocket", query) + assert.Equal(t, FieldMonitorDisplay, display.Type) + display, _ = DisplayFromUrl("/displays/pit/websocket", query) + assert.Equal(t, PitDisplay, display.Type) + + // Test the nickname and arbitrary parameters. + query["nickname"] = []string{"Test Nickname"} + query["key1"] = []string{"value1"} + query["key2"] = []string{"value2"} + query["color"] = []string{"%230f0"} + display, _ = DisplayFromUrl("/display/websocket", query) + assert.Equal(t, "Test Nickname", display.Nickname) + if assert.Equal(t, 3, len(display.Configuration)) { + assert.Equal(t, "value1", display.Configuration["key1"]) + assert.Equal(t, "value2", display.Configuration["key2"]) + assert.Equal(t, "#0f0", display.Configuration["color"]) + } +} + +func TestDisplayToUrl(t *testing.T) { + display := &Display{Id: "254", Nickname: "Test Nickname", Type: PitDisplay, + Configuration: map[string]string{"f": "1", "z": "#fff", "a": "3", "c": "4"}} + assert.Equal(t, "/displays/pit?displayId=254&nickname=Test+Nickname&a=3&c=4&f=1&z=%23fff", display.ToUrl()) +} + +func TestNextDisplayId(t *testing.T) { + arena := setupTestArena(t) + + assert.Equal(t, "874", arena.NextDisplayId()) + + // The next random numbers for the test seed are 514 and 653; check that a number is skipped if already used. + display := &Display{Id: "514"} + arena.RegisterDisplay(display) + assert.Equal(t, "653", arena.NextDisplayId()) +} + +func TestDisplayRegisterUnregister(t *testing.T) { + arena := setupTestArena(t) + + display := &Display{Id: "254", Nickname: "Placeholder", Type: PlaceholderDisplay, Configuration: map[string]string{}} + arena.RegisterDisplay(display) + if assert.Contains(t, arena.Displays, "254") { + assert.Equal(t, "Placeholder", arena.Displays["254"].Nickname) + assert.Equal(t, PlaceholderDisplay, arena.Displays["254"].Type) + assert.Equal(t, 1, arena.Displays["254"].ConnectionCount) + } + + // Register a second instance of the same display. + display2 := &Display{Id: "254", Nickname: "Pit", Type: PitDisplay, Configuration: map[string]string{}} + arena.RegisterDisplay(display2) + if assert.Contains(t, arena.Displays, "254") { + assert.Equal(t, "Pit", arena.Displays["254"].Nickname) + assert.Equal(t, PitDisplay, arena.Displays["254"].Type) + assert.Equal(t, 2, arena.Displays["254"].ConnectionCount) + } + + // Register a second display. + display3 := &Display{Id: "148", Type: FieldMonitorDisplay, Configuration: map[string]string{}} + arena.RegisterDisplay(display3) + if assert.Contains(t, arena.Displays, "148") { + assert.Equal(t, 1, arena.Displays["148"].ConnectionCount) + } + + // Update the first display. + display4 := &Display{Id: "254", Nickname: "Alliance", Type: AllianceStationDisplay, + Configuration: map[string]string{"station": "B2"}} + arena.UpdateDisplay(display4) + if assert.Contains(t, arena.Displays, "254") { + assert.Equal(t, "Alliance", arena.Displays["254"].Nickname) + assert.Equal(t, AllianceStationDisplay, arena.Displays["254"].Type) + assert.Equal(t, 2, arena.Displays["254"].ConnectionCount) + } + + // Disconnect both displays. + arena.MarkDisplayDisconnected(display) + arena.MarkDisplayDisconnected(display3) + if assert.Contains(t, arena.Displays, "148") { + assert.Equal(t, 0, arena.Displays["148"].ConnectionCount) + } + if assert.Contains(t, arena.Displays, "254") { + assert.Equal(t, 1, arena.Displays["254"].ConnectionCount) + } +} + +func TestDisplayUpdateError(t *testing.T) { + arena := setupTestArena(t) + + display := &Display{Id: "254", Configuration: map[string]string{}} + err := arena.UpdateDisplay(display) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "doesn't exist") + } +} diff --git a/field/test_helpers.go b/field/test_helpers.go index 65bff00..e4a19a5 100644 --- a/field/test_helpers.go +++ b/field/test_helpers.go @@ -9,12 +9,14 @@ import ( "fmt" "github.com/Team254/cheesy-arena/model" "github.com/stretchr/testify/assert" + "math/rand" "os" "path/filepath" "testing" ) func SetupTestArena(t *testing.T, uniqueName string) *Arena { + rand.Seed(0) model.BaseDir = ".." dbPath := filepath.Join(model.BaseDir, fmt.Sprintf("%s_test.db", uniqueName)) os.Remove(dbPath) diff --git a/model/event_settings.go b/model/event_settings.go index 7950c13..23d8e2a 100644 --- a/model/event_settings.go +++ b/model/event_settings.go @@ -8,7 +8,6 @@ package model type EventSettings struct { Id int Name string - DisplayBackgroundColor string NumElimAlliances int SelectionRound2Order string SelectionRound3Order string @@ -46,7 +45,6 @@ func (database *Database) GetEventSettings() (*EventSettings, error) { if err != nil { // Database record doesn't exist yet; create it now. eventSettings.Name = "Untitled Event" - eventSettings.DisplayBackgroundColor = "#00ff00" eventSettings.NumElimAlliances = 8 eventSettings.SelectionRound2Order = "L" eventSettings.SelectionRound3Order = "" diff --git a/model/event_settings_test.go b/model/event_settings_test.go index 582499a..59fdae7 100644 --- a/model/event_settings_test.go +++ b/model/event_settings_test.go @@ -13,12 +13,11 @@ func TestEventSettingsReadWrite(t *testing.T) { eventSettings, err := db.GetEventSettings() assert.Nil(t, err) - assert.Equal(t, EventSettings{Id: 0, Name: "Untitled Event", DisplayBackgroundColor: "#00ff00", - NumElimAlliances: 8, SelectionRound2Order: "L", SelectionRound3Order: "", TBADownloadEnabled: true, - ApTeamChannel: 157, ApAdminChannel: 11, ApAdminWpaKey: "1234Five"}, *eventSettings) + assert.Equal(t, EventSettings{Id: 0, Name: "Untitled Event", NumElimAlliances: 8, SelectionRound2Order: "L", + SelectionRound3Order: "", TBADownloadEnabled: true, ApTeamChannel: 157, ApAdminChannel: 11, + ApAdminWpaKey: "1234Five"}, *eventSettings) eventSettings.Name = "Chezy Champs" - eventSettings.DisplayBackgroundColor = "#ff00ff" eventSettings.NumElimAlliances = 6 eventSettings.SelectionRound2Order = "F" eventSettings.SelectionRound3Order = "L" diff --git a/static/css/alliance_station_display.css b/static/css/alliance_station_display.css index bf98154..6626055 100644 --- a/static/css/alliance_station_display.css +++ b/static/css/alliance_station_display.css @@ -24,9 +24,6 @@ body[data-mode=fieldReset] { .mode { display: none; } -body[data-mode=displayId] .mode#displayId { - display: block; -} body[data-mode=logo] .mode#logo { display: block; } @@ -48,21 +45,6 @@ body[data-mode=fieldReset] .mode#fieldReset { margin: auto auto; } -/* Display ID Mode */ -#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; -} - /* Field Reset Mode */ #fieldReset { position: absolute; @@ -171,4 +153,4 @@ body[data-position=right] #inMatch #blueScore { } #teamRank { background-color: transparent; -} \ No newline at end of file +} diff --git a/static/css/lib/bootstrap-colorpicker.min.css b/static/css/lib/bootstrap-colorpicker.min.css deleted file mode 100644 index fa8b561..0000000 --- a/static/css/lib/bootstrap-colorpicker.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Bootstrap Colorpicker - * http://mjolnic.github.io/bootstrap-colorpicker/ - * - * Originally written by (c) 2012 Stefan Petre - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0.txt - * - */.colorpicker-saturation{float:left;width:100px;height:100px;cursor:crosshair;background-image:url("../../img/lib/bootstrap-colorpicker/saturation.png")}.colorpicker-saturation i{position:absolute;top:0;left:0;display:block;width:5px;height:5px;margin:-4px 0 0 -4px;border:1px solid #000;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-saturation i b{display:block;width:5px;height:5px;border:1px solid #fff;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-hue,.colorpicker-alpha{float:left;width:15px;height:100px;margin-bottom:4px;margin-left:4px;cursor:row-resize}.colorpicker-hue i,.colorpicker-alpha i{position:absolute;top:0;left:0;display:block;width:100%;height:1px;margin-top:-1px;background:#000;border-top:1px solid #fff}.colorpicker-hue{background-image:url("../../img/lib/bootstrap-colorpicker/hue.png")}.colorpicker-alpha{display:none;background-image:url("../../img/lib/bootstrap-colorpicker/alpha.png")}.colorpicker{top:0;left:0;z-index:2500;min-width:130px;padding:4px;margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1}.colorpicker:before,.colorpicker:after{display:table;line-height:0;content:""}.colorpicker:after{clear:both}.colorpicker:before{position:absolute;top:-7px;left:6px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.colorpicker:after{position:absolute;top:-6px;left:7px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.colorpicker div{position:relative}.colorpicker.colorpicker-with-alpha{min-width:140px}.colorpicker.colorpicker-with-alpha .colorpicker-alpha{display:block}.colorpicker-color{height:10px;margin-top:5px;clear:both;background-image:url("../img/bootstrap-colorpicker/alpha.png");background-position:0 100%}.colorpicker-color div{height:10px}.colorpicker-element .input-group-addon i,.colorpicker-element .add-on i{display:inline-block;width:16px;height:16px;vertical-align:text-top;cursor:pointer}.colorpicker.colorpicker-inline{position:relative;z-index:auto;display:inline-block;float:none}.colorpicker.colorpicker-horizontal{width:110px;height:auto;min-width:110px}.colorpicker.colorpicker-horizontal .colorpicker-saturation{margin-bottom:4px}.colorpicker.colorpicker-horizontal .colorpicker-color{width:100px}.colorpicker.colorpicker-horizontal .colorpicker-hue,.colorpicker.colorpicker-horizontal .colorpicker-alpha{float:left;width:100px;height:15px;margin-bottom:4px;margin-left:0;cursor:col-resize}.colorpicker.colorpicker-horizontal .colorpicker-hue i,.colorpicker.colorpicker-horizontal .colorpicker-alpha i{position:absolute;top:0;left:0;display:block;width:1px;height:15px;margin-top:0;background:#fff;border:0}.colorpicker.colorpicker-horizontal .colorpicker-hue{background-image:url("../img/bootstrap-colorpicker/hue-horizontal.png")}.colorpicker.colorpicker-horizontal .colorpicker-alpha{background-image:url("../img/bootstrap-colorpicker/alpha-horizontal.png")}.colorpicker.colorpicker-hidden{display:none}.colorpicker.colorpicker-visible{display:block}.colorpicker-inline.colorpicker-visible{display:inline-block} \ No newline at end of file diff --git a/static/css/placeholder_display.css b/static/css/placeholder_display.css new file mode 100644 index 0000000..1b87df9 --- /dev/null +++ b/static/css/placeholder_display.css @@ -0,0 +1,37 @@ +/* + Copyright 2018 Team 254. All Rights Reserved. + Author: pat@patfairbank.com (Patrick Fairbank) +*/ + +html { + -webkit-user-select: none; + -moz-user-select: none; + overflow: hidden; +} +body { + background-color: #000; + color: #ff0; + text-align: center; + font-family: "FuturaLTBold"; +} +#displayId { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto auto; + height: 1px; + line-height: 1px; + font-size: 500px; +} +#displayNickname { + position: absolute; + bottom: 15%; + left: 0; + right: 0; + margin: auto auto; + height: 1px; + line-height: 1px; + font-size: 75px; +} diff --git a/static/js/alliance_station_display.js b/static/js/alliance_station_display.js index 226b62b..9e52e37 100644 --- a/static/js/alliance_station_display.js +++ b/static/js/alliance_station_display.js @@ -3,25 +3,20 @@ // // Client-side methods for the alliance station display. -var allianceStation = ""; +var station = ""; var blinkInterval; var currentScreen = "blank"; var websocket; -// Handles a websocket message to set which alliance station this display represents. -var handleAllianceStation = function(station) { - allianceStation = station; -}; - // Handles a websocket message to change which screen is displayed. var handleAllianceStationDisplayMode = function(targetScreen) { currentScreen = targetScreen; - if (allianceStation === "") { + if (station === "") { // Don't do anything if this screen hasn't been assigned a position yet. return; } $("body").attr("data-mode", targetScreen); - switch (allianceStation[1]) { + switch (station[1]) { case "1": $("body").attr("data-position", "right"); break; @@ -36,44 +31,42 @@ var handleAllianceStationDisplayMode = function(targetScreen) { // Handles a websocket message to update the team to display. var handleMatchLoad = function(data) { - if (allianceStation !== "") { - var team = data.Teams[allianceStation]; + if (station !== "") { + var team = data.Teams[station]; if (team) { $("#teamNumber").text(team.Id); - $("#teamNameText").attr("data-alliance-bg", allianceStation[0]).text(team.Nickname); + $("#teamNameText").attr("data-alliance-bg", station[0]).text(team.Nickname); var ranking = data.Rankings[team.Id]; if (ranking && data.MatchType === "Qualification") { var rankingText = ranking.Rank; - $("#teamRank").attr("data-alliance-bg", allianceStation[0]).text(rankingText); + $("#teamRank").attr("data-alliance-bg", station[0]).text(rankingText); } else { - $("#teamRank").attr("data-alliance-bg", allianceStation[0]).text(""); + $("#teamRank").attr("data-alliance-bg", station[0]).text(""); } } else { $("#teamNumber").text(""); - $("#teamNameText").attr("data-alliance-bg", allianceStation[0]).text(""); - $("#teamRank").attr("data-alliance-bg", allianceStation[0]).text(""); + $("#teamNameText").attr("data-alliance-bg", station[0]).text(""); + $("#teamRank").attr("data-alliance-bg", station[0]).text(""); } - } else { - $("body").attr("data-mode", "displayId"); } }; // Handles a websocket message to update the team connection status. var handleArenaStatus = function(data) { - stationStatus = data.AllianceStations[allianceStation]; + stationStatus = data.AllianceStations[station]; var blink = false; if (stationStatus && stationStatus.Bypass) { $("#match").attr("data-status", "bypass"); } else if (stationStatus) { if (!stationStatus.DsConn || !stationStatus.DsConn.DsLinked) { - $("#match").attr("data-status", allianceStation[0]); + $("#match").attr("data-status", station[0]); } else if (!stationStatus.DsConn.RobotLinked) { blink = true; if (!blinkInterval) { blinkInterval = setInterval(function() { var status = $("#match").attr("data-status"); - $("#match").attr("data-status", (status === "") ? allianceStation[0] : ""); + $("#match").attr("data-status", (status === "") ? station[0] : ""); }, 250); } } else { @@ -107,15 +100,12 @@ var handleRealtimeScore = function(data) { }; $(function() { - if (displayId === "") { - displayId = Math.floor(Math.random() * 10000); - window.location = "/displays/alliance_station?displayId=" + displayId; - } - $("#displayId").text(displayId); + // Read the configuration for this display from the URL query string. + var urlParams = new URLSearchParams(window.location.search); + station = urlParams.get("station"); // Set up the websocket back to the server. - websocket = new CheesyWebsocket("/displays/alliance_station/websocket?displayId=" + displayId, { - allianceStation: function(event) { handleAllianceStation(event.data); }, + websocket = new CheesyWebsocket("/displays/alliance_station/websocket", { allianceStationDisplayMode: function(event) { handleAllianceStationDisplayMode(event.data); }, arenaStatus: function(event) { handleArenaStatus(event.data); }, matchLoad: function(event) { handleMatchLoad(event.data); }, diff --git a/static/js/audience_display.js b/static/js/audience_display.js index 57086f2..53b747c 100644 --- a/static/js/audience_display.js +++ b/static/js/audience_display.js @@ -399,6 +399,10 @@ var initializeSponsorDisplay = function() { }; $(function() { + // Read the configuration for this display from the URL query string. + var urlParams = new URLSearchParams(window.location.search); + document.body.style.backgroundColor = urlParams.get("background"); + // Set up the websocket back to the server. websocket = new CheesyWebsocket("/displays/audience/websocket", { allianceSelection: function(event) { handleAllianceSelection(event.data); }, diff --git a/static/js/cheesy-websocket.js b/static/js/cheesy-websocket.js index 39f9e92..980326e 100644 --- a/static/js/cheesy-websocket.js +++ b/static/js/cheesy-websocket.js @@ -15,20 +15,42 @@ var CheesyWebsocket = function(path, events) { } url += path; + // Append the page's query string to the websocket URL. + url += window.location.search; + // Insert a default error-handling event if a custom one doesn't already exist. if (!events.hasOwnProperty("error")) { events.error = function(event) { // Data is just an error string. console.log(event.data); alert(event.data); - } + }; } + // Parse the display parameters that will be present in the query string if this is a display. + var displayId = new URLSearchParams(window.location.search).get("displayId"); + // Insert an event to allow the server to force-reload the client for any display. events.reload = function(event) { - location.reload(); + if (event.data === null || event.data === displayId) { + location.reload(); + } }; + // Insert an event to allow reconfiguration if this is a display. + if (!events.hasOwnProperty("displayConfiguration")) { + events.displayConfiguration = function (event) { + if (displayId in event.data.DisplayUrls) { + var newUrl = event.data.DisplayUrls[displayId]; + + // Reload the display if the configuration has changed. + if (newUrl !== window.location.pathname + window.location.search) { + window.location = newUrl; + } + } + }; + } + this.connect = function() { this.websocket = $.websocket(url, { open: function() { diff --git a/static/js/lib/bootstrap-colorpicker.min.js b/static/js/lib/bootstrap-colorpicker.min.js deleted file mode 100644 index b9883e4..0000000 --- a/static/js/lib/bootstrap-colorpicker.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):window.jQuery&&!window.jQuery.fn.colorpicker&&a(window.jQuery)}(function(a){"use strict";var b=function(a){this.value={h:0,s:0,b:0,a:1},this.origFormat=null,a&&(void 0!==a.toLowerCase?this.setColor(a):void 0!==a.h&&(this.value=a))};b.prototype={constructor:b,colors:{aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4","indianred ":"#cd5c5c","indigo ":"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"},_sanitizeNumber:function(a){return"number"==typeof a?a:isNaN(a)||null===a||""===a||void 0===a?1:void 0!==a.toLowerCase?parseFloat(a):1},setColor:function(a){a=a.toLowerCase(),this.value=this.stringToHSB(a)||{h:0,s:0,b:0,a:1}},stringToHSB:function(b){b=b.toLowerCase();var c=this,d=!1;return a.each(this.stringParsers,function(a,e){var f=e.re.exec(b),g=f&&e.parse.apply(c,[f]),h=e.format||"rgba";return g?(d=h.match(/hsla?/)?c.RGBtoHSB.apply(c,c.HSLtoRGB.apply(c,g)):c.RGBtoHSB.apply(c,g),c.origFormat=h,!1):!0}),d},setHue:function(a){this.value.h=1-a},setSaturation:function(a){this.value.s=a},setBrightness:function(a){this.value.b=1-a},setAlpha:function(a){this.value.a=parseInt(100*(1-a),10)/100},toRGB:function(a,b,c,d){a||(a=this.value.h,b=this.value.s,c=this.value.b),a*=360;var e,f,g,h,i;return a=a%360/60,i=c*b,h=i*(1-Math.abs(a%2-1)),e=f=g=c-i,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a],{r:Math.round(255*e),g:Math.round(255*f),b:Math.round(255*g),a:d||this.value.a}},toHex:function(a,b,c,d){var e=this.toRGB(a,b,c,d);return"#"+(1<<24|parseInt(e.r)<<16|parseInt(e.g)<<8|parseInt(e.b)).toString(16).substr(1)},toHSL:function(a,b,c,d){a=a||this.value.h,b=b||this.value.s,c=c||this.value.b,d=d||this.value.a;var e=a,f=(2-b)*c,g=b*c;return g/=f>0&&1>=f?f:2-f,f/=2,g>1&&(g=1),{h:isNaN(e)?0:e,s:isNaN(g)?0:g,l:isNaN(f)?0:f,a:isNaN(d)?0:d}},toAlias:function(a,b,c,d){var e=this.toHex(a,b,c,d);for(var f in this.colors)if(this.colors[f]==e)return f;return!1},RGBtoHSB:function(a,b,c,d){a/=255,b/=255,c/=255;var e,f,g,h;return g=Math.max(a,b,c),h=g-Math.min(a,b,c),e=0===h?null:g===a?(b-c)/h:g===b?(c-a)/h+2:(a-b)/h+4,e=(e+360)%6*60/360,f=0===h?0:h/g,{h:this._sanitizeNumber(e),s:f,b:g,a:this._sanitizeNumber(d)}},HueToRGB:function(a,b,c){return 0>c?c+=1:c>1&&(c-=1),1>6*c?a+(b-a)*c*6:1>2*c?b:2>3*c?a+(b-a)*(2/3-c)*6:a},HSLtoRGB:function(a,b,c,d){0>b&&(b=0);var e;e=.5>=c?c*(1+b):c+b-c*b;var f=2*c-e,g=a+1/3,h=a,i=a-1/3,j=Math.round(255*this.HueToRGB(f,e,g)),k=Math.round(255*this.HueToRGB(f,e,h)),l=Math.round(255*this.HueToRGB(f,e,i));return[j,k,l,this._sanitizeNumber(d)]},toString:function(a){switch(a=a||"rgba"){case"rgb":var b=this.toRGB();return"rgb("+b.r+","+b.g+","+b.b+")";case"rgba":var b=this.toRGB();return"rgba("+b.r+","+b.g+","+b.b+","+b.a+")";case"hsl":var c=this.toHSL();return"hsl("+Math.round(360*c.h)+","+Math.round(100*c.s)+"%,"+Math.round(100*c.l)+"%)";case"hsla":var c=this.toHSL();return"hsla("+Math.round(360*c.h)+","+Math.round(100*c.s)+"%,"+Math.round(100*c.l)+"%,"+c.a+")";case"hex":return this.toHex();case"alias":return this.toAlias()||this.toHex();default:return!1}},stringParsers:[{re:/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,format:"hex",parse:function(a){return[parseInt(a[1],16),parseInt(a[2],16),parseInt(a[3],16),1]}},{re:/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/,format:"hex",parse:function(a){return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16),1]}},{re:/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*?\)/,format:"rgb",parse:function(a){return[a[1],a[2],a[3],1]}},{re:/rgb\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*?\)/,format:"rgb",parse:function(a){return[2.55*a[1],2.55*a[2],2.55*a[3],1]}},{re:/rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,format:"rgba",parse:function(a){return[a[1],a[2],a[3],a[4]]}},{re:/rgba\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,format:"rgba",parse:function(a){return[2.55*a[1],2.55*a[2],2.55*a[3],a[4]]}},{re:/hsl\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*?\)/,format:"hsl",parse:function(a){return[a[1]/360,a[2]/100,a[3]/100,a[4]]}},{re:/hsla\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,format:"hsla",parse:function(a){return[a[1]/360,a[2]/100,a[3]/100,a[4]]}},{re:/^([a-z]{3,})$/,format:"alias",parse:function(a){var b=this.colorNameToHex(a[0])||"#000000",c=this.stringParsers[0].re.exec(b),d=c&&this.stringParsers[0].parse.apply(this,[c]);return d}}],colorNameToHex:function(a){return"undefined"!=typeof this.colors[a.toLowerCase()]?this.colors[a.toLowerCase()]:!1}};var c={horizontal:!1,inline:!1,color:!1,format:!1,input:"input",container:!1,component:".add-on, .input-group-addon",sliders:{saturation:{maxLeft:100,maxTop:100,callLeft:"setSaturation",callTop:"setBrightness"},hue:{maxLeft:0,maxTop:100,callLeft:!1,callTop:"setHue"},alpha:{maxLeft:0,maxTop:100,callLeft:!1,callTop:"setAlpha"}},slidersHorz:{saturation:{maxLeft:100,maxTop:100,callLeft:"setSaturation",callTop:"setBrightness"},hue:{maxLeft:100,maxTop:0,callLeft:"setHue",callTop:!1},alpha:{maxLeft:100,maxTop:0,callLeft:"setAlpha",callTop:!1}},template:'
'},d=function(d,e){this.element=a(d).addClass("colorpicker-element"),this.options=a.extend({},c,this.element.data(),e),this.component=this.options.component,this.component=this.component!==!1?this.element.find(this.component):!1,this.component&&0===this.component.length&&(this.component=!1),this.container=this.options.container===!0?this.element:this.options.container,this.container=this.container!==!1?a(this.container):!1,this.input=this.element.is("input")?this.element:this.options.input?this.element.find(this.options.input):!1,this.input&&0===this.input.length&&(this.input=!1),this.color=new b(this.options.color!==!1?this.options.color:this.getValue()),this.format=this.options.format!==!1?this.options.format:this.color.origFormat,this.picker=a(this.options.template),this.picker.addClass(this.options.inline?"colorpicker-inline colorpicker-visible":"colorpicker-hidden"),this.options.horizontal&&this.picker.addClass("colorpicker-horizontal"),("rgba"===this.format||"hsla"===this.format)&&this.picker.addClass("colorpicker-with-alpha"),this.picker.on("mousedown.colorpicker",a.proxy(this.mousedown,this)),this.picker.appendTo(this.container?this.container:a("body")),this.input!==!1&&(this.input.on({"keyup.colorpicker":a.proxy(this.keyup,this)}),this.component===!1&&this.element.on({"focus.colorpicker":a.proxy(this.show,this)}),this.options.inline===!1&&this.element.on({"focusout.colorpicker":a.proxy(this.hide,this)})),this.component!==!1&&this.component.on({"click.colorpicker":a.proxy(this.show,this)}),this.input===!1&&this.component===!1&&this.element.on({"click.colorpicker":a.proxy(this.show,this)}),this.update(),a(a.proxy(function(){this.element.trigger("create")},this))};d.version="2.0.0-beta",d.Color=b,d.prototype={constructor:d,destroy:function(){this.picker.remove(),this.element.removeData("colorpicker").off(".colorpicker"),this.input!==!1&&this.input.off(".colorpicker"),this.component!==!1&&this.component.off(".colorpicker"),this.element.removeClass("colorpicker-element"),this.element.trigger({type:"destroy"})},reposition:function(){if(this.options.inline!==!1)return!1;var a=this.container&&this.container[0]!==document.body?"position":"offset",b=this.component?this.component[a]():this.element[a]();this.picker.css({top:b.top+(this.component?this.component.outerHeight():this.element.outerHeight()),left:b.left})},show:function(b){return this.isDisabled()?!1:(this.picker.addClass("colorpicker-visible").removeClass("colorpicker-hidden"),this.reposition(),a(window).on("resize.colorpicker",a.proxy(this.reposition,this)),!this.hasInput()&&b&&b.stopPropagation&&b.preventDefault&&(b.stopPropagation(),b.preventDefault()),this.options.inline===!1&&a(window.document).on({"mousedown.colorpicker":a.proxy(this.hide,this)}),void this.element.trigger({type:"showPicker",color:this.color}))},hide:function(){this.picker.addClass("colorpicker-hidden").removeClass("colorpicker-visible"),a(window).off("resize.colorpicker",this.reposition),a(document).off({"mousedown.colorpicker":this.hide}),this.update(),this.element.trigger({type:"hidePicker",color:this.color})},updateData:function(a){return a=a||this.color.toString(this.format),this.element.data("color",a),a},updateInput:function(a){return a=a||this.color.toString(this.format),this.input!==!1&&this.input.prop("value",a),a},updatePicker:function(a){void 0!==a&&(this.color=new b(a));var c=this.options.horizontal===!1?this.options.sliders:this.options.slidersHorz,d=this.picker.find("i");return 0!==d.length?(this.options.horizontal===!1?(c=this.options.sliders,d.eq(1).css("top",c.hue.maxTop*(1-this.color.value.h)).end().eq(2).css("top",c.alpha.maxTop*(1-this.color.value.a))):(c=this.options.slidersHorz,d.eq(1).css("left",c.hue.maxLeft*(1-this.color.value.h)).end().eq(2).css("left",c.alpha.maxLeft*(1-this.color.value.a))),d.eq(0).css({top:c.saturation.maxTop-this.color.value.b*c.saturation.maxTop,left:this.color.value.s*c.saturation.maxLeft}),this.picker.find(".colorpicker-saturation").css("backgroundColor",this.color.toHex(this.color.value.h,1,1,1)),this.picker.find(".colorpicker-alpha").css("backgroundColor",this.color.toHex()),this.picker.find(".colorpicker-color, .colorpicker-color div").css("backgroundColor",this.color.toString(this.format)),a):void 0},updateComponent:function(a){if(a=a||this.color.toString(this.format),this.component!==!1){var b=this.component.find("i").eq(0);b.length>0?b.css({backgroundColor:a}):this.component.css({backgroundColor:a})}return a},update:function(a){var b=this.updateComponent();return(this.getValue(!1)!==!1||a===!0)&&(this.updateInput(b),this.updateData(b)),this.updatePicker(),b},setValue:function(a){this.color=new b(a),this.update(),this.element.trigger({type:"changeColor",color:this.color,value:a})},getValue:function(a){a=void 0===a?"#000000":a;var b;return b=this.hasInput()?this.input.val():this.element.data("color"),(void 0===b||""===b||null===b)&&(b=a),b},hasInput:function(){return this.input!==!1},isDisabled:function(){return this.hasInput()?this.input.prop("disabled")===!0:!1},disable:function(){return this.hasInput()?(this.input.prop("disabled",!0),!0):!1},enable:function(){return this.hasInput()?(this.input.prop("disabled",!1),!0):!1},currentSlider:null,mousePointer:{left:0,top:0},mousedown:function(b){b.stopPropagation(),b.preventDefault();var c=a(b.target),d=c.closest("div"),e=this.options.horizontal?this.options.slidersHorz:this.options.sliders;if(!d.is(".colorpicker")){if(d.is(".colorpicker-saturation"))this.currentSlider=a.extend({},e.saturation);else if(d.is(".colorpicker-hue"))this.currentSlider=a.extend({},e.hue);else{if(!d.is(".colorpicker-alpha"))return!1;this.currentSlider=a.extend({},e.alpha)}var f=d.offset();this.currentSlider.guide=d.find("i")[0].style,this.currentSlider.left=b.pageX-f.left,this.currentSlider.top=b.pageY-f.top,this.mousePointer={left:b.pageX,top:b.pageY},a(document).on({"mousemove.colorpicker":a.proxy(this.mousemove,this),"mouseup.colorpicker":a.proxy(this.mouseup,this)}).trigger("mousemove")}return!1},mousemove:function(a){a.stopPropagation(),a.preventDefault();var b=Math.max(0,Math.min(this.currentSlider.maxLeft,this.currentSlider.left+((a.pageX||this.mousePointer.left)-this.mousePointer.left))),c=Math.max(0,Math.min(this.currentSlider.maxTop,this.currentSlider.top+((a.pageY||this.mousePointer.top)-this.mousePointer.top)));return this.currentSlider.guide.left=b+"px",this.currentSlider.guide.top=c+"px",this.currentSlider.callLeft&&this.color[this.currentSlider.callLeft].call(this.color,b/100),this.currentSlider.callTop&&this.color[this.currentSlider.callTop].call(this.color,c/100),this.update(!0),this.element.trigger({type:"changeColor",color:this.color}),!1},mouseup:function(b){return b.stopPropagation(),b.preventDefault(),a(document).off({"mousemove.colorpicker":this.mousemove,"mouseup.colorpicker":this.mouseup}),!1},keyup:function(a){if(38===a.keyCode)this.color.value.a<1&&(this.color.value.a=Math.round(100*(this.color.value.a+.01))/100),this.update(!0);else if(40===a.keyCode)this.color.value.a>0&&(this.color.value.a=Math.round(100*(this.color.value.a-.01))/100),this.update(!0);else{var c=this.input.val();this.color=new b(c),this.getValue(!1)!==!1&&(this.updateData(),this.updateComponent(),this.updatePicker())}this.element.trigger({type:"changeColor",color:this.color,value:c})}},a.colorpicker=d,a.fn.colorpicker=function(b){var c=arguments;return this.each(function(){var e=a(this),f=e.data("colorpicker"),g="object"==typeof b?b:{};f||"string"==typeof b?"string"==typeof b&&f[b].apply(f,Array.prototype.slice.call(c,1)):e.data("colorpicker",new d(this,g))})},a.fn.colorpicker.constructor=d}); \ No newline at end of file diff --git a/static/js/placeholder_display.js b/static/js/placeholder_display.js new file mode 100644 index 0000000..fa7aa7a --- /dev/null +++ b/static/js/placeholder_display.js @@ -0,0 +1,20 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Client-side logic for the placeholder display. + +var websocket; + +$(function() { + // Read the configuration for this display from the URL query string. + var urlParams = new URLSearchParams(window.location.search); + $("#displayId").text(urlParams.get("displayId")); + var nickname = urlParams.get("nickname"); + if (nickname !== null) { + $("#displayNickname").text(nickname); + } + + // Set up the websocket back to the server. + websocket = new CheesyWebsocket("/display/websocket", { + }); +}); diff --git a/static/js/setup_displays.js b/static/js/setup_displays.js new file mode 100644 index 0000000..0609a61 --- /dev/null +++ b/static/js/setup_displays.js @@ -0,0 +1,60 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Client-side logic for the display configuration page. + +var displayTemplate = Handlebars.compile($("#displayTemplate").html()); +var websocket; + +var configureDisplay = function(displayId) { + // Convert configuration string into map. + var configurationMap = {} + $.each($("#displayConfiguration" + displayId).val().split("&"), function(index, param) { + var keyValuePair = param.split("="); + configurationMap[keyValuePair[0]] = keyValuePair[1]; + }); + + websocket.send("configureDisplay", { + Id: displayId, + Nickname: $("#displayNickname" + displayId).val(), + Type: parseInt($("#displayType" + displayId).val()), + Configuration: configurationMap + }); +}; + +var undoChanges = function() { + window.location.reload(); +}; + +var reloadDisplay = function(displayId) { + websocket.send("reloadDisplay", displayId); +}; + +var reloadAllDisplays = function() { + websocket.send("reloadAllDisplays"); +}; + +// Handles a websocket message to refresh the display list. +var handleDisplayConfiguration = function(data) { + $("#displayContainer").empty(); + + $.each(data.Displays, function(displayId, display) { + var displayRow = displayTemplate(display); + $("#displayContainer").append(displayRow); + $("#displayNickname" + displayId).val(display.Nickname); + $("#displayType" + displayId).val(display.Type); + + // Convert configuration map to query string format. + var configurationString = $.map(Object.entries(display.Configuration), function(entry) { + return entry.join("="); + }).join("&"); + $("#displayConfiguration" + displayId).val(configurationString); + }); +}; + +$(function() { + // Set up the websocket back to the server. + websocket = new CheesyWebsocket("/setup/displays/websocket", { + displayConfiguration: function(event) { handleDisplayConfiguration(event.data); } + }); +}); diff --git a/templates/alliance_station_display.html b/templates/alliance_station_display.html index 981bae0..7d16b8c 100644 --- a/templates/alliance_station_display.html +++ b/templates/alliance_station_display.html @@ -14,7 +14,6 @@ -
| ID | +# Connected | +Nickname | +Type | +Configuration | +Action | +
|---|