diff --git a/cmd/departureboard/README.md b/cmd/departureboard/README.md index 5eacfd1..c25f580 100644 --- a/cmd/departureboard/README.md +++ b/cmd/departureboard/README.md @@ -66,3 +66,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ## Attribution + +Bulletin board icons created by catkuro - [Flaticon](https://www.flaticon.com/free-icons/bulletin-board diff --git a/cmd/departureboard/departureboard.go b/cmd/departureboard/departureboard.go index acb69e8..33c0963 100644 --- a/cmd/departureboard/departureboard.go +++ b/cmd/departureboard/departureboard.go @@ -10,11 +10,10 @@ import ( "net/http" "os" "path/filepath" + "runtime" "sort" "github.com/go-stomp/stomp/v3" - "github.com/pkg/browser" - "github.com/sqweek/dialog" "github.com/jonathanp0/go-simsig/gateway" "github.com/jonathanp0/go-simsig/wttxml" @@ -37,71 +36,15 @@ var pass = flag.String("pass", "", "SimSig License Password(optional)") var helpFlag = flag.Bool("help", false, "Print help text") var stop = make(chan bool) -func main() { - flag.Parse() - if *helpFlag { - fmt.Fprintf(os.Stderr, "Usage of %s\n", os.Args[0]) - flag.PrintDefaults() - os.Exit(1) - } - - //Open File Dialog for WTT - var filename string - if *wttFile != "" { - filename = *wttFile - } else { - var err error - filename, err = dialog.File().Title("Select Active SimSig WTT").SetStartDir("C:\\Users\\Public\\Documents\\SimSig\\Timetables").Filter("SimSig WTT(*.WTT)", "wtt").Load() - if err != nil { - println("No WTT specified on command line or in dialog") - os.Exit(1) - } - } - - println("Reading WTT File ", filename, "...") - - //Read WTT - data, err := wttxml.ReadSavedTimetable(filename) - if err != nil { - println("Error reading WTT: " + err.Error()) - os.Exit(1) - } - - //Build stop list from WTT - var wtt wttxml.SimSigTimetable - err = xml.Unmarshal(data, &wtt) - if err != nil { - println("WTT Parsing Error: " + err.Error()) - os.Exit(1) - } - - locations := buildSortedLocationList(wtt.Timetables.Timetable) +//global variables +var locations []string +var stopsAtLocations map[string]*LocationStopList - stopsAtLocations := buildLocationStopList(locations, wtt.Timetables.Timetable) - - for _, locStops := range stopsAtLocations { - sort.Sort(locStops) - } - - //Iniate STOMP Connection - - subscribed := make(chan bool) - - //Global state variables - var currentClock gateway.ClockMsg - - println("Connecting to SimSig at ", *serverAddr, "...") - go recvMessages(¤tClock, stopsAtLocations, subscribed) - - // wait until we know the receiver has subscribed - <-subscribed +func main() { - println("Connected. Launched web interface att http://localhost:8090/") - go webInterface(locations, stopsAtLocations, ¤tClock) - browser.OpenURL("http://localhost:8090/") + runtime.LockOSThread() + runWindowsUI() - //run indefinitely - <-stop } //Gateway Message Processing @@ -153,8 +96,27 @@ func processDelayMessage(m *gateway.TrainDelay, locations LocationStopListMap) { } } +func gatewayConnection(user string, password string, address string) { + //Iniate STOMP Connection + subscribed := make(chan bool) + + //Global state variables + var currentClock gateway.ClockMsg + + go recvMessages(¤tClock, stopsAtLocations, subscribed, user, password, address) + + // wait until we know the receiver has subscribed + <-subscribed + + go webInterface(locations, stopsAtLocations, ¤tClock) + webInterfaceReady() + + //run indefinitely + <-stop +} + //Main communication thread for Interface Gateway -func recvMessages(clock *gateway.ClockMsg, locations LocationStopListMap, subscribed chan bool) { +func recvMessages(clock *gateway.ClockMsg, locations LocationStopListMap, subscribed chan bool, user string, pass string, serverAddr string) { defer func() { stop <- true }() @@ -162,25 +124,25 @@ func recvMessages(clock *gateway.ClockMsg, locations LocationStopListMap, subscr //login credentials var options []func(*stomp.Conn) error = []func(*stomp.Conn) error{} - if *user != "" { - options = append(options, stomp.ConnOpt.Login(*user, *pass)) + if user != "" { + options = append(options, stomp.ConnOpt.Login(user, pass)) } - conn, err := stomp.Dial("tcp", *serverAddr, options...) + conn, err := stomp.Dial("tcp", serverAddr, options...) if err != nil { - println("cannot connect to server", err.Error()) + updateStatus("cannot connect to server: " + err.Error()) return } subMvt, err := conn.Subscribe(movementQueueName, stomp.AckAuto) if err != nil { - println("cannot subscribe to", movementQueueName, err.Error()) + updateStatus("cannot subscribe to " + movementQueueName + ": " + err.Error()) return } subSimsig, err := conn.Subscribe(simsigQueueName, stomp.AckAuto) if err != nil { - println("cannot subscribe to", simsigQueueName, err.Error()) + updateStatus("cannot subscribe to " + simsigQueueName + ": " + err.Error()) return } conn.Send("/topic/SimSig", "text/plain", []byte("{\"idrequest\":{}}")) @@ -193,7 +155,7 @@ func recvMessages(clock *gateway.ClockMsg, locations LocationStopListMap, subscr var decodedMsg gateway.TrainMovementMessage err := json.Unmarshal(msg.Body, &decodedMsg) if err != nil { - println("Error parsing Train Movement message:", err.Error()) + showError("Error parsing Train Movement message: " + err.Error()) continue } if decodedMsg.TrainLocation != nil { @@ -205,7 +167,7 @@ func recvMessages(clock *gateway.ClockMsg, locations LocationStopListMap, subscr var decodedMsg gateway.SimSigMessage err := json.Unmarshal(msg.Body, &decodedMsg) if err != nil { - println("Error parsing SimSig message:", err.Error()) + showError("Error parsing SimSig message: " + err.Error()) continue } if decodedMsg.Clock != nil { @@ -253,10 +215,13 @@ func serveDepartureBoard(currentClock *gateway.ClockMsg, location string, stopLi data.Location = location data.Stops = stopList.Stops - tmpl := template.Must(template.ParseFiles(localTemplatePath("tmpl/board.tmpl"))) - err := tmpl.Execute(w, data) + tmpl, err := template.ParseFiles(localTemplatePath("tmpl/board.tmpl")) if err != nil { - println("board.tmpl error: " + err.Error()) + showError("board.tmpl error: " + err.Error()) + } + err = tmpl.Execute(w, data) + if err != nil { + showError("board.tmpl error: " + err.Error()) } } @@ -284,10 +249,13 @@ func webInterface(locations []string, locationStops map[string]*LocationStopList data.Area = currentClock.AreaID data.Locations = locations - tmpl := template.Must(template.ParseFiles(localTemplatePath("tmpl/index.tmpl"))) - err := tmpl.Execute(w, data) + tmpl, err := template.ParseFiles(localTemplatePath("tmpl/index.tmpl")) if err != nil { - println("index.tmpl error: " + err.Error()) + showError("index.tmpl error: " + err.Error()) + } + err = tmpl.Execute(w, data) + if err != nil { + showError("index.tmpl error: " + err.Error()) } }) @@ -295,6 +263,32 @@ func webInterface(locations []string, locationStops map[string]*LocationStopList } // Timetable Parsing +func loadTimetable(filename string) string { + + //Read WTT + data, err := wttxml.ReadSavedTimetable(filename) + if err != nil { + return ("Error reading WTT: " + err.Error()) + } + + //Build stop list from WTT + var wtt wttxml.SimSigTimetable + err = xml.Unmarshal(data, &wtt) + if err != nil { + return ("WTT Parsing Error: " + err.Error()) + } + + locations = buildSortedLocationList(wtt.Timetables.Timetable) + + stopsAtLocations = buildLocationStopList(locations, wtt.Timetables.Timetable) + + for _, locStops := range stopsAtLocations { + sort.Sort(locStops) + } + + return "" +} + func buildSortedLocationList(timetables []wttxml.Timetable) []string { locations := map[string]bool{} diff --git a/cmd/departureboard/gui.go b/cmd/departureboard/gui.go new file mode 100644 index 0000000..791466a --- /dev/null +++ b/cmd/departureboard/gui.go @@ -0,0 +1,123 @@ +package main + +import ( + "github.com/lxn/walk" + . "github.com/lxn/walk/declarative" + "github.com/pkg/browser" + "github.com/sqweek/dialog" +) + +var mainWindow *walk.MainWindow +var statusLink *walk.LinkLabel + +func runWindowsUI() { + + var timetableLabel *walk.Label + var userEdit, passwordEdit, addressEdit *walk.LineEdit + var connectButton, chooseButton *walk.PushButton + + MainWindow{ + AssignTo: &mainWindow, + Title: "SimSig Departure Board", + Size: Size{200, 270}, + MinSize: Size{200, 270}, + MaxSize: Size{600, 270}, + Layout: VBox{}, + Children: []Widget{ + Label{ + Text: "SimSig Departure Board", + Font: Font{PointSize: 18, Bold: true}, + }, + Composite{ + Layout: Grid{Columns: 2}, + Children: []Widget{ + PushButton{ + AssignTo: &chooseButton, + Text: "Choose Timetable", + OnClicked: func() { + var filename string + var err error + filename, err = dialog.File().Title("Select Active SimSig WTT").SetStartDir("C:\\Users\\Public\\Documents\\SimSig\\Timetables").Filter("SimSig WTT(*.WTT)", "wtt").Load() + if err != nil { + walk.MsgBox(mainWindow, "Timetable Load Error", err.Error(), walk.MsgBoxOK|walk.MsgBoxIconError) + return + } + result := loadTimetable(filename) + if result != "" { + walk.MsgBox(mainWindow, "Timetable Load Error", result, walk.MsgBoxOK|walk.MsgBoxIconError) + } else { + timetableLabel.SetText(filename) + connectButton.SetEnabled(true) + chooseButton.SetEnabled(false) + } + }, + }, + Label{ + AssignTo: &timetableLabel, + Text: "No timetable selected", + EllipsisMode: EllipsisPath, + }, + + Label{ + Text: "User:", + TextAlignment: AlignFar, + }, + LineEdit{AssignTo: &userEdit}, + + Label{ + Text: "Password:", + TextAlignment: AlignFar, + }, + LineEdit{ + AssignTo: &passwordEdit, + PasswordMode: true, + }, + Label{ + Text: "Interface Gateway:", + TextAlignment: AlignFar, + }, + LineEdit{ + AssignTo: &addressEdit, + Text: "localhost:51515", + }, + }, + }, + PushButton{ + AssignTo: &connectButton, + Text: "Connect", + Enabled: false, + OnClicked: func() { + statusLink.SetText("Connecting to " + addressEdit.Text()) + connectButton.SetEnabled(false) + go gatewayConnection(userEdit.Text(), passwordEdit.Text(), addressEdit.Text()) + }, + }, + LinkLabel{ + AssignTo: &statusLink, + //MaxSize: Size{500, 0}, + Text: `Not Connected to SimSig`, + OnLinkActivated: func(link *walk.LinkLabelLink) { + browser.OpenURL("http://localhost:8090/") + }, + }, + }, + }.Run() +} + +func updateStatus(status string) { + mainWindow.Synchronize(func() { + statusLink.SetText(status) + }) +} + +func webInterfaceReady() { + mainWindow.Synchronize(func() { + statusLink.SetText("Open Departure Board") + }) +} + +func showError(message string) { + mainWindow.Synchronize(func() { + walk.MsgBox(mainWindow, "Error", message, walk.MsgBoxOK|walk.MsgBoxIconError) + }) +} diff --git a/cmd/departureboard/window.manifest b/cmd/departureboard/window.manifest new file mode 100644 index 0000000..1492a19 --- /dev/null +++ b/cmd/departureboard/window.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + PerMonitorV2, PerMonitor + True + + + \ No newline at end of file diff --git a/go.mod b/go.mod index 9aaba3c..c276469 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,11 @@ module github.com/jonathanp0/go-simsig go 1.14 require ( + github.com/akavel/rsrc v0.10.2 // indirect github.com/go-stomp/stomp/v3 v3.0.3 + github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 + github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/sqweek/dialog v0.0.0-20211002065838-9a201b55ab91 + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index 5068187..3b29513 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I= github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I= +github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/go-stomp/stomp v1.0.2 h1:apk/+6YIaphgNd+K0MoOkxENskEa9dSDUq5OADDcqKw= github.com/go-stomp/stomp v2.1.4+incompatible h1:D3SheUVDOz9RsjVWkoh/1iCOwD0qWjyeTZMUZ0EXg2Y= github.com/go-stomp/stomp/v3 v3.0.3 h1:7YQGJCDMkbA05Rw8dS00LxwU1mhzEHS69gMlPjMZGDk= @@ -8,10 +10,17 @@ github.com/jonathanp0/go-simsig v0.0.0-20220114202851-dce4229f8483 h1:lkf5Q4/+Px github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/sqweek/dialog v0.0.0-20211002065838-9a201b55ab91 h1:Ap4SC7+bIAFzh81vREQSElqYUtuxPgknVl1ol5rOf9w= github.com/sqweek/dialog v0.0.0-20211002065838-9a201b55ab91/go.mod h1:/qNPSY91qTz/8TgHEMioAUc6q7+3SOybeKczHMXFcXw= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71 h1:X/2sJAybVknnUnV7AD2HdT6rm2p5BP6eH2j+igduWgk= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=