Compare commits

...

328 Commits

Author SHA1 Message Date
Michael Rausch 25d8c8f9c4 Minor bug fixes.
Server will now only save incoming connections if in lobbying mode as it should
Commented out boat status printing

tags: #story[1047]
2017-07-27 14:15:55 +12:00
Zhi You Tan 1d9dd76356 For client's lobby view, it now can shows the connected host IP
#story[1055]
2017-07-27 13:12:53 +12:00
Zhi You Tan c2c34705d5 removed an unnecessary print statement and caught an exception 2017-07-27 13:01:04 +12:00
Zhi You Tan 96ed5e445e Replaced print stack trace with print statements 2017-07-27 12:47:18 +12:00
Kusal Ekanayake 870dc07fd2 Slight improvements to hosting.
Allow a host/client to disconnect and reconnect/make lobby, leave lobby and play the game.

#pair[kre39,hyi25] #story[1047]
2017-07-27 12:45:22 +12:00
Haoming Yin 34704bd93d Merge remote-tracking branch 'origin/develop' into develop 2017-07-26 23:32:32 +12:00
Haoming Yin 201c88a253 Fixed a bug that program crashes when it receive regatta xml during the race, as it doesn't know how to handle the xml.
#story[984]
2017-07-26 23:32:16 +12:00
Peter Galloway 6c4da58d9d Merge remote-tracking branch 'origin/develop' into develop 2017-07-26 22:42:48 +12:00
Peter Galloway b56fa5cba3 refactored to make boat trails work again #fix #refactor 2017-07-26 22:42:37 +12:00
Peter Galloway 9cedbeb6f6 Fixed latency issues caused by clientside movement packets queueing up but currently the trails aren't working anymore #fix #refactor 2017-07-26 22:02:46 +12:00
Michael Rausch 4c6d107102 Merge remote-tracking branch 'origin/develop' into develop 2017-07-26 20:28:21 +12:00
Michael Rausch 7917a2584b Fixed alignment for wind direction 2017-07-26 20:28:13 +12:00
Alistair McIntyre f99f8a0d7c Merge remote-tracking branch 'origin/develop' into develop 2017-07-26 20:13:09 +12:00
Alistair McIntyre 7db716f51c Boat should move towards optimal angle upwind and downwind when pressing spacebar (VMG)
#story[988]
2017-07-26 20:12:57 +12:00
Haoming Yin 592e5a088f Merge remote-tracking branch 'origin/develop' into develop 2017-07-26 19:58:45 +12:00
Haoming Yin 2d6850950c Merge branch 'develop' into Documenting_client_and_server
# Conflicts:
#	src/main/java/seng302/client/ClientToServerThread.java
2017-07-26 19:57:43 +12:00
Haoming Yin ef6821a0cd Updated and added more documentations
#story[1047]
2017-07-26 19:55:35 +12:00
Michael Rausch 6e9535d78f Fixed race timer & Added boats to team position list
- Race status messages are sent at regular intervals instead of once at race start
- Boat positions are initialised on the Team Position list
- Timer counts up from when host clicks ready

Tags: #story[377]
2017-07-26 19:35:59 +12:00
Alistair McIntyre 72a45f5984 Fixed a bug where if boatMaxSpeed was 0 and sails were up, boat would not decelerate.
#story[986]
2017-07-26 17:11:34 +12:00
Alistair McIntyre de600fa062 Merge remote-tracking branch 'origin/develop' into develop 2017-07-26 17:10:29 +12:00
Haoming Yin 87e3b4f246 Merge branch 'story61_player_highlighting' into 'develop'
Story61 player highlighting

Added some highlighting for the players boat and a random name generator to help distinguish clients. The highlighting implementation is pretty hackish since they will be replaced by the visualiser overhaul anyways. To avoid refactoring a lot of code I just made some unused javaFX elements invisible rather than removing them, I can do it properly if requested.

See merge request !48
2017-07-26 16:31:53 +12:00
Alistair McIntyre 7b47d72ef0 Merge branch 'develop' of /home/cosc/student/ajm412/Documents/SENG302/team-13 with conflicts. 2017-07-26 16:22:01 +12:00
Calum 7392bdb80d Added documentation
#document
2017-07-26 16:03:11 +12:00
Zhi You Tan af9f1417f1 Documented client packet parser, client state, client state querying runnable, client to server thread and replaced e.printStackTrace() with client log messages. 2017-07-26 16:01:41 +12:00
Calum 8fc00fd750 Enabled boat path for only the player.
#story[987]
2017-07-26 16:01:01 +12:00
Calum 4e5b67abfa Merge branch 'develop' into story61_player_highlighting 2017-07-26 15:52:29 +12:00
Calum 84e8ac89fc Added random name generator until players can chose how to identify themselves. 2017-07-26 15:52:02 +12:00
Calum 12d081a1af Added highlighting for the player and moved all their assets to the foreground
#story[987]
2017-07-26 15:49:00 +12:00
William Muir 5e6b402bf5 Minor structural changes. GameState now has a thread which updates itself so its update
rate can be independent of sending packets to the client in the MainServerThread

#story[986] #pair[wmu16, ptg19]
2017-07-26 14:48:32 +12:00
Alistair McIntyre 2bfa6cb038 Adjusted the velocity calculations to allow for Acceleration/Deceleration of the boats, rather than just coming to a complete halt when the sails are pulled in.
#story[986]
2017-07-26 14:01:51 +12:00
William Muir d99055901f Removed Music 2017-07-26 11:38:29 +12:00
Haoming Yin c77a48f589 Merge remote-tracking branch 'origin/develop' into develop 2017-07-25 23:06:06 +12:00
Haoming Yin 0135426dfe Changed lobby player profile to a lower risk pictures
#story[1055]
2017-07-25 23:05:52 +12:00
William Muir 37c745c139 Polar velocities should now work as intended.
Snapping to VMG still needs to be implemented.
Still an issue of not being able to pass the total upwind or downwind point

tags: #story[986]
2017-07-25 22:27:26 +12:00
William Muir 7880039801 Merge remote-tracking branch 'origin/develop' into develop 2017-07-25 22:19:19 +12:00
William Muir a56dac1e87 Polar velocities should now work as intended.
Snapping to VMG still needs to be implemented.
Still an issue of not being able to pass the total upwind or downwind point

tags: #story[986]
2017-07-25 22:19:03 +12:00
Haoming Yin 4ae422b47b When a player connects to the server, lobby will now show their user id and profile instead of showing all the profile gif at the beginning,
#story[1055] #pair[hyi25, zyt10]
2017-07-25 21:50:23 +12:00
Michael Rausch c8a96dcce9 Fixed XML Loading error, used VMG to calculate boat velocity
- XML read from stream instead of file
- Started implementing VMG to calculate boat velocity dynamically

Tags: #pair[wmu16, mra106] #story[986]
2017-07-25 20:14:50 +12:00
Kusal Ekanayake 4f2dca7ecf Added proper the starting locations for marks and boats. 2017-07-25 18:48:33 +12:00
Zhi You Tan e569574c01 Merge remote-tracking branch 'origin/develop' into develop 2017-07-25 17:17:45 +12:00
Zhi You Tan f544734b4d Fixed sending wrong race xml when a player disconnected because xml is getting data from gamestate yacht but the yachts are not updated is player disconnect.
Heartbeat packet was sent out at wrong rate which cause the player disconnect detection to be slow. Heartbeat packet is send out every 200ms now.

#story[1055] #pair[hyi25, zyt10]
2017-07-25 17:17:36 +12:00
Kusal Ekanayake 80b439470b Merge remote-tracking branch 'origin/develop' into develop 2017-07-25 15:22:56 +12:00
Kusal Ekanayake 539197cef5 Added a visual indicator of wind speed during race (text box)
#pair[kre39,mra106] #story[1040]
2017-07-25 15:22:46 +12:00
Zhi You Tan 1a867be387 Added keystroke frequency limit
Updated client and server log format

#story[988] #pair[hyi25, zyt10]
2017-07-25 15:13:48 +12:00
Kusal Ekanayake 3785cd705f Fixed wind speed and direction being sent correctly.
#pair[kre39,mra106] #story[1036]
2017-07-25 15:08:10 +12:00
Kusal Ekanayake 5d7a438080 Changed default ip back to non local host. 2017-07-25 14:28:21 +12:00
Kusal Ekanayake 2f12f3e34f Fixed bug to let multiple people play at the same time. 2017-07-25 14:27:52 +12:00
Zhi You Tan 52bfa3ad34 Merge remote-tracking branch 'origin/Story1055_Send_Race_Status_When_Host_Ready' into develop
# Conflicts:
#	src/main/java/seng302/controllers/LobbyController.java
2017-07-25 00:03:58 +12:00
Zhi You Tan d1d659b698 Lobby view will switch to race view when received race status packet with race start type.
Ready button can only be pressed by host. Once pressed, it will send out race status packet with race start to all clients.

#story[1055]
2017-07-25 00:01:59 +12:00
William Muir cdb9337aed Deleted GameServerThread after being re merged in
#chore
2017-07-24 23:20:47 +12:00
Zhi You Tan 8b0af5bb62 Updated observer so it sends out updated boats.xml when client disconnects
#story[1047] #pair[wmu16, zyt10]
2017-07-24 21:35:31 +12:00
Zhi You Tan 83232a935e Merge remote-tracking branch 'origin/Story984_Send_XML_After_Clients_Connect' into develop
# Conflicts:
#	src/main/java/seng302/gameServer/ServerToClientThread.java
2017-07-24 21:30:12 +12:00
William Muir a30a1aa7c7 Tweaking to server loop making it send packets at 5Hz
Commented out some smoothing code in BoatGroup that was dependend on FPS screwing with movement
2017-07-24 21:14:17 +12:00
William Muir 07cebb6c5b Updated velocity in yacht constructor so the boat can be seen properly working for test purposes 2017-07-24 16:47:17 +12:00
Kusal Ekanayake 45bf65a3d3 Made clients receive new xml after new clients connect.
#pair[kre39,zyt10] #story[984]
2017-07-24 16:15:19 +12:00
Kusal Ekanayake 60f5a99b0c Merge branch 'Merging_GameLoop_with_Broadcast' into develop
# Conflicts:
#	src/main/java/seng302/controllers/StartScreenController.java
2017-07-24 15:32:52 +12:00
Kusal Ekanayake 526c12127f Merging game lobby with game state broadcast. Merge conflicts resolved.
Port numbers updated to 4942.
2017-07-24 15:26:51 +12:00
Kusal Ekanayake 1daac842f2 Merging game lobby with game state broadcast 2017-07-24 15:01:07 +12:00
Kusal Ekanayake 3e4a6f0f2e Merge remote-tracking branch 'origin/Story1055_Lobby_View_Update' into Merging_GameState_With_Lobby
# Conflicts:
#	src/main/java/seng302/client/ClientPacketParser.java
#	src/main/java/seng302/client/ClientToServerThread.java
#	src/main/java/seng302/controllers/LobbyController.java
#	src/main/java/seng302/controllers/StartScreenController.java
#	src/main/java/seng302/gameServer/ServerToClientThread.java
2017-07-24 14:58:14 +12:00
Kusal Ekanayake c1e937049e Merge remote-tracking branch 'origin/Story62_Swtich_To_RaceView_When_Race_Start' into Merging_GameLoop_with_Broadcast
# Conflicts:
#	src/main/java/seng302/controllers/LobbyController.java
#	src/main/java/seng302/controllers/StartScreenController.java
#	src/main/java/seng302/gameServer/ServerToClientThread.java
2017-07-24 14:37:56 +12:00
Zhi You Tan 8f8d5c7384 Removed a test sending xml
#story[1055]
2017-07-24 14:24:54 +12:00
Zhi You Tan 027c7a1480 Updated start screen controller to connect to itself after setting up server.
#story[1047]
2017-07-24 12:09:23 +12:00
Zhi You Tan df2efa3329 Lobby controller list view is able to react to changes in client state boats and update the list view appropriately.
#story[1055]
2017-07-24 11:15:10 +12:00
Zhi You Tan 9d754c8819 Implemented list views initialisation which will set the first pane to be your source id (after three way handshake) and the remaining pane to be the source id of other players based on boats.xml received.
Updated client parser and client state to save a list of player's boat

WIP: refresh list view to show the latest update in players

#story[1055]
2017-07-23 20:42:21 +12:00
Zhi You Tan e11ceed28c Merge remote-tracking branch 'origin/Story62_Creating_Game_Loop' into Story1055_Lobby_View_Update
# Conflicts:
#	src/main/java/seng302/controllers/StartScreenController.java
#	src/main/java/seng302/gameServer/ServerToClientThread.java
2017-07-23 19:59:30 +12:00
Michael Rausch 8b8b6e4afa Fixed map glitch when race starts, and race XML bug
- Race XML wasn't being sent to clients, this was causing a null ptr exception
- Boat location was being set to an invalid lat/lng

Tags: #story[1047]
2017-07-23 18:20:13 +12:00
Kusal Ekanayake ed2a22b573 Tried to merge game loop with the broadcast. Minor error in canvas. need to fix.
#story[1047]
2017-07-23 17:37:45 +12:00
Kusal Ekanayake 41851ee925 Merge branch 'Story62_Creating_Game_Loop' into Merging_GameLoop_with_Broadcast
# Conflicts:
#	src/main/java/seng302/controllers/StartScreenController.java
#	src/main/java/seng302/gameServer/ServerListenThread.java
#	src/main/java/seng302/gameServer/ServerToClientThread.java
#	src/main/java/seng302/models/Yacht.java
2017-07-23 17:34:24 +12:00
Kusal Ekanayake ffc61942a9 Fixed broken pipe error.
Was caused by boat ids being over 1000 and when turned into xml having a comma placed between the hundreds and the thousands? So to fix, I just mad the max id number 999 so there should not be any issues regarding that if another broken pipe error occurs. Also switched to use the correct yacht constructor.

#story[1047]
2017-07-23 17:10:18 +12:00
Kusal Ekanayake 2e4382bff6 Trying to fix the boat information that is being sent over.
Current issue is that a broken pipe error keeps occurring potentially due to how the messages are being sent so quickly that the previous message may have not finished sending. Also only caused the xm packets to be sent on trigger when a new client connects.

#story[1047]
2017-07-23 16:22:59 +12:00
Zhi You Tan f542dbb61e Changed the competitors list view to eight individual list view.
Added eight individual image view to support future player icon implementation.

#story[1055]
2017-07-23 03:00:29 +12:00
William Muir 2869d139a3 Three way handshake implemented client and server side and functioning
Server generates a new Id for connections (Size of connections + 1)
Client now stores an id allocated to it by the server
The id is currently being saved in the clientToServer thread client side

tags: #story[987] #implement
2017-07-22 17:44:37 +12:00
William Muir 3ec930491f Minor refactor, threads now start themselves
tags: #story[989] #refactor
2017-07-22 16:45:24 +12:00
William Muir a0005064ac Fixed the Yacht clas so it now works.
Lists and Maps are instantiated as they should be in GameState which were creating NullPointers
Introduced ServerLog to server to client threads for bug reporting
Introduced some print statements to test the game state updating upon receiving key presses

tags: #story[989] #refactor #fix
2017-07-22 16:32:05 +12:00
Michael Rausch 33fae9d69a Added race boats to XML Generator
Tags: #story[1047]
2017-07-21 16:56:46 +12:00
Peter Galloway 913e5fee7b Hooked up key press actions to the GameState, applying the relevant maths to update headings etc.
tags: #story[989]  #pair[ptg19, wmu16]
2017-07-21 16:50:09 +12:00
Zhi You Tan 3992073303 Set race started state in client state when packet is passed in client packet parser.
#story[1055]
2017-07-21 16:36:56 +12:00
Zhi You Tan 797a99f632 Merge remote-tracking branch 'origin/develop' into Story62_Swtich_To_RaceView_When_Race_Start
# Conflicts:
#	src/main/java/seng302/gameServer/GameState.java
2017-07-21 16:35:04 +12:00
Zhi You Tan e891ed8a64 LobbyView now can change to RaceView upon race start packet received.
Added port number text field in start screen controller.
Created a client state.

#story[1055] #pair[hyi25, zyt10]
2017-07-21 16:14:45 +12:00
Michael Rausch e8c2cf809b Fixed Null Ptr Exception in GameState 2017-07-21 15:42:20 +12:00
Michael Rausch ec761893c7 Merge branch 'Story62_Reading_Keystrokes' into 'develop'
Story62 reading keystrokes

Big merge

See merge request !44
2017-07-21 15:39:39 +12:00
Kusal Ekanayake 5df7efda03 Started implementing the gameState broadcasts.
Initial xml files are almost broad casted, just need to create them (or import the for the regatta). Started the setup for sending boat location packets, should work once we get at least the boat xml working when being sent.

#story[1047]
2017-07-21 13:16:43 +12:00
Kusal Ekanayake 2fff73c075 Merge remote-tracking branch 'origin/984_Xml_Generation' into Broadcasting_GameState
# Conflicts:
#	src/main/java/seng302/controllers/FinishScreenViewController.java
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/gameServer/GameServerThread.java
#	src/main/java/seng302/gameServer/GameState.java
#	src/main/java/seng302/models/Yacht.java
2017-07-21 12:31:35 +12:00
Zhi You Tan d37cbd263e Merge remote-tracking branch 'origin/Story62_Creating_Game_Loop' into Story62_Reading_Keystrokes
# Conflicts:
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/gameServer/MainServerThread.java
#	src/main/java/seng302/gameServer/ServerToClientThread.java
2017-07-21 11:13:40 +12:00
Peter Galloway 12c2f31af9 Merge remote-tracking branch 'origin/Story62_Creating_Game_Loop' into Story62_Creating_Game_Loop
# Conflicts:
#	src/main/java/seng302/gameServer/GameState.java
2017-07-20 19:38:07 +12:00
Peter Galloway 49c0c029c3 adjusted the way the server is receiving key presses to enable them be passed through to the game state #pair[ptg19, wmu16] #story[989] 2017-07-20 19:35:59 +12:00
Alistair McIntyre da7a34fc55 Started adding functionality to calculate yacht velocity from the wind speed and direction using polar tables. Also began writing tests to cover this functionality, as it can't currently be tested within the game itself.
#story[986]
2017-07-20 14:30:13 +12:00
Michael Rausch 322ff740e2 Added race start time to race XML
- Added race start time to race XML
- Added documentation
- Removed test code in MainServerThread

Tags: #story[984]
2017-07-20 14:28:59 +12:00
Kusal Ekanayake b1575e57df Host also can be it's own client.
The host can connect to itself to become a client, packets are also sending from the host to client, update method should be ready to fully implemented. Added chatter packets to packet types to be used mostly for testing but can be further implemented for proper use in the future.

#story[1055]
2017-07-20 13:53:53 +12:00
Michael Rausch 82b219cdba Boat and race XML now generated dynamically
- Removed course from XML Generator as it was not needed
- Boat and race XML added
- Method names in Yacht class updated to follow JavaBean standard so they can be read by the template engine

Tags: #story[984]
2017-07-20 13:30:55 +12:00
Peter Galloway e317de7562 Added mock yachts to the game state for each client #story[1047] 2017-07-20 13:11:37 +12:00
Zhi You Tan 1e80d76acd Retrieve local host ip address and show it on lobby view.
#story[1055] #pair[hyi25, zyt10]
2017-07-20 12:52:53 +12:00
Peter Galloway 176d65e0b2 Merge branch 'Story62_Reading_Keystrokes' into Story62_Creating_Game_Loop 2017-07-20 12:01:53 +12:00
William Muir 0c08f5a03c Refactoring to remove all superflous classes related to the server
GameServerThread --> MainServerThread
All server classes consolidated into the gameServer package and all others removed

tags: #story[1055] #refactor #fix
2017-07-20 11:46:06 +12:00
William Muir e257602b78 Merge remote-tracking branch 'origin/Story62_Reading_Keystrokes' into Story62_Reading_Keystrokes
# Conflicts:
#	src/main/java/seng302/controllers/LobbyController.java
#	src/main/java/seng302/gameServer/MainServerThread.java
2017-07-20 11:35:27 +12:00
William Muir 8f00f3a80c Refactoring for server package, Changed GameServerThread to MainServerThread.
All Server classes now in single gameServer package

tags: #story[1055]
2017-07-20 11:22:30 +12:00
Peter Galloway 63d24c001f Added position update in yacht #story[1047] 2017-07-19 19:41:07 +12:00
Peter 67668fe1fc Merge branch 'Story62_Reading_Keystrokes' into Story62_Creating_Game_Loop 2017-07-19 18:13:58 +12:00
Kusal Ekanayake dbbb41e12f Multiple clients are now shown in lobby of host.
When multiple clients connect to the host, the lobby table now fills up with the names of the threads (will need to be changed later). Re-enabled multiple people connecting and the lobby table. Formatting changes in the lobby screen also made.

#story[1047]
2017-07-19 16:05:21 +12:00
Michael Rausch 45053ba507 Added XML Generation
- Implemented a wrapper for Apache Freemake
- Implemented the regatta XML generator

Tags: #story[984]
2017-07-19 14:47:16 +12:00
William Muir b301ce5d27 fixed build (i think?)
tags: #story[1055] pair[wmu16, zyt10]
2017-07-18 12:26:47 +12:00
William Muir f02bd3b3f8 Merge remote-tracking branch 'origin/Story62_Reading_Keystrokes' into Story62_Reading_Keystrokes 2017-07-18 12:23:09 +12:00
William Muir e83eaa38e1 Upon hosting, and then creating a new instance and connecting to that IP, button transmissions work and print out on server!! :D
Took the send method out of the Message class as it didnt make sense to have it there. This meant taking it out of all subclasses too

tags: #story[1055] pair[wmu16, zyt10]
2017-07-18 12:22:58 +12:00
Kusal Ekanayake 102b5f3ca1 Added a program icon that I snipped from the logo using photoshop. 2017-07-18 10:46:41 +12:00
Haoming Yin 63958a6717 WIP: Implemented a temporary workaround to send an instance test to client server upon connection.
Still needs reengineering to change socket channels for sending to ouput stream in the message class.
Only client to server "working".

#story[1047] #pair[hyi25, wmu16] #pair[cir27, zyt10]
2017-07-17 17:00:04 +12:00
Haoming Yin 4b8ac32ca9 Merge branch '1047_Hosting_Game' into Story62_Reading_Keystrokes 2017-07-17 10:55:17 +12:00
Haoming Yin 00b29a1890 Merge remote-tracking branch 'origin/develop' into Story62_Reading_Keystrokes 2017-07-17 10:55:08 +12:00
Haoming Yin c7e5f93bc4 Merged GeoUtility and GeometryUtils classes
#story[1047]
2017-07-16 21:58:40 +12:00
Haoming Yin e4d87c91a2 Merge branch 'develop' into 1047_Hosting_Game 2017-07-16 21:54:08 +12:00
Zhi You Tan f84091e54e Removed (fxml) table view and its table column from lobby controller because the table view is removed from LobbyView.fxml
#story[988]
2017-07-14 18:27:54 +12:00
Zhi You Tan e03e8825b2 Merge remote-tracking branch 'origin/1047_Hosting_Game' into Story62_Reading_Keystrokes 2017-07-14 18:23:28 +12:00
Zhi You Tan 355f8543f5 Implemented a more reliable way for keystroke input and added boat action packet type so stream parser is able to read and decode the message appropriately.
#story[988] #pair[hyi25, zyt10]
2017-07-14 18:23:07 +12:00
William Muir 77e7db79cc WIP: Worked on new server thread class that would create and store multiple THREADS of connections. Researched Authorative server structures.
Fixed the current structure of the server to work with the old StreamReciever style and hook up to the Stream Parser

tags: #story[1047] pair[wmu16, mra106]
2017-07-14 17:09:33 +12:00
Zhi You Tan 2809d0d832 Merge branch '1047_Hosting_Game' into Story62_Reading_Keystrokes
# Conflicts:
#	src/main/resources/views/RaceView.fxml
2017-07-14 16:28:42 +12:00
William Muir 5b908ec355 Merge remote-tracking branch 'origin/1047_Hosting_Game' into 1047_Hosting_Game
# Conflicts:
#	src/main/java/seng302/gameServer/GameServerThread.java
2017-07-13 22:55:49 +12:00
William Muir c480fca72a WIP: Worked on new server thread class that would create and store multiple THREADS of connections. Researched Authorative server structures
tags: #story[1047]
2017-07-13 22:55:03 +12:00
Michael Rausch c19f66a6a4 Added garbage collection for disconnected players
- Heartbeat messages are sent out from their own thread to each player
- If a heartbeat message can't be sent to a player, they are removed from the list of players
- Added equals method for players
Tags: #story[1047]
2017-07-13 22:07:03 +12:00
Michael Rausch 1e6fd1af09 Fixed bug where players were being added to the GameState twice
#story[1047]
2017-07-13 19:27:47 +12:00
Michael Rausch 55db2c9961 Fixed buffer overflow in message header
- Fixed buffer overflow by adding a reset method that  clears the buffer and sets the position to zero before re-writing the header
Tags: #story[1047]
2017-07-13 19:15:45 +12:00
Haoming Yin 6ec8b0c3c5 Terminated the game server socket when click exit lobby button
- the whole server thread should be terminated instead. To be fixed in the future.

#story[1047] #issue[28]
2017-07-13 17:51:25 +12:00
Kusal Ekanayake 78557a4536 Key presses are transmitted to a host (but there is no host currently connected)
#pair[kre39,zyt10] #story[988]
2017-07-13 15:39:48 +12:00
William Muir 8090cd7985 WIP: Adapted the old server thread class to the GameServerThread class to allow multiple clients to connect
tags: #story[1047]  #pair[wmu16]
2017-07-13 14:40:11 +12:00
Kusal Ekanayake 5ce34bed92 Key presses now assigned to enum and empty packet class is constructed.
#pair[kre39,zyt10] #story[988]
2017-07-13 14:35:41 +12:00
William Muir 035841f221 WIP: Adapted the old server thread class to the GameServerThread class to allow multiple clients to connect
tags: #story[1047]  #pair[wmu16, mra106]
2017-07-11 17:03:32 +12:00
Kusal Ekanayake ef61a687d6 Researched and implemented a way for the game to listen for key presses. When one of the valid key controls are pressed, feedback is given in the console. Has yet to be connected to a method which will create and send a message to the server.
#pair[kre39,zyt10] #story[988]
2017-07-11 16:32:15 +12:00
Haoming Yin fcb1e5e593 Removed unnecessary Position and GeoPoint classes to clear the code base.
- put utility classes in a package

#story[1047]
2017-07-10 23:51:01 +12:00
William Muir 752863a0d3 WIP: Created some basic controllers for the UI and started implementing backend for server stuff
Created GameState Class. Static, contains all info about current state of game: list of players and their respective details (coords etc) also Host info

Clients will observe this GameState class

Upon pressing host, new GameState is created and new GameConnectionListener created which listens for connections

Player class created which will be used to take each connection and store it as a player with other info about them regarding the game in the GameState class

tags: #story[1047]  #pair[wmu16, zyt10]
2017-07-10 16:03:13 +12:00
William Muir 1a3e330eb4 Created some basic UI for new start screen (host and connect) and a lobby
tags: #story[1047]  #pair[wmu16, zyt10]
2017-07-10 13:43:25 +12:00
William Muir 5f9da6b40a Fixed the bug where the polar file could not be read after being packaged
tags: #story[955]  #pair[wmu16, zyt10]
2017-07-10 12:36:32 +12:00
Michael Rausch fe76ef9cdc Merge remote-tracking branch 'origin/develop' 2017-05-25 16:58:01 +12:00
Michael Rausch acbaa838ec Change default port to 4940
#story[956] #pair[wmu16, mra106]
2017-05-25 16:56:41 +12:00
Haoming Yin f4134d83b5 Merge branch 'issue#8_create_finish_screen' into 'develop'
Issue#8 create finish screen

Created a finish screen which user can see position of the final result and able to go back to start screen to rewatch new race

See merge request !41
2017-05-25 16:52:59 +12:00
Zhi You Tan 24cc10e1cd Merge remote-tracking branch 'origin/develop' into issue#8_create_finish_screen
# Conflicts:
#	src/main/java/seng302/controllers/CanvasController.java
2017-05-25 16:51:32 +12:00
Zhi You Tan 20b79b40f2 Fix the button position #story[923] 2017-05-25 16:45:35 +12:00
Zhi You Tan 72e2776b7e Fix the finish screen size #story[923] 2017-05-25 16:41:59 +12:00
Kusal Ekanayake 49e4c92da6 Merge branch 'develop' 2017-05-25 16:40:24 +12:00
Kusal Ekanayake ba761e4951 Merge branch 'remove_observers' into 'develop'
Remove observers

Refactor for annotations and a few improvements here and there. Fix for several tracked issues and numerous other bug fixes.

See merge request !40
2017-05-25 16:36:43 +12:00
Calum c1aa1d8eae merged with dev 2017-05-25 16:32:25 +12:00
Calum 4231c3ccd8 Merge remote-tracking branch 'origin/develop' into remove_observers 2017-05-25 16:30:15 +12:00
Calum 65223ceaaf shifted annotation layers. Merged with dev. 2017-05-25 16:29:27 +12:00
Kusal Ekanayake ca22615c08 Add scroll bar to the side panel to cater for smaller screen. 2017-05-25 16:18:47 +12:00
Zhi You Tan 559a9f38c0 Merge remote-tracking branch 'origin/develop' into issue#8_create_finish_screen
# Conflicts:
#	src/main/java/seng302/controllers/CanvasController.java
#	src/main/java/seng302/models/stream/StreamParser.java
2017-05-25 16:12:44 +12:00
Kusal Ekanayake 762829e5ff Merge branch 'develop' 2017-05-25 15:56:10 +12:00
Kusal Ekanayake 835f79b113 Swapped to the local stream as default 2017-05-25 15:56:02 +12:00
Kusal Ekanayake e0854bc68c Merge remote-tracking branch 'origin/master' 2017-05-25 14:55:35 +12:00
Kusal Ekanayake cec7014856 Set axis ons parkline style 2017-05-25 14:54:34 +12:00
Calum b73e4c89db Merge remote-tracking branch 'origin/develop' into remove_observers
# Conflicts:
#	src/main/java/seng302/controllers/CanvasController.java
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/fxObjects/BoatGroup.java
#	src/main/java/seng302/fxObjects/MarkGroup.java
#	src/main/java/seng302/models/Yacht.java
2017-05-25 14:52:54 +12:00
Kusal Ekanayake f1a9da83fc Quick fixes before merge 2017-05-25 14:22:01 +12:00
Kusal Ekanayake 6e903bfbed Merge branch 'develop' into 38b_LayLines
# Conflicts:
#	src/main/java/seng302/App.java
#	src/main/java/seng302/models/Yacht.java
#	src/main/java/seng302/models/stream/StreamParser.java
2017-05-25 14:16:55 +12:00
Michael Rausch 23d62f552e Merged develop onto this branch
Tidied code, removed print statements

#story[956] #pair[wmu16, mra106]
2017-05-25 14:06:33 +12:00
Kusal Ekanayake 53f6a6b8c5 Implimented a new way to determine positions
#story[952]
2017-05-25 14:06:33 +12:00
Michael Rausch af81bf5891 Merge remote-tracking branch 'origin/develop' into 38b_LayLines
# Conflicts:
#	src/main/java/seng302/models/BoatGroup.java
#	src/main/java/seng302/models/Event.java
#	src/main/java/seng302/models/mark/Mark.java
#	src/main/java/seng302/models/stream/StreamParser.java
2017-05-25 13:56:08 +12:00
Michael Rausch e72ac1def8 Laylines are now togglable. Still not completely fool proof
Laylines appear only for the selected boat. The dissapear if the boat is not seleccted. Laylines are the colour of the boat

Laylines do not yet automatically update for a selected boat when it passes a mark

There is still a polling interval of a second in which if you select a boat after it has just passed a mark it still thinks the next mark is that mark and laylines are displayed incorrectly

#story[956] #pair[wmu16, mra106]
2017-05-25 13:48:48 +12:00
Zhi You Tan f163dfdd11 Removed a print statement 2017-05-25 13:14:36 +12:00
Zhi You Tan a1eda8d91d Fix boatPos in StreamParser.java being empty and affecting all the assessors 2017-05-25 12:58:11 +12:00
Zhi You Tan 5ed02a1fe1 Merge remote-tracking branch 'origin/develop' into issue#8_create_finish_screen
# Conflicts:
#	src/main/java/seng302/controllers/CanvasController.java
2017-05-25 12:32:10 +12:00
Zhi You Tan 66d4a4b958 Fix finish screen showing wrong ("-" as) position 2017-05-25 12:28:13 +12:00
Calum adbb9ffe3b Removed completed TODO request. 2017-05-25 10:31:37 +12:00
Calum 0cd2867ac0 Made annotations more readable when overlapping. 2017-05-25 10:30:01 +12:00
Calum 4d29354797 Merge branch 'develop' into remove_observers
# Conflicts:
#	src/main/java/seng302/controllers/CanvasController.java
#	src/main/java/seng302/fxObjects/BoatGroup.java
#	src/main/java/seng302/models/stream/StreamParser.java
2017-05-25 10:25:54 +12:00
Calum a6d9c66fc9 Removed unused import statements and class variables. Removed non error print statements. 2017-05-25 10:20:39 +12:00
William Muir 99588c7ff8 Laylines now working upon selecting a boat. Still need to make their visibility togglable
Currently just paints upon clicking

tags: #story[956]
2017-05-25 09:54:00 +12:00
William Muir 68c3e3e999 Merged develop onto this branchk
tags: #story[956]
2017-05-25 09:21:47 +12:00
William Muir 67b5650288 Merge branch 'develop' into 38b_LayLines
# Conflicts:
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/models/Yacht.java
2017-05-25 09:19:42 +12:00
Calum 3085125f3e Merged with develop 2017-05-25 01:21:40 +12:00
Calum 5e26ad7c36 Merge remote-tracking branch 'origin/develop' into remove_observers
# Conflicts:
#	src/main/java/seng302/App.java
#	src/main/java/seng302/controllers/CanvasController.java
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/models/Yacht.java
2017-05-25 01:12:10 +12:00
Calum 765ea06c3b Several tweaks and improvements to a annotations and other visual aspects of them program. Fixed bug causing minimization crashes.
#implement #refactor #issue[23]
2017-05-25 01:02:33 +12:00
William Muir ffe70a8313 Trying to get laylines to display on proper respective sides of the gate
Having a great time here, Not quite working. There was an issue where the laylines are crossed and I think its tied to the fact that sometimes the two seperate points of the gate mark appear to have the same x y coordinate so the line function doesnt work? idk

tags: #story[956] #pair[wmu16]
2017-05-25 00:22:15 +12:00
Haoming Yin 331e0fc6ab Merge branch 'Story34_Sparklines' into 'develop'
Story34 sparklines



See merge request !38
2017-05-24 22:24:50 +12:00
Kusal Ekanayake b3fd735f5c Merge branch 'develop' into Story34_Sparklines
# Conflicts:
#	src/main/java/seng302/controllers/CanvasController.java
2017-05-24 21:50:15 +12:00
William Muir 3cbbdb070f Basic implementation for mapping windspeed to draw a polar on a gate complete
Created functionality to grab the closest windspeed value to map to VMG values based off the current wind speed in the Polar Table

Created new RaceXML mark object which contains ALL marks for purposes of sequencing

Displaying correct (?) polars for one point only on a gate

Created functionality to receive leg data for each boat and then map that to the next gate. This may only work for the current race due to a slight fudge factor

Created functionality to receive wind speed

tags: #story[956] #pair[wmu16, mra106]
2017-05-24 20:31:07 +12:00
Haoming Yin d2e55bf964 Removed the FPS background, and lowered the update frequency so it doesn't flip too fast.
#story[923]
2017-05-24 19:33:24 +12:00
Kusal Ekanayake 05cdadac79 Merge remote-tracking branch 'origin/Story34_Sparklines' into Story34_Sparklines 2017-05-24 18:57:09 +12:00
Kusal Ekanayake e6aed88188 Merge with develop.
#story[952]
2017-05-24 18:56:56 +12:00
Kusal Ekanayake d032314ddb Merge branch 'develop' into Story34_Sparklines
# Conflicts:
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/models/Yacht.java
2017-05-24 18:56:04 +12:00
Peter Galloway 6301fd2fb7 Merge branch 'issue#5_fix_pre-race_boats_on_leaderboard' into 'develop'
Issue#5 fix pre race boats on leaderboard

Updated leaderboard so it only shows competing boats and pre-race no longer shows only one boat

See merge request !36
2017-05-24 18:52:37 +12:00
Kusal Ekanayake 34c3899ec4 Removed unused css for charts 2017-05-24 18:51:52 +12:00
Kusal Ekanayake 46f5fc5172 Merge with develop.
#story[952]
2017-05-24 18:47:27 +12:00
Kusal Ekanayake 81c021b59a Merge branch 'develop' into Story34_Sparklines
# Conflicts:
#	src/main/java/seng302/App.java
#	src/main/resources/views/RaceView.fxml
2017-05-24 18:45:01 +12:00
Kusal Ekanayake 8ab57e4e61 Cleaned up code for review.
#story[952]
2017-05-24 18:43:50 +12:00
Calum 14a7305a2d Made FPS marker not draw on canvas.
Have to revert the use of the observer pattern since there is no time to change how and when data is passed.
2017-05-24 17:17:06 +12:00
Zhi You Tan 13ff179840 Merge remote-tracking branch 'origin/develop' into issue#5_fix_pre-race_boats_on_leaderboard 2017-05-24 16:18:33 +12:00
Zhi You Tan e7060d4b6f Finish screen will show when race finishes, added functionality to return to start screen when in finish screen, updated finishScreenView.fxml to have controller and also four corner anchors to fit to parent 2017-05-24 16:17:56 +12:00
Zhi You Tan 641039720e Merge remote-tracking branch 'origin/develop' into issue#8_create_finish_screen 2017-05-24 16:12:37 +12:00
Michael Rausch 76c0d34760 Merge branch '37-display_map_on_canvas' into 'develop'
37 display map on canvas

Merge request for story #928

See merge request !37
2017-05-24 15:05:39 +12:00
Michael Rausch 71637d7286 Removed dead code from canvas controller and canvas map
Tags: #story[928]
2017-05-24 15:04:12 +12:00
William Muir 1cac7cc189 Calculation of upwind downwind leg given a boat, wind and next mark now works. created GeometryUtils class
Can accurately calculate if a boat is going upwind or downward using a line function for the wind vector from the gate and the boat position from the gate.

Requires knowledge of the next mark which requires the boat to have passed a mark. This could be fixed by extracting the leg number from the race status packet and mapping these to gates in an initalisation step

tags: #story[956]
2017-05-24 14:57:22 +12:00
Haoming Yin c42942430f Fixed bug that grid panel pushes annotation panel up out of window.
#story[928]
2017-05-24 14:57:06 +12:00
Kusal Ekanayake 7abb36c362 The axis is labeled correctly and is scaled accordingly.
The sparkline should work from the start of a race now also. We now have the boat in first place listed at the top along with the axis labeling and marking matching the position (eg, 1st at top. 6th at bottom). Boats picked up on the visualiser park way through the race are able to added and drawn onto the sparkline.

#story[952]
2017-05-24 14:42:41 +12:00
Zhi You Tan c449da2916 [WIP] created finishScreenView.fxml, finishScreenViewController.java 2017-05-24 14:24:14 +12:00
Haoming Yin 3fd0c0a2dd Merge remote-tracking branch 'origin/37-display_map_on_canvas' into 37-display_map_on_canvas
# Conflicts:
#	src/main/java/seng302/App.java
2017-05-24 13:55:42 +12:00
Haoming Yin 397f7d003a Fixed the size of race canvas and race view so that canvas won't be stretched
- canvas view is set to 1280 * 960

#story[928]
2017-05-24 13:54:05 +12:00
Haoming Yin f85d3bf5fe Plugged Canvas map to canvas view controller to display map
- rebase on the latest develop status
- optimised the scaling factor the map to fit the canvas view
- a new image containing map image is displayed under race canvas

#story[928]
2017-05-24 13:52:49 +12:00
Calum d22d758757 Fix issues caused by not updating the time since last mark value frequently. BoatAnnotations now has an update() function that must be called somewhat regularly. 2017-05-24 03:23:02 +12:00
Calum acbde5aad8 Moved boat annotations into their own class. Implemented observer pattern.
Observer pattern appears to have caused issues with updating Text objects.
Made annotations look nicer. Kinda.

#refactor
2017-05-24 03:09:11 +12:00
Haoming Yin 8f93956ff1 Added methods to calculate optimal map size given a geo boundary.
- From zoom level 20 to 1, once find a size that contains the whole boundary, then the size will be used to retrieve map image from google

#story[928]
2017-05-24 00:49:03 +12:00
Haoming Yin 4fe4ac1079 Added get map size (width and height) method in canvasMap with given boundary
#story[928]
2017-05-24 00:49:03 +12:00
Haoming Yin 8a2f0a9f45 Added unit tests for Mercator projection class.
- changed its methods to static
- add some documentation for its methods

#story[928]
2017-05-24 00:49:03 +12:00
Haoming Yin 5cc865f0af Created Mercator projection to convert between Geo location and planar projection point.
- MapGeo and MapPoint encapsulate geo location and planar projection point into classes.

#story[928]
2017-05-24 00:49:03 +12:00
Haoming Yin 189ba93e64 Created a canvas map class to fetch map image from google
- also added Bound class to encapsulate map boundary.
- created TestMapView and its controller just for testing.

#story[928]
2017-05-24 00:49:03 +12:00
William Muir 89464e033e Initial work on calculating when a gate is upwind or downwind
Need to know if a gate is upwind or downwind to use the appropriate polar table

Currently calculate the angle between the next mark and the vector of the current mark to the wind, if this angle is less than 90 degrees than the next mark should be down wind

Pretty poor implementation currently, just prototype

Doesn't appear to be working as intended currently. Just a prototype for how we could implement further

tags: #story[956]
2017-05-24 00:08:55 +12:00
Zhi You Tan 6d7c36e31f Updated code to check if boat is in the race before displaying in the leaderboard
#story[923]
2017-05-23 20:00:23 +12:00
Michael Rausch ca8ea03870 Added method in mark group to draw lay lines, also added next mark to Yacht class.
#story[956]
2017-05-23 18:54:42 +12:00
Kusal Ekanayake 3f57adb9cf Merge branch 'develop' into Story34_Sparklines
# Conflicts:
#	src/main/java/seng302/App.java
#	src/main/java/seng302/controllers/CanvasController.java
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/resources/views/RaceView.fxml
2017-05-23 14:06:24 +12:00
Zhi You Tan 2686dac62e Fixed Yacht position initialise. TO-DO: check if the boat is participating before showing up on leaderboard. 2017-05-23 11:53:23 +12:00
Zhi You Tan ffd40fef6d Merge remote-tracking branch 'origin/develop' into issue#5_fix_pre-race_boats_on_leaderboard
# Conflicts:
#	src/main/java/seng302/controllers/RaceController.java
#	src/main/java/seng302/controllers/StartScreenController.java
2017-05-23 11:31:18 +12:00
William Muir e1b8e19966 Initial work on finding next marks for boats as from mark rounding messages
Marks were very difficult to extend, need large refactor

Marks now have a compound ID as well as their sourceID. They need this compound ID to be identifiable by the
mark rounding messages.

tags: #story[956]  #pair[wmu16, mra106]
2017-05-22 18:54:07 +12:00
William Muir dec742cf54 Merge remote-tracking branch 'origin/develop' into 38b_LayLines 2017-05-22 18:15:58 +12:00
Zhi You Tan 6f1b0b06c3 Fixed est time to next mark and time from last mark annotation update after commit e0750f53 in BoatGroup.java
#story[924] #story[927]
2017-05-22 16:17:52 +12:00
Michael Rausch 408d70c420 Merge branch 'issue#10_unifying_marks' into 'develop'
Issue#10 unifying marks

Marks work properly, reading boat position packets and moving.

First and last marks are colored green and red respectively.

See merge request !35
2017-05-22 16:07:50 +12:00
Alistair McIntyre e51c966969 Made sure only boats in participant list are replaced. Also removed mark coloring as it wasn't as accurate as was necessary.
#story[923]
2017-05-22 16:05:24 +12:00
William Muir 08eacacfd4 Polar table is now parsed form the stored file in resources on startup in main
story[956]
2017-05-22 15:45:16 +12:00
William Muir 87150b3c72 Merge remote-tracking branch 'origin/develop' into 38b_LayLines 2017-05-22 15:40:11 +12:00
William Muir e385ac5c09 Initial work on static parser class for polar files
story[956]
2017-05-22 15:39:53 +12:00
Alistair McIntyre c30629542b Merged develop into issue10 branch. Fixed merge issues and completed manual testing before merge request.
#story[923]
2017-05-22 15:27:23 +12:00
Alistair McIntyre 3f9fa24c69 Merge branch 'develop' into issue#10_unifying_marks
# Conflicts:
#	src/main/java/seng302/App.java
#	src/main/java/seng302/controllers/CanvasController.java
2017-05-22 15:13:48 +12:00
Alistair McIntyre 78573fa837 Made start/finish lines a different color.
#story[923]
2017-05-22 15:10:05 +12:00
Michael Rausch d4837cacda Merge branch 'Issue#4_boat_movement' into 'develop'
Issue#4 boat movement

This merge will break several things:
1 boat wakes and trails will be the most effective and will have to be manually re-implemented in the re-engineered boatgroup
2 boat annotations in Raceviewcontroller have been effected
3 boat selection (on click) in Raceviewcontroller has been effected

See merge request !34
2017-05-22 15:03:11 +12:00
Peter Galloway 0367805f0f fixed bug from merge where est time to next mark was null on some boats #story[923] 2017-05-22 15:01:04 +12:00
Peter Galloway e0750f5341 Merge branch 'develop' into Issue#4_boat_movement
# Conflicts:
#	src/main/java/seng302/controllers/CanvasController.java
#	src/main/java/seng302/models/BoatGroup.java
#	src/main/java/seng302/models/Race.java
2017-05-22 14:56:08 +12:00
Calum 80528c9c42 fixed bug in canvas controller
#bug
2017-05-22 14:30:31 +12:00
alistairjmcintyre be633c0e60 Marks display correctly on the canvas, no double ups or anything like that left.
#[issue10]
2017-05-22 13:22:55 +12:00
cir27 97f1ccb6c1 Fixed a bug where an error was caused when attempting to flip the race around the prime meridian.
All x values are now flipped horizontally if the difference between the smallest lon, e.g. -179.5 and the largest e.g. 179.5 is greater than 180.

#story[923] #bug
2017-05-22 05:41:57 +12:00
cir27 68a243725b Fixed a bug where an error was caused when attempting to flip the race around the prime meridian.
All x values are now flipped horizontally if the difference between the smallest lon, e.g. -179.5 and the largest e.g. 179.5 is greater than 180.

#story[923] #bug
2017-05-22 05:39:36 +12:00
Kusal Ekanayake 8f81060a18 Managed to make the sparklines coordinate with their colour.
However the symbols had to be removed. The sparkline can also be intialised at any time now but this might need to be tested further to ensure that it works.

#story[952]
2017-05-21 19:49:11 +12:00
Kusal Ekanayake 07c76f12e1 Merge branch 'develop' into Story34_Sparklines 2017-05-21 17:37:41 +12:00
cir27 c6ab96a86f Merge branch 'develop' of https://eng-git.canterbury.ac.nz/seng302-2017/team-13 into develop 2017-05-21 16:05:37 +12:00
Kusal Ekanayake 059c0de1fa Quickfix for null pointer in boatObject
#pair[ptg19,kre39]
2017-05-21 15:58:17 +12:00
Kusal Ekanayake 8e147bd1bd Making the boat sparkline initialise after the race screen is shown.
#story[952]
2017-05-21 14:56:05 +12:00
Michael Rausch 4d3cfe71f7 Merge branch 'develop' into Issue#4_boat_movement
Conflicts:
	src/main/java/seng302/models/BoatGroup.java
	src/main/java/seng302/models/Wake.java
2017-05-19 21:40:16 +12:00
Michael Rausch 5adb7c3762 Fixed failing ColorsTest
#story[923]
2017-05-19 20:57:40 +12:00
Michael Rausch 937b309b07 Added boat trails to the boat group, fixed annotations
- Set colours for the annotations
- Added boat trails back into the boat group
- Re-added time until next mark and time since last mark rounding

#story[923]
2017-05-19 20:55:02 +12:00
Kusal Ekanayake 48d58ea660 Sparklines now update when a boat passes a mark.
There are issue of initialising the sparkline before the race starts and when the race is being viewed when it starts. It works great though when you join a race halfway.
#story[952]
2017-05-19 18:08:26 +12:00
Michael Rausch aaf2e6a3f0 Added boat wakes back to the visualiser
#story[923]
2017-05-19 16:57:29 +12:00
Kusal Ekanayake 422dcd4501 Sparklines now update with boat position change.
Need to make it work for any amount of boats in the race and to work when the race first starts as it currently only shows one boat.
#story[952]
2017-05-19 15:15:05 +12:00
Kusal Ekanayake 951a726309 The line chart now has series which display on it.
Need to get the right data to display on the linechart when it updates.

#story[952]
2017-05-18 20:56:42 +12:00
Alistair McIntyre b692ddcbe6 Marks and gates correctly displaying from XML messages and mark(boat) position packets. Will need a little polishing but marks and gates are moving around.
#story[923] #pair[ajm412, ptg19]
2017-05-18 18:39:23 +12:00
Alistair McIntyre 5d6b356602 all marks displaying except currently gate marks are displaying as a single dot
#story[923] #pair[ajm412, ptg19]
2017-05-18 18:02:25 +12:00
Alistair McIntyre 08057edb28 Merge branch 'Issue#4_boat_movement' into issue#10_unifying_marks
# Conflicts:
#	src/main/java/seng302/controllers/CanvasController.java
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/models/BoatGroup.java
#	src/main/java/seng302/models/mark/MarkGroup.java
2017-05-18 17:04:00 +12:00
Kusal Ekanayake 390aabc78f Adding race yacht series to the sparkline.
Next step is to make the series of the boats update when new position information is received.

#story[952]
2017-05-18 14:44:10 +12:00
William Muir a2123df0c5 Fixed Boat selection and lost annotations (Estimated and Leg Time)
Trails and Wakes, however,  are still not re implemented yet.

story[923]
2017-05-18 14:41:08 +12:00
Kusal Ekanayake b87008f590 Merge branch 'develop' into Story34_Sparklines 2017-05-18 13:22:51 +12:00
Kusal Ekanayake ecc0e722b5 Started sparkline prototype
#story[952]
2017-05-18 13:19:59 +12:00
Zhi You Tan 6f9a8e5581 Merge remote-tracking branch 'origin/develop' into issue#5_fix_pre-race_boats_on_leaderboard 2017-05-18 12:20:02 +12:00
Zhi You Tan b17bba3629 Fixed leaderboard on start screen and race view to show all boats correctly during pre-race.
#story[923]
2017-05-18 12:18:51 +12:00
Michael Rausch cf4d7e03f5 Merge branch 'issue#1_wakes_3.0' into 'develop'
Issue#1 wakes 3.0

Changed methods in the wake class to fix issues caused by unexpected velocities making the wakes go crazy.
Improved performance.
Made wakes look different.

See merge request !33
2017-05-18 11:45:28 +12:00
Michael Rausch 73eeeb0ef9 Removed extra print stmt and changed default server back to official 2017-05-18 11:45:06 +12:00
Peter Galloway 0f79353936 fixes from merge #story[923] 2017-05-17 20:15:47 +12:00
Peter Galloway 38b44fa92b Merge branch 'develop' into Issue#4_boat_movement
# Conflicts:
#	src/main/java/seng302/controllers/Controller.java
#	src/main/java/seng302/controllers/RaceController.java
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/models/BoatGroup.java
#	src/main/java/seng302/models/mark/MarkGroup.java
#	src/main/java/seng302/models/stream/StreamParser.java
#	src/test/java/seng302/models/stream/StreamReceiverTest.java
2017-05-17 20:05:40 +12:00
alistairjmcintyre 3fd13ddc0a Adjusted XMLParser to use model Mark objects rather than the simple datatype that existed in the XMLParser previously. Began attempting to implement them into the canvas controller but have issues
#[issue10]
2017-05-17 19:46:05 +12:00
Peter Galloway 2e375978bd cleaned up code for merging back to develop #story[923] 2017-05-17 19:18:31 +12:00
Calum 45db731a60 Fixed a merge issue.
#story[923] #bug #refactor
2017-05-17 18:06:36 +12:00
Peter Galloway 95e353c14e fixed initialization bug #story[923] 2017-05-17 17:55:21 +12:00
Calum 8a3a41294a Fixed error causing build failure.
#story[923] #bug #refactor
2017-05-17 17:41:45 +12:00
Calum f41858e2c7 Tidied variable names. 2017-05-17 17:35:59 +12:00
Peter Galloway aaa3dc93f1 boat movement working reliably now, still need to fix one bug at initialization #story[923] 2017-05-17 17:34:47 +12:00
Calum e5eab0a6c8 Merge remote-tracking branch 'origin/develop' into issue#1_wakes_3.0
# Conflicts:
#	src/main/java/seng302/App.java
#	src/main/java/seng302/models/BoatGroup.java
2017-05-17 17:27:34 +12:00
Calum 7c39368126 Removed unnecessary constant 2017-05-17 17:23:10 +12:00
Calum ade926e2f2 StreamParser class now only shares thread safe classes.
#story[923] #bug
2017-05-17 16:44:05 +12:00
Calum c63c8e4d73 Stripped out excess code in Wake class. 2017-05-17 16:33:45 +12:00
Michael Rausch 3c418b2aa4 Changed boat location message to milliseconds 2017-05-17 14:57:31 +12:00
Michael Rausch d34a158c34 Merge branch '38a_Select_Boats' into 'develop'
38a select boats

Selection of boats now possible by selecting a boat from a drop down menu to focus that boat singularly or clicking a boat on the canvas to toggle that boats annotations.

Changed the annotations slider to remove the low annotation option. Only now includes None, Important and All. I think this makes much more sense.

Refactor of the race view controller class to combine all timelines for polling the Stream Parser to update race view data such as tables into one timeline for polling. In the future this should be improved further from polling to an observer model.

See merge request !31
2017-05-17 14:38:23 +12:00
Michael Rausch 6e3d037021 Merge branch 'develop' into 38a_Select_Boats 2017-05-17 14:34:10 +12:00
Zhi You Tan 7d160eaf54 Removed unused imports from controller.java 2017-05-17 13:16:00 +12:00
Peter Galloway 4fc99edbd6 Merge branch 'issue#14_split_start_screen_view&controller' into 'develop'
Split Start Screen out from Main View view and controller.

The start screen has its own view and controller now. Returned main view to its original function.

(This solves issue#14)

See merge request !32
2017-05-17 13:07:48 +12:00
Zhi You Tan 4da8c1645e Split Start Screen out from Main View view and controller.
#story[923]
2017-05-17 12:56:13 +12:00
William Muir 1c01aab1e7 Merge on to develop
story[955]
2017-05-17 10:34:00 +12:00
William Muir 6a85b0800f Merge on to develop
story[955]
2017-05-17 10:06:50 +12:00
William Muir 1acb0fbac4 Merge on to develop
story[955]
2017-05-17 10:04:28 +12:00
William Muir 2b294702a9 Merge remote-tracking branch 'origin/develop' into 38a_Select_Boats
# Conflicts:
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/models/BoatGroup.java
2017-05-17 09:53:18 +12:00
William Muir afd97d6e05 Reformating of code to google style.
#story[955]
2017-05-17 00:34:36 +12:00
William Muir 2d5a7a8a49 Large tidying of RaceViewController class. Fixing updating for combo boxes
#story[955]
2017-05-16 15:06:01 +12:00
Peter Galloway 9e3036e134 Stripped down the boatgroup to the basic movement components and then tried to adjust how position updates are being dealt with to make everything more logically understandable. I made some progress in terms of understanding but the position update is still not as reliable as I would like. I will be explaining to other team members how this part of the code is working so the time I have spent is not completely wasted #story[923] 2017-05-15 23:17:36 +12:00
Haoming Yin 8dec458ba9 Added methods to calculate optimal map size given a geo boundary.
- From zoom level 20 to 1, once find a size that contains the whole boundary, then the size will be used to retrieve map image from google

#story[928]
2017-05-15 19:57:23 +12:00
Michael Rausch da07d885da Merge branch 'Story47CourseLimits' into 'develop'
Story47 course limits

The idea was to have the course canvas update whenever we receive a new xml packet for the race data. Specifically for a few seconds after the race where the course boundaries change, going from a boundary which contains the start line to cutting off the start line as soon as the boats start.

See merge request !30
2017-05-15 18:43:49 +12:00
Michael Rausch a9de005e1a Removed a extraneous print stmt 2017-05-15 18:41:44 +12:00
Michael Rausch e03e121da4 Merge branch 'develop' into Story47CourseLimits
Conflicts:
	src/main/java/seng302/App.java
2017-05-15 18:37:31 +12:00
Peter Galloway fc3ca70e5d Merge branch 'issue#10_unifying_marks' into Issue#4_boat_movement
# Conflicts:
#	src/main/java/seng302/controllers/CanvasController.java
#	src/main/java/seng302/controllers/Controller.java
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/models/stream/StreamPacket.java
#	src/main/java/seng302/models/stream/StreamParser.java
2017-05-15 18:09:47 +12:00
Kusal Ekanayake 9c7144c918 Removed dud print statement
#story[889]
2017-05-15 17:55:06 +12:00
Peter Galloway ed8d70c3b3 commented out buffered code and cleaned up some other areas to try and make the boatgroup clear enough to modify #story[923] 2017-05-15 17:53:11 +12:00
Kusal Ekanayake fa501460cb Cleaned up code for merge.
#story[889]
2017-05-15 17:21:56 +12:00
Kusal Ekanayake 110143ae6e Added a dynamically updating course limits file to the model.
This will make manual testing of the course limits easier. When the race starts, the limits encapsulate the start line but soon after the race starts the course limits close off around the start line (this is for the model). This means the model acts in a very similar way to the actual live stream.

#story[889]
2017-05-15 17:09:12 +12:00
Haoming Yin eda3d76077 Added get map size (width and height) method in canvasMap with given boundary
#story[928]
2017-05-15 17:06:28 +12:00
William Muir 51f090324a CSS on annotation selection and minor bug fixes. Changed annotation slider to only have None, important and All
#story[955]
2017-05-15 16:44:28 +12:00
Michael Rausch 335540ff4a Merge branch 'story36_elapsed_time' into 'develop'
Story36 elapsed time

Implemented time elapsed since last mark on annotation

See merge request !29
2017-05-15 16:33:21 +12:00
Zhi You Tan 5fa47ff65b Implemented elapsed time since last mark on annotation.
#story[927]
2017-05-15 16:31:04 +12:00
Zhi You Tan 3a1c1a5e43 Merge remote-tracking branch 'origin/develop' into story36_elapsed_time
# Conflicts:
#	src/main/java/seng302/models/BoatGroup.java
2017-05-15 15:54:44 +12:00
Michael Rausch 4c7f530458 Merge branch 'story33_add_estimated_time_annotation' into 'develop'
Story33 add estimated time annotation

Added estimated time to next mark to annotation

See merge request !28
2017-05-15 15:50:46 +12:00
Zhi You Tan 2e914a7704 Fixed estimate time to next mark to match acceptance criteria which is countdown in minutes and seconds.
#story[924]
2017-05-15 15:49:21 +12:00
Calum 8fbb9d6d4e Added/improved documentation
#chore
2017-05-15 15:36:18 +12:00
Calum 23bc643c91 Removed some unused functions and imports caused by code refactor.
#chore
2017-05-15 15:20:21 +12:00
Calum c4fe116267 MarkGroups refactored to be independent of BoatGroups as their functionality has diverged.
#issue[10] #refactor
2017-05-15 15:17:54 +12:00
Kusal Ekanayake 081d7e3dcb Fixed the parsing of bytes being off by one byte in the stream parser fr the race status packet.
#story[924] #pair[kre39,zyt10]
2017-05-15 14:52:57 +12:00
William Muir 764ae37ce4 Gave the boatgroups a selection attribute, allowing them to be highlighted upon clicking
Boats can be clicked on canvas or from selection drop down on the side

#story[955]
2017-05-15 14:09:09 +12:00
Zhi You Tan e62a609b6b Prepared StreamParser.java, BoatGroup.java, Yacht.java for leg timer annotation (story 36)
#story[927]
2017-05-15 13:47:30 +12:00
Haoming Yin 4b1a4aae87 Added unit tests for Mercator projection class.
- changed its methods to static
- add some documentation for its methods

#story[928]
2017-05-15 13:23:04 +12:00
Peter Galloway ccda5f2a2e changing branch name #story[923] 2017-05-15 12:40:11 +12:00
Haoming Yin 3fd8b1b855 Created Mercator projection to convert between Geo location and planar projection point.
- MapGeo and MapPoint encapsulate geo location and planar projection point into classes.

#story[928]
2017-05-15 12:24:36 +12:00
Kusal Ekanayake 94d1982670 Shifted the canvas to the back of the window when updated.
This is to preserve the way the boats are still shown on the front when the course i reloaded.
As the main live stream has been down, this still needs to be tested.
2017-05-15 10:46:28 +12:00
Zhi You Tan 39efafc75f Fixed Annotation.java, ImportantAnnotationController.java, RaceViewController.java to include estimate time to next mark after merge
#story[924]
2017-05-15 10:36:20 +12:00
Kusal Ekanayake afe0c9f1a6 Merge branch 'develop' into Story47CourseLimits
# Conflicts:
#	src/main/resources/views/RaceView.fxml
2017-05-15 10:33:22 +12:00
Zhi You Tan 04b105d74b Merge branch 'develop' into story33_add_estimated_time_annotation
# Conflicts:
#	src/main/java/seng302/controllers/RaceViewController.java
2017-05-15 10:19:58 +12:00
Zhi You Tan 1ab6351d48 Fixed estimated time to next mark (annotation) to update correctly
#story[924]
2017-05-15 10:11:19 +12:00
Michael Rausch 9c348df5a5 Merge branch 'merge_request_test' into 'master'
Added .codeclimate.yml for static code analysis



See merge request !26
2017-05-15 01:28:57 +12:00
Peter Galloway 256ec046fc adjusted givePointsXY to try and use the marks from the XML parser, stopped partway as the marks from the XML parser and the standard marks need to be merged before this can be fixed properly #story[923] 2017-05-14 22:12:16 +12:00
Peter Galloway 85d4d63287 Merge branch 'story35_create_annotation_view' into 'develop'
Story35 create annotation view

## Addresses issue #9 and story 35

# Change Log
* Styled application using CSS (Issue #9)
* User can select to only show important annotations using the annotation slider
* Added view to select important annotations
* Important annotations are updated in real-time

# Testing
* Unit tests in test/java/seng302/visualizer/annotations/*
* Controllers have been tested manually

# Acceptance Criteria

* There is a mechanism to chose the ‘important’ annotations while viewing a race.

* Any number of annotations (from zero to the number of annotations) can be chosen.

* If the partial annotations toggle is active, choosing an annotation makes that annotation immediately visible for all boats, and deselecting an annotation should likewise remove it from all boats.

* When the partial annotations toggle becomes active, all annotations chosen as ‘important’, and only those annotations, should be shown.

See merge request !27
2017-05-14 22:00:02 +12:00
Michael Rausch 213d36ed56 Merge remote-tracking branch 'origin/develop' into story35_create_annotation_view 2017-05-14 21:05:48 +12:00
Michael Rausch ff6bfc9516 Important annotations are displayed correctly when updated.
- Important annotations can be selected, the live view updates in real time
- Change local time text colour to white
- Split the important annotations state into a separate class

Tags: #story[926]
2017-05-14 20:27:25 +12:00
Peter Galloway ae5678482b changed package "parsers" to be called "stream" #story[923] 2017-05-14 20:26:49 +12:00
Peter Galloway 63514cfafb deleted duplicated packet type class #story[923] 2017-05-14 20:20:14 +12:00
Peter Galloway 03ca60f2e1 deleted a whole bunch of legacy code, primarily the old controllers and old parsers #story[923] 2017-05-14 20:12:35 +12:00
Calum 89ef6e5277 Wake calculation now changed to be based off of the separation between wakes.
This allows wakes to auto correct their position better and stops the system reliance on "realistic data".
Wakes have several options for behaviour until the ideal settings are decided upon.
Note that MarkGroup position updating is currently disabled.

#implement #refactor #issue[1] #story[923]
2017-05-14 17:24:15 +12:00
Zhi You Tan 47880d09bc Implemented estimated time to next mark. Added checkbox for estimated time to next mark. To be fix: change from long to human readable time, update time every second
#story[923]
2017-05-12 20:04:09 +12:00
Zhi You Tan 5472765b95 Removed unused methods in RaceViewController.java
#story[923]
2017-05-12 18:24:28 +12:00
Zhi You Tan 14d975dce4 Fixed a bug where clicking "watch race" before race starts will cause boats to travel in unexpected ways.
#story[923]
2017-05-12 17:41:47 +12:00
Michael Rausch a23bdd0c53 Added functionality to select important annotations and ui changes
- Added CSS for the race view & welcome screen
- Added view to select important annotations
- Annotations are enabled/disabled when the user changes the important annotations

Tags: #story[926]
2017-05-11 21:44:56 +12:00
cir27 b0d8c3db0a Merge remote-tracking branch 'origin/develop' into develop 2017-05-11 16:40:50 +12:00
Kusal Ekanayake 4a75c062ce Making the course limits change when a new xml packet is received..
#story[889]
2017-05-11 15:49:49 +12:00
Michael Rausch fe90a3bf13 Merge branch 'master' into story35_create_annotation_view 2017-05-11 13:43:02 +12:00
Michael Rausch 711f6f4c45 Merge branch 'master' into develop 2017-05-11 13:29:51 +12:00
Haoming Yin 8fa7829a3c Created a canvas map class to fetch map image from google
- also added Bound class to encapsulate map boundary.
- created TestMapView and its controller just for testing.

#story[928]
2017-05-10 20:52:46 +12:00
Peter Galloway 6d7697a0eb commented out the problematic buffering functionality #story[923] 2017-05-10 13:58:10 +12:00
Michael Rausch cdd80af27b Added .codeclimate.yml for static code analysis 2017-05-10 11:56:18 +12:00
Zhi You Tan 2cb09b81f8 Updated .mailmap for everyone 2017-05-04 16:53:16 +12:00
Zhi You Tan ac3f3bfd55 Added a check if race started, then start screen switches to race view automatically.
#story[572]
2017-05-04 14:32:06 +12:00
cir27 3a72409fb8 Merge remote-tracking branch 'origin/develop' into develop 2017-05-02 21:50:30 +12:00
cir27 fd092bb7e1 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	src/main/java/seng302/App.java
2017-05-02 16:41:49 +12:00
cir27 9c60521d00 Merge branch 'develop' of C:\Users\CJIRWIN\Documents\team-13 with conflicts. 2017-05-02 16:41:40 +12:00
136 changed files with 7520 additions and 4660 deletions
+17
View File
@@ -0,0 +1,17 @@
engines:
pmd:
enabled: true
channel: "beta"
fixme:
enabled: true
config:
strings:
- FIXME
- TODO
- BUG
- FIX
ratings:
paths:
- "**.java"
+5 -3
View File
@@ -16,10 +16,12 @@
# https://www.kernel.org/pub/software/scm/git/docs/git-shortlog.html
# http://stacktoheap.com/blog/2013/01/06/using-mailmap-to-fix-authors-list-in-git/
Michael Rausch <mra106@uclive.ac.nz> <me@michaelrausch.nz> <michael@michaelrausch.net>
Michael Rausch <mra106@uclive.ac.nz> <me@michaelrausch.nz>
Michael Rausch <mra106@uclive.ac.nz> <michael@michaelrausch.net>
Kusal Ekanayake <kre39@uclive.ac.nz> kre39 <kre39@uclive.ac.nz>
Haoming Yin <hyi25@uclive.ac.nz> <haoming.y@icloud.com>
Peter Galloway <ptg19@uclive.ac.nz> Peter <ptg19@uclive.ac.nz>
Zhi You Tan <zyt10@uclive.ac.nz> zyt10 <zyt10@uclive.ac.nz>
Zhi You Tan <zyt10@uclive.ac.nz> Ryan Tan <ryan_zhiyou@hotmail.com>
Alistair McIntyre <ajm412@uclive.ac.nz> alistairjmcintyre <alistairjmcintyre@gmail.com>
Calum <cir27@uclive.ac.nz> cir27 <cir27@uclive.ac.nz>
+7
View File
@@ -36,6 +36,13 @@
<version>2.7.13</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.26-incubating</version>
</dependency>
</dependencies>
<build>
+15 -53
View File
@@ -4,75 +4,37 @@ import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import seng302.models.parsers.StreamParser;
import seng302.models.parsers.StreamReceiver;
import seng302.server.ServerThread;
import seng302.client.ClientPacketParser;
import seng302.client.ClientState;
import seng302.models.PolarTable;
public class App extends Application {
public class App extends Application
{
@Override
public void start(Stage primaryStage) throws Exception {
PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv"));
Parent root = FXMLLoader.load(getClass().getResource("/views/MainView.fxml"));
primaryStage.setTitle("RaceVision");
primaryStage.setScene(new Scene(root));
primaryStage.setMaximized(true);
primaryStage.setScene(new Scene(root, 1530, 960));
primaryStage.setMaxWidth(1530);
primaryStage.setMaxHeight(960);
primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("/PP.png")));
// primaryStage.setMaximized(true);
primaryStage.show();
primaryStage.setOnCloseRequest(e -> {
StreamParser.appClose();
StreamReceiver.noMoreBytes();
System.out.println("[CLIENT] Exiting program");
ClientPacketParser.appClose();
System.exit(0);
});
ClientState.primaryStage = primaryStage;
}
public static void main(String[] args) {
StreamReceiver sr = null;
new ServerThread("Racevision Test Server");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (args.length == 1 && args[0].equals("-standalone")){
return;
}
if (args.length == 3 && args[0].equals("-server")){
sr = new StreamReceiver(args[1], Integer.valueOf(args[2]), "RaceStream");
} else if(args.length == 2 && args[0].equals("-server")){
switch (args[1]) {
case "internal":
sr = new StreamReceiver("localhost", 4949, "RaceStream");
break;
case "staffserver":
sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941, "RaceStream");
break;
case "official":
sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream");
break;
}
}
//Change the StreamReceiver in this else block to change the default data source.
else{
sr = new StreamReceiver("localhost", 4949, "RaceStream");
}
sr.start();
StreamParser streamParser = new StreamParser("StreamParser");
streamParser.start();
launch(args);
}
}
@@ -0,0 +1,638 @@
package seng302.client;
import java.io.IOException;
import java.io.StringReader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.PriorityBlockingQueue;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import seng302.models.Yacht;
import seng302.models.mark.Mark;
import seng302.models.stream.XMLParser;
import seng302.models.stream.packets.BoatPositionPacket;
import seng302.models.stream.packets.StreamPacket;
/**
* The purpose of this class is to take in the stream of divided packets so they can be read
* and parsed in by turning the byte arrays into useful data. There are two public static hashmaps
* that are threadsafe so the visualiser can always access the latest speed and position available
* Created by kre39 on 23/04/17.
*/
public class ClientPacketParser {
public static ConcurrentHashMap<Long, PriorityBlockingQueue<BoatPositionPacket>> markLocations = new ConcurrentHashMap<>();
public static ConcurrentHashMap<Long, PriorityBlockingQueue<BoatPositionPacket>> boatLocations = new ConcurrentHashMap<>();
private static boolean newRaceXmlReceived = false;
private static boolean raceStarted = false;
private static XMLParser xmlObject = new XMLParser();
private static boolean raceFinished = false;
private static boolean streamStatus = false;
private static long timeSinceStart = -1;
private static Map<Integer, Yacht> boats = new ConcurrentHashMap<>();
private static Map<Integer, Yacht> boatsPos = new ConcurrentSkipListMap<>();
private static double windDirection = 0;
private static Double windSpeed = 0d;
private static Long currentTimeLong;
private static String currentTimeString;
private static boolean appRunning;
private static Map<Integer, Yacht> clientStateBoats = new ConcurrentHashMap<>();
//CONVERSION CONSTANTS
public static final Double MS_TO_KNOTS = 1.94384;
/**
* Used to initialise the thread name and stream parser object so a thread can be executed
*/
public ClientPacketParser() {
}
/**
* Looks at the type of the packet then sends it to the appropriate parser to extract the
* specific data associated with that packet type
*
* @param packet the packet to be looked at and processed
*/
public static void parsePacket(StreamPacket packet) {
try {
switch (packet.getType()) {
case HEARTBEAT:
extractHeartBeat(packet);
break;
case RACE_STATUS:
extractRaceStatus(packet);
break;
case DISPLAY_TEXT_MESSAGE:
extractDisplayMessage(packet);
break;
case XML_MESSAGE:
extractXmlMessage(packet);
break;
case RACE_START_STATUS:
extractRaceStartStatus(packet);
break;
case YACHT_EVENT_CODE:
extractYachtEventCode(packet);
break;
case YACHT_ACTION_CODE:
extractYachtActionCode(packet);
break;
case CHATTER_TEXT:
extractChatterText(packet);
break;
case BOAT_LOCATION:
extractBoatLocation(packet);
break;
case MARK_ROUNDING:
extractMarkRounding(packet);
break;
case COURSE_WIND:
extractCourseWind(packet);
break;
case AVG_WIND:
extractAvgWind(packet);
break;
}
} catch (NullPointerException e) {
System.out.println("Error parsing packet");
}
}
/**
* Extracts the seq num used in the heartbeat packet
*
* @param packet Packet parsed in to use the payload
*/
private static void extractHeartBeat(StreamPacket packet) {
long heartbeat = bytesToLong(packet.getPayload());
}
private static String getTimeZoneString() {
Integer offset = xmlObject.getRegattaXML().getUtcOffset();
StringBuilder utcOffset = new StringBuilder();
utcOffset.append("GMT");
if (offset > 0) {
utcOffset.append("+");
utcOffset.append(offset);
} else if (offset < 0) {
utcOffset.append("-");
utcOffset.append(offset);
}
return utcOffset.toString();
}
/**
* Extracts the useful race status data from race status type packets. This method will also
* print to the console the current state of the race (if it has started/finished or is about to
* start), along side this it'll also display the amount of time since the race has started or
* time till it starts
*
* @param packet Packet parsed in to use the payload
*/
private static void extractRaceStatus(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long currentTime = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
long raceId = bytesToLong(Arrays.copyOfRange(payload, 7, 11));
int raceStatus = payload[11];
long expectedStartTime = bytesToLong(Arrays.copyOfRange(payload,12,18));
long windDir = bytesToLong(Arrays.copyOfRange(payload,18,20));
long rawWindSpeed = bytesToLong(Arrays.copyOfRange(payload,20,22));
currentTimeLong = currentTime;
DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
if (xmlObject.getRegattaXML() != null) {
format.setTimeZone(TimeZone.getTimeZone(getTimeZoneString()));
currentTimeString = format.format((new Date(currentTime)).getTime());
}
long timeTillStart =
((new Date(expectedStartTime)).getTime() - (new Date(currentTime)).getTime()) / 1000;
if (timeTillStart > 0) {
timeSinceStart = timeTillStart;
} else {
if (raceStatus == 4 || raceStatus == 8) {
raceFinished = true;
raceStarted = false;
ClientState.setRaceStarted(false);
} else if (!raceStarted) {
raceStarted = true;
ClientState.setRaceStarted(true);
raceFinished = false;
}
timeSinceStart = timeTillStart;
}
double windDirFactor = 0x4000 / 90; //0x4000 is 90 degrees, 0x8000 is 180 degrees, etc...
windDirection = windDir / windDirFactor;
windSpeed = rawWindSpeed / 1000 * MS_TO_KNOTS;
int noBoats = payload[22];
int raceType = payload[23];
for (int i = 0; i < noBoats; i++) {
long boatStatusSourceID = bytesToLong(
Arrays.copyOfRange(payload, 24 + (i * 20), 28 + (i * 20)));
int boatStatus = (int) payload[28 + (i * 20)];
int boatLegNumber = (int) payload[29 + (i * 20)];
int boatPenaltyAwarded = (int) payload[30 + (i * 20)];
int boatPenaltyServed = (int) payload[31 + (i * 20)];
long estTimeAtNextMark = bytesToLong(
Arrays.copyOfRange(payload, 32 + (i * 20), 38 + (i * 20)));
long estTimeAtFinish = bytesToLong(
Arrays.copyOfRange(payload, 38 + (i * 20), 44 + (i * 20)));
Yacht boat = boats.get((int) boatStatusSourceID);
boat.setBoatStatus((boatStatus));
setBoatLegPosition(boat, boatLegNumber);
boat.setPenaltiesAwarded(boatPenaltyAwarded);
boat.setPenaltiesServed(boatPenaltyServed);
boat.setEstimateTimeAtNextMark(estTimeAtNextMark);
boat.setEstimateTimeAtFinish(estTimeAtFinish);
// Update Client State boats when receive race status packet.
// Potentially could replace boats in ClientPacketParser.
Yacht clientBoat = ClientState.getBoats().get((int) boatStatusSourceID);
clientBoat.setBoatStatus((boatStatus));
setBoatLegPosition(clientBoat, boatLegNumber);
clientBoat.setPenaltiesAwarded(boatPenaltyAwarded);
clientBoat.setPenaltiesServed(boatPenaltyServed);
clientBoat.setEstimateTimeAtNextMark(estTimeAtNextMark);
clientBoat.setEstimateTimeAtFinish(estTimeAtFinish);
}
// 3 is race started.
// ClientState race started flag will be set to true if race started, else set false.
if (raceStatus == 3) {
ClientState.setRaceStarted(true);
} else {
ClientState.setRaceStarted(false);
}
}
private static void setBoatLegPosition(Yacht updatingBoat, Integer leg){
Integer placing = 1;
if (/* TODO implement when we are getting this data /TODO leg != updatingBoat.getLegNumber() && */(raceStarted || raceFinished)) {
for (Yacht boat : boats.values()) {
placing = boat.getSourceId();
/* See above to-do
if (boat.getLegNumber() != null && leg <= boat.getLegNumber()){
placing += 1;
}*/
}
updatingBoat.setPosition(placing.toString());
updatingBoat.setLegNumber(leg);
boatsPos.putIfAbsent(placing, updatingBoat);
boatsPos.replace(placing, updatingBoat);
} else if(updatingBoat.getLegNumber() == null){
updatingBoat.setPosition("-");
updatingBoat.setLegNumber(leg);
}
}
/**
* Used to extract the messages passed through with the display message packet
*
* @param packet Packet parsed in to use the payload
*/
private static void extractDisplayMessage(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
int numOfLines = payload[3];
int totalLen = 0;
for (int i = 0; i < numOfLines; i++) {
int lineNum = payload[4 + totalLen];
int textLength = payload[5 + totalLen];
byte[] messageTextBytes = Arrays
.copyOfRange(payload, 6 + totalLen, 6 + textLength + totalLen);
String messageText = new String(messageTextBytes);
totalLen += 2 + textLength;
}
}
/**
* Used to read in the xml data. Will call the specific methods to create the course and boats
*
* @param packet Packet parsed in to use the payload
*/
private static void extractXmlMessage(StreamPacket packet) {
xmlObject = new XMLParser();
byte[] payload = packet.getPayload();
int messageType = payload[9];
long messageLength = bytesToLong(Arrays.copyOfRange(payload, 12, 14));
String xmlMessage = new String(
(Arrays.copyOfRange(payload, 14, (int) (14 + messageLength)))).trim();
//Create XML document Object
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = null;
Document doc = null;
try {
db = dbf.newDocumentBuilder();
doc = db.parse(new InputSource(new StringReader(xmlMessage)));
} catch (ParserConfigurationException | IOException | SAXException e) {
System.out.println("[ClientPacketParser] ParserConfigurationException | IOException | SAXException");
}
xmlObject.constructXML(doc, messageType);
if (messageType == 7) { //7 is the boat XML
boats = xmlObject.getBoatXML().getCompetingBoats();
// Set/Update the ClientState boats after receiving new boat xml.
// Flag boatsUpdated in ClientState to true.
ClientState.setBoats(xmlObject.getBoatXML().getCompetingBoats());
ClientState.setBoatsUpdated(true);
}
if (messageType == 6) { //6 is race info xml
newRaceXmlReceived = true;
}
}
/**
* Extracts the race start status from the packet, currently is unused within the app but
* is here for potential future use
*
* @param packet Packet parsed in to use the payload
*/
private static void extractRaceStartStatus(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
long raceStartTime = bytesToLong(Arrays.copyOfRange(payload, 9, 15));
long raceId = bytesToLong(Arrays.copyOfRange(payload, 15, 19));
int notificationType = payload[19];
}
/**
* When a yacht event occurs this will parse the byte array to retrieve the necessary info,
* currently unused
*
* @param packet Packet parsed in to use the payload
*/
private static void extractYachtEventCode(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
long raceId = bytesToLong(Arrays.copyOfRange(payload, 9, 13));
long subjectId = bytesToLong(Arrays.copyOfRange(payload, 13, 17));
long incidentId = bytesToLong(Arrays.copyOfRange(payload, 17, 21));
int eventId = payload[21];
}
/**
* When a yacht action occurs this will parse the parse the byte array to retrieve the necessary
* info, currently unused
*
* @param packet Packet parsed in to use the payload
*/
private static void extractYachtActionCode(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
long subjectId = bytesToLong(Arrays.copyOfRange(payload, 9, 13));
long incidentId = bytesToLong(Arrays.copyOfRange(payload, 13, 17));
int eventId = payload[17];
}
/**
* Strips the message from the chatter text type packets, currently the message is unused
*
* @param packet Packet parsed in to use the payload
*/
private static void extractChatterText(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
int messageType = payload[1];
int length = payload[2];
String message = new String(Arrays.copyOfRange(payload, 3, 3 + length));
System.out.println(message);
}
/**
* Used to breakdown the boatlocation packets so the boat coordinates, id and groundspeed are
* all used All the other extra data is still being read and translated however is unused.
*
* @param packet Packet parsed in to use the payload
*/
private static void extractBoatLocation(StreamPacket packet) {
byte[] payload = packet.getPayload();
int deviceType = (int) payload[15];
long timeValid = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
long seq = bytesToLong(Arrays.copyOfRange(payload, 11, 15));
long boatId = bytesToLong(Arrays.copyOfRange(payload, 7, 11));
long rawLat = bytesToLong(Arrays.copyOfRange(payload, 16, 20));
long rawLon = bytesToLong(Arrays.copyOfRange(payload, 20, 24));
//Converts the double to a usable lat/lon
double lat = ((180d * (double) rawLat) / Math.pow(2, 31));
double lon = ((180d * (double) rawLon) / Math.pow(2, 31));
// System.out.println("[CLIENT] Lat: " + lat + " Lon: " + lon);
long heading = bytesToLong(Arrays.copyOfRange(payload, 28, 30));
double groundSpeed = bytesToLong(Arrays.copyOfRange(payload, 38, 40)) / 1000.0;
//type 1 is a racing yacht and type 3 is a mark, needed for updating positions of the mark and boat
if (deviceType == 1){
Yacht boat = boats.get((int) boatId);
boat.setVelocity(groundSpeed);
BoatPositionPacket boatPacket = new BoatPositionPacket(boatId, timeValid, lat, lon, heading, groundSpeed);
//add a new priority que to the boatLocations HashMap
if (!boatLocations.containsKey(boatId)) {
boatLocations.put(boatId,
new PriorityBlockingQueue<>(256, new Comparator<BoatPositionPacket>() {
@Override
public int compare(BoatPositionPacket p1, BoatPositionPacket p2) {
return (int) (p1.getTimeValid() - p2.getTimeValid());
}
}));
}
boatLocations.get(boatId).put(boatPacket);
} else if (deviceType == 3) {
BoatPositionPacket markPacket = new BoatPositionPacket(boatId, timeValid, lat, lon,
heading, groundSpeed);
//add a new priority que to the boatLocations HashMap
if (!markLocations.containsKey(boatId)) {
markLocations.put(boatId,
new PriorityBlockingQueue<>(256, new Comparator<BoatPositionPacket>() {
@Override
public int compare(BoatPositionPacket p1, BoatPositionPacket p2) {
return (int) (p1.getTimeValid() - p2.getTimeValid());
}
}));
}
markLocations.get(boatId).put(markPacket);
}
}
/**
* This packet type is received when a mark or gate is rounded by a boat
*
* @param packet The packet containing the payload
*/
private static void extractMarkRounding(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
long raceId = bytesToLong(Arrays.copyOfRange(payload, 9, 13));
long subjectId = bytesToLong(Arrays.copyOfRange(payload, 13, 17));
int boatStatus = payload[17];
int roundingSide = payload[18];
int markType = payload[19];
int markId = payload[20];
// assign mark rounding time to boat
boats.get((int)subjectId).setMarkRoundingTime(timeStamp);
for (Mark mark : xmlObject.getRaceXML().getAllCompoundMarks()) {
if (mark.getCompoundMarkID() == markId) {
boats.get((int)subjectId).setLastMarkRounded(mark);
}
}
}
/**
* This packet type contains periodic data on the state of the wind
*
* @param packet The packet containing the payload
*/
private static void extractCourseWind(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
int selectedWindId = payload[1];
int loopCount = payload[2];
ArrayList<String> windInfo = new ArrayList<>();
for (int i = 0; i < loopCount; i++) {
String wind = "WindId: " + payload[3 + (20 * i)];
wind +=
"\nTime: " + bytesToLong(Arrays.copyOfRange(payload, 4 + (20 * i), 10 + (20 * i)));
wind += "\nRaceId: " + bytesToLong(
Arrays.copyOfRange(payload, 10 + (20 * i), 14 + (20 * i)));
wind += "\nWindDirection: " + bytesToLong(
Arrays.copyOfRange(payload, 14 + (20 * i), 16 + (20 * i)));
wind += "\nWindSpeed: " + bytesToLong(
Arrays.copyOfRange(payload, 16 + (20 * i), 18 + (20 * i)));
wind += "\nBestUpWindAngle: " + bytesToLong(
Arrays.copyOfRange(payload, 18 + (20 * i), 20 + (20 * i)));
wind += "\nBestDownWindAngle: " + bytesToLong(
Arrays.copyOfRange(payload, 20 + (20 * i), 22 + (20 * i)));
wind += "\nFlags: " + String
.format("%8s", Integer.toBinaryString(payload[22 + (20 * i)] & 0xFF))
.replace(' ', '0');
windInfo.add(wind);
}
}
/**
* This packet conatins the average wind to ground speed
*
* @param packet The packet containing the paylaod
*/
private static void extractAvgWind(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
long rawPeriod = bytesToLong(Arrays.copyOfRange(payload, 7, 9));
long rawSamplePeriod = bytesToLong(Arrays.copyOfRange(payload, 9, 11));
long period2 = bytesToLong(Arrays.copyOfRange(payload, 11, 13));
long speed2 = bytesToLong(Arrays.copyOfRange(payload, 13, 15));
long period3 = bytesToLong(Arrays.copyOfRange(payload, 15, 17));
long speed3 = bytesToLong(Arrays.copyOfRange(payload, 17, 19));
long period4 = bytesToLong(Arrays.copyOfRange(payload, 19, 21));
long speed4 = bytesToLong(Arrays.copyOfRange(payload, 21, 23));
}
/**
* takes an array of up to 7 bytes and returns a positive
* long constructed from the input bytes
*
* @return a positive long if there is less than 7 bytes -1 otherwise
*/
private static long bytesToLong(byte[] bytes) {
long partialLong = 0;
int index = 0;
for (byte b : bytes) {
if (index > 6) {
return -1;
}
partialLong = partialLong | (b & 0xFFL) << (index * 8);
index++;
}
return partialLong;
}
/**
* returns false if race not started, true otherwise
*
* @return race started status
*/
public static boolean isRaceStarted() {
return raceStarted;
}
/**
* returns false if stream not connected, true otherwise
*
* @return stream started status
*/
public static boolean isStreamStatus() {
return streamStatus;
}
/**
* returns race timer
*
* @return race timer in long
*/
public static long getTimeSinceStart() {
return timeSinceStart;
}
/**
* return false if race not finished, true otherwise
*
* @return race finished status
*/
public static boolean isRaceFinished() {
return raceFinished;
}
/**
* return a map of boats with sourceID and the boat
*
* @return map of boats
*/
public static Map<Integer, Yacht> getBoats() {
return boats;
}
/**
* returns the latest updated object from xml parser
*
* @return the latest xml object
*/
public static XMLParser getXmlObject() {
return xmlObject;
}
/**
* returns the wind direction in degrees
*
* @return a double wind direction value
*/
public static double getWindDirection() {
return windDirection;
}
/**
* Returns the wind speed in knots
* @return A double indicating the wind speed in knots
*/
public static Double getWindSpeed() {
return windSpeed;
}
/**
* returns stream time in formatted string format
*
* @return String of stream time
*/
public static String getCurrentTimeString() {
return currentTimeString;
}
/**
* used in boat position since tree map can sort position efficiently.
*
* @return a map of time to finish and boat.
*/
public static Map<Integer, Yacht> getBoatsPos() {
return boatsPos;
}
/**
* returns current time in stream in long
*
* @return a long value of current time
*/
public static Long getCurrentTimeLong() {
return currentTimeLong;
}
public static void appClose() {
appRunning = false;
}
/**
* Used to check if a new un-processed xml has been found, if so will return true before
* toggling off so that the next check will return false.
*
* @return the status of if new xml has been received
*/
public static boolean isNewRaceXmlReceived() {
if (newRaceXmlReceived) {
newRaceXmlReceived = false;
return true;
} else {
return false;
}
}
}
@@ -0,0 +1,78 @@
package seng302.client;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javafx.stage.Stage;
import seng302.models.Yacht;
/**
* Used by the client to store static variables, which other threads and classes
* observer so that they can update their status accordingly.
*/
public class ClientState {
private static String hostIp = "";
private static Boolean isHost = false;
private static Boolean raceStarted = false;
private static Boolean connectedToHost = false;
private static Map<Integer, Yacht> boats = new ConcurrentHashMap<>();
private static Boolean boatsUpdated = true;
private static String clientSourceId = "";
public static Stage primaryStage;
public static String getHostIp() {
return hostIp;
}
public static void setHostIp(String hostIp) {
ClientState.hostIp = hostIp;
}
public static Boolean isHost() {
return isHost;
}
public static void setHost(Boolean isHost) {
ClientState.isHost = isHost;
}
public static Boolean isRaceStarted() {
return raceStarted;
}
public static void setRaceStarted(Boolean raceStarted) {
ClientState.raceStarted = raceStarted;
}
public static Boolean isConnectedToHost() {
return connectedToHost;
}
public static void setConnectedToHost(Boolean connectedToHost) {
ClientState.connectedToHost = connectedToHost;
}
public static Map<Integer, Yacht> getBoats() {
return boats;
}
public static Boolean isBoatsUpdated() {
return boatsUpdated;
}
public static void setBoatsUpdated(Boolean boatsUpdated) {
ClientState.boatsUpdated = boatsUpdated;
}
public static String getClientSourceId() {
return clientSourceId;
}
public static void setClientSourceId(String clientSourceId) {
ClientState.clientSourceId = clientSourceId;
}
public static void setBoats(Map<Integer, Yacht> boats) {
ClientState.boats = boats;
}
}
@@ -0,0 +1,54 @@
package seng302.client;
import java.util.Observable;
/**
* Used by LobbyController to run a separate thread-loop
* updates the controller when change is detected.
*/
public class ClientStateQueryingRunnable extends Observable implements Runnable {
private Boolean terminate = false;
public ClientStateQueryingRunnable() {}
/**
* Notifies observers(the lobby controller) that "game started" if ClientState
* raceStarted flag is true and terminates itself. Also, it notifies observers
* to add/remove players if ClientState boatsUpdated flag is true, then resets
* the flag to false;
*/
@Override
public void run() {
while(!terminate) {
// Sleeping the thread so it will respond to the if statement below
// if you know a better fix, pls tell me :) -ryan
try {
Thread.sleep(0);
} catch (InterruptedException e) {
continue;
}
if (ClientState.isRaceStarted() && ClientState.isConnectedToHost()) {
setChanged();
notifyObservers("game started");
terminate();
}
if (ClientState.isBoatsUpdated()) {
setChanged();
notifyObservers("update players");
ClientState.setBoatsUpdated(false);
}
}
}
/**
* Used to terminate the thread.
*
* Currently called by the main while loop when game started is detected.
*/
public void terminate() {
terminate = true;
}
}
@@ -0,0 +1,216 @@
package seng302.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import seng302.models.stream.packets.StreamPacket;
import seng302.server.messages.BoatActionMessage;
import seng302.server.messages.Message;
/**
* A class describing a single connection to a Server for the purposes of sending and receiving on
* its own thread.
*/
public class ClientToServerThread implements Runnable {
private static final int LOG_LEVEL = 1;
private Thread thread;
private Integer ourID;
private Socket socket;
private InputStream is;
private OutputStream os;
private Boolean updateClient = true;
private ByteArrayOutputStream crcBuffer;
/**
* Constructor for ClientToServerThread which takes in ipAddress and portNumber and attempts to
* connect to the specified ipAddress and port.
*
* Upon successful socket connection, threeWayHandshake will be preformed and the instance will
* be put on a thread and run immediately.
*
* @param ipAddress a string of ip address to be connected to
* @param portNumber an integer port number
* @throws Exception SocketConnection if fail to connect to ip address and port number
* combination
*/
public ClientToServerThread(String ipAddress, Integer portNumber) throws Exception {
socket = new Socket(ipAddress, portNumber);
is = socket.getInputStream();
os = socket.getOutputStream();
Integer allocatedID = threeWayHandshake();
if (allocatedID != null) {
ourID = allocatedID;
clientLog("Successful handshake. Allocated ID: " + ourID, 0);
ClientState.setClientSourceId(String.valueOf(ourID));
} else {
clientLog("Unsuccessful handshake", 1);
closeSocket();
return;
}
thread = new Thread(this);
thread.start();
}
/**
* Prints out log messages and the time happened.
* Only perform task if log level is below LOG_LEVEL variable.
*
* @param message a string of message to be printed out
* @param logLevel an int for log level
*/
static void clientLog(String message, int logLevel) {
if (logLevel <= LOG_LEVEL) {
System.out.println(
"[CLIENT " + LocalDateTime.now().toLocalTime().toString() + "] " + message);
}
}
/**
* Perform the thread loop. It exits the loop if ClientState connected to host
* variable is false.
*/
public void run() {
int sync1;
int sync2;
// TODO: 14/07/17 wmu16 - Work out how to fix this while loop
while (ClientState.isConnectedToHost()) {
try {
crcBuffer = new ByteArrayOutputStream();
sync1 = readByte();
sync2 = readByte();
//checking if it is the start of the packet
if (sync1 == 0x47 && sync2 == 0x83) {
int type = readByte();
//No. of milliseconds since Jan 1st 1970
long timeStamp = Message.bytesToLong(getBytes(6));
skipBytes(4);
long payloadLength = Message.bytesToLong(getBytes(2));
byte[] payload = getBytes((int) payloadLength);
Checksum checksum = new CRC32();
checksum.update(crcBuffer.toByteArray(), 0, crcBuffer.size());
long computedCrc = checksum.getValue();
long packetCrc = Message.bytesToLong(getBytes(4));
if (computedCrc == packetCrc) {
ClientPacketParser
.parsePacket(new StreamPacket(type, payloadLength, timeStamp, payload));
} else {
clientLog("Packet has been dropped", 1);
}
}
} catch (Exception e) {
closeSocket();
Platform.runLater(new Runnable() {
@Override
public void run() {
Alert alert = new Alert(AlertType.ERROR);
alert.setHeaderText("Host has disconnected");
alert.setContentText("Cannot find Server");
alert.showAndWait();
}
});
clientLog("Disconnected from server", 1);
return;
}
}
closeSocket();
clientLog("Disconnected from server", 0);
}
/**
* Listens for an allocated sourceID and returns it to the server
*
* @return the sourceID allocated to us by the server
*/
private Integer threeWayHandshake() {
Integer ourSourceID = null;
while (true) {
try {
ourSourceID = is.read();
} catch (IOException e) {
clientLog("Three way handshake failed", 1);
}
if (ourSourceID != null) {
try {
os.write(ourSourceID);
return ourSourceID;
} catch (IOException e) {
clientLog("Three way handshake failed", 1);
return null;
}
}
}
}
/**
* Send the post-start race course information
*/
public void sendBoatActionMessage(BoatActionMessage boatActionMessage) {
try {
os.write(boatActionMessage.getBuffer());
} catch (IOException e) {
clientLog("Could not write to server", 1);
}
}
public void closeSocket() {
try {
socket.close();
} catch (IOException e) {
clientLog("Failed to close the socket", 1);
}
}
private int readByte() throws Exception {
int currentByte = -1;
try {
currentByte = is.read();
crcBuffer.write(currentByte);
} catch (IOException e) {
clientLog("Read byte failed", 1);
}
if (currentByte == -1) {
throw new Exception();
}
return currentByte;
}
private byte[] getBytes(int n) throws Exception {
byte[] bytes = new byte[n];
for (int i = 0; i < n; i++) {
bytes[i] = (byte) readByte();
}
return bytes;
}
private void skipBytes(long n) throws Exception {
for (int i = 0; i < n; i++) {
readByte();
}
}
public Thread getThread() {
return thread;
}
}
@@ -1,32 +1,44 @@
package seng302.controllers;
import javafx.animation.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.PriorityBlockingQueue;
import javafx.animation.AnimationTimer;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import seng302.models.*;
import seng302.models.mark.*;
import seng302.models.parsers.StreamParser;
import seng302.models.parsers.StreamReceiver;
import seng302.models.parsers.packets.BoatPositionPacket;
import seng302.models.parsers.XMLParser;
import seng302.models.parsers.XMLParser.RaceXMLObject.CompoundMark;
import seng302.models.parsers.XMLParser.RaceXMLObject.Limit;
import javafx.scene.text.Text;
import seng302.client.ClientPacketParser;
import seng302.client.ClientState;
import seng302.fxObjects.BoatGroup;
import seng302.models.Colors;
import seng302.models.Yacht;
import seng302.models.mark.GateMark;
import seng302.models.mark.Mark;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.PriorityBlockingQueue;
import seng302.fxObjects.MarkGroup;
import seng302.models.mark.MarkType;
import seng302.models.mark.SingleMark;
import seng302.models.map.Boundary;
import seng302.models.map.CanvasMap;
import seng302.models.stream.XMLParser;
import seng302.models.stream.XMLParser.RaceXMLObject.Limit;
import seng302.models.stream.XMLParser.RaceXMLObject.Participant;
import seng302.models.stream.packets.BoatPositionPacket;
import seng302.utilities.GeoPoint;
import seng302.utilities.GeoUtility;
/**
* Created by ptg19 on 15/03/17.
@@ -41,15 +53,14 @@ public class CanvasController {
private ResizableCanvas canvas;
private Group group;
private GraphicsContext gc;
private ImageView mapImage;
private final int MARK_SIZE = 10;
private final int BUFFER_SIZE = 50;
private final int CANVAS_WIDTH = 720;
private final int CANVAS_HEIGHT = 720;
private final int LHS_BUFFER = BUFFER_SIZE;
private final int RHS_BUFFER = BUFFER_SIZE + MARK_SIZE / 2;
private final int TOP_BUFFER = BUFFER_SIZE;
private final int BOT_BUFFER = TOP_BUFFER + MARK_SIZE / 2;
private final int PANEL_WIDTH = 1260; // it should be 1280 but, minors 40 to cancel the bias.
private final int PANEL_HEIGHT = 960;
private final int CANVAS_WIDTH = 1100;
private final int CANVAS_HEIGHT = 920;
private boolean horizontalInversion = false;
private double distanceScaleFactor;
private ScaleDirection scaleDirection;
@@ -59,16 +70,19 @@ public class CanvasController {
private Mark maxLonPoint;
private double referencePointX;
private double referencePointY;
private double metersToPixels;
private List<RaceObject> raceObjects = new ArrayList<>();
private List<Mark> raceMarks = new ArrayList<>();
private double metersPerPixelX;
private double metersPerPixelY;
private List<MarkGroup> markGroups = new ArrayList<>();
private List<BoatGroup> boatGroups = new ArrayList<>();
private Text FPSDisplay = new Text();
private Polygon raceBorder = new Polygon();
//FRAME RATE
private static final double UPDATE_TIME = 0.016666; // 1 / 60 ie 60fps
private Double frameRate = 60.0;
private final long[] frameTimes = new long[30];
private int frameTimeIndex = 0;
private boolean arrayFilled = false;
private DecimalFormat decimalFormat2dp = new DecimalFormat("0.00");
public AnimationTimer timer;
@@ -86,33 +100,44 @@ public class CanvasController {
canvas = new ResizableCanvas();
group = new Group();
// create image view for map, bind panel size to image
mapImage = new ImageView();
canvasPane.getChildren().add(mapImage);
mapImage.fitWidthProperty().bind(canvasPane.widthProperty());
mapImage.fitHeightProperty().bind(canvasPane.heightProperty());
canvasPane.getChildren().add(canvas);
canvasPane.getChildren().add(group);
// Bind canvas size to stack pane size.
canvas.widthProperty().bind(new SimpleDoubleProperty(CANVAS_WIDTH));
canvas.heightProperty().bind(new SimpleDoubleProperty(CANVAS_HEIGHT));
//group.minWidth(CANVAS_WIDTH);
//group.minHeight(CANVAS_HEIGHT);
}
public void initializeCanvas() {
gc = canvas.getGraphicsContext2D();
gc.save();
gc.setFill(Color.SKYBLUE);
gc.fillRect(0,0, CANVAS_WIDTH, CANVAS_HEIGHT);
gc.restore();
gc.setGlobalAlpha(0.5);
fitMarksToCanvas();
drawGoogleMap();
FPSDisplay.setLayoutX(5);
FPSDisplay.setLayoutY(20);
FPSDisplay.setStrokeWidth(2);
group.getChildren().add(FPSDisplay);
group.getChildren().add(raceBorder);
initializeMarks();
initializeBoats();
// TODO: 1/05/17 wmu16 - Change this call to now draw the marks as from the xml
drawBoats();
timer = new AnimationTimer() {
private long lastTime = 0;
private int FPSCount = 30;
@Override
public void handle(long now) {
//fps stuff
if (lastTime == 0) {
lastTime = now;
} else {
if (now - lastTime >= (1e8 / 60)) { //Fix for framerate going above 60 when minimized
long oldFrameTime = frameTimes[frameTimeIndex];
frameTimes[frameTimeIndex] = now;
frameTimeIndex = (frameTimeIndex + 1) % frameTimes.length;
@@ -123,150 +148,224 @@ public class CanvasController {
if (arrayFilled) {
elapsedNanos = now - oldFrameTime;
long elapsedNanosPerFrame = elapsedNanos / frameTimes.length;
Double frameRate = 1_000_000_000.0 / elapsedNanosPerFrame ;
frameRate = 1_000_000_000.0 / elapsedNanosPerFrame;
if (FPSCount-- == 0) {
FPSCount = 30;
drawFps(frameRate.intValue());
}
// TODO: 1/05/17 cir27 - Make the RaceObjects update on the actual delay.
elapsedNanos = 1000 / 60;
updateRaceObjects();
if (StreamParser.isRaceFinished()) {
raceViewController.updateSparkLine();
}
updateGroups();
if (ClientPacketParser.isRaceFinished()) {
this.stop();
}
lastTime = now;
}
}
if (ClientPacketParser.isRaceFinished()) {
this.stop();
switchToFinishScreen();
}
}
};
}
private void switchToFinishScreen() {
try {
// canvas view -> anchor pane -> grid pane -> main view
GridPane gridPane = (GridPane) canvasPane.getParent().getParent();
AnchorPane contentPane = (AnchorPane) gridPane.getParent();
contentPane.getChildren().removeAll();
contentPane.getChildren().clear();
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
contentPane.getChildren().addAll(
(Pane) FXMLLoader.load(getClass().getResource("/views/FinishScreenView.fxml")));
} catch (javafx.fxml.LoadException e) {
System.out.println("[Controller] FXML load exception");
} catch (IOException e) {
System.out.println("[Controller] IO exception");
}
}
/**
* First find the top right and bottom left points' geo locations, then retrieve
* map from google to display on image view. - Haoming 22/5/2017
*/
private void drawGoogleMap() {
findMetersPerPixel();
Point2D topLeftPoint = findScaledXY(maxLatPoint.getLatitude(), minLonPoint.getLongitude());
// distance from top left extreme to panel origin (top left corner)
double distanceFromTopLeftToOrigin = Math.sqrt(
Math.pow(topLeftPoint.getX() * metersPerPixelX, 2) + Math
.pow(topLeftPoint.getY() * metersPerPixelY, 2));
// angle from top left extreme to panel origin
double bearingFromTopLeftToOrigin = Math
.toDegrees(Math.atan2(-topLeftPoint.getX(), topLeftPoint.getY()));
// the top left extreme
GeoPoint topLeftPos = new GeoPoint(maxLatPoint.getLatitude(), minLonPoint.getLongitude());
GeoPoint originPos = GeoUtility
.getGeoCoordinate(topLeftPos, bearingFromTopLeftToOrigin, distanceFromTopLeftToOrigin);
// distance from origin corner to bottom right corner of the panel
double distanceFromOriginToBottomRight = Math.sqrt(
Math.pow(PANEL_HEIGHT * metersPerPixelY, 2) + Math
.pow(PANEL_WIDTH * metersPerPixelX, 2));
double bearingFromOriginToBottomRight = Math
.toDegrees(Math.atan2(PANEL_WIDTH, -PANEL_HEIGHT));
GeoPoint bottomRightPos = GeoUtility
.getGeoCoordinate(originPos, bearingFromOriginToBottomRight,
distanceFromOriginToBottomRight);
Boundary boundary = new Boundary(originPos.getLat(), bottomRightPos.getLng(),
bottomRightPos.getLat(), originPos.getLng());
CanvasMap canvasMap = new CanvasMap(boundary);
mapImage.setImage(canvasMap.getMapImage());
}
/**
* Adds border marks to the canvas, taken from the XML file
*
* NOTE: This is quite confusing as objects are grabbed from the XMLParser such as Mark and CompoundMark which are
* named the same as those in the model package but are, however not the same, so they do not have things such as
* a type and must be derived from the number of marks in a compound mark etc..
* NOTE: This is quite confusing as objects are grabbed from the XMLParser such as Mark and
* CompoundMark which are named the same as those in the model package but are, however not the
* same, so they do not have things such as a type and must be derived from the number of marks
* in a compound mark etc..
*/
private void addRaceBorder() {
XMLParser.RaceXMLObject raceXMLObject = StreamParser.getXmlObject().getRaceXML();
XMLParser.RaceXMLObject raceXMLObject = ClientPacketParser.getXmlObject().getRaceXML();
ArrayList<Limit> courseLimits = raceXMLObject.getCourseLimit();
gc.setStroke(Color.DARKBLUE);
gc.setLineWidth(3);
double[] xBoundaryPoints = new double[courseLimits.size()];
double[] yBoundaryPoints = new double[courseLimits.size()];
for (int i = 0; i < courseLimits.size() - 1; i++) {
Limit thisPoint1 = courseLimits.get(i);
SingleMark thisMark1 = new SingleMark("", thisPoint1.getLat(), thisPoint1.getLng(), thisPoint1.getSeqID());
Limit thisPoint2 = courseLimits.get(i+1);
SingleMark thisMark2 = new SingleMark("", thisPoint2.getLat(), thisPoint2.getLng(), thisPoint2.getSeqID());
Point2D borderPoint1 = findScaledXY(thisMark1);
Point2D borderPoint2 = findScaledXY(thisMark2);
gc.strokeLine(borderPoint1.getX(), borderPoint1.getY(),
borderPoint2.getX(), borderPoint2.getY());
xBoundaryPoints[i] = borderPoint1.getX();
yBoundaryPoints[i] = borderPoint1.getY();
raceBorder.setStroke(new Color(0.0f, 0.0f, 0.74509807f, 1));
raceBorder.setStrokeWidth(3);
raceBorder.setFill(new Color(0,0,0,0));
List<Double> boundaryPoints = new ArrayList<>();
for (Limit limit : courseLimits) {
Point2D location = findScaledXY(limit.getLat(), limit.getLng());
boundaryPoints.add(location.getX());
boundaryPoints.add(location.getY());
}
Limit thisPoint1 = courseLimits.get(courseLimits.size()-1);
SingleMark thisMark1 = new SingleMark("", thisPoint1.getLat(), thisPoint1.getLng(), thisPoint1.getSeqID());
Limit thisPoint2 = courseLimits.get(0);
SingleMark thisMark2 = new SingleMark("", thisPoint2.getLat(), thisPoint2.getLng(), thisPoint2.getSeqID());
Point2D borderPoint1 = findScaledXY(thisMark1);
Point2D borderPoint2 = findScaledXY(thisMark2);
gc.strokeLine(borderPoint1.getX(), borderPoint1.getY(),
borderPoint2.getX(), borderPoint2.getY());
xBoundaryPoints[courseLimits.size()-1] = borderPoint1.getX();
yBoundaryPoints[courseLimits.size()-1] = borderPoint1.getY();
gc.setFill(Color.LIGHTBLUE);
gc.fillPolygon(xBoundaryPoints,yBoundaryPoints,yBoundaryPoints.length);
raceBorder.getPoints().setAll(boundaryPoints);
}
private void updateGroups() {
for (BoatGroup boatGroup : boatGroups) {
// some raceObjects will have multiple ID's (for instance gate marks)
//checking if the current "ID" has any updates associated with it
if (ClientPacketParser.boatLocations.containsKey(boatGroup.getRaceId())) {
updateBoatGroup(boatGroup);
}
}
for (MarkGroup markGroup : markGroups) {
for (Long id : markGroup.getRaceIds()) {
if (ClientPacketParser.markLocations.containsKey(id)) {
updateMarkGroup(id, markGroup);
}
}
}
checkForCourseChanges();
}
private void checkForCourseChanges() {
if (ClientPacketParser.isNewRaceXmlReceived()) {
addRaceBorder();
}
}
private void updateBoatGroup(BoatGroup boatGroup) {
PriorityBlockingQueue<BoatPositionPacket> movementQueue = ClientPacketParser.boatLocations
.get(boatGroup.getRaceId());
// giving the movementQueue a 5 packet buffer to account for slightly out of order packets
if (movementQueue.size() > 0) {
try {
BoatPositionPacket positionPacket = movementQueue.take();
Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon());
double heading = 360.0 / 0xffff * positionPacket.getHeading();
boatGroup.setDestination(
p2d.getX(), p2d.getY(), heading, positionPacket.getGroundSpeed(),
positionPacket.getTimeValid(), frameRate);
} catch (InterruptedException e){
System.out.println("[CanvasController] Interrupted Exception");
}
}
}
void updateMarkGroup (long raceId, MarkGroup markGroup) {
PriorityBlockingQueue<BoatPositionPacket> movementQueue = ClientPacketParser.markLocations
.get(raceId);
if (movementQueue.size() > 0){
try {
BoatPositionPacket positionPacket = movementQueue.take();
Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon());
markGroup.moveMarkTo(p2d.getX(), p2d.getY(), raceId);
} catch (InterruptedException e) {
System.out.println("[CanvasController] Interrupted exception");
}
}
}
/**
* Adds the course marks to the canvas, taken from the XMl file
*
* NOTE: This is quite confusing as objects are grabbed from the XMLParser such as Mark and CompoundMark which are
* named the same as those in the model package but are, however not the same, so they do not have things such as
* a type and must be derived from the number of marks in a compound mark etc..
* Draws all the boats.
*/
private void addCourseMarks() {
XMLParser.RaceXMLObject raceXMLObject = StreamParser.getXmlObject().getRaceXML();
ArrayList<CompoundMark> compoundMarks = raceXMLObject.getCompoundMarks();
RaceObject markGroup;
private void initializeBoats() {
Map<Integer, Yacht> boats = ClientPacketParser.getBoats();
Group wakes = new Group();
Group trails = new Group();
Group annotations = new Group();
for (CompoundMark compoundMark : compoundMarks) {
ArrayList<Participant> participants = ClientPacketParser.getXmlObject().getRaceXML()
.getParticipants();
ArrayList<Integer> participantIDs = new ArrayList<>();
for (Participant p : participants) {
participantIDs.add(p.getsourceID());
}
//If the compound mark has 2 marks then its a gate mark
if (compoundMark.getMarks().size() == 2) {
CompoundMark.Mark mark1 = compoundMark.getMarks().get(0);
CompoundMark.Mark mark2 = compoundMark.getMarks().get(1);
SingleMark singleMark1 = new SingleMark(mark1.getMarkName(), mark1.getTargetLat(), mark1.getTargetLng(), mark1.getSourceID());
SingleMark singleMark2 = new SingleMark(mark1.getMarkName(), mark2.getTargetLat(), mark2.getTargetLng(), mark2.getSourceID());
GateMark thisGateMark = new GateMark(compoundMark.getcMarkName(),
(compoundMark.getMarkID().equals(1)) ? MarkType.OPEN_GATE : MarkType.CLOSED_GATE,
singleMark1,
singleMark2,
singleMark1.getLatitude(),
singleMark1.getLongitude());
markGroup = new MarkGroup(thisGateMark,
findScaledXY(thisGateMark.getSingleMark1()),
findScaledXY(thisGateMark.getSingleMark2()));
raceObjects.add(markGroup);
raceMarks.add(thisGateMark);
//Otherwise its a single mark
for (Yacht boat : boats.values()) {
if (participantIDs.contains(boat.getSourceId())) {
boat.setColour(Colors.getColor());
BoatGroup boatGroup = new BoatGroup(boat, boat.getColour());
if (boat.getSourceId().equals(Integer.parseInt(ClientState.getClientSourceId()))) {
boatGroup.setAsPlayer();
boatGroups.add(boatGroup);
annotations.getChildren().add(boatGroup.getAnnotations());
} else {
CompoundMark.Mark singleMark = compoundMark.getMarks().get(0);
Mark thisSingleMark = new SingleMark(singleMark.getMarkName(),
singleMark.getTargetLat(),
singleMark.getTargetLng(),
singleMark.getSourceID());
markGroup = new MarkGroup(thisSingleMark, findScaledXY(thisSingleMark));
raceObjects.add(markGroup);
raceMarks.add(thisSingleMark);
//Move annotations and boat to bottom of group keeping player ontop.
if (boatGroups.size() > 0) {
boatGroups.add(0, boatGroup);
} else {
boatGroups.add(boatGroup);
}
if (annotations.getChildren().size() > 0) {
annotations.getChildren().add(0, boatGroup.getAnnotations());
} else {
annotations.getChildren().add(boatGroup.getAnnotations());
}
}
trails.getChildren().add(boatGroup.getTrail());
wakes.getChildren().add(boatGroup.getWake());
}
}
group.getChildren().addAll(trails);
group.getChildren().addAll(wakes);
group.getChildren().addAll(annotations);
group.getChildren().addAll(boatGroups);
}
private void updateRaceObjects(){
for (RaceObject raceObject : raceObjects) {
raceObject.updatePosition(1000 / 60);
// some raceObjects will have multiply ID's (for instance gate marks)
for (long id : raceObject.getRaceIds()) {
//checking if the current "ID" has any updates associated with it
if (StreamParser.boatPositions.containsKey(id)) {
move(id, raceObject);
}
}
}
}
private void initializeMarks() {
List<Mark> allMarks = ClientPacketParser.getXmlObject().getRaceXML()
.getNonDupCompoundMarks();
for (Mark mark : allMarks) {
if (mark.getMarkType() == MarkType.SINGLE_MARK) {
SingleMark sMark = (SingleMark) mark;
private void move(long id, RaceObject raceObject){
PriorityBlockingQueue<BoatPositionPacket> movementQueue = StreamParser.boatPositions.get(id);
if (movementQueue.size() > 0){
BoatPositionPacket positionPacket = movementQueue.peek();
MarkGroup markGroup = new MarkGroup(sMark, findScaledXY(sMark));
markGroups.add(markGroup);
} else {
GateMark gMark = (GateMark) mark;
//this code adds a delay to reading from the movementQueue
//in case things being put into the movement queue are slightly
//out of order
int delayTime = 1000;
int loopTime = delayTime * 10;
long timeDiff = (System.currentTimeMillis()%loopTime - positionPacket.getTimeValid()%loopTime);
if (timeDiff < 0){
timeDiff = loopTime + timeDiff;
}
if (timeDiff > delayTime) {
try {
positionPacket = movementQueue.take();
Point2D p2d = latLonToXY(positionPacket.getLat(), positionPacket.getLon());
double heading = 360.0 / 0xffff * positionPacket.getHeading();
raceObject.setDestination(p2d.getX(), p2d.getY(), heading, positionPacket.getGroundSpeed(), (int) id);
} catch (InterruptedException e){
e.printStackTrace();
}
MarkGroup markGroup = new MarkGroup(gMark, findScaledXY(gMark.getSingleMark1()),
findScaledXY(gMark.getSingleMark2())); //should be 2 objects in the list.
markGroups.add(markGroup);
}
}
group.getChildren().addAll(markGroups);
}
class ResizableCanvas extends Canvas {
@@ -299,97 +398,67 @@ public class CanvasController {
public double prefHeight(double width) {
return getHeight();
}
}
private void drawFps(int fps){
if (raceViewController.isDisplayFps()){
gc.clearRect(5,5,50,20);
gc.setFill(Color.SKYBLUE);
gc.fillRect(4,4,51,21);
gc.setFill(Color.BLACK);
gc.setFont(new Font(14));
gc.setLineWidth(3);
gc.fillText(fps + " FPS", 5, 20);
FPSDisplay.setVisible(true);
FPSDisplay.setText(String.format("%d FPS", fps));
} else {
gc.clearRect(5,5,50,20);
gc.setFill(Color.SKYBLUE);
gc.fillRect(4,4,51,21);
FPSDisplay.setVisible(false);
}
}
/**
* Draws all the boats.
*/
private void drawBoats() {
// Map<Boat, TimelineInfo> timelineInfos = raceViewController.getTimelineInfos();
// List<Boat> boats = raceViewController.getStartingBoats();
Map<Integer, Yacht> boats = StreamParser.getBoats();
Double startingX = raceObjects.get(0).getLayoutX();
Double startingY = raceObjects.get(0).getLayoutY();
Group boatAnnotations = new Group();
for (Yacht boat : boats.values()) {
// for (Boat boat : boats) {
boat.setColour(Colors.getColor());
BoatGroup boatGroup = new BoatGroup(boat, boat.getColour());
boatGroup.moveTo(startingX, startingY, 0d);
//boatGroup.setStage(raceViewController.getStage());
raceObjects.add(boatGroup);
boatAnnotations.getChildren().add(boatGroup.getLowPriorityAnnotations());
}
group.getChildren().add(boatAnnotations);
group.getChildren().addAll(raceObjects);
}
/**
* Calculates x and y location for every marker that fits it to the canvas the race will be drawn on.
* Calculates x and y location for every marker that fits it to the canvas the race will be
* drawn on.
*/
private void fitMarksToCanvas() {
//Check is called once to avoid unnecessarily change the course limits once the race is running
ClientPacketParser.isNewRaceXmlReceived();
findMinMaxPoint();
double minLonToMaxLon = scaleRaceExtremities();
calculateReferencePointLocation(minLonToMaxLon);
givePointsXY();
//givePointsXY();
addRaceBorder();
findMetersToPixels();
}
/**
* Sets the class variables minLatPoint, maxLatPoint, minLonPoint, maxLonPoint to the marker with the leftmost
* marker, rightmost marker, southern most marker and northern most marker respectively.
* Sets the class variables minLatPoint, maxLatPoint, minLonPoint, maxLonPoint to the marker
* with the leftmost marker, rightmost marker, southern most marker and northern most marker
* respectively.
*/
private void findMinMaxPoint() {
List<Limit> sortedPoints = new ArrayList<>();
for (Limit limit : StreamParser.getXmlObject().getRaceXML().getCourseLimit()) {
for (Limit limit : ClientPacketParser.getXmlObject().getRaceXML().getCourseLimit()) {
sortedPoints.add(limit);
}
sortedPoints.sort(Comparator.comparingDouble(Limit::getLat));
Limit minLatMark = sortedPoints.get(0);
Limit maxLatMark = sortedPoints.get(sortedPoints.size()-1);
minLatPoint = new SingleMark(minLatMark.toString(), minLatMark.getLat(), minLatMark.getLng(), minLatMark.getSeqID());
maxLatPoint = new SingleMark(maxLatMark.toString(), maxLatMark.getLat(), maxLatMark.getLng(), maxLatMark.getSeqID());
minLatPoint = new SingleMark(minLatMark.toString(), minLatMark.getLat(), minLatMark.getLng(), minLatMark.getSeqID(), minLatMark.getSeqID());
maxLatPoint = new SingleMark(maxLatMark.toString(), maxLatMark.getLat(), maxLatMark.getLng(), maxLatMark.getSeqID(), minLatMark.getSeqID());
sortedPoints.sort(Comparator.comparingDouble(Limit::getLng));
//If the course is on a point on the earth where longitudes wrap around.
Limit minLonMark = sortedPoints.get(0);
Limit maxLonMark = sortedPoints.get(sortedPoints.size()-1);
SingleMark thisMinLon = new SingleMark(minLonMark.toString(), minLonMark.getLat(), minLonMark.getLng(), minLonMark.getSeqID());
SingleMark thisMaxLon = new SingleMark(maxLonMark.toString(), maxLonMark.getLat(), maxLonMark.getLng(), maxLonMark.getSeqID());
// TODO: 30/03/17 cir27 - Correctly account for longitude wrapping around.
if (thisMaxLon.getLongitude() - thisMinLon.getLongitude() > 180) {
SingleMark temp = thisMinLon;
thisMinLon = thisMaxLon;
thisMaxLon = temp;
minLonPoint = new SingleMark(minLonMark.toString(), minLonMark.getLat(), minLonMark.getLng(), minLonMark.getSeqID(), minLonMark.getSeqID());
maxLonPoint = new SingleMark(maxLonMark.toString(), maxLonMark.getLat(), maxLonMark.getLng(), maxLonMark.getSeqID(), minLonMark.getSeqID());
if (maxLonPoint.getLongitude() - minLonPoint.getLongitude() > 180) {
horizontalInversion = true;
}
minLonPoint = thisMinLon;
maxLonPoint = thisMaxLon;
}
/**
* Calculates the location of a reference point, this is always the point with minimum latitude, in relation to the
* canvas.
* Calculates the location of a reference point, this is always the point with minimum latitude,
* in relation to the canvas.
*
* @param minLonToMaxLon The horizontal distance between the point of minimum longitude to maximum longitude.
* @param minLonToMaxLon The horizontal distance between the point of minimum longitude to
* maximum longitude.
*/
private void calculateReferencePointLocation(double minLonToMaxLon) {
Mark referencePoint = minLatPoint;
@@ -397,44 +466,51 @@ public class CanvasController {
if (scaleDirection == ScaleDirection.HORIZONTAL) {
referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, minLonPoint));
referencePointX = LHS_BUFFER + distanceScaleFactor * Math.sin(referenceAngle) * Mark.calculateDistance(referencePoint, minLonPoint);
referencePointX = BUFFER_SIZE + distanceScaleFactor * Math.sin(referenceAngle) * Mark.calculateDistance(referencePoint, minLonPoint);
referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, maxLatPoint));
referencePointY = CANVAS_HEIGHT - (TOP_BUFFER + BOT_BUFFER);
referencePointY = CANVAS_HEIGHT - (BUFFER_SIZE + BUFFER_SIZE);
referencePointY -= distanceScaleFactor * Math.cos(referenceAngle) * Mark.calculateDistance(referencePoint, maxLatPoint);
referencePointY = referencePointY / 2;
referencePointY += TOP_BUFFER;
referencePointY += BUFFER_SIZE;
referencePointY += distanceScaleFactor * Math.cos(referenceAngle) * Mark.calculateDistance(referencePoint, maxLatPoint);
} else {
referencePointY = CANVAS_HEIGHT - BOT_BUFFER;
referencePointY = CANVAS_HEIGHT - BUFFER_SIZE;
referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, minLonPoint));
referencePointX = LHS_BUFFER;
referencePointX = BUFFER_SIZE;
referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * Mark.calculateDistance(referencePoint, minLonPoint);
referencePointX += ((CANVAS_WIDTH - (LHS_BUFFER + RHS_BUFFER)) - (minLonToMaxLon * distanceScaleFactor)) / 2;
referencePointX += ((CANVAS_WIDTH - (BUFFER_SIZE + BUFFER_SIZE)) - (minLonToMaxLon * distanceScaleFactor)) / 2;
}
if(horizontalInversion) {
referencePointX = CANVAS_WIDTH - BUFFER_SIZE - (referencePointX - BUFFER_SIZE);
}
}
/**
* Finds the scale factor necessary to fit all race markers within the onscreen map and assigns it to distanceScaleFactor
* Returns the max horizontal distance of the map.
* Finds the scale factor necessary to fit all race markers within the onscreen map and assigns
* it to distanceScaleFactor Returns the max horizontal distance of the map.
*/
private double scaleRaceExtremities() {
double vertAngle = Math.abs(Mark.calculateHeadingRad(minLatPoint, maxLatPoint));
double vertDistance = Math.cos(vertAngle) * Mark.calculateDistance(minLatPoint, maxLatPoint);
double vertDistance =
Math.cos(vertAngle) * Mark.calculateDistance(minLatPoint, maxLatPoint);
double horiAngle = Mark.calculateHeadingRad(minLonPoint, maxLonPoint);
if (horiAngle <= (Math.PI / 2))
if (horiAngle <= (Math.PI / 2)) {
horiAngle = (Math.PI / 2) - horiAngle;
else
} else {
horiAngle = horiAngle - (Math.PI / 2);
double horiDistance = Math.cos(horiAngle) * Mark.calculateDistance(minLonPoint, maxLonPoint);
}
double horiDistance =
Math.cos(horiAngle) * Mark.calculateDistance(minLonPoint, maxLonPoint);
double vertScale = (CANVAS_HEIGHT - (TOP_BUFFER + BOT_BUFFER)) / vertDistance;
double vertScale = (CANVAS_HEIGHT - (BUFFER_SIZE + BUFFER_SIZE)) / vertDistance;
if ((horiDistance * vertScale) > (CANVAS_WIDTH - (RHS_BUFFER + LHS_BUFFER))) {
distanceScaleFactor = (CANVAS_WIDTH - (RHS_BUFFER + LHS_BUFFER)) / horiDistance;
if ((horiDistance * vertScale) > (CANVAS_WIDTH - (BUFFER_SIZE + BUFFER_SIZE))) {
distanceScaleFactor = (CANVAS_WIDTH - (BUFFER_SIZE + BUFFER_SIZE)) / horiDistance;
scaleDirection = ScaleDirection.HORIZONTAL;
} else {
distanceScaleFactor = vertScale;
@@ -443,97 +519,79 @@ public class CanvasController {
return horiDistance;
}
/**
* Give all markers in the course an x,y location relative to a given reference with a known x,y location. Distances
* are scaled according to the distanceScaleFactor variable.
*/
private void givePointsXY() {
List<Mark> allPoints = new ArrayList<>(raceViewController.getRace().getCourse());
List<Mark> processed = new ArrayList<>();
RaceObject markGroup;
for (Mark mark : allPoints) {
if (!processed.contains(mark)) {
if (mark.getMarkType() != MarkType.SINGLE_MARK) {
GateMark gateMark = (GateMark) mark;
markGroup = new MarkGroup(mark, findScaledXY(gateMark.getSingleMark1()), findScaledXY(gateMark.getSingleMark2()));
raceObjects.add(markGroup);
} else {
markGroup = new MarkGroup(mark, findScaledXY(mark));
raceObjects.add(markGroup);
}
processed.add(mark);
}
}
}
private Point2D findScaledXY(Mark unscaled) {
return findScaledXY (minLatPoint.getLatitude(), minLatPoint.getLongitude(),
unscaled.getLatitude(), unscaled.getLongitude());
return findScaledXY(unscaled.getLatitude(), unscaled.getLongitude());
}
private Point2D findScaledXY (double latA, double lonA, double latB, double lonB) {
public Point2D findScaledXY (double unscaledLat, double unscaledLon) {
double distanceFromReference;
double angleFromReference;
int xAxisLocation = (int) referencePointX;
int yAxisLocation = (int) referencePointY;
angleFromReference = Mark.calculateHeadingRad(latA, lonA, latB, lonB);
distanceFromReference = Mark.calculateDistance(latA, lonA, latB, lonB);
angleFromReference = Mark
.calculateHeadingRad(minLatPoint.getLatitude(), minLatPoint.getLongitude(), unscaledLat,
unscaledLon);
distanceFromReference = Mark
.calculateDistance(minLatPoint.getLatitude(), minLatPoint.getLongitude(), unscaledLat,
unscaledLon);
if (angleFromReference >= 0 && angleFromReference <= Math.PI / 2) {
xAxisLocation += (int) Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
yAxisLocation -= (int) Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
xAxisLocation += (int) Math
.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
yAxisLocation -= (int) Math
.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
} else if (angleFromReference >= 0) {
angleFromReference = angleFromReference - Math.PI / 2;
xAxisLocation += (int) Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
yAxisLocation += (int) Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
xAxisLocation += (int) Math
.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
yAxisLocation += (int) Math
.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
} else if (angleFromReference < 0 && angleFromReference >= -Math.PI / 2) {
angleFromReference = Math.abs(angleFromReference);
xAxisLocation -= (int) Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
yAxisLocation -= (int) Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
xAxisLocation -= (int) Math
.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
yAxisLocation -= (int) Math
.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
} else {
angleFromReference = Math.abs(angleFromReference) - Math.PI / 2;
xAxisLocation -= (int) Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
yAxisLocation += (int) Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
xAxisLocation -= (int) Math
.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
yAxisLocation += (int) Math
.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
}
if(horizontalInversion) {
xAxisLocation = CANVAS_WIDTH - BUFFER_SIZE - (xAxisLocation - BUFFER_SIZE);
}
return new Point2D(xAxisLocation, yAxisLocation);
}
/**
* Find the number of meters per pixel.
*/
private void findMetersToPixels () {
Double angularDistance;
Double angle;
Double straightLineDistance;
if (scaleDirection == ScaleDirection.HORIZONTAL) {
angularDistance = Mark.calculateDistance(minLonPoint, maxLonPoint);
angle = Mark.calculateHeadingRad(minLonPoint, maxLonPoint);
if (angle > Math.PI / 2) {
straightLineDistance = Math.cos(angle - Math.PI) * angularDistance;
} else {
straightLineDistance = Math.cos(angle) * angularDistance;
}
metersToPixels = (CANVAS_WIDTH - RHS_BUFFER - LHS_BUFFER) / straightLineDistance;
} else {
angularDistance = Mark.calculateDistance(minLatPoint, maxLatPoint);
angle = Mark.calculateHeadingRad(minLatPoint, maxLatPoint);
if (angle < Math.PI / 2) {
straightLineDistance = Math.cos(angle) * angularDistance;
} else {
straightLineDistance = Math.cos(-angle + Math.PI * 2) * angularDistance;
}
metersToPixels = (CANVAS_HEIGHT - TOP_BUFFER - BOT_BUFFER) / straightLineDistance;
}
private void findMetersPerPixel() {
Point2D p1, p2;
Mark m1, m2;
double theta, distance, dx, dy, dHorizontal, dVertical;
m1 = new SingleMark("m1", maxLatPoint.getLatitude(), minLonPoint.getLongitude(), 1, 0);
m2 = new SingleMark("m2", minLatPoint.getLatitude(), maxLonPoint.getLongitude(), 2, 0);
p1 = findScaledXY(m1);
p2 = findScaledXY(m2);
theta = Mark.calculateHeadingRad(m1, m2);
distance = Mark.calculateDistance(m1, m2);
dHorizontal = Math.abs(Math.sin(theta) * distance);
dVertical = Math.abs(Math.cos(theta) * distance);
dx = Math.abs(p1.getX() - p2.getX());
dy = Math.abs(p1.getY() - p2.getY());
metersPerPixelX = dHorizontal / dx;
metersPerPixelY = dVertical / dy;
}
private Point2D latLonToXY (double latitude, double longitude) {
return findScaledXY(minLatPoint.getLatitude(), minLatPoint.getLongitude(), latitude, longitude);
List<BoatGroup> getBoatGroups() {
return boatGroups;
}
List<RaceObject> getRaceObjects() {
return raceObjects;
List<MarkGroup> getMarkGroups() {
return markGroups;
}
}
+75 -133
View File
@@ -1,164 +1,106 @@
package seng302.controllers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import seng302.models.Yacht;
import seng302.models.parsers.StreamParser;
import seng302.models.parsers.XMLParser;
import java.io.IOException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import seng302.client.ClientPacketParser;
import seng302.client.ClientState;
import seng302.client.ClientToServerThread;
import seng302.server.messages.BoatActionMessage;
import seng302.server.messages.BoatActionType;
public class Controller implements Initializable {
@FXML
private AnchorPane contentPane;
@FXML
private Label timeTillLive;
@FXML
private Button streamButton;
@FXML
private Button switchToRaceViewButton;
@FXML
private TableView<Yacht> teamList;
@FXML
private TableColumn<Yacht, String> boatNameCol;
@FXML
private TableColumn<Yacht, String> shortNameCol;
@FXML
private TableColumn<Yacht, String> countryCol;
@FXML
private TableColumn<Yacht, String> posCol;
@FXML
private Label realTime;
private ClientToServerThread clientToServerThread;
private long lastSendingTime;
private int KEY_STROKE_SENDING_FREQUENCY = 50;
private XMLParser xmlParser;
private void setContentPane(String jfxUrl){
public Object setContentPane(String jfxUrl) {
try {
contentPane.getChildren().removeAll();
contentPane.getChildren().clear();
contentPane.getChildren().addAll((Pane) FXMLLoader.load(getClass().getResource(jfxUrl)));
}
catch(javafx.fxml.LoadException e){
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
FXMLLoader fxmlLoader = new FXMLLoader((getClass().getResource(jfxUrl)));
Parent view = fxmlLoader.load();
contentPane.getChildren().addAll(view);
return fxmlLoader.getController();
} catch (javafx.fxml.LoadException e) {
System.err.println(e.getCause());
}
catch(IOException e){
} catch (IOException e) {
System.err.println(e);
}
return null;
}
@Override
public void initialize(URL location, ResourceBundle resources) {
//DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
//format.setTimeZone(TimeZone.getTimeZone("GMT-8"));
//realTime.setText(format.format(new Date()));
setUpStartScreen();
lastSendingTime = System.currentTimeMillis();
}
/**
* Running a timer to update the livestream status on welcome screen. Update interval is 1 second.
*/
public void startStream() {
if (StreamParser.isStreamStatus()) {
xmlParser = StreamParser.getXmlObject();
streamButton.setVisible(false);
realTime.setVisible(true);
timeTillLive.setVisible(true);
timeTillLive.setTextFill(Color.GREEN);
timeTillLive.setText("Connecting...");
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Platform.runLater(() -> {
if (StreamParser.isRaceFinished()) {
realTime.setText(StreamParser.getCurrentTimeString());
timeTillLive.setTextFill(Color.RED);
timeTillLive.setText("Race finished! Waiting for new race...");
switchToRaceViewButton.setDisable(true);
} else if (StreamParser.getTimeSinceStart() > 0) {
realTime.setText(StreamParser.getCurrentTimeString());
updateTeamList();
timeTillLive.setTextFill(Color.RED);
switchToRaceViewButton.setDisable(false);
String timerMinute = Long.toString(StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(StreamParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
void setUpStartScreen() {
contentPane.getChildren().removeAll();
contentPane.getChildren().clear();
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
StartScreenController startScreenController = (StartScreenController) setContentPane("/views/StartScreenView.fxml");
startScreenController.setController(this);
ClientPacketParser.boatLocations.clear();
}
String timerString = "-" + timerMinute + ":" + timerSecond;
timeTillLive.setText(timerString);
} else {
realTime.setText(StreamParser.getCurrentTimeString());
updateTeamList();
timeTillLive.setTextFill(Color.BLACK);
switchToRaceViewButton.setDisable(false);
String timerMinute = Long.toString(-1 * StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(-1 * StreamParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
/** Handle the key-pressed event from the text field. */
public void keyPressed(KeyEvent e) {
BoatActionMessage boatActionMessage;
long currentTime = System.currentTimeMillis();
if (currentTime - lastSendingTime > KEY_STROKE_SENDING_FREQUENCY && ClientState.isRaceStarted()) {
lastSendingTime = currentTime;
switch (e.getCode()) {
case SPACE: // align with vmg
boatActionMessage = new BoatActionMessage(BoatActionType.VMG);
clientToServerThread.sendBoatActionMessage(boatActionMessage);
break;
case PAGE_UP: // upwind
boatActionMessage = new BoatActionMessage(BoatActionType.UPWIND);
clientToServerThread.sendBoatActionMessage(boatActionMessage);
break;
case PAGE_DOWN: // downwind
boatActionMessage = new BoatActionMessage(BoatActionType.DOWNWIND);
clientToServerThread.sendBoatActionMessage(boatActionMessage);
break;
case ENTER: // tack/gybe
boatActionMessage = new BoatActionMessage(BoatActionType.TACK_GYBE);
clientToServerThread.sendBoatActionMessage(boatActionMessage);
break;
//TODO Allow a zoom in and zoom out methods
case Z: // zoom in
System.out.println("Zoom in");
break;
case X: // zoom out
System.out.println("Zoom out");
break;
}
String timerString = timerMinute + ":" + timerSecond;
timeTillLive.setText(timerString);
}
});
}
}, 0, 1000);
} else {
timeTillLive.setText("Stream not available.");
timeTillLive.setTextFill(Color.RED);
}
}
public void switchToRaceView() {
setContentPane("/views/RaceView.fxml");
public void keyReleased(KeyEvent e) {
switch (e.getCode()) {
//TODO 12/07/17 Determine the sail state and send the appropriate packet (eg. if sails are in, send a sail out packet)
case SHIFT: // sails in/sails out
BoatActionMessage boatActionMessage = new BoatActionMessage(BoatActionType.SAILS_IN);
clientToServerThread.sendBoatActionMessage(boatActionMessage);
break;
}
}
private void updateTeamList() {
ObservableList<Yacht> data = FXCollections.observableArrayList();
teamList.setItems(data);
boatNameCol.setCellValueFactory(
new PropertyValueFactory<>("boatName")
);
shortNameCol.setCellValueFactory(
new PropertyValueFactory<>("shortName")
);
countryCol.setCellValueFactory(
new PropertyValueFactory<>("country")
);
posCol.setCellValueFactory(
new PropertyValueFactory<>("position")
);
// if (StreamParser.isRaceStarted()) {
data.addAll(StreamParser.getBoatsPos().values());
// } else {
// for (Yacht boat : StreamParser.getBoats().values()) {
// boat.setPosition("-");
// data.add(boat);
// }
// }
teamList.refresh();
// posCol.setSortType(TableColumn.SortType.ASCENDING);
// teamList.getSortOrder().add(posCol);
// posCol.setSortable(false);
public void setClientToServerThread(ClientToServerThread ctt) {
clientToServerThread = ctt;
}
}
@@ -0,0 +1,97 @@
package seng302.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import seng302.client.ClientPacketParser;
import seng302.models.Yacht;
import seng302.models.stream.XMLParser.RaceXMLObject.Participant;
public class FinishScreenViewController implements Initializable {
@FXML
private GridPane finishScreenGridPane;
@FXML
private TableView<Yacht> finishOrderTable;
@FXML
private TableColumn<Yacht, String> posCol;
@FXML
private TableColumn<Yacht, String> boatNameCol;
@FXML
private TableColumn<Yacht, String> shortNameCol;
@FXML
private TableColumn<Yacht, String> countryCol;
@Override
public void initialize(URL location, ResourceBundle resources) {
finishScreenGridPane.getStylesheets()
.add(getClass().getResource("/css/master.css").toString());
finishOrderTable.getStylesheets().add(getClass().getResource("/css/master.css").toString());
// set up data for table
ObservableList<Yacht> data = FXCollections.observableArrayList();
finishOrderTable.setItems(data);
// setting table col data
posCol.setCellValueFactory(
new PropertyValueFactory<>("position")
);
boatNameCol.setCellValueFactory(
new PropertyValueFactory<>("boatName")
);
shortNameCol.setCellValueFactory(
new PropertyValueFactory<>("shortName")
);
countryCol.setCellValueFactory(
new PropertyValueFactory<>("country")
);
// check if the boat is racing
ArrayList<Participant> participants = ClientPacketParser.getXmlObject().getRaceXML()
.getParticipants();
ArrayList<Integer> participantIDs = new ArrayList<>();
for (Participant p : participants) {
participantIDs.add(p.getsourceID());
}
// add data to table
for (Yacht boat : ClientPacketParser.getBoatsPos().values()) {
if (participantIDs.contains(boat.getSourceId())) {
data.add(boat);
}
}
finishOrderTable.refresh();
}
private void setContentPane(String jfxUrl) {
try {
// get the main controller anchor pane (FinishView -> MainView)
AnchorPane contentPane = (AnchorPane) finishScreenGridPane.getParent();
contentPane.getChildren().removeAll();
contentPane.getChildren().clear();
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
contentPane.getChildren()
.addAll((Pane) FXMLLoader.load(getClass().getResource(jfxUrl)));
} catch (javafx.fxml.LoadException e) {
System.out.println("[Controller] FXML load exception");
} catch (IOException e) {
System.out.println("[Controller] IO exception");
}
}
public void switchToStartScreenView() {
setContentPane("/views/StartScreenView.fxml");
}
}
@@ -0,0 +1,235 @@
package seng302.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.text.Text;
import seng302.client.ClientState;
import seng302.client.ClientStateQueryingRunnable;
import seng302.gameServer.GameStages;
import seng302.gameServer.GameState;
import seng302.gameServer.MainServerThread;
/**
* A class describing the actions of the lobby screen
* Created by wmu16 on 10/07/17.
*/
public class LobbyController implements Initializable, Observer{
@FXML
private GridPane lobbyScreen;
@FXML
private Text lobbyIpText;
@FXML
private Button readyButton;
@FXML
private ListView firstListView;
@FXML
private ListView secondListView;
@FXML
private ListView thirdListView;
@FXML
private ListView fourthListView;
@FXML
private ListView fifthListView;
@FXML
private ListView sixthListView;
@FXML
private ListView seventhListView;
@FXML
private ListView eighthListView;
@FXML
private ImageView firstImageView;
@FXML
private ImageView secondImageView;
@FXML
private ImageView thirdImageView;
@FXML
private ImageView fourthImageView;
@FXML
private ImageView fifthImageView;
@FXML
private ImageView sixthImageView;
@FXML
private ImageView seventhImageView;
@FXML
private ImageView eighthImageView;
private static List<ObservableList<String>> competitors = new ArrayList<>();
private static ObservableList<String> firstCompetitor = FXCollections.observableArrayList();
private static ObservableList<String> secondCompetitor = FXCollections.observableArrayList();
private static ObservableList<String> thirdCompetitor = FXCollections.observableArrayList();
private static ObservableList<String> fourthCompetitor = FXCollections.observableArrayList();
private static ObservableList<String> fifthCompetitor = FXCollections.observableArrayList();
private static ObservableList<String> sixthCompetitor = FXCollections.observableArrayList();
private static ObservableList<String> seventhCompetitor = FXCollections.observableArrayList();
private static ObservableList<String> eighthCompetitor = FXCollections.observableArrayList();
private ClientStateQueryingRunnable clientStateQueryingRunnable;
private static List<ImageView> imageViews;
private static List<ListView> listViews;
private int MAX_NUM_PLAYERS = 8;
private Boolean switchedPane = false;
private MainServerThread mainServerThread;
private Controller controller;
private void setContentPane(String jfxUrl) {
try {
AnchorPane contentPane = (AnchorPane) lobbyScreen.getParent();
contentPane.getChildren().removeAll();
contentPane.getChildren().clear();
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
contentPane.getChildren()
.addAll((Pane) FXMLLoader.load(getClass().getResource(jfxUrl)));
} catch (javafx.fxml.LoadException e) {
System.out.println("[Controller] FXML load exception");
} catch (IOException e) {
System.out.println("[Controller] IO exception");
} catch (NullPointerException e) {
// System.out.println("[Controller] Null Pointer Exception");
}
}
@Override
public void initialize(URL location, ResourceBundle resources) {
if (ClientState.isHost()) {
lobbyIpText.setText("Lobby Host IP: " + ClientState.getHostIp());
readyButton.setDisable(false);
}
else {
lobbyIpText.setText("Connected to IP: " + ClientState.getHostIp());
readyButton.setDisable(true);
}
// put all javafx objects in lists, so we can iterate though conveniently
imageViews = new ArrayList<>();
Collections.addAll(imageViews, firstImageView, secondImageView, thirdImageView, fourthImageView,
fifthImageView, sixthImageView, seventhImageView, eighthImageView);
listViews = new ArrayList<>();
Collections.addAll(listViews, firstListView, secondListView, thirdListView, fourthListView, fifthListView,
sixthListView, seventhListView, eighthListView);
competitors = new ArrayList<>();
Collections.addAll(competitors, firstCompetitor, secondCompetitor, thirdCompetitor,
fourthCompetitor, fifthCompetitor, sixthCompetitor, seventhCompetitor, eighthCompetitor);
initialiseListView();
initialiseImageView(); // parrot gif init
// set up client state query thread, so that when it receives the race-started packet
// it can switch to the race view
ClientStateQueryingRunnable clientStateQueryingRunnable = new ClientStateQueryingRunnable();
clientStateQueryingRunnable.addObserver(this);
Thread clientStateQueryingThread = new Thread(clientStateQueryingRunnable, "Client State querying thread");
clientStateQueryingThread.setDaemon(true);
clientStateQueryingThread.start();
}
/**
* Observers "ClientStateQueryingRunnable".
* When the clients state has been marked to "race start", the querying thread
* will notify this lobby to change the view
* @param o
* @param arg
*/
@Override
public void update(Observable o, Object arg) {
Platform.runLater(new Runnable() {
@Override
public void run() {
if (arg.equals("game started") && !switchedPane) {
switchToRaceView();
}
if (arg.equals(("update players"))) {
initialiseListView();
}
}
});
}
/**
* Reset all ListViews and ImageViews according to the current competitors
*/
private void initialiseListView() {
listViews.forEach(listView -> listView.getItems().clear());
imageViews.forEach(gif -> gif.setVisible(false));
competitors.forEach(ol -> ol.removeAll());
List<Integer> ids = new ArrayList<>(ClientState.getBoats().keySet());
for (int i = 0; i < ids.size(); i++) {
competitors.get(i).add(ClientState.getBoats().get(ids.get(i)).getBoatName());
listViews.get(i).setItems(competitors.get(i));
imageViews.get(i).setVisible(true);
}
}
/**
* Loads preset images into imageViews
*/
private void initialiseImageView() {
for (int i = 0; i < MAX_NUM_PLAYERS; i++) {
imageViews.get(i).setImage(new Image(getClass().getResourceAsStream("/pics/sail.png")));
}
// Image image1 = new Image(getClass().getResourceAsStream("/pics/sail.png"));
// firstImageView.setImage(image1);
// Image image2 = new Image(getClass().getResourceAsStream("/pics/sail.png"));
// secondImageView.setImage(image2);
// Image image3 = new Image(getClass().getResourceAsStream("/pics/sail.png"));
// thirdImageView.setImage(image3);
// Image image4 = new Image(getClass().getResourceAsStream("/pics/sail.png"));
// fourthImageView.setImage(image4);
// Image image5 = new Image(getClass().getResourceAsStream("/pics/sail.png"));
// fifthImageView.setImage(image5);
// Image image6 = new Image(getClass().getResourceAsStream("/pics/sail.png"));
// sixthImageView.setImage(image6);
// Image image7 = new Image(getClass().getResourceAsStream("/pics/sail.png"));
// seventhImageView.setImage(image7);
// Image image8 = new Image(getClass().getResourceAsStream("/pics/sail.png"));
// eighthImageView.setImage(image8);
}
@FXML
public void leaveLobbyButtonPressed() {
if (ClientState.isHost()) {
GameState.setCurrentStage(GameStages.CANCELLED);
mainServerThread.terminate();
}
ClientState.setConnectedToHost(false);
controller.setUpStartScreen();
}
@FXML
public void readyButtonPressed() {
GameState.setCurrentStage(GameStages.RACING);
mainServerThread.startGame();
}
private void switchToRaceView() {
if (!switchedPane) {
switchedPane = true;
setContentPane("/views/RaceView.fxml");
}
}
public void setMainServerThread(MainServerThread mainServerThread) {
this.mainServerThread = mainServerThread;
}
public void setController(Controller controller) {
this.controller = controller;
}
}
@@ -1,82 +0,0 @@
package seng302.controllers;
import seng302.models.Race;
import seng302.models.Yacht;
import seng302.models.parsers.ConfigParser;
import seng302.models.parsers.CourseParser;
import seng302.models.parsers.StreamParser;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Random;
/**
* Created by zyt10 on 17/03/17.
* run before CanvasController to initialize race events
* the CanvasController then uses the event data to make the animations
*/
public class RaceController {
Race race = null;
public void initializeRace() {
String raceConfigFile = "/config/config.xml";
String teamsConfigFile = "/config/teams.xml";
try {
race = createRace(raceConfigFile, teamsConfigFile);
} catch (Exception e) {
System.out.println("There was an error creating the race.");
}
if (race != null) {
race.startRace();
} else {
System.out.println("There was an error creating the race. Exiting.");
}
}
public Race createRace(String configFile, String teamsConfigFile) throws Exception {
Race race = new Race();
// StreamParser.xmlObject
// Read team names from file
// TeamsParser tp = new TeamsParser(teamsConfigFile);
// Read course from file
// ConfigParser config = new ConfigParser(configFile);
ArrayList<String> boatNames = new ArrayList<>();
// ArrayList<Boat> teams = tp.getBoats();
Map<Long, Yacht> teams = StreamParser.getBoatsPos();
//get race size
int numberOfBoats = teams.size();
//get time scale
// double timeScale = config.getTimeScale();
// race.setTimeScale(timeScale);
for (Yacht boat : teams.values()) {
boatNames.add(boat.getBoatName());
race.addBoat(boat);
}
// Shuffle team names
long seed = System.nanoTime();
Collections.shuffle(boatNames, new Random(seed));
if (numberOfBoats > Array.getLength(boatNames.toArray())) {
return null;
}
CourseParser course = new CourseParser("/config/course.xml");
race.addCourse(course.getCourse());
return race;
}
public Race getRace() {
return race;
}
}
@@ -1,37 +0,0 @@
package seng302.controllers;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import seng302.models.Race;
import java.net.URL;
import java.util.ResourceBundle;
/**
* Created by ptg19 on 20/03/17.
*/
public class RaceResultController implements Initializable{
@FXML private AnchorPane window;
@FXML private VBox resultsVBox;
private Race race;
RaceResultController(Race race){
this.race = race;
}
@Override
public void initialize(URL location, ResourceBundle resources) {
int boatPosition = this.race.getFinishedBoats().length;
for (int i = this.race.getFinishedBoats().length - 1; i >= 0; i--){
resultsVBox.getChildren().add(0, new Text(boatPosition + ": " + this.race.getFinishedBoats()[i].getBoatName()));
boatPosition--;
}
}
}
@@ -1,35 +1,62 @@
package seng302.controllers;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Slider;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Line;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import javafx.util.StringConverter;
import seng302.client.ClientPacketParser;
import seng302.utilities.GeoUtility;
import seng302.controllers.annotations.Annotation;
import seng302.controllers.annotations.ImportantAnnotationController;
import seng302.controllers.annotations.ImportantAnnotationDelegate;
import seng302.controllers.annotations.ImportantAnnotationsState;
import seng302.fxObjects.BoatGroup;
import seng302.fxObjects.MarkGroup;
import seng302.models.*;
import seng302.models.parsers.ConfigParser;
import seng302.models.parsers.StreamParser;
import seng302.models.mark.GateMark;
import seng302.models.mark.Mark;
import seng302.models.mark.SingleMark;
import seng302.models.stream.XMLParser;
import java.io.IOException;
import java.util.*;
import seng302.models.stream.XMLParser.RaceXMLObject.Participant;
import java.util.stream.Collectors;
/**
* Created by ptg19 on 29/03/17.
*/
public class RaceViewController extends Thread{
public class RaceViewController extends Thread implements ImportantAnnotationDelegate {
@FXML
private Text windSpeedText;
@FXML
private LineChart raceSparkLine;
@FXML
private NumberAxis sparklineYAxis;
@FXML
private VBox positionVbox;
@FXML
@@ -43,64 +70,105 @@ public class RaceViewController extends Thread{
@FXML
private Slider annotationSlider;
@FXML
private Button selectAnnotationBtn;
@FXML
private ComboBox boatSelectionComboBox;
@FXML
private CanvasController includedCanvasController;
private ArrayList<Yacht> startingBoats = new ArrayList<>();
private static ArrayList<Yacht> startingBoats = new ArrayList<>();
private boolean displayFps;
private Timeline timerTimeline;
private Map<Yacht, TimelineInfo> timelineInfos = new HashMap<>();
private ArrayList<Yacht> boatOrder = new ArrayList<>();
private Race race;
private Stage stage;
private static HashMap<Integer, Series<String, Double>> sparkLineData = new HashMap<>();
private static ArrayList<Yacht> racingBoats = new ArrayList<>();
private ImportantAnnotationsState importantAnnotations;
private Yacht selectedBoat;
public void initialize() {
// Load a default important annotation state
importantAnnotations = new ImportantAnnotationsState();
RaceController raceController = new RaceController();
raceController.initializeRace();
race = raceController.getRace();
for (Yacht boat : race.getBoats()) {
startingBoats.add(boat);
}
// try{
// initializeTimelines();
// }
// catch (Exception e){
// e.printStackTrace();
// }
//Formatting the y axis of the sparkline
raceSparkLine.getYAxis().setRotate(180);
raceSparkLine.getYAxis().setTickLabelRotation(180);
raceSparkLine.getYAxis().setTranslateX(-5);
raceSparkLine.getYAxis().setAutoRanging(false);
sparklineYAxis.setTickMarkVisible(false);
startingBoats = new ArrayList<>(ClientPacketParser.getBoats().values());
includedCanvasController.setup(this);
includedCanvasController.initializeCanvas();
initializeTimer();
initializeSettings();
initialiseWindDirection();
initialisePositionVBox();
//set wind direction!!!!!!! can't find another place to put my code --haoming
// double windDirection = new ConfigParser("/config/config.xml").getWindDirection();
// windDirectionText.setText(String.format("%.1f°", windDirection));
// windArrowText.setRotate(windDirection);
initializeUpdateTimer();
initialiseFPSCheckBox();
initialiseAnnotationSlider();
initialiseBoatSelectionComboBox();
includedCanvasController.timer.start();
selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView());
}
/**
* The important annotations have been changed, update this view
*
* @param importantAnnotationsState The current state of the selected annotations
*/
public void importantAnnotationsChanged(ImportantAnnotationsState importantAnnotationsState) {
this.importantAnnotations = importantAnnotationsState;
setAnnotations((int) annotationSlider.getValue()); // Refresh the displayed annotations
}
private void initializeSettings() {
/**
* Loads the "select annotations" view in a new window
*/
private void loadSelectAnnotationView() {
try {
FXMLLoader fxmlLoader = new FXMLLoader();
Stage stage = new Stage();
// Set controller
ImportantAnnotationController controller = new ImportantAnnotationController(this,
stage);
fxmlLoader.setController(controller);
// Load FXML and set CSS
fxmlLoader
.setLocation(getClass().getResource("/views/importantAnnotationSelectView.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 469, 298);
scene.getStylesheets().add(getClass().getResource("/css/master.css").toString());
stage.initStyle(StageStyle.UNDECORATED);
stage.setScene(scene);
stage.show();
controller.loadState(importantAnnotations);
} catch (IOException e) {
System.out.println("[RaceViewController] IO exception");
}
}
private void initialiseFPSCheckBox() {
displayFps = true;
toggleFps.selectedProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
displayFps = !displayFps;
toggleFps.selectedProperty().addListener(
(observable, oldValue, newValue) -> displayFps = !displayFps);
}
});
//SLIFER STUFF BELOW
private void initialiseAnnotationSlider() {
annotationSlider.setLabelFormatter(new StringConverter<Double>() {
@Override
public String toString(Double n) {
if (n == 0) return "None";
if (n == 1) return "Low";
if (n == 2) return "Medium";
if (n == 3) return "All";
if (n == 0) {
return "None";
}
if (n == 1) {
return "Important";
}
if (n == 2) {
return "All";
}
return "All";
}
@@ -110,15 +178,13 @@ public class RaceViewController extends Thread{
switch (s) {
case "None":
return 0d;
case "Low":
case "Important":
return 1d;
case "Medium":
return 2d;
case "All":
return 3d;
return 2d;
default:
return 3d;
return 2d;
}
}
});
@@ -126,22 +192,102 @@ public class RaceViewController extends Thread{
annotationSlider.valueProperty().addListener((obs, oldval, newVal) ->
setAnnotations((int) annotationSlider.getValue()));
annotationSlider.setValue(3);
annotationSlider.setValue(2);
}
private void initializeTimer(){
/**
* Used to add any new boats into the race that may have started late or not have had data received yet
*/
void updateSparkLine(){
// Collect the racing boats that aren't already in the chart
ArrayList<Yacht> sparkLineCandidates = startingBoats.stream().filter(yacht -> !sparkLineData.containsKey(yacht.getSourceId())
&& yacht.getPosition() != null & yacht.getPosition() != "-").collect(Collectors.toCollection(ArrayList::new));
// Obtain the qualifying boats to set the max on the Y axis
racingBoats = startingBoats.stream().filter(yacht ->
yacht.getPosition() != null & yacht.getPosition() != "-").collect(Collectors.toCollection(ArrayList::new));
sparklineYAxis.setUpperBound(racingBoats.size() + 1);
// Create a new data series for new boats
sparkLineCandidates.stream().filter(yacht -> yacht.getPosition() != null).forEach(yacht -> {
Series<String, Double> yachtData = new Series<>();
yachtData.setName(yacht.getBoatName());
yachtData.getData().add(new XYChart.Data<>(Integer.toString(yacht.getLegNumber()), 1 + racingBoats.size() - Double.parseDouble(yacht.getPosition())));
sparkLineData.put(yacht.getSourceId(), yachtData);
});
// Lambda function to sort the series in order of leg (later legs shown more to the right)
List<XYChart.Series<String, Double>> positions = new ArrayList<>(sparkLineData.values());
Collections.sort(positions, (o1, o2) -> {
Integer leg1 = Integer.parseInt(o1.getData().get(o1.getData().size()-1).getXValue());
Integer leg2 = Integer.parseInt(o2.getData().get(o2.getData().size()-1).getXValue());
if (leg2 < leg1){
return 1;
} else {
return -1;
}
});
// Adds the new data series to the sparkline (and set the colour of the series)
raceSparkLine.setCreateSymbols(false);
positions.stream().filter(spark -> !raceSparkLine.getData().contains(spark)).forEach(spark -> {
raceSparkLine.getData().add(spark);
spark.getNode().lookup(".chart-series-line").setStyle("-fx-stroke:" + getBoatColorAsRGB(spark.getName()));
});
}
/**
* Updates the yachts sparkline of the desired boat and using the new leg number
* @param yacht The yacht to be updated on the sparkline
* @param legNumber the leg number that the position will be assigned to
*/
public static void updateYachtPositionSparkline(Yacht yacht, Integer legNumber){
XYChart.Series<String, Double> positionData = sparkLineData.get(yacht.getSourceId());
positionData.getData().add(new XYChart.Data<>(Integer.toString(legNumber), 1 + racingBoats.size() - Double.parseDouble(yacht.getPosition())));
}
/**
* gets the rgb string of the boats colour to use for the chart via css
* @param boatName boat passed in to get the boats colour
* @return the colour as an rgb string
*/
private String getBoatColorAsRGB(String boatName){
Color color = Color.WHITE;
for (Yacht yacht: startingBoats){
if (Objects.equals(yacht.getBoatName(), boatName)){
color = yacht.getColour();
}
}
if (color == null){
return String.format( "#%02X%02X%02X",255,255,255);
}
return String.format( "#%02X%02X%02X",
(int)( color.getRed() * 255 ),
(int)( color.getGreen() * 255 ),
(int)( color.getBlue() * 255 ) );
}
/**
* Initalises a timer which updates elements of the RaceView such as wind direction, boat
* orderings etc.. which are dependent on the info from the stream parser constantly.
* Updates of each of these attributes are called ONCE EACH SECOND
*/
private void initializeUpdateTimer() {
timerTimeline = new Timeline();
timerTimeline.setCycleCount(Timeline.INDEFINITE);
// Run timer update every second
timerTimeline.getKeyFrames().add(
new KeyFrame(Duration.seconds(1),
event -> {
if (StreamParser.isRaceFinished()) {
timerLabel.setFill(Color.RED);
timerLabel.setText("Race Finished!");
} else {
timerLabel.setText(currentTimer());
}
updateRaceTime();
updateWindDirection();
// updateOrder();
updateBoatSelectionComboBox();
updateOrder();
})
);
@@ -149,130 +295,257 @@ public class RaceViewController extends Thread{
timerTimeline.playFromStart();
}
private void initialiseWindDirection() {
Timeline windDirTimeline = new Timeline();
windDirTimeline.setCycleCount(Timeline.INDEFINITE);
windDirTimeline.getKeyFrames().add(
new KeyFrame(Duration.seconds(1),
event -> {
windDirectionText.setText(String.format("%.1f°", StreamParser.getWindDirection()));
windArrowText.setRotate(StreamParser.getWindDirection());
})
);
windDirTimeline.playFromStart();
}
private void initialisePositionVBox() {
Timeline posVBoxTimeline = new Timeline();
posVBoxTimeline.setCycleCount(Timeline.INDEFINITE);
posVBoxTimeline.getKeyFrames().add(
new KeyFrame(Duration.seconds(1),
event -> {
showOrder();
})
);
posVBoxTimeline.playFromStart();
}
/**
* Generates time line for each boat, and stores time time into timelineInfos hash map
* Iterates over all corners until ones SeqID matches with the boats current leg number.
* Then it gets the compoundMarkID of that corner and uses it to fetch the appropriate mark
* Returns null if no next mark found.
* @param bg The BoatGroup to find the next mark of
* @return The next Mark or null if none found
*/
private void initializeTimelines() {
HashMap<Yacht, List> boat_events = race.getEvents();
for (Yacht boat : boat_events.keySet()) {
startingBoats.add(boat);
// // x, y are the real time coordinates
// DoubleProperty x = new SimpleDoubleProperty();
// DoubleProperty y = new SimpleDoubleProperty();
//
// List<KeyFrame> keyFrames = new ArrayList<>();
// List<Event> events = boat_events.get(boat);
//
// // iterates all events and convert each event to keyFrame, then add them into a list
// for (Event event : events) {
// if (event.getIsFinishingEvent()) {
// keyFrames.add(
// new KeyFrame(Duration.seconds(event.getTime()),
// onFinished -> {race.setBoatFinished(boat); handleEvent(event);},
// new KeyValue(x, event.getThisMark().getLatitude()),
// new KeyValue(y, event.getThisMark().getLongitude())
// )
// );
// } else {
// keyFrames.add(
// new KeyFrame(Duration.seconds(event.getTime()),
// onFinished ->{
// handleEvent(event);
// boat.setHeading(event.getBoatHeading());
// },
// new KeyValue(x, event.getThisMark().getLatitude()),
// new KeyValue(y, event.getThisMark().getLongitude())
// )
// );
// }
// }
// timelineInfos.put(boat, new TimelineInfo(new Timeline(keyFrames.toArray(new KeyFrame[keyFrames.size()])), x, y));
}
setRaceDuration();
private Mark getNextMark(BoatGroup bg) {
Integer legNumber = bg.getBoat().getLegNumber();
List<XMLParser.RaceXMLObject.Corner> markSequence = ClientPacketParser.getXmlObject()
.getRaceXML().getCompoundMarkSequence();
if (legNumber == 0) {
return null;
} else if (legNumber == markSequence.size() - 1) {
return null;
}
private void setRaceDuration(){
Double maxDuration = 0.0;
Timeline maxTimeline = null;
for (XMLParser.RaceXMLObject.Corner corner : markSequence) {
if (legNumber + 2 == corner.getSeqID()) {
Integer thisCompoundMarkID = corner.getCompoundMarkID();
for (TimelineInfo timelineInfo : timelineInfos.values()) {
Timeline timeline = timelineInfo.getTimeline();
if (timeline.getTotalDuration().toMillis() >= maxDuration) {
maxDuration = timeline.getTotalDuration().toMillis();
maxTimeline = timeline;
for (Mark mark : ClientPacketParser.getXmlObject().getRaceXML()
.getAllCompoundMarks()) {
if (mark.getCompoundMarkID() == thisCompoundMarkID) {
return mark;
}
}
}
}
// Timelines are paused by default
timeline.play();
timeline.pause();
return null;
}
maxTimeline.setOnFinished(event -> {
race.setRaceFinished();
loadRaceResultView();
/**
* Updates the wind direction arrow and text as from info from the ClientPacketParser
*/
private void updateWindDirection() {
windDirectionText.setText(String.format("%.1f°", ClientPacketParser.getWindDirection()));
windArrowText.setRotate(ClientPacketParser.getWindDirection());
windSpeedText.setText(String.format("%.1f Knots", ClientPacketParser.getWindSpeed()));
}
/**
* Updates the clock for the race
*/
private void updateRaceTime() {
if (ClientPacketParser.isRaceFinished()) {
timerLabel.setFill(Color.RED);
timerLabel.setText("Race Finished!");
} else {
timerLabel.setText(getTimeSinceStartOfRace());
}
}
/**
* Grabs the boats currently in the race as from the ClientPacketParser and sets them to be selectable
* in the boat selection combo box
*/
private void updateBoatSelectionComboBox() {
ObservableList<Yacht> observableBoats = FXCollections
.observableArrayList(ClientPacketParser.getBoatsPos().values());
boatSelectionComboBox.setItems(observableBoats);
}
/**
* Updates the order of the boats as from the ClientPacketParser and sets them in the boat order
* section
*/
private void updateOrder() {
positionVbox.getChildren().clear();
positionVbox.getChildren().removeAll();
positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString());
// list of racing boat id
ArrayList<Participant> participants = ClientPacketParser.getXmlObject().getRaceXML()
.getParticipants();
ArrayList<Integer> participantIDs = new ArrayList<>();
for (Participant p : participants) {
participantIDs.add(p.getsourceID());
}
if (ClientPacketParser.isRaceStarted()) {
/*
for (Yacht boat : ClientPacketParser.getBoatsPos().values()) {
System.out.println("Hi tjere" + boat.getBoatName());
if (participantIDs.contains(boat.getSourceId()) || true
) { // check if the boat is racing
if (boat.getBoatStatus() == 69) { // 3 is finish status
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " (Finished)");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
positionVbox.getChildren().add(textToAdd);
} else {
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " ");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
textToAdd.setStyle("");
positionVbox.getChildren().add(textToAdd);
System.out.println("Adding " + textToAdd.getText());
}
}
}
*/
for (Yacht boat : ClientPacketParser.getBoats().values()){
Text textToAdd = new Text(boat.getSourceId() + ". " + boat.getShortName() + " ");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
textToAdd.setStyle("");
positionVbox.getChildren().add(textToAdd);
}
} else {
for (Yacht boat : ClientPacketParser.getBoats().values()) {
if (participantIDs.contains(boat.getSourceId())) { // check if the boat is racing
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " ");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
textToAdd.setStyle("");
positionVbox.getChildren().add(textToAdd);
}
}
}
}
private void updateLaylines(BoatGroup bg) {
Mark nextMark = getNextMark(bg);
Boolean isUpwind = null;
// Can only calc leg direction if there is a next mark and it is a gate mark
if (nextMark != null) {
if (nextMark instanceof GateMark) {
if (bg.isUpwindLeg(includedCanvasController, nextMark)) {
isUpwind = true;
} else {
isUpwind = false;
}
for(MarkGroup mg : includedCanvasController.getMarkGroups()) {
mg.removeLaylines();
if (mg.getMainMark().getId() == nextMark.getId()) {
SingleMark singleMark1 = ((GateMark) nextMark).getSingleMark1();
SingleMark singleMark2 = ((GateMark) nextMark).getSingleMark2();
Point2D markPoint1 = includedCanvasController.findScaledXY(singleMark1.getLatitude(), singleMark1.getLongitude());
Point2D markPoint2 = includedCanvasController.findScaledXY(singleMark2.getLatitude(), singleMark2.getLongitude());
HashMap<Double, Double> angleAndSpeed;
if (isUpwind) {
angleAndSpeed = PolarTable
.getOptimalUpwindVMG(ClientPacketParser.getWindSpeed());
} else {
angleAndSpeed = PolarTable
.getOptimalDownwindVMG(ClientPacketParser.getWindSpeed());
}
Double resultingAngle = angleAndSpeed.keySet().iterator().next();
Point2D boatCurrentPos = new Point2D(bg.getBoatLayoutX(), bg.getBoatLayoutY());
Point2D gateMidPoint = markPoint1.midpoint(markPoint2);
Integer lineFuncResult = GeoUtility.lineFunction(boatCurrentPos, gateMidPoint, markPoint2);
Line rightLayline = new Line();
Line leftLayline = new Line();
if (lineFuncResult == 1) {
rightLayline = makeRightLayline(markPoint2, 180 - resultingAngle,
ClientPacketParser
.getWindDirection());
leftLayline = makeLeftLayline(markPoint1, 180 - resultingAngle,
ClientPacketParser
.getWindDirection());
} else if (lineFuncResult == -1) {
rightLayline = makeRightLayline(markPoint1, 180 - resultingAngle,
ClientPacketParser
.getWindDirection());
leftLayline = makeLeftLayline(markPoint2, 180 - resultingAngle,
ClientPacketParser
.getWindDirection());
}
leftLayline.setStrokeWidth(0.5);
leftLayline.setStroke(bg.getBoat().getColour());
rightLayline.setStrokeWidth(0.5);
rightLayline.setStroke(bg.getBoat().getColour());
bg.setLaylines(leftLayline, rightLayline);
mg.addLaylines(leftLayline, rightLayline);
}
}
}
}
}
private Point2D getPointRotation(Point2D ref, Double distance, Double angle){
Double newX = ref.getX() + (ref.getX() + distance -ref.getX())*Math.cos(angle) - (ref.getY() + distance -ref.getY())*Math.sin(angle);
Double newY = ref.getY() + (ref.getX() + distance -ref.getX())*Math.sin(angle) + (ref.getY() + distance -ref.getY())*Math.cos(angle);
return new Point2D(newX, newY);
}
public Line makeLeftLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) {
Point2D ep = getPointRotation(startPoint, 50.0, baseAngle + layLineAngle);
Line line = new Line(startPoint.getX(), startPoint.getY(), ep.getX(), ep.getY());
return line;
}
public Line makeRightLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) {
Point2D ep = getPointRotation(startPoint, 50.0, baseAngle - layLineAngle);
Line line = new Line(startPoint.getX(), startPoint.getY(), ep.getX(), ep.getY());
return line;
}
/**
* Initialised the combo box with any boats currently in the race and adds the required listener
* for the combobox to take action upon selection
*/
private void initialiseBoatSelectionComboBox() {
updateBoatSelectionComboBox();
boatSelectionComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
//This listener is fired whenever the combo box changes. This means when the values are updated
//We dont want to set the selected value if the values are updated but nothing clicked (null)
if (newValue != null && newValue != selectedBoat) {
Yacht thisYacht = (Yacht) newValue;
setSelectedBoat(thisYacht);
}
});
}
/**
* Play each boats timerTimeline
*/
public void playTimelines(){
for (TimelineInfo timelineInfo : timelineInfos.values()){
Timeline timeline = timelineInfo.getTimeline();
if (timeline.getStatus() == Animation.Status.PAUSED){
timeline.play();
}
}
}
/**
* Pause each boats timerTimeline
*/
public void pauseTimelines(){
for (TimelineInfo timelineInfo : timelineInfos.values()){
Timeline timeline = timelineInfo.getTimeline();
if (timeline.getStatus() == Animation.Status.RUNNING){
timeline.pause();
}
}
}
/**
* Display the list of boats in the order they finished the race
*/
private void loadRaceResultView() {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/views/FinishView.fxml"));
loader.setController(new RaceResultController(race));
try {
contentAnchorPane.getChildren().removeAll();
@@ -286,40 +559,6 @@ public class RaceViewController extends Thread{
}
}
public void handleEvent(Event event) {
Yacht boat = event.getBoat();
boatOrder.remove(boat);
boat.setMarkLastPast(event.getMarkPosInRace());
boatOrder.add(boat);
boatOrder.sort(new Comparator<Yacht>() {
@Override
public int compare(Yacht b1, Yacht b2) {
return b2.getMarkLastPast() - b1.getMarkLastPast();
}
});
showOrder();
}
private void showOrder() {
positionVbox.getChildren().clear();
positionVbox.getChildren().removeAll();
// for (Boat boat : boatOrder) {
// positionVbox.getChildren().add(new Text(boat.getShortName() + " " + boat.getSpeedInKnots() + " Knots"));
// }
for (Yacht boat : StreamParser.getBoatsPos().values()) {
// System.out.println(boat.getBoatStatus());
if (boat.getBoatStatus() == 3) { // 3 is finish status
positionVbox.getChildren().add(new Text(boat.getPosition() + ". " +
boat.getShortName() + " (Finished)"));
} else {
positionVbox.getChildren().add(new Text(boat.getPosition() + ". " +
boat.getShortName() + " "));
}
}
}
/**
* Convert seconds to a string of the format mm:ss
@@ -334,18 +573,18 @@ public class RaceViewController extends Thread{
return String.format("%02d:%02d", time / 60, time % 60);
}
private String currentTimer() {
private String getTimeSinceStartOfRace() {
String timerString = "0:00";
if (StreamParser.getTimeSinceStart() > 0) {
String timerMinute = Long.toString(StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(StreamParser.getTimeSinceStart() % 60);
if (ClientPacketParser.getTimeSinceStart() > 0) {
String timerMinute = Long.toString(ClientPacketParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(ClientPacketParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
}
timerString = "-" + timerMinute + ":" + timerSecond;
} else {
String timerMinute = Long.toString(-1 * StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(-1 * StreamParser.getTimeSinceStart() % 60);
String timerMinute = Long.toString(-1 * ClientPacketParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(-1 * ClientPacketParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
}
@@ -354,76 +593,58 @@ public class RaceViewController extends Thread{
return timerString;
}
public void stopTimer() {
timerTimeline.stop();
}
public void startTimer() {
timerTimeline.play();
}
public boolean isDisplayFps() {
boolean isDisplayFps() {
return displayFps;
}
public Race getRace() {
return race;
}
public Map<Yacht, TimelineInfo> getTimelineInfos() {
return timelineInfos;
}
public ArrayList<Yacht> getStartingBoats(){
return startingBoats;
}
private void setAnnotations(Integer annotationLevel) {
switch (annotationLevel) {
// No Annotations
case 0:
for (RaceObject ro : includedCanvasController.getRaceObjects()) {
if(ro instanceof BoatGroup) {
BoatGroup bg = (BoatGroup) ro;
bg.setTeamNameObjectVisible(false);
bg.setVelocityObjectVisible(false);
bg.setLineGroupVisible(false);
bg.setWakeVisible(false);
}
for (BoatGroup bg : includedCanvasController.getBoatGroups()) {
bg.setVisibility(false, false, false, false, false, false);
}
break;
// Important Annotations
case 1:
for (RaceObject ro : includedCanvasController.getRaceObjects()) {
if(ro instanceof BoatGroup) {
BoatGroup bg = (BoatGroup) ro;
bg.setTeamNameObjectVisible(true);
bg.setVelocityObjectVisible(false);
bg.setLineGroupVisible(false);
bg.setWakeVisible(false);
}
for (BoatGroup bg : includedCanvasController.getBoatGroups()) {
bg.setVisibility(
importantAnnotations.getAnnotationState(Annotation.NAME),
importantAnnotations.getAnnotationState(Annotation.SPEED),
importantAnnotations.getAnnotationState(Annotation.ESTTIMETONEXTMARK),
importantAnnotations.getAnnotationState(Annotation.LEGTIME),
importantAnnotations.getAnnotationState(Annotation.TRACK),
importantAnnotations.getAnnotationState(Annotation.WAKE)
);
}
break;
// All Annotations
case 2:
for (RaceObject ro : includedCanvasController.getRaceObjects()) {
if(ro instanceof BoatGroup) {
BoatGroup bg = (BoatGroup) ro;
bg.setTeamNameObjectVisible(true);
bg.setVelocityObjectVisible(false);
bg.setLineGroupVisible(true);
bg.setWakeVisible(false);
}
for (BoatGroup bg : includedCanvasController.getBoatGroups()) {
bg.setVisibility(true, true, true, true, true, true);
}
break;
case 3:
for (RaceObject ro : includedCanvasController.getRaceObjects()) {
if(ro instanceof BoatGroup) {
BoatGroup bg = (BoatGroup) ro;
bg.setTeamNameObjectVisible(true);
bg.setVelocityObjectVisible(true);
bg.setLineGroupVisible(true);
bg.setWakeVisible(true);
}
}
break;
/**
* Sets all the annotations of the selected boat to be visible and all others to be hidden
*
* @param yacht The yacht for which we want to view all annotations
*/
private void setSelectedBoat(Yacht yacht) {
for (BoatGroup bg : includedCanvasController.getBoatGroups()) {
//We need to iterate over all race groups to get the matching boat group belonging to this boat if we
//are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup.
if (bg.getBoat().getHullID().equals(yacht.getHullID())) {
updateLaylines(bg);
bg.setIsSelected(true);
selectedBoat = yacht;
} else {
bg.setIsSelected(false);
}
}
}
@@ -434,4 +655,14 @@ public class RaceViewController extends Thread{
Stage getStage() {
return stage;
}
/**
* Used for when the boat attempts to add data to the sparkline (first checks if the sparkline contains info on it)
* @param yachtId
* @return
*/
public static boolean sparkLineStatus(Integer yachtId) {
return sparkLineData.containsKey(yachtId);
}
}
@@ -0,0 +1,164 @@
package seng302.controllers;
import java.net.Inet4Address;
import java.net.NetworkInterface;
import java.util.Enumeration;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import seng302.client.ClientState;
import seng302.client.ClientToServerThread;
import seng302.gameServer.GameState;
import seng302.gameServer.MainServerThread;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* A Class describing the actions of the start screen controller
* Created by wmu16 on 10/07/17.
*/
public class StartScreenController {
@FXML
private TextField ipTextField;
@FXML
private TextField portTextField;
@FXML
private GridPane startScreen2;
private Controller controller;
/**
* Loads the fxml content into the parent pane
* @param jfxUrl
* @return the controller of the fxml
*/
private Object setContentPane(String jfxUrl) {
try {
AnchorPane contentPane = (AnchorPane) startScreen2.getParent();
contentPane.getChildren().removeAll();
contentPane.getChildren().clear();
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(jfxUrl));
contentPane.getChildren().addAll((Pane) fxmlLoader.load());
return fxmlLoader.getController();
} catch (javafx.fxml.LoadException e) {
System.out.println("[Controller] FXML load exception");
} catch (IOException e) {
System.out.println("[Controller] IO exception");
}
return null;
}
/**
* ATTEMPTS TO:
* Sets up a new game state with your IP address as designated as the host.
* Starts a thread to listen for incoming connections.
* Starts a client to server thread and connects to own ip.
* Switches to the lobby screen
*/
@FXML
public void hostButtonPressed() {
try {
String ipAddress = InetAddress.getLocalHost().getHostAddress();
// get the lobby controller so that we can pass the game server thread to it
new GameState(getLocalHostIp());
MainServerThread mainServerThread = new MainServerThread();
ClientState.setHost(true);
// host will connect and handshake to itself after setting up the server
// TODO: 24/07/17 wmu16 - Make port number some static global type constant?
ClientToServerThread clientToServerThread = new ClientToServerThread(ClientState.getHostIp(), 4942);
ClientState.setConnectedToHost(true);
controller.setClientToServerThread(clientToServerThread);
LobbyController lobbyController = (LobbyController) setContentPane("/views/LobbyView.fxml");
lobbyController.setMainServerThread(mainServerThread);
lobbyController.setController(controller);
} catch (Exception e) {
Alert alert = new Alert(AlertType.ERROR);
alert.setHeaderText("Cannot host");
alert.setContentText("Oops, failed to host, try to restart.");
alert.showAndWait();
}
}
/**
* ATTEMPTS TO:
* Connect to an ip address and port using the ip and port specified on start screen.
* Starts a Client To Server Thread to maintain connection to host.
* Switch view to lobby view.
*/
@FXML
public void connectButtonPressed() {
// TODO: 10/07/17 wmu16 - Finish function
try {
String ipAddress = ipTextField.getText().trim().toLowerCase();
Integer port = Integer.valueOf(portTextField.getText().trim());
ClientToServerThread clientToServerThread = new ClientToServerThread(ipAddress, port);
ClientState.setHost(false);
ClientState.setConnectedToHost(true);
ClientState.setHostIp(ipAddress);
controller.setClientToServerThread(clientToServerThread);
LobbyController lobbyController = (LobbyController) setContentPane("/views/LobbyView.fxml");
lobbyController.setController(controller);
} catch (Exception e) {
Alert alert = new Alert(AlertType.ERROR);
alert.setHeaderText("Cannot reach the host");
alert.setContentText("Please check your host IP address.");
alert.showAndWait();
}
}
public void setController(Controller controller) {
this.controller = controller;
}
/**
* Gets the local host ip address and sets this ip to ClientState.
* Only runs by the host.
*
* @return the localhost ip address
*/
private String getLocalHostIp() {
String ipAddress = null;
try {
Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
while (e.hasMoreElements()) {
NetworkInterface ni = e.nextElement();
if (ni.isLoopback())
continue;
if(ni.isPointToPoint())
continue;
if(ni.isVirtual())
continue;
Enumeration<InetAddress> addresses = ni.getInetAddresses();
while(addresses.hasMoreElements()) {
InetAddress address = addresses.nextElement();
if(address instanceof Inet4Address) { // skip all ipv6
ipAddress = address.getHostAddress();
}
}
}
} catch (Exception e) {
System.out.println("[StartScreenController] Exception");
}
if (ipAddress == null) {
System.out.println("[HOST] Cannot obtain local host ip address.");
}
ClientState.setHostIp(ipAddress);
return ipAddress;
}
}
@@ -0,0 +1,13 @@
package seng302.controllers.annotations;
/**
* Annotations the user can select as important
*/
public enum Annotation {
SPEED,
WAKE,
TRACK,
NAME,
ESTTIMETONEXTMARK,
LEGTIME
}
@@ -0,0 +1,145 @@
package seng302.controllers.annotations;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import java.net.URL;;
import java.util.ResourceBundle;
public class ImportantAnnotationController implements Initializable {
/*
* JavaFX Outlets
*/
@FXML
private CheckBox boatWakeSelect;
@FXML
private CheckBox boatSpeedSelect;
@FXML
private CheckBox boatTrackSelect;
@FXML
private CheckBox boatNameSelect;
@FXML
private CheckBox boatEstTimeToNextMarkSelect;
@FXML
private CheckBox boatElapsedTimeSelect;
@FXML
private AnchorPane annotationSelectWindow;
@FXML
private Button closeButton;
private ImportantAnnotationDelegate delegate;
private ImportantAnnotationsState importantAnnotationsState;
private Stage stage;
public ImportantAnnotationController(ImportantAnnotationDelegate delegate, Stage stage) {
this.delegate = delegate;
importantAnnotationsState = new ImportantAnnotationsState();
this.stage = stage;
}
/**
* Sets whether or not an annotation is considered important, then
* sends an update to the delegate
*
* @param annotation The annotation
* @param isSet True if annotation is important
*/
private void setAnnotation(Annotation annotation, Boolean isSet) {
importantAnnotationsState.setAnnotationState(annotation, isSet);
sendUpdate();
}
/**
* Sends an update to the delegate when the important
* annotations have changed
*/
private void sendUpdate() {
this.delegate.importantAnnotationsChanged(importantAnnotationsState);
}
/**
* Load the current state of the 'important annotations'
*
* @param currentState hashmap containing the states of each annotation
*/
public void loadState(ImportantAnnotationsState currentState) {
this.importantAnnotationsState = currentState;
// Initialise checkboxes
for (Annotation annotation : importantAnnotationsState.getAnnotations()) {
switch (annotation) {
case WAKE:
boatWakeSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case SPEED:
boatSpeedSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case TRACK:
boatTrackSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case NAME:
boatNameSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case ESTTIMETONEXTMARK:
boatEstTimeToNextMarkSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case LEGTIME:
boatElapsedTimeSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
default:
break;
}
}
}
/**
* View did load
*
* @param location .
* @param resources .
*/
@Override
public void initialize(URL location, ResourceBundle resources) {
boatWakeSelect
.setOnAction(event -> setAnnotation(Annotation.WAKE, boatWakeSelect.isSelected()));
boatSpeedSelect
.setOnAction(event -> setAnnotation(Annotation.SPEED, boatSpeedSelect.isSelected()));
boatTrackSelect
.setOnAction(event -> setAnnotation(Annotation.TRACK, boatTrackSelect.isSelected()));
boatNameSelect
.setOnAction(event -> setAnnotation(Annotation.NAME, boatNameSelect.isSelected()));
boatEstTimeToNextMarkSelect.setOnAction(event -> setAnnotation(Annotation.ESTTIMETONEXTMARK,
boatEstTimeToNextMarkSelect.isSelected()));
boatElapsedTimeSelect.setOnAction(
event -> setAnnotation(Annotation.LEGTIME, boatElapsedTimeSelect.isSelected()));
// TODO: 26/07/17 cir27 - Create a more robust fix for this when the annotation for the game are decided upon.
boatEstTimeToNextMarkSelect.setVisible(false);
boatEstTimeToNextMarkSelect.setDisable(true);
boatElapsedTimeSelect.setVisible(false);
boatElapsedTimeSelect.setDisable(true);
closeButton.setOnAction(event -> stage.close());
}
}
@@ -0,0 +1,16 @@
package seng302.controllers.annotations;
/**
* An ImportantAnnotationDelegate handles updating the important annotations
* displayed to the user on behalf of the ImportantAnnotationController
*/
public interface ImportantAnnotationDelegate {
/**
* The important annotations have been changed, update the
* annotations displayed to the user
* @param importantAnnotationsState The current state of the selected annotations
*/
void importantAnnotationsChanged(ImportantAnnotationsState importantAnnotationsState);
}
@@ -0,0 +1,52 @@
package seng302.controllers.annotations;
import java.util.HashMap;
import java.util.Map;
public class ImportantAnnotationsState {
public static final Boolean DEFAULT_ANNOTATION_STATE = true;
private Map<Annotation, Boolean> currentState;
/**
* Stores the users preference for the annotations
* they consider to be important
*/
public ImportantAnnotationsState(){
this.currentState = new HashMap<>();
initialiseState();
}
/**
* Set each annotation to the default annotation state
*/
private void initialiseState(){
for (Annotation annotation : getAnnotations()){
currentState.put(annotation, DEFAULT_ANNOTATION_STATE);
}
}
/**
* Sets the state (visibility) of an annotation
* @param annotation The annotation to set
* @param visible Whether or not the annotation should be visible
*/
public void setAnnotationState(Annotation annotation, Boolean visible){
this.currentState.put(annotation, visible);
}
/**
* Returns the state (visibility) of a specific annotation
* @param annotation The annotation to check
* @return True if visible, else false
*/
public Boolean getAnnotationState(Annotation annotation){
return this.currentState.containsKey(annotation) && this.currentState.get(annotation);
}
/**
* @return Return an array containing all defined annotations
*/
public Annotation[] getAnnotations(){
return Annotation.class.getEnumConstants();
}
}
@@ -0,0 +1,154 @@
package seng302.fxObjects;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import seng302.client.ClientPacketParser;
import seng302.models.Yacht;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
/**
* Collection of annotations for boats.
*/
public class BoatAnnotations extends Group{
//Text offset constants
private static final double X_OFFSET_TEXT = 18d;
private static final double Y_OFFSET_TEXT_INIT = -29d;
private static final double Y_OFFSET_PER_TEXT = 12d;
//Background constants
private static final double TEXT_BUFFER = 3;
private static final double BACKGROUND_X = X_OFFSET_TEXT - TEXT_BUFFER;
private static final double BACKGROUND_Y = Y_OFFSET_TEXT_INIT - TEXT_BUFFER;
private static final double BACKGROUND_H_PER_TEXT = 9.5d;
private static final double BACKGROUND_W = 125d;
private static final double BACKGROUND_ARC_SIZE = 10;
private Rectangle background = new Rectangle();
private Text teamNameObject;
private Text velocityObject;
private Text estTimeToNextMarkObject;
private Text legTimeObject;
private boolean isPlayer = false;
private Yacht boat;
BoatAnnotations (Yacht boat, Color theme) {
super.setCache(true);
this.boat = boat;
background.setX(BACKGROUND_X);
background.setY(BACKGROUND_Y);
background.setWidth(BACKGROUND_W);
background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * 4);
background.setArcHeight(BACKGROUND_ARC_SIZE);
background.setArcWidth(BACKGROUND_ARC_SIZE);
background.setFill(new Color(1, 1, 1, 0.75));
background.setStroke(theme);
background.setStrokeWidth(2);
background.setCache(true);
background.setCacheHint(CacheHint.SPEED);
teamNameObject = getTextObject(boat.getShortName(), theme);
teamNameObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT);
velocityObject = getTextObject("0 m/s", theme);
velocityObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 2);
velocityObject.setVisible(false);
estTimeToNextMarkObject = getTextObject("Next mark: ", theme);
estTimeToNextMarkObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 3);
estTimeToNextMarkObject.setVisible(false);
legTimeObject = getTextObject("Last mark: -", theme);
legTimeObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 4);
legTimeObject.setVisible(false);
this.setVisible(true, false, false, false);
super.getChildren().addAll(background, teamNameObject, velocityObject, estTimeToNextMarkObject, legTimeObject);
}
/**
* Return a text object with caching and a color applied
*
* @param defaultText The default text to display
* @param fill The text fill color
* @return The text object
*/
private Text getTextObject(String defaultText, Color fill) {
Text text = new Text(defaultText);
text.setFill(fill);
text.setStrokeWidth(2);
text.setCacheHint(CacheHint.SPEED);
text.setCache(true);
return text;
}
void update () {
teamNameObject.setText("Player: " + boat.getShortName());
velocityObject.setText(String.format(String.format("%.2f m/s", boat.getVelocityMMS())));
if (boat.getTimeTillNext() != null) {
DateFormat format = new SimpleDateFormat("mm:ss");
String timeToNextMark = format
.format(boat.getTimeTillNext() - ClientPacketParser.getCurrentTimeLong());
estTimeToNextMarkObject.setText("Next mark: " + timeToNextMark);
} else {
estTimeToNextMarkObject.setText("Next mark: -");
}
if (boat.getMarkRoundTime() != null) {
DateFormat format = new SimpleDateFormat("mm:ss");
String elapsedTime = format
.format(ClientPacketParser.getCurrentTimeLong() - boat.getMarkRoundTime());
legTimeObject.setText("Last mark: " + elapsedTime);
}else {
legTimeObject.setText("Last mark: - ");
}
}
void setVisible(boolean nameVisibility, boolean speedVisibility,
boolean estTimeVisibility, boolean lastMarkVisibility) {
int totalVisible = 0;
/*
This is a temporary fix until the new annotation group is added along with the visualiser
overhaul.
*/
totalVisible = updateVisibility(nameVisibility, teamNameObject, totalVisible);
if (isPlayer)
totalVisible = updateVisibility(speedVisibility, velocityObject, totalVisible);
// totalVisible = updateVisibility(estTimeVisibility, estTimeToNextMarkObject, totalVisible);
// totalVisible = updateVisibility(lastMarkVisibility, legTimeObject, totalVisible);
if (totalVisible != 0) {
background.setVisible(true);
background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * totalVisible);
} else {
background.setVisible(false);
}
}
private int updateVisibility (boolean visibility, Text text, int totalVisible) {
if (visibility){
totalVisible ++;
text.setVisible(true);
text.setLayoutX(X_OFFSET_TEXT);
text.setLayoutY(Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * totalVisible);
} else {
text.setVisible(false);
}
return totalVisible;
}
/**
* Sets these annotations to show more detailed info.
*/
public void setAsPlayer () {
isPlayer = true;
velocityObject.setVisible(true);
}
}
@@ -0,0 +1,349 @@
package seng302.fxObjects;
import java.util.ArrayList;
import javafx.geometry.Point2D;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.transform.Rotate;
import seng302.client.ClientPacketParser;
import seng302.models.Yacht;
import seng302.utilities.GeoUtility;
import seng302.controllers.CanvasController;
import seng302.models.mark.GateMark;
import seng302.models.mark.Mark;
import seng302.models.mark.SingleMark;
/**
* BoatGroup is a javafx group that by default contains a graphical objects for representing a 2
* dimensional boat. It contains a single polygon for the boat, a group of lines to show it's path,
* a wake object and two text labels to annotate the boat teams name and the boats velocity. The
* boat will update it's position onscreen everytime UpdatePosition is called unless the window is
* minimized in which case it attempts to store animations and apply them when the window is
* maximised.
*/
public class BoatGroup extends Group {
//Constants for drawing
private static final double BOAT_HEIGHT = 15d;
private static final double BOAT_WIDTH = 10d;
//Variables for boat logic.
private boolean isStopped = true;
private double xIncrement;
private double yIncrement;
private long lastTimeValid = 0;
private Double lastRotation = 0.0;
private long framesToMove;
//Graphical objects
private Yacht boat;
private Group lineGroup = new Group();
private Polygon boatPoly;
private Wake wake;
private Line leftLayLine;
private Line rightLayline;
private Double distanceTravelled = 0.0;
private Point2D lastPoint;
private boolean destinationSet;
private BoatAnnotations boatAnnotations;
private Color color;
private Boolean isSelected = true; //All boats are initialised as selected\
private boolean isPlayer = false;
/**
* Creates a BoatGroup with the default triangular boat polygon.
*
* @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used
* to tell which BoatGroup to update.
* @param color The colour of the boat polygon and the trailing line.
*/
public BoatGroup(Yacht boat, Color color) {
destinationSet = false;
this.boat = boat;
initChildren(color);
}
/**
* Creates a BoatGroup with the boat being the default polygon. The head of the boat should be
* at point (0,0).
*
* @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used
* to tell which BoatGroup to update.
* @param color The colour of the boat polygon and the trailing line.
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat
* polygon.
*/
public BoatGroup(Yacht boat, Color color, double... points) {
destinationSet = false;
this.boat = boat;
initChildren(color, points);
}
/**
* Creates the javafx objects that will be the in the group by default.
*
* @param color The colour of the boat polygon and the trailing line.
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat
* polygon.
*/
private void initChildren(Color color, double... points) {
this.color = color;
boatPoly = new Polygon(points);
boatPoly.setFill(this.color);
boatPoly.setOnMouseEntered(event -> {
boatPoly.setFill(Color.FLORALWHITE);
boatPoly.setStroke(Color.RED);
});
boatPoly.setOnMouseExited(event -> {
boatPoly.setFill(this.color);
boatPoly.setStroke(Color.BLACK);
});
boatPoly.setOnMouseClicked(event -> setIsSelected(!isSelected));
boatPoly.setCache(true);
boatPoly.setCacheHint(CacheHint.SPEED);
boatAnnotations = new BoatAnnotations(boat, this.color);
leftLayLine = new Line();
rightLayline = new Line();
wake = new Wake(0, -BOAT_HEIGHT);
super.getChildren().addAll(boatPoly, boatAnnotations);
}
/**
* Creates the javafx objects that will be the in the group by default.
*
* @param color The colour of the boat polygon and the trailing line.
*/
private void initChildren(Color color) {
initChildren(color,
-BOAT_WIDTH / 2, BOAT_HEIGHT / 2,
0.0, -BOAT_HEIGHT / 2,
BOAT_WIDTH / 2, BOAT_HEIGHT / 2);
}
/**
* Moves the boat and its children annotations from its current coordinates by specified
* amounts.
*
* @param dx The amount to move the X coordinate by
* @param dy The amount to move the Y coordinate by
*/
private void moveGroupBy(double dx, double dy) {
boatPoly.setLayoutX(boatPoly.getLayoutX() + dx);
boatPoly.setLayoutY(boatPoly.getLayoutY() + dy);
boatAnnotations.setLayoutX(boatAnnotations.getLayoutX() + dx);
boatAnnotations.setLayoutY(boatAnnotations.getLayoutY() + dy);
wake.setLayoutX(wake.getLayoutX() + dx);
wake.setLayoutY(wake.getLayoutY() + dy);
}
/**
* Moves the boat and its children annotations to coordinates specified
*
* @param x The X coordinate to move the boat to
* @param y The Y coordinate to move the boat to
*/
private void moveTo(double x, double y, double rotation) {
rotateTo(rotation);
boatPoly.setLayoutX(x);
boatPoly.setLayoutY(y);
boatAnnotations.setLayoutX(x);
boatAnnotations.setLayoutY(y);
wake.setLayoutX(x);
wake.setLayoutY(y);
wake.rotate(rotation);
}
private void rotateTo(double rotation) {
boatPoly.getTransforms().setAll(new Rotate(rotation));
}
/**
* Sets the destination of the boat and the headng it should have once it reaches
*
* @param newXValue The X co-ordinate the boat needs to move to.
* @param newYValue The Y co-ordinate the boat needs to move to.
* @param rotation Rotation to move graphics to.
* @param timeValid the time the position values are valid for
*/
public void setDestination(double newXValue, double newYValue, double rotation,
double groundSpeed, long timeValid, double frameRate) {
destinationSet = true;
Double dx = Math.abs(boatPoly.getLayoutX() - newXValue);
Double dy = Math.abs(boatPoly.getLayoutY() - newYValue);
moveTo(newXValue, newYValue, rotation);
rotateTo(rotation);
wake.setRotation(rotation, groundSpeed);
boat.setVelocity(groundSpeed);
isStopped = false;
lastRotation = rotation;
boatAnnotations.update();
distanceTravelled += Math.sqrt((dx * dx) + (dy * dy));
if (distanceTravelled > 10 && isPlayer) {
distanceTravelled = 0d;
if (lastPoint != null) {
Line l = new Line(
lastPoint.getX(),
lastPoint.getY(),
boatPoly.getLayoutX(),
boatPoly.getLayoutY()
);
l.getStrokeDashArray().setAll(3d, 7d);
l.setStroke(boat.getColour());
l.setCache(true);
l.setCacheHint(CacheHint.SPEED);
lineGroup.getChildren().add(l);
}
if (destinationSet) {
lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
}
}
}
/**
* This function works out if a boat is going upwind or down wind. It looks at the boats current position, the next
* gates position and the current wind
* If bot the wind vector from the next gate and the boat from the next gate lay on the same side, then the boat is
* going up wind, if they are on different sides of the gate, then the boat is going downwind
* @param canvasController
*/
public Boolean isUpwindLeg(CanvasController canvasController, Mark nextMark) {
Double windAngle = ClientPacketParser.getWindDirection();
GateMark thisGateMark = (GateMark) nextMark;
SingleMark nextMark1 = thisGateMark.getSingleMark1();
SingleMark nextMark2 = thisGateMark.getSingleMark2();
Point2D nextMarkPoint1 = canvasController.findScaledXY(nextMark1.getLatitude(), nextMark1.getLongitude());
Point2D nextMarkPoint2 = canvasController.findScaledXY(nextMark2.getLatitude(), nextMark2.getLongitude());
Point2D boatCurrentPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
Point2D windTestPoint = GeoUtility.makeArbitraryVectorPoint(nextMarkPoint1, windAngle, 10d);
Integer boatLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, boatCurrentPoint);
Integer windLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, windTestPoint);
/*
If both the wind vector from the gate and the boat from the gate are on the same side of that gate, then the
boat is travelling into the wind. thus upwind. Otherwise if they are on different sides, then the boat is going
with the wind.
*/
if (boatLineFuncResult == windLineFuncResult) {
return true;
} else {
return false;
}
}
public void setIsSelected(Boolean isSelected) {
this.isSelected = isSelected;
setLineGroupVisible(isSelected);
setWakeVisible(isSelected);
boatAnnotations.setVisible(isSelected);
setLayLinesVisible(isSelected);
}
public void setVisibility (boolean teamName, boolean velocity, boolean estTime, boolean legTime, boolean trail, boolean wake) {
boatAnnotations.setVisible(teamName, velocity, estTime, legTime);
this.wake.setVisible(wake);
this.lineGroup.setVisible(trail);
}
public void setLineGroupVisible(Boolean visible) {
lineGroup.setVisible(visible);
}
public void setWakeVisible(Boolean visible) {
wake.setVisible(visible);
}
public void setLayLinesVisible(Boolean visible) {
leftLayLine.setVisible(visible);
rightLayline.setVisible(visible);
}
public void setLaylines(Line line1, Line line2) {
this.leftLayLine = line1;
this.rightLayline = line2;
}
public ArrayList<Line> getLaylines() {
ArrayList<Line> laylines = new ArrayList<>();
laylines.add(leftLayLine);
laylines.add(rightLayline);
return laylines;
}
public Yacht getBoat() {
return boat;
}
/**
* Returns all raceIds associated with this group. For BoatGroups the ID's are for the boat.
*
* @return An array containing all ID's associated with this RaceObject.
*/
public long getRaceId() {
return boat.getSourceId();
}
public Group getWake () {
return wake;
}
public Group getTrail() {
return lineGroup;
}
public Group getAnnotations() {
return boatAnnotations;
}
public Double getBoatLayoutX() {
return boatPoly.getLayoutX();
}
public Double getBoatLayoutY() {
return boatPoly.getLayoutY();
}
public boolean isStopped() {
return isStopped;
}
@Override
public String toString() {
return boat.toString();
}
/**
* Sets this boat to appear highlighted
*/
public void setAsPlayer() {
boatPoly.getPoints().setAll(
-BOAT_WIDTH / 1.75, BOAT_HEIGHT / 1.75,
0.0, -BOAT_HEIGHT / 1.75,
BOAT_WIDTH / 1.75, BOAT_HEIGHT / 1.75
);
boatPoly.setStroke(Color.BLACK);
boatPoly.setStrokeWidth(3);
boatAnnotations.setAsPlayer();
isPlayer = true;
}
}
@@ -0,0 +1,167 @@
package seng302.fxObjects;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import seng302.models.mark.GateMark;
import seng302.models.mark.Mark;
import seng302.models.mark.MarkType;
import seng302.models.mark.SingleMark;
/**
* Grouping of javaFX objects needed to represent a Mark on screen.
*/
public class MarkGroup extends Group {
private static int MARK_RADIUS = 5;
private static int LINE_THICKNESS = 2;
private static double DASHED_GAP_LEN = 2d;
private static double DASHED_LINE_LEN = 5d;
private List<Mark> marks = new ArrayList<>();
private Mark mainMark;
/**
* Constructor for singleMark groups
* @param mark
* @param points
*/
public MarkGroup (SingleMark mark, Point2D points) {
marks.add(mark);
mainMark = mark;
Color color = Color.BLACK;
if (mark.getName().equals("Start")){
color = Color.GREEN;
} else if (mark.getName().equals("Finish")){
color = Color.RED;
}
Circle markCircle;
markCircle = new Circle(
points.getX(),
points.getY(),
MARK_RADIUS,
color
);
super.getChildren().add(markCircle);
}
public void addLaylines(Line line1, Line line2) {
super.getChildren().addAll(line1, line2);
}
public void removeLaylines() {
ArrayList<Node> toRemove = new ArrayList<>();
for(Node node : super.getChildren()) {
if (node instanceof Line) {
Line layLine = (Line) node;
/***
* OOHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHhhh
*/
if (layLine.getStrokeWidth() == 0.5){
toRemove.add(layLine);
}
}
}
super.getChildren().removeAll(toRemove);
}
public MarkGroup(GateMark mark, Point2D points1, Point2D points2) {
marks.add(mark.getSingleMark1());
marks.add(mark.getSingleMark2());
mainMark = mark;
Color color = Color.BLACK;
if (mark.getName().equals("Start")){
color = Color.GREEN;
} else if (mark.getName().equals("Finish")){
color = Color.RED;
}
Circle markCircle;
markCircle = new Circle(
points1.getX(),
points1.getY(),
MARK_RADIUS,
color
);
super.getChildren().add(markCircle);
markCircle = new Circle(
points2.getX(),
points2.getY(),
MARK_RADIUS,
color
);
super.getChildren().add(markCircle);
Line line = new Line(
points1.getX(),
points1.getY(),
points2.getX(),
points2.getY()
);
line.setStrokeWidth(LINE_THICKNESS);
line.setStroke(color);
if (mark.getMarkType() == MarkType.OPEN_GATE) {
line.getStrokeDashArray().addAll(DASHED_GAP_LEN, DASHED_LINE_LEN);
}
super.getChildren().add(line);
}
public void moveMarkTo (double x, double y, long raceId)
{
if (mainMark.getMarkType() == MarkType.SINGLE_MARK) {
Circle markCircle = (Circle) super.getChildren().get(0);
//One of the test streams produced frequent, jittery movements. Added this as a fix.
if (Math.abs(markCircle.getCenterX() - x) > 5 || Math.abs(markCircle.getCenterY() - y) > 5) {
markCircle.setCenterX(x);
markCircle.setCenterY(y);
}
} else {
Circle markCircle1 = (Circle) super.getChildren().get(0);
Circle markCircle2 = (Circle) super.getChildren().get(1);
Line connectingLine = (Line) super.getChildren().get(2);
if (marks.get(0).getId() == raceId) {
if (Math.abs(markCircle1.getCenterX() - x) > 5 || Math.abs(markCircle1.getCenterY() - y) > 5) {
markCircle1.setCenterX(x);
markCircle1.setCenterY(y);
connectingLine.setStartX(markCircle1.getCenterX());
connectingLine.setStartY(markCircle1.getCenterY());
}
} else if (marks.get(1).getId() == raceId) {
if (Math.abs(markCircle2.getCenterX() - x) > 5 || Math.abs(markCircle2.getCenterY() - y) > 5) {
markCircle2.setCenterX(x);
markCircle2.setCenterY(y);
connectingLine.setEndX(markCircle2.getCenterX());
connectingLine.setEndY(markCircle2.getCenterY());
}
}
}
}
public boolean hasRaceId (int... raceIds) {
for (int id : raceIds)
for (Mark mark : marks)
if (id == mark.getId())
return true;
return false;
}
public long[] getRaceIds () {
long[] idArray = new long[marks.size()];
int i = 0;
for (Mark mark : marks)
idArray[i++] = mark.getId();
return idArray;
}
public Mark getMainMark() {
return mainMark;
}
}
+112
View File
@@ -0,0 +1,112 @@
package seng302.fxObjects;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
/**
* A group containing objects used to represent wakes onscreen. Contains functionality for their animation.
*/
public class Wake extends Group {
//The number of wakes
private int numWakes = 8;
//The total possible difference between the first wake and the last. Increasing/Decreasing this will make wakes fan out more/less.
private final double MAX_DIFF = 75;
//Increasing/decreasing this will alter the speed that wakes converge when the heading stop changing. Anything over about 1500 may cause oscillation.
private final int UNIFICATION_SPEED = 45;
private Arc[] arcs = new Arc[numWakes];
private double[] rotationalVelocities = new double[numWakes];
private double[] rotations = new double[numWakes];
/**
* Create a wake at the given location.
*
* @param startingX x location where the tip of wake arcs will be.
* @param startingY y location where the tip of wake arcs will be.
*/
Wake(double startingX, double startingY) {
super.setLayoutX(startingX);
super.setLayoutY(startingY);
Arc arc;
for (int i = 0; i < numWakes; i++) {
//Default triangle is -110 deg out of phase with a default wake and has angle of 40 deg.
arc = new Arc(0, 0, 0, 0, -110, 40);
arc.setCache(true);
arc.setCacheHint(CacheHint.ROTATE);
arc.setType(ArcType.OPEN);
arc.setStroke(new Color(0.18, 0.7, 1.0, 1.0 + (-0.99 / numWakes * i)));
arc.setStrokeWidth(3.0);
arc.setStrokeLineCap(StrokeLineCap.ROUND);
arc.setFill(new Color(0.0, 0.0, 0.0, 0.0));
arcs[i] = arc;
arc.getTransforms().setAll(
new Rotate(1)
);
}
super.getChildren().addAll(arcs);
}
void setRotation (double rotation, double velocity) {
// if (Math.abs(rotations[0] - rotation) > 20) {
rotate(rotation);
// } else {
// rotations[0] = rotation;
// ((Rotate) arcs[0].getTransforms().get(0)).setAngle(rotation);
// for (int i = 1; i < numWakes; i++) {
// double wakeSeparationRad = Math.toRadians(rotations[i - 1] - rotations[i]);
// double shortestDistance = Math.atan2(
// Math.sin(wakeSeparationRad),
// Math.cos(wakeSeparationRad)
// );
// double distDeg = Math.toDegrees(shortestDistance);
// if (rotationalVelocities[i - 1] < 0.01 && rotationalVelocities[i - 1] > -0.01) {
// rotationalVelocities[i] = distDeg / UNIFICATION_SPEED * 2 * Math.log(Math.abs(distDeg) + 1) / Math.log(MAX_DIFF / numWakes);
//
// } else {
// if (distDeg < (MAX_DIFF / numWakes)) {
// rotationalVelocities[i] = distDeg / UNIFICATION_SPEED * Math.log(Math.abs(distDeg) + 1) / Math.log(MAX_DIFF / numWakes);
// } else
// rotationalVelocities[i] = rotationalVelocities[i - 1];
// }
// }
// }
double rad = (14 / numWakes) + velocity;
for (Arc arc : arcs) {
arc.setRadiusX(rad);
arc.setRadiusY(rad);
rad += (14 / numWakes) + (velocity / 2.5);
}
}
/**
* Arcs rotate based on the distance they would have travelled over the supplied time interval.
*/
void updatePosition() {
for (int i = 0; i < numWakes; i++) {
rotations[i] = rotations[i] + rotationalVelocities[i];
((Rotate) arcs[i].getTransforms().get(0)).setAngle(rotations[i]);
}
}
/**
* Rotate all wakes to the given rotation.
*
* @param rotation the from north angle in degrees to rotate to.
*/
void rotate(double rotation) {
for (int i = 0; i < arcs.length; i++) {
rotations[i] = rotation;
rotationalVelocities[i] = 0;
arcs[i].getTransforms().setAll(new Rotate(rotation));
}
}
}
@@ -0,0 +1,17 @@
package seng302.gameServer;
import seng302.models.Player;
public interface ClientConnectionDelegate {
/**
* A player has connected to the server
* @param serverToClientThread The player that has connected
*/
void clientConnected(ServerToClientThread serverToClientThread);
/**
* A player has disconnected from the server
* @param player The player that has disconnected
*/
void clientDisconnected(Player player);
}
@@ -0,0 +1,24 @@
package seng302.gameServer;
/**
* An enum describing the states of the game
* Created by wmu16 on 11/07/17.
*/
public enum GameStages {
LOBBYING(0),
PRE_RACE(1),
RACING(2),
FINISHED(3),
CANCELLED(4);
private long code;
GameStages(long code) {
this.code = code;
}
public long getCode(){
return code;
}
}
@@ -0,0 +1,193 @@
package seng302.gameServer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import seng302.client.ClientPacketParser;
import seng302.models.Player;
import seng302.models.Yacht;
import seng302.server.messages.BoatActionType;
/**
* A Static class to hold information about the current state of the game (model)
* Created by wmu16 on 10/07/17.
*/
public class GameState implements Runnable {
private static Integer STATE_UPDATES_PER_SECOND = 60;
private static Long previousUpdateTime;
public static Double windDirection;
private static Double windSpeed;
private static String hostIpAddress;
private static List<Player> players;
private static Map<Integer, Yacht> yachts;
private static Boolean isRaceStarted;
private static GameStages currentStage;
private static long startTime = System.currentTimeMillis();
public GameState(String hostIpAddress) {
windDirection = 180d;
windSpeed = 10000d;
this.hostIpAddress = hostIpAddress;
players = new ArrayList<>();
currentStage = GameStages.LOBBYING;
isRaceStarted = false;
yachts = new HashMap<>();
//set this when game stage changes to prerace
previousUpdateTime = System.currentTimeMillis();
yachts = new HashMap<>();
new Thread(this).start();
}
public static String getHostIpAddress() {
return hostIpAddress;
}
public static List<Player> getPlayers() {
return players;
}
public static void addPlayer(Player player) {
players.add(player);
}
public static void removePlayer(Player player) {
players.remove(player);
}
public static void addYacht(Integer sourceId, Yacht yacht) {
yachts.put(sourceId, yacht);
}
public static void removeYacht(Integer yachtId) {
yachts.remove(yachtId);
}
public static Boolean getIsRaceStarted() {
return isRaceStarted;
}
public static GameStages getCurrentStage() {
return currentStage;
}
public static void setCurrentStage(GameStages currentStage) {
if (currentStage == GameStages.RACING){
startTime = System.currentTimeMillis();
}
GameState.currentStage = currentStage;
}
public static long getStartTime(){
return startTime;
}
public static Double getWindDirection() {
return windDirection;
}
public static Double getWindSpeedMMS() {
return windSpeed;
}
public static Double getWindSpeedKnots() {
return windSpeed / 1000 * ClientPacketParser.MS_TO_KNOTS;
}
public static Map<Integer, Yacht> getYachts() {
return yachts;
}
public static void updateBoat(Integer sourceId, BoatActionType actionType) {
Yacht playerYacht = yachts.get(sourceId);
// System.out.println("-----------------------");
switch (actionType) {
case VMG:
playerYacht.turnToVMG();
// System.out.println("Snapping to VMG");
break;
case SAILS_IN:
playerYacht.toggleSailIn();
// System.out.println("Toggling Sails");
break;
case SAILS_OUT:
playerYacht.toggleSailIn();
// System.out.println("Toggling Sails");
break;
case TACK_GYBE:
playerYacht.tackGybe(windDirection);
// System.out.println("Tack/Gybe");
break;
case UPWIND:
playerYacht.turnUpwind();
// System.out.println("Moving upwind");
break;
case DOWNWIND:
playerYacht.turnDownwind();
// System.out.println("Moving downwind");
break;
}
// printBoatStatus(playerYacht);
}
public static void update() {
Long timeInterval = System.currentTimeMillis() - previousUpdateTime;
previousUpdateTime = System.currentTimeMillis();
for (Yacht yacht : yachts.values()) {
yacht.update(timeInterval);
}
}
/**
* Generates a new ID based off the size of current players + 1
* @return a playerID to be allocated to a new connetion
*/
public static Integer getUniquePlayerID() {
// TODO: 22/07/17 wmu16 - This may not be robust enough and may have to be improved on.
return yachts.size() + 1;
}
/**
* A thread to have the game state update itself at certain intervals
*/
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000 / STATE_UPDATES_PER_SECOND);
} catch (InterruptedException e) {
System.out.println("[GameState] interrupted exception");
}
if (currentStage == GameStages.PRE_RACE) {
update();
}
//RACING
if (currentStage == GameStages.RACING) {
update();
}
}
}
private static void printBoatStatus(Yacht playerYacht) {
System.out.println("-----------------------");
System.out.println("Sails are in: " + playerYacht.getSailIn());
System.out.println("Heading: " + playerYacht.getHeading());
System.out.println("Velocity: " + playerYacht.getVelocityMMS() / 1000);
System.out.println("Lat: " + playerYacht.getLocation().getLat());
System.out.println("Lng: " + playerYacht.getLocation().getLng());
System.out.println("-----------------------\n");
}
}
@@ -0,0 +1,81 @@
package seng302.gameServer;
import seng302.models.Player;
import seng302.server.messages.Heartbeat;
import seng302.server.messages.Message;
import java.io.IOException;
import java.util.*;
/**
* Send Heartbeat messages to connected player at a specified interval
* Will call .clientDisconnected on the delegate when a heartbeat message
* cannot be sent to a player
*/
public class HeartbeatThread extends Thread{
private final int HEARTBEAT_PERIOD = 200;
private ClientConnectionDelegate delegate;
private Integer seqNum;
private Stack<Player> disconnectedPlayers;
public HeartbeatThread(ClientConnectionDelegate delegate){
this.delegate = delegate;
seqNum = 0;
disconnectedPlayers = new Stack<>();
}
/**
* A player has lost connection to the server
* The player is added to a stack so that the delegate
* can be notified
*
* @param player The player that has disconnected
*/
private void playerLostConnection(Player player){
disconnectedPlayers.push(player);
}
/**
* Sends a heartbeat message to each connected player
* The delegate is notified if a player has disconnected
*/
private void sendHeartbeatToAllPlayers(){
Message heartbeat = new Heartbeat(seqNum);
for (Player player : GameState.getPlayers()){
if (!player.getSocket().isConnected()) {
playerLostConnection(player);
}
try {
player.getSocket().getOutputStream().write(heartbeat.getBuffer());
} catch (IOException e) {
playerLostConnection(player);
}
}
updateDelegate();
seqNum++;
}
/**
* Notifies the delegate about
* each disconnected player
*/
private void updateDelegate() {
while (!disconnectedPlayers.empty()){
delegate.clientDisconnected(disconnectedPlayers.pop());
}
}
public void run(){
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
sendHeartbeatToAllPlayers();
}
}, 0, HEARTBEAT_PERIOD);
}
}
@@ -0,0 +1,157 @@
package seng302.gameServer;
import java.time.LocalDateTime;
import java.util.Observable;
import seng302.client.ClientPacketParser;
import seng302.models.Player;
import seng302.models.stream.PacketBufferDelegate;
import seng302.models.stream.packets.StreamPacket;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.logging.Logger;
/**
* A class describing the overall server, which creates and collects server threads for each client
* Created by wmu16 on 13/07/17.
*/
public class MainServerThread extends Observable implements Runnable, ClientConnectionDelegate{
private static final int PORT = 4942;
private static final Integer CLIENT_UPDATES_PER_SECOND = 10;
private static final int LOG_LEVEL = 1;
private boolean terminated;
private Thread thread;
private ServerSocket serverSocket = null;
private ArrayList<ServerToClientThread> serverToClientThreads = new ArrayList<>();
public MainServerThread() {
try {
serverSocket = new ServerSocket(PORT);
} catch (IOException e) {
serverLog("IO error in server thread handler upon trying to make new server socket", 0);
}
terminated = false;
thread = new Thread(this);
thread.start();
}
public void run() {
ServerListenThread serverListenThread;
HeartbeatThread heartbeatThread;
serverListenThread = new ServerListenThread(serverSocket, this);
heartbeatThread = new HeartbeatThread(this);
heartbeatThread.start();
serverListenThread.start();
//You should handle interrupts in some way, so that the thread won't keep on forever if you exit the app.
while (!terminated) {
try {
Thread.sleep(1000 / CLIENT_UPDATES_PER_SECOND);
} catch (InterruptedException e) {
serverLog("Interrupted exception in Main Server Thread thread sleep", 1);
}
if (GameState.getCurrentStage() == GameStages.PRE_RACE) {
updateClients();
}
//RACING
if (GameState.getCurrentStage() == GameStages.RACING) {
updateClients();
}
//FINISHED
else if (GameState.getCurrentStage() == GameStages.FINISHED) {
}
}
// TODO: 14/07/17 wmu16 - Send out disconnect packet to clients
try {
serverSocket.close();
return;
} catch (IOException e) {
System.out.println("IO error in server thread handler upon closing socket");
}
}
public void updateClients() {
for (ServerToClientThread serverToClientThread : serverToClientThreads) {
serverToClientThread.updateClient();
}
}
static void serverLog(String message, int logLevel){
if(logLevel <= LOG_LEVEL){
System.out.println("[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message);
}
}
/**
* A client has tried to connect to the server
* @param serverToClientThread The player that connected
*/
@Override
public void clientConnected(ServerToClientThread serverToClientThread) {
serverLog("Player Connected From " + serverToClientThread.getThread().getName(), 0);
serverToClientThreads.add(serverToClientThread);
this.addObserver(serverToClientThread);
setChanged();
notifyObservers();
}
/**
* A player has left the game, remove the player from the GameState
* @param player The player that left
*/
@Override
public void clientDisconnected(Player player) {
try {
player.getSocket().close();
} catch (Exception e) {
serverLog("Cannot disconnect the socket for the disconnected player.", 0);
}
serverLog("Player " + player.getYacht().getSourceId() + "'s socket disconnected", 0);
GameState.removeYacht(player.getYacht().getSourceId());
GameState.removePlayer(player);
for (ServerToClientThread serverToClientThread : serverToClientThreads) {
if (serverToClientThread.getSocket() == player.getSocket()) {
this.deleteObserver(serverToClientThread);
}
}
setChanged();
notifyObservers();
}
public void startGame() {
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
for (ServerToClientThread serverToClientThread : serverToClientThreads) {
serverToClientThread.sendRaceStatusMessage();
}
}
}, 0, 500);
}
public void terminate() {
terminated = true;
}
}
@@ -0,0 +1,46 @@
package seng302.gameServer;
import seng302.models.Player;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
* A class for a thread to listen to connections
* Created by wmu16 on 11/07/17.
*/
public class ServerListenThread extends Thread{
private ServerSocket serverSocket;
private ClientConnectionDelegate delegate;
public ServerListenThread(ServerSocket serverSocket, ClientConnectionDelegate delegate){
this.serverSocket = serverSocket;
this.delegate = delegate;
}
/**
* Listens for a connection and upon finding one, creates a Player object and adds it to the universal GameState
*/
private void acceptConnection() {
try {
Socket thisClient = serverSocket.accept();
if (thisClient != null && GameState.getCurrentStage().equals(GameStages.LOBBYING)) {
ServerToClientThread thisConnection = new ServerToClientThread(thisClient);
delegate.clientConnected(thisConnection);
} else {
thisClient.close();
}
} catch (IOException e) {
e.getMessage();
}
}
public void run(){
while (true){
acceptConnection();
}
}
}
@@ -0,0 +1,37 @@
package seng302.gameServer;
import java.util.Arrays;
import seng302.models.stream.packets.StreamPacket;
import seng302.server.messages.BoatActionType;
public class ServerPacketParser {
public static BoatActionType extractBoatAction(StreamPacket packet) {
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long actionTypeValue = bytesToLong(Arrays.copyOfRange(payload, 0, 1));
return BoatActionType.getType((int) actionTypeValue);
}
/**
* takes an array of up to 7 bytes and returns a positive
* long constructed from the input bytes
*
* @return a positive long if there is less than 7 bytes -1 otherwise
*/
private static long bytesToLong(byte[] bytes) {
long partialLong = 0;
int index = 0;
for (byte b : bytes) {
if (index > 6) {
return -1;
}
partialLong = partialLong | (b & 0xFFL) << (index * 8);
index++;
}
return partialLong;
}
}
@@ -0,0 +1,372 @@
package seng302.gameServer;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import seng302.models.Player;
import seng302.models.Yacht;
import seng302.models.stream.packets.PacketType;
import seng302.models.stream.packets.StreamPacket;
import seng302.models.xml.Race;
import seng302.models.xml.Regatta;
import seng302.models.xml.XMLGenerator;
import seng302.server.messages.BoatActionType;
import seng302.server.messages.BoatLocationMessage;
import seng302.server.messages.BoatStatus;
import seng302.server.messages.BoatSubMessage;
import seng302.server.messages.Message;
import seng302.server.messages.RaceStatus;
import seng302.server.messages.RaceStatusMessage;
import seng302.server.messages.RaceType;
import seng302.server.messages.XMLMessage;
import seng302.server.messages.XMLMessageSubType;
/**
* A class describing a single connection to a Client for the purposes of sending and receiving on
* its own thread. All server threads created and owned by the server thread handler which can
* trigger client updates on its threads Created by wmu16 on 13/07/17.
*/
public class ServerToClientThread implements Runnable, Observer {
private static final Integer LOG_LEVEL = 1;
private static final Integer MAX_ID_ATTEMPTS = 10;
private Thread thread;
private InputStream is;
private OutputStream os;
private Socket socket;
private ByteArrayOutputStream crcBuffer;
private Boolean userIdentified = false;
private Boolean connected = true;
private Boolean updateClient = true;
// private Boolean initialisedRace = true;
private Integer seqNo;
private Integer sourceId;
private XMLGenerator xml;
public ServerToClientThread(Socket socket) {
this.socket = socket;
BufferedReader fn;
String fName = "";
BufferedReader ln;
String lName = "";
try {
is = socket.getInputStream();
os = socket.getOutputStream();
fn = new BufferedReader(
new InputStreamReader(
ServerToClientThread.class.getResourceAsStream(
"/server_config/CSV_Database_of_First_Names.csv"
)
)
);
List<String> all = fn.lines().collect(Collectors.toList());
fName = all.get(ThreadLocalRandom.current().nextInt(0, all.size()));
ln = new BufferedReader(
new InputStreamReader(
ServerToClientThread.class.getResourceAsStream(
"/server_config/CSV_Database_of_Last_Names.csv"
)
)
);
all = ln.lines().collect(Collectors.toList());
lName = all.get(ThreadLocalRandom.current().nextInt(0, all.size()));
} catch (IOException e) {
serverLog("IO error in server thread upon grabbing streams", 1);
}
//Attempt threeway handshake with connection
sourceId = GameState.getUniquePlayerID();
if (threeWayHandshake(sourceId)) {
serverLog("Successful handshake. Client allocated id: " + sourceId, 0);
Yacht yacht = new Yacht(
"Yacht", sourceId, sourceId.toString(), fName, fName + " " + lName, "NZ"
);
// Yacht yacht = new Yacht("Kappa", "Kap", new GeoPoint(57.6708220, 11.8321340), 90.0);
GameState.addYacht(sourceId, yacht);
GameState.addPlayer(new Player(socket, yacht));
} else {
serverLog("Unsuccessful handshake. Connection rejected", 1);
closeSocket();
return;
}
seqNo = 0;
thread = new Thread(this);
thread.start();
}
static void serverLog(String message, int logLevel) {
if (logLevel <= LOG_LEVEL) {
System.out.println(
"[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message);
}
}
@Override
public void update(Observable o, Object arg) {
sendSetupMessages();
}
public void run() {
int sync1;
int sync2;
// TODO: 14/07/17 wmu16 - Work out how to fix this while loop
while (socket.isConnected()) {
try {
//Perform a write if it is time to as delegated by the MainServerThread
if (updateClient) {
// TODO: 13/07/17 wmu16 - Write out game state - some function that would write all appropriate messages to this output stream
// ChatterMessage chatterMessage = new ChatterMessage(4, 14, "Hello, it's me");
// sendMessage(chatterMessage);
// try {
// GameState.outputState(os);
// } catch (IOException e) {
// System.out.println("IO error in server thread upon writing to output stream");
// }
// sendBoatLocationPackets();
updateClient = false;
}
crcBuffer = new ByteArrayOutputStream();
sync1 = readByte();
sync2 = readByte();
//checking if it is the start of the packet
if (sync1 == 0x47 && sync2 == 0x83) {
int type = readByte();
//No. of milliseconds since Jan 1st 1970
long timeStamp = Message.bytesToLong(getBytes(6));
skipBytes(4);
long payloadLength = Message.bytesToLong(getBytes(2));
byte[] payload = getBytes((int) payloadLength);
Checksum checksum = new CRC32();
checksum.update(crcBuffer.toByteArray(), 0, crcBuffer.size());
long computedCrc = checksum.getValue();
long packetCrc = Message.bytesToLong(getBytes(4));
if (computedCrc == packetCrc) {
//System.out.println("RECEIVED A PACKET");
switch (PacketType.assignPacketType(type)) {
case BOAT_ACTION:
BoatActionType actionType = ServerPacketParser
.extractBoatAction(
new StreamPacket(type, payloadLength, timeStamp, payload));
GameState.updateBoat(sourceId, actionType);
break;
}
} else {
serverLog("Packet has been dropped", 1);
}
}
} catch (Exception e) {
// TODO: 24/07/17 zyt10 - fix a logic here when a client disconnected
// serverLog("ERROR OCCURRED, CLOSING SERVER CONNECTION: " + socket.getRemoteSocketAddress().toString(), 1);
closeSocket();
return;
}
}
}
private void sendSetupMessages() {
xml = new XMLGenerator();
Race race = new Race();
for (Yacht yacht : GameState.getYachts().values()) {
race.addBoat(yacht);
}
//@TODO calculate lat/lng values
xml.setRegatta(new Regatta("RaceVision Test Game", 57.6679590, 11.8503233));
xml.setRace(race);
XMLMessage xmlMessage;
xmlMessage = new XMLMessage(xml.getRegattaAsXml(), XMLMessageSubType.REGATTA,
xml.getRegattaAsXml().length());
sendMessage(xmlMessage);
xmlMessage = new XMLMessage(xml.getBoatsAsXml(), XMLMessageSubType.BOAT,
xml.getBoatsAsXml().length());
sendMessage(xmlMessage);
xmlMessage = new XMLMessage(xml.getRaceAsXml(), XMLMessageSubType.RACE,
xml.getRaceAsXml().length());
sendMessage(xmlMessage);
}
public void updateClient() {
sendBoatLocationPackets();
updateClient = true;
}
/**
* Tries to confirm the connection just accepted.
* Sends ID, expects that ID echoed for confirmation,
* if so, sends a confirmation packet back to that connection
* Creates a player instance with that ID and this thread and adds it to the GameState
* If not, close the socket and end the threads execution
*
* @param id the id to try and assign to the connection
* @return A boolean indicating if it was a successful handshake
*/
private Boolean threeWayHandshake(Integer id) {
Integer confirmationID = null;
Integer identificationAttempt = 0;
while (!userIdentified) {
try {
os.write(id); //Send out new ID looking for echo
confirmationID = is.read();
} catch (IOException e) {
serverLog("Three way handshake failed", 1);
}
if (id.equals(confirmationID)) { //ID is echoed back. Connection is a client
return true;
} else if (identificationAttempt > MAX_ID_ATTEMPTS) { //No response. not a client. tidy up and go home.
return false;
}
identificationAttempt++;
}
return true;
}
private void closeSocket() {
try {
socket.close();
} catch (IOException e) {
System.out.println("IO error in server thread upon trying to close socket");
}
}
private int readByte() throws Exception {
int currentByte = -1;
try {
// @TODO @FIX ConnectionReset Exception when a client disconnects before it is garbage collected
currentByte = is.read();
crcBuffer.write(currentByte);
} catch (IOException e) {
serverLog("Socket read failed", 1);
}
if (currentByte == -1) {
throw new Exception();
}
return currentByte;
}
private byte[] getBytes(int n) throws Exception {
byte[] bytes = new byte[n];
for (int i = 0; i < n; i++) {
bytes[i] = (byte) readByte();
}
return bytes;
}
private void skipBytes(long n) throws Exception {
for (int i = 0; i < n; i++) {
readByte();
}
}
public void sendMessage(Message message) {
try {
os.write(message.getBuffer());
} catch (SocketException e) {
//serverLog("Player " + sourceId + " side socket disconnected", 1);
return;
} catch (IOException e) {
serverLog("Message send failed", 1);
}
}
private int getSeqNo() {
seqNo++;
return seqNo;
}
private void sendBoatLocationPackets() {
ArrayList<Yacht> yachts = new ArrayList<>(GameState.getYachts().values());
for (Yacht yacht : yachts) {
// System.out.println("[SERVER] Lat: " + yacht.getLocation().getLat() + " Lon: " + yacht.getLocation().getLng());
BoatLocationMessage boatLocationMessage =
new BoatLocationMessage(
yacht.getSourceId(),
getSeqNo(),
yacht.getLocation().getLat(),
yacht.getLocation().getLng(),
yacht.getHeading(),
(long) yacht.getVelocityMMS());
sendMessage(boatLocationMessage);
}
}
public Thread getThread() {
return thread;
}
public void sendRaceStatusMessage() {
// variables taken from GameServerThread
List<BoatSubMessage> boatSubMessages = new ArrayList<>();
BoatStatus boatStatus;
RaceStatus raceStatus;
for (Player player : GameState.getPlayers()) {
Yacht y = player.getYacht();
if (GameState.getCurrentStage() == GameStages.PRE_RACE) {
boatStatus = BoatStatus.PRESTART;
} else if (GameState.getCurrentStage() == GameStages.RACING) {
boatStatus = BoatStatus.RACING;
} else {
boatStatus = BoatStatus.UNDEFINED;
}
BoatSubMessage m = new BoatSubMessage(y.getSourceId(), boatStatus, 0, 0, 0, 1234l,
1234l);
boatSubMessages.add(m);
}
if (GameState.getCurrentStage() == GameStages.RACING) {
raceStatus = RaceStatus.STARTED;
} else {
raceStatus = RaceStatus.WARNING;
}
sendMessage(new RaceStatusMessage(1, raceStatus, GameState.getStartTime(), GameState.getWindDirection(),
GameState.getWindSpeedMMS().longValue(), GameState.getPlayers().size(),
RaceType.MATCH_RACE, 1, boatSubMessages));
}
public Socket getSocket() {
return socket;
}
}
-358
View File
@@ -1,358 +0,0 @@
package seng302.models;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
/**
* BoatGroup is a javafx group that by default contains a graphical objects for representing a 2 dimensional boat.
* It contains a single polygon for the boat, a group of lines to show it's path, a wake object and two text labels to
* annotate the boat teams name and the boats velocity. The boat will update it's position onscreen everytime
* UpdatePosition is called unless the window is minimized in which case it attempts to store animations and apply them
* when the window is maximised.
*/
public class BoatGroup extends RaceObject{
//Constants for drawing
private static final double TEAMNAME_X_OFFSET = 10d;
private static final double TEAMNAME_Y_OFFSET = -15d;
private static final double VELOCITY_X_OFFSET = 10d;
private static final double VELOCITY_Y_OFFSET = -5d;
private static final double BOAT_HEIGHT = 15d;
private static final double BOAT_WIDTH = 10d;
//Variables for boat logic.
private Point2D lastPoint;
private int wakeGenerationDelay = 10;
private double distanceTravelled;
//Graphical objects
private Yacht boat;
private Group lineGroup = new Group();
private Polygon boatPoly;
private Text teamNameObject;
private Text velocityObject;
private Wake wake;
//Handles boat moving when connecting to a stream
private boolean setToInitialLocation = false;
private boolean destinationSet;
//Variables for handling minimization
private Stage stage;
private boolean isMaximized= true;
private List<Line> lineStorage = new ArrayList<>();
private int setCallCount = 5;
/**
* Creates a BoatGroup with the default triangular boat polygon.
* @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used to tell which
* BoatGroup to update.
* @param color The colour of the boat polygon and the trailing line.
*/
public BoatGroup (Yacht boat, Color color){
this.boat = boat;
initChildren(color);
}
/**
* Creates a BoatGroup with the boat being the default polygon. The head of the boat should be at point (0,0).
* @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used to tell which
* BoatGroup to update.
* @param color The colour of the boat polygon and the trailing line.
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat polygon.
*/
public BoatGroup (Yacht boat, Color color, double... points)
{
this.boat = boat;
initChildren(color, points);
}
/**
* Creates the javafx objects that will be the in the group by default.
* @param color The colour of the boat polygon and the trailing line.
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat polygon.
*/
private void initChildren (Color color, double... points) {
boatPoly = new Polygon(points);
boatPoly.setFill(color);
teamNameObject = new Text(boat.getShortName());
velocityObject = new Text(String.valueOf(boat.getVelocity()));
teamNameObject.setX(TEAMNAME_X_OFFSET);
teamNameObject.setY(TEAMNAME_Y_OFFSET);
teamNameObject.relocate(teamNameObject.getX(), teamNameObject.getY());
velocityObject.setX(VELOCITY_X_OFFSET);
velocityObject.setY(VELOCITY_Y_OFFSET);
velocityObject.relocate(velocityObject.getX(), velocityObject.getY());
destinationSet = false;
wake = new Wake(0, -BOAT_HEIGHT);
super.getChildren().addAll(teamNameObject, velocityObject, boatPoly);
}
/**
* Creates the javafx objects that will be the in the group by default.
* @param color The colour of the boat polygon and the trailing line.
*/
private void initChildren (Color color) {
initChildren(color,
-BOAT_WIDTH / 2, BOAT_HEIGHT / 2,
0.0, -BOAT_HEIGHT / 2,
BOAT_WIDTH / 2, BOAT_HEIGHT / 2);
}
/**
* Moves the boat and its children annotations from its current coordinates by specified amounts.
* @param dx The amount to move the X coordinate by
* @param dy The amount to move the Y coordinate by
*/
public void moveGroupBy(double dx, double dy, double rotation) {
boatPoly.setLayoutX(boatPoly.getLayoutX() + dx);
boatPoly.setLayoutY(boatPoly.getLayoutY() + dy);
teamNameObject.setLayoutX(teamNameObject.getLayoutX() + dx);
teamNameObject.setLayoutY(teamNameObject.getLayoutY() + dy);
velocityObject.setLayoutX(velocityObject.getLayoutX() + dx);
velocityObject.setLayoutY(velocityObject.getLayoutY() + dy);
wake.setLayoutX(wake.getLayoutX() + dx);
wake.setLayoutY(wake.getLayoutY() + dy);
rotateTo(rotation + currentRotation);
}
/**
* Moves the boat and its children annotations to coordinates specified
* @param x The X coordinate to move the boat to
* @param y The Y coordinate to move the boat to
* @param rotation The heading in degrees from north the boat should rotate to.
*/
public void moveTo (double x, double y, double rotation) {
rotateTo(rotation);
moveTo(x, y);
}
/**
* Moves the boat and its children annotations to coordinates specified
* @param x The X coordinate to move the boat to
* @param y The Y coordinate to move the boat to
*/
public void moveTo (double x, double y) {
boatPoly.setLayoutX(x);
boatPoly.setLayoutY(y);
teamNameObject.setLayoutX(x);
teamNameObject.setLayoutY(y);
velocityObject.setLayoutX(x);
velocityObject.setLayoutY(y);
wake.setLayoutX(x);
wake.setLayoutY(y);
wake.rotate(currentRotation);
}
/**
* Updates the position of all graphics in the BoatGroup based off of the given time interval.
* @param timeInterval The interval, in milliseconds, the boat should update it's position based on.
*/
public void updatePosition (long timeInterval) {
//Calculate the movement of the boat.
if (isMaximized) {
double dx = pixelVelocityX * timeInterval;
double dy = pixelVelocityY * timeInterval;
double rotation = rotationalVelocity * timeInterval;
distanceTravelled += Math.abs(dx) + Math.abs(dy);
moveGroupBy(dx, dy, rotation);
//Draw a new section of the trail every 20 pixels of movement.
if (distanceTravelled > 20) {
distanceTravelled = 0;
if (lastPoint != null) {
Line l = new Line(
lastPoint.getX(),
lastPoint.getY(),
boatPoly.getLayoutX(),
boatPoly.getLayoutY()
);
l.getStrokeDashArray().setAll(3d, 7d);
l.setStroke(boatPoly.getFill());
lineGroup.getChildren().add(l);
}
if (destinationSet) { //Only begin drawing after the first destination is set
lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
}
}
wake.updatePosition(timeInterval);
}
}
/**
* Sets the destination of the boat and the headng it should have once it reaches
* @param newXValue The X co-ordinate the boat needs to move to.
* @param newYValue The Y co-ordinate the boat needs to move to.
* @param rotation Rotation to move graphics to.
* @param raceIds RaceID of the object to move.
*/
public void setDestination (double newXValue, double newYValue, double rotation, double groundSpeed, int... raceIds) {
if (hasRaceId(raceIds)) {
if (setToInitialLocation) {
destinationSet = true;
boat.setVelocity(groundSpeed);
if (currentRotation < 0)
currentRotation = 360 - currentRotation;
double dx = newXValue - boatPoly.getLayoutX();
double dy = newYValue - boatPoly.getLayoutY();
//Check movement is reasonable. Assumes a 1000 * 1000 canvas
if (Math.abs(dx) > 50 || Math.abs(dy) > 50) {
dx = 0;
dy = 0;
moveTo(newXValue, newYValue);
}
pixelVelocityX = dx / expectedUpdateInterval;
pixelVelocityY = dy / expectedUpdateInterval;
rotationalGoal = rotation;
calculateRotationalVelocity();
if (wakeGenerationDelay > 0) {
wake.rotate(rotationalGoal);
rotateTo(rotationalGoal); //Need to test with this removed.
rotationalVelocity = 0;
wakeGenerationDelay--;
} else {
wake.setRotationalVelocity(rotationalVelocity, rotationalGoal, boat.getVelocity());
}
velocityObject.setText(String.format("%.2f m/s", boat.getVelocity()));
} else {
setToInitialLocation = true;
rotationalGoal = rotation;
moveTo(newXValue, newYValue, rotation);
}
}
//If minimized generate lines every 5 calls to set destination.
if (!isMaximized) {
setToInitialLocation = false;
wakeGenerationDelay = 2;
if(setCallCount-- == 0) {
setCallCount = 5;
if (lastPoint != null) {
Line l = new Line(
lastPoint.getX(),
lastPoint.getY(),
newXValue,
newYValue
);
l.getStrokeDashArray().setAll(3d, 7d);
l.setStroke(boatPoly.getFill());
lineStorage.add(l);
}
if (destinationSet) { //Only begin drawing after the first destination is set
lastPoint = new Point2D(newXValue, newYValue);
}
}
}
}
public void setDestination (double newXValue, double newYValue, double groundSpeed, int... raceIDs) {
destinationSet = true;
if (hasRaceId(raceIDs)) {
double rotation = Math.abs(
Math.toDegrees(
Math.atan(
(newYValue - boatPoly.getLayoutY()) / (newXValue - boatPoly.getLayoutX())
)
)
);
setDestination(newXValue, newYValue, rotation, groundSpeed, raceIDs);
}
}
public void rotateTo (double rotation) {
currentRotation = rotation;
boatPoly.getTransforms().setAll(new Rotate(rotation));
}
public void forceRotation () {
rotateTo (rotationalGoal);
wake.rotate(rotationalGoal);
}
public void setTeamNameObjectVisible(Boolean visible) {
teamNameObject.setVisible(visible);
}
public void setVelocityObjectVisible(Boolean visible) {
velocityObject.setVisible(visible);
}
public void setLineGroupVisible(Boolean visible) {
lineGroup.setVisible(visible);
}
public void setWakeVisible(Boolean visible) {
wake.setVisible(visible);
}
public Yacht getBoat() {
return boat;
}
/**
* Returns true if this BoatGroup contains at least one of the given IDs.
*
* @param raceIds The ID's to check the BoatGroup for.
* @return True if the BoatGroup contains at east one of the given IDs, false otherwise.
*/
public boolean hasRaceId (int... raceIds) {
for (int id : raceIds) {
if (id == boat.getSourceID())
return true;
}
return false;
}
/**
* Returns all raceIds associated with this group. For BoatGroups the ID's are for the boat.
*
* @return An array containing all ID's associated with this RaceObject.
*/
public int[] getRaceIds () {
return new int[] {boat.getSourceID()};
}
/**
* Due to javaFX limitations annotations associated with a boat that you want to appear below all boats in the
* Z-axis need to be pulled out of the BoatGroup and added to the parent group of the BoatGroups. This function
* returns these annotations as a group.
*
* @return A group containing low priority annotations.
*/
public Group getLowPriorityAnnotations () {
Group group = new Group();
group.getChildren().addAll(wake, lineGroup);
return group;
}
/**
* Use this function to let the BoatGroup know about the stage it is in. If it knows about it's stage then it will
* listen to the iconified property of that stage and change it's behaviour upon minimization. Without setting the
* Stage there is guarantee that the BoatGroup will draw properly when the stage is minimized.
*
* @param stage The stage that the BoatGroup is added to.
*/
public void setStage (Stage stage) {
/* TODO: 4/05/17 cir27 - Find a way to get the stage to this point. Need to pass it through multiple controllers.
App.start() -> Controller.setContentPane -> RaceViewController -> CanvasController
*/
this.stage = stage;
this.stage.iconifiedProperty().addListener(e -> {
isMaximized = !stage.isIconified();
if (!lineStorage.isEmpty()) {
lineGroup.getChildren().addAll(lineStorage);
lineStorage.clear();
}
});
}
}
+2 -2
View File
@@ -3,10 +3,10 @@ package seng302.models;
import javafx.scene.paint.Color;
/**
* Created by ryan_ on 16/03/2017.
* Enum for randomly generating colours.
*/
public enum Colors {
RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE;
RED, PERU, SEAGREEN, GREEN, BLUE, PURPLE;
static Integer index = 0;
-172
View File
@@ -1,172 +0,0 @@
package seng302.models;
import seng302.models.mark.Mark;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Event class containing the time of specific event, related team/boat, and
* event location such as leg.
*/
public class Event {
private Double time; // Time the event occurs
private Yacht boat;
private boolean isFinishingEvent = false; // This event occurs when a boat finishes the race
private Mark mark1; // This mark
private Mark mark2; // Next mark
private int markPosInRace; // the position of the current mark in the race course
private double heading;
private final double ORIGIN_LAT = 32.320504;
private final double ORIGIN_LON = -64.857063;
private final double SCALE = 16000;
/**
* Event class containing the time of specific event, related team/boat, and
* event location such as leg.
*
* @param eventTime, what time the event happens
* @param eventBoat, the boat that the event belongs to
*/
public Event(Double eventTime, Yacht eventBoat, Mark mark1, Mark mark2, int markPosInRace) {
this.time = eventTime;
this.boat = eventBoat;
this.mark1 = mark1;
this.mark2 = mark2;
this.markPosInRace = markPosInRace;
this.heading = angleFromCoordinate(mark1, mark2);
}
/**
* Event class containing the time of specific event, related team/boat, and
* event location such as leg.
*
* @param eventTime, what time the event happens
* @param eventBoat, the boat that the event belongs to
*/
public Event(Double eventTime, Yacht eventBoat, Mark mark1, int markPosInRace) {
this.time = eventTime;
this.boat = eventBoat;
this.mark1 = mark1;
this.markPosInRace = markPosInRace;
this.isFinishingEvent = true;
}
public double getTime() {
return this.time;
}
public void setTime(double eventTime) {
this.time = eventTime;
}
/**
* Gets the time in a formatted string
*
* @return the string of time
*/
public String getTimeString() {
return (new SimpleDateFormat("mm:ss:SSS")).format(new Date(time.longValue()));
}
public Yacht getBoat() {
return this.boat;
}
public void setBoat(Yacht eventBoat) {
this.boat = eventBoat;
}
public boolean getIsFinishingEvent() {
return this.isFinishingEvent;
}
/**
* Get a string that contains the timestamp and course information for this event
*
* @return A string that details what happened in this event
*/
public String getEventString() {
// This event is a boat finishing the race
if (this.isFinishingEvent) {
return (this.getTimeString() + ", " + this.getBoat().getBoatName() + " finished the race");
}
// System.out.println(this.getDistanceBetweenMarks());
return (this.getTimeString() + ", " + this.getBoat().getBoatName() + " passed " + this.mark1.getName() + " going heading " + this.getBoatHeading() + "°");
}
/**
* @return the distance between the two marks
*/
public double getDistanceBetweenMarks() {
double earth_radius = 6378.137;
double dLat = this.mark2.getLatitude() * Math.PI / 180 - this.mark1.getLatitude() * Math.PI / 180;
double dLon = this.mark2.getLongitude() * Math.PI / 180 - this.mark1.getLongitude() * Math.PI / 180;
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.mark1.getLatitude() * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
double d = earth_radius * c;
return d * 1000;
}
/**
* Calculates current boat heading direction.
* @return the boats heading as degree. vertical upward is 0 degree, and degree goes up clockwise.
*/
public double getBoatHeading() {
if (mark2 == null){
return 0.0;
}
double x1 = (mark1.getLongitude() - ORIGIN_LON) * SCALE;
double y1 = (ORIGIN_LAT - mark1.getLatitude()) * SCALE;
double x2 = (mark2.getLongitude() - ORIGIN_LON) * SCALE;
double y2 = (ORIGIN_LAT - mark2.getLatitude()) * SCALE;
double headingRadians = Math.atan2(y2-y1, x2-x1);
if (headingRadians < 0){
headingRadians += 2 * Math.PI;
}
// Convert back to degrees, and flip 180 degrees
// return ((headingRadians) * 180) / Math.PI;
return (Math.toDegrees(headingRadians) + 90) % 360;
}
/**
* Calculates the angle between to angular co-ordinates on a sphere.
*
* @param geoPointOne first geographical location
* @param geoPointTwo second geographical location
* @return the angle from point one to point two
*/
private Double angleFromCoordinate(Mark geoPointOne, Mark geoPointTwo) {
if (geoPointTwo == null)
return null;
double x1 = geoPointOne.getLatitude();
double y1 = -geoPointOne.getLongitude();
double x2 = geoPointTwo.getLatitude();
double y2 = -geoPointTwo.getLongitude();
return Math.toDegrees(Math.atan2(x2-x1, y2-y1));
}
public double getHeading() {
return heading;
}
public Mark getThisMark() {
return this.mark1;
}
public int getMarkPosInRace() {
return markPosInRace;
}
}
-116
View File
@@ -1,116 +0,0 @@
package seng302.models;
import seng302.models.mark.SingleMark;
/**
* Represents the leg of a race.
*/
public class Leg {
private int heading;
private int distance;
private boolean isFinishingLeg;
private SingleMark startingSingleMark;
/**
* Create a new leg
*
* @param heading, the magnetic heading of this leg
* @param distance, the total distance of this leg in meters
* @param singleMark, the singleMark this leg starts on
*/
public Leg(int heading, int distance, SingleMark singleMark) {
this.heading = heading;
this.distance = distance;
this.startingSingleMark = singleMark;
this.isFinishingLeg = false;
}
/**
* Create a new leg
*
* @param heading, the magnetic heading of this leg
* @param distance, the total distance of this leg in meters
* @param markerName, the name of the marker this leg starts on
*/
public Leg(int heading, int distance, String markerName) {
this.heading = heading;
this.distance = distance;
this.startingSingleMark = new SingleMark(markerName);
this.isFinishingLeg = false;
}
/**
* Get the heading of this leg
* @return int
*/
public int getHeading() {
return this.heading;
}
/**
* Set the heading for this leg
* @param heading
*/
public void setHeading(int heading) {
this.heading = heading;
}
/**
* Get the total distance of this leg in meters
* @return int
*/
public int getDistance() {
return this.distance;
}
/**
* Set the distance of this leg in meters
* @param distance
*/
public void setDistance(int distance) {
this.distance = distance;
}
/**
* Returns the marker this leg started on
* @return SingleMark
*/
public SingleMark getMarker() {
return this.startingSingleMark;
}
/**
* Set the singleMark this leg starts on
* @param singleMark
*/
public void setMarker(SingleMark singleMark) {
this.startingSingleMark = singleMark;
}
/**
* Returns the name of the marker this leg started on
* @return String
*/
public String getMarkerLabel() {
return this.startingSingleMark.getName();
}
/**
* Specify whether or not the race finishes on this leg
*
* @param isFinishingLeg whether or not the race finishes on this leg
*/
public void setFinishingLeg(boolean isFinishingLeg) {
this.isFinishingLeg = isFinishingLeg;
}
/**
* Returns whether or not the race finishes after this leg
* @return true if this the race finishes after this leg
*/
public boolean getIsFinishingLeg() {
return this.isFinishingLeg;
}
}
+72
View File
@@ -0,0 +1,72 @@
package seng302.models;
import javafx.scene.paint.Color;
import java.io.IOException;
import java.net.Socket;
import java.nio.channels.SocketChannel;
/**
* A Class defining a player and their respective details in the game as held by the model
* Created by wmu16 on 10/07/17.
*/
public class Player {
private Socket socket;
private Yacht yacht;
private Integer lastMarkPassed;
public Player(Socket socket, Yacht yacht) {
this.socket = socket;
this.yacht = yacht;
}
public Socket getSocket() {
return socket;
}
public Integer getLastMarkPassed() {
return lastMarkPassed;
}
public void setLastMarkPassed(Integer lastMarkPassed) {
this.lastMarkPassed = lastMarkPassed;
}
public Yacht getYacht() {
return yacht;
}
@Override
public String toString() {
String playerAddress = null;
if (socket == null){
return "Disconnected Player";
}
playerAddress = socket.getRemoteSocketAddress().toString();
return playerAddress;
}
@Override
public boolean equals(Object obj) {
if (obj == null){
return false;
}
if (!(obj instanceof Player)){
return false;
}
return ((Player) obj).socket.equals(socket);
}
@Override
public int hashCode(){
return socket.hashCode();
}
}
@@ -0,0 +1,184 @@
package seng302.models;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
/**
* A static class for parsing and storing the polars. Will parse the whole polar table and also store the optimised
* upwind and downwind in separate tables here as well
* Created by wmu16 on 22/05/17.
*/
public final class PolarTable {
//A Polar table will consist of a wind speed key to a hashmap value of pairs of wind angles and boat speeds
private static HashMap<Double, HashMap<Double, Double>> polarTable;
private static HashMap<Double, HashMap<Double, Double>> upwindOptimal;
private static HashMap<Double, HashMap<Double, Double>> downwindOptimal;
private static int upTwaIndex;
private static int dnTwaIndex;
/**
* Iterates through each row of the polar table, in pairs, to extract the row into a hashmap of angle to boat speed.
* These angle boatspeed hashmaps are then added to an outer hashmap at the end of wind speed key to each row hashmap
* as a value
*/
public static void parsePolarFile(InputStream polarFile) {
polarTable = new HashMap<>();
upwindOptimal = new HashMap<>();
downwindOptimal = new HashMap<>();
String line;
Boolean isHeaderLine = true;
try (BufferedReader br = new BufferedReader(new InputStreamReader(polarFile))) {
while ((line = br.readLine()) != null) {
String[] thisLine = line.split(",");
//Initial line in file
if (isHeaderLine) {
deduceHeaders(thisLine);
isHeaderLine = false;
} else {
HashMap<Double, Double> thisPolar = new HashMap<>();
HashMap<Double, Double> thisUpWindPolar = new HashMap<>();
HashMap<Double, Double> thisDnWindPolar = new HashMap<>();
Double thisWindSpeed = Double.parseDouble(thisLine[0]);
// -3 <== -1 for length -1, and a further -2 as we iterate in pairs of 2 so finish before final 2
for (int i = 1; i < thisLine.length; i += 2) {
Double thisWindAngle = Double.parseDouble(thisLine[i]);
Double thisBoatSpeed = Double.parseDouble(thisLine[i + 1]);
thisPolar.put(thisWindAngle, thisBoatSpeed);
if (i == upTwaIndex) {
thisUpWindPolar.put(thisWindAngle, thisBoatSpeed);
} else if (i == dnTwaIndex) {
thisDnWindPolar.put(thisWindAngle, thisBoatSpeed);
}
}
polarTable.put(thisWindSpeed, thisPolar);
upwindOptimal.put(thisWindSpeed, thisUpWindPolar);
downwindOptimal.put(thisWindSpeed, thisDnWindPolar);
}
}
} catch (IOException e) {
System.out.println("[PolarTable] IO exception");
}
}
/**
* Parses the header line of a polar file
* @param thisLine The line which is the header of a polar file
*/
private static void deduceHeaders(String[] thisLine) {
for (int i = 0; i < thisLine.length; i++) {
String thisItem = thisLine[i];
if (thisItem.toLowerCase().startsWith("uptwa")) {
upTwaIndex = i;
}
else if (thisItem.toLowerCase().startsWith("dntwa")) {
dnTwaIndex = i;
}
}
}
/**
* @return The entire polar table
*/
public static HashMap<Double, HashMap<Double, Double>> getPolarTable() {
return polarTable;
}
/**
* @return The polar table just containing the optimal upwind values
*/
public static HashMap<Double, HashMap<Double, Double>> getUpwindOptimal() {
return upwindOptimal;
}
/**
* @return The polar table just containing the optimal downwind values
*/
public static HashMap<Double, HashMap<Double, Double>> getDownwindOptimal() {
return downwindOptimal;
}
/**
* Will raise an exception if a polar table has just one row of data
* @param thisWindSpeed The current wind speed
* @return HashMap containing just the optimal upwind angle and resulting boat speed
*/
public static HashMap<Double, Double> getOptimalUpwindVMG(Double thisWindSpeed) {
Double polarWindSpeed = getClosestWindSpeedInPolar(thisWindSpeed);
return upwindOptimal.get(polarWindSpeed);
}
/**
* Will raise an exception if a polar table has just one row of data
* @param thisWindSpeed The current wind speed
* @return HashMap containing just the optimal downwind angle and resulting boat speed
*/
public static HashMap<Double, Double> getOptimalDownwindVMG(Double thisWindSpeed) {
Double polarWindSpeed = getClosestWindSpeedInPolar(thisWindSpeed);
return downwindOptimal.get(polarWindSpeed);
}
public static Double getBoatSpeed(Double thisWindSpeed, Double thisHeading) {
Double polarWindSpeed = getClosestWindSpeedInPolar(thisWindSpeed);
Double polarAngle = getClosestAngleInPolar(polarTable.get(polarWindSpeed), thisHeading);
return polarTable.get(polarWindSpeed).get(polarAngle);
}
public static Double getClosestWindSpeedInPolar(Double thisWindSpeed) {
Double smallestDif = Double.POSITIVE_INFINITY;
Double closestWind = 0d;
for (Double polarWindSpeed : polarTable.keySet()) {
Double difference = Math.abs(polarWindSpeed - thisWindSpeed);
if (difference < smallestDif) {
smallestDif = difference;
closestWind = polarWindSpeed;
}
}
return closestWind;
}
public static Double getClosestAngleInPolar(HashMap<Double, Double> thisWindSpeedPolar, Double thisHeading) {
Double smallestDif = Double.POSITIVE_INFINITY;
Double closestAngle = 0d;
for (Double polarAngle : thisWindSpeedPolar.keySet()) {
Double difference = Math.abs(polarAngle - thisHeading);
if (difference < smallestDif) {
smallestDif = difference;
closestAngle = polarAngle;
}
}
return closestAngle;
}
}
-198
View File
@@ -1,198 +0,0 @@
package seng302.models;
import seng302.models.mark.Mark;
import java.util.*;
/**
* Race class containing the boats and legs in the race
* Created by mra106 on 8/3/2017.
*/
public class Race {
private ArrayList<Yacht> boats; // The boats in the race
private ArrayList<Yacht> finishingOrder; // The order in which the boats finish the race
private HashMap<Yacht, List> events = new HashMap<>(); // The events that occur in the race
private List<Mark> course; // Marks in the race
private long startTime = 0;
private double timeScale = 1;
private boolean raceFinished = false; // Race is finished
private int raceTime = -2; // Current time in the race
/**
* Race class containing the boats and legs in the race
*/
public Race() {
this.boats = new ArrayList<>();
this.finishingOrder = new ArrayList<>();
this.course = new ArrayList<>();
}
/**
* Add a boat to the race
*
* @param boat, the boat to add
*/
public void addBoat(Yacht boat) {
boats.add(boat);
}
/**
* Returns a list of boats in a random order
*
* @return a list of boats
*/
public Yacht[] getShuffledBoats() {
// Shuffle the list of boats
long seed = System.nanoTime();
Collections.shuffle(this.boats, new Random(seed));
return boats.toArray(new Yacht[boats.size()]);
}
/**
* Returns a list of boats in the order that they
* finished the race (position 0 is first place)
*
* @return a list of boats
*/
public Yacht[] getFinishedBoats() {
return this.finishingOrder.toArray(new Yacht[this.finishingOrder.size()]);
}
/**
* Returns a list of boats in the race
*
* @return a list of the boats competing in the race
*/
public Yacht[] getBoats() {
return boats.toArray(new Yacht[boats.size()]);
}
/**
* Sets time scale
*
* @param timeScale
*/
public void setTimeScale(double timeScale) {
this.timeScale = timeScale;
}
/**
* Generate all events that will happen during the race.
*/
private void generateEvents() {
for (Yacht boat : this.boats) {
double totalDistance = 0;
int numberOfMarks = this.course.size();
for (int i = 0; i < numberOfMarks; i++) {
Double time = (totalDistance / boat.getVelocity() / timeScale);
// If there are singleMarks after this event
if (i < numberOfMarks - 1) {
Event event = new Event(time, boat, course.get(i), course.get(i + 1), i);
try {
events.get(boat).add(event);
} catch (NullPointerException e) {
events.put(boat, new ArrayList<>(Arrays.asList(event)));
}
totalDistance += event.getDistanceBetweenMarks();
//System.out.println(totalDistance);
//System.out.println(boat.getVelocity());
}
// There are no more marks after this event
else{
Event event = new Event(time, boat, course.get(i), i);
events.get(boat).add(event);
}
}
}
}
/**
* Starts a race and generates all events for the race.
*/
public void startRace() {
// record start time.
this.startTime = System.currentTimeMillis();
generateEvents();
}
/**
* Set the race course
* @param course a list of marks in the course
*/
public void addCourse(List<Mark> course) {
this.course = course;
}
/**
* Get a list of marks in the course
* @return
*/
public List<Mark> getCourse() {
return course;
}
/**
* Get a map of the events in the race
* @return
*/
public HashMap<Yacht, List> getEvents() {
return events;
}
/**
* Set a boat as finished
* @param boat The boat that has finished the race/home/cosc/student/wmu16
*/
public void setBoatFinished(Yacht boat){
this.finishingOrder.add(boat);
}
/**
* Set the race as finished
*/
public void setRaceFinished(){
this.raceFinished = true;
}
/**
* Return whether or not the race is finished
* @return true if the race is finished
*/
public boolean isRaceFinished(){
return this.raceFinished;
}
/**
* Set the race time
* @param raceTime the race time in seconds
*/
public void setRaceTime(int raceTime){
this.raceTime = raceTime;
}
/**
* Return the race time
* @return the race time in seconds
*/
public int getRaceTime(){
return this.raceTime;
}
/**
* Increment the race time by one second
*/
public void incrementRaceTime(){
this.raceTime += this.timeScale;
}
}
@@ -1,87 +0,0 @@
package seng302.models;
import javafx.geometry.Point2D;
import javafx.scene.Group;
/**
* RaceObject defines the behaviour that animated objects whose position is updated from a yacht race data stream must
* adhere to.
*/
public abstract class RaceObject extends Group {
//Time between sections of race
protected static double expectedUpdateInterval = 200;
protected double rotationalGoal;
protected double currentRotation;
protected double rotationalVelocity;
protected double pixelVelocityX;
protected double pixelVelocityY;
public Point2D getPosition () {
return new Point2D(super.getLayoutX(), getLayoutY());
}
public static double getExpectedUpdateInterval() {
return expectedUpdateInterval;
}
/**
*
*/
public static void setExpectedUpdateInterval(double expectedUpdateInterval) {
RaceObject.expectedUpdateInterval = expectedUpdateInterval;
}
/**
* Calculates the rotational velocity required to reach the rotationalGoal from the currentRotation.
*/
protected void calculateRotationalVelocity () {
if (Math.abs(rotationalGoal - currentRotation) > 180) {
if (rotationalGoal - currentRotation >= 0) {
this.rotationalVelocity = ((rotationalGoal - currentRotation) - 360) / expectedUpdateInterval;
} else {
this.rotationalVelocity = (360 + (rotationalGoal - currentRotation)) / expectedUpdateInterval;
}
} else {
this.rotationalVelocity = (rotationalGoal - currentRotation) / expectedUpdateInterval;
}
//Sometimes the rotation is too large to be realistic. In that case just do it instantly.
if (Math.abs(rotationalVelocity) > 1) {
rotationalVelocity = 0;
rotateTo(rotationalGoal);
}
}
/**
* Sets the destination of everything within the RaceObject that has an ID in the array raceIds. The destination is
* set to the co-ordinates (x, y) with the given rotation.
* @param x X co-ordinate to move the graphics to.
* @param y Y co-ordinate to move the graphics to.
* @param rotation Rotation to move graphics to.
* @param raceIds RaceID of the object to move.
*/
public abstract void setDestination (double x, double y, double rotation, double groundSpeed, int... raceIds);
/**
* Sets the destination of everything within the RaceObject that has an ID in the array raceIds. The destination is
* set to the co-ordinates (x, y).
* @param x X co-ordinate to move the graphic to.
* @param y Y co-ordinate to move the graphic to.
* @param raceIds RaceID to the object to move.
*/
public abstract void setDestination (double x, double y, double groundSpeed, int... raceIds);
public abstract void updatePosition (long timeInterval);
public abstract void moveTo (double x, double y, double rotation);
public abstract void moveTo (double x, double y);
public abstract void moveGroupBy(double x, double y, double rotation);
public abstract void rotateTo (double rotation);
public abstract boolean hasRaceId (int... raceIds);
public abstract int[] getRaceIds ();
}
@@ -1,31 +0,0 @@
package seng302.models;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
/**
* Created by zyt10 on 17/03/17.
* this class is literally just to associate a timeline with a DoubleProperty x and y
*/
public class TimelineInfo {
private Timeline timeline;
private DoubleProperty x;
private DoubleProperty y;
public TimelineInfo(Timeline timeline, DoubleProperty x, DoubleProperty y) {
this.timeline = timeline;
this.x = x;
this.y = y;
}
public Timeline getTimeline() {
return timeline;
}
public DoubleProperty getX() {
return x;
}
public DoubleProperty getY() {
return y;
}
}
-107
View File
@@ -1,107 +0,0 @@
package seng302.models;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.transform.Rotate;
/**
* By default wake is a group containing 5 arcs. Each arc starts from the same point. Each arc is larger and more
* transparent than the last. On calling updatePositions() arcs rotate at velocities given by setRotationalVelocity().
* The larger and more transparent an arc is the longer the delay before it rotates at the latest velocity. It is
* assumed that rotationalVelocities() are set regularly as wakes do not stop rotating and an array of velocities needs
* to be populated for the class to work as expected.
*/
class Wake extends Group {
private int numWakes = 5;
private double[] velocities = new double[13];
private Arc[] arcs = new Arc[numWakes];
private double[] rotations = new double[numWakes];
private int[] velocityIndices = new int[numWakes];
private double sum = 0;
private static double max;
/**
* Create a wake at the given location.
* @param startingX x location where the tip of wake arcs will be.
* @param startingY y location where the tip of wake arcs will be.
*/
Wake(double startingX, double startingY) {
super.setLayoutX(startingX);
super.setLayoutY(startingY);
Arc arc;
for (int i = 0; i < numWakes; i++) {
//Default triangle is -110 deg out of phase with a default wake and has angle of 40 deg.
arc = new Arc(0,0,0,0,-110,40);
//Opacity increases from 0.5 -> 0 evenly over the 5 wake arcs.
arc.setFill(new Color(0.18, 0.7, 1.0, 1.0 + -0.175 * i));
arc.setType(ArcType.ROUND);
arcs[i] = arc;
}
super.getChildren().addAll(arcs);
}
/**
* Sets the rotationalVelocity of each arc. Each arc is 3 velocities behind the next smallest arc. The smallest uses
* the latest given velocity.
* @param rotationalVelocity The rotationalVelocity the wake should move at.
* @param rotationGoal Where the wake will rotate to if the wake is calculated to be on a straight section. This is
* used to prevent desynchronisation with the Boat polygon.
* @param velocity The real world velocity of the boat in m/s.
*/
void setRotationalVelocity (double rotationalVelocity, double rotationGoal, double velocity) {
sum -= Math.abs(velocities[(velocityIndices[0] + 10) % 13]);
sum += Math.abs(rotationalVelocity);
max = Math.max(max, rotationalVelocity);
if (sum < (max / 3))
rotate (rotationGoal); //In relatively straight segments the wake snaps to match the boats current position.
//This stops the wake from eventually becoming out of sync with the boat.
//This accounts for rogue rotations that are greater than what would be realistic. Value is kinda rough.
//Basically just for our internal mock.
if (Math.abs(rotationalVelocity) > 0.05) {
rotationalVelocity = 0;
rotate(rotationGoal);
}
//Update the index of the array of recent velocities that each wake uses. Each wake is 3 velocities behind the
//next smallest wake.
velocityIndices[0] = (13 + (velocityIndices[0] - 1) % 13) % 13;
velocities[velocityIndices[0]] = rotationalVelocity;
for (int i = 1; i < numWakes; i++)
velocityIndices[i] = (velocityIndices[0] + 3 * i) % 13;
//Scale wakes based on velocity.
double baseRad = 20;
double rad;
for (Arc arc :arcs) {
rad = baseRad + velocity;
arc.setRadiusX(rad);
arc.setRadiusY(rad);
baseRad += 5 + (velocity / 2);
}
}
/**
* Arcs rotate based on the distance they would have travelled over the supplied time interval.
* @param timeInterval the time interval, in microseconds, that the wake should move.
*/
void updatePosition (long timeInterval) {
for (int i = 0; i < numWakes; i++) {
rotations[i] = rotations[i] + velocities[velocityIndices[i]] * timeInterval;
arcs[i].getTransforms().setAll(new Rotate(rotations[i]));
}
}
/**
* Rotate all wakes to the given rotation.
* @param rotation the from north angle in degrees to rotate to.
*/
void rotate (double rotation) {
for (int i = 0; i < arcs.length; i++) {
rotations[i] = rotation;
arcs[i].getTransforms().setAll(new Rotate(rotation));
}
}
}
+263 -26
View File
@@ -1,9 +1,16 @@
package seng302.models;
import javafx.scene.paint.Color;
import static seng302.utilities.GeoUtility.getGeoCoordinate;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import javafx.scene.paint.Color;
import seng302.client.ClientPacketParser;
import seng302.controllers.RaceViewController;
import seng302.gameServer.GameState;
import seng302.models.mark.Mark;
import seng302.utilities.GeoPoint;
/**
* Yacht class for the racing boat.
@@ -12,32 +19,68 @@ import java.text.SimpleDateFormat;
* also done outside Boat class because some old variables are not used anymore.
*/
public class Yacht {
private final Double TURN_STEP = 5.0;
private Double lastHeading;
private Boolean sailIn;
// Used in boat group
private Color colour;
private double velocity;
private Integer markLastPast;
private String boatType;
private Integer sourceID;
private Integer sourceId;
private String hullID; //matches HullNum in the XML spec.
private String shortName;
private String boatName;
private String country;
// Situational data
// Boat status
private Integer boatStatus;
private Integer legNumber;
private Integer penaltiesAwarded;
private Integer penaltiesServed;
private Long estimateTimeAtNextMark;
private Long estimateTimeAtFinish;
private String position;
private GeoPoint location;
private Double heading;
private Double velocity;
private Long timeTillNext;
private Long markRoundTime;
// Mark rounding
private Mark lastMarkRounded;
private Mark nextMark;
/**
* @param location latlon location of the boat stored in a geopoint
* @param heading heading of the boat in degrees from 0 to 365 with 0 being north
*/
public Yacht(GeoPoint location, Double heading) {
this.location = location;
this.heading = heading;
this.velocity = 0.0;
this.sailIn = false;
}
/**
* Used in EventTest and RaceTest.
*
* @param boatName Create a yacht object with name.
*/
public Yacht (String boatName) {
public Yacht(String boatName, String shortName, GeoPoint location, Double heading) {
this.boatName = boatName;
this.shortName = shortName;
this.location = location;
this.heading = heading;
this.velocity = 0.0;
this.sailIn = false;
}
/**
@@ -51,34 +94,191 @@ public class Yacht {
this.boatName = boatName;
this.velocity = boatVelocity;
this.shortName = shortName;
this.sourceID = id;
this.sourceId = id;
this.sailIn = false;
}
public Yacht(String boatType, Integer sourceID, String hullID, String shortName, String boatName, String country) {
public Yacht(String boatType, Integer sourceId, String hullID, String shortName,
String boatName, String country) {
this.boatType = boatType;
this.sourceID = sourceID;
this.sourceId = sourceId;
this.hullID = hullID;
this.shortName = shortName;
this.boatName = boatName;
this.country = country;
this.position = "-";
this.sailIn = false;
this.location = new GeoPoint(57.670341, 11.826856);
this.heading = 120.0; //In degrees
this.velocity = 0d; //in mms-1
}
/**
* @param timeInterval since last update in milliseconds
*/
public void update(Long timeInterval) {
Double secondsElapsed = timeInterval / 1000000.0;
Double windSpeedKnots = GameState.getWindSpeedKnots();
Double trueWindAngle = Math.abs(GameState.getWindDirection() - heading);
Double boatSpeedInKnots = PolarTable.getBoatSpeed(windSpeedKnots, trueWindAngle);
Double maxBoatSpeed = boatSpeedInKnots / ClientPacketParser.MS_TO_KNOTS * 1000;
if (sailIn && velocity <= maxBoatSpeed && maxBoatSpeed != 0d) {
if (velocity < maxBoatSpeed) {
velocity += maxBoatSpeed / 15; // Acceleration
}
if (velocity > maxBoatSpeed) {
velocity = maxBoatSpeed; // Prevent the boats from exceeding top speed
}
} else { // Deceleration
if (velocity > 0d) {
if (maxBoatSpeed != 0d) {
velocity -= maxBoatSpeed / 600;
} else {
velocity -= velocity / 100;
}
if (velocity < 0) {
velocity = 0d;
}
}
}
Double metersCovered = velocity * secondsElapsed;
location = getGeoCoordinate(location, heading, metersCovered);
}
public Double getHeading() {
return heading;
}
public void adjustHeading(Double amount) {
Double newVal = heading + amount;
lastHeading = heading;
// TODO: 24/07/17 wmu16 - '%' in java does remainder, we need modulo. All this must be changed here, this is why we have neg values!
heading = (double) Math.floorMod(newVal.longValue(), 360L);
}
public void tackGybe(Double windDirection) {
Double normalizedHeading = normalizeHeading();
adjustHeading(-2 * normalizedHeading);
}
public void toggleSailIn() {
sailIn = !sailIn;
}
public void turnUpwind() {
Double normalizedHeading = normalizeHeading();
if (normalizedHeading == 0) {
if (lastHeading < 180) {
adjustHeading(-TURN_STEP);
} else {
adjustHeading(TURN_STEP);
}
} else if (normalizedHeading == 180) {
if (lastHeading < 180) {
adjustHeading(TURN_STEP);
} else {
adjustHeading(-TURN_STEP);
}
} else if (normalizedHeading < 180) {
adjustHeading(-TURN_STEP);
} else {
adjustHeading(TURN_STEP);
}
}
public void turnDownwind() {
Double normalizedHeading = normalizeHeading();
if (normalizedHeading == 0) {
if (lastHeading < 180) {
adjustHeading(TURN_STEP);
} else {
adjustHeading(-TURN_STEP);
}
} else if (normalizedHeading == 180) {
if (lastHeading < 180) {
adjustHeading(-TURN_STEP);
} else {
adjustHeading(TURN_STEP);
}
} else if (normalizedHeading < 180) {
adjustHeading(TURN_STEP);
} else {
adjustHeading(-TURN_STEP);
}
}
public void turnToVMG() {
Double normalizedHeading = normalizeHeading();
Double optimalHeading;
HashMap<Double, Double> optimalPolarMap;
if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind
optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots());
optimalHeading = optimalPolarMap.keySet().iterator().next();
} else {
optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots());
optimalHeading = optimalPolarMap.keySet().iterator().next();
}
// Take optimal heading and turn into correct
optimalHeading =
optimalHeading + (double) Math.floorMod(GameState.getWindDirection().longValue(), 360L);
turnTowardsHeading(optimalHeading);
}
private void turnTowardsHeading(Double newHeading) {
System.out.println(newHeading);
if (heading < 90 && newHeading > 270) {
adjustHeading(-TURN_STEP);
} else {
if (heading < newHeading) {
adjustHeading(TURN_STEP);
} else {
adjustHeading(-TURN_STEP);
}
}
}
private Double normalizeHeading() {
Double normalizedHeading = heading - GameState.windDirection;
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L);
return normalizedHeading;
}
public String getBoatType() {
return boatType;
}
public Integer getSourceID() {
return sourceID;
public Integer getSourceId() {
//@TODO Remove and merge with Creating Game Loop
if (sourceId == null) return 0;
return sourceId;
}
public String getHullID() {
if (hullID == null) return "";
return hullID;
}
public String getShortName() {
return shortName;
}
public String getBoatName() {
return boatName;
}
public String getCountry() {
if (country == null) return "";
return country;
}
@@ -95,6 +295,10 @@ public class Yacht {
}
public void setLegNumber(Integer legNumber) {
if (colour != null && position != "-" && legNumber != this.legNumber&& RaceViewController.sparkLineStatus(
sourceId)) {
RaceViewController.updateYachtPositionSparkline(this, legNumber);
}
this.legNumber = legNumber;
}
@@ -114,14 +318,8 @@ public class Yacht {
this.penaltiesServed = penaltiesServed;
}
public Long getEstimateTimeAtNextMark() {
// DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
// return format.format(estimateTimeAtNextMark);
return estimateTimeAtNextMark;
}
public void setEstimateTimeAtNextMark(Long estimateTimeAtNextMark) {
this.estimateTimeAtNextMark = estimateTimeAtNextMark;
timeTillNext = estimateTimeAtNextMark;
}
public String getEstimateTimeAtFinish() {
@@ -149,19 +347,58 @@ public class Yacht {
this.colour = colour;
}
public double getVelocity() {
return velocity;
}
public void setVelocity(double velocity) {
this.velocity = velocity;
}
public Integer getMarkLastPast() {
return markLastPast;
public void setMarkRoundingTime(Long markRoundingTime) {
this.markRoundTime = markRoundingTime;
}
public void setMarkLastPast(Integer markLastPast) {
this.markLastPast = markLastPast;
public double getVelocityMMS() {
return velocity;
}
public Double getVelocityKnots() {
return velocity / 1000 * ClientPacketParser.MS_TO_KNOTS;
}
public Long getTimeTillNext() {
return timeTillNext;
}
public Long getMarkRoundTime() {
return markRoundTime;
}
public Mark getLastMarkRounded() {
return lastMarkRounded;
}
public void setLastMarkRounded(Mark lastMarkRounded) {
this.lastMarkRounded = lastMarkRounded;
}
public void setNextMark(Mark nextMark) {
this.nextMark = nextMark;
}
public Mark getNextMark(){
return nextMark;
}
public Boolean getSailIn() {
return sailIn;
}
@Override
public String toString() {
return boatName;
}
public GeoPoint getLocation() {
return location;
}
}
@@ -0,0 +1,44 @@
package seng302.models.map;
/**
* The Boundary class represents a rectangle territorial boundary on a map. It
* contains four extremity double values(N, E, S, W). N and S are represented as
* latitudes in radians. E and W are represented as longitudes in radians.
*
* Created by Haoming on 10/5/17
*/
public class Boundary {
private double northLat, eastLng, southLat, westLng;
public Boundary(double northLat, double eastLng, double southLat, double westLng) {
this.northLat = northLat;
this.eastLng = eastLng;
this.southLat = southLat;
this.westLng = westLng;
}
double getCentreLat() {
return (northLat + southLat) / 2;
}
double getCentreLng() {
return (eastLng + westLng) / 2;
}
double getNorthLat() {
return northLat;
}
double getEastLng() {
return eastLng;
}
double getSouthLat() {
return southLat;
}
double getWestLng() {
return westLng;
}
}
@@ -0,0 +1,106 @@
package seng302.models.map;
import javafx.geometry.Point2D;
import javafx.scene.image.Image;
import seng302.utilities.GeoPoint;
import javax.net.ssl.HttpsURLConnection;
import java.net.URL;
import java.lang.Math;
/**
* CanvasMap retrieves a map image with given geo boundary from Google Map server.
* By passing a rectangle like geo boundary, it returns a map image with the
* highest resolution. However, due to free quote account usage limit, the maximum
* resolution is only 1280 * 1280.
*
* Created by Haoming on 15/5/2017
*/
public class CanvasMap {
private Boundary boundary;
private long width, height; // desired image size
private int zoom;
private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE";
public CanvasMap(Boundary boundary) {
this.boundary = boundary;
calculateOptimalMapSize();
}
public Image getMapImage() {
try {
URL url = new URL(getRequest());
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
return new Image(connection.getInputStream());
} catch (Exception e) {
System.out.println("[CanvasMap] Exception");
return null;
}
}
private String getRequest() {
StringBuilder sb = new StringBuilder();
sb.append("https://maps.googleapis.com/maps/api/staticmap?");
sb.append(String.format("center=%f,%f", boundary.getCentreLat(), boundary.getCentreLng()));
sb.append(String.format("&zoom=%d", zoom));
sb.append(String.format("&size=%dx%d&scale=2", width, height));
sb.append("&style=feature:all|element:labels|visibility:off"); // hide all labels on map
// sb.append(String.format("&markers=%f,%f", boundary.getSouthLat(), boundary.getWestLng()));
// sb.append(String.format("&key=%s", KEY));
return sb.toString();
}
private void calculateOptimalMapSize() {
for (int z = 20; z > 0; z--) {
MapSize mapSize = getMapSize(z, boundary);
zoom = z;
width = mapSize.width;
height = mapSize.height;
// if map size is valid, exit the loop as we have the highest resolution
if (mapSize.isValid()) break;
}
}
private MapSize getMapSize(int zoom, Boundary boundary) {
double scale = Math.pow(2, zoom);
GeoPoint geoSW = new GeoPoint(boundary.getSouthLat(), boundary.getWestLng());
GeoPoint geoNE = new GeoPoint(boundary.getNorthLat(), boundary.getEastLng());
Point2D pointSW = MercatorProjection.toMapPoint(geoSW);
Point2D pointNE = MercatorProjection.toMapPoint(geoNE);
return new MapSize(Math.abs(pointNE.getX() - pointSW.getX()) * scale,
Math.abs(pointNE.getY() - pointSW.getY()) * scale);
}
class MapSize {
long width, height;
MapSize(double width, double height) {
this.width = Math.round(width);
this.height = Math.round(height);
}
/**
* Map size is valid when width and height are both less than 640 pixels
* @return true if both dimensions are less than 640px
*/
boolean isValid() {
return Math.max(width, height) <= 640;
}
}
public long getWidth() {
return width;
}
public long getHeight() {
return height;
}
public int getZoom() {
return zoom;
}
}
@@ -0,0 +1,55 @@
package seng302.models.map;
import javafx.geometry.Point2D;
import seng302.utilities.GeoPoint;
/**
* An utility class useful to convert between Geo locations and Mercator projection
* planar coordinates.
* Created by Haoming on 15/5/2017
*/
public class MercatorProjection {
private static final double MERCATOR_RANGE = 256;
private static final double pixelsPerLngDegree = MERCATOR_RANGE / 360.0;
private static final double pixelsPerLngRadian = MERCATOR_RANGE / (2 * Math.PI);
/**
* A help function keeps the value in bound between -0.9999 and 0.9999.
* @param value in bound value
* @return the value in bound
*/
private static double bound(double value) {
return Math.min(Math.max(value, -0.9999), 0.9999);
}
/**
* Projects a Geo Location (lat, lng) on a planar
* @param geo GeoPoint (lat, lng) location to be projected
* @return the projection Point2D (x, y) on planar
*/
public static Point2D toMapPoint(GeoPoint geo) {
double x, y;
Point2D origin = new Point2D(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0);
x = (origin.getX() + geo.getLng() * pixelsPerLngDegree);
// NOTE(appleton): Truncating to 0.9999 effectively limits latitude to
// 89.189. This is about a third of a tile past the edge of the world tile.
double sinY = bound(Math.sin(Math.toRadians(geo.getLat())));
y = origin.getY() + 0.5 * Math.log((1 + sinY) / (1 - sinY)) * (-pixelsPerLngRadian);
return new Point2D(x, y);
}
/**
* Converts the planar projection (x, y) back to Geo Location (lat, lng)
* @param point Point2D (x, y) to be converted back
* @return the original Geo location converted from the given projection point
*/
public static GeoPoint toMapGeo(Point2D point) {
Point2D origin = new Point2D(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0);
double lng = (point.getX() - origin.getX()) / pixelsPerLngDegree;
double latRadians = (point.getY() - origin.getY()) / (-pixelsPerLngRadian);
double lat = Math.toDegrees(2 * Math.atan(Math.exp(latRadians)) - Math.PI / 2.0);
return new GeoPoint(lat, lng);
}
}
@@ -0,0 +1,23 @@
package seng302.models.map;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import java.net.URL;
import java.util.ResourceBundle;
public class TestMapController implements Initializable{
@FXML
private Canvas mapCanvas;
@Override
public void initialize(URL location, ResourceBundle resources) {
GraphicsContext gc = mapCanvas.getGraphicsContext2D();
Boundary bound = new Boundary(57.662943, 11.848501, 57.673945, 11.824966);
CanvasMap canvasMap = new CanvasMap(bound);
gc.drawImage(canvasMap.getMapImage(), 0, 0, canvasMap.getWidth(), canvasMap.getHeight());
}
}
@@ -16,8 +16,8 @@ public class GateMark extends Mark {
* @param singleMark1 one single mark inside of the gate mark
* @param singleMark2 the second mark inside of the gate mark
*/
public GateMark(String name, MarkType type, SingleMark singleMark1, SingleMark singleMark2, double latitude, double longitude) {
super(name, type, latitude, longitude);
public GateMark(String name, MarkType type, SingleMark singleMark1, SingleMark singleMark2, double latitude, double longitude, int compoundMarkID) {
super(name, type, latitude, longitude, compoundMarkID);
this.singleMark1 = singleMark1;
this.singleMark2 = singleMark2;
}
@@ -39,12 +39,10 @@ public class GateMark extends Mark {
}
public double getLatitude(){
//return (this.getSingleMark1().getLatitude() + this.getSingleMark2().getLatitude()) / 2;
return (this.getSingleMark1().getLatitude());
}
public double getLongitude(){
//return (this.getSingleMark1().getLongitude() + this.getSingleMark2().getLongitude()) / 2;
return (this.getSingleMark1().getLongitude());
}
+26 -14
View File
@@ -10,25 +10,28 @@ public abstract class Mark {
private MarkType markType;
private double latitude;
private double longitude;
private int id;
private long id;
private int compoundMarkID;
/**
* Create a mark instance by passing its name and type
* @param name the name of the mark
* @param markType the type of mark. either GATE_MARK or SINGLE_MARK.
*/
public Mark (String name, MarkType markType, int id) {
public Mark (String name, MarkType markType, int sourceID, int compoundMarkID) {
this.name = name;
this.markType = markType;
this.id = id;
this.id = sourceID;
this.compoundMarkID = compoundMarkID;
}
public Mark(String name, MarkType markType, double latitude, double longitude) {
public Mark(String name, MarkType markType, double latitude, double longitude, int compoundMarkID) {
this.name = name;
this.markType = markType;
this.latitude = latitude;
this.longitude = longitude;
id = 0;
this.id = 0;
this.compoundMarkID = compoundMarkID;
}
/**
@@ -47,20 +50,24 @@ public abstract class Mark {
}
/**
* Calculate the heading in radians from geographical location with latitude1, longitude 1 to geographical
* latitude2, longitude 2
* Calculate the heading in radians from geographical location with latitude1, longitude 1 to
* geographical latitude2, longitude 2
*
* @param longitude1 Longitude of first point in degrees
* @param longitude2 Longitude of second point in degrees
* @param latitude1 Latitude of first point in degrees
* @param latitude2 Latitude of first point in degrees
* @return Heading in radians
*/
public static double calculateHeadingRad (Double latitude1, Double longitude1, Double latitude2, Double longitude2) {
public static double calculateHeadingRad(Double latitude1, Double longitude1, Double latitude2,
Double longitude2) {
latitude1 = Math.toRadians(latitude1);
latitude2 = Math.toRadians(latitude2);
Double longDiff = Math.toRadians(longitude2 - longitude1);
Double y = Math.sin(longDiff) * Math.cos(latitude2);
Double x = Math.cos(latitude1)*Math.sin(latitude2)-Math.sin(latitude1)*Math.cos(latitude2)*Math.cos(longDiff);
Double x =
Math.cos(latitude1) * Math.sin(latitude2) - Math.sin(latitude1) * Math.cos(latitude2)
* Math.cos(longDiff);
return Math.atan2(y, x);
}
@@ -80,8 +87,8 @@ public abstract class Mark {
}
/**
* Calculate the distance in meters from geographical location with latitude1, longitude 1 to geographical
* latitude2, longitude 2
* Calculate the distance in meters from geographical location with latitude1, longitude 1 to
* geographical latitude2, longitude 2
*
* @param longitude1 Longitude of first point in degrees
* @param longitude2 Longitude of second point in degrees
@@ -89,14 +96,16 @@ public abstract class Mark {
* @param latitude2 Latitude of first point in degrees
* @return Distance in meters
*/
public static Double calculateDistance (Double latitude1, Double longitude1, Double latitude2, Double longitude2) {
public static Double calculateDistance(Double latitude1, Double longitude1, Double latitude2,
Double longitude2) {
Double theta = longitude1 - longitude2;
Double dist = Math.sin(Math.toRadians(latitude1)) * Math.sin(Math.toRadians(latitude2)) +
Math.cos(Math.toRadians(latitude1)) * Math.cos(Math.toRadians(latitude2)) *
Math.cos(Math.toRadians(theta));
dist = Math.acos(dist);
dist = Math.toDegrees(dist);
dist = dist * 60 * 1.1508; //nautical mile (distance between two degrees) * (degrees in a minute)
dist = dist * 60
* 1.1508; //nautical mile (distance between two degrees) * (degrees in a minute)
dist = dist * 1609.344; //ratio of miles to metres
return dist;
}
@@ -125,7 +134,7 @@ public abstract class Mark {
return longitude;
}
public int getId() {
public long getId() {
return id;
}
@@ -133,4 +142,7 @@ public abstract class Mark {
this.id = id;
}
public int getCompoundMarkID() {
return compoundMarkID;
}
}
@@ -1,231 +0,0 @@
package seng302.models.mark;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.transform.Rotate;
import seng302.models.RaceObject;
import java.util.ArrayList;
import java.util.List;
/**
* Created by CJIRWIN on 26/04/2017.
*/
public class MarkGroup extends RaceObject {
private static int MARK_RADIUS = 5;
private static int LINE_THICKNESS = 2;
private static double DASHED_GAP_LEN = 2d;
private static double DASHED_LINE_LEN = 5d;
private List<Mark> marks = new ArrayList<>();
private Mark mainMark;
private double[] nodePixelVelocitiesX;
private double[] nodePixelVelocitiesY;
private Point2D[] nodeDestinations;
public MarkGroup (Mark mark, Point2D... points) {
nodePixelVelocitiesX = new double[points.length];
nodePixelVelocitiesY = new double[points.length];
nodeDestinations = new Point2D[points.length];
marks.add(mark);
mainMark = mark;
Color color = Color.BLACK;
if (mark.getName().equals("Start")){
color = Color.GREEN;
} else if (mark.getName().equals("Finish")){
color = Color.RED;
}
Circle markCircle;
if (mark.getMarkType() == MarkType.SINGLE_MARK) {
markCircle = new Circle(
points[0].getX(),
points[0].getY(),
MARK_RADIUS,
color
);
nodeDestinations = new Point2D[]{
new Point2D(markCircle.getCenterX(), markCircle.getCenterY()
)
};
super.getChildren().add(markCircle);
} else {
marks.add(((GateMark) mark).getSingleMark1());
marks.add(((GateMark) mark).getSingleMark2());
nodePixelVelocitiesX = new double[]{0d,0d};
nodePixelVelocitiesY = new double[]{0d,0d};
nodeDestinations = new Point2D[2];
markCircle = new Circle(
points[0].getX(),
points[0].getY(),
MARK_RADIUS,
color
);
nodeDestinations[0] = new Point2D(markCircle.getCenterX(), markCircle.getCenterY());
super.getChildren().add(markCircle);
markCircle = new Circle(
points[1].getX(),
points[1].getY(),
MARK_RADIUS,
color
);
nodeDestinations[1] = new Point2D(markCircle.getCenterX(), markCircle.getCenterY());
super.getChildren().add(markCircle);
Line line = new Line(
points[0].getX(),
points[0].getY(),
points[1].getX(),
points[1].getY()
);
line.setStrokeWidth(LINE_THICKNESS);
line.setStroke(color);
if (mark.getMarkType() == MarkType.OPEN_GATE) {
line.getStrokeDashArray().addAll(DASHED_GAP_LEN, DASHED_LINE_LEN);
}
super.getChildren().add(line);
}
}
public void setDestination (double x, double y, double rotation, double groundSpeed, int... raceIds) {
setDestination(x, y, 0, raceIds);
this.rotationalGoal = rotation;
calculateRotationalVelocity();
}
public void setDestination (double x, double y, double groundSpeed, int... raceIds) {
for (int i = 0; i < marks.size(); i++)
for (int id : raceIds)
if (id == marks.get(i).getId())
setDestinationChild(x, y, 0, Math.max(0, i-1));
}
private void setDestinationChild (double x, double y, double speed, int childIndex) {
//double relativeX = x - super.getLayoutX();
//double relativeY = y - super.getLayoutY();
Circle markCircle = (Circle) super.getChildren().get(childIndex);
this.nodeDestinations[childIndex] = new Point2D(x, y);
//if (Math.abs(relativeX - markCircle.getCenterX()) > 30 && Math.abs(relativeY - markCircle.getCenterY()) > 30) {
this.nodePixelVelocitiesX[childIndex] = (x - markCircle.getCenterX()) / expectedUpdateInterval;
this.nodePixelVelocitiesY[childIndex] = (y - markCircle.getCenterY()) / expectedUpdateInterval;
//}
}
public void rotateTo (double rotation) {
if (mainMark.getMarkType() != MarkType.SINGLE_MARK) {
Line line = (Line) super.getChildren().get(2);
double xCenter = Math.abs(line.getEndX() - line.getStartX());
double yCenter = Math.abs(line.getEndY() - line.getStartY());
super.getTransforms().setAll(new Rotate(rotation, xCenter, yCenter));
}
}
public void updatePosition (long timeInterval) {
Circle markCircle = (Circle) super.getChildren().get(0);
if (nodePixelVelocitiesX[0] > 0 && markCircle.getCenterX() > nodeDestinations[0].getX() ||
nodePixelVelocitiesX[0] < 0 && markCircle.getCenterX() < nodeDestinations[0].getY())
nodePixelVelocitiesX[0] = 0;
else if (nodePixelVelocitiesX[0] != 0)
markCircle.setCenterX(markCircle.getCenterX() + nodePixelVelocitiesX[0] * timeInterval);
if (nodePixelVelocitiesY[0] > 0 && markCircle.getCenterY() > nodeDestinations[0].getY() ||
nodePixelVelocitiesY[0] < 0 && markCircle.getCenterY() < nodeDestinations[0].getY())
nodePixelVelocitiesY[0] = 0;
else if (nodePixelVelocitiesY[0] != 0)
markCircle.setCenterY(markCircle.getCenterY() + nodePixelVelocitiesY[0] * timeInterval);
if (mainMark.getMarkType() != MarkType.SINGLE_MARK) {
Line line = (Line) super.getChildren().get(2);
line.setStartX(markCircle.getCenterX());
line.setStartY(markCircle.getCenterY());
markCircle = (Circle) super.getChildren().get(1);
if (nodePixelVelocitiesX[1] > 0 && markCircle.getCenterX() >= nodeDestinations[1].getX() ||
nodePixelVelocitiesX[1] < 0 && markCircle.getCenterX() <= nodeDestinations[1].getX())
nodePixelVelocitiesX[1] = 0;
else if (nodePixelVelocitiesX[1] != 0)
markCircle.setCenterX(markCircle.getCenterX() + nodePixelVelocitiesX[1] * timeInterval);
if (nodePixelVelocitiesY[1] > 0 && markCircle.getCenterY() > nodeDestinations[1].getY() ||
nodePixelVelocitiesY[1] < 0 && markCircle.getCenterY() < nodeDestinations[1].getY())
nodePixelVelocitiesY[1] = 0;
else if (nodePixelVelocitiesY[1] != 0)
markCircle.setCenterY(markCircle.getCenterY() + nodePixelVelocitiesY[1] * timeInterval);
line.setEndX(markCircle.getCenterX());
line.setEndY(markCircle.getCenterY());
}
}
public void moveGroupBy (double x, double y, double rotation) {
if (mainMark.getMarkType() != MarkType.SINGLE_MARK) {
Line line = (Line) super.getChildren().get(2);
for (int childIndex = 0; childIndex < 2; childIndex++){
Circle mark = (Circle) super.getChildren().get(childIndex);
mark.setCenterY(mark.getCenterY() + y);
mark.setCenterX(mark.getCenterX() + x);
}
line.setStartX(line.getStartX() + x);
line.setStartY(line.getStartY() + y);
line.setEndX(line.getEndX() + x);
line.setEndY(line.getEndY() + y);
} else {
Circle mark = (Circle) super.getChildren().get(0);
mark.setCenterY(mark.getCenterY() + y);
mark.setCenterX(mark.getCenterX() + x);
}
rotateTo(currentRotation + rotation);
}
public void moveTo (double x, double y, double rotation) {
moveTo(x, y);
rotateTo(rotation);
}
public void moveTo (double x, double y) {
Circle markCircle = (Circle) super.getChildren().get(0);
markCircle.setCenterX(x);
markCircle.setCenterY(y);
if (mainMark.getMarkType() != MarkType.SINGLE_MARK) {
markCircle = (Circle) super.getChildren().get(1);
markCircle.setCenterX(x);
markCircle.setCenterY(y);
Line line = (Line) super.getChildren().get(2);
line.setStartX(x);
line.setStartY(y);
line.setEndX(x);
line.setEndY(y);
}
}
public boolean hasRaceId (int... raceIds) {
for (int id : raceIds)
for (Mark mark : marks)
if (id == mark.getId())
return true;
return false;
}
public static int getMarkRadius() {
return MARK_RADIUS;
}
public static void setMarkRadius(int markRadius) {
MARK_RADIUS = markRadius;
}
public int[] getRaceIds () {
int[] idArray = new int[marks.size()];
int i = 0;
for (Mark mark : marks)
idArray[i++] = mark.getId();
return idArray;
}
}
@@ -5,5 +5,5 @@ package seng302.models.mark;
* Created by Haoming Yin (hyi25) on 17/3/17.
*/
public enum MarkType {
SINGLE_MARK, OPEN_GATE, CLOSED_GATE
SINGLE_MARK, OPEN_GATE
}
@@ -9,8 +9,6 @@ public class SingleMark extends Mark {
private double lat;
private double lon;
private String name;
private int id;
/**
* Represents a marker
@@ -19,24 +17,12 @@ public class SingleMark extends Mark {
* @param lat, the latitude of the marker
* @param lon, the longitude of the marker
*/
public SingleMark(String name, double lat, double lon, int id) {
super(name, MarkType.SINGLE_MARK, id);
public SingleMark(String name, double lat, double lon, int sourceID, int compoundMarkID) {
super(name, MarkType.SINGLE_MARK, sourceID, compoundMarkID);
this.lat = lat;
this.lon = lon;
this.id = id;
}
/**
* Represents the marker at the beginning of a leg
*
* @param name, the name of the marker
*/
public SingleMark(String name) {
super(name, MarkType.SINGLE_MARK, 0);
this.lat = 0;
this.lon = 0;
this.id = 0;
}
public double getLatitude() {
return this.lat;
@@ -1,78 +0,0 @@
package seng302.models.parsers;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.DoubleSummaryStatistics;
public class ConfigParser extends FileParser {
private Document doc;
public ConfigParser(String path) {
super(path);
this.doc = this.parseFile();
}
/**
* Gets wind direction from config file.
*
* @return a double type degree, or 0 if no value or invalid value is found
*/
public double getWindDirection() {
return getDoubleByTagName("wind-direction", 0.0);
}
/**
* Gets a non negative time scale for the race
*
* @return a double type scale, or 0 if no scale or invalid scale is found
*/
public double getTimeScale() {
return getDoubleByTagName("time-scale", 1.0);
}
/**
* Gets a double type number by given tag name found in xml file
*
* @param tagName a string of tag name
* @param defaultVal value returned if no value or invalid value is found
* @return value found
*/
public double getDoubleByTagName(String tagName, double defaultVal) {
double val = defaultVal;
try {
Node node = this.doc.getElementsByTagName(tagName).item(0);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
val = Double.valueOf(element.getTextContent());
}
} catch (Exception e) {
} finally {
return val;
}
}
/**
* Gets a string by given tag name found in xml file
*
* @param tagName a string of tag name
* @param defaultVal a string returned if no value or invalid value is found
* @return string found
*/
public String getStringByTagName(String tagName, String defaultVal) {
String string = defaultVal;
try {
Node node = this.doc.getElementsByTagName(tagName).item(0);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
string = element.getTextContent();
}
} catch (Exception e) {
} finally {
return string;
}
}
}
@@ -1,145 +0,0 @@
package seng302.models.parsers;
import org.w3c.dom.*;
import seng302.models.mark.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.NoSuchElementException;
/**
* parse a course xml file
* Created by Haoming Yin (hyi25) on 16/3/2017
*/
public class CourseParser extends FileParser {
private Document doc;
private HashMap<String, Mark> marks = new HashMap<>();
public CourseParser(String path) {
super(path);
this.doc = this.parseFile();
}
/**
* create a mark by given node
*
* @param node
* @return a mark, or null if fails to create a mark
*/
private SingleMark generateSingleMark(Node node) {
try {
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
String name = element.getElementsByTagName("name").item(0).getTextContent();
double lat = Double.valueOf(element.getElementsByTagName("latitude").item(0).getTextContent());
double lon = Double.valueOf(element.getElementsByTagName("longitude").item(0).getTextContent());
int id = Integer.valueOf(element.getElementsByTagName("id").item(0).getTextContent());
SingleMark singleMark = new SingleMark(name, lat, lon, id);
return singleMark;
} else {
throw new NoSuchElementException("Cannot generate a mark by given node.");
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* generate an arrayList of gates
*
* @return an arrayList of gates, or null if no gate has been found.
*/
private void generateGateMarks() {
ArrayList<GateMark> gateMarks = new ArrayList<>();
try {
NodeList nodes = doc.getElementsByTagName("gate");
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
String name = element.getElementsByTagName("name").item(0).getTextContent();
SingleMark mark1 = generateSingleMark(element.getElementsByTagName("mark").item(0));
SingleMark mark2 = generateSingleMark(element.getElementsByTagName("mark").item(1));
GateMark gateMark;
if (name.equals("Start") || name.equals("Finish"))
gateMark = new GateMark(name, MarkType.CLOSED_GATE, mark1, mark2, mark1.getLatitude(), mark1.getLongitude());
else
gateMark = new GateMark(name, MarkType.OPEN_GATE, mark1, mark2, mark1.getLatitude(), mark1.getLongitude());
marks.put(name, gateMark);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* generate an arrayList of marks
*
* @return an arrayList of marks, or null if no gate has been found.
*/
private void generateSingleMarks() {
ArrayList<SingleMark> singleMarks = new ArrayList<>();
try {
// find the "marks" tag
Node node = doc.getElementsByTagName("marks").item(0);
// iterate all "marks"'s children
for (Node n = node.getFirstChild(); n != null; n = n.getNextSibling()) {
// if node's tag name is "mark"
if (n.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) n;
if (element.getNodeName() == "mark") {
Mark mark = generateSingleMark(n);
marks.put(mark.getName(), mark);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* return the order of all the marks along a course
*
* @return an arrayList of the names of ordered course marks
*/
private ArrayList<String> getOrder() {
ArrayList<String> markOrder = new ArrayList<>();
try {
Node orderNode = doc.getElementsByTagName("order").item(0);
for (Node node = orderNode.getFirstChild(); node != null; node = node.getNextSibling()) {
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
String name = element.getTextContent();
markOrder.add(name);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return markOrder;
}
public ArrayList<Mark> getCourse() {
generateSingleMarks();
generateGateMarks();
ArrayList<Mark> course = new ArrayList<>();
try {
for (String mark : getOrder()) {
course.add(marks.get(mark));
}
} catch (Exception e) {
e.printStackTrace();
}
return course;
}
}
@@ -1,54 +0,0 @@
package seng302.models.parsers;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
/**
* Created by Haoming Yin (hyi25) on 16/3/2017
*/
public abstract class FileParser {
private String filePath;
public FileParser() {}
public FileParser(String path) {
this.filePath = path;
}
protected Document parseFile() {
try {
InputStream is = getClass().getResourceAsStream(this.filePath);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(is);
// optional, in order to recover info from broken line.
doc.getDocumentElement().normalize();
return doc;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
protected Document parseFile(String xmlString) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xmlString)));
// optional, in order to recover info from broken line.
doc.getDocumentElement().normalize();
return doc;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
@@ -1,53 +0,0 @@
package seng302.models.parsers;
/**
* Created by Kusal on 4/24/2017.
*/
public enum PacketType {
HEARTBEAT,
RACE_STATUS,
DISPLAY_TEXT_MESSAGE,
XML_MESSAGE,
RACE_START_STATUS,
YACHT_EVENT_CODE,
YACHT_ACTION_CODE,
CHATTER_TEXT,
BOAT_LOCATION,
MARK_ROUNDING,
COURSE_WIND,
AVG_WIND,
OTHER;
static PacketType assignPacketType(int packetType){
switch(packetType){
case 1:
return HEARTBEAT;
case 12:
return RACE_STATUS;
case 20:
return DISPLAY_TEXT_MESSAGE;
case 26:
return XML_MESSAGE;
case 27:
return RACE_START_STATUS;
case 29:
return YACHT_EVENT_CODE;
case 31:
return YACHT_ACTION_CODE;
case 36:
return CHATTER_TEXT;
case 37:
return BOAT_LOCATION;
case 38:
return MARK_ROUNDING;
case 44:
return COURSE_WIND;
case 47:
return AVG_WIND;
default:
}
return OTHER;
}
}
@@ -1,44 +0,0 @@
package seng302.models.parsers;
/**
* Created by kre39 on 23/04/17.
*/
public class StreamPacket {
//Change int to an ENUM for the type
private PacketType type;
private long messageLength;
private long timeStamp;
private byte[] payload;
StreamPacket(int type, long messageLength, long timeStamp, byte[] payload) {
this.type = PacketType.assignPacketType(type);
this.messageLength = messageLength;
this.timeStamp = timeStamp;
this.payload = payload;
// System.out.println("type = " + this.type.toString());
//switch the packet type to deal with what ever specific packet you want to deal with
// if (this.type == PacketType.XML_MESSAGE){
// //System.out.println("--------");
// System.out.println(new String(payload));
// //StreamParser.parsePacket(this);
// }
}
PacketType getType() {
return type;
}
public long getMessageLength() {
return messageLength;
}
byte[] getPayload() {
return payload;
}
long getTimeStamp() {
return timeStamp;
}
}
@@ -1,583 +0,0 @@
package seng302.models.parsers;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import seng302.models.Yacht;
import seng302.models.parsers.packets.BoatPositionPacket;
import seng302.models.parsers.packets.StreamPacket;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.StringReader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.PriorityBlockingQueue;
/**
* The purpose of this class is to take in the stream of divided packets so they can be read
* and parsed in by turning the byte arrays into useful data. There are two public static hashmaps
* that are threadsafe so the visualiser can always access the latest speed and position available
* Created by kre39 on 23/04/17.
*
*/
public class StreamParser extends Thread{
public static ConcurrentHashMap<Long, PriorityBlockingQueue<BoatPositionPacket>> boatPositions = new ConcurrentHashMap<>();
private String threadName;
private Thread t;
private static boolean raceStarted = false;
private static XMLParser xmlObject;
private static boolean raceFinished = false;
private static boolean streamStatus = false;
private static long timeSinceStart = -1;
private static Map<Integer, Yacht> boats = new HashMap<>();
private static Map<Long, Yacht> boatsPos = new TreeMap<>();
private static double windDirection = 0;
private static String currentTimeString;
private static boolean appRunning;
/**
* Used to initialise the thread name and stream parser object so a thread can be executed
*
* @param threadName name of the thread
*/
public StreamParser(String threadName){
this.threadName = threadName;
}
/**
* Used to within threading so when the stream parser thread runs, it will keep looking for a packet to
* process until it is unable to find anymore packets
*
*/
public void run(){
appRunning = true;
try {
System.out.println("[CLIENT] Start of stream");
streamStatus = true;
xmlObject = new XMLParser();
while (StreamReceiver.packetBuffer == null || StreamReceiver.packetBuffer.size() < 1) {
Thread.sleep(1);
}
while (appRunning){
StreamPacket packet = StreamReceiver.packetBuffer.peek();
//this code adds a delay to reading from the packetBuffer so
//out of order packets have time to order themselves in the queue
int delayTime = 1000;
int loopTime = delayTime * 10;
long transitTime = (System.currentTimeMillis()%loopTime - packet.getTimeStamp()%loopTime);
if (transitTime < 0){
transitTime = loopTime + transitTime;
}
if (transitTime < delayTime) {
long sleepTime = delayTime - (transitTime);
Thread.sleep(sleepTime);
}
packet = StreamReceiver.packetBuffer.take();
parsePacket(packet);
Thread.sleep(1);
while (StreamReceiver.packetBuffer.peek() == null) {
Thread.sleep(1);
}
}
} catch (Exception e){
e.printStackTrace();
}
}
/**
* Used to start the stream parser thread when multithreading
*
*/
public void start () {
System.out.println("[CLIENT] Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
/**
* Looks at the type of the packet then sends it to the appropriate parser to extract the
* specific data associated with that packet type
*
* @param packet the packet to be looked at and processed
*/
private static void parsePacket(StreamPacket packet) {
try{
switch (packet.getType()){
case HEARTBEAT:
extractHeartBeat(packet);
break;
case RACE_STATUS:
extractRaceStatus(packet);
break;
case DISPLAY_TEXT_MESSAGE:
extractDisplayMessage(packet);
break;
case XML_MESSAGE:
extractXmlMessage(packet);
break;
case RACE_START_STATUS:
extractRaceStartStatus(packet);
break;
case YACHT_EVENT_CODE:
extractYachtEventCode(packet);
break;
case YACHT_ACTION_CODE:
extractYachtActionCode(packet);
break;
case CHATTER_TEXT:
extractChatterText(packet);
break;
case BOAT_LOCATION:
extractBoatLocation(packet);
break;
case MARK_ROUNDING:
extractMarkRounding(packet);
break;
case COURSE_WIND:
extractCourseWind(packet);
break;
case AVG_WIND:
extractAvgWind(packet);
break;
default:
break;
//System.out.println(packet.getType().toString());
}
}
catch (NullPointerException e){
System.out.println("Error parsing packet");
}
}
/**
* Extracts the seq num used in the heartbeat packet
*
* @param packet Packet parsed in to use the payload
*/
private static void extractHeartBeat(StreamPacket packet) {
long heartbeat = bytesToLong(packet.getPayload());
}
private static String getTimeZoneString() {
Integer offset = xmlObject.getRegattaXML().getUtcOffset();
StringBuilder utcOffset = new StringBuilder();
utcOffset.append("GMT");
if (offset > 0) {
utcOffset.append("+");
utcOffset.append(offset);
} else if (offset < 0) {
utcOffset.append("-");
utcOffset.append(offset);
}
return utcOffset.toString();
}
/**
* Extracts the useful race status data from race status type packets. This method will also print to the
* console the current state of the race (if it has started/finished or is about to start), along side
* this it'll also display the amount of time since the race has started or time till it starts
*
* @param packet Packet parsed in to use the payload
*/
private static void extractRaceStatus(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long currentTime = bytesToLong(Arrays.copyOfRange(payload,1,7));
long raceId = bytesToLong(Arrays.copyOfRange(payload,7,11));
int raceStatus = payload[11];
// System.out.println("raceStatus = " + raceStatus);
long expectedStartTime = bytesToLong(Arrays.copyOfRange(payload,12,18));
DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
if (xmlObject.getRegattaXML() != null) {
format.setTimeZone(TimeZone.getTimeZone(getTimeZoneString()));
currentTimeString = format.format((new Date (currentTime)).getTime());
}
long timeTillStart = ((new Date (expectedStartTime)).getTime() - (new Date (currentTime)).getTime())/1000;
if (timeTillStart > 0) {
timeSinceStart = timeTillStart;
//System.out.println("Time till start: " + timeTillStart + " Seconds");
} else {
if (raceStatus == 4 || raceStatus == 8){
raceFinished = true;
raceStarted = false;
System.out.println("[CLIENT] Race has finished");
} else if (!raceStarted){
raceStarted = true;
raceFinished = false;
System.out.println("[CLIENT] Race has started");
}
//System.out.println("Time since start: " + -1 * timeTillStart + " Seconds");
timeSinceStart = timeTillStart;
}
long windDir = bytesToLong(Arrays.copyOfRange(payload,18,20));
double windDirFactor = 0x4000 / 90; //0x4000 is 90 degrees, 0x8000 is 180 degrees, etc...
windDirection = windDir / windDirFactor;
long windSpeed = bytesToLong(Arrays.copyOfRange(payload,20,22));
int noBoats = payload[22];
int raceType = payload[23];
// ArrayList<String> boatStatuses = new ArrayList<>();
boatsPos = new TreeMap<>();
for (int i = 0; i < noBoats; i++){
Long boatStatusSourceID = bytesToLong(Arrays.copyOfRange(payload,24 + (i * 20),28+ (i * 20)));
Yacht boat = boats.get((int)(long) boatStatusSourceID);
boat.setBoatStatus((int)payload[28 + (i * 20)]);
boat.setLegNumber((int)payload[29 + (i * 20)]);
boat.setPenaltiesAwarded((int)payload[29 + (i * 20)]);
boat.setPenaltiesServed((int)payload[30 + (i * 20)]);
Long estTimeAtNextMark = bytesToLong(Arrays.copyOfRange(payload,31 + (i * 20),37+ (i * 20)));
boat.setEstimateTimeAtNextMark(estTimeAtNextMark);
Long estTimeAtFinish = bytesToLong(Arrays.copyOfRange(payload,37 + (i * 20),43+ (i * 20)));
boat.setEstimateTimeAtFinish(estTimeAtFinish);
boatsPos.put(estTimeAtFinish, boat);
// String boatStatus = "SourceID: " + boatStatusSourceID;
// boatStatus += "\nBoat Status: " + (int)payload[28 + (i * 20)];
// boatStatus += "\nLegNumber: " + (int)payload[29 + (i * 20)];
// boatStatus += "\nPenaltiesAwarded: " + (int)payload[29 + (i * 20)];
// boatStatus += "\nPenaltiesServed: " + (int)payload[30 + (i * 20)];
// boatStatus += "\nEstTimeAtNextMark: " + bytesToLong(Arrays.copyOfRange(payload,31 + (i * 20),37+ (i * 20)));
// boatStatus += "\nEstTimeAtFinish: " + bytesToLong(Arrays.copyOfRange(payload,37 + (i * 20),43+ (i * 20)));
// boatStatuses.add(boatStatus);
}
if (isRaceStarted()) {
int pos = 1;
for (Yacht yacht : boatsPos.values()) {
yacht.setPosition(String.valueOf(pos));
pos++;
}
} else {
for (Yacht yacht : boatsPos.values()) {
yacht.setPosition("-");
}
}
}
/**
* Used to extract the messages passed through with the display message packet
*
* @param packet Packet parsed in to use the payload
*/
private static void extractDisplayMessage(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
int numOfLines = payload[3];
int totalLen = 0;
for (int i = 0; i < numOfLines; i++){
int lineNum = payload[4 + totalLen];
int textLength = payload[5 + totalLen];
byte[] messageTextBytes = Arrays.copyOfRange(payload,6 + totalLen,6 + textLength + totalLen);
String messageText = new String(messageTextBytes);
totalLen += 2 + textLength;
}
}
/**
* Used to read in the xml data. Will call the specific methods to create the course and boats
*
* @param packet Packet parsed in to use the payload
*/
private static void extractXmlMessage(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageType = payload[9];
long messagelength = bytesToLong(Arrays.copyOfRange(payload,12,14));
String xmlMessage = new String((Arrays.copyOfRange(payload,14,(int) (14 + messagelength)))).trim();
//System.out.println("xmlMessage2 = " + xmlMessage);
//Create XML document Object
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = null;
Document doc = null;
try {
db = dbf.newDocumentBuilder();
doc = db.parse(new InputSource(new StringReader(xmlMessage)));
} catch (ParserConfigurationException | IOException | SAXException e) {
e.printStackTrace();
}
xmlObject.constructXML(doc, messageType);
if (messageType == 7) { //7 is the boat XML
boats = xmlObject.getBoatXML().getCompetingBoats();
}
}
/**
* Extracts the race start status from the packet, currently is unused within the app but
* is here for potential future use
*
* @param packet Packet parsed in to use the payload
*/
private static void extractRaceStartStatus(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload,1,7));
long raceStartTime = bytesToLong(Arrays.copyOfRange(payload,9,15));
long raceId = bytesToLong(Arrays.copyOfRange(payload,15,19));
int notificationType = payload[19];
}
/**
* When a yacht event occurs this will parse the byte array to retrieve the necessary info,
* currently unused
*
* @param packet Packet parsed in to use the payload
*/
private static void extractYachtEventCode(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload,1,7));
long raceId = bytesToLong(Arrays.copyOfRange(payload,9,13));
long subjectId = bytesToLong(Arrays.copyOfRange(payload,13,17));
long incidentId = bytesToLong(Arrays.copyOfRange(payload,17,21));
int eventId = payload[21];
}
/**
* When a yacht action occurs this will parse the parse the byte array to retrieve the necessary info,
* currently unused
*
* @param packet Packet parsed in to use the payload
*/
private static void extractYachtActionCode(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload,1,7));
long subjectId = bytesToLong(Arrays.copyOfRange(payload,9,13));
long incidentId = bytesToLong(Arrays.copyOfRange(payload,13,17));
int eventId = payload[17];
// System.out.println("eventId = " + eventId);
}
/**
* Strips the message from the chatter text type packets, currently the message is unused
*
* @param packet Packet parsed in to use the payload
*/
private static void extractChatterText(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
int messageType = payload[1];
int length = payload[2];
String message = new String(Arrays.copyOfRange(payload,3,3 + length));
}
/**
* Used to breakdown the boatlocation packets so the boat coordinates, id and groundspeed are all used
* All the other extra data is still being read and translated however is unused.
*
* @param packet Packet parsed in to use the payload
*/
private static void extractBoatLocation(StreamPacket packet){
byte[] payload = packet.getPayload();
int deviceType = (int)payload[15];
long timeValid = bytesToLong(Arrays.copyOfRange(payload,1,7));
long seq = bytesToLong(Arrays.copyOfRange(payload,11,15));
long boatId = bytesToLong(Arrays.copyOfRange(payload,7,11));
long rawLat = bytesToLong(Arrays.copyOfRange(payload,16,20));
long rawLon = bytesToLong(Arrays.copyOfRange(payload,20,24));
//Converts the double to a usable lat/lon
double lat = ((180d * (double)rawLat)/Math.pow(2,31));
double lon = ((180d *(double)rawLon)/Math.pow(2,31));
long heading = bytesToLong(Arrays.copyOfRange(payload,28,30));
double groundSpeed = bytesToLong(Arrays.copyOfRange(payload,38,40))/1000.0;
//type 1 is a racing yacht and type 3 is a mark, needed for updating positions of the mark and boat
if (deviceType == 1 || deviceType == 3){
BoatPositionPacket boatPacket = new BoatPositionPacket(boatId, timeValid, lat, lon, heading, groundSpeed);
//add a new priority que to the boatPositions HashMap
if (!boatPositions.containsKey(boatId)){
boatPositions.put(boatId, new PriorityBlockingQueue<BoatPositionPacket>(256, new Comparator<BoatPositionPacket>() {
@Override
public int compare(BoatPositionPacket p1, BoatPositionPacket p2) {
return (int) (p1.getTimeValid() - p2.getTimeValid());
}
}));
}
//Adding the boatPacket to the priority que
boatPositions.get(boatId).put(boatPacket);
}
}
/**
* This packet type is received when a mark or gate is rounded by a boat
*
* @param packet The packet containing the payload
*/
private static void extractMarkRounding(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload,1,7));
long raceId = bytesToLong(Arrays.copyOfRange(payload,9,13));
long subjectId = bytesToLong(Arrays.copyOfRange(payload,13,17));
int boatStatus = payload[17];
int roundingSide = payload[18];
int markType = payload[19];
int markId = payload[20];
}
/**
* This packet type contains periodic data on the state of the wind
*
* @param packet The packet containing the payload
*/
private static void extractCourseWind(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
int selectedWindId = payload[1];
int loopCount = payload[2];
ArrayList<String> windInfo = new ArrayList<>();
for (int i = 0; i < loopCount; i++){
String wind = "WindId: " + payload[3 + (20 * i)];
wind += "\nTime: " + bytesToLong(Arrays.copyOfRange(payload,4 + (20 * i),10 + (20 * i)));
wind += "\nRaceId: " + bytesToLong(Arrays.copyOfRange(payload,10 + (20 * i),14 + (20 * i)));
wind += "\nWindDirection: " + bytesToLong(Arrays.copyOfRange(payload,14 + (20 * i),16 + (20 * i)));
wind += "\nWindSpeed: " + bytesToLong(Arrays.copyOfRange(payload,16 + (20 * i),18 + (20 * i)));
wind += "\nBestUpWindAngle: " + bytesToLong(Arrays.copyOfRange(payload,18 + (20 * i),20 + (20 * i)));
wind += "\nBestDownWindAngle: " + bytesToLong(Arrays.copyOfRange(payload,20 + (20 * i),22 + (20 * i)));
wind += "\nFlags: " + String.format("%8s", Integer.toBinaryString(payload[22 + (20 * i)] & 0xFF)).replace(' ', '0');
windInfo.add(wind);
}
}
/**
* This packet conatins the average wind to ground speed
*
* @param packet The packet containing the paylaod
*/
private static void extractAvgWind(StreamPacket packet){
byte[] payload = packet.getPayload();
int messageVersionNo = payload[0];
long timeStamp = bytesToLong(Arrays.copyOfRange(payload,1,7));
long rawPeriod = bytesToLong(Arrays.copyOfRange(payload,7,9));
long rawSamplePeriod = bytesToLong(Arrays.copyOfRange(payload,9,11));
long period2 = bytesToLong(Arrays.copyOfRange(payload,11,13));
long speed2 = bytesToLong(Arrays.copyOfRange(payload,13,15));
long period3 = bytesToLong(Arrays.copyOfRange(payload,15,17));
long speed3 = bytesToLong(Arrays.copyOfRange(payload,17,19));
long period4 = bytesToLong(Arrays.copyOfRange(payload,19,21));
long speed4 = bytesToLong(Arrays.copyOfRange(payload,21,23));
}
/**
* takes an array of up to 7 bytes and returns a positive
* long constructed from the input bytes
*
* @return a positive long if there is less than 7 bytes -1 otherwise
*/
private static long bytesToLong(byte[] bytes){
long partialLong = 0;
int index = 0;
for (byte b: bytes){
if (index > 6){
return -1;
}
partialLong = partialLong | (b & 0xFFL) << (index * 8);
index++;
}
return partialLong;
}
/**
* returns false if race not started, true otherwise
*
* @return race started status
*/
public static boolean isRaceStarted() {
return raceStarted;
}
/**
* returns false if stream not connected, true otherwise
*
* @return stream started status
*/
public static boolean isStreamStatus() {
return streamStatus;
}
/**
* returns race timer
*
* @return race timer in long
*/
public static long getTimeSinceStart() {
return timeSinceStart;
}
/**
* return false if race not finished, true otherwise
*
* @return race finished status
*/
public static boolean isRaceFinished() {
return raceFinished;
}
/**
* return a map of boats with sourceID and the boat
*
* @return map of boats
*/
public static Map<Integer, Yacht> getBoats() {
return boats;
}
/**
* returns the latest updated object from xml parser
*
* @return the latest xml object
*/
public static XMLParser getXmlObject() {
return xmlObject;
}
/**
* returns the wind direction in degrees
*
* @return a double wind direction value
*/
public static double getWindDirection() {
return windDirection;
}
/**
* returns stream time in formatted string format
*
* @return String of stream time
*/
public static String getCurrentTimeString() {
return currentTimeString;
}
/**
* used in boat position since tree map can sort position efficiently.
*
* @return a map of time to finish and boat.
*/
public static Map<Long, Yacht> getBoatsPos() {
return boatsPos;
}
public static void appClose(){
appRunning = false;
System.out.println("[CLIENT] Shutting down stream parser");
}
}
@@ -1,162 +0,0 @@
package seng302.models.parsers;
import seng302.models.parsers.packets.StreamPacket;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
public class StreamReceiver extends Thread {
private InputStream stream;
private Socket host;
private ByteArrayOutputStream crcBuffer;
private Thread t;
private String threadName;
public static PriorityBlockingQueue<StreamPacket> packetBuffer;
private static boolean moreBytes;
public StreamReceiver(String hostAddress, int hostPort, String threadName) {
this.threadName = threadName;
this.setDaemon(true);
try {
host = new Socket(hostAddress, hostPort);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void run(){
PriorityBlockingQueue<StreamPacket> pq = new PriorityBlockingQueue<>(256, new Comparator<StreamPacket>() {
@Override
public int compare(StreamPacket s1, StreamPacket s2) {
return (int) (s1.getTimeStamp() - s2.getTimeStamp());
}
});
packetBuffer = pq;
connect();
}
public void start () {
System.out.println("[CLIENT] Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
public StreamReceiver(Socket host, PriorityBlockingQueue packetBuffer){
this.host=host;
this.packetBuffer = packetBuffer;
}
public void connect(){
try {
stream = host.getInputStream();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
int sync1;
int sync2;
moreBytes = true;
while(moreBytes) {
try {
crcBuffer = new ByteArrayOutputStream();
sync1 = readByte();
sync2 = readByte();
//checking if it is the start of the packet
if(sync1 == 0x47 && sync2 == 0x83) {
int type = readByte();
//No. of milliseconds since Jan 1st 1970
long timeStamp = bytesToLong(getBytes(6));
skipBytes(4);
long payloadLength = bytesToLong(getBytes(2));
byte[] payload = getBytes((int) payloadLength);
Checksum checksum = new CRC32();
checksum.update(crcBuffer.toByteArray(), 0, crcBuffer.size());
long computedCrc = checksum.getValue();
long packetCrc = bytesToLong(getBytes(4));
if (computedCrc == packetCrc) {
packetBuffer.add(new StreamPacket(type, payloadLength, timeStamp, payload));
} else {
System.err.println("Packet has been dropped");
}
}
} catch (Exception e) {
moreBytes = false;
}
}
}
private int readByte() throws Exception {
int currentByte = -1;
try {
currentByte = stream.read();
crcBuffer.write(currentByte);
} catch (IOException e) {
e.printStackTrace();
}
if (currentByte == -1){
throw new Exception();
}
return currentByte;
}
private byte[] getBytes(int n) throws Exception{
byte[] bytes = new byte[n];
for (int i = 0; i < n; i++){
bytes[i] = (byte) readByte();
}
return bytes;
}
private void skipBytes(long n) throws Exception{
for (int i=0; i < n; i++){
readByte();
}
}
/**
* takes an array of up to 7 bytes in little endian format and
* returns a positive long constructed from the input bytes
*
* @return a positive long if there is less than 8 bytes -1 otherwise
*/
private long bytesToLong(byte[] bytes){
long partialLong = 0;
int index = 0;
for (byte b: bytes){
if (index > 6){
return -1;
}
partialLong = partialLong | (b & 0xFFL) << (index * 8);
index++;
}
return partialLong;
}
public static void main(String[] args) {
StreamReceiver sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941,"TestThread1");
//StreamReceiver sr = new StreamReceiver("livedata.americascup.com", 4941, "TestThread2");
sr.start();
}
public static void noMoreBytes(){
moreBytes = false;
System.out.println("[CLIENT] Shutting down stream receiver");
}
}
@@ -1,64 +0,0 @@
//package seng302.models.parsers;
//
//import org.w3c.dom.*;
//import seng302.models.Yacht;
//
//import java.util.ArrayList;
//import java.util.NoSuchElementException;
//
//public class TeamsParser extends FileParser {
//
// private Document doc;
//
// public TeamsParser(String path) {
// super(path);
// this.doc = this.parseFile();
// }
//
// /**
// * Create a boat instance by a given team node
// * @param node a boat node containing name, alias and velocity
// * @return an instance of Boat
// */
// private Yacht parseBoat(Node node) {
// try {
// if (node.getNodeType() == Node.ELEMENT_NODE) {
// Element element = (Element) node;
// String name = element.getElementsByTagName("name").item(0).getTextContent();
// String alias = element.getElementsByTagName("alias").item(0).getTextContent();
// double velocity = Double.valueOf(element.getElementsByTagName("velocity").item(0).getTextContent());
// int id = Integer.valueOf(element.getElementsByTagName("id").item(0).getTextContent());
// Yacht boat = new Yacht(name, velocity, alias, id);
// return boat;
// } else {
// throw new NoSuchElementException("Cannot generate a boat by given node");
// }
// } catch (Exception e) {
// e.printStackTrace();
// return null;
// }
// }
//
// /**
// * Create an arraylist of boats instance.
// * @return an arraylist of boats in teams file
// */
// public ArrayList<Yacht> getBoats() {
// ArrayList<Yacht> boats = new ArrayList<>();
//
// try {
// NodeList nodes = this.doc.getElementsByTagName("team");
// for (int i = 0; i < nodes.getLength(); i++) {
// Node node = nodes.item(i);
// boats.add(parseBoat(node));
// }
// return boats;
// } catch (Exception e) {
// e.printStackTrace();
// return null;
// }
// }
//
//
//}
//
@@ -0,0 +1,7 @@
package seng302.models.stream;
import seng302.models.stream.packets.StreamPacket;
public interface PacketBufferDelegate {
boolean addToBuffer(StreamPacket streamPacket);
}
@@ -1,15 +1,18 @@
package seng302.models.parsers;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import seng302.models.Yacht;
package seng302.models.stream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import seng302.models.Yacht;
import seng302.models.mark.GateMark;
import seng302.models.mark.Mark;
import seng302.models.mark.MarkType;
import seng302.models.mark.SingleMark;
/**
* Class to create an XML object from the XML Packet Messages.
@@ -21,7 +24,6 @@ import java.util.Map;
*
* xmlP = new XMLParser(doc, xmlMessageType);
* RegattaXMLObject rXmlObj = xmlP.createRegattaXML(); // creates a regattaXML object.
*
*/
public class XMLParser {
@@ -31,10 +33,12 @@ public class XMLParser {
private RegattaXMLObject regattaXML;
private BoatXMLObject boatXML;
public XMLParser() {}
public XMLParser() {
}
/**
* Constructor for XMLParser
*
* @param doc Document to create XML object.
* @param messageType Defines if a message is a RegattaXML(5), RaceXML(6), BoatXML(7).
*/
@@ -53,13 +57,22 @@ public class XMLParser {
}
}
public RaceXMLObject getRaceXML() { return raceXML; }
public RegattaXMLObject getRegattaXML() { return regattaXML; }
public BoatXMLObject getBoatXML() { return boatXML; }
public RaceXMLObject getRaceXML() {
return raceXML;
}
public RegattaXMLObject getRegattaXML() {
return regattaXML;
}
public BoatXMLObject getBoatXML() {
return boatXML;
}
/**
* Returns the text content of a given child element tag, assuming it exists, as an Integer.
*
* @param ele Document Element with child elements.
* @param tag Tag to find in document elements child elements.
* @return Text content from tag if found, null otherwise.
@@ -75,6 +88,7 @@ public class XMLParser {
/**
* Returns the text content of a given child element tag, assuming it exists, as an String.
*
* @param ele Document Element with child elements.
* @param tag Tag to find in document elements child elements.
* @return Text content from tag if found, null otherwise.
@@ -90,6 +104,7 @@ public class XMLParser {
/**
* Returns the text content of a given child element tag, assuming it exists, as a Double.
*
* @param ele Document Element with child elements.
* @param tag Tag to find in document elements child elements.
* @return Text content from tag if found, null otherwise.
@@ -105,9 +120,11 @@ public class XMLParser {
/**
* Returns the text content of an attribute of a given Node, assuming it exists, as a String.
*
* @param n A node object that should have some attributes
* @param attr The attribute you want to get from the given node.
* @return The String representation of the text content of an attribute in the given node, else returns null.
* @return The String representation of the text content of an attribute in the given node, else
* returns null.
*/
private static String getNodeAttributeString(Node n, String attr) {
Node attrItem = n.getAttributes().getNamedItem(attr);
@@ -120,9 +137,11 @@ public class XMLParser {
/**
* Returns the text content of an attribute of a given Node, assuming it exists, as an Integer.
*
* @param n A node object that should have some attributes
* @param attr The attribute you want to get from the given node.
* @return The Integer representation of the text content of an attribute in the given node, else returns null.
* @return The Integer representation of the text content of an attribute in the given node,
* else returns null.
*/
private static Integer getNodeAttributeInt(Node n, String attr) {
Node attrItem = n.getAttributes().getNamedItem(attr);
@@ -135,9 +154,11 @@ public class XMLParser {
/**
* Returns the text content of an attribute of a given Node, assuming it exists, as a Double.
*
* @param n A node object that should have some attributes
* @param attr The attribute you want to get from the given node.
* @return The Double representation of the text content of an attribute in the given node, else returns null.
* @return The Double representation of the text content of an attribute in the given node, else
* returns null.
*/
private static Double getNodeAttributeDouble(Node n, String attr) {
Node attrItem = n.getAttributes().getNamedItem(attr);
@@ -149,6 +170,7 @@ public class XMLParser {
}
public class RegattaXMLObject {
//Regatta Info
private Integer regattaID;
private String regattaName;
@@ -160,6 +182,7 @@ public class XMLParser {
/**
* Constructor for a RegattaXMLObject.
* Takes the information from a Document object and creates a more usable format.
*
* @param doc XML Document Object
*/
RegattaXMLObject(Document doc) {
@@ -173,12 +196,29 @@ public class XMLParser {
this.utcOffset = getElementInt(docEle, "UtcOffset");
}
public Integer getRegattaID() { return regattaID; }
public String getRegattaName() { return regattaName; }
public String getCourseName() { return courseName; }
public Double getCentralLat() { return centralLat; }
public Double getCentralLng() { return centralLng; }
public Integer getUtcOffset() { return utcOffset; }
public Integer getRegattaID() {
return regattaID;
}
public String getRegattaName() {
return regattaName;
}
public String getCourseName() {
return courseName;
}
public Double getCentralLat() {
return centralLat;
}
public Double getCentralLng() {
return centralLng;
}
public Integer getUtcOffset() {
return utcOffset;
}
}
@@ -195,13 +235,18 @@ public class XMLParser {
//Non atomic race attributes
private ArrayList<Participant> participants;
private ArrayList<CompoundMark> course;
private ArrayList<Mark> allMarks;
private ArrayList<Mark> nonDuplicateMarks;
private ArrayList<Corner> compoundMarkSequence;
private ArrayList<Limit> courseLimit;
// ensures there's no duplicate marks.
private List<Long> seenSourceIDs = new ArrayList<Long>();
/**
* Constructor for a RaceXMLObject.
* Takes the information from a Document object and creates a more usable format.
*
* @param doc XML Document Object
*/
RaceXMLObject(Document doc) {
@@ -214,7 +259,8 @@ public class XMLParser {
Node raceStart = docEle.getElementsByTagName("RaceStartTime").item(0);
this.raceStartTime = getNodeAttributeString(raceStart, "Start");
this.postponeStatus = Boolean.parseBoolean(getNodeAttributeString(raceStart, "Postpone"));
this.postponeStatus = Boolean
.parseBoolean(getNodeAttributeString(raceStart, "Postpone"));
//Participants
participants = new ArrayList<>();
@@ -238,21 +284,15 @@ public class XMLParser {
}
//Course
course = new ArrayList<>();
NodeList cMarkList = docEle.getElementsByTagName("Course").item(0).getChildNodes();
for (int i = 0; i < cMarkList.getLength(); i++) {
Node cMarkNode = cMarkList.item(i);
if (cMarkNode.getNodeName().equals("CompoundMark")) {
CompoundMark cMark = new CompoundMark(cMarkNode);
course.add(cMark);
}
}
allMarks = new ArrayList<>();
nonDuplicateMarks = new ArrayList<>();
createCompoundMarks(docEle);
//Course Mark Sequence
compoundMarkSequence = new ArrayList<>();
NodeList cornerList = docEle.getElementsByTagName("CompoundMarkSequence").item(0).getChildNodes();
NodeList cornerList = docEle.getElementsByTagName("CompoundMarkSequence").item(0)
.getChildNodes();
for (int i = 0; i < cornerList.getLength(); i++) {
Node cornerNode = cornerList.item(i);
if (cornerNode.getNodeName().equals("Corner")) {
@@ -274,18 +314,116 @@ public class XMLParser {
}
}
public Integer getRaceID() { return raceID; }
public String getRaceType() { return raceType; }
public String getCreationTimeDate() { return creationTimeDate; }
public String getRaceStartTime() { return raceStartTime; }
public Boolean getPostponeStatus() { return postponeStatus; }
public ArrayList<Participant> getParticipants() { return participants; }
public ArrayList<CompoundMark> getCompoundMarks() { return course; }
public ArrayList<Corner> getCompoundMarkSequence() { return compoundMarkSequence; }
public ArrayList<Limit> getCourseLimit() { return courseLimit; }
private void createCompoundMarks(Element docEle) {
NodeList cMarkList = docEle.getElementsByTagName("Course").item(0).getChildNodes();
for (int i = 0; i < cMarkList.getLength(); i++) {
Node cMarkNode = cMarkList.item(i);
if (cMarkNode.getNodeName().equals("CompoundMark")) {
createAndAddMark(cMarkNode);
}
}
}
private void createAndAddMark(Node compoundMark) {
Boolean markSeen = false;
List<SingleMark> marksList = new ArrayList<>();
Integer compoundMarkID = getNodeAttributeInt(compoundMark, "CompoundMarkID");
String cMarkName = getNodeAttributeString(compoundMark, "Name");
NodeList childMarks = compoundMark.getChildNodes();
for (int i = 0; i < childMarks.getLength(); i++) {
Node markNode = childMarks.item(i);
if (markNode.getNodeName().equals("Mark")) {
Integer sourceID = getNodeAttributeInt(markNode, "SourceID");
String markName = getNodeAttributeString(markNode, "Name");
Double targetLat = getNodeAttributeDouble(markNode, "TargetLat");
Double targetLng = getNodeAttributeDouble(markNode, "TargetLng");
SingleMark mark = new SingleMark(markName, targetLat, targetLng, sourceID, compoundMarkID);
marksList.add(mark);
}
}
for (SingleMark mark : marksList) {
if (seenSourceIDs.contains(mark.getId())) {
markSeen = true;
} else {
seenSourceIDs.add(mark.getId());
}
}
if (marksList.size() == 1) {
if (!markSeen) {
nonDuplicateMarks.add(marksList.get(0));
}
allMarks.add(marksList.get(0));
} else if (marksList.size() == 2) {
GateMark thisGateMark = new GateMark(cMarkName, MarkType.OPEN_GATE, marksList.get(0),
marksList.get(1), marksList.get(0).getLatitude(),
marksList.get(0).getLongitude(), compoundMarkID);
if(!markSeen) {
nonDuplicateMarks.add(thisGateMark);
}
allMarks.add(thisGateMark);
}
}
public Integer getRaceID() {
return raceID;
}
public String getRaceType() {
return raceType;
}
public String getCreationTimeDate() {
return creationTimeDate;
}
public String getRaceStartTime() {
return raceStartTime;
}
public Boolean getPostponeStatus() {
return postponeStatus;
}
public ArrayList<Participant> getParticipants() {
return participants;
}
/**
* @return Returns ALL compound marks as stated in the RaceXML (INCLUDING DUPLICATE MARKS)
*/
public List<Mark> getAllCompoundMarks() {
return allMarks;
}
/**
* @return Returns Marks from the race XML without any duplicates
*/
public List<Mark> getNonDupCompoundMarks() {
return nonDuplicateMarks;
}
public ArrayList<Corner> getCompoundMarkSequence() {
return compoundMarkSequence;
}
public ArrayList<Limit> getCourseLimit() {
return courseLimit;
}
public class Participant {
Integer sourceID;
String entry;
@@ -294,57 +432,17 @@ public class XMLParser {
this.entry = entry;
}
public Integer getsourceID() { return sourceID; }
public String getEntry() { return entry; }
public Integer getsourceID() {
return sourceID;
}
public class CompoundMark {
private Integer markID;
private String cMarkName;
private ArrayList<Mark> marks;
CompoundMark(Node compoundMark) {
marks = new ArrayList<>();
this.markID = getNodeAttributeInt(compoundMark, "CompoundMarkID");
this.cMarkName = getNodeAttributeString(compoundMark, "Name");
NodeList childMarks = compoundMark.getChildNodes();
for (int i = 0; i < childMarks.getLength(); i++) {
Node markNode = childMarks.item(i);
if (markNode.getNodeName().equals("Mark")) {
Mark mark = new Mark(markNode);
marks.add(mark);
}
}
}
public Integer getMarkID() { return markID; }
public String getcMarkName() { return cMarkName; }
public ArrayList<Mark> getMarks() { return marks; }
public class Mark {
private Integer seqID;
private Integer sourceID;
private String markName;
private Double targetLat;
private Double targetLng;
Mark(Node markNode) {
this.seqID = getNodeAttributeInt(markNode, "SeqID");
this.sourceID = getNodeAttributeInt(markNode, "SourceID");
this.markName = getNodeAttributeString(markNode, "Name");
this.targetLat = getNodeAttributeDouble(markNode, "TargetLat");
this.targetLng = getNodeAttributeDouble(markNode, "TargetLng");
}
public Integer getSeqID() { return seqID; }
public Integer getSourceID() { return sourceID; }
public String getMarkName() { return markName; }
public Double getTargetLat() { return targetLat; }
public Double getTargetLng() { return targetLng; }
public String getEntry() {
return entry;
}
}
public class Corner {
private Integer seqID;
private Integer compoundMarkID;
private String rounding;
@@ -357,13 +455,25 @@ public class XMLParser {
this.zoneSize = getNodeAttributeInt(cornerNode, "ZoneSize");
}
public Integer getSeqID() { return seqID; }
public Integer getCompoundMarkID() { return compoundMarkID; }
public String getRounding() { return rounding; }
public Integer getZoneSize() { return zoneSize; }
public Integer getSeqID() {
return seqID;
}
public Integer getCompoundMarkID() {
return compoundMarkID;
}
public String getRounding() {
return rounding;
}
public Integer getZoneSize() {
return zoneSize;
}
}
public class Limit {
private Integer seqID;
private Double lat;
private Double lng;
@@ -374,9 +484,17 @@ public class XMLParser {
this.lng = getNodeAttributeDouble(limitNode, "Lon");
}
public Integer getSeqID() { return seqID; }
public Double getLat() { return lat; }
public Double getLng() { return lng; }
public Integer getSeqID() {
return seqID;
}
public Double getLat() {
return lat;
}
public Double getLng() {
return lng;
}
}
}
@@ -402,6 +520,7 @@ public class XMLParser {
/**
* Constructor for a BoatXMLObject.
* Takes the information from a Document object and creates a more usable format.
*
* @param doc XML Document Object
*/
BoatXMLObject(Document doc) {
@@ -439,54 +558,53 @@ public class XMLParser {
getNodeAttributeString(currentBoat, "Country"));
this.boats.add(boat);
if (boat.getBoatType().equals("Yacht")) {
competingBoats.put(boat.getSourceID(), boat);
competingBoats.put(boat.getSourceId(), boat);
}
}
//System.out.println(this.getBoats());
}
}
public String getLastModified() { return lastModified; }
public Integer getVersion() { return version; }
public String getBoatType() { return boatType; }
public Double getBoatLength() { return boatLength; }
public Double getHullLength() { return hullLength; }
public Double getMarkZoneSize() { return markZoneSize; }
public Double getCourseZoneSize() { return courseZoneSize; }
public ArrayList<Double> getZoneLimits() { return zoneLimits; }
public ArrayList<Yacht> getBoats() { return boats; }
public String getLastModified() {
return lastModified;
}
public Integer getVersion() {
return version;
}
public String getBoatType() {
return boatType;
}
public Double getBoatLength() {
return boatLength;
}
public Double getHullLength() {
return hullLength;
}
public Double getMarkZoneSize() {
return markZoneSize;
}
public Double getCourseZoneSize() {
return courseZoneSize;
}
public ArrayList<Double> getZoneLimits() {
return zoneLimits;
}
public ArrayList<Yacht> getBoats() {
return boats;
}
public Map<Integer, Yacht> getCompetingBoats() {
return competingBoats;
}
// public class Boat {
//
// private String boatType;
// private Integer sourceID;
// private String hullID; //matches HullNum in the XML spec.
// private String shortName;
// private String boatName;
// private String country;
//
// Boat(Node boatNode) {
// this.boatType = getNodeAttributeString(boatNode, "Type");
// this.sourceID = getNodeAttributeInt(boatNode, "SourceID");
// this.hullID = getNodeAttributeString(boatNode, "HullNum");
// this.shortName = getNodeAttributeString(boatNode, "ShortName");
// this.boatName = getNodeAttributeString(boatNode, "BoatName");
// this.country = getNodeAttributeString(boatNode, "Country");
// }
//
// public String getBoatType() { return boatType; }
// public Integer getSourceID() { return sourceID; }
// public String getHullID() { return hullID; }
// public String getShortName() { return shortName; }
// public String getBoatName() { return boatName; }
// public String getCountry() { return country; }
//
// }
}
}
@@ -1,4 +1,4 @@
package seng302.models.parsers.packets;
package seng302.models.stream.packets;
public class BoatPositionPacket {
private long boatId;
@@ -1,4 +1,4 @@
package seng302.models.parsers.packets;
package seng302.models.stream.packets;
/**
* Created by Kusal on 4/24/2017.
@@ -16,6 +16,7 @@ public enum PacketType {
MARK_ROUNDING,
COURSE_WIND,
AVG_WIND,
BOAT_ACTION,
OTHER;
public static PacketType assignPacketType(int packetType){
@@ -44,10 +45,10 @@ public enum PacketType {
return COURSE_WIND;
case 47:
return AVG_WIND;
case 100:
return BOAT_ACTION;
default:
}
return OTHER;
}
}
@@ -1,4 +1,4 @@
package seng302.models.parsers.packets;
package seng302.models.stream.packets;
/**
* Created by kre39 on 23/04/17.
@@ -0,0 +1,53 @@
package seng302.models.xml;
import seng302.models.Yacht;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A Race object that can be parsed into XML
*/
public class Race {
private List<Yacht> yachts;
private LocalDateTime startTime;
public Race(){
yachts = new ArrayList<>();
startTime = LocalDateTime.now();
}
/**
* Add a boat to the race
* @param yacht The boat to add
*/
public void addBoat(Yacht yacht){
yachts.add(yacht);
}
/**
* Get a list of boats in the race
* @return A List of boats
*/
public List<Yacht> getBoats(){
return Collections.unmodifiableList(yachts);
}
/**
* Set the time until the race starts
* @param seconds The time in seconds until the race starts
*/
public void setRaceStartDelay(Integer seconds){
startTime = startTime.plusMinutes(seconds);
}
/**
* Get the time the race starts
* @return The time the race starts
*/
public String getRaceStartTime(){
return startTime.toString();
}
}
@@ -0,0 +1,77 @@
package seng302.models.xml;
/**
* A Race regatta that can be parsed into XML
*/
public class Regatta {
private final Double DEFAULT_ALTITUDE = 0d;
private final Integer DEFAULT_REGATTA_ID = 0;
private Integer id;
private String name;
private String courseName;
private Double latitude;
private Double longitude;
private Double altitude;
private Integer utcOffset;
private Double magneticVariation;
public Regatta(String name, Double latitude, Double longitude) {
this.name = name;
this.id = DEFAULT_REGATTA_ID;
this.courseName = name;
this.latitude = latitude;
this.longitude = longitude;
this.altitude = DEFAULT_ALTITUDE;
this.utcOffset = 0;
this.magneticVariation = 0d;
}
public void setMagneticVariation(Double magneticVariation){
this.magneticVariation = magneticVariation;
}
public void setUtcOffset(Integer offset){
this.utcOffset = offset;
}
/*
NOTE!! The following getters must follow the JavaBean standard (getPropertyName()), and must be public.
*/
public String getName(){
return name;
}
public String getCourseName(){
return courseName;
}
public Integer getRegattaId(){
return id;
}
public Double getLatitude() {
return latitude;
}
public Double getLongitude() {
return longitude;
}
public Double getAltitude() {
return altitude;
}
public Integer getUtcOffset(){
return utcOffset;
}
public Double getMagneticVariation(){
return magneticVariation;
}
}
@@ -0,0 +1,161 @@
package seng302.models.xml;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.apache.commons.io.IOUtils;
import seng302.server.messages.XMLMessageSubType;
import java.io.*;
import java.net.URISyntaxException;
/**
* An XML generator to generate the Race, Boat, and Regatta XML dynamically
*/
public class XMLGenerator {
private static final String XML_TEMPLATE_DIR = "/server_config/xml_templates";
private static final String REGATTA_TEMPLATE_NAME = "regatta.ftlh";
private static final String BOATS_TEMPLATE_NAME = "boats.ftlh";
private static final String RACE_TEMPLATE_NAME = "race.ftlh";
private Configuration configuration;
private Regatta regatta;
private Race race;
/**
* Set up a configuration instance for Apache Freemake
*/
private void setupConfiguration() {
configuration = new Configuration(Configuration.VERSION_2_3_26);
try {
configuration.setClassForTemplateLoading(getClass(), XML_TEMPLATE_DIR);
} catch (NullPointerException e){
System.out.println("[FATAL] Server could not load XML Template directory, ensure this directory isn't empty");
}
}
/**
* Create an instance of the XML Generator
*/
public XMLGenerator(){
setupConfiguration();
}
/**
* Set the race regatta to send to players
* Note: This must be set before a regatta message can be generated
* @param regatta The race regatta
*/
public void setRegatta(Regatta regatta){
this.regatta = regatta;
}
/**
* Set the race to send to players
* Note: This must be set before a boat or race message can be generated
* @param race The race
*/
public void setRace(Race race){
this.race = race;
}
/**
* Parse an XML template and generate the output as a string
* @param templateName The templates file name
* @param type The XML message sub type
*/
private String parseToXmlString(String templateName, XMLMessageSubType type) throws IOException, TemplateException {
Template template;
ByteArrayOutputStream os = new ByteArrayOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(os);
template = configuration.getTemplate(templateName);
switch (type) {
case REGATTA:
template.process(regatta, writer);
break;
case BOAT:
template.process(race, writer);
break;
case RACE:
template.process(race, writer);
break;
default:
throw new UnsupportedOperationException();
}
try {
return os.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
System.out.println("[FATAL] UTF-8 Not supported");
return null;
}
}
/**
* Get the race regatta as a string
* Note: Regatta must be set before calling this
* @return String containing the regatta XML, null if there was an error
*/
public String getRegattaAsXml(){
String result = null;
if (regatta == null) return null;
try {
result = parseToXmlString(REGATTA_TEMPLATE_NAME, XMLMessageSubType.REGATTA);
} catch (TemplateException e) {
System.out.println("[FATAL] Error parsing regatta");
} catch (IOException e) {
System.out.println("[FATAL] Error reading regatta");
}
return result;
}
/**
* Get the boats XML as a string
* Note: Race must be set before calling this
* @return String containing the boats XML, null if there was an error
*/
public String getBoatsAsXml() {
String result = null;
if (race == null) return null;
try {
result = parseToXmlString(BOATS_TEMPLATE_NAME, XMLMessageSubType.BOAT);
} catch (TemplateException e) {
System.out.println("[FATAL] Error parsing boats");
} catch (IOException e) {
System.out.println("[FATAL] Error reading boats");
}
return result;
}
/**
* Get the race XML as a string
* Note: Race must be set before calling this
* @return String containing the race XML, null if there was an error
*/
public String getRaceAsXml() {
String result = null;
if (race == null) return null;
try {
result = parseToXmlString(RACE_TEMPLATE_NAME, XMLMessageSubType.RACE);
} catch (TemplateException e) {
System.out.println("[FATAL] Error parsing race");
} catch (IOException e) {
System.out.println("[FATAL] Error reading race");
}
return result;
}
}
@@ -1,323 +0,0 @@
package seng302.server;
import seng302.server.messages.*;
import seng302.server.simulator.Boat;
import seng302.server.simulator.Simulator;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
public class ServerThread implements Runnable, Observer {
private StreamingServerSocket server;
private long startTime;
private boolean raceStarted = false;
private Map<Integer,Boolean> boatsFinished = new HashMap<>();
private List<Boat> boats;
private Simulator raceSimulator;
private boolean sendingRaceFinishedLocationMessages = true;
private final int HEARTBEAT_PERIOD = 5000;
private final int RACE_STATUS_PERIOD = 1000/2;
private final int RACE_START_STATUS_PERIOD = 1000;
private final int BOAT_LOCATION_PERIOD = 1000/5;
private final int PORT_NUMBER = 4949;
private final int TIME_TILL_RACE_START = 20*1000;
private static final int LOG_LEVEL = 1;
public ServerThread(String threadName){
Thread runner = new Thread(this, threadName);
runner.setDaemon(true);
serverLog("Spawning Server", 0);
raceSimulator = new Simulator(BOAT_LOCATION_PERIOD);
raceSimulator.addObserver(this);
// run race simulator, so it can send boats' static location.
Thread raceSimulatorThread = new Thread(raceSimulator, "Race Simulator");
boats = raceSimulator.getBoats();
for (Boat b : boats){
boatsFinished.put(b.getSourceID(), false);
}
runner.start();
raceSimulatorThread.start();
}
static void serverLog(String message, int logLevel){
if(logLevel <= LOG_LEVEL){
System.out.println("[SERVER] " + message);
}
}
/**
* Creates and returns an XML Message from the file specified
* @param fileName The source XML file
* @param type The XML Message type
* @return The XML Message
*/
private Message getXmlMessage(String fileName, XMLMessageSubType type){
String fileContents = null;
try {
InputStream thisStream = this.getClass().getResourceAsStream(fileName);
fileContents = new String(org.apache.commons.io.IOUtils.toByteArray(thisStream));
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException e){
return null;
}
if (fileContents != null){
return new XMLMessage(fileContents, type, server.getSequenceNumber());
}
return null;
}
/**
* @return Get a race status message for the current race
*/
private Message getRaceStatusMessage(){
List<BoatSubMessage> boatSubMessages = new ArrayList<>();
BoatStatus boatStatus;
RaceStatus raceStatus;
boolean thereAreBoatsNotFinished = false;
for (Boat b : boats){
if (!raceStarted){
boatStatus = BoatStatus.PRESTART;
thereAreBoatsNotFinished = true;
}
else if(boatsFinished.get(b.getSourceID())){
boatStatus = BoatStatus.FINISHED;
}
else{
boatStatus = BoatStatus.PRESTART;
thereAreBoatsNotFinished = true;
}
BoatSubMessage m = new BoatSubMessage(b.getSourceID(), boatStatus, b.getLastPassedCorner().getSeqID(), 0, 0, b.getEstimatedTimeTillFinish(), b.getEstimatedTimeTillFinish());
boatSubMessages.add(m);
}
if (thereAreBoatsNotFinished){
if (raceStarted){
raceStatus = RaceStatus.STARTED;
}
else{
long currentTime = System.currentTimeMillis();
long timeDifference = startTime - currentTime;
if (timeDifference > 60*3){
raceStatus = RaceStatus.PRESTART;
}
else if (timeDifference > 60){
raceStatus = RaceStatus.WARNING;
}
else{
raceStatus = RaceStatus.PREPARATORY;
}
}
}
else{
raceStatus = RaceStatus.TERMINATED;
}
return new RaceStatusMessage(1, raceStatus, startTime, WindDirection.SOUTH,
100, boats.size(), RaceType.MATCH_RACE, 1, boatSubMessages);
}
/**
* Starts an instance of the race simulator
*/
private void startRaceSim(){
serverLog("Starting Running Race Simulator", 0);
// set race started to true, so the simulator will start moving boats
raceSimulator.setRaceStarted(true);
}
/**
* Starts sending heartbeat messages to the client
*/
private void startSendingHeartbeats(){
serverLog("Sending Heartbeats", 0);
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
Message heartbeat = new Heartbeat(server.getSequenceNumber());
try {
server.send(heartbeat);
} catch (IOException e) {
System.out.print("");
}
}
}, 0, HEARTBEAT_PERIOD);
}
/**
* Start sending race start status messages until race starts
*/
private void startSendingRaceStartStatusMessages(){
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
Message raceStartStatusMessage = new RaceStartStatusMessage(server.getSequenceNumber(), startTime , 1,
RaceStartNotificationType.SET_RACE_START_TIME);
try {
if (startTime < System.currentTimeMillis() && !raceStarted){
startRaceSim();
raceStarted = true;
serverLog("Race Started", 0);
}
else{
server.send(raceStartStatusMessage);
}
} catch (IOException e) {
System.out.print("");
}
}
}, 0, RACE_START_STATUS_PERIOD);
}
/**
* Start sending race start status messages until race starts
*/
private void startSendingRaceStatusMessages(){
serverLog("Sending race status messages", 0);
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
Message raceStatusMessage = getRaceStatusMessage();
try {
server.send(raceStatusMessage);
} catch (IOException e) {
System.out.print("");
}
}
}, 0, RACE_STATUS_PERIOD);
}
/**
* Sends the race, boat, and regatta XML files to the client
*/
private void sendXml(){
try{
Message raceData = getXmlMessage("/server_config/race.xml", XMLMessageSubType.RACE);
Message boatData = getXmlMessage("/server_config/boats.xml", XMLMessageSubType.BOAT);
Message regatta = getXmlMessage("/server_config/regatta.xml", XMLMessageSubType.REGATTA);
if (raceData != null){
server.send(raceData);
serverLog("Sending race data", 0);
}
if (boatData != null){
server.send(boatData);
serverLog("Sending boat data", 0);
}
if (regatta != null){
server.send(regatta);
serverLog("Sending regatta data", 0);
}
} catch (IOException e) {
serverLog("Couldn't send an XML Message: " + e.getMessage(), 0);
}
}
public void run() {
try{
server = new StreamingServerSocket(PORT_NUMBER);
}
catch (IOException e){
serverLog("Failed to bind socket: " + e.getMessage(), 0);
}
// Wait for client to connect
server.start();
startTime = System.currentTimeMillis() + TIME_TILL_RACE_START;
startSendingHeartbeats();
sendXml();
startSendingRaceStartStatusMessages();
startSendingRaceStatusMessages();
}
/**
* Start sending static boat position updates when race has finished
*/
private void startSendingRaceFinishedBoatPostions(){
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
try {
for (Boat b : raceSimulator.getBoats()){
Message m = new BoatLocationMessage(b.getSourceID(), server.getSequenceNumber(), b.getLat(),
b.getLng(), b.getLastPassedCorner().getBearingToNextCorner(),
((long) 0));
server.send(m);
}
} catch (IOException e) {
System.out.print("");
}
}
}, 0, BOAT_LOCATION_PERIOD);
}
/**
* Send a boat location message when they are updated by the simulator
* @param o .
* @param arg .
*/
@Override
@SuppressWarnings("unchecked")
public void update(Observable o, Object arg) {
// Only send if server started
// TODO: I don't understand why i need to check server is null or not ... confused - haoming 2/5/17
if(server == null || !server.isStarted()){
return;
}
int numOfBoatsFinished = 0;
for (Boat boat : (List<Boat>) arg){
try {
if (boat.isFinished()) {
numOfBoatsFinished ++;
if (!boatsFinished.get(boat.getSourceID())) {
boatsFinished.put(boat.getSourceID(), true);
serverLog("Boat " + boat.getSourceID() + " finished the race", 1);
}
}
Message m = new BoatLocationMessage(boat.getSourceID(), 1, boat.getLat(),
boat.getLng(), boat.getLastPassedCorner().getBearingToNextCorner(),
((long) boat.getSpeed()));
server.send(m);
} catch (IOException e) {
serverLog("Couldn't send a boat status message", 3);
return;
}
catch (NullPointerException e){
e.printStackTrace();
}
}
if (numOfBoatsFinished == ((List<Boat>) arg).size()) {
startSendingRaceFinishedBoatPostions();
}
}
}
@@ -1,63 +0,0 @@
package seng302.server;
import seng302.server.messages.Message;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.Channels;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
class StreamingServerSocket {
private ServerSocketChannel socket;
private SocketChannel client;
private short seqNum;
private boolean isServerStarted;
StreamingServerSocket(int port) throws IOException{
socket = ServerSocketChannel.open();
socket.socket().bind(new InetSocketAddress("localhost", port));
//socket.setSoTimeout(10000);
seqNum = 0;
isServerStarted = false;
}
void start(){
ServerThread.serverLog("Listening For Connections",0);
try {
client = socket.accept();
} catch (IOException e) {
e.getMessage();
}
if (client.socket() == null){
start();
}
else{
isServerStarted = true;
ServerThread.serverLog("client connected from " + client.socket().getInetAddress(),0);
}
}
void send(Message message) throws IOException{
if (client == null){
return;
}
message.send(client);
seqNum++;
}
public short getSequenceNumber(){
return seqNum;
}
public boolean isStarted(){
return isServerStarted;
}
}
@@ -0,0 +1,33 @@
package seng302.server.messages;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
/**
* Created by kre39 on 12/07/17.
*/
public class BoatActionMessage extends Message{
private final MessageType MESSAGE_TYPE = MessageType.BOAT_ACTION;
private final int MESSAGE_SIZE = 1;
private BoatActionType actionType;
public BoatActionMessage(BoatActionType actionType) {
this.actionType = actionType;
setHeader(new Header(MessageType.BOAT_ACTION, 0, (short) 1)); // the second variable is the source id
allocateBuffer();
writeHeaderToBuffer();
// Write message fields
putInt(actionType.getValue(), 1);
writeCRC();
rewind();
}
@Override
public int getSize() {
return MESSAGE_SIZE;
}
}
@@ -0,0 +1,38 @@
package seng302.server.messages;
import java.util.HashMap;
import java.util.Map;
/**
* Created by kre39 on 12/07/17.
*/
public enum BoatActionType {
VMG(1),
SAILS_IN(2),
SAILS_OUT(3),
TACK_GYBE(4),
UPWIND(5),
DOWNWIND(6);
private final int type;
private static final Map<Integer, BoatActionType> intToTypeMap = new HashMap<>();
static {
for (BoatActionType type : BoatActionType.values()) {
intToTypeMap.put(type.getValue(), type);
}
}
BoatActionType(int type){
this.type = type;
}
public static BoatActionType getType(int value) {
return intToTypeMap.get(value);
}
public int getValue() {
return this.type;
}
}
@@ -1,10 +1,7 @@
package seng302.server.messages;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.io.OutputStream;
public class BoatLocationMessage extends Message {
private final int MESSAGE_SIZE = 56;
@@ -42,9 +39,8 @@ public class BoatLocationMessage extends Message {
* @param boatSpeed The boats speed
*/
public BoatLocationMessage(int sourceId, int sequenceNum, double latitude, double longitude, double heading, long boatSpeed){
boatSpeed /= 10;
messageVersionNumber = 1;
time = System.currentTimeMillis() / 1000L;
time = System.currentTimeMillis();
this.sourceId = sourceId;
this.sequenceNum = sequenceNum;
this.deviceType = DeviceType.RACING_YACHT;
@@ -67,6 +63,36 @@ public class BoatLocationMessage extends Message {
this.rudderAngle = 0;
setHeader(new Header(MessageType.BOAT_LOCATION, 1, (short) getSize()));
allocateBuffer();
writeHeaderToBuffer();
long headingToSend = (long)((heading/360.0) * 65535.0);
putByte((byte) messageVersionNumber);
putInt(time, 6);
putInt((int) sourceId, 4);
putUnsignedInt((int) sequenceNum, 4);
putByte((byte) deviceType.getCode());
putInt((int) latLonToBinaryPackedLong(latitude), 4);
putInt((int) latLonToBinaryPackedLong(longitude), 4);
putInt((int) altitude, 4);
putInt(headingToSend, 2);
putInt((int) pitch, 2);
putInt((int) roll, 2);
putInt((int) boatSpeed, 2);
putUnsignedInt((int) COG, 2);
putUnsignedInt((int) SOG, 2);
putUnsignedInt((int) apparentWindSpeed, 2);
putInt((int) apparentWindAngle, 2);
putUnsignedInt((int) trueWindSpeed, 2);
putUnsignedInt((int) trueWindDirection, 2);
putInt((int) trueWindAngle, 2);
putUnsignedInt((int) currentDrift, 2);
putUnsignedInt((int) currentSet, 2);
putInt((int) rudderAngle, 2);
writeCRC();
rewind();
}
/**
@@ -127,41 +153,4 @@ public class BoatLocationMessage extends Message {
public int getSize() {
return MESSAGE_SIZE;
}
@Override
public void send(SocketChannel outputStream) throws IOException{
allocateBuffer();
writeHeaderToBuffer();
long headingToSend = (long)((heading/360.0) * 65535.0);
putByte((byte) messageVersionNumber);
putInt(time, 6);
putInt((int) sourceId, 4);
putUnsignedInt((int) sequenceNum, 4);
putByte((byte) deviceType.getCode());
putInt((int) latLonToBinaryPackedLong(latitude), 4);
putInt((int) latLonToBinaryPackedLong(longitude), 4);
putInt((int) altitude, 4);
putInt(headingToSend, 2);
putInt((int) pitch, 2);
putInt((int) roll, 2);
putInt((int) boatSpeed, 2);
putUnsignedInt((int) COG, 2);
putUnsignedInt((int) SOG, 2);
putUnsignedInt((int) apparentWindSpeed, 2);
putInt((int) apparentWindAngle, 2);
putUnsignedInt((int) trueWindSpeed, 2);
putUnsignedInt((int) trueWindDirection, 2);
putInt((int) trueWindAngle, 2);
putUnsignedInt((int) currentDrift, 2);
putUnsignedInt((int) currentSet, 2);
putInt((int) rudderAngle, 2);
writeCRC();
rewind();
outputStream.write(getBuffer());
}
}
@@ -0,0 +1,38 @@
package seng302.server.messages;
/**
* Created by kre39 on 20/07/17.
*/
public class ChatterMessage extends Message {
private final long MESSAGE_VERSION_NUMBER = 1;
private final int MESSAGE_SIZE = 3;
private int message_type;
private int message_size = 21;
private String message;
public ChatterMessage(int message_type, int message_size, String message) {
this.message_type = message_type;
this.message_size = message_size;
this.message = message;
setHeader(new Header(MessageType.CHATTER_TEXT, 1, (short) getSize()));
allocateBuffer();
writeHeaderToBuffer();
putByte((byte) MESSAGE_VERSION_NUMBER);
putInt(message_type, 1);
putInt(message_size, 1);
putBytes(message.getBytes());
writeCRC();
rewind();
}
@Override
public int getSize() {
return MESSAGE_SIZE + message_size;
}
}
@@ -43,10 +43,21 @@ public class Header {
buff.position(buffPos);
}
/**
* Reset the buffer
*/
public void reset(){
buffPos = 0;
buff.clear();
buff.position(buffPos);
}
/**
* @return a ByteBuffer containing the message header
*/
public ByteBuffer getByteBuffer(){
reset();
putInBuffer(ByteBuffer.allocate(1).put((byte)syncByte1).array(), syncByte1);
putInBuffer(ByteBuffer.allocate(1).put((byte)syncByte2).array(), syncByte2);
@@ -1,32 +1,16 @@
package seng302.server.messages;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.util.zip.CRC32;
import java.io.OutputStream;
public class Heartbeat extends Message {
private final int MESSAGE_SIZE = 4;
private int seqNo;
/**
* Heartbeat from the AC35 Streaming data spec
* @param seqNo Increment every time a message is sent
*/
public Heartbeat(int seqNo){
this.seqNo = seqNo;
}
@Override
public int getSize() {
return MESSAGE_SIZE;
}
@Override
public void send(SocketChannel outputStream) throws IOException {
setHeader(new Header(MessageType.HEARTBEAT, 0x01, (short) getSize()));
allocateBuffer();
@@ -36,7 +20,11 @@ public class Heartbeat extends Message {
writeCRC();
rewind();
}
outputStream.write(getBuffer());
@Override
public int getSize() {
return MESSAGE_SIZE;
}
}
@@ -1,10 +1,7 @@
package seng302.server.messages;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.io.OutputStream;
public class MarkRoundingMessage extends Message{
private final long MESSAGE_VERSION_NUMBER = 1;
@@ -33,15 +30,6 @@ public class MarkRoundingMessage extends Message{
this.markId = markId;
setHeader(new Header(MessageType.MARK_ROUNDING, 1, (short) getSize()));
}
@Override
public int getSize() {
return MESSAGE_SIZE;
}
@Override
public void send(SocketChannel outputStream) throws IOException {
allocateBuffer();
writeHeaderToBuffer();
@@ -56,7 +44,10 @@ public class MarkRoundingMessage extends Message{
writeCRC();
rewind();
}
outputStream.write(getBuffer());
@Override
public int getSize() {
return MESSAGE_SIZE;
}
}
@@ -1,9 +1,9 @@
package seng302.server.messages;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.zip.CRC32;
@@ -33,11 +33,6 @@ public abstract class Message {
*/
public abstract int getSize();
/**
* Send the message as through the outputStream
*/
public abstract void send(SocketChannel outputStream) throws IOException;
/**
* Allocate byte buffer to correct size
*/
@@ -45,6 +40,7 @@ public abstract class Message {
buffer = ByteBuffer.allocate(Header.getSize() + getSize() + CRC_SIZE);
buffer.order(ByteOrder.LITTLE_ENDIAN);
bufferPosition = 0;
buffer.position(bufferPosition);
}
/**
@@ -161,10 +157,10 @@ public abstract class Message {
}
/**
* @return The current buffer
* @return The current buffer as a byte array
*/
public ByteBuffer getBuffer(){
return buffer;
public byte[] getBuffer(){
return buffer.array();
}
/**
@@ -193,6 +189,25 @@ public abstract class Message {
return data;
}
/**
* takes an array of up to 7 bytes in little endian format and
* returns a positive long constructed from the input bytes
*
* @return a positive long if there is less than 8 bytes -1 otherwise
*/
public static long bytesToLong(byte[] bytes){
long partialLong = 0;
int index = 0;
for (byte b: bytes){
if (index > 6){
return -1;
}
partialLong = partialLong | (b & 0xFFL) << (index * 8);
index++;
}
return partialLong;
}
/**
* Reverse an array of bytes
* @param data The byte[] to reverse
@@ -16,7 +16,8 @@ public enum MessageType {
BOAT_LOCATION(37),
MARK_ROUNDING(38),
COURSE_WIND(44),
AVERAGE_WIND(47);
AVERAGE_WIND(47),
BOAT_ACTION(100);
private int code;
@@ -1,10 +1,7 @@
package seng302.server.messages;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.io.OutputStream;
public class RaceStartStatusMessage extends Message {
private final int MESSAGE_SIZE = 20;
@@ -32,15 +29,6 @@ public class RaceStartStatusMessage extends Message {
this.raceId = raceId;
setHeader(new Header(MessageType.RACE_START_STATUS, 1, (short) getSize()));
}
@Override
public int getSize() {
return MESSAGE_SIZE;
}
@Override
public void send(SocketChannel outputStream) throws IOException {
allocateBuffer();
writeHeaderToBuffer();
@@ -53,7 +41,11 @@ public class RaceStartStatusMessage extends Message {
writeCRC();
rewind();
}
outputStream.write(getBuffer());
@Override
public int getSize() {
return MESSAGE_SIZE;
}
}
@@ -1,7 +1,7 @@
package seng302.server.messages;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.io.OutputStream;
import java.util.List;
import java.util.zip.CRC32;
@@ -9,12 +9,14 @@ public class RaceStatusMessage extends Message{
private final MessageType MESSAGE_TYPE = MessageType.RACE_STATUS;
private final int MESSAGE_VERSION = 2; //Always set to 1
private final int MESSAGE_BASE_SIZE = 24;
private final double windDirFactor = 0x4000 / 90;
private long currentTime;
private long raceId;
private RaceStatus raceStatus;
private long expectedStartTime;
private WindDirection raceWindDirection;
private double raceWindDirection;
private long windSpeed;
private long numBoatsInRace;
private RaceType raceType;
@@ -33,13 +35,13 @@ public class RaceStatusMessage extends Message{
* @param sourceId The source of this message
* @param boats A list of boat status sub messages
*/
public RaceStatusMessage(long raceId, RaceStatus raceStatus, long expectedStartTime, WindDirection raceWindDirection,
public RaceStatusMessage(long raceId, RaceStatus raceStatus, long expectedStartTime, double raceWindDirection,
long windSpeed, long numBoatsInRace, RaceType raceType, long sourceId, List<BoatSubMessage> boats){
currentTime = System.currentTimeMillis();
this.raceId = raceId;
this.raceStatus = raceStatus;
this.expectedStartTime = expectedStartTime;
this.raceWindDirection = raceWindDirection;
this.raceWindDirection = raceWindDirection * windDirFactor;
this.windSpeed = windSpeed;
this.numBoatsInRace = numBoatsInRace;
this.raceType = raceType;
@@ -47,22 +49,6 @@ public class RaceStatusMessage extends Message{
crc = new CRC32();
setHeader(new Header(MESSAGE_TYPE, (int) sourceId, (short) getSize()));
}
/**
* @return the size of this message in bytes
*/
@Override
public int getSize() {
return MESSAGE_BASE_SIZE + (20 * ((int) numBoatsInRace));
}
/**
* Send this message as a stream of bytes
* @param outputStream The output stream to send the message
*/
@Override
public void send(SocketChannel outputStream) throws IOException {
allocateBuffer();
writeHeaderToBuffer();
@@ -71,7 +57,7 @@ public class RaceStatusMessage extends Message{
putInt((int) raceId, 4);
putByte((byte) raceStatus.getCode());
putInt(expectedStartTime, 6);
putInt((int) raceWindDirection.getCode(), 2);
putInt((int) this.raceWindDirection, 2);
putInt((int) windSpeed, 2);
putByte((byte) numBoatsInRace);
putByte((byte) raceType.getCode());
@@ -82,7 +68,14 @@ public class RaceStatusMessage extends Message{
writeCRC();
rewind();
}
outputStream.write(getBuffer());
/**
* @return the size of this message in bytes
*/
@Override
public int getSize() {
return MESSAGE_BASE_SIZE + (20 * ((int) numBoatsInRace));
}
}
@@ -1,20 +0,0 @@
package seng302.server.messages;
/**
* Enum containing the supported wind directions
*/
public enum WindDirection {
NORTH(0x0000L),
EAST(0x4000L),
SOUTH(0x8000L);
private long code;
WindDirection(long code) {
this.code = code;
}
public long getCode() {
return code;
}
}
@@ -1,12 +1,7 @@
package seng302.server.messages;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.util.zip.CRC32;
import java.io.OutputStream;
public class XMLMessage extends Message{
private final MessageType MESSAGE_TYPE = MessageType.XML_MESSAGE;
@@ -35,20 +30,6 @@ public class XMLMessage extends Message{
sequence = sequenceNum;
setHeader(new Header(MESSAGE_TYPE, 0x01, (short) getSize()));
}
/**
* @return The length of this message
*/
public int getSize(){
return MESSAGE_SIZE + content.length();
}
/**
* Send this message as a stream of bytes
* @param outputStream The output stream to send the message
*/
public void send(SocketChannel outputStream) throws IOException {
allocateBuffer();
writeHeaderToBuffer();
@@ -63,7 +44,12 @@ public class XMLMessage extends Message{
writeCRC();
rewind();
}
outputStream.write(getBuffer());
/**
* @return The length of this message
*/
public int getSize(){
return MESSAGE_SIZE + content.length();
}
}
@@ -1,7 +1,8 @@
package seng302.server.simulator;
import seng302.server.simulator.mark.Corner;
import seng302.server.simulator.mark.Position;
import seng302.utilities.GeoPoint;
import seng302.utilities.GeoUtility;
public class Boat {
@@ -29,8 +30,8 @@ public class Boat {
*/
public void move(double heading, double duration) {
Double distance = speed * duration / 1000000; // convert mm to meter
Position originPos = new Position(lat, lng);
Position newPos = GeoUtility.getGeoCoordinate(originPos, heading, distance);
GeoPoint originPos = new GeoPoint(lat, lng);
GeoPoint newPos = GeoUtility.getGeoCoordinate(originPos, heading, distance);
this.lat = newPos.getLat();
this.lng = newPos.getLng();
}
@@ -2,8 +2,9 @@ package seng302.server.simulator;
import seng302.server.simulator.mark.Corner;
import seng302.server.simulator.mark.Mark;
import seng302.server.simulator.mark.Position;
import seng302.server.simulator.parsers.RaceParser;
import seng302.utilities.GeoPoint;
import seng302.utilities.GeoUtility;
import java.util.List;
import java.util.Observable;
@@ -54,7 +55,6 @@ public class Simulator extends Observable implements Runnable {
numOfFinishedBoats += moveBoat(boat, lapse);
}
}
//System.out.println(boats.get(0));
setChanged();
notifyObservers(boats);
@@ -62,11 +62,9 @@ public class Simulator extends Observable implements Runnable {
try {
Thread.sleep(lapse);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("[Simulator] interrupted exception ");
}
}
System.out.println("[SERVER] Race simulator has been terminated");
}
/**
@@ -80,8 +78,8 @@ public class Simulator extends Observable implements Runnable {
boat.move(boat.getLastPassedCorner().getBearingToNextCorner(), duration);
Position boatPos = new Position(boat.getLat(), boat.getLng());
Position lastMarkPos = boat.getLastPassedCorner().getCompoundMark().getMark1();
GeoPoint boatPos = new GeoPoint(boat.getLat(), boat.getLng());
GeoPoint lastMarkPos = boat.getLastPassedCorner().getCompoundMark().getMark1();
double distanceFromLastMark = GeoUtility.getDistance(boatPos, lastMarkPos);
// if a boat passes its heading mark
@@ -97,13 +95,13 @@ public class Simulator extends Observable implements Runnable {
}
// move compensate distance for the mark just passed
Position pos = GeoUtility.getGeoCoordinate(
GeoPoint pos = GeoUtility.getGeoCoordinate(
boat.getLastPassedCorner().getCompoundMark().getMark1(),
boat.getLastPassedCorner().getBearingToNextCorner(),
compensateDistance);
boat.setLat(pos.getLat());
boat.setLng(pos.getLng());
distanceFromLastMark = GeoUtility.getDistance(new Position(boat.getLat(), boat.getLng()),
distanceFromLastMark = GeoUtility.getDistance(new GeoPoint(boat.getLat(), boat.getLng()),
boat.getLastPassedCorner().getCompoundMark().getMark1());
}
}
@@ -1,10 +1,12 @@
package seng302.server.simulator.mark;
import seng302.utilities.GeoPoint;
/**
* An abstract class to represent general marks
* Created by Haoming Yin (hyi25) on 17/3/17.
*/
public class Mark extends Position {
public class Mark extends GeoPoint {
private int seqID;
private String name;
@@ -22,7 +24,7 @@ public class Mark extends Position {
*/
@Override
public String toString() {
return String.format("Mark%d: %s, source: %d, lat: %f, lng: %f", seqID, name, sourceID, lat, lng);
return String.format("Mark%d: %s, source: %d, lat: %f, lng: %f", seqID, name, sourceID, getLat(), getLng());
}
public int getSeqID() {
@@ -31,7 +31,7 @@ public abstract class FileParser {
doc.getDocumentElement().normalize();
return doc;
} catch (Exception e) {
e.printStackTrace();
System.out.println("[FileParser] Exception");
return null;
}
}
@@ -45,7 +45,7 @@ public abstract class FileParser {
doc.getDocumentElement().normalize();
return doc;
} catch (Exception e) {
e.printStackTrace();
System.out.println("[FileParser] Exception");
}
return null;
}
@@ -1,18 +1,18 @@
package seng302.server.simulator.mark;
package seng302.utilities;
public class Position {
/**
* A class represent Geo location (latitude, longitude).
* Created by Haoming on 15/5/2017
*/
public class GeoPoint {
double lat, lng;
public Position(double lat, double lng) {
public GeoPoint(double lat, double lng) {
this.lat = lat;
this.lng = lng;
}
public String toString() {
return String.format("Position at lat:%f lng:%f.", lat, lng);
}
public double getLat() {
return lat;
}
@@ -1,6 +1,6 @@
package seng302.server.simulator;
package seng302.utilities;
import seng302.server.simulator.mark.Position;
import javafx.geometry.Point2D;
public class GeoUtility {
@@ -13,7 +13,7 @@ public class GeoUtility {
* @param p2 second geographical position
* @return the distance in meter between two points in meters
*/
public static Double getDistance(Position p1, Position p2) {
public static Double getDistance(GeoPoint p1, GeoPoint p2) {
double dLat = Math.toRadians(p2.getLat() - p1.getLat());
double dLon = Math.toRadians(p2.getLng() - p1.getLng());
@@ -42,7 +42,7 @@ public class GeoUtility {
* ( Baghdad) to 35°N,135°E ( Osaka), you would start on a heading of 60°
* and end up on a heading of 120°
*/
public static Double getBearing(Position p1, Position p2) {
public static Double getBearing(GeoPoint p1, GeoPoint p2) {
double dLon = Math.toRadians(p2.getLng() - p1.getLng());
@@ -64,7 +64,7 @@ public class GeoUtility {
* @param distance the distance in meter, from original position to the new position
* @return the new position
*/
public static Position getGeoCoordinate(Position origin, Double bearing, Double distance) {
public static GeoPoint getGeoCoordinate(GeoPoint origin, Double bearing, Double distance) {
double b = Math.toRadians(bearing); // bearing to radians
double d = distance / 1000.0; // distance to km
@@ -77,6 +77,57 @@ public class GeoUtility {
+ Math.atan2(Math.sin(b) * Math.sin(d / EARTH_RADIUS) * Math.cos(originLat),
Math.cos(d / EARTH_RADIUS) - Math.sin(originLat) * Math.sin(endLat));
return new Position(Math.toDegrees(endLat), Math.toDegrees(endLng));
return new GeoPoint(Math.toDegrees(endLat), Math.toDegrees(endLng));
}
/**
* Performs the line function on two points of a line and a test point to test which side of the line that point is
* on. If the return value is
* return 1, then the point is on one side of the line,
* return -1 then the point is on the other side of the line
* return 0 then the point is exactly on the line.
* @param linePoint1 One point of the line
* @param linePoint2 Second point of the line
* @param testPoint The point to test with this line
* @return A return value indicating which side of the line the point is on
*/
public static Integer lineFunction(Point2D linePoint1, Point2D linePoint2, Point2D testPoint) {
Double x = testPoint.getX();
Double y = testPoint.getY();
Double x1 = linePoint1.getX();
Double y1 = linePoint1.getY();
Double x2 = linePoint2.getX();
Double y2 = linePoint2.getY();
Double result = (x - x1)*(y2 - y1) - (y - y1)*(x2 - x1); //Line function
if (result > 0) {
return 1;
}
else if (result < 0) {
return -1;
}
else {
return 0;
}
}
/**
* Given a point and a vector (angle and vector length) Will create a new point, that vector away from the origin
* point
* @param originPoint The point with which to use as the base for our vector addition
* @param angleInDeg (DEGREES) The angle at which our new point is being created (in degrees!)
* @param vectorLength The length out on this angle from the origin point to create the new point
* @return a Point2D
*/
public static Point2D makeArbitraryVectorPoint(Point2D originPoint, Double angleInDeg, Double vectorLength) {
Double endPointX = originPoint.getX() + vectorLength * Math.cos(Math.toRadians(angleInDeg));
Double endPointY = originPoint.getY() + vectorLength * Math.sin(Math.toRadians(angleInDeg));
return new Point2D(endPointX, endPointY);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+8
View File
@@ -0,0 +1,8 @@
Tws,Twa0,Bsp0,Twa1,Bsp1,UpTwa,UpBsp,Twa2,Bsp2,Twa3,Bsp3,Twa4,Bsp4,Twa5,Bsp5,Twa6,Bsp6,DnTwa,DnBsp,Twa7,Bsp7
4,0,0,30,4,45,8,60,9,75,10,90,10,115,10,145,10,155,10,175,4
8,0,0,30,7,43,10,60,11,75,11,90,11,115,12,145,12,153,12,175,10
12,0,0,30,11,43,14.4,60,16,75,20,90,23,115,24,145,23,153,21.6,175,14
16,0,0,30,12,42,19.2,60,25,75,27,90,31,115,32,145,30,153,28.8,175,20
20,0,0,30,13,41,24,60,29,75,37,90,39,115,40,145,38,153,36,175,24
25,0,0,30,15,40,30,60,38,75,44,90,49,115,50,145,49,151,47,175,30
30,0,0,30,15,42,30,60,37,75,42,90,48,115,49,145,48,150,46,175,32
1 Tws Twa0 Bsp0 Twa1 Bsp1 UpTwa UpBsp Twa2 Bsp2 Twa3 Bsp3 Twa4 Bsp4 Twa5 Bsp5 Twa6 Bsp6 DnTwa DnBsp Twa7 Bsp7
2 4 0 0 30 4 45 8 60 9 75 10 90 10 115 10 145 10 155 10 175 4
3 8 0 0 30 7 43 10 60 11 75 11 90 11 115 12 145 12 153 12 175 10
4 12 0 0 30 11 43 14.4 60 16 75 20 90 23 115 24 145 23 153 21.6 175 14
5 16 0 0 30 12 42 19.2 60 25 75 27 90 31 115 32 145 30 153 28.8 175 20
6 20 0 0 30 13 41 24 60 29 75 37 90 39 115 40 145 38 153 36 175 24
7 25 0 0 30 15 40 30 60 38 75 44 90 49 115 50 145 49 151 47 175 30
8 30 0 0 30 15 42 30 60 37 75 42 90 48 115 49 145 48 150 46 175 32
+2 -2
View File
@@ -12,8 +12,8 @@
</mark>
<mark>
<name>Start2</name>
<latitude>57.6706330</latitude>
<longitude>11.8281330</longitude>
<latitude>57.6703330</latitude>
<longitude>11.8271333</longitude>
<id>123</id>
</mark>
</gate>
+200
View File
@@ -0,0 +1,200 @@
/**
Background colours
*/
.background-blue{
-fx-background-color: #119796;
}
.background-dark{
-fx-background-color: #2C2c36;
}
/**
Exit button with no background
*/
.clear-exit-btn{
-fx-background-insets: 0;
-fx-background-color: #119796;
-fx-border-style: none;
}
/**
Buttons
*/
.blue-ui-btn{
-fx-background-color: #119796;
-fx-text-fill: #fff;
}
.text-white {
-fx-text-fill: white !important;
-fx-fill:white !important;
}
/**
Sliders
*/
.ui-slider .thumb {
-fx-background-color: rgb(60, 60, 60);
-fx-border-radius: 10;
-fx-border-color: darkgray;
}
.ui-slider .track{
-fx-background-color: #119796;
}
.ui-slider .axis{
-fx-tick-label-fill: white;
}
.ui-slider .axis .axis-label{
-fx-text-fill: white;
}
/**
Checkbox
*/
.ui-checkbox .box{
-fx-background-color: white;
-fx-graphic:none;
-fx-shape: none;
}
.ui-checkbox .box .mark{
-fx-background-image: none;
-fx-image: none;
-fx-graphic: none;
-fx-shape: none;
}
.ui-checkbox:selected .box{
-fx-background-color: #119796;
-fx-shape: none;
}
.ui-checkbox:selected .box .mark{
-fx-background-color: #119796;
-fx-shape: none;
-fx-graphic: none;
}
/**
Table
*/
.ui-table{
-fx-background-color: transparent;
}
.ui-table:focused{
-fx-background-color: transparent;
}
.ui-table .column-header-background{
-fx-background-color: white
}
.ui-table .column-header-background .label{
-fx-background-color: transparent;
-fx-text-fill: black;
}
.ui-table .column-header {
-fx-background-color: transparent;
}
.ui-table .table-cell{
-fx-text-fill: white;
-fx-border-style: none;
}
.table-row-cell{
-fx-background-color: #119796;
-fx-background-insets: 0, 0 0 1 0;
-fx-padding: 0.0em; /* 0 */
-fx-border-style: none;
}
.table-row-cell:odd{
-fx-background-color: #0e6d6c;
-fx-background-insets: 0, 0 0 1 0;
-fx-padding: 0.0em; /* 0 */
-fx-border-style: none;
}
.table-row-cell:selected {
-fx-background-color: #005797;
-fx-background-insets: 0;
-fx-border-style: none;
}
/**
Combo Box
*/
.combo-box-base {
-fx-background-color: #119796;
-fx-text-fill: white;
}
.combo-box-popup .list-view .list-cell:hover {
-fx-text-fill: white;
}
.combo-box .cell:selected {
-fx-text-fill: white;
}
/**
Remove scroll bars
*/
.ui-table *.scroll-bar:horizontal *.increment-button,
.ui-table *.scroll-bar:horizontal *.decrement-button {
-fx-background-color: null;
-fx-background-radius: 0;
-fx-background-insets: 0;
-fx-padding: 0;
}
.ui-table *.scroll-bar:horizontal *.increment-arrow,
.ui-table *.scroll-bar:horizontal *.decrement-arrow {
-fx-background-color: null;
-fx-background-radius: 0;
-fx-background-insets: 0;
-fx-padding: 0;
-fx-shape: null;
}
.ui-table *.scroll-bar:vertical *.increment-arrow,
.ui-table *.scroll-bar:vertical *.decrement-arrow {
-fx-background-color: #0e6d6c;
-fx-background-radius: 0;
-fx-background-insets: 0;
-fx-padding: 0;
-fx-shape: null;
}
.ui-table *.scroll-bar:vertical *.increment-button,
.ui-table *.scroll-bar:vertical *.decrement-button {
-fx-background-color: #0e6d6c;
-fx-background-radius: 0;
-fx-background-insets: 0;
-fx-padding: 0;
}
.chart{
}
.chart-title {
-fx-text-fill: #ffffff;
-fx-font-size: 1.6em;
}
.axis-label {
-fx-text-fill: #ffffff;
}
.axis {
-fx-tick-label-fill: #ffffff;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Some files were not shown because too many files have changed in this diff Show More