mirror of
https://github.com/michaelrausch/Party-Parrots-At-Sea.git
synced 2026-05-09 06:18:44 +00:00
Compare commits
861 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fcff65dd6 | |||
| 4c730ea890 | |||
| a501b21d66 | |||
| 5fb8a0c2c1 | |||
| d867a4b7a2 | |||
| 3c91db59f3 | |||
| 72a390b484 | |||
| 978def4cf7 | |||
| 2331942bf9 | |||
| d41bdfebbc | |||
| d0565503e8 | |||
| f0c48e76e1 | |||
| 3639e0d3ce | |||
| 0276911b88 | |||
| af3a400841 | |||
| 5a2a78a7d5 | |||
| 5fc7442359 | |||
| 39cfaf6780 | |||
| 7a11b5eb77 | |||
| faf4600f51 | |||
| 3e383465a9 | |||
| ac279583df | |||
| ef2659a7b9 | |||
| 769d1956b3 | |||
| 8ca455ed24 | |||
| 7bda2bc580 | |||
| d7a290478d | |||
| d73e4f8ec5 | |||
| 6e02d3e533 | |||
| 7b4a70817b | |||
| 87acce71ea | |||
| 95b1c8a01f | |||
| dfd421c3b7 | |||
| 4596f1e0f8 | |||
| a59d06fbbf | |||
| 65286f273b | |||
| 9727e86249 | |||
| db614fe845 | |||
| 85899e3fbe | |||
| 87d6799c10 | |||
| 67f0c213c2 | |||
| c103595bba | |||
| 86a7c2565c | |||
| 21e6819f16 | |||
| 28569506f0 | |||
| dad2ebf693 | |||
| 02a7b804c1 | |||
| 9dbb31dcef | |||
| a932f41cc2 | |||
| 3570a3d2eb | |||
| 85852df176 | |||
| 6d045e9976 | |||
| 4fc00f8a3c | |||
| 9c2bac36b6 | |||
| 8501fc0b6d | |||
| e9e7f306cd | |||
| fa68a5fdff | |||
| 07a39722fb | |||
| 1d4ab0e1fa | |||
| 653651f97f | |||
| b1ba6e729a | |||
| 99685d76a9 | |||
| 711c94001b | |||
| 7f3d66d01d | |||
| 1db75a8ae4 | |||
| 7b9d28ade9 | |||
| 5843fc9212 | |||
| 3542c646f8 | |||
| 8fb5ea2223 | |||
| 958c1e216f | |||
| 100689a20b | |||
| 2da887e677 | |||
| bd213bcd9e | |||
| bf016356a6 | |||
| 76a750a764 | |||
| 000d562ffe | |||
| d02f2385dd | |||
| a5261494cd | |||
| ac47e9d88a | |||
| 7329f7dc65 | |||
| 7c5f146b11 | |||
| e3fbbd4590 | |||
| dc8baa09a3 | |||
| 720ce0ae5b | |||
| a7a667b4bc | |||
| 4e68cf31cf | |||
| d03460d69e | |||
| 50baf6f85b | |||
| 23a04facbe | |||
| 7fbecc8b3d | |||
| 9cb5956f3c | |||
| 366ebb3adb | |||
| 458bd5408f | |||
| c00d165f47 | |||
| d2bb15471a | |||
| c0cd260610 | |||
| 6a9357f598 | |||
| 6909f99773 | |||
| ce5424cc79 | |||
| c125708a4a | |||
| 2dc0ba07d9 | |||
| 2a3231d334 | |||
| d6a436d2eb | |||
| baacd8a9c0 | |||
| e1dddd317d | |||
| b9ae9c4730 | |||
| fab5f9229f | |||
| ff92262a78 | |||
| a4547e12cb | |||
| 1a39b6e4a3 | |||
| 58446ffaed | |||
| 1f09005147 | |||
| 3754b71f4d | |||
| 1d2222b599 | |||
| 89ceab0c4b | |||
| 76dabb8138 | |||
| 8b543488e3 | |||
| c52c345e53 | |||
| 9420717b44 | |||
| 9c1fe72f6b | |||
| 2e892f35ea | |||
| bedd742c93 | |||
| add6856727 | |||
| 4dc6b2af2b | |||
| e65558b8ac | |||
| e4cc5a0950 | |||
| 4b4f8a25a3 | |||
| c77adf385e | |||
| 9b7f4a93d8 | |||
| e780b47160 | |||
| 028fc44dce | |||
| a185c9dc96 | |||
| 52e10997f1 | |||
| f2f750298c | |||
| 1755b00079 | |||
| a844585faf | |||
| b7fe79a5cc | |||
| 2d5492601f | |||
| 0ee12021e2 | |||
| 6218d5506b | |||
| d79b0421c2 | |||
| 0b978593d4 | |||
| 8cb5b8caec | |||
| 2093e79b6a | |||
| 8ec6490627 | |||
| 0a7a9018f3 | |||
| 93cb0ca600 | |||
| 25c2de63a2 | |||
| 2411a3cc2c | |||
| fda233f5ad | |||
| c58cb1a476 | |||
| d2fd9ebaea | |||
| fda6625256 | |||
| 6ddaaa0dfa | |||
| 55c22fa7e2 | |||
| a7b8b0dbc3 | |||
| 7e1686a980 | |||
| 32b231e78a | |||
| 3ad37faedc | |||
| 09c4f98056 | |||
| 1c2870649a | |||
| a2ee4411be | |||
| a23352ef85 | |||
| f9d5bd10b1 | |||
| 0b8ad137b3 | |||
| a746191dba | |||
| 1772d1c05e | |||
| 1d7b527130 | |||
| 430779c943 | |||
| 9c79897e01 | |||
| 07386ed2db | |||
| abb15f6edf | |||
| 87f2f1fe63 | |||
| 249ad9e5c0 | |||
| 9d02d2fbea | |||
| b1598ccb0f | |||
| 08304f9c3e | |||
| 4cc48a355e | |||
| 2c5fddb695 | |||
| 126d8ea870 | |||
| 30a6cb98ec | |||
| 8813d06010 | |||
| 0fbca89030 | |||
| 421ef3c65a | |||
| 5937f8b640 | |||
| 5ec67d0b80 | |||
| 941febaf62 | |||
| a545e9dbc3 | |||
| b0e7dddaf3 | |||
| ed0a783374 | |||
| 97696cc95b | |||
| 4375b73257 | |||
| f97b18d594 | |||
| c7857872ce | |||
| 79105a1bdc | |||
| a4b22190c0 | |||
| a3ce5998ff | |||
| 7f0329dda6 | |||
| 8a40119a98 | |||
| a470cb66a2 | |||
| ecf2c52cfa | |||
| 43788bd153 | |||
| 81c2a8e0fd | |||
| e90a0ce435 | |||
| a727014fcb | |||
| ae28ccf228 | |||
| 281ce2d842 | |||
| f8af9cc259 | |||
| 8af80e6c3a | |||
| 874cdec654 | |||
| 99d5545ed3 | |||
| 423f1acdb6 | |||
| 454e9ac9f1 | |||
| a8e70b3631 | |||
| eb83e9dcc5 | |||
| 1ab849fd0d | |||
| db078538ff | |||
| 1c0d869894 | |||
| f9e6df46c1 | |||
| 5228c078bc | |||
| f5f73ede6b | |||
| 1160f274ee | |||
| 53f5d63f15 | |||
| 1150ec3e43 | |||
| c655cb3fab | |||
| 8e24c204fd | |||
| 7885b3fae2 | |||
| b01d39f19f | |||
| bbf494e9a1 | |||
| eae201cb4b | |||
| 87ef37a689 | |||
| b2c7b65191 | |||
| 908c0749cf | |||
| 47c5e6f155 | |||
| 9deba732b0 | |||
| b82d0d0137 | |||
| f1ad03e913 | |||
| 6cae338c1e | |||
| 7894e31926 | |||
| 25d8c8f9c4 | |||
| 1d9dd76356 | |||
| c2c34705d5 | |||
| 96ed5e445e | |||
| 870dc07fd2 | |||
| ecbb3f6658 | |||
| 34704bd93d | |||
| 201c88a253 | |||
| 6c4da58d9d | |||
| b56fa5cba3 | |||
| 9cedbeb6f6 | |||
| 4c6d107102 | |||
| 7917a2584b | |||
| f99f8a0d7c | |||
| 7db716f51c | |||
| 592e5a088f | |||
| 2d6850950c | |||
| ef6821a0cd | |||
| 6e9535d78f | |||
| 72a45f5984 | |||
| de600fa062 | |||
| 87e3b4f246 | |||
| 7b47d72ef0 | |||
| 7392bdb80d | |||
| af9f1417f1 | |||
| 8fc00fd750 | |||
| 4e5b67abfa | |||
| 84e8ac89fc | |||
| 12d081a1af | |||
| 5e6b402bf5 | |||
| 2bfa6cb038 | |||
| 8ac44d13df | |||
| d99055901f | |||
| 9c9f6e4e80 | |||
| 08e369f1ae | |||
| b0e99ab444 | |||
| c77a48f589 | |||
| 0135426dfe | |||
| 37c745c139 | |||
| 7880039801 | |||
| a56dac1e87 | |||
| 4ae422b47b | |||
| acd54dec7a | |||
| 6242ab0b2e | |||
| c8a96dcce9 | |||
| 4f2dca7ecf | |||
| e569574c01 | |||
| f544734b4d | |||
| 80b439470b | |||
| 539197cef5 | |||
| 1a867be387 | |||
| 3785cd705f | |||
| 5d7a438080 | |||
| 2f12f3e34f | |||
| 52bfa3ad34 | |||
| d1d659b698 | |||
| cdb9337aed | |||
| 8b0af5bb62 | |||
| 83232a935e | |||
| a30a1aa7c7 | |||
| 07cebb6c5b | |||
| 45bf65a3d3 | |||
| 60f5a99b0c | |||
| 526c12127f | |||
| 1daac842f2 | |||
| 3e4a6f0f2e | |||
| c1e937049e | |||
| 8f8d5c7384 | |||
| aad93d8913 | |||
| 027c7a1480 | |||
| df2efa3329 | |||
| 9d754c8819 | |||
| e11ceed28c | |||
| 8b8b6e4afa | |||
| ed2a22b573 | |||
| 41851ee925 | |||
| ffc61942a9 | |||
| 2e4382bff6 | |||
| f542dbb61e | |||
| 2869d139a3 | |||
| 3ec930491f | |||
| a0005064ac | |||
| 33fae9d69a | |||
| 913e5fee7b | |||
| 3992073303 | |||
| 797a99f632 | |||
| e891ed8a64 | |||
| e8c2cf809b | |||
| ec761893c7 | |||
| 5df7efda03 | |||
| 2fff73c075 | |||
| d37cbd263e | |||
| 3ec1242a9a | |||
| 881f7f8e30 | |||
| 12c2f31af9 | |||
| 49c0c029c3 | |||
| da7a34fc55 | |||
| 322ff740e2 | |||
| b1575e57df | |||
| 82b219cdba | |||
| e317de7562 | |||
| 9ecaa7c3b3 | |||
| 037b0db01b | |||
| 1e80d76acd | |||
| 360c55fdb9 | |||
| 176d65e0b2 | |||
| 0c08f5a03c | |||
| e257602b78 | |||
| 8f00f3a80c | |||
| 63d24c001f | |||
| 67668fe1fc | |||
| dbbb41e12f | |||
| 45053ba507 | |||
| d9f5f7a137 | |||
| b301ce5d27 | |||
| f02bd3b3f8 | |||
| e83eaa38e1 | |||
| 102b5f3ca1 | |||
| 63958a6717 | |||
| 4b8ac32ca9 | |||
| 00b29a1890 | |||
| c7e5f93bc4 | |||
| e4d87c91a2 | |||
| f84091e54e | |||
| e03e8825b2 | |||
| 355f8543f5 | |||
| 77e7db79cc | |||
| 2809d0d832 | |||
| 5b908ec355 | |||
| c480fca72a | |||
| c19f66a6a4 | |||
| 1e6fd1af09 | |||
| 55db2c9961 | |||
| 6ec8b0c3c5 | |||
| 78557a4536 | |||
| 8090cd7985 | |||
| 5ce34bed92 | |||
| 035841f221 | |||
| ef61a687d6 | |||
| fcb1e5e593 | |||
| 752863a0d3 | |||
| 1a3e330eb4 | |||
| 5f9da6b40a | |||
| aee62c29fe | |||
| fe76ef9cdc | |||
| acbaa838ec | |||
| f4134d83b5 | |||
| 24cc10e1cd | |||
| 20b79b40f2 | |||
| 72e2776b7e | |||
| 49e4c92da6 | |||
| ba761e4951 | |||
| c1aa1d8eae | |||
| 4231c3ccd8 | |||
| 65223ceaaf | |||
| ca22615c08 | |||
| 559a9f38c0 | |||
| 762829e5ff | |||
| 835f79b113 | |||
| 3bd8added4 | |||
| e0854bc68c | |||
| cec7014856 | |||
| ba527a1979 | |||
| b73e4c89db | |||
| f1a9da83fc | |||
| 6e903bfbed | |||
| 23d62f552e | |||
| 53f6a6b8c5 | |||
| 945acb6071 | |||
| af81bf5891 | |||
| e72ac1def8 | |||
| f163dfdd11 | |||
| a1eda8d91d | |||
| 5ed02a1fe1 | |||
| 66d4a4b958 | |||
| adbb9ffe3b | |||
| 0cd2867ac0 | |||
| 4d29354797 | |||
| a6d9c66fc9 | |||
| 99588c7ff8 | |||
| 68c3e3e999 | |||
| 67b5650288 | |||
| 3085125f3e | |||
| 5e26ad7c36 | |||
| 765ea06c3b | |||
| ffe70a8313 | |||
| 331e0fc6ab | |||
| b3fd735f5c | |||
| 3cbbdb070f | |||
| d2e55bf964 | |||
| 05cdadac79 | |||
| e6aed88188 | |||
| d032314ddb | |||
| 6301fd2fb7 | |||
| 34c3899ec4 | |||
| 46f5fc5172 | |||
| 81c021b59a | |||
| 8ab57e4e61 | |||
| 14a7305a2d | |||
| 13ff179840 | |||
| e7060d4b6f | |||
| 641039720e | |||
| 6f132f1e38 | |||
| 76c0d34760 | |||
| 71637d7286 | |||
| 1cac7cc189 | |||
| c42942430f | |||
| 7abb36c362 | |||
| a1e8d29b9c | |||
| c449da2916 | |||
| 3fd0c0a2dd | |||
| 397f7d003a | |||
| f85d3bf5fe | |||
| d22d758757 | |||
| acbde5aad8 | |||
| 8f93956ff1 | |||
| 4fe4ac1079 | |||
| 8a2f0a9f45 | |||
| 5cc865f0af | |||
| 189ba93e64 | |||
| 89464e033e | |||
| 6d7c36e31f | |||
| ca8ea03870 | |||
| 3f57adb9cf | |||
| 2686dac62e | |||
| ffd40fef6d | |||
| e1b8e19966 | |||
| dec742cf54 | |||
| 6f1b0b06c3 | |||
| 408d70c420 | |||
| e51c966969 | |||
| 08eacacfd4 | |||
| 87150b3c72 | |||
| e385ac5c09 | |||
| c30629542b | |||
| 3f9fa24c69 | |||
| 78573fa837 | |||
| d4837cacda | |||
| 0367805f0f | |||
| e0750f5341 | |||
| 80528c9c42 | |||
| e26f2af93d | |||
| be633c0e60 | |||
| 97f1ccb6c1 | |||
| 68a243725b | |||
| 8f81060a18 | |||
| 07c76f12e1 | |||
| c6ab96a86f | |||
| 059c0de1fa | |||
| 8e147bd1bd | |||
| 4d3cfe71f7 | |||
| 5adb7c3762 | |||
| 937b309b07 | |||
| 48d58ea660 | |||
| aaf2e6a3f0 | |||
| 422dcd4501 | |||
| 951a726309 | |||
| b692ddcbe6 | |||
| 5d6b356602 | |||
| 08057edb28 | |||
| 390aabc78f | |||
| a2123df0c5 | |||
| 6a6ed3ed44 | |||
| b87008f590 | |||
| ecc0e722b5 | |||
| 6f9a8e5581 | |||
| b17bba3629 | |||
| cf4d7e03f5 | |||
| 73eeeb0ef9 | |||
| 0f79353936 | |||
| 38b44fa92b | |||
| 3fd13ddc0a | |||
| 2e375978bd | |||
| 45db731a60 | |||
| 95e353c14e | |||
| 8a3a41294a | |||
| f41858e2c7 | |||
| aaa3dc93f1 | |||
| e5eab0a6c8 | |||
| 7c39368126 | |||
| ade926e2f2 | |||
| c63c8e4d73 | |||
| 3c418b2aa4 | |||
| d34a158c34 | |||
| 6e3d037021 | |||
| 7d160eaf54 | |||
| 4fc99edbd6 | |||
| 4da8c1645e | |||
| 1c01aab1e7 | |||
| 6a85b0800f | |||
| 1acb0fbac4 | |||
| 2b294702a9 | |||
| afd97d6e05 | |||
| 2d5a7a8a49 | |||
| 9e3036e134 | |||
| 8dec458ba9 | |||
| da07d885da | |||
| a9de005e1a | |||
| e03e121da4 | |||
| fc3ca70e5d | |||
| 9c7144c918 | |||
| ed8d70c3b3 | |||
| fa501460cb | |||
| 110143ae6e | |||
| eda3d76077 | |||
| 51f090324a | |||
| 335540ff4a | |||
| 5fa47ff65b | |||
| 3a1c1a5e43 | |||
| 4c7f530458 | |||
| 2e914a7704 | |||
| 8fbb9d6d4e | |||
| 23bc643c91 | |||
| c4fe116267 | |||
| 081d7e3dcb | |||
| 764ae37ce4 | |||
| e62a609b6b | |||
| 4b1a4aae87 | |||
| ccda5f2a2e | |||
| 3fd8b1b855 | |||
| 94d1982670 | |||
| 39efafc75f | |||
| afe0c9f1a6 | |||
| 04b105d74b | |||
| 1ab6351d48 | |||
| 9c348df5a5 | |||
| 256ec046fc | |||
| 85d4d63287 | |||
| 213d36ed56 | |||
| ff6bfc9516 | |||
| ae5678482b | |||
| 63514cfafb | |||
| 03ca60f2e1 | |||
| 89ef6e5277 | |||
| 47880d09bc | |||
| 5472765b95 | |||
| 14d975dce4 | |||
| a23bdd0c53 | |||
| b0d8c3db0a | |||
| 4a75c062ce | |||
| fe90a3bf13 | |||
| 711f6f4c45 | |||
| 8fa7829a3c | |||
| 6d7697a0eb | |||
| cdd80af27b | |||
| 2cb09b81f8 | |||
| b529d621e9 | |||
| 24667991f1 | |||
| ac3f3bfd55 | |||
| f2c4929656 | |||
| 4c038a823a | |||
| 3080c1bf27 | |||
| 59809c39ea | |||
| 9b063190ce | |||
| 49f2398669 | |||
| d7a4d20ceb | |||
| 0855e268c8 | |||
| 49b8d75aea | |||
| b1a9a7845b | |||
| b1e749bafb | |||
| f6fc6c0693 | |||
| ad1371bce0 | |||
| e77df0a5dd | |||
| 6935bd514e | |||
| a4cc5f222c | |||
| a0bb7b85b4 | |||
| fd8ed92f88 | |||
| c7b6261602 | |||
| 3aefb14faf | |||
| 8521b68855 | |||
| e37b244f45 | |||
| 7f40fb6283 | |||
| 83316f7a17 | |||
| a56e55ae70 | |||
| 07234ee33a | |||
| 145d59df45 | |||
| 7e8c3af9ce | |||
| aa6ef72670 | |||
| ed8781b382 | |||
| 7a5f4e8f8c | |||
| d992422efd | |||
| f0d6312fa5 | |||
| 772ece25a0 | |||
| d063a41ad4 | |||
| 6d02f05f05 | |||
| 3a72409fb8 | |||
| b3a89279d6 | |||
| 4432ba26e5 | |||
| fe824a8f71 | |||
| 8233b75e05 | |||
| 3af15b2b95 | |||
| fd092bb7e1 | |||
| 9c60521d00 | |||
| a4dfcca302 | |||
| 04ce6f6103 | |||
| 178af141f0 | |||
| c5c2d4375d | |||
| 3aa183042f | |||
| 944755fde1 | |||
| cd78c35bf6 | |||
| f8d003002b | |||
| 56fab768f3 | |||
| c92744f21c | |||
| 1f71fd1967 | |||
| 33ae7beeb4 | |||
| 130efa3a51 | |||
| 7df55fc1a3 | |||
| 3adadcc1e1 | |||
| 5b027a29d8 | |||
| 2a9d0fb82c | |||
| ee6a543f8d | |||
| d927531354 | |||
| 1d47df09eb | |||
| a9709c4f84 | |||
| a06806c42d | |||
| 57de058582 | |||
| 978493853d | |||
| a5ca9218da | |||
| 5fe330bfbb | |||
| c80cff87f7 | |||
| 9a864cc2bd | |||
| c07f13180f | |||
| f672eafd6d | |||
| 6a361c0d4b | |||
| b597f010dc | |||
| a670f677e9 | |||
| a77423b937 | |||
| 50083a9297 | |||
| d5aa430d4a | |||
| e7f9954970 | |||
| ec57851de2 | |||
| 0eb767b615 | |||
| 45b77c05d4 | |||
| b9900925b8 | |||
| d94290c58d | |||
| f50aabff7b | |||
| d07c660eb9 | |||
| 6491efec4c | |||
| 25038da2a1 | |||
| 85f461c88c | |||
| 1cf55f3e96 | |||
| 9a995ddcc1 | |||
| 0b2ef3de00 | |||
| 99e50aa7ac | |||
| a898290c0b | |||
| 6cbff1097b | |||
| 246083460e | |||
| 1d28334346 | |||
| 3e97f016d5 | |||
| ab0d4634d6 | |||
| 1e1e482b79 | |||
| a0624cfef6 | |||
| 02a35b4c02 | |||
| 80409c08a6 | |||
| 6149f7be60 | |||
| 474f0ee427 | |||
| f3ee618900 | |||
| b939086e10 | |||
| ffdfc24e65 | |||
| 07bbd7e06d | |||
| 765f27f987 | |||
| d204bee55d | |||
| 0f4ad48de0 | |||
| fe480d5cb6 | |||
| 8a04a0e5b7 | |||
| 705a0a2eaf | |||
| d1289b0de1 | |||
| e1de5e0989 | |||
| f5b9160304 | |||
| 0a22812165 | |||
| 104fd86179 | |||
| 67a702ffcd | |||
| 65c0e6f77d | |||
| 245bd184b4 | |||
| 8c8f253233 | |||
| 8b8422de3a | |||
| 7bf2d4c40e | |||
| 2a67f04d15 | |||
| b2ea8196d5 | |||
| 7f38191d03 | |||
| f6b7a3042f | |||
| 3bdc6ce5cc | |||
| bc31987f96 | |||
| eaff4c5aac | |||
| 95bafdc0d1 | |||
| c776d22941 | |||
| 749c6b7fef | |||
| 912c081606 | |||
| c73bf7dd3e | |||
| a3ae015be8 | |||
| 1f8f1f0f86 | |||
| 5eebab2748 | |||
| 8cbd1cc4aa | |||
| 00c1a89f58 | |||
| d51825ffb7 | |||
| 42569e6ad7 | |||
| ef874b4245 | |||
| 46037b5aea | |||
| 6874f288ee | |||
| b6fd90e9d7 | |||
| f078c34bf9 | |||
| c1e4a6156c | |||
| 71e14259f6 | |||
| 403dc480c4 | |||
| 3dc1a7f9c0 | |||
| 672194adb4 | |||
| fdb84b6675 | |||
| ba352183bf | |||
| dd480080c9 | |||
| 4047978ea2 | |||
| a649b11bbf | |||
| 247560ee43 | |||
| 50e7ece477 | |||
| b5129c5c80 | |||
| 6a27dedd74 | |||
| edc306da22 | |||
| 15ded667fe | |||
| 34872a822b | |||
| 9817fc9093 | |||
| dde4b2fcba | |||
| 623600a8a9 | |||
| bff4986242 | |||
| c689530068 | |||
| 7022be1979 | |||
| 971a3920a3 | |||
| b252797e9b | |||
| c758afe3e3 | |||
| f8d3f53158 | |||
| 4a8672a20b | |||
| 32109e8565 | |||
| 5d6060c690 | |||
| a95d030817 | |||
| ffa84c6e87 | |||
| 4a6978ff79 | |||
| 1497858cc0 | |||
| 2c125d4ce0 | |||
| 8bf5455f42 | |||
| 48f7e41905 | |||
| b50ac62a4b | |||
| 6fc55bb82c | |||
| 65ac864bf2 | |||
| cf6bbdd1f1 | |||
| bbe7cbee8f | |||
| 55ba343426 | |||
| e6ace5fb2f | |||
| 550ab59231 | |||
| 213303d674 | |||
| 74c81eb7b3 | |||
| c33586e7f5 | |||
| 5dd5e50738 | |||
| 304f30ece6 | |||
| e8b1720fee | |||
| e0cea098c1 | |||
| 50ab481b18 | |||
| d39aacba83 | |||
| 798fe4da0e | |||
| e19f110f19 | |||
| 59a4a74a97 | |||
| 590ef557d3 | |||
| 7bc5c8f8a4 | |||
| bf8244ce49 | |||
| a860cc0ec1 | |||
| bb8c681270 | |||
| 9872095e50 | |||
| 403aaa76ae | |||
| 8578bc4a5b | |||
| ed004d1423 | |||
| a91b2b4f7e | |||
| 24f9607e5a | |||
| 2384013139 | |||
| 42ffd1b1f8 | |||
| a2d06909c9 | |||
| c8b7b95df8 | |||
| 9e22eac4d8 | |||
| a41f2e4bde | |||
| 039e61def6 | |||
| 54d329c5cf | |||
| f46a98fad9 | |||
| 6a6816dda1 | |||
| f6ea2953e9 | |||
| 7591a79323 | |||
| c12760b70a | |||
| a526b0d65a | |||
| ef098e63d7 | |||
| 29a0b23670 | |||
| 44b5b0b771 | |||
| 00f9cc4698 | |||
| 3d4d5a3dab | |||
| ed577fad6a | |||
| ae61260665 | |||
| 0e4bb0f942 | |||
| ee34e5028f | |||
| 3b8dd11758 | |||
| cc04e2dd6d | |||
| b88cf6a101 | |||
| d10985f890 | |||
| c08504293b | |||
| 4bc49da10d | |||
| 683f4ba94e | |||
| 8fd06c84ac | |||
| 23b163e6c1 | |||
| 6383b9a6f8 | |||
| 0b3ebf229f | |||
| d6fe155d4d | |||
| 8829728f19 | |||
| a6c9156ae5 | |||
| 121f996a43 | |||
| 44d4f25413 | |||
| 16abfcffda | |||
| e7ba9d962d | |||
| b7631c0b46 | |||
| 0039703f03 | |||
| d439c9673f | |||
| f255b0ea6d | |||
| 9d01ed8eb3 | |||
| 94e4e853c3 | |||
| ffa39e6a9c | |||
| 9e4ae60885 | |||
| 2cd4366d07 | |||
| 11c5e1e9ba | |||
| 550812d8e1 | |||
| 9ca5f5e7fd |
@@ -0,0 +1,17 @@
|
||||
engines:
|
||||
pmd:
|
||||
enabled: true
|
||||
channel: "beta"
|
||||
|
||||
fixme:
|
||||
enabled: true
|
||||
config:
|
||||
strings:
|
||||
- FIXME
|
||||
- TODO
|
||||
- BUG
|
||||
- FIX
|
||||
|
||||
ratings:
|
||||
paths:
|
||||
- "**.java"
|
||||
@@ -84,6 +84,9 @@ nbactions.xml
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
# get rid of these annoying files
|
||||
.idea/
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/dictionaries
|
||||
@@ -177,3 +180,7 @@ local.properties
|
||||
.recommenders/
|
||||
|
||||
Makefile
|
||||
|
||||
infer-out/
|
||||
infer.txt
|
||||
log.log
|
||||
@@ -17,3 +17,11 @@
|
||||
# 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 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@gmail.com>
|
||||
Calum <cir27@uclive.ac.nz> cir27 <cir27@uclive.ac.nz>
|
||||
@@ -1,2 +1,16 @@
|
||||
# Design Decisions
|
||||
- Code structure
|
||||
|
||||
App creates a race instance which can:
|
||||
instantiate a file parser to extract race setting and team information;
|
||||
creates and passes legs and teams/boats into event generator to create events;
|
||||
runs a race and iterates all events that returned from the generator;
|
||||
prints out event details, including time, involved boats and legs.
|
||||
|
||||
- Configuration file
|
||||
|
||||
We decided to store the team information including team names and boat currentVelocity, as well as race configuration setting in external file.
|
||||
|
||||
To read external files, "Json-simple" library has been used to parse information.
|
||||
By using this library, we did not have to write our json parser and benefited from the flexibility of json files.
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"race-name": "AC35",
|
||||
"time-scale": 2.0,
|
||||
"race-size": 6,
|
||||
"teams": [
|
||||
{
|
||||
"team-name": "Oracle Team USA",
|
||||
"velocity": 20.9
|
||||
},
|
||||
{
|
||||
"team-name": "Artemis Racing",
|
||||
"velocity": 18.3
|
||||
},
|
||||
{
|
||||
"team-name": "Emirates Team New Zealand",
|
||||
"velocity": 21.5
|
||||
},
|
||||
{
|
||||
"team-name": "Groupama Team France",
|
||||
"velocity": 19.9
|
||||
},
|
||||
{
|
||||
"team-name": "Land Rover BAR",
|
||||
"velocity": 17.6
|
||||
},
|
||||
{
|
||||
"team-name": "SoftBank Team Japan",
|
||||
"velocity": 16.6
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@ You can specify a config file using the using the -f flag, for example 'java -ja
|
||||
|
||||
## The config file
|
||||
|
||||
The teams/boats are specified in the config file under 'teams', each team requires a team name, and a velocity (in meters per second).
|
||||
The teams/boats are specified in the config file under 'teams', each team requires a team name, and a currentVelocity (in meters per second).
|
||||
|
||||
The 'time-scale' option lets you change how long the race takes to complete. A time-scale of 1.0 is normal speed, 2.0 is 2x etc.
|
||||
|
||||
|
||||
@@ -20,11 +20,56 @@
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>1.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.json-simple</groupId>
|
||||
<artifactId>json-simple</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>2.7.13</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
|
||||
<dependency>
|
||||
<groupId>info.cukes</groupId>
|
||||
<artifactId>cucumber-junit</artifactId>
|
||||
<version>1.2.5</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/info.cukes/cucumber-java -->
|
||||
<dependency>
|
||||
<groupId>info.cukes</groupId>
|
||||
<artifactId>cucumber-java</artifactId>
|
||||
<version>1.2.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
<version>2.3.26-incubating</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl -->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.1.7</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-cli</groupId>
|
||||
<artifactId>commons-cli</artifactId>
|
||||
<version>1.4</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,111 +1,102 @@
|
||||
package seng302;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.io.FileNotFoundException;
|
||||
import ch.qos.logback.classic.Level;
|
||||
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 org.apache.commons.cli.CommandLine;
|
||||
import org.apache.commons.cli.CommandLineParser;
|
||||
import org.apache.commons.cli.DefaultParser;
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import seng302.model.PolarTable;
|
||||
|
||||
public class App {
|
||||
public class App extends Application {
|
||||
|
||||
/**
|
||||
* Builds a race object for the AC35 course
|
||||
*
|
||||
* @return a Race object for the AC35 course
|
||||
*/
|
||||
public static Race createRace(String configFile) throws Exception {
|
||||
Race race = new Race();
|
||||
FileParser fp;
|
||||
private static Logger logger = LoggerFactory.getLogger(App.class);
|
||||
|
||||
// Read team names from file
|
||||
try{
|
||||
fp = new FileParser(configFile);
|
||||
}
|
||||
catch (FileNotFoundException e){
|
||||
System.out.println("Config file does not exist");
|
||||
return null;
|
||||
public static void parseArgs(String[] args) throws ParseException {
|
||||
Options options = new Options();
|
||||
CommandLineParser parser = new DefaultParser();
|
||||
CommandLine cmd;
|
||||
|
||||
ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory
|
||||
.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
|
||||
options.addOption("debugLevel", true, "Set the application debug level");
|
||||
|
||||
cmd = parser.parse(options, args);
|
||||
|
||||
if (cmd.hasOption("debugLevel")) {
|
||||
|
||||
switch (cmd.getOptionValue("debugLevel")) {
|
||||
case "DEBUG":
|
||||
rootLogger.setLevel(Level.DEBUG);
|
||||
break;
|
||||
|
||||
case "ALL":
|
||||
rootLogger.setLevel(Level.ALL);
|
||||
break;
|
||||
|
||||
case "WARNING":
|
||||
rootLogger.setLevel(Level.WARN);
|
||||
break;
|
||||
|
||||
case "ERROR":
|
||||
rootLogger.setLevel(Level.ERROR);
|
||||
break;
|
||||
|
||||
case "INFO":
|
||||
rootLogger.setLevel(Level.INFO);
|
||||
|
||||
case "TRACE":
|
||||
rootLogger.setLevel(Level.TRACE);
|
||||
|
||||
default:
|
||||
rootLogger.setLevel(Level.ALL);
|
||||
}
|
||||
} else {
|
||||
rootLogger.setLevel(Level.WARN);
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<String> boatNames = new ArrayList<>();
|
||||
ArrayList<Map<String, Object>> teams = fp.getTeams();
|
||||
@Override
|
||||
public void start(Stage primaryStage) throws Exception {
|
||||
Parent root = FXMLLoader.load(getClass().getResource("/views/StartScreenView.fxml"));
|
||||
primaryStage.setTitle("Party Parrots at Sea");
|
||||
Scene scene = new Scene(root, 1530, 960);
|
||||
scene.getStylesheets().add(getClass().getResource("/css/master.css").toString());
|
||||
primaryStage.setScene(scene);
|
||||
// primaryStage.setMaxWidth(1530);
|
||||
// primaryStage.setMaxHeight(960);
|
||||
primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("/PP.png")));
|
||||
// primaryStage.setMaximized(true);
|
||||
|
||||
//get race size
|
||||
int numberOfBoats = (int) fp.getRaceSize();
|
||||
primaryStage.show();
|
||||
|
||||
//get time scale
|
||||
double timeScale = fp.getTimeScale();
|
||||
race.setTimeScale(timeScale);
|
||||
primaryStage.setOnCloseRequest(e -> {
|
||||
// ClientPacketParser.appClose();
|
||||
// ClientPacketParser.appClose();
|
||||
System.exit(0);
|
||||
});
|
||||
|
||||
for (Map<String, Object> team : teams) {
|
||||
boatNames.add((String) team.get("team-name"));
|
||||
}
|
||||
|
||||
// Shuffle team names
|
||||
long seed = System.nanoTime();
|
||||
Collections.shuffle(boatNames, new Random(seed));
|
||||
|
||||
if (numberOfBoats > Array.getLength(boatNames.toArray())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add boats to the race
|
||||
for (int i = 0; i < numberOfBoats; i++) {
|
||||
race.addBoat(new Boat(boatNames.get(i), (Double) (teams.get(i).get("velocity"))));
|
||||
}
|
||||
|
||||
race.addLeg(new Leg(35, 100, "Start"));
|
||||
race.addLeg(new Leg(10, 300, "Marker 1"));
|
||||
race.addLeg(new Leg(350, 400, "Leeward Gate"));
|
||||
race.addLeg(new Leg(10, 400, "Windward Gate"));
|
||||
|
||||
Leg finishingLeg = new Leg(10, 400, "Leeward Gate");
|
||||
finishingLeg.setFinishingLeg(true);
|
||||
|
||||
race.addLeg(finishingLeg);
|
||||
|
||||
return race;
|
||||
// ClientState.primaryStage = primaryStage;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Race race = null;
|
||||
String raceConfigFile;
|
||||
|
||||
if (args.length == 2 && args[0].equals("-f")){
|
||||
raceConfigFile = args[1];
|
||||
}
|
||||
else{
|
||||
// Use default config
|
||||
raceConfigFile = "doc/examples/config.json";
|
||||
}
|
||||
|
||||
try {
|
||||
race = createRace(raceConfigFile);
|
||||
} catch (Exception e) {
|
||||
System.out.println("There was an error creating the race.");
|
||||
parseArgs(args);
|
||||
} catch (ParseException e) {
|
||||
logger.error("Could not parse command line arguments");
|
||||
}
|
||||
|
||||
// If race was created
|
||||
if (race != null) {
|
||||
race.displayStartingBoats();
|
||||
|
||||
System.out.println("\n\n");
|
||||
System.out.println("######################");
|
||||
System.out.println("# Live Race Updates ");
|
||||
System.out.println("######################");
|
||||
|
||||
race.startRace();
|
||||
|
||||
System.out.println("\n\n");
|
||||
System.out.println("######################");
|
||||
System.out.println("# Race Results ");
|
||||
System.out.println("######################");
|
||||
|
||||
race.showRaceMarkerResults();
|
||||
race.displayFinishingOrder();
|
||||
|
||||
} else {
|
||||
System.out.println("There was an error creating the race. Exiting.");
|
||||
}
|
||||
launch(args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package seng302;
|
||||
|
||||
/**
|
||||
* Represents a boat in the race.
|
||||
*/
|
||||
public class Boat {
|
||||
|
||||
private String teamName; // The name of the team, this is also the name of the boat
|
||||
private double velocity; // In meters/second
|
||||
|
||||
public Boat(String teamName) {
|
||||
this.teamName = teamName;
|
||||
this.velocity = 10; // Default velocity
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a boat in the race.
|
||||
*
|
||||
* @param teamName The name of the team sailing the boat
|
||||
* @param boatVelocity The speed of the boat in meters/second
|
||||
*/
|
||||
public Boat(String teamName, double boatVelocity) {
|
||||
this.teamName = teamName;
|
||||
this.velocity = boatVelocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the team sailing the boat
|
||||
*
|
||||
* @return The name of the team
|
||||
*/
|
||||
public String getTeamName() {
|
||||
return this.teamName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the team sailing the boat
|
||||
*
|
||||
* @param teamName The name of the team
|
||||
*/
|
||||
public void setTeamName(String teamName) {
|
||||
this.teamName = teamName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets velocity of the boat
|
||||
*
|
||||
* @return a float number of the boat velocity
|
||||
*/
|
||||
public double getVelocity() {
|
||||
return this.velocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets velocity of the boat
|
||||
*
|
||||
* @param velocity The velocity of boat
|
||||
*/
|
||||
public void setVelocity(double velocity) {
|
||||
this.velocity = velocity;
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package seng302;
|
||||
|
||||
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 long time; // Time the event occurs
|
||||
private Boat boat;
|
||||
private Leg leg; // Leg of the race the event occurs on
|
||||
private boolean isFinishingEvent = false; // This event occurs when a boat finishes the race
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param eventLeg, the leg the event happens on
|
||||
*/
|
||||
public Event(long eventTime, Boat eventBoat, Leg eventLeg) {
|
||||
this.time = eventTime;
|
||||
this.boat = eventBoat;
|
||||
this.leg = eventLeg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param eventLeg, the leg the event happens on
|
||||
* @param isFinishingEvent, true if this event is the boat crossing the finishing line
|
||||
*/
|
||||
public Event(long eventTime, Boat eventBoat, Leg eventLeg, boolean isFinishingEvent) {
|
||||
this.time = eventTime;
|
||||
this.boat = eventBoat;
|
||||
this.leg = eventLeg;
|
||||
this.isFinishingEvent = isFinishingEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time for the event
|
||||
*
|
||||
* @return the time for event in millisecond
|
||||
*/
|
||||
public long getTime() {
|
||||
return this.time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time for the event
|
||||
*
|
||||
* @param eventTime the time for event in millisecond
|
||||
*/
|
||||
public void setTime(long 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the involved boat
|
||||
*
|
||||
* @return the boat involved in the event
|
||||
*/
|
||||
public Boat getBoat() {
|
||||
return this.boat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the involved boat
|
||||
*
|
||||
* @param eventBoat the involved boat
|
||||
*/
|
||||
public void setBoat(Boat eventBoat) {
|
||||
this.boat = eventBoat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the involved location/leg
|
||||
*
|
||||
* @return the leg involved in the event
|
||||
*/
|
||||
public Leg getLeg() {
|
||||
return this.leg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the involved location/leg
|
||||
*
|
||||
* @param eventLeg the involved leg
|
||||
*/
|
||||
public void setLeg(Leg eventLeg) {
|
||||
this.leg = eventLeg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the boat in this event passes
|
||||
* the marker.
|
||||
*/
|
||||
public void boatPassedMarker() {
|
||||
this.leg.addBoatToMarker(boat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this event is the boat finishing the race
|
||||
*/
|
||||
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() {
|
||||
String currentHeading = Integer.toString(this.getLeg().getHeading());
|
||||
|
||||
// This event is a boat finishing the race
|
||||
if (this.isFinishingEvent) {
|
||||
return (this.getTimeString() + ", " + this.getBoat().getTeamName() + " finished the race");
|
||||
}
|
||||
|
||||
return (this.getTimeString() + ", " + this.getBoat().getTeamName() + " passed " + this.getLeg().getMarkerLabel() + " going heading " + currentHeading + "°");
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package seng302;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* Read team name from a given Json file. So that user can extract information
|
||||
* efficiently from external files.
|
||||
*/
|
||||
|
||||
public class FileParser {
|
||||
|
||||
private String filePath;
|
||||
private JSONObject content;
|
||||
|
||||
/**
|
||||
* used to construct an instance of file parser
|
||||
*
|
||||
* @param filePath a string like path to show location of desired file to
|
||||
* be parsed
|
||||
*/
|
||||
public FileParser(String filePath) throws Exception {
|
||||
this.filePath = filePath;
|
||||
this.readFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads content from a given file, and return the content as JSONObject.
|
||||
* Throws FileNotFoundException, if the given file cannot be found.
|
||||
*/
|
||||
private void readFile() throws FileNotFoundException {
|
||||
JSONParser parser = new JSONParser();
|
||||
try {
|
||||
this.content = (JSONObject) parser.parse(new FileReader(filePath));
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets time scale setting parameter.
|
||||
*
|
||||
* @return long time scale. -1 if parameter is invalid (eg. scale is
|
||||
* negative number, or containing non numeric character) or cannot be found.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public double getTimeScale() {
|
||||
try {
|
||||
double timeScale = (double) this.content.get("time-scale");
|
||||
return timeScale >= 0 ? timeScale : -1;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets race name in the setting file.
|
||||
*
|
||||
* @return a string of race name. null if race name is invalid or cannot
|
||||
* be found.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public String getRaceName() {
|
||||
try {
|
||||
return (String) this.content.get("race-name");
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of teams who participate the race.
|
||||
*
|
||||
* @return an ArrayList containing strings of team names. null if teams
|
||||
* setting is invalid or there is no team.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ArrayList<Map<String, Object>> getTeams() {
|
||||
try {
|
||||
return (ArrayList<Map<String, Object>>) this.content.get("teams");
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total number of teams.
|
||||
*
|
||||
* @return the number of teams. 0 if no teams or anything goes wrong.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public long getTotalNumberOfTeams() {
|
||||
ArrayList<Map<String, Object>> teams = getTeams();
|
||||
try {
|
||||
return teams.size();
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of boats that would compete during a race. Returns the
|
||||
* total number of race size if parameter is invalid or cannot be found.
|
||||
*
|
||||
* @return an int of the race size.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public long getRaceSize() {
|
||||
long totalTeams = this.getTotalNumberOfTeams();
|
||||
try {
|
||||
long raceSize = (long) this.content.get("race-size");
|
||||
return raceSize >= 0 && raceSize <= totalTeams ? raceSize : totalTeams;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return totalTeams;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package seng302;
|
||||
|
||||
/**
|
||||
* Represents the leg of a race.
|
||||
*/
|
||||
public class Leg {
|
||||
private int heading;
|
||||
private int distance;
|
||||
private boolean isFinishingLeg;
|
||||
private Marker startingMarker;
|
||||
|
||||
/**
|
||||
* Create a new leg
|
||||
*
|
||||
* @param heading, the magnetic heading of this leg
|
||||
* @param distance, the total distance of this leg in meters
|
||||
* @param marker, the marker this leg starts on
|
||||
*/
|
||||
public Leg(int heading, int distance, Marker marker) {
|
||||
this.heading = heading;
|
||||
this.distance = distance;
|
||||
this.startingMarker = marker;
|
||||
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.startingMarker = new Marker(markerName);
|
||||
this.isFinishingLeg = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the heading of this leg
|
||||
*/
|
||||
public int getHeading() {
|
||||
return this.heading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the heading for this leg
|
||||
*/
|
||||
public void setHeading(int heading) {
|
||||
this.heading = heading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total distance of this leg in meters
|
||||
*/
|
||||
public int getDistance() {
|
||||
return this.distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the distance of this leg in meters
|
||||
*/
|
||||
public void setDistance(int distance) {
|
||||
this.distance = distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the marker this leg started on
|
||||
*/
|
||||
public Marker getMarker() {
|
||||
return this.startingMarker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the marker this leg starts on
|
||||
*/
|
||||
public void setMarker(Marker marker) {
|
||||
this.startingMarker = marker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the marker this leg started on
|
||||
*/
|
||||
public String getMarkerLabel() {
|
||||
return this.startingMarker.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the marker that the boat has passed it
|
||||
*/
|
||||
public void addBoatToMarker(Boat boat) {
|
||||
this.startingMarker.addBoat(boat);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package seng302;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Represents the marker at the beginning of a leg
|
||||
*/
|
||||
class Marker{
|
||||
private String name;
|
||||
private ArrayList<Boat> boatOrder;
|
||||
|
||||
/**
|
||||
* Represents the marker at the beginning of a leg
|
||||
*
|
||||
* @param name, the name of the marker
|
||||
*/
|
||||
public Marker(String name){
|
||||
this.name = name;
|
||||
this.boatOrder = new ArrayList<Boat>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of the marker
|
||||
*
|
||||
* @param name, the name of the marker
|
||||
*/
|
||||
public void setName(String name){
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the marker
|
||||
*
|
||||
* @return the name of the marker
|
||||
*/
|
||||
public String getName(){
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a boat as they pass the marker
|
||||
*
|
||||
* @param boat, the boat that passed the marker
|
||||
*/
|
||||
public void addBoat(Boat boat){
|
||||
this.boatOrder.add(boat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of boats in the order they passed the marker
|
||||
*
|
||||
* @return An array of boats in the order they passed the marker
|
||||
*/
|
||||
public Boat[] getBoats(){
|
||||
return this.boatOrder.toArray(new Boat[this.boatOrder.size()]);
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
package seng302;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Race class containing the boats and legs in the race
|
||||
*/
|
||||
public class Race {
|
||||
private ArrayList<Boat> boats; // The boats in the race
|
||||
private ArrayList<Leg> legs; // The legs in the race
|
||||
private ArrayList<Boat> finishingOrder; // The order in which the boats finish the race
|
||||
private PriorityQueue<Event> events; // The events that occur in the race
|
||||
private int numberOfBoats = 0;
|
||||
private long startTime = 0;
|
||||
private double timeScale = 1;
|
||||
|
||||
/**
|
||||
* Race class containing the boats and legs in the race
|
||||
*/
|
||||
public Race() {
|
||||
this.boats = new ArrayList<Boat>();
|
||||
this.legs = new ArrayList<Leg>();
|
||||
this.finishingOrder = new ArrayList<Boat>();
|
||||
|
||||
// create a priority queue with a custom Comparator to order events
|
||||
this.events = new PriorityQueue<Event>(new Comparator<Event>() {
|
||||
@Override
|
||||
public int compare(Event o1, Event o2) {
|
||||
Long time1 = o1.getTime();
|
||||
Long time2 = o2.getTime();
|
||||
|
||||
// order event asc. by time. if tie appears, then order team
|
||||
// name alphabetically.
|
||||
if (time1 != time2) {
|
||||
return time1.compareTo(time2);
|
||||
} else {
|
||||
return o1.getBoat().getTeamName().compareTo(o2.getBoat().getTeamName());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a boat to the race
|
||||
*
|
||||
* @param boat, the boat to add
|
||||
*/
|
||||
public void addBoat(Boat boat) {
|
||||
boats.add(boat);
|
||||
numberOfBoats += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of boats in a random order
|
||||
*
|
||||
* @returns a list of boats
|
||||
*/
|
||||
public Boat[] getShuffledBoats() {
|
||||
// Shuffle the list of boats
|
||||
long seed = System.nanoTime();
|
||||
Collections.shuffle(this.boats, new Random(seed));
|
||||
|
||||
return boats.toArray(new Boat[boats.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of boats in the order that they
|
||||
* finished the race (position 0 is first place)
|
||||
*
|
||||
* @returns a list of boats
|
||||
*/
|
||||
public Boat[] getFinishedBoats() {
|
||||
return this.finishingOrder.toArray(new Boat[this.finishingOrder.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of boats in the race
|
||||
*
|
||||
* @returns the number of boats in the race
|
||||
*/
|
||||
public int getNumberOfBoats() {
|
||||
return numberOfBoats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of boats in the race
|
||||
*
|
||||
* @return a list of the boats competing in the race
|
||||
*/
|
||||
public Boat[] getBoats() {
|
||||
return boats.toArray(new Boat[boats.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the order in which the boats finished the race
|
||||
*/
|
||||
public void displayFinishingOrder() {
|
||||
int numberOfBoats = this.getNumberOfBoats();
|
||||
Boat[] boats = this.getFinishedBoats();
|
||||
|
||||
System.out.println("--- Finishing Order ---");
|
||||
|
||||
for (int i = 0; i < Array.getLength(boats); i++) {
|
||||
System.out.println("#" + Integer.toString(i + 1) + " - " + boats[i].getTeamName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the list of boats competing in the race
|
||||
*/
|
||||
public void displayStartingBoats() {
|
||||
int numberOfBoats = this.getNumberOfBoats();
|
||||
Boat[] boats = this.getBoats();
|
||||
|
||||
System.out.println("######################");
|
||||
System.out.println("# Competing Boats ");
|
||||
System.out.println("######################");
|
||||
|
||||
for (int i = 0; i < numberOfBoats; i++) {
|
||||
String velocityKnots = String.format("%1.2f", boats[i].getVelocity() * 1.943844492);
|
||||
|
||||
System.out.println(boats[i].getTeamName() + " Velocity: " + velocityKnots + " Knots/s");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a leg to the race
|
||||
*
|
||||
* @param leg, the leg to add to the race
|
||||
*/
|
||||
public void addLeg(Leg leg) {
|
||||
this.legs.add(leg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets legs array
|
||||
*
|
||||
* @return an array of legs
|
||||
*/
|
||||
public ArrayList<Leg> getLegs() {
|
||||
return this.legs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
//calculate the time every boat passes each leg, and create an event
|
||||
for (Boat boat : this.boats) {
|
||||
long totalDistance = 0;
|
||||
for (Leg leg : this.legs) {
|
||||
long time = (long) (1000 * totalDistance / boat.getVelocity());
|
||||
Event event = new Event(time, boat, leg);
|
||||
events.add(event);
|
||||
totalDistance += leg.getDistance();
|
||||
|
||||
// If finishing leg, add another event for when the boat finishes the race
|
||||
if (leg.getIsFinishingLeg()) {
|
||||
time = (long) (1000 * totalDistance / boat.getVelocity());
|
||||
event = new Event(time, boat, leg, true);
|
||||
events.add(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how far a boat has travelled in meters
|
||||
*
|
||||
* @param velocity the velocity of boat
|
||||
* @return a float number of distance the boat has been travelled
|
||||
*/
|
||||
public float getDistanceTravelled(long velocity) {
|
||||
long timeDiff = System.currentTimeMillis() - this.startTime;
|
||||
long timeElapse = (long) (timeDiff / 1000 * this.timeScale);
|
||||
return timeElapse * velocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over events in the race and print the
|
||||
* event string for each event
|
||||
*/
|
||||
public void iterateEvents() {
|
||||
// iterates all events. ends when no event in events.
|
||||
while (!events.isEmpty()) {
|
||||
Event peekEvent = events.peek();
|
||||
long currentTime = (long) ((System.currentTimeMillis() - this.startTime) * this.timeScale);
|
||||
|
||||
if (currentTime > peekEvent.getTime()) {
|
||||
Event nextEvent = events.poll();
|
||||
|
||||
// Display a summary of the event
|
||||
System.out.println(nextEvent.getEventString());
|
||||
nextEvent.boatPassedMarker();
|
||||
|
||||
// If event is a boat finishing the race
|
||||
if (nextEvent.getIsFinishingEvent()) {
|
||||
this.finishingOrder.add(nextEvent.getBoat());
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for 100ms to throttle the while loop
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (java.lang.InterruptedException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the race and print each marker with the order
|
||||
* in which the boats passed that marker
|
||||
*/
|
||||
public void startRace() {
|
||||
// record start time.
|
||||
generateEvents();
|
||||
this.startTime = System.currentTimeMillis();
|
||||
iterateEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the order in which the boats passed each marker
|
||||
*/
|
||||
public void showRaceMarkerResults() {
|
||||
for (Leg leg : this.legs) {
|
||||
Boat[] boats = leg.getMarker().getBoats();
|
||||
|
||||
System.out.println("--- " + leg.getMarkerLabel() + " ---");
|
||||
|
||||
// Print the order in which the boats passed the marker
|
||||
for (int i = 0; i < this.getNumberOfBoats(); i++) {
|
||||
System.out.println("#" + Integer.toString(i + 1) + " - " + boats[i].getTeamName());
|
||||
}
|
||||
|
||||
System.out.println("");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package seng302.gameServer;
|
||||
|
||||
import seng302.model.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,702 @@
|
||||
package seng302.gameServer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javafx.scene.paint.Color;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.xml.sax.InputSource;
|
||||
import seng302.gameServer.messages.BoatAction;
|
||||
import seng302.gameServer.messages.BoatStatus;
|
||||
import seng302.gameServer.messages.CustomizeRequestType;
|
||||
import seng302.gameServer.messages.MarkRoundingMessage;
|
||||
import seng302.gameServer.messages.MarkType;
|
||||
import seng302.gameServer.messages.Message;
|
||||
import seng302.gameServer.messages.RoundingBoatStatus;
|
||||
import seng302.gameServer.messages.YachtEventCodeMessage;
|
||||
import seng302.model.GeoPoint;
|
||||
import seng302.model.Limit;
|
||||
import seng302.model.Player;
|
||||
import seng302.model.PolarTable;
|
||||
import seng302.model.ServerYacht;
|
||||
import seng302.model.mark.CompoundMark;
|
||||
import seng302.model.mark.Mark;
|
||||
import seng302.model.mark.MarkOrder;
|
||||
import seng302.utilities.GeoUtility;
|
||||
import seng302.utilities.XMLParser;
|
||||
|
||||
/**
|
||||
* A Static class to hold information about the current state of the game (model)
|
||||
* Also contains logic for updating itself on regular time intervals on its own thread
|
||||
* Created by wmu16 on 10/07/17.
|
||||
*/
|
||||
public class GameState implements Runnable {
|
||||
|
||||
@FunctionalInterface
|
||||
interface NewMessageListener {
|
||||
|
||||
void notify(Message message);
|
||||
}
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(GameState.class);
|
||||
|
||||
private static final Integer STATE_UPDATES_PER_SECOND = 60;
|
||||
public static Integer MAX_PLAYERS = 8;
|
||||
public static Double ROUNDING_DISTANCE = 50d; // TODO: 14/08/17 wmu16 - Look into this value further
|
||||
public static final Double MARK_COLLISION_DISTANCE = 15d;
|
||||
public static final Double YACHT_COLLISION_DISTANCE = 25.0;
|
||||
public static final Double BOUNCE_DISTANCE_MARK = 20.0;
|
||||
public static final Double BOUNCE_DISTANCE_YACHT = 30.0;
|
||||
public static final Double COLLISION_VELOCITY_PENALTY = 0.3;
|
||||
|
||||
private static Long previousUpdateTime;
|
||||
public static Double windDirection;
|
||||
private static Double windSpeed;
|
||||
|
||||
private static Boolean customizationFlag; // dirty flag to tell if a player has customized their boat.
|
||||
|
||||
private static String hostIpAddress;
|
||||
private static List<Player> players;
|
||||
private static Map<Integer, ServerYacht> yachts;
|
||||
private static Boolean isRaceStarted;
|
||||
private static GameStages currentStage;
|
||||
private static MarkOrder markOrder;
|
||||
private static long startTime;
|
||||
private static Set<Mark> marks;
|
||||
private static List<Limit> courseLimit;
|
||||
|
||||
private static List<NewMessageListener> markListeners;
|
||||
|
||||
private static Map<Player, String> playerStringMap = new HashMap<>();
|
||||
/*
|
||||
Ideally I would like to make this class an object instantiated by the server and given to
|
||||
it's created threads if necessary. Outside of that I think the dependencies on it
|
||||
(atm only Yacht & GameClient) can be removed from most other classes. The observable list of
|
||||
players could be pulled directly from the server by the GameClient since it instantiates it
|
||||
and it is reasonable for it to pull data. The current setup of publicly available statics is
|
||||
pretty meh IMO because anything can change it making it unreliable and like people did with
|
||||
the old ServerParser class everything that needs shared just gets thrown in the static
|
||||
collections and things become a real mess.
|
||||
*/
|
||||
|
||||
public GameState(String hostIpAddress) {
|
||||
windDirection = 180d;
|
||||
windSpeed = 10000d;
|
||||
this.hostIpAddress = hostIpAddress;
|
||||
yachts = new HashMap<>();
|
||||
players = new ArrayList<>();
|
||||
GameState.hostIpAddress = hostIpAddress;
|
||||
customizationFlag = false;
|
||||
|
||||
currentStage = GameStages.LOBBYING;
|
||||
isRaceStarted = false;
|
||||
//set this when game stage changes to prerace
|
||||
previousUpdateTime = System.currentTimeMillis();
|
||||
markOrder = new MarkOrder(); //This could be instantiated at some point with a select map?
|
||||
markListeners = new ArrayList<>();
|
||||
|
||||
resetStartTime();
|
||||
|
||||
new Thread(this, "GameState").start(); //Run the auto updates on the game state
|
||||
|
||||
marks = new MarkOrder().getAllMarks();
|
||||
setCourseLimit("/server_config/race.xml");
|
||||
}
|
||||
|
||||
private void setCourseLimit(String url) {
|
||||
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
documentBuilderFactory.setNamespaceAware(true);
|
||||
DocumentBuilder documentBuilder;
|
||||
Document document = null;
|
||||
try {
|
||||
documentBuilder = documentBuilderFactory.newDocumentBuilder();
|
||||
document = documentBuilder.parse(new InputSource(getClass().getResourceAsStream(url)));
|
||||
} catch (Exception e) {
|
||||
// sorry, we have to catch general one, otherwise we have to catch five different exceptions.
|
||||
logger.trace("Failed to load course limit for boundary collision detection.", e);
|
||||
}
|
||||
courseLimit = XMLParser.parseRace(document).getCourseLimit();
|
||||
}
|
||||
|
||||
public static String getHostIpAddress() {
|
||||
return hostIpAddress;
|
||||
}
|
||||
|
||||
public static Set<Mark> getMarks() {
|
||||
return Collections.unmodifiableSet(marks);
|
||||
}
|
||||
|
||||
public static List<Player> getPlayers() {
|
||||
return players;
|
||||
}
|
||||
|
||||
public static void addPlayer(Player player) {
|
||||
players.add(player);
|
||||
String playerText = player.getYacht().getSourceId() + " " + player.getYacht().getBoatName()
|
||||
+ " " + player.getYacht().getCountry();
|
||||
playerStringMap.put(player, playerText);
|
||||
}
|
||||
|
||||
public static void removePlayer(Player player) {
|
||||
players.remove(player);
|
||||
playerStringMap.remove(player);
|
||||
}
|
||||
|
||||
public static void addYacht(Integer sourceId, ServerYacht 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) {
|
||||
GameState.currentStage = currentStage;
|
||||
}
|
||||
|
||||
public static MarkOrder getMarkOrder() {
|
||||
return markOrder;
|
||||
}
|
||||
|
||||
public static long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public static void resetStartTime(){
|
||||
startTime = System.currentTimeMillis() + MainServerThread.TIME_TILL_START;
|
||||
}
|
||||
|
||||
public static Double getWindDirection() {
|
||||
return windDirection;
|
||||
}
|
||||
|
||||
public static void setWindDirection(Double newWindDirection) {
|
||||
windDirection = newWindDirection;
|
||||
}
|
||||
|
||||
public static void setWindSpeed(Double newWindSpeed) {
|
||||
windSpeed = newWindSpeed;
|
||||
}
|
||||
|
||||
public static Double getWindSpeedMMS() {
|
||||
return windSpeed;
|
||||
}
|
||||
|
||||
public static Double getWindSpeedKnots() {
|
||||
return GeoUtility.mmsToKnots(windSpeed); // TODO: 26/07/17 cir27 - remove magic numbers
|
||||
}
|
||||
|
||||
public static Map<Integer, ServerYacht> getYachts() {
|
||||
return yachts;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 (currentStage != GameStages.FINISHED) {
|
||||
try {
|
||||
Thread.sleep(1000 / STATE_UPDATES_PER_SECOND);
|
||||
} catch (InterruptedException e) {
|
||||
System.out.println("[GameState] interrupted exception");
|
||||
}
|
||||
if (currentStage == GameStages.PRE_RACE || currentStage == GameStages.RACING) {
|
||||
update();
|
||||
}
|
||||
|
||||
if (currentStage == GameStages.RACING) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateBoat(Integer sourceId, BoatAction actionType) {
|
||||
ServerYacht playerYacht = yachts.get(sourceId);
|
||||
switch (actionType) {
|
||||
case VMG:
|
||||
playerYacht.turnToVMG();
|
||||
break;
|
||||
case SAILS_IN:
|
||||
playerYacht.toggleSailIn();
|
||||
break;
|
||||
case SAILS_OUT:
|
||||
playerYacht.toggleSailIn();
|
||||
break;
|
||||
case TACK_GYBE:
|
||||
playerYacht.tackGybe(windDirection);
|
||||
break;
|
||||
case UPWIND:
|
||||
playerYacht.turnUpwind();
|
||||
break;
|
||||
case DOWNWIND:
|
||||
playerYacht.turnDownwind();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called periodically in this GameState thread to update the GameState values
|
||||
*/
|
||||
public void update() {
|
||||
Boolean raceFinished = true;
|
||||
|
||||
Double timeInterval = (System.currentTimeMillis() - previousUpdateTime) / 1000000.0;
|
||||
previousUpdateTime = System.currentTimeMillis();
|
||||
if (System.currentTimeMillis() > startTime) {
|
||||
GameState.setCurrentStage(GameStages.RACING);
|
||||
}
|
||||
for (ServerYacht yacht : yachts.values()) {
|
||||
updateVelocity(yacht);
|
||||
yacht.runAutoPilot();
|
||||
yacht.updateLocation(timeInterval);
|
||||
if (yacht.getBoatStatus() != BoatStatus.FINISHED) {
|
||||
checkCollision(yacht);
|
||||
checkForLegProgression(yacht);
|
||||
raceFinished = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (raceFinished) {
|
||||
currentStage = GameStages.FINISHED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the yacht has crossed the course limit
|
||||
*
|
||||
* @param yacht the yacht to be tested
|
||||
* @return a boolean value of if there is a boundary collision
|
||||
*/
|
||||
private static Boolean checkBoundaryCollision(ServerYacht yacht) {
|
||||
for (int i = 0; i < courseLimit.size() - 1; i++) {
|
||||
if (GeoUtility.checkCrossedLine(courseLimit.get(i), courseLimit.get(i + 1),
|
||||
yacht.getLastLocation(), yacht.getLocation()) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (GeoUtility.checkCrossedLine(courseLimit.get(courseLimit.size() - 1), courseLimit.get(0),
|
||||
yacht.getLastLocation(), yacht.getLocation()) != 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void checkCollision(ServerYacht serverYacht) {
|
||||
ServerYacht collidedYacht = checkYachtCollision(serverYacht);
|
||||
if (collidedYacht != null) {
|
||||
GeoPoint originalLocation = serverYacht.getLocation();
|
||||
serverYacht.setLocation(
|
||||
calculateBounceBack(serverYacht, originalLocation, BOUNCE_DISTANCE_YACHT)
|
||||
);
|
||||
serverYacht.setCurrentVelocity(
|
||||
serverYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY
|
||||
);
|
||||
collidedYacht.setLocation(
|
||||
calculateBounceBack(collidedYacht, originalLocation, BOUNCE_DISTANCE_YACHT)
|
||||
);
|
||||
collidedYacht.setCurrentVelocity(
|
||||
collidedYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY
|
||||
);
|
||||
notifyMessageListeners(
|
||||
new YachtEventCodeMessage(serverYacht.getSourceId())
|
||||
);
|
||||
} else {
|
||||
Mark collidedMark = checkMarkCollision(serverYacht);
|
||||
if (collidedMark != null) {
|
||||
serverYacht.setLocation(
|
||||
calculateBounceBack(serverYacht, collidedMark, BOUNCE_DISTANCE_MARK)
|
||||
);
|
||||
serverYacht.setCurrentVelocity(
|
||||
serverYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY
|
||||
);
|
||||
notifyMessageListeners(
|
||||
new YachtEventCodeMessage(serverYacht.getSourceId())
|
||||
);
|
||||
}
|
||||
else{
|
||||
if (checkBoundaryCollision(serverYacht)) {
|
||||
serverYacht.setLocation(
|
||||
calculateBounceBack(serverYacht, serverYacht.getLocation(),
|
||||
BOUNCE_DISTANCE_YACHT)
|
||||
);
|
||||
serverYacht.setCurrentVelocity(
|
||||
serverYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY
|
||||
);
|
||||
notifyMessageListeners(
|
||||
new YachtEventCodeMessage(serverYacht.getSourceId())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void updateVelocity(ServerYacht yacht) {
|
||||
Double velocity = yacht.getCurrentVelocity();
|
||||
Double trueWindAngle = Math.abs(windDirection - yacht.getHeading());
|
||||
Double boatSpeedInKnots = PolarTable.getBoatSpeed(getWindSpeedKnots(), trueWindAngle);
|
||||
Double maxBoatSpeed = GeoUtility.knotsToMMS(boatSpeedInKnots);
|
||||
// TODO: 15/08/17 remove magic numbers from these equations.
|
||||
if (yacht.getSailIn()) {
|
||||
if (velocity < maxBoatSpeed - 500) {
|
||||
yacht.changeVelocity(maxBoatSpeed / 100);
|
||||
} else if (velocity > maxBoatSpeed + 500) {
|
||||
yacht.changeVelocity(-velocity / 200);
|
||||
} else {
|
||||
yacht.setCurrentVelocity(maxBoatSpeed);
|
||||
}
|
||||
} else {
|
||||
if (velocity > 3000) {
|
||||
yacht.changeVelocity(-velocity / 200);
|
||||
} else if (velocity > 100) {
|
||||
yacht.changeVelocity(-velocity / 50);
|
||||
} else if (velocity <= 100) {
|
||||
yacht.setCurrentVelocity(0d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the distance to the next mark (closest of the two if a gate mark). For purposes of
|
||||
* mark rounding
|
||||
*
|
||||
* @return A distance in metres. Returns -1 if there is no next mark
|
||||
* @throws IndexOutOfBoundsException If the next mark is null (ie the last mark in the race)
|
||||
* Check first using {@link seng302.model.mark.MarkOrder#isLastMark(Integer)}
|
||||
*/
|
||||
private Double calcDistanceToCurrentMark(ServerYacht yacht) throws IndexOutOfBoundsException {
|
||||
Integer currentMarkSeqID = yacht.getCurrentMarkSeqID();
|
||||
CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID);
|
||||
GeoPoint location = yacht.getLocation();
|
||||
|
||||
if (currentMark.isGate()) {
|
||||
Mark sub1 = currentMark.getSubMark(1);
|
||||
Mark sub2 = currentMark.getSubMark(2);
|
||||
Double distance1 = GeoUtility.getDistance(location, sub1);
|
||||
Double distance2 = GeoUtility.getDistance(location, sub2);
|
||||
if (distance1 < distance2) {
|
||||
yacht.setClosestCurrentMark(sub1);
|
||||
return distance1;
|
||||
} else {
|
||||
yacht.setClosestCurrentMark(sub2);
|
||||
return distance2;
|
||||
}
|
||||
} else {
|
||||
yacht.setClosestCurrentMark(currentMark.getSubMark(1));
|
||||
return GeoUtility.getDistance(location, currentMark.getSubMark(1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** lobbyController.setPlayerListSource(clientLobbyList);
|
||||
* 4 Different cases of progression in the race 1 - Passing the start line 2 - Passing any
|
||||
* in-race Gate 3 - Passing any in-race Mark 4 - Passing the finish line
|
||||
*
|
||||
* @param yacht the current yacht to check for progression
|
||||
*/
|
||||
private void checkForLegProgression(ServerYacht yacht) {
|
||||
Integer currentMarkSeqID = yacht.getCurrentMarkSeqID();
|
||||
CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID);
|
||||
|
||||
Boolean hasProgressed;
|
||||
if (currentMarkSeqID == 0) {
|
||||
hasProgressed = checkStartLineCrossing(yacht);
|
||||
} else if (markOrder.isLastMark(currentMarkSeqID)) {
|
||||
hasProgressed = checkFinishLineCrossing(yacht);
|
||||
} else if (currentMark.isGate()) {
|
||||
hasProgressed = checkGateRounding(yacht);
|
||||
} else {
|
||||
hasProgressed = checkMarkRounding(yacht);
|
||||
}
|
||||
|
||||
if (hasProgressed) {
|
||||
yacht.incrementLegNumber();
|
||||
sendMarkRoundingMessage(yacht);
|
||||
logMarkRounding(yacht);
|
||||
yacht.setHasPassedLine(false);
|
||||
yacht.setHasEnteredRoundingZone(false);
|
||||
yacht.setHasPassedThroughGate(false);
|
||||
if (!markOrder.isLastMark(currentMarkSeqID)) {
|
||||
yacht.incrementMarkSeqID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If we pass the start line gate in the correct direction, progress
|
||||
*
|
||||
* @param yacht The current yacht to check for
|
||||
*/
|
||||
private Boolean checkStartLineCrossing(ServerYacht yacht) {
|
||||
Integer currentMarkSeqID = yacht.getCurrentMarkSeqID();
|
||||
CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID);
|
||||
GeoPoint lastLocation = yacht.getLastLocation();
|
||||
GeoPoint location = yacht.getLocation();
|
||||
|
||||
Mark mark1 = currentMark.getSubMark(1);
|
||||
Mark mark2 = currentMark.getSubMark(2);
|
||||
CompoundMark nextMark = markOrder.getNextMark(currentMarkSeqID);
|
||||
|
||||
Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location);
|
||||
if (crossedLine > 0) {
|
||||
Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint());
|
||||
if (crossedLine == 2 && isClockwiseCross || crossedLine == 1 && !isClockwiseCross) {
|
||||
yacht.setClosestCurrentMark(mark1);
|
||||
yacht.setBoatStatus(BoatStatus.RACING);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This algorithm checks for mark rounding. And increments the currentMarSeqID number attribute
|
||||
* of the yacht if so. A visual representation of this algorithm can be seen on the Wiki under
|
||||
* 'mark passing algorithm'
|
||||
*
|
||||
* @param yacht The current yacht to check for
|
||||
*/
|
||||
private Boolean checkMarkRounding(ServerYacht yacht) {
|
||||
Integer currentMarkSeqID = yacht.getCurrentMarkSeqID();
|
||||
CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID);
|
||||
GeoPoint lastLocation = yacht.getLastLocation();
|
||||
GeoPoint location = yacht.getLocation();
|
||||
GeoPoint nextPoint = markOrder.getNextMark(currentMarkSeqID).getMidPoint();
|
||||
GeoPoint prevPoint = markOrder.getPreviousMark(currentMarkSeqID).getMidPoint();
|
||||
GeoPoint midPoint = GeoUtility.getDirtyMidPoint(nextPoint, prevPoint);
|
||||
|
||||
if (calcDistanceToCurrentMark(yacht) < ROUNDING_DISTANCE) {
|
||||
yacht.setHasEnteredRoundingZone(true);
|
||||
}
|
||||
|
||||
//In case current mark is a gate, loop through all marks just in case
|
||||
for (Mark thisCurrentMark : currentMark.getMarks()) {
|
||||
if (GeoUtility.isPointInTriangle(lastLocation, location, midPoint, thisCurrentMark)) {
|
||||
yacht.setHasPassedLine(true);
|
||||
}
|
||||
}
|
||||
|
||||
return yacht.hasPassedLine() && yacht.hasEnteredRoundingZone();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a gate line has been crossed and in the correct direction
|
||||
*
|
||||
* @param yacht The current yacht to check for
|
||||
*/
|
||||
private Boolean checkGateRounding(ServerYacht yacht) {
|
||||
Integer currentMarkSeqID = yacht.getCurrentMarkSeqID();
|
||||
CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID);
|
||||
GeoPoint lastLocation = yacht.getLastLocation();
|
||||
GeoPoint location = yacht.getLocation();
|
||||
|
||||
Mark mark1 = currentMark.getSubMark(1);
|
||||
Mark mark2 = currentMark.getSubMark(2);
|
||||
CompoundMark prevMark = markOrder.getPreviousMark(currentMarkSeqID);
|
||||
CompoundMark nextMark = markOrder.getNextMark(currentMarkSeqID);
|
||||
|
||||
Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location);
|
||||
|
||||
//We have crossed the line
|
||||
if (crossedLine > 0) {
|
||||
Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint());
|
||||
|
||||
//Check we cross the line in the correct direction
|
||||
if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) {
|
||||
yacht.setHasPassedThroughGate(true);
|
||||
}
|
||||
}
|
||||
|
||||
Boolean prevMarkSide = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint());
|
||||
Boolean nextMarkSide = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint());
|
||||
|
||||
if (yacht.hasPassedThroughGate()) {
|
||||
//Check if we need to round this gate after passing through
|
||||
if (prevMarkSide == nextMarkSide) {
|
||||
return checkMarkRounding(yacht);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we pass the finish gate in the correct direction
|
||||
*
|
||||
* @param yacht The current yacht to check for
|
||||
*/
|
||||
private Boolean checkFinishLineCrossing(ServerYacht yacht) {
|
||||
Integer currentMarkSeqID = yacht.getCurrentMarkSeqID();
|
||||
CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID);
|
||||
GeoPoint lastLocation = yacht.getLastLocation();
|
||||
GeoPoint location = yacht.getLocation();
|
||||
|
||||
Mark mark1 = currentMark.getSubMark(1);
|
||||
Mark mark2 = currentMark.getSubMark(2);
|
||||
CompoundMark prevMark = markOrder.getPreviousMark(currentMarkSeqID);
|
||||
|
||||
Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location);
|
||||
if (crossedLine > 0) {
|
||||
Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint());
|
||||
if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) {
|
||||
yacht.setClosestCurrentMark(mark1);
|
||||
yacht.setBoatStatus(BoatStatus.FINISHED);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles player customization.
|
||||
*
|
||||
* @param playerID The ID of the player being modified.
|
||||
* @param requestType the type of player customization the player wants
|
||||
* @param customizeData the data related to the customization (color, name, shape)
|
||||
*/
|
||||
public static void customizePlayer(long playerID, CustomizeRequestType requestType,
|
||||
byte[] customizeData) {
|
||||
ServerYacht playerYacht = yachts.get((int) playerID);
|
||||
|
||||
if (requestType.equals(CustomizeRequestType.NAME)) {
|
||||
String name = new String(customizeData);
|
||||
playerYacht.setBoatName(name);
|
||||
} else if (requestType.equals(CustomizeRequestType.COLOR)) {
|
||||
int red = customizeData[0] & 0xFF;
|
||||
int green = customizeData[1] & 0xFF;
|
||||
int blue = customizeData[2] & 0xFF;
|
||||
Color yachtColor = Color.rgb(red, green, blue);
|
||||
playerYacht.setBoatColor(yachtColor);
|
||||
}
|
||||
}
|
||||
|
||||
private static Mark checkMarkCollision(ServerYacht yacht) {
|
||||
Set<Mark> marksInRace = GameState.getMarks();
|
||||
for (Mark mark : marksInRace) {
|
||||
if (GeoUtility.getDistance(yacht.getLocation(), mark)
|
||||
<= MARK_COLLISION_DISTANCE) {
|
||||
return mark;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the new position of the boat after it has had a collision
|
||||
*
|
||||
* @return The boats new position
|
||||
*/
|
||||
private static GeoPoint calculateBounceBack(ServerYacht yacht, GeoPoint collidedWith,
|
||||
Double bounceDistance) {
|
||||
Double heading = GeoUtility.getBearing(yacht.getLastLocation(), collidedWith);
|
||||
// Invert heading
|
||||
heading -= 180;
|
||||
Integer newHeading = Math.floorMod(heading.intValue(), 360);
|
||||
return GeoUtility
|
||||
.getGeoCoordinate(yacht.getLocation(), newHeading.doubleValue(), bounceDistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collision detection which iterates through all the yachts and check if any yacht collided
|
||||
* with this yacht. Return collided yacht or null if no collision.
|
||||
*
|
||||
* @return yacht to compare to all other yachts.
|
||||
*/
|
||||
private static ServerYacht checkYachtCollision(ServerYacht yacht) {
|
||||
|
||||
for (ServerYacht otherYacht : GameState.getYachts().values()) {
|
||||
if (otherYacht != yacht) {
|
||||
Double distance = GeoUtility
|
||||
.getDistance(otherYacht.getLocation(), yacht.getLocation());
|
||||
if (distance < YACHT_COLLISION_DISTANCE) {
|
||||
return otherYacht;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void sendMarkRoundingMessage(ServerYacht yacht) {
|
||||
Integer sourceID = yacht.getSourceId();
|
||||
Integer currentMarkSeqID = yacht.getCurrentMarkSeqID();
|
||||
CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID);
|
||||
MarkType markType = (currentMark.isGate()) ? MarkType.GATE : MarkType.ROUNDING_MARK;
|
||||
Mark roundingMark = yacht.getClosestCurrentMark();
|
||||
|
||||
// TODO: 13/8/17 figure out the rounding side, rounded mark source ID and boat status.
|
||||
Message markRoundingMessage = new MarkRoundingMessage(0, 0,
|
||||
sourceID, RoundingBoatStatus.RACING, roundingMark.getRoundingSide(), markType,
|
||||
currentMarkSeqID + 1);
|
||||
|
||||
notifyMessageListeners(markRoundingMessage);
|
||||
}
|
||||
|
||||
private static void notifyMessageListeners(Message message) {
|
||||
for (NewMessageListener mpl : markListeners) {
|
||||
mpl.notify(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void logMarkRounding(ServerYacht yacht) {
|
||||
Mark roundingMark = yacht.getClosestCurrentMark();
|
||||
logger.debug(
|
||||
String.format("Yacht srcID(%d) passed Mark srcID(%d)", yacht.getSourceId(),
|
||||
roundingMark.getSourceID()));
|
||||
}
|
||||
|
||||
|
||||
public static void addMarkPassListener(NewMessageListener listener) {
|
||||
markListeners.add(listener);
|
||||
}
|
||||
|
||||
public static void setCustomizationFlag() {
|
||||
customizationFlag = true;
|
||||
}
|
||||
|
||||
public static Boolean getCustomizationFlag() {
|
||||
return customizationFlag;
|
||||
}
|
||||
|
||||
public static void resetCustomizationFlag() {
|
||||
customizationFlag = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package seng302.gameServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Stack;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import seng302.model.Player;
|
||||
import seng302.gameServer.messages.Heartbeat;
|
||||
import seng302.gameServer.messages.Message;
|
||||
|
||||
/**
|
||||
* 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 implements Runnable {
|
||||
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<>();
|
||||
|
||||
Thread thread = new Thread(this, "HeartBeat");
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,317 @@
|
||||
package seng302.gameServer;
|
||||
|
||||
import seng302.gameServer.messages.*;
|
||||
import seng302.model.GeoPoint;
|
||||
import seng302.model.Player;
|
||||
import seng302.model.PolarTable;
|
||||
import seng302.model.ServerYacht;
|
||||
import seng302.model.mark.CompoundMark;
|
||||
import seng302.utilities.GeoUtility;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 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 implements Runnable, ClientConnectionDelegate {
|
||||
|
||||
private static final int PORT = 4942;
|
||||
private static final Integer CLIENT_UPDATES_PER_SECOND = 60;
|
||||
private static final int LOG_LEVEL = 1;
|
||||
private static final int WARNING_TIME = 10 * -1000;
|
||||
private static final int PREPATORY_TIME = 5 * -1000;
|
||||
public static final int TIME_TILL_START = 10 * 1000;
|
||||
|
||||
private static final int MAX_WIND_SPEED = 12000;
|
||||
private static final int MIN_WIND_SPEED = 8000;
|
||||
|
||||
public static int windSpeed = 1000;
|
||||
|
||||
private boolean terminated;
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private ServerSocket serverSocket = null;
|
||||
private ArrayList<ServerToClientThread> serverToClientThreads = new ArrayList<>();
|
||||
|
||||
public MainServerThread() {
|
||||
new GameState("localhost");
|
||||
try {
|
||||
serverSocket = new ServerSocket(PORT);
|
||||
} catch (IOException e) {
|
||||
serverLog("IO error in server thread handler upon trying to make new server socket", 0);
|
||||
}
|
||||
PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv"));
|
||||
GameState.addMarkPassListener(this::broadcastMessage);
|
||||
terminated = false;
|
||||
thread = new Thread(this, "MainServer");
|
||||
startUpdatingWind();
|
||||
thread.start();
|
||||
}
|
||||
|
||||
|
||||
public void run() {
|
||||
|
||||
new HeartbeatThread(this);
|
||||
new ServerListenThread(serverSocket, this);
|
||||
|
||||
//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.LOBBYING && GameState
|
||||
.getCustomizationFlag()) {
|
||||
// TODO: 16/08/17 ajm412: This can probably be done in a nicer way via those fancy functional interfaces.
|
||||
for (ServerToClientThread thread : serverToClientThreads) {
|
||||
thread.sendSetupMessages();
|
||||
}
|
||||
GameState.resetCustomizationFlag();
|
||||
}
|
||||
|
||||
if (GameState.getCurrentStage() == GameStages.PRE_RACE) {
|
||||
updateClients();
|
||||
}
|
||||
|
||||
//RACING
|
||||
if (GameState.getCurrentStage() == GameStages.RACING) {
|
||||
updateClients();
|
||||
}
|
||||
|
||||
//FINISHED
|
||||
else if (GameState.getCurrentStage() == GameStages.FINISHED) {
|
||||
terminate();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 14/07/17 wmu16 - Send out disconnect packet to clients
|
||||
try {
|
||||
for (ServerToClientThread serverToClientThread : serverToClientThreads) {
|
||||
serverToClientThread.terminate();
|
||||
}
|
||||
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.sendBoatLocationPackets();
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastMessage(Message message) {
|
||||
for (ServerToClientThread serverToClientThread : serverToClientThreads) {
|
||||
serverToClientThread.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateWind(){
|
||||
Integer direction = GameState.getWindDirection().intValue();
|
||||
Integer windSpeed = GameState.getWindSpeedMMS().intValue();
|
||||
|
||||
Random random = new Random();
|
||||
|
||||
if (Math.floorMod(random.nextInt(), 2) == 0){
|
||||
direction += random.nextInt(4);
|
||||
windSpeed += random.nextInt(20) + 50;
|
||||
}
|
||||
else{
|
||||
direction -= random.nextInt(4);
|
||||
windSpeed -= random.nextInt(20) + 50;
|
||||
}
|
||||
|
||||
direction = Math.floorMod(direction, 360);
|
||||
|
||||
if (windSpeed > MAX_WIND_SPEED){
|
||||
windSpeed -= random.nextInt(1000);
|
||||
}
|
||||
|
||||
if (windSpeed <= MIN_WIND_SPEED){
|
||||
windSpeed += random.nextInt(1000);
|
||||
}
|
||||
|
||||
GameState.setWindSpeed(Double.valueOf(windSpeed));
|
||||
GameState.setWindDirection(direction.doubleValue());
|
||||
}
|
||||
|
||||
private static void startUpdatingWind(){
|
||||
Timer timer = new Timer();
|
||||
timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateWind();
|
||||
}
|
||||
}, 0, 500);
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
serverToClientThread.addConnectionListener(() -> {
|
||||
for (ServerToClientThread thread : serverToClientThreads) {
|
||||
thread.sendSetupMessages();
|
||||
}
|
||||
});
|
||||
serverToClientThread.addDisconnectListener(this::clientDisconnected);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
ServerToClientThread closedConnection = null;
|
||||
for (ServerToClientThread serverToClientThread : serverToClientThreads) {
|
||||
if (serverToClientThread.getSocket() == player.getSocket()) {
|
||||
closedConnection = serverToClientThread;
|
||||
} else if (GameState.getCurrentStage() != GameStages.RACING){
|
||||
serverToClientThread.sendSetupMessages();
|
||||
}
|
||||
}
|
||||
serverToClientThreads.remove(closedConnection);
|
||||
closedConnection.terminate();
|
||||
}
|
||||
|
||||
public void startGame() {
|
||||
initialiseBoatPositions();
|
||||
Timer t = new Timer();
|
||||
|
||||
t.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
broadcastMessage(makeRaceStatusMessage());
|
||||
if (GameState.getCurrentStage() == GameStages.PRE_RACE || GameState.getCurrentStage() == GameStages.LOBBYING) {
|
||||
broadcastMessage(makeRaceStartMessage());
|
||||
}
|
||||
}
|
||||
}, 0, 500);
|
||||
}
|
||||
|
||||
|
||||
private RaceStartStatusMessage makeRaceStartMessage() {
|
||||
Long raceStartTime = GameState.getStartTime();
|
||||
|
||||
return new RaceStartStatusMessage(1, raceStartTime ,
|
||||
1, RaceStartNotificationType.SET_RACE_START_TIME);
|
||||
}
|
||||
|
||||
private RaceStatusMessage makeRaceStatusMessage() {
|
||||
// variables taken from GameServerThread
|
||||
|
||||
List<BoatSubMessage> boatSubMessages = new ArrayList<>();
|
||||
RaceStatus raceStatus;
|
||||
|
||||
for (Player player : GameState.getPlayers()) {
|
||||
ServerYacht y = player.getYacht();
|
||||
BoatSubMessage m = new BoatSubMessage(y.getSourceId(), y.getBoatStatus(),
|
||||
y.getLegNumber(),
|
||||
0, 0, 1234L,
|
||||
1234L);
|
||||
boatSubMessages.add(m);
|
||||
}
|
||||
|
||||
long timeTillStart = System.currentTimeMillis() - GameState.getStartTime();
|
||||
|
||||
if (GameState.getCurrentStage() == GameStages.LOBBYING) {
|
||||
raceStatus = RaceStatus.PRESTART;
|
||||
} else if (GameState.getCurrentStage() == GameStages.PRE_RACE) {
|
||||
raceStatus = RaceStatus.PRESTART;
|
||||
|
||||
if (timeTillStart > WARNING_TIME) {
|
||||
raceStatus = RaceStatus.WARNING;
|
||||
}
|
||||
|
||||
if (timeTillStart > PREPATORY_TIME) {
|
||||
raceStatus = RaceStatus.PREPARATORY;
|
||||
}
|
||||
} else {
|
||||
raceStatus = RaceStatus.STARTED;
|
||||
}
|
||||
|
||||
return new RaceStatusMessage(1, raceStatus, GameState.getStartTime(),
|
||||
GameState.getWindDirection(),
|
||||
GameState.getWindSpeedMMS().longValue(), GameState.getPlayers().size(),
|
||||
RaceType.MATCH_RACE, 1, boatSubMessages);
|
||||
}
|
||||
|
||||
public void terminate() {
|
||||
terminated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise boats to specific spaced out geopoints behind starting line.
|
||||
*/
|
||||
private void initialiseBoatPositions() {
|
||||
// Getting the start line compound marks
|
||||
// if (gameClient== null) {
|
||||
// return;
|
||||
// }
|
||||
CompoundMark cm = GameState.getMarkOrder().getMarkOrder().get(0);
|
||||
GeoPoint startMark1 = cm.getSubMark(1);
|
||||
GeoPoint startMark2 = cm.getSubMark(2);
|
||||
|
||||
// Calculating midpoint
|
||||
Double perpendicularAngle = GeoUtility.getBearing(startMark1, startMark2);
|
||||
Double length = GeoUtility.getDistance(startMark1, startMark2);
|
||||
GeoPoint midpoint = GeoUtility.getGeoCoordinate(startMark1, perpendicularAngle, length / 2);
|
||||
|
||||
// Setting each boats position side by side
|
||||
double DISTANCE_FACTOR = 50.0; // distance apart in meters
|
||||
int boatIndex = 0;
|
||||
for (ServerYacht yacht : GameState.getYachts().values()) {
|
||||
int distanceApart = boatIndex / 2;
|
||||
|
||||
if (boatIndex % 2 == 1 && boatIndex != 0) {
|
||||
distanceApart++;
|
||||
distanceApart *= -1;
|
||||
}
|
||||
|
||||
GeoPoint spawnMark = GeoUtility
|
||||
.getGeoCoordinate(midpoint, perpendicularAngle, distanceApart * DISTANCE_FACTOR);
|
||||
|
||||
if (yacht.getHeading() < perpendicularAngle) {
|
||||
spawnMark = GeoUtility
|
||||
.getGeoCoordinate(spawnMark, perpendicularAngle + 90, DISTANCE_FACTOR);
|
||||
} else {
|
||||
spawnMark = GeoUtility
|
||||
.getGeoCoordinate(spawnMark, perpendicularAngle + 270, DISTANCE_FACTOR);
|
||||
}
|
||||
|
||||
yacht.setLocation(spawnMark);
|
||||
boatIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package seng302.gameServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* A class for a thread to listen to connections
|
||||
* Created by wmu16 on 11/07/17.
|
||||
*/
|
||||
public class ServerListenThread implements Runnable {
|
||||
private ServerSocket serverSocket;
|
||||
private ClientConnectionDelegate delegate;
|
||||
|
||||
public ServerListenThread(ServerSocket serverSocket, ClientConnectionDelegate delegate){
|
||||
this.serverSocket = serverSocket;
|
||||
this.delegate = delegate;
|
||||
|
||||
Thread thread = new Thread(this, "ServerListen");
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (serverSocket != null && !serverSocket.isClosed()){
|
||||
acceptConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package seng302.gameServer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import seng302.gameServer.messages.BoatAction;
|
||||
import seng302.gameServer.messages.ClientType;
|
||||
import seng302.gameServer.messages.CustomizeRequestType;
|
||||
import seng302.gameServer.messages.Message;
|
||||
import seng302.model.stream.packets.StreamPacket;
|
||||
|
||||
|
||||
public class ServerPacketParser {
|
||||
|
||||
public static BoatAction extractBoatAction(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long actionTypeValue = Message.bytesToLong(Arrays.copyOfRange(payload, 0, 1));
|
||||
return BoatAction.getType((int) actionTypeValue);
|
||||
}
|
||||
|
||||
public static ClientType extractClientType(StreamPacket packet){
|
||||
byte[] payload = packet.getPayload();
|
||||
long value = Message.bytesToLong(Arrays.copyOfRange(payload, 0, 1));
|
||||
return ClientType.getClientType((int) value);
|
||||
}
|
||||
|
||||
public static CustomizeRequestType extractCustomizationType(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
long type = Message.bytesToLong(Arrays.copyOfRange(payload, 4, 5));
|
||||
return CustomizeRequestType.getRequestType((int) type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
package seng302.gameServer;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
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.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Observable;
|
||||
import java.util.Observer;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Checksum;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import seng302.gameServer.messages.BoatAction;
|
||||
import seng302.gameServer.messages.BoatLocationMessage;
|
||||
import seng302.gameServer.messages.ClientType;
|
||||
import seng302.gameServer.messages.CustomizeRequestType;
|
||||
import seng302.gameServer.messages.Message;
|
||||
import seng302.gameServer.messages.RegistrationResponseMessage;
|
||||
import seng302.gameServer.messages.RegistrationResponseStatus;
|
||||
import seng302.gameServer.messages.XMLMessage;
|
||||
import seng302.gameServer.messages.XMLMessageSubType;
|
||||
import seng302.gameServer.messages.YachtEventCodeMessage;
|
||||
import seng302.gameServer.messages.YachtEventCodeMessage;
|
||||
import seng302.model.Player;
|
||||
import seng302.model.ServerYacht;
|
||||
import seng302.model.stream.packets.PacketType;
|
||||
import seng302.model.stream.packets.StreamPacket;
|
||||
import seng302.model.stream.xml.generator.Race;
|
||||
import seng302.model.stream.xml.generator.Regatta;
|
||||
import seng302.utilities.XMLGenerator;
|
||||
import seng302.gameServer.messages.BoatAction;
|
||||
import seng302.gameServer.messages.BoatLocationMessage;
|
||||
import seng302.gameServer.messages.ClientType;
|
||||
import seng302.gameServer.messages.Message;
|
||||
import seng302.gameServer.messages.RegistrationResponseMessage;
|
||||
import seng302.gameServer.messages.RegistrationResponseStatus;
|
||||
import seng302.gameServer.messages.XMLMessage;
|
||||
import seng302.gameServer.messages.XMLMessageSubType;
|
||||
import seng302.gameServer.messages.YachtEventCodeMessage;
|
||||
import seng302.model.Player;
|
||||
import seng302.model.ServerYacht;
|
||||
import seng302.model.stream.packets.PacketType;
|
||||
import seng302.model.stream.packets.StreamPacket;
|
||||
import seng302.model.stream.xml.generator.Race;
|
||||
import seng302.model.stream.xml.generator.Regatta;
|
||||
import seng302.utilities.XMLGenerator;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* Called to notify listeners when this thread receives a connection correctly.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ConnectionListener {
|
||||
void notifyConnection ();
|
||||
}
|
||||
|
||||
// TODO: 17/08/17 this is only temporary disconnects should be handled consistently
|
||||
@FunctionalInterface
|
||||
interface DisconnectListener {
|
||||
void notifyDisconnect (Player player);
|
||||
}
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(ServerToClientThread.class);
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private InputStream is;
|
||||
private OutputStream os;
|
||||
private Socket socket;
|
||||
|
||||
private ByteArrayOutputStream crcBuffer;
|
||||
|
||||
private Integer seqNo;
|
||||
private Integer sourceId;
|
||||
|
||||
private ClientType clientType;
|
||||
private Boolean isRegistered = false;
|
||||
|
||||
private XMLGenerator xml;
|
||||
|
||||
private List<ConnectionListener> connectionListeners = new ArrayList<>();
|
||||
private DisconnectListener disconnectListener;
|
||||
|
||||
private ServerYacht yacht;
|
||||
private Player player;
|
||||
|
||||
public ServerToClientThread(Socket socket) {
|
||||
this.socket = socket;
|
||||
seqNo = 0;
|
||||
|
||||
try{
|
||||
is = socket.getInputStream();
|
||||
os = socket.getOutputStream();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
thread = new Thread(this, "ServerToClient");
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private void setUpPlayer(){
|
||||
BufferedReader fn;
|
||||
String fName = "";
|
||||
BufferedReader ln;
|
||||
String lName = "";
|
||||
|
||||
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()));
|
||||
|
||||
ServerYacht yacht = new ServerYacht(
|
||||
"Yacht", sourceId, sourceId.toString(), fName, fName + " " + lName, "NZ"
|
||||
);
|
||||
|
||||
yacht.addObserver(this); // TODO: yacht can notify mark rounding message hyi25 13/8/17
|
||||
player = new Player(socket, yacht);
|
||||
GameState.addYacht(sourceId, yacht);
|
||||
GameState.addPlayer(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Observable o, Object arg) {
|
||||
if (arg != null) {
|
||||
sendMessage((Message) arg);
|
||||
} else {
|
||||
sendSetupMessages();
|
||||
}
|
||||
}
|
||||
|
||||
private void completeRegistration(ClientType clientType) throws IOException {
|
||||
// Fail if not a player
|
||||
if (!clientType.equals(ClientType.PLAYER)){
|
||||
RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(0, RegistrationResponseStatus.FAILURE_GENERAL);
|
||||
os.write(responseMessage.getBuffer());
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameState.getPlayers().size() >= GameState.MAX_PLAYERS){
|
||||
RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(0, RegistrationResponseStatus.FAILURE_FULL);
|
||||
os.write(responseMessage.getBuffer());
|
||||
return;
|
||||
}
|
||||
|
||||
Integer sourceId = GameState.getUniquePlayerID();
|
||||
RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(sourceId, RegistrationResponseStatus.SUCCESS_PLAYING);
|
||||
|
||||
this.clientType = clientType;
|
||||
this.sourceId = sourceId;
|
||||
isRegistered = true;
|
||||
os.write(responseMessage.getBuffer());
|
||||
|
||||
setUpPlayer();
|
||||
|
||||
for (ConnectionListener listener : connectionListeners) {
|
||||
listener.notifyConnection();
|
||||
}
|
||||
}
|
||||
|
||||
public void run() {
|
||||
int sync1;
|
||||
int sync2;
|
||||
// TODO: 14/07/17 wmu16 - Work out how to fix this while loop
|
||||
|
||||
while (socket.isConnected() && !socket.isClosed()) {
|
||||
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) {
|
||||
switch (PacketType.assignPacketType(type, payload)) {
|
||||
case BOAT_ACTION:
|
||||
BoatAction actionType = ServerPacketParser
|
||||
.extractBoatAction(
|
||||
new StreamPacket(type, payloadLength, timeStamp, payload));
|
||||
GameState.updateBoat(sourceId, actionType);
|
||||
break;
|
||||
|
||||
case RACE_REGISTRATION_REQUEST:
|
||||
ClientType requestedType = ServerPacketParser.extractClientType(
|
||||
new StreamPacket(type, payloadLength, timeStamp, payload));
|
||||
|
||||
completeRegistration(requestedType);
|
||||
break;
|
||||
|
||||
case RACE_CUSTOMIZATION_REQUEST:
|
||||
Long sourceID = Message
|
||||
.bytesToLong(Arrays.copyOfRange(payload, 0, 3));
|
||||
CustomizeRequestType requestType = ServerPacketParser
|
||||
.extractCustomizationType(
|
||||
new StreamPacket(type, payloadLength, timeStamp, payload));
|
||||
GameState.customizePlayer(sourceID, requestType,
|
||||
Arrays.copyOfRange(payload, 6, payload.length));
|
||||
GameState.setCustomizationFlag();
|
||||
// TODO: 17/08/2017 ajm412: Send a response packet here, not really necessary until we do shapes.
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logger.warn("Packet has been dropped", 1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
closeSocket();
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger.warn("Closed serverToClientThread" + thread, 1);
|
||||
}
|
||||
|
||||
public void sendSetupMessages() {
|
||||
xml = new XMLGenerator();
|
||||
Race race = new Race();
|
||||
|
||||
for (ServerYacht yacht : GameState.getYachts().values()) {
|
||||
race.addBoat(yacht);
|
||||
}
|
||||
|
||||
//@TODO calculate lat/lng values
|
||||
xml.setRegatta(
|
||||
new Regatta(
|
||||
"Party Parrot Test Server", "Bermuda Test Course",
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
currentByte = is.read();
|
||||
crcBuffer.write(currentByte);
|
||||
} catch (SocketException se) {
|
||||
disconnectListener.notifyDisconnect(this.player);
|
||||
} catch (IOException e) {
|
||||
disconnectListener.notifyDisconnect(this.player);
|
||||
logger.warn("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) {
|
||||
logger.warn("Player " + sourceId + " side socket disconnected", 1);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Message send failed", 1);
|
||||
}
|
||||
}
|
||||
|
||||
private int getSeqNo() {
|
||||
seqNo++;
|
||||
return seqNo;
|
||||
}
|
||||
|
||||
|
||||
public void sendBoatLocationPackets() {
|
||||
ArrayList<ServerYacht> yachts = new ArrayList<>(GameState.getYachts().values());
|
||||
for (ServerYacht yacht : yachts) {
|
||||
BoatLocationMessage boatLocationMessage =
|
||||
new BoatLocationMessage(
|
||||
yacht.getSourceId(),
|
||||
getSeqNo(),
|
||||
yacht.getLocation().getLat(),
|
||||
yacht.getLocation().getLng(),
|
||||
yacht.getHeading(),
|
||||
yacht.getCurrentVelocity().longValue());
|
||||
|
||||
sendMessage(boatLocationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public Thread getThread() {
|
||||
return thread;
|
||||
}
|
||||
|
||||
public Socket getSocket() {
|
||||
return socket;
|
||||
}
|
||||
|
||||
public ServerYacht getYacht() {
|
||||
return yacht;
|
||||
}
|
||||
|
||||
public void sendCollisionMessage(Integer yachtId) {
|
||||
sendMessage(new YachtEventCodeMessage(yachtId));
|
||||
}
|
||||
|
||||
public void addConnectionListener(ConnectionListener listener) {
|
||||
connectionListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeConnectionListener(ConnectionListener listener) {
|
||||
connectionListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void terminate () {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ioe) {
|
||||
logger.warn("IOException attempting to terminate serverToClientThread " + this.thread);
|
||||
}
|
||||
}
|
||||
|
||||
public void addDisconnectListener(DisconnectListener disconnectListener) {
|
||||
this.disconnectListener = disconnectListener;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Created by kre39 on 12/07/17.
|
||||
*/
|
||||
public enum BoatAction {
|
||||
|
||||
VMG(1),
|
||||
SAILS_IN(2),
|
||||
SAILS_OUT(3),
|
||||
TACK_GYBE(4),
|
||||
UPWIND(5),
|
||||
DOWNWIND(6),
|
||||
MAINTAIN_HEADING(7);
|
||||
|
||||
private final int type;
|
||||
private static final Map<Integer, BoatAction> intToTypeMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (BoatAction type : BoatAction.values()) {
|
||||
intToTypeMap.put(type.getValue(), type);
|
||||
}
|
||||
}
|
||||
|
||||
BoatAction(int type){
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static BoatAction getType(int value) {
|
||||
return intToTypeMap.get(value);
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* 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 BoatAction actionType;
|
||||
|
||||
public BoatActionMessage(BoatAction 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,162 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
public class BoatLocationMessage extends Message {
|
||||
|
||||
private final int MESSAGE_SIZE = 56;
|
||||
|
||||
private long messageVersionNumber;
|
||||
private long time;
|
||||
private long sourceId;
|
||||
private long sequenceNum;
|
||||
private DeviceType deviceType;
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
private long altitude;
|
||||
private Double heading;
|
||||
private long pitch;
|
||||
private long roll;
|
||||
private long boatSpeed;
|
||||
private long COG;
|
||||
private long SOG;
|
||||
private long apparentWindSpeed;
|
||||
private long apparentWindAngle;
|
||||
private long trueWindSpeed;
|
||||
private long trueWindDirection;
|
||||
private long trueWindAngle;
|
||||
private long currentDrift;
|
||||
private long currentSet;
|
||||
private long rudderAngle;
|
||||
|
||||
/**
|
||||
* Describes the location, altitude and sensor data from the boat.
|
||||
*
|
||||
* @param sourceId ID of the boat
|
||||
* @param sequenceNum Sequence number of the message
|
||||
* @param latitude The boats latitude
|
||||
* @param longitude The boats longitude
|
||||
* @param heading The boats heading
|
||||
* @param boatSpeed The boats speed
|
||||
*/
|
||||
public BoatLocationMessage(int sourceId, int sequenceNum, double latitude, double longitude,
|
||||
double heading, long boatSpeed) {
|
||||
messageVersionNumber = 1;
|
||||
time = System.currentTimeMillis();
|
||||
this.sourceId = sourceId;
|
||||
this.sequenceNum = sequenceNum;
|
||||
this.deviceType = DeviceType.RACING_YACHT;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.altitude = 0;
|
||||
this.heading = heading;
|
||||
this.pitch = 0;
|
||||
this.roll = 0;
|
||||
this.boatSpeed = boatSpeed;
|
||||
this.COG = 2;
|
||||
this.SOG = boatSpeed;
|
||||
this.apparentWindSpeed = 0;
|
||||
this.apparentWindAngle = 0;
|
||||
this.trueWindSpeed = 0;
|
||||
this.trueWindDirection = 0;
|
||||
this.trueWindAngle = 0;
|
||||
this.currentDrift = 0;
|
||||
this.currentSet = 0;
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary latitude or longitude to floating point number
|
||||
*
|
||||
* @param binaryPackedLatLon Binary packed lat OR lon
|
||||
* @return Floating point lat/lon
|
||||
*/
|
||||
public static double binaryPackedToLatLon(long binaryPackedLatLon) {
|
||||
return (double) binaryPackedLatLon * 180.0 / 2147483648.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary packed heading to floating point number
|
||||
*
|
||||
* @param binaryPackedHeading Binary packed heading
|
||||
* @return heading as a decimal
|
||||
*/
|
||||
public static double binaryPackedHeadingToDouble(long binaryPackedHeading) {
|
||||
return (double) binaryPackedHeading * 360.0 / 65536.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary packed wind angle to floating point number
|
||||
*
|
||||
* @param binaryPackedWindAngle Binary packed wind angle
|
||||
* @return wind angle as a decimal
|
||||
*/
|
||||
public static double binaryPackedWindAngleToDouble(long binaryPackedWindAngle) {
|
||||
return (double) binaryPackedWindAngle * 180.0 / 32768.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a latitude or longitude to a binary packed long
|
||||
*
|
||||
* @param latLon A floating point latitude/longitude
|
||||
* @return A binary packed lat/lon
|
||||
*/
|
||||
public static long latLonToBinaryPackedLong(double latLon) {
|
||||
return (long) ((536870912 * latLon) / 45);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a heading to a binary packed long
|
||||
*
|
||||
* @param heading A floating point heading
|
||||
* @return A binary packed heading
|
||||
*/
|
||||
public static long headingToBinaryPackedLong(double heading) {
|
||||
return (long) ((8192 * heading) / 45);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a wind angle to a binary packed long
|
||||
*
|
||||
* @param windAngle Floating point wind angle
|
||||
* @return A binary packed wind angle
|
||||
*/
|
||||
public static long windAngleToBinaryPackedLong(double windAngle) {
|
||||
return (long) ((8192 * windAngle) / 45);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return MESSAGE_SIZE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* The current status of a boat
|
||||
*/
|
||||
public enum BoatStatus {
|
||||
UNDEFINED(0),
|
||||
PRESTART(1),
|
||||
RACING(2),
|
||||
FINISHED(3),
|
||||
DNS(4),
|
||||
DNF(5),
|
||||
DSQ(6),
|
||||
CS(7);
|
||||
|
||||
private long code;
|
||||
|
||||
BoatStatus(long code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public long getCode(){
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* The status of each boat, sent within a race status message
|
||||
*/
|
||||
public class BoatSubMessage{
|
||||
private final int MESSAGE_SIZE = 20;
|
||||
|
||||
private long sourceId;
|
||||
private BoatStatus boatStatus;
|
||||
private long legNumber;
|
||||
private long numberPenaltiesAwarded;
|
||||
private long numberPenaltiesServed;
|
||||
private long estimatedTimeAtNextMark;
|
||||
private long estimatedTimeAtFinish;
|
||||
private ByteBuffer buff = ByteBuffer.allocate(getSize());
|
||||
private int buffPos = 0;
|
||||
|
||||
/**
|
||||
* Boat Sub message from section 4.2 of the AC35 streaming data interface spec
|
||||
* @param sourceId The source ID of the boat
|
||||
* @param boatStatus The boats status
|
||||
* @param legNumber The leg the boat is on (0= prestart, 1=start to first mark etc)
|
||||
* @param numberPenaltiesAwarded The number of penalties awarded to the boat
|
||||
* @param numberPenaltiesServed The number of penalties served to the boat
|
||||
* @param estimatedTimeAtFinish The estimated time (UTC) the boat will finish the race
|
||||
* @param estimatedTimeAtNextMark The estimated time (UTC) the boat will arrive at the next mark
|
||||
*/
|
||||
public BoatSubMessage(long sourceId, BoatStatus boatStatus, long legNumber, long numberPenaltiesAwarded, long numberPenaltiesServed,
|
||||
long estimatedTimeAtFinish, long estimatedTimeAtNextMark){
|
||||
this.sourceId = sourceId;
|
||||
this.boatStatus = boatStatus;
|
||||
this.legNumber = legNumber;
|
||||
this.numberPenaltiesAwarded = numberPenaltiesAwarded;
|
||||
this.numberPenaltiesServed = numberPenaltiesServed;
|
||||
this.estimatedTimeAtFinish = estimatedTimeAtFinish;
|
||||
this.estimatedTimeAtNextMark = estimatedTimeAtNextMark;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The size of this message in bytes
|
||||
*/
|
||||
public int getSize(){
|
||||
return MESSAGE_SIZE;
|
||||
}
|
||||
|
||||
private void putInBuffer(byte[] bytes, long val){
|
||||
byte[] tmp = bytes.clone();
|
||||
Message.reverse(tmp);
|
||||
|
||||
buff.put(tmp);
|
||||
buffPos += tmp.length;
|
||||
buff.position(buffPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a ByteBuffer containing this boat status message
|
||||
*/
|
||||
public ByteBuffer getByteBuffer(){
|
||||
// Source ID, 4 bytes
|
||||
putInBuffer(Message.intToByteArray(sourceId, 4), 4);
|
||||
|
||||
// Boat Status, 1 byte
|
||||
putInBuffer(ByteBuffer.allocate(1).put((byte) (boatStatus.getCode() & 0xff)).array(), 1);
|
||||
|
||||
// Leg number, 1 byte
|
||||
putInBuffer(ByteBuffer.allocate(1).put((byte) (legNumber & 0xff)).array(), 1);
|
||||
|
||||
// Number of penalties awarded, 1 byte
|
||||
putInBuffer(ByteBuffer.allocate(1).put((byte) (numberPenaltiesAwarded & 0xff)).array(), 1);
|
||||
|
||||
// Number of penalties served, 1 byte
|
||||
putInBuffer(ByteBuffer.allocate(1).put((byte) (numberPenaltiesServed & 0xff)).array(), 1);
|
||||
|
||||
// Estimated time at next mark, 6 bytes
|
||||
putInBuffer(Message.intToByteArray((int) estimatedTimeAtNextMark, 6),6);
|
||||
|
||||
// Estimated time at finish, 6 bytes
|
||||
putInBuffer(Message.intToByteArray((int) estimatedTimeAtFinish, 6), 6);
|
||||
|
||||
return buff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package seng302.gameServer.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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
public enum ClientType {
|
||||
SPECTATOR(0x00),
|
||||
PLAYER(0x01),
|
||||
CONTROL_TUTORIAL(0x02),
|
||||
GHOST_MODE(0x03);
|
||||
|
||||
private int type;
|
||||
|
||||
ClientType(int type){
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getCode(){
|
||||
return type;
|
||||
}
|
||||
|
||||
public static ClientType getClientType(int typeCode){
|
||||
switch (typeCode){
|
||||
case 0x00:
|
||||
return SPECTATOR;
|
||||
case 0x01:
|
||||
return PLAYER;
|
||||
case 0x02:
|
||||
return CONTROL_TUTORIAL;
|
||||
case 0x03:
|
||||
return GHOST_MODE;
|
||||
default:
|
||||
return PLAYER;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
// TODO: 14/08/17 ajm412: this may eventually need adjusting due to conforming to the agreed spec.
|
||||
public class CustomizeRequestMessage extends Message {
|
||||
|
||||
|
||||
private static int MESSAGE_LENGTH = 6;
|
||||
|
||||
//Message fields
|
||||
private CustomizeRequestType customizeType;
|
||||
private Integer payloadLength;
|
||||
|
||||
public CustomizeRequestMessage(CustomizeRequestType customizeType, double sourceID,
|
||||
byte[] payload) {
|
||||
payloadLength = payload.length;
|
||||
setHeader(new Header(MessageType.CUSTOMIZATION_REQUEST, 1, (short) getSize()));
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
|
||||
putInt((int) sourceID, 4);
|
||||
putInt((int) customizeType.getType(), 2);
|
||||
putBytes(payload);
|
||||
|
||||
writeCRC();
|
||||
rewind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return MESSAGE_LENGTH + payloadLength; // placeholder
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
// TODO: 14/08/17 ajm412: this may eventually need adjusting due to conforming to the agreed spec.
|
||||
public enum CustomizeRequestType {
|
||||
NAME(0x00),
|
||||
COLOR(0x01),
|
||||
SHAPE(0x02);
|
||||
|
||||
private int type;
|
||||
|
||||
CustomizeRequestType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
int getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public static CustomizeRequestType getRequestType(int typeCode) {
|
||||
switch (typeCode) {
|
||||
case 0x00:
|
||||
return NAME;
|
||||
case 0x01:
|
||||
return COLOR;
|
||||
case 0x02:
|
||||
return SHAPE;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* Created by ajm412 on 14/08/17.
|
||||
*/
|
||||
public class CustomizeResponseMessage extends Message {
|
||||
|
||||
private static int MESSAGE_LENGTH = 2;
|
||||
|
||||
public CustomizeResponseMessage(CustomizeResponseType responseType) {
|
||||
setHeader(new Header(MessageType.CUSTOMIZATION_RESPONSE, 1, (short) getSize()));
|
||||
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
putInt(responseType.getType(), 2);
|
||||
|
||||
writeCRC();
|
||||
rewind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return MESSAGE_LENGTH; // placeholder
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
// TODO: 14/08/17 ajm412: this may eventually need adjusting due to conforming to the agreed spec.
|
||||
public enum CustomizeResponseType {
|
||||
SUCCESS(0x00),
|
||||
FAILURE(0x01),
|
||||
FAILURE_MALFORMED_DATA(0x02),
|
||||
FAILURE_INCOMPATIBLE(0x03);
|
||||
|
||||
private int type;
|
||||
|
||||
CustomizeResponseType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
int getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public static CustomizeResponseType getResponseType(int typeCode) {
|
||||
switch (typeCode) {
|
||||
case 0x00:
|
||||
return SUCCESS;
|
||||
case 0x01:
|
||||
return FAILURE;
|
||||
case 0x02:
|
||||
return FAILURE_MALFORMED_DATA;
|
||||
case 0x03:
|
||||
return FAILURE_INCOMPATIBLE;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
public enum DeviceType {
|
||||
UNKNOWN(0),
|
||||
RACING_YACHT(1);
|
||||
|
||||
private long code;
|
||||
|
||||
DeviceType(long code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public long getCode(){
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class Header {
|
||||
// From API spec
|
||||
private final int syncByte1 = 0x47;
|
||||
private final int syncByte2 = 0x83;
|
||||
|
||||
private MessageType messageType;
|
||||
private int timeStamp;
|
||||
private int sourceId;
|
||||
private short messageLength;
|
||||
private static final int MESSAGE_LEN = 15;
|
||||
private ByteBuffer buff;
|
||||
private int buffPos;
|
||||
|
||||
/**
|
||||
* Message Header from section 3.2 of the AC35 Streaming
|
||||
* Data spec
|
||||
* @param messageType The type of the message following this header
|
||||
* @param sourceId The message source (as defined in the spec)
|
||||
* @param messageLength The length of the message following this header
|
||||
*/
|
||||
public Header(MessageType messageType, int sourceId, Short messageLength){
|
||||
this.messageType = messageType;
|
||||
this.sourceId = sourceId;
|
||||
this.messageLength = messageLength;
|
||||
timeStamp = (int) (System.currentTimeMillis() / 1000L);
|
||||
buff = ByteBuffer.allocate(MESSAGE_LEN);
|
||||
buffPos = 0;
|
||||
}
|
||||
|
||||
private void putInBuffer(byte[] bytes, long val){
|
||||
byte[] tmp = bytes.clone();
|
||||
Message.reverse(tmp);
|
||||
|
||||
buff.put(tmp);
|
||||
buffPos += tmp.length;
|
||||
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);
|
||||
|
||||
putInBuffer(ByteBuffer.allocate(1).put((byte)messageType.getCode()).array(), messageType.getCode());
|
||||
|
||||
putInBuffer(Message.intToByteArray(timeStamp, 6), timeStamp);
|
||||
|
||||
putInBuffer(Message.intToByteArray(sourceId, 4), sourceId);
|
||||
|
||||
putInBuffer(Message.intToByteArray(messageLength, 2), messageLength);
|
||||
|
||||
return buff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of this message
|
||||
* @return the size of the message
|
||||
*/
|
||||
public static Integer getSize(){
|
||||
return MESSAGE_LEN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
public class Heartbeat extends Message {
|
||||
private final int MESSAGE_SIZE = 4;
|
||||
|
||||
/**
|
||||
* Heartbeat from the AC35 Streaming data spec
|
||||
* @param seqNo Increment every time a message is sent
|
||||
*/
|
||||
public Heartbeat(int seqNo){
|
||||
setHeader(new Header(MessageType.HEARTBEAT, 0x01, (short) getSize()));
|
||||
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
putUnsignedInt(seqNo, 4);
|
||||
|
||||
writeCRC();
|
||||
rewind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return MESSAGE_SIZE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
public class MarkRoundingMessage extends Message{
|
||||
private final long MESSAGE_VERSION_NUMBER = 1;
|
||||
private final int MESSAGE_SIZE = 21;
|
||||
|
||||
private long time;
|
||||
private long ackNumber;
|
||||
private long raceId;
|
||||
private long sourceId;
|
||||
private RoundingBoatStatus boatStatus;
|
||||
private RoundingSide roundingSide;
|
||||
private long markId;
|
||||
|
||||
|
||||
/**
|
||||
* This message is sent when a boat passes a mark, start line, or finish line
|
||||
* The purpose of this is to record the time when yachts cross marks
|
||||
* @param ackNumber ackNumber
|
||||
* @param raceId raceId
|
||||
* @param sourceId boatSourceId
|
||||
* @param roundingBoatStatus roundingBoatStatus
|
||||
* @param roundingSide roundingSide
|
||||
* @param markId markId
|
||||
*/
|
||||
public MarkRoundingMessage(int ackNumber, int raceId, int sourceId, RoundingBoatStatus roundingBoatStatus,
|
||||
RoundingSide roundingSide, MarkType markType, int markId) {
|
||||
this.time = System.currentTimeMillis();
|
||||
this.ackNumber = ackNumber;
|
||||
this.raceId = raceId;
|
||||
this.sourceId = sourceId;
|
||||
this.boatStatus = roundingBoatStatus;
|
||||
this.roundingSide = roundingSide;
|
||||
this.markId = markId;
|
||||
|
||||
setHeader(new Header(MessageType.MARK_ROUNDING, 1, (short) getSize()));
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
putByte((byte) MESSAGE_VERSION_NUMBER);
|
||||
putInt((int) time, 6);
|
||||
putInt((int) ackNumber, 2);
|
||||
putInt((int) raceId, 4);
|
||||
putInt((int) sourceId, 4);
|
||||
putByte((byte) boatStatus.getCode());
|
||||
putByte((byte) roundingSide.getCode());
|
||||
putByte((byte) markType.getCode());
|
||||
putByte((byte) markId);
|
||||
|
||||
writeCRC();
|
||||
rewind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return MESSAGE_SIZE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* Types of marks boats can round
|
||||
*/
|
||||
public enum MarkType {
|
||||
UNKNOWN(0),
|
||||
ROUNDING_MARK(1),
|
||||
GATE(2);
|
||||
|
||||
private long code;
|
||||
|
||||
MarkType(long code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public long getCode(){
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
public abstract class Message {
|
||||
private final int CRC_SIZE = 4;
|
||||
private Header header;
|
||||
private ByteBuffer buffer;
|
||||
private int bufferPosition;
|
||||
private CRC32 crc;
|
||||
|
||||
/**
|
||||
* @param header Set the header for this message
|
||||
*/
|
||||
void setHeader(Header header){
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the header specified for this message
|
||||
*/
|
||||
Header getHeader(){
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the size of the message
|
||||
*/
|
||||
public abstract int getSize();
|
||||
|
||||
/**
|
||||
* Allocate byte buffer to correct size
|
||||
*/
|
||||
void allocateBuffer(){
|
||||
buffer = ByteBuffer.allocate(Header.getSize() + getSize() + CRC_SIZE);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bufferPosition = 0;
|
||||
buffer.position(bufferPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the set header to the byte buffer
|
||||
*/
|
||||
void writeHeaderToBuffer(){
|
||||
buffer.put(getHeader().getByteBuffer().array());
|
||||
bufferPosition += Header.getSize();
|
||||
buffer.position(bufferPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the buffer position by n bytes
|
||||
* @param size Number of bytes to move the buffer by
|
||||
*/
|
||||
private void moveBufferPositionBy(int size){
|
||||
bufferPosition += size;
|
||||
buffer.position(bufferPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an unsigned byte in the buffer
|
||||
*/
|
||||
void putUnsignedByte(byte b){
|
||||
buffer.put(ByteBuffer.allocate(1).put((byte) (b & 0xff)).array());
|
||||
moveBufferPositionBy(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an signed byte in the buffer
|
||||
*/
|
||||
void putByte(byte b){
|
||||
buffer.put(ByteBuffer.allocate(1).put(b).array());
|
||||
moveBufferPositionBy(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Place an unsigned integer of the specified length in the buffer
|
||||
* @param val The integer value to add (Note: This must be long due to java not supporting unsigned integers)
|
||||
* @param size The size of the int to be added to the buffer
|
||||
*/
|
||||
void putUnsignedInt(long val, int size){
|
||||
if (size <= 1){
|
||||
putUnsignedByte((byte) val);
|
||||
|
||||
}
|
||||
else if (size < 4){
|
||||
// Use short
|
||||
byte[] tmp = Message.intToByteArray(val, size); //ByteBuffer.allocate(size).putShort((short) (val & 0xffff)).array();
|
||||
reverse(tmp);
|
||||
buffer.put(tmp);
|
||||
moveBufferPositionBy(size);
|
||||
}
|
||||
else{
|
||||
// Use int
|
||||
byte[] tmp = Message.intToByteArray(val, size);
|
||||
reverse(tmp);
|
||||
moveBufferPositionBy(size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a signed int of a specified length in the buffer
|
||||
* @param val The integer value to add
|
||||
* @param size The size of the integer to be added to the buffer
|
||||
*/
|
||||
void putInt(long val, int size){
|
||||
if (size < 4){
|
||||
byte[] tmp = Message.intToByteArray(val, size);
|
||||
reverse(tmp);
|
||||
buffer.put(tmp);
|
||||
}
|
||||
else{
|
||||
byte[] tmp = Message.intToByteArray(val, size);
|
||||
reverse(tmp);
|
||||
buffer.put(tmp);
|
||||
}
|
||||
moveBufferPositionBy(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an array of bytes to the buffer
|
||||
* @param bytes to write
|
||||
*/
|
||||
void putBytes(byte[] bytes){
|
||||
buffer.put(bytes);
|
||||
moveBufferPositionBy(bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a ByteBuffer of bytes to the buffer
|
||||
* @param bytes to write
|
||||
* @param size number of bytes
|
||||
*/
|
||||
void putBytes(ByteBuffer bytes, int size){
|
||||
buffer.put(bytes.array());
|
||||
moveBufferPositionBy(size);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the CRC of the buffer and append it to the end of the buffer
|
||||
*/
|
||||
void writeCRC(){
|
||||
crc = new CRC32();
|
||||
|
||||
buffer.position(0);
|
||||
|
||||
byte[] data = Arrays.copyOfRange(buffer.array(), 0, buffer.array().length-CRC_SIZE);
|
||||
crc.update(data);
|
||||
buffer.position(bufferPosition);
|
||||
|
||||
putInt((int) crc.getValue(), CRC_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current buffer as a byte array
|
||||
*/
|
||||
public byte[] getBuffer(){
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the buffer to the beginning
|
||||
*/
|
||||
void rewind(){
|
||||
buffer.flip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an integer to an array of bytes
|
||||
* @param val The value to add
|
||||
* @param len The width of the integer in the buffer
|
||||
* @return A byte array to be sent
|
||||
*/
|
||||
public static byte[] intToByteArray(long val, int len){
|
||||
int index = 0;
|
||||
byte[] data = new byte[len];
|
||||
|
||||
for (int i = 0; i < len; i++){
|
||||
data[len - index - 1] = (byte) (val & 0xFF);
|
||||
val >>>= 8;
|
||||
index++;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* takes an array of up to 7 bytes in little endian format and
|
||||
* returns a positive long constructed from the input bytes
|
||||
*
|
||||
* @param bytes the bytes to be converted to long
|
||||
* @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
|
||||
*/
|
||||
public static void reverse(byte[] data) {
|
||||
for (int left = 0, right = data.length - 1; left < right; left++, right--) {
|
||||
byte temp = (byte) (data[left] & 0xff);
|
||||
data[left] = (byte) (data[right] & 0xff);
|
||||
data[right] = (byte) (temp & 0xff);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* Enum containing the types of messages
|
||||
* sent by the server
|
||||
*/
|
||||
public enum MessageType {
|
||||
HEARTBEAT(1),
|
||||
RACE_STATUS(12),
|
||||
DISPLAY_TEXT_MESSAGE(20),
|
||||
XML_MESSAGE(26),
|
||||
RACE_START_STATUS(27),
|
||||
YACHT_EVENT_CODE(29),
|
||||
YACHT_ACTION_CODE(31),
|
||||
CHATTER_TEXT(36),
|
||||
BOAT_LOCATION(37),
|
||||
MARK_ROUNDING(38),
|
||||
COURSE_WIND(44),
|
||||
AVERAGE_WIND(47),
|
||||
BOAT_ACTION(100),
|
||||
REGISTRATION_REQUEST(101),
|
||||
REGISTRATION_RESPONSE(102),
|
||||
CUSTOMIZATION_REQUEST(103),
|
||||
CUSTOMIZATION_RESPONSE(104);
|
||||
|
||||
|
||||
private int code;
|
||||
|
||||
MessageType(int code){
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message code (From the API Spec)
|
||||
* @return the message code
|
||||
*/
|
||||
int getCode(){
|
||||
return this.code;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* The types of race start status messages
|
||||
*/
|
||||
public enum RaceStartNotificationType {
|
||||
SET_RACE_START_TIME(1),
|
||||
RACE_POSTPONED(2),
|
||||
RACE_ABANDONED(3),
|
||||
RACE_TERMINATED(4);
|
||||
|
||||
private final long type;
|
||||
|
||||
RaceStartNotificationType(long type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
long getType(){
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
public class RaceStartStatusMessage extends Message {
|
||||
private final int MESSAGE_SIZE = 20;
|
||||
|
||||
private long version;
|
||||
private long timeStamp;
|
||||
private long ackNumber;
|
||||
private long raceStartTime;
|
||||
private long raceId;
|
||||
private RaceStartNotificationType notificationType;
|
||||
|
||||
/**
|
||||
* Message sent to clients with the expected start time of the race
|
||||
* @param ackNumber Sequence number of message.
|
||||
* @param raceStartTime Expected race start time
|
||||
* @param raceId Race ID#
|
||||
* @param notificationType Type of this notification
|
||||
*/
|
||||
public RaceStartStatusMessage(long ackNumber, long raceStartTime, long raceId, RaceStartNotificationType notificationType){
|
||||
this.version = 1;
|
||||
this.timeStamp = System.currentTimeMillis() / 1000L;
|
||||
this.ackNumber = ackNumber;
|
||||
this.raceStartTime = raceStartTime;
|
||||
this.notificationType = notificationType;
|
||||
this.raceId = raceId;
|
||||
|
||||
setHeader(new Header(MessageType.RACE_START_STATUS, 1, (short) getSize()));
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
putUnsignedByte((byte) version);
|
||||
putInt((int) timeStamp, 6);
|
||||
putInt((int) ackNumber, 2);
|
||||
putInt((int) raceStartTime, 6);
|
||||
putInt((int) raceId, 4);
|
||||
putUnsignedByte((byte) notificationType.getType());
|
||||
|
||||
writeCRC();
|
||||
rewind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return MESSAGE_SIZE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* The current status of the race
|
||||
*/
|
||||
public enum RaceStatus {
|
||||
NOTACTIVE(0),
|
||||
WARNING(1), // Between 3:00 and 1:00 before start
|
||||
PREPARATORY(2), // Less than 1:00 before start
|
||||
STARTED(3),
|
||||
ABANDONED(6),
|
||||
POSTPONED(7),
|
||||
TERMINATED(8),
|
||||
RACE_START_TIME_NOT_SET(9),
|
||||
PRESTART(10); // More than 3:00 before start
|
||||
|
||||
private int code;
|
||||
|
||||
RaceStatus(int code){
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode(){
|
||||
return this.code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
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 double raceWindDirection;
|
||||
private long windSpeed;
|
||||
private long numBoatsInRace;
|
||||
private RaceType raceType;
|
||||
private List<BoatSubMessage> boats;
|
||||
private CRC32 crc;
|
||||
|
||||
/**
|
||||
* A message containing the current status of the race
|
||||
* @param raceId The ID of the current race
|
||||
* @param raceStatus The status of the race
|
||||
* @param expectedStartTime The expected start time
|
||||
* @param raceWindDirection The wind direction (north, east, south)
|
||||
* @param windSpeed The wind speed in mm/sec
|
||||
* @param numBoatsInRace The number of boats in the race
|
||||
* @param raceType The race type (Match/fleet)
|
||||
* @param sourceId The source of this message
|
||||
* @param boats A list of boat status sub messages
|
||||
*/
|
||||
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 * windDirFactor+100.0;
|
||||
this.windSpeed = windSpeed;
|
||||
this.numBoatsInRace = numBoatsInRace;
|
||||
this.raceType = raceType;
|
||||
this.boats = boats;
|
||||
crc = new CRC32();
|
||||
|
||||
setHeader(new Header(MESSAGE_TYPE, (int) sourceId, (short) getSize()));
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
putByte((byte) MESSAGE_VERSION);
|
||||
putInt(currentTime, 6);
|
||||
putInt((int) raceId, 4);
|
||||
putByte((byte) raceStatus.getCode());
|
||||
putInt(expectedStartTime, 6);
|
||||
putInt((int) this.raceWindDirection, 2);
|
||||
putInt((int) windSpeed, 2);
|
||||
putByte((byte) numBoatsInRace);
|
||||
putByte((byte) raceType.getCode());
|
||||
|
||||
for (BoatSubMessage boatSubMessage : boats){
|
||||
putBytes(boatSubMessage.getByteBuffer(), boatSubMessage.getSize());
|
||||
}
|
||||
|
||||
writeCRC();
|
||||
rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the size of this message in bytes
|
||||
*/
|
||||
@Override
|
||||
public int getSize() {
|
||||
return MESSAGE_BASE_SIZE + (20 * ((int) numBoatsInRace));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* Enum containing the types of races
|
||||
* sent by the server
|
||||
*/
|
||||
public enum RaceType {
|
||||
MATCH_RACE(1),
|
||||
FLEET_RACE(2);
|
||||
|
||||
private long code;
|
||||
|
||||
RaceType(long code){
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public long getCode(){
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
|
||||
public class RegistrationRequestMessage extends Message {
|
||||
private static int MESSAGE_LENGTH = 2;
|
||||
|
||||
public RegistrationRequestMessage(ClientType type){
|
||||
setHeader(new Header(MessageType.REGISTRATION_REQUEST, 1, (short) getSize()));
|
||||
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
putInt(type.getCode(), 2);
|
||||
|
||||
writeCRC();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return MESSAGE_LENGTH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
public class RegistrationResponseMessage extends Message{
|
||||
|
||||
public RegistrationResponseMessage(int clientSourceID, RegistrationResponseStatus status){
|
||||
setHeader(new Header(MessageType.REGISTRATION_RESPONSE, 1, (short) getSize()));
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
putInt(clientSourceID, 4);
|
||||
putInt(status.getCode(), 1);
|
||||
|
||||
writeCRC();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
public enum RegistrationResponseStatus {
|
||||
SUCCESS_SPECTATING(0x00),
|
||||
SUCCESS_PLAYING(0x01),
|
||||
SUCCESS_TUTORIAL(0x02),
|
||||
SUCCESS_GHOSTING(0x03),
|
||||
|
||||
FAILURE_GENERAL(0x10),
|
||||
FAILURE_FULL(0x11);
|
||||
|
||||
private int code;
|
||||
|
||||
RegistrationResponseStatus(int code){
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message code (From the API Spec)
|
||||
* @return the message code
|
||||
*/
|
||||
int getCode(){
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public static RegistrationResponseStatus getResponseStatus(int typeCode){
|
||||
switch (typeCode){
|
||||
case 0x00:
|
||||
return SUCCESS_SPECTATING;
|
||||
case 0x01:
|
||||
return SUCCESS_PLAYING;
|
||||
case 0x02:
|
||||
return SUCCESS_TUTORIAL;
|
||||
case 0x03:
|
||||
return SUCCESS_GHOSTING;
|
||||
case 0x10:
|
||||
return FAILURE_GENERAL;
|
||||
case 0x11:
|
||||
return FAILURE_FULL;
|
||||
default:
|
||||
return FAILURE_GENERAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* The status of a boat rounding a mark
|
||||
*/
|
||||
public enum RoundingBoatStatus {
|
||||
UNKNOWN(0),
|
||||
RACING(1),
|
||||
DSQ(2),
|
||||
WITHDRAWN(3);
|
||||
|
||||
private long code;
|
||||
|
||||
RoundingBoatStatus(long code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public long getCode(){
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* The side the boat rounded the mark
|
||||
*/
|
||||
public enum RoundingSide {
|
||||
UNKNOWN(0, "Unknown"),
|
||||
PORT(1, "Port"),
|
||||
STARBOARD(2, "Stbd"),
|
||||
SP(3, "SP"),
|
||||
PS(4, "PS");
|
||||
|
||||
|
||||
private long code;
|
||||
private String name;
|
||||
|
||||
RoundingSide(long code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public long getCode(){
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public static RoundingSide getRoundingSide(String identifier) {
|
||||
RoundingSide roundingSide = UNKNOWN;
|
||||
switch (identifier) {
|
||||
case "Unknown":
|
||||
roundingSide = UNKNOWN;
|
||||
break;
|
||||
case "Port":
|
||||
roundingSide = PORT;
|
||||
break;
|
||||
case "Stbd":
|
||||
roundingSide = STARBOARD;
|
||||
break;
|
||||
case "SP":
|
||||
roundingSide = SP;
|
||||
break;
|
||||
case "PS":
|
||||
roundingSide = PS;
|
||||
break;
|
||||
}
|
||||
|
||||
return roundingSide;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
public class XMLMessage extends Message{
|
||||
private final MessageType MESSAGE_TYPE = MessageType.XML_MESSAGE;
|
||||
private final int MESSAGE_VERSION = 1; //Always set to 1
|
||||
private final int MESSAGE_SIZE = 14;
|
||||
|
||||
// Message fields
|
||||
private long timeStamp;
|
||||
private long ack = 0x00; //Unused
|
||||
private XMLMessageSubType xmlMessageSubType;
|
||||
private long length;
|
||||
private long sequence;
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* XML Message from the AC35 Streaming data spec
|
||||
* @param content The XML content
|
||||
* @param type The XML Message Sub Type
|
||||
* @param sequenceNum sequenceNum
|
||||
*/
|
||||
public XMLMessage(String content, XMLMessageSubType type, long sequenceNum){
|
||||
this.content = content;
|
||||
this.xmlMessageSubType = type;
|
||||
timeStamp = System.currentTimeMillis() / 1000L;
|
||||
ack = 0;
|
||||
length = this.content.length();
|
||||
sequence = sequenceNum;
|
||||
|
||||
setHeader(new Header(MESSAGE_TYPE, 0x01, (short) getSize()));
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
// Write message fields
|
||||
putUnsignedByte((byte) MESSAGE_VERSION);
|
||||
putInt((int) ack, 2);
|
||||
putInt((int) timeStamp, 6);
|
||||
putByte((byte)xmlMessageSubType.getType());
|
||||
putInt((int) sequence, 2);
|
||||
putInt((int) length, 2);
|
||||
putBytes(content.getBytes());
|
||||
|
||||
writeCRC();
|
||||
rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The length of this message
|
||||
*/
|
||||
public int getSize(){
|
||||
return MESSAGE_SIZE + content.length();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* Enum containing the types of XML messages
|
||||
*/
|
||||
public enum XMLMessageSubType {
|
||||
REGATTA(5),
|
||||
RACE(6),
|
||||
BOAT(7);
|
||||
|
||||
private int type;
|
||||
|
||||
XMLMessageSubType(int type){
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getType(){
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package seng302.gameServer.messages;
|
||||
|
||||
/**
|
||||
* Created by zyt10 on 10/08/17.
|
||||
*/
|
||||
public class YachtEventCodeMessage extends Message {
|
||||
|
||||
private final MessageType MESSAGE_TYPE = MessageType.YACHT_EVENT_CODE;
|
||||
private final int MESSAGE_VERSION = 1; //Always set to 1
|
||||
private final int MESSAGE_SIZE = 22;
|
||||
|
||||
// Message fields
|
||||
private long timeStamp;
|
||||
private long ack = 0x00; //Unused
|
||||
private int raceId;
|
||||
private int destSourceId;
|
||||
private int incidentId;
|
||||
private int eventId;
|
||||
|
||||
|
||||
public YachtEventCodeMessage(Integer subjectId) {
|
||||
timeStamp = System.currentTimeMillis() / 1000L;
|
||||
ack = 0;
|
||||
raceId = 1;
|
||||
destSourceId = subjectId; // collision boat source id
|
||||
incidentId = 0;
|
||||
eventId = 33;
|
||||
|
||||
setHeader(new Header(MESSAGE_TYPE, 0x01, (short) getSize()));
|
||||
allocateBuffer();
|
||||
writeHeaderToBuffer();
|
||||
|
||||
// Write message fields
|
||||
putUnsignedByte((byte) MESSAGE_VERSION);
|
||||
putInt((int) timeStamp, 6);
|
||||
putInt((int) ack, 2);
|
||||
putInt((int) raceId, 4);
|
||||
putInt((int) destSourceId, 4);
|
||||
putInt((int) incidentId, 4);
|
||||
putInt((int) eventId, 1);
|
||||
|
||||
writeCRC();
|
||||
rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The length of this message
|
||||
*/
|
||||
public int getSize() {
|
||||
return MESSAGE_SIZE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package seng302.model;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Observable;
|
||||
import java.util.Observer;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
||||
import javafx.beans.property.ReadOnlyLongProperty;
|
||||
import javafx.beans.property.ReadOnlyLongWrapper;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import seng302.model.mark.CompoundMark;
|
||||
|
||||
/**
|
||||
* Yacht class for the racing boat. <p> Class created to store more variables (eg. boat statuses)
|
||||
* compared to the XMLParser boat class, also done outside Boat class because some old variables are
|
||||
* not used anymore.
|
||||
*/
|
||||
public class ClientYacht extends Observable {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface YachtLocationListener {
|
||||
void notifyLocation(ClientYacht clientYacht, double lat, double lon, double heading,
|
||||
Boolean sailsIn, double velocity);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface MarkRoundingListener {
|
||||
void notifyRounding(ClientYacht yacht, CompoundMark markPassed, int legNumber);
|
||||
}
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(ClientYacht.class);
|
||||
|
||||
|
||||
private String boatType;
|
||||
private Integer sourceId;
|
||||
private String hullID; //matches HullNum in the XML spec.
|
||||
private String shortName;
|
||||
private String boatName;
|
||||
private String country;
|
||||
private Integer position;
|
||||
|
||||
private Long estimateTimeAtFinish;
|
||||
private Boolean sailIn = true;
|
||||
private Integer currentMarkSeqID = 0;
|
||||
private Long markRoundTime;
|
||||
private Long timeTillNext;
|
||||
private Double heading;
|
||||
private Integer legNumber = 0;
|
||||
private GeoPoint location;
|
||||
private Integer boatStatus;
|
||||
private Double currentVelocity;
|
||||
|
||||
private List<YachtLocationListener> locationListeners = new ArrayList<>();
|
||||
private List<MarkRoundingListener> markRoundingListeners = new ArrayList<>();
|
||||
private ReadOnlyDoubleWrapper velocityProperty = new ReadOnlyDoubleWrapper();
|
||||
private ReadOnlyLongWrapper timeTillNextProperty = new ReadOnlyLongWrapper();
|
||||
private ReadOnlyLongWrapper timeSinceLastMarkProperty = new ReadOnlyLongWrapper();
|
||||
private ReadOnlyIntegerWrapper placingProperty = new ReadOnlyIntegerWrapper();
|
||||
private CompoundMark lastMarkRounded;
|
||||
private Color colour;
|
||||
|
||||
public ClientYacht(String boatType, Integer sourceId, String hullID, String shortName,
|
||||
String boatName, String country) {
|
||||
this.boatType = boatType;
|
||||
this.sourceId = sourceId;
|
||||
this.hullID = hullID;
|
||||
this.shortName = shortName;
|
||||
this.boatName = boatName;
|
||||
this.country = country;
|
||||
this.location = new GeoPoint(57.670341, 11.826856);
|
||||
this.heading = 120.0; //In degrees
|
||||
this.currentVelocity = 0d;
|
||||
this.boatStatus = 1;
|
||||
this.colour = Color.rgb(0, 0, 0, 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ServerToClientThread as the observer, this observer pattern mainly server for the boat
|
||||
* rounding package.
|
||||
*/
|
||||
@Override
|
||||
public void addObserver(Observer o) {
|
||||
super.addObserver(o);
|
||||
}
|
||||
|
||||
public String getBoatType() {
|
||||
return boatType;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Integer getBoatStatus() {
|
||||
return boatStatus;
|
||||
}
|
||||
|
||||
public void setBoatStatus(Integer boatStatus) {
|
||||
this.boatStatus = boatStatus;
|
||||
}
|
||||
|
||||
public Integer getLegNumber() {
|
||||
return legNumber;
|
||||
}
|
||||
|
||||
public void setLegNumber(Integer legNumber) {
|
||||
this.legNumber = legNumber;
|
||||
}
|
||||
|
||||
public void setEstimateTimeTillNextMark(Long estimateTimeTillNextMark) {
|
||||
timeTillNext = estimateTimeTillNextMark;
|
||||
}
|
||||
|
||||
public String getEstimateTimeAtFinish() {
|
||||
DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
|
||||
return format.format(estimateTimeAtFinish);
|
||||
}
|
||||
|
||||
public void setEstimateTimeAtFinish(Long estimateTimeAtFinish) {
|
||||
this.estimateTimeAtFinish = estimateTimeAtFinish;
|
||||
}
|
||||
|
||||
public Integer getPlacing() {
|
||||
return placingProperty.get();
|
||||
}
|
||||
|
||||
public void setPlacing(Integer position) {
|
||||
placingProperty.set(position);
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty placingProperty() {
|
||||
return placingProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public void updateVelocityProperty(double velocity) {
|
||||
this.velocityProperty.set(velocity);
|
||||
}
|
||||
|
||||
public void setMarkRoundingTime(Long markRoundingTime) {
|
||||
this.markRoundTime = markRoundingTime;
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty getVelocityProperty() {
|
||||
return velocityProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public ReadOnlyLongProperty timeTillNextProperty() {
|
||||
return timeTillNextProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public Long getTimeTillNext() {
|
||||
return timeTillNext;
|
||||
}
|
||||
|
||||
public Long getMarkRoundTime() {
|
||||
return markRoundTime;
|
||||
}
|
||||
|
||||
public CompoundMark getLastMarkRounded() {
|
||||
return lastMarkRounded;
|
||||
}
|
||||
|
||||
public void setLastMarkRounded(CompoundMark lastMarkRounded) {
|
||||
this.lastMarkRounded = lastMarkRounded;
|
||||
}
|
||||
|
||||
public GeoPoint getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public Integer getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(Integer position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public void toggleSail() {
|
||||
sailIn = !sailIn;
|
||||
}
|
||||
//// TODO: 15/08/17 asd
|
||||
|
||||
/**
|
||||
* Sets the current location of the boat in lat and long whilst preserving the last location
|
||||
*
|
||||
* @param lat Latitude
|
||||
* @param lng Longitude
|
||||
*/
|
||||
public void setLocation(Double lat, Double lng) {
|
||||
location.setLat(lat);
|
||||
location.setLng(lng);
|
||||
}
|
||||
|
||||
public Double getHeading() {
|
||||
return heading;
|
||||
}
|
||||
|
||||
public void setHeading(Double heading) {
|
||||
this.heading = heading;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return boatName;
|
||||
}
|
||||
|
||||
public void updateTimeSinceLastMarkProperty(long timeSinceLastMark) {
|
||||
this.timeSinceLastMarkProperty.set(timeSinceLastMark);
|
||||
}
|
||||
|
||||
public ReadOnlyLongProperty timeSinceLastMarkProperty() {
|
||||
return timeSinceLastMarkProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public void setTimeTillNext(Long timeTillNext) {
|
||||
this.timeTillNext = timeTillNext;
|
||||
}
|
||||
|
||||
|
||||
public Color getColour() {
|
||||
return colour;
|
||||
}
|
||||
|
||||
public void setColour(Color colour) {
|
||||
this.colour = colour;
|
||||
}
|
||||
|
||||
|
||||
public void updateLocation(double lat, double lng, double heading, double velocity) {
|
||||
setLocation(lat, lng);
|
||||
this.heading = heading;
|
||||
// this.currentVelocity = velocity;
|
||||
updateVelocityProperty(velocity);
|
||||
for (YachtLocationListener yll : locationListeners) {
|
||||
yll.notifyLocation(this, lat, lng, heading, sailIn, velocity);
|
||||
}
|
||||
}
|
||||
|
||||
public void addLocationListener(YachtLocationListener listener) {
|
||||
locationListeners.add(listener);
|
||||
}
|
||||
|
||||
public void addMarkRoundingListener(MarkRoundingListener listener) {
|
||||
markRoundingListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeMarkRoundingListener(MarkRoundingListener listener) {
|
||||
markRoundingListeners.remove(listener);
|
||||
}
|
||||
|
||||
public boolean getSailIn () {
|
||||
return sailIn;
|
||||
}
|
||||
|
||||
public void roundMark(CompoundMark mark, long markRoundTime, long timeSinceLastMark) {
|
||||
this.markRoundTime = markRoundTime;
|
||||
timeSinceLastMarkProperty.set(timeSinceLastMark);
|
||||
lastMarkRounded = mark;
|
||||
legNumber++;
|
||||
for (MarkRoundingListener listener : markRoundingListeners) {
|
||||
listener.notifyRounding(this, lastMarkRounded, legNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package seng302.model;
|
||||
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Enum for generating colours.
|
||||
*/
|
||||
public enum Colors {
|
||||
RED, PERU, GOLD, GREEN, BLUE, PURPLE, DEEPPINK, GRAY;
|
||||
|
||||
public static Color getColor(Integer index) {
|
||||
return Color.valueOf(values()[index].toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package seng302.model;
|
||||
|
||||
/**
|
||||
* A class represent Geo location (latitude, lnggitude).
|
||||
* Created by Haoming on 15/5/2017
|
||||
*/
|
||||
public class GeoPoint {
|
||||
|
||||
private double lat, lng;
|
||||
|
||||
public GeoPoint(double lat, double lng) {
|
||||
this.lat = lat;
|
||||
this.lng = lng;
|
||||
}
|
||||
|
||||
public double getLat() {
|
||||
return lat;
|
||||
}
|
||||
|
||||
public void setLat(double lat) {
|
||||
this.lat = lat;
|
||||
}
|
||||
|
||||
public double getLng() {
|
||||
return lng;
|
||||
}
|
||||
|
||||
public void setLng(double lng) {
|
||||
this.lng = lng;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "lat: " + lat + " lng: " + lng;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package seng302.model;
|
||||
|
||||
/**
|
||||
* Stores data on the border of a race
|
||||
*/
|
||||
public class Limit extends GeoPoint {
|
||||
|
||||
private Integer seqID;
|
||||
|
||||
public Limit(Integer seqID, Double lat, Double lng) {
|
||||
super(lat, lng);
|
||||
this.seqID = seqID;
|
||||
}
|
||||
|
||||
public Integer getSeqID() {
|
||||
return seqID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package seng302.model;
|
||||
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* 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 ServerYacht yacht;
|
||||
private Integer lastMarkPassed;
|
||||
|
||||
|
||||
public Player(Socket socket, ServerYacht 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 ServerYacht 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,182 @@
|
||||
package seng302.model;
|
||||
|
||||
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
|
||||
* @param polarFile polarFile to be parsed
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package seng302.model;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Observable;
|
||||
import java.util.TimeZone;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import seng302.model.stream.parser.RaceStartData;
|
||||
import seng302.model.stream.parser.RaceStatusData;
|
||||
|
||||
/**
|
||||
* Class for storing race data that does not relate to specific vessels or marks such as time or wind.
|
||||
* Calculates the state of critical race attributes when relevant data is added.
|
||||
*/
|
||||
public class RaceState {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CollisionListener {
|
||||
void notifyCollision(GeoPoint location);
|
||||
}
|
||||
|
||||
// private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
|
||||
private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("HH:mm:ss");
|
||||
private ReadOnlyDoubleWrapper windSpeed = new ReadOnlyDoubleWrapper();
|
||||
private ReadOnlyDoubleWrapper windDirection = new ReadOnlyDoubleWrapper();
|
||||
private long serverSystemTime;
|
||||
private long expectedStartTime;
|
||||
private boolean isRaceStarted = false;
|
||||
long timeTillStart;
|
||||
private ObservableList<ClientYacht> playerPositions;
|
||||
private List<ClientYacht> collisions = new ArrayList<>();
|
||||
private List<CollisionListener> collisionListeners = new ArrayList<>();
|
||||
|
||||
public RaceState() {
|
||||
playerPositions = FXCollections.observableArrayList();
|
||||
}
|
||||
|
||||
public void updateState (RaceStatusData data) {
|
||||
this.windSpeed.set(data.getWindSpeed());
|
||||
this.windDirection.set(data.getWindDirection());
|
||||
this.serverSystemTime = data.getCurrentTime();
|
||||
this.expectedStartTime = data.getExpectedStartTime();
|
||||
this.isRaceStarted = data.isRaceStarted();
|
||||
}
|
||||
|
||||
public void setTimeZone (TimeZone timeZone) {
|
||||
DATE_TIME_FORMAT.setTimeZone(timeZone);
|
||||
}
|
||||
|
||||
public void updateState (RaceStartData data) {
|
||||
this.timeTillStart = data.getRaceStartTime();
|
||||
}
|
||||
|
||||
public String getRaceTimeStr () {
|
||||
long raceTime = serverSystemTime - expectedStartTime;
|
||||
if (raceTime < 0) {
|
||||
return "-" + DATE_TIME_FORMAT.format(-1 * (raceTime - 1000));
|
||||
} else {
|
||||
return DATE_TIME_FORMAT.format(serverSystemTime - expectedStartTime);
|
||||
}
|
||||
}
|
||||
|
||||
public long getTimeTillStart () {
|
||||
return (expectedStartTime - serverSystemTime);
|
||||
}
|
||||
|
||||
public double getWindSpeed() {
|
||||
return windSpeed.doubleValue();
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty windSpeedProperty() {
|
||||
return windSpeed.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty windDirectionProperty() {
|
||||
return windDirection.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public long getRaceTime() {
|
||||
return serverSystemTime;
|
||||
}
|
||||
|
||||
public boolean isRaceStarted () {
|
||||
return isRaceStarted;
|
||||
}
|
||||
|
||||
public void setBoats(Collection<ClientYacht> clientYachts) {
|
||||
playerPositions.setAll(clientYachts);
|
||||
}
|
||||
|
||||
public void sortPlayers() {
|
||||
playerPositions.sort((yacht1, yacht2) -> Integer.compare(yacht2.getLegNumber(),
|
||||
yacht1.getLegNumber()));
|
||||
}
|
||||
|
||||
public ObservableList<ClientYacht> getPlayerPositions() {
|
||||
return playerPositions;
|
||||
}
|
||||
|
||||
public void storeCollision(ClientYacht yacht) {
|
||||
collisions.add(yacht);
|
||||
for (CollisionListener collisionListener : collisionListeners) {
|
||||
collisionListener.notifyCollision(yacht.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
public void addCollisionListener(CollisionListener collisionListener) {
|
||||
collisionListeners.add(collisionListener);
|
||||
}
|
||||
|
||||
public void removeCollisionListener(CollisionListener collisionListener) {
|
||||
collisionListeners.remove(collisionListener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
package seng302.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Observable;
|
||||
import java.util.Observer;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import seng302.gameServer.GameState;
|
||||
import seng302.gameServer.messages.BoatStatus;
|
||||
import seng302.model.mark.Mark;
|
||||
import seng302.utilities.GeoUtility;
|
||||
|
||||
/**
|
||||
* Yacht class for the racing boat. <p> Class created to store more variables (eg. boat statuses)
|
||||
* compared to the XMLParser boat class, also done outside Boat class because some old variables are
|
||||
* not used anymore.
|
||||
*/
|
||||
public class ServerYacht extends Observable {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(ClientYacht.class);
|
||||
|
||||
public static final Double TURN_STEP = 5.0;
|
||||
|
||||
//Boat info
|
||||
private String boatType;
|
||||
private Integer sourceId;
|
||||
private String hullID; //matches HullNum in the XML spec.
|
||||
private String shortName;
|
||||
private String boatName;
|
||||
private String country;
|
||||
private BoatStatus boatStatus;
|
||||
|
||||
private Color boatColor;
|
||||
|
||||
|
||||
//Location
|
||||
private Double lastHeading;
|
||||
private Boolean sailIn;
|
||||
private Double heading;
|
||||
private GeoPoint lastLocation;
|
||||
private GeoPoint location;
|
||||
private Double currentVelocity;
|
||||
private Boolean isAuto;
|
||||
private Double autoHeading;
|
||||
private Integer legNumber;
|
||||
|
||||
//Mark Rounding
|
||||
private Integer currentMarkSeqID;
|
||||
private Boolean hasEnteredRoundingZone;
|
||||
private Mark closestCurrentMark;
|
||||
private Boolean hasPassedLine;
|
||||
private Boolean hasPassedThroughGate;
|
||||
|
||||
|
||||
public ServerYacht(String boatType, Integer sourceId, String hullID, String shortName,
|
||||
String boatName, String country) {
|
||||
this.boatType = boatType;
|
||||
this.boatStatus = BoatStatus.PRESTART;
|
||||
this.sourceId = sourceId;
|
||||
this.hullID = hullID;
|
||||
this.shortName = shortName;
|
||||
this.boatName = boatName;
|
||||
this.country = country;
|
||||
this.sailIn = false;
|
||||
this.isAuto = false;
|
||||
this.location = new GeoPoint(57.67046, 11.83751);
|
||||
this.lastLocation = location;
|
||||
this.heading = 120.0; //In degrees
|
||||
this.currentVelocity = 0d; //in mms-1
|
||||
this.currentMarkSeqID = 0;
|
||||
this.legNumber = 0;
|
||||
this.boatColor = Colors.getColor(sourceId - 1);
|
||||
|
||||
this.hasEnteredRoundingZone = false;
|
||||
this.hasPassedLine = false;
|
||||
this.hasPassedThroughGate = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Changes the boats current currentVelocity by a set amount, positive or negative
|
||||
*
|
||||
* @param velocityChange The ammount to change the currentVelocity by, in mms-1
|
||||
*/
|
||||
public void changeVelocity(Double velocityChange) {
|
||||
currentVelocity += velocityChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the boat to a new GeoPoint whilst preserving the last location
|
||||
*
|
||||
* @param secondsElapsed The seconds elapsed since the last update of this yacht
|
||||
*/
|
||||
public void updateLocation(Double secondsElapsed) {
|
||||
lastLocation = location;
|
||||
location = GeoUtility.getGeoCoordinate(location, heading, currentVelocity * secondsElapsed);
|
||||
}
|
||||
|
||||
public void setLocation(GeoPoint geoPoint) {
|
||||
location = geoPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ServerToClientThread as the observer, this observer pattern mainly server for the boat
|
||||
* rounding package.
|
||||
*/
|
||||
@Override
|
||||
public void addObserver(Observer o) {
|
||||
super.addObserver(o);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the heading of the boat by a given amount, while recording the boats last heading.
|
||||
*
|
||||
* @param amount the amount by which to adjust the boat heading.
|
||||
*/
|
||||
public void adjustHeading(Double amount) {
|
||||
Double newVal = heading + amount;
|
||||
lastHeading = heading;
|
||||
heading = (double) Math.floorMod(newVal.longValue(), 360L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the boats direction from one side of the wind to the other.
|
||||
*/
|
||||
public void tackGybe(Double windDirection) {
|
||||
if (isAuto) {
|
||||
disableAutoPilot();
|
||||
} else {
|
||||
Double normalizedHeading = normalizeHeading();
|
||||
Double newVal = (-2 * normalizedHeading) + heading;
|
||||
Double newHeading = (double) Math.floorMod(newVal.longValue(), 360L);
|
||||
setAutoPilot(newHeading);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the boats auto pilot feature, which will move the boat towards a given heading.
|
||||
*
|
||||
* @param thisHeading The heading to move the boat towards.
|
||||
*/
|
||||
private void setAutoPilot(Double thisHeading) {
|
||||
isAuto = true;
|
||||
autoHeading = thisHeading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the auto pilot function.
|
||||
*/
|
||||
public void disableAutoPilot() {
|
||||
isAuto = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the boat towards the given heading when the auto pilot was set. Disables the auto pilot
|
||||
* in the event that the boat is within the range of 1 turn step of its goal.
|
||||
*/
|
||||
public void runAutoPilot() {
|
||||
if (isAuto) {
|
||||
turnTowardsHeading(autoHeading);
|
||||
if (Math.abs(heading - autoHeading)
|
||||
<= TURN_STEP) { //Cancel when within 1 turn step of target.
|
||||
isAuto = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleSailIn() {
|
||||
sailIn = !sailIn;
|
||||
}
|
||||
|
||||
public void turnUpwind() {
|
||||
disableAutoPilot();
|
||||
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() {
|
||||
disableAutoPilot();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the VMG from the polartable for upwind or downwind depending on the boats direction,
|
||||
* and uses this to calculate a heading to move the yacht towards.
|
||||
*/
|
||||
public void turnToVMG() {
|
||||
if (isAuto) {
|
||||
disableAutoPilot();
|
||||
} else {
|
||||
Double normalizedHeading = normalizeHeading();
|
||||
Double optimalHeading;
|
||||
HashMap<Double, Double> optimalPolarMap;
|
||||
|
||||
if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind
|
||||
optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots());
|
||||
} else {
|
||||
optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots());
|
||||
}
|
||||
optimalHeading = optimalPolarMap.keySet().iterator().next();
|
||||
|
||||
if (normalizedHeading > 180) {
|
||||
optimalHeading = 360 - optimalHeading;
|
||||
}
|
||||
|
||||
// Take optimal heading and turn into a boat heading rather than a wind heading.
|
||||
optimalHeading =
|
||||
optimalHeading + GameState.getWindDirection();
|
||||
|
||||
setAutoPilot(optimalHeading);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a given heading and rotates the boat towards that heading. This does not care about
|
||||
* being upwind or downwind, just which direction will reach a given heading faster.
|
||||
*
|
||||
* @param newHeading The heading to turn the yacht towards.
|
||||
*/
|
||||
private void turnTowardsHeading(Double newHeading) {
|
||||
Double newVal = heading - newHeading;
|
||||
if (Math.floorMod(newVal.longValue(), 360L) > 180) {
|
||||
adjustHeading(TURN_STEP / 5);
|
||||
} else {
|
||||
adjustHeading(-TURN_STEP / 5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a heading normalized for the wind direction. Heading direction into the wind is 0,
|
||||
* directly away is 180.
|
||||
*
|
||||
* @return The normalized heading accounting for wind direction.
|
||||
*/
|
||||
private Double normalizeHeading() {
|
||||
Double normalizedHeading = heading - GameState.windDirection;
|
||||
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L);
|
||||
return normalizedHeading;
|
||||
}
|
||||
|
||||
public Integer getSourceId() {
|
||||
//@TODO Remove and merge with Creating Game Loop
|
||||
if (sourceId == null) {
|
||||
return 0;
|
||||
}
|
||||
return sourceId;
|
||||
}
|
||||
|
||||
// TODO: 15/08/17 This method is implicitly called from the XML generator for boats DO NOT DELETE
|
||||
public String getHullID() {
|
||||
if (hullID == null) {
|
||||
return "";
|
||||
}
|
||||
return hullID;
|
||||
}
|
||||
|
||||
// TODO: 15/08/17 This method is implicitly called from the XML generator for boats DO NOT DELETE
|
||||
public String getShortName() {
|
||||
return shortName;
|
||||
}
|
||||
|
||||
public String getBoatName() {
|
||||
return boatName;
|
||||
}
|
||||
|
||||
public String getCountry() {
|
||||
if (country == null) {
|
||||
return "";
|
||||
}
|
||||
return country;
|
||||
}
|
||||
|
||||
public void setBoatName(String name) {
|
||||
boatName = name;
|
||||
shortName = name.split(" ")[0];
|
||||
}
|
||||
|
||||
public GeoPoint getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
|
||||
public Double getHeading() {
|
||||
return heading;
|
||||
}
|
||||
|
||||
public void setHeading(Double heading) {
|
||||
this.heading = heading;
|
||||
}
|
||||
|
||||
public Boolean getSailIn() {
|
||||
return sailIn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return boatName;
|
||||
}
|
||||
|
||||
public Double getCurrentVelocity() {
|
||||
return currentVelocity;
|
||||
}
|
||||
|
||||
public void setCurrentVelocity(Double currentVelocity) {
|
||||
this.currentVelocity = currentVelocity;
|
||||
}
|
||||
|
||||
public Integer getCurrentMarkSeqID() {
|
||||
return currentMarkSeqID;
|
||||
}
|
||||
|
||||
public GeoPoint getLastLocation() {
|
||||
return lastLocation;
|
||||
}
|
||||
|
||||
public Mark getClosestCurrentMark() {
|
||||
return closestCurrentMark;
|
||||
}
|
||||
|
||||
public void setClosestCurrentMark(Mark closestCurrentMark) {
|
||||
this.closestCurrentMark = closestCurrentMark;
|
||||
}
|
||||
|
||||
public void setHasEnteredRoundingZone(Boolean hasEnteredRoundingZone) {
|
||||
this.hasEnteredRoundingZone = hasEnteredRoundingZone;
|
||||
}
|
||||
|
||||
public void setHasPassedLine(Boolean hasPassedLine) {
|
||||
this.hasPassedLine = hasPassedLine;
|
||||
}
|
||||
|
||||
public void setHasPassedThroughGate(Boolean hasPassedThroughGate) {
|
||||
this.hasPassedThroughGate = hasPassedThroughGate;
|
||||
}
|
||||
|
||||
public BoatStatus getBoatStatus() {
|
||||
return boatStatus;
|
||||
}
|
||||
|
||||
public void setBoatStatus(BoatStatus boatStatus) {
|
||||
this.boatStatus = boatStatus;
|
||||
}
|
||||
|
||||
public void incrementMarkSeqID() {
|
||||
currentMarkSeqID++;
|
||||
}
|
||||
|
||||
public Boolean hasEnteredRoundingZone() {
|
||||
return hasEnteredRoundingZone;
|
||||
}
|
||||
|
||||
public Boolean hasPassedThroughGate() {
|
||||
return hasPassedThroughGate;
|
||||
}
|
||||
|
||||
public Boolean hasPassedLine() {
|
||||
return hasPassedLine;
|
||||
}
|
||||
|
||||
public void incrementLegNumber() {
|
||||
legNumber++;
|
||||
}
|
||||
|
||||
public Integer getLegNumber() {
|
||||
return legNumber;
|
||||
}
|
||||
|
||||
public void setBoatColor(Color color) {
|
||||
this.boatColor = color;
|
||||
}
|
||||
|
||||
public Color getBoatColor() {
|
||||
return boatColor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package seng302.model.mark;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import seng302.gameServer.messages.RoundingSide;
|
||||
import seng302.model.GeoPoint;
|
||||
import seng302.utilities.GeoUtility;
|
||||
|
||||
public class CompoundMark {
|
||||
|
||||
private int compoundMarkId;
|
||||
private String name;
|
||||
private List<Mark> marks = new ArrayList<>();
|
||||
private GeoPoint midPoint;
|
||||
|
||||
public CompoundMark(int markID, String name, List<Mark> marks) {
|
||||
this.compoundMarkId = markID;
|
||||
this.name = name;
|
||||
this.marks.addAll(marks);
|
||||
if (marks.size() > 1) {
|
||||
this.midPoint = GeoUtility.getDirtyMidPoint(marks.get(0), marks.get(1));
|
||||
} else {
|
||||
this.midPoint = marks.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints out compoundMark's info and its marks, good for testing
|
||||
* @return a string showing its details
|
||||
*/
|
||||
@Override
|
||||
public String toString(){
|
||||
String info = String.format(
|
||||
"CompoundMark: %d (%s), [%s", compoundMarkId, name, marks.get(0).toString()
|
||||
);
|
||||
if (marks.size() > 1) {
|
||||
info += String.format(", %s", marks.get(1).toString());
|
||||
}
|
||||
return info + "]";
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return compoundMarkId;
|
||||
}
|
||||
|
||||
public void setId (int markID) {
|
||||
this.compoundMarkId = markID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void setRoundingSide(RoundingSide roundingSide) {;
|
||||
switch (roundingSide) {
|
||||
case SP:
|
||||
getSubMark(1).setRoundingSide(RoundingSide.STARBOARD);
|
||||
getSubMark(2).setRoundingSide(RoundingSide.PORT);
|
||||
break;
|
||||
case PS:
|
||||
getSubMark(1).setRoundingSide(RoundingSide.PORT);
|
||||
getSubMark(2).setRoundingSide(RoundingSide.STARBOARD);
|
||||
break;
|
||||
case PORT:
|
||||
getSubMark(1).setRoundingSide(RoundingSide.PORT);
|
||||
break;
|
||||
case STARBOARD:
|
||||
getSubMark(1).setRoundingSide(RoundingSide.STARBOARD);
|
||||
break;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mark contained in the compound mark. Marks are numbered 1 to n;
|
||||
* @param singleMarkId the id of the desired mark contained in this compound mark.
|
||||
* @return the desired mark. Returns null if the ID is not in range (1, NUM_MARKS)
|
||||
*/
|
||||
public Mark getSubMark(int singleMarkId) {
|
||||
try {
|
||||
return marks.get(singleMarkId - 1);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: This is a 'dirty' mid point as it is simply calculated as an xy point would be.
|
||||
* NO CHECKING FOR LAT / LNG WRAPPING IS DONE IN CREATION OF THIS MIDPOINT
|
||||
*
|
||||
* @return GeoPoint of the midpoint of the two marks, or the one mark if there is only one
|
||||
*/
|
||||
public GeoPoint getMidPoint() {
|
||||
return midPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this CompoundMark is a Gate. It is generally cleaner to program to a
|
||||
* specific singleMark or the list of marks.
|
||||
*
|
||||
* @return True if the compound mark is a gate, false otherwise.
|
||||
*/
|
||||
public boolean isGate () {
|
||||
return marks.size() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of marks in the compoundMark
|
||||
*
|
||||
* @return All marks contained in this mark.
|
||||
*/
|
||||
public List<Mark> getMarks () {
|
||||
return marks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 0;
|
||||
for (Mark mark : marks) {
|
||||
hash += Double.hashCode(mark.getSourceID()) + Double.hashCode(mark.getLat())
|
||||
+ Double.hashCode(mark.getLng()) + mark.getName().hashCode();
|
||||
}
|
||||
return hash + getName().hashCode() + Integer.hashCode(getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package seng302.model.mark;
|
||||
|
||||
/**
|
||||
* Stores the data for the cornering of a mark.
|
||||
*/
|
||||
public class Corner {
|
||||
|
||||
private Integer seqID;
|
||||
private Integer compoundMarkID;
|
||||
private String rounding;
|
||||
private Integer zoneSize;
|
||||
|
||||
public Corner(Integer seqID, Integer compoundMarkID, String rounding, Integer zoneSize) {
|
||||
this.seqID = seqID;
|
||||
this.compoundMarkID = compoundMarkID;
|
||||
this.rounding = rounding;
|
||||
this.zoneSize = zoneSize;
|
||||
}
|
||||
|
||||
public Integer getSeqID() {
|
||||
return seqID;
|
||||
}
|
||||
|
||||
public Integer getCompoundMarkID() {
|
||||
return compoundMarkID;
|
||||
}
|
||||
|
||||
public String getRounding() {
|
||||
return rounding;
|
||||
}
|
||||
|
||||
public Integer getZoneSize() {
|
||||
return zoneSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package seng302.model.mark;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import seng302.gameServer.messages.RoundingSide;
|
||||
import seng302.model.GeoPoint;
|
||||
|
||||
/**
|
||||
* An abstract class to represent general marks
|
||||
* Created by Haoming Yin (hyi25) on 17/3/17.
|
||||
*/
|
||||
public class Mark extends GeoPoint {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface PositionListener {
|
||||
void notifyPositionChange(Mark mark, double lat, double lon);
|
||||
}
|
||||
|
||||
private int seqID;
|
||||
private String name;
|
||||
private int sourceID;
|
||||
private List<PositionListener> positionListeners = new ArrayList<>();
|
||||
private RoundingSide roundingSide;
|
||||
|
||||
public Mark(String name, int seqID, double lat, double lng, int sourceID) {
|
||||
super(lat, lng);
|
||||
this.name = name;
|
||||
this.sourceID = sourceID;
|
||||
this.seqID = seqID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints out mark's info and its geo location, good for testing
|
||||
* @return a string showing its details
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Mark%d: %s, source: %d, lat: %f, lng: %f", seqID, name, sourceID, getLat(), getLng());
|
||||
}
|
||||
|
||||
public int getSeqID() {
|
||||
return seqID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getSourceID() {
|
||||
return sourceID;
|
||||
}
|
||||
|
||||
public RoundingSide getRoundingSide() {
|
||||
return roundingSide;
|
||||
}
|
||||
|
||||
public void setRoundingSide(RoundingSide roundingSide) {
|
||||
this.roundingSide = roundingSide;
|
||||
}
|
||||
|
||||
public void setSourceID(int sourceID) {
|
||||
this.sourceID = sourceID;
|
||||
}
|
||||
|
||||
public void updatePosition (double lat, double lon) {
|
||||
this.setLat(lat);
|
||||
this.setLng(lon);
|
||||
for (PositionListener listener : positionListeners) {
|
||||
listener.notifyPositionChange(this, lat, lon);
|
||||
}
|
||||
}
|
||||
|
||||
public void addPositionListener (PositionListener listener) {
|
||||
positionListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removePositionListener (PositionListener listener) {
|
||||
positionListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package seng302.model.mark;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import seng302.gameServer.messages.RoundingSide;
|
||||
import seng302.model.stream.xml.generator.Race;
|
||||
import seng302.model.stream.xml.parser.RaceXMLData;
|
||||
import seng302.utilities.XMLGenerator;
|
||||
import seng302.utilities.XMLParser;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class to hold the order of the marks in the race.
|
||||
*/
|
||||
public class MarkOrder {
|
||||
private List<CompoundMark> raceMarkOrder;
|
||||
private Logger logger = LoggerFactory.getLogger(MarkOrder.class);
|
||||
private Set<Mark> allMarks;
|
||||
|
||||
public MarkOrder(){
|
||||
loadRaceProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An ordered list of marks in the race
|
||||
* OR null if the mark order could not be loaded
|
||||
*/
|
||||
public List<CompoundMark> getMarkOrder() {
|
||||
if (raceMarkOrder == null){
|
||||
logger.warn("Race order accessed but not instantiated");
|
||||
return null;
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(raceMarkOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param seqID The seqID of the current mark the boat is heading to
|
||||
* @return A Boolean indicating if this coming mark is the last one (finish line)
|
||||
*/
|
||||
public Boolean isLastMark(Integer seqID) {
|
||||
return seqID == raceMarkOrder.size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param currentSeqID The seqID of the current mark the boat is heading to
|
||||
* @return The mark last passed
|
||||
* @throws IndexOutOfBoundsException if there is no next mark. Check seqID != 0 first
|
||||
*/
|
||||
public CompoundMark getPreviousMark(Integer currentSeqID) throws IndexOutOfBoundsException {
|
||||
return raceMarkOrder.get(currentSeqID - 1);
|
||||
}
|
||||
|
||||
public CompoundMark getCurrentMark(Integer currentSeqID) {
|
||||
return raceMarkOrder.get(currentSeqID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param currentSeqID The seqID of the current mark the boat is heading to
|
||||
* @return The mark following the mark that the boat is heading to
|
||||
* @throws IndexOutOfBoundsException if there is no next mark. Check using {@link
|
||||
* #isLastMark(Integer)}
|
||||
*/
|
||||
public CompoundMark getNextMark(Integer currentSeqID) throws IndexOutOfBoundsException {
|
||||
return raceMarkOrder.get(currentSeqID + 1);
|
||||
}
|
||||
|
||||
public Set<Mark> getAllMarks(){
|
||||
return Collections.unmodifiableSet(allMarks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the race order from an XML string
|
||||
* @param xml An AC35 RaceXML
|
||||
* @return An ordered list of marks in the race
|
||||
*/
|
||||
private List<CompoundMark> loadRaceOrderFromXML(String xml) {
|
||||
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db;
|
||||
Document doc;
|
||||
allMarks = new HashSet<>();
|
||||
|
||||
try {
|
||||
db = dbf.newDocumentBuilder();
|
||||
doc = db.parse(new InputSource(new StringReader(xml)));
|
||||
} catch (ParserConfigurationException | IOException | SAXException e) {
|
||||
logger.error("Failed to read generated race XML");
|
||||
return null;
|
||||
}
|
||||
|
||||
RaceXMLData data = XMLParser.parseRace(doc);
|
||||
|
||||
if (data != null){
|
||||
logger.debug("Loaded RaceXML for mark order");
|
||||
List<Corner> corners = data.getMarkSequence();
|
||||
Map<Integer, CompoundMark> marks = data.getCompoundMarks();
|
||||
List<CompoundMark> course = new ArrayList<>();
|
||||
for (Corner corner : corners){
|
||||
CompoundMark compoundMark = marks.get(corner.getCompoundMarkID());
|
||||
compoundMark.setRoundingSide(
|
||||
RoundingSide.getRoundingSide(corner.getRounding())
|
||||
);
|
||||
course.add(compoundMark);
|
||||
allMarks.addAll(compoundMark.getMarks());
|
||||
}
|
||||
|
||||
return course;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the raceXML and mark order
|
||||
*/
|
||||
private void loadRaceProperties(){
|
||||
XMLGenerator generator = new XMLGenerator();
|
||||
|
||||
generator.setRace(new Race());
|
||||
|
||||
String raceXML = generator.getRaceAsXml();
|
||||
|
||||
if (raceXML == null){
|
||||
logger.error("Failed to generate raceXML (for race properties)");
|
||||
return;
|
||||
}
|
||||
raceMarkOrder = loadRaceOrderFromXML(raceXML);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package seng302.model.stream.packets;
|
||||
|
||||
public enum PacketType {
|
||||
HEARTBEAT,
|
||||
RACE_STATUS,
|
||||
DISPLAY_TEXT_MESSAGE,
|
||||
RACE_XML,
|
||||
REGATTA_XML,
|
||||
BOAT_XML,
|
||||
RACE_START_STATUS,
|
||||
YACHT_EVENT_CODE,
|
||||
YACHT_ACTION_CODE,
|
||||
CHATTER_TEXT,
|
||||
BOAT_LOCATION,
|
||||
MARK_ROUNDING,
|
||||
COURSE_WIND,
|
||||
AVG_WIND,
|
||||
BOAT_ACTION,
|
||||
OTHER,
|
||||
RACE_REGISTRATION_REQUEST,
|
||||
RACE_REGISTRATION_RESPONSE,
|
||||
RACE_CUSTOMIZATION_REQUEST,
|
||||
RACE_CUSTOMIZATION_RESPONSE;
|
||||
|
||||
public static PacketType assignPacketType(int packetType, byte[] payload){
|
||||
switch(packetType){
|
||||
case 1:
|
||||
return HEARTBEAT;
|
||||
case 12:
|
||||
return RACE_STATUS;
|
||||
case 20:
|
||||
return DISPLAY_TEXT_MESSAGE;
|
||||
case 26:
|
||||
switch (payload[9]) { //The type of XML message
|
||||
case 5:
|
||||
return REGATTA_XML;
|
||||
case 6:
|
||||
return RACE_XML;
|
||||
case 7:
|
||||
return BOAT_XML;
|
||||
}
|
||||
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;
|
||||
case 100:
|
||||
return BOAT_ACTION;
|
||||
case 101:
|
||||
return RACE_REGISTRATION_REQUEST;
|
||||
case 102:
|
||||
return RACE_REGISTRATION_RESPONSE;
|
||||
case 103:
|
||||
return RACE_CUSTOMIZATION_REQUEST;
|
||||
case 104:
|
||||
return RACE_CUSTOMIZATION_RESPONSE;
|
||||
default:
|
||||
}
|
||||
return OTHER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package seng302.model.stream.packets;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public StreamPacket(int type, long messageLength, long timeStamp, byte[] payload) {
|
||||
this.type = PacketType.assignPacketType(type, payload);
|
||||
this.messageLength = messageLength;
|
||||
this.timeStamp = timeStamp;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public PacketType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public long getMessageLength() {
|
||||
return messageLength;
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package seng302.model.stream.parser;
|
||||
|
||||
|
||||
/**
|
||||
* Simple data wrapper for mark rounding data packet.
|
||||
*/
|
||||
public class MarkRoundingData {
|
||||
|
||||
private int boatId;
|
||||
private int markId;
|
||||
private int roundingSide;
|
||||
private long timeStamp;
|
||||
|
||||
public MarkRoundingData(int boatId, int markId, int roundingSide, long timeStamp) {
|
||||
this.boatId = boatId;
|
||||
this.markId = markId;
|
||||
this.roundingSide = roundingSide;
|
||||
this.timeStamp = timeStamp;
|
||||
}
|
||||
|
||||
public int getBoatId() {
|
||||
return boatId;
|
||||
}
|
||||
|
||||
public int getMarkId() {
|
||||
return markId;
|
||||
}
|
||||
|
||||
public int getRoundingSide() {
|
||||
return roundingSide;
|
||||
}
|
||||
|
||||
public long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package seng302.model.stream.parser;
|
||||
|
||||
public class PositionUpdateData {
|
||||
|
||||
public enum DeviceType {
|
||||
YACHT_TYPE,
|
||||
MARK_TYPE
|
||||
}
|
||||
|
||||
private int deviceId;
|
||||
private DeviceType type;
|
||||
private double lat;
|
||||
private double lon;
|
||||
private double heading;
|
||||
private double groundSpeed;
|
||||
|
||||
public PositionUpdateData(int deviceId, DeviceType type, double lat, double lon,
|
||||
double heading, double groundSpeed) {
|
||||
this.deviceId = deviceId;
|
||||
this.type = type;
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
this.heading = heading;
|
||||
this.groundSpeed = groundSpeed;
|
||||
}
|
||||
|
||||
public int getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public DeviceType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public double getLat() {
|
||||
return lat;
|
||||
}
|
||||
|
||||
public double getLon() {
|
||||
return lon;
|
||||
}
|
||||
|
||||
public double getHeading() {
|
||||
return heading;
|
||||
}
|
||||
|
||||
public double getGroundSpeed() {
|
||||
return groundSpeed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package seng302.model.stream.parser;
|
||||
|
||||
/**
|
||||
* Class for storing data parsed from race start status packet
|
||||
*/
|
||||
public class RaceStartData {
|
||||
|
||||
private long raceId;
|
||||
private long raceStartTime;
|
||||
private int notificationType;
|
||||
private long timeStamp;
|
||||
|
||||
public RaceStartData (long raceId, long raceStartTime, int notificationType, long timeStamp) {
|
||||
this.raceId = raceId;
|
||||
this.raceStartTime = raceStartTime;
|
||||
this.notificationType = notificationType;
|
||||
this.timeStamp = timeStamp;
|
||||
}
|
||||
|
||||
public long getRaceId() {
|
||||
return raceId;
|
||||
}
|
||||
|
||||
public long getRaceStartTime() {
|
||||
return raceStartTime;
|
||||
}
|
||||
|
||||
public int getNotificationType() {
|
||||
return notificationType;
|
||||
}
|
||||
|
||||
public long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package seng302.model.stream.parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Stores parsed data from race status packets
|
||||
*/
|
||||
public class RaceStatusData {
|
||||
|
||||
//CONVERSION CONSTANTS
|
||||
private static final double WIND_DIR_FACTOR = 0x4000 / 90; //0x4000 is 90 degrees
|
||||
private static final double MS_TO_KNOTS = 1.94384;
|
||||
|
||||
private double windDirection;
|
||||
private double windSpeed;
|
||||
private boolean raceStarted = false;
|
||||
private long currentTime;
|
||||
private long expectedStartTime;
|
||||
private List<long[]> boatData = new ArrayList<>();
|
||||
|
||||
public RaceStatusData(
|
||||
long windDir, long rawWindSpeed, int raceStatus, long currentTime, long expectedStartTime) {
|
||||
|
||||
windDirection = windDir / WIND_DIR_FACTOR;
|
||||
windSpeed = rawWindSpeed / 1000 * MS_TO_KNOTS;
|
||||
raceStarted = raceStatus == 3;
|
||||
this.currentTime = currentTime;
|
||||
this.expectedStartTime = expectedStartTime;
|
||||
}
|
||||
|
||||
public void addBoatData (long boatID, long estTimeToNextMark, long estTimeToFinish, int leg, int boatStatus) {
|
||||
boatData.add(new long[] {boatID, estTimeToNextMark, estTimeToFinish, leg, boatStatus});
|
||||
}
|
||||
|
||||
public double getWindDirection() {
|
||||
return windDirection;
|
||||
}
|
||||
|
||||
public double getWindSpeed() {
|
||||
return windSpeed;
|
||||
}
|
||||
|
||||
public boolean isRaceStarted() {
|
||||
return raceStarted;
|
||||
}
|
||||
|
||||
public long getCurrentTime() {
|
||||
return currentTime;
|
||||
}
|
||||
|
||||
public long getExpectedStartTime() {
|
||||
return expectedStartTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data for boats collected form race status packets.
|
||||
*
|
||||
* @return A list of boat data. Boat data is in the form
|
||||
* [boatID, estTimeToNextMark, estTimeToFinish, legNumber].
|
||||
*/
|
||||
public List<long[]> getBoatData () {
|
||||
return boatData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package seng302.model.stream.parser;
|
||||
|
||||
/**
|
||||
* Stores parsed data from yacht event code packet
|
||||
*/
|
||||
public class YachtEventData {
|
||||
private Long subjectId;
|
||||
private Long incidentId;
|
||||
private Integer eventId;
|
||||
private Long timeStamp;
|
||||
|
||||
public YachtEventData(Long subjectId, Long incidentId, Integer eventId, Long timeStamp) {
|
||||
this.subjectId = subjectId;
|
||||
this.incidentId = incidentId;
|
||||
this.eventId = eventId;
|
||||
this.timeStamp = timeStamp;
|
||||
}
|
||||
|
||||
public Long getSubjectId() {
|
||||
return subjectId;
|
||||
}
|
||||
|
||||
public Long getIncidentId() {
|
||||
return incidentId;
|
||||
}
|
||||
|
||||
public Integer getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
|
||||
public Long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package seng302.model.stream.xml.generator;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import seng302.model.ServerYacht;
|
||||
|
||||
/**
|
||||
* A Race object that can be parsed into XML
|
||||
*/
|
||||
public class Race {
|
||||
|
||||
private List<ServerYacht> 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(ServerYacht yacht) {
|
||||
yachts.add(yacht);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of boats in the race
|
||||
* @return A List of boats
|
||||
*/
|
||||
public List<ServerYacht> 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.model.stream.xml.generator;
|
||||
|
||||
/**
|
||||
* 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, String courseName, Double latitude, Double longitude) {
|
||||
this.name = name;
|
||||
this.id = DEFAULT_REGATTA_ID;
|
||||
this.courseName = courseName;
|
||||
|
||||
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,46 @@
|
||||
package seng302.model.stream.xml.parser;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import seng302.model.Limit;
|
||||
import seng302.model.mark.CompoundMark;
|
||||
import seng302.model.mark.Corner;
|
||||
|
||||
/**
|
||||
* Process a Document object containing race data in XML format and stores the data.
|
||||
*/
|
||||
public class RaceXMLData {
|
||||
|
||||
private List<Integer> participants;
|
||||
private Map<Integer, CompoundMark> compoundMarks;
|
||||
private List<Corner> markSequence;
|
||||
private List<Limit> courseLimit;
|
||||
|
||||
public RaceXMLData(List<Integer> participants, List<CompoundMark> compoundMarks,
|
||||
List<Corner> markSequence, List<Limit> courseLimit) {
|
||||
this.participants = participants;
|
||||
this.markSequence = markSequence;
|
||||
this.courseLimit = courseLimit;
|
||||
this.compoundMarks = new HashMap<>();
|
||||
for (CompoundMark cMark : compoundMarks) {
|
||||
this.compoundMarks.put(cMark.getId(), cMark);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Integer> getParticipants() {
|
||||
return participants;
|
||||
}
|
||||
|
||||
public Map<Integer, CompoundMark> getCompoundMarks() {
|
||||
return compoundMarks;
|
||||
}
|
||||
|
||||
public List<Corner> getMarkSequence() {
|
||||
return markSequence;
|
||||
}
|
||||
|
||||
public List<Limit> getCourseLimit() {
|
||||
return courseLimit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package seng302.model.stream.xml.parser;
|
||||
|
||||
/**
|
||||
* Stores data from regatta xml packet.
|
||||
*/
|
||||
public class RegattaXMLData {
|
||||
//Regatta Info
|
||||
private Integer regattaID;
|
||||
private String regattaName;
|
||||
private String courseName;
|
||||
private Double centralLat;
|
||||
private Double centralLng;
|
||||
private Integer utcOffset;
|
||||
|
||||
public RegattaXMLData (Integer regattaID, String regattaName, String courseName,
|
||||
Double centralLat, Double centralLng, Integer utcOffset) {
|
||||
this.regattaID = regattaID;
|
||||
this.regattaName = regattaName;
|
||||
this.courseName = courseName;
|
||||
this.centralLat = centralLat;
|
||||
this.centralLng = centralLng;
|
||||
this.utcOffset = 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package seng302.utilities;
|
||||
|
||||
import javafx.geometry.Point2D;
|
||||
import seng302.model.GeoPoint;
|
||||
|
||||
public class GeoUtility {
|
||||
|
||||
private static double EARTH_RADIUS = 6378.137;
|
||||
private static Double MS_TO_KNOTS = 1.943844492;
|
||||
|
||||
/**
|
||||
* Calculates the euclidean distance between two markers on the canvas using xy coordinates
|
||||
*
|
||||
* @param p1 first geographical position
|
||||
* @param p2 second geographical position
|
||||
* @return the distance in meter between two points in meters
|
||||
*/
|
||||
public static Double getDistance(GeoPoint p1, GeoPoint p2) {
|
||||
|
||||
double dLat = Math.toRadians(p2.getLat() - p1.getLat());
|
||||
double dLon = Math.toRadians(p2.getLng() - p1.getLng());
|
||||
|
||||
double a = Math.pow(Math.sin(dLat / 2), 2.0)
|
||||
+ Math.cos(Math.toRadians(p1.getLat())) * Math.cos(Math.toRadians(p2.getLat()))
|
||||
* Math.pow(Math.sin(dLon / 2), 2.0);
|
||||
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
double d = EARTH_RADIUS * c;
|
||||
|
||||
return d * 1000; // distance from km to meter
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the angle between to angular co-ordinates on a sphere.
|
||||
*
|
||||
* @param p1 the first geographical position, start point
|
||||
* @param p2 the second geographical position, end point
|
||||
* @return the initial bearing in degree from p1 to p2, value range (0 ~ 360 deg.). vertical up
|
||||
* is 0 deg. horizontal right is 90 deg.
|
||||
*
|
||||
* NOTE: The final bearing will differ from the initial bearing by varying degrees according to
|
||||
* distance and latitude (if you were to go from say 35°N,45°E (≈ 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(GeoPoint p1, GeoPoint p2) {
|
||||
return (Math.toDegrees(getBearingRad(p1, p2)) + 360.0) % 360.0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* WARNING: this function DOES NOT account for wrapping around on lats / longs etc.
|
||||
* SO BE CAREFUL IN USING THIS FUNCTION
|
||||
*
|
||||
* @param p1 GeoPoint 1
|
||||
* @param p2 GeoPoint 2
|
||||
* @return GeoPoint midPoint
|
||||
*/
|
||||
public static GeoPoint getDirtyMidPoint(GeoPoint p1, GeoPoint p2) {
|
||||
return new GeoPoint((p1.getLat() + p2.getLat()) / 2, (p1.getLng() + p2.getLng()) / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the angle between to angular co-ordinates on a sphere in radians.
|
||||
*
|
||||
* @param p1 the first geographical position, start point
|
||||
* @param p2 the second geographical position, end point
|
||||
* @return the initial bearing in degree from p1 to p2, value range (0 ~ 360 deg.). vertical up
|
||||
* is 0 deg. horizontal right is 90 deg.
|
||||
*
|
||||
* NOTE: The final bearing will differ from the initial bearing by varying degrees according to
|
||||
* distance and latitude (if you were to go from say 35°N,45°E (≈ 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 getBearingRad(GeoPoint p1, GeoPoint p2) {
|
||||
double dLon = Math.toRadians(p2.getLng() - p1.getLng());
|
||||
|
||||
double y = Math.sin(dLon) * Math.cos(Math.toRadians(p2.getLat()));
|
||||
double x = Math.cos(Math.toRadians(p1.getLat())) * Math.sin(Math.toRadians(p2.getLat()))
|
||||
- Math.sin(Math.toRadians(p1.getLat())) * Math.cos(Math.toRadians(p2.getLat())) * Math
|
||||
.cos(dLon);
|
||||
|
||||
return Math.atan2(y, x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an existing point in lat/lng, distance in (in meter) and bearing (in degrees),
|
||||
* calculates the new lat/lng.
|
||||
*
|
||||
* @param origin the original position within lat / lng
|
||||
* @param bearing the bearing in degree, from original position to the new position
|
||||
* @param distance the distance in meter, from original position to the new position
|
||||
* @return the new position
|
||||
*/
|
||||
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
|
||||
|
||||
double originLat = Math.toRadians(origin.getLat());
|
||||
double originLng = Math.toRadians(origin.getLng());
|
||||
|
||||
double endLat = Math.asin(Math.sin(originLat) * Math.cos(d / EARTH_RADIUS)
|
||||
+ Math.cos(originLat) * Math.sin(d / EARTH_RADIUS) * Math.cos(b));
|
||||
double endLng = originLng
|
||||
+ 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 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the line formed by lastLocation and location doesn't intersect the line segment
|
||||
* formed by mark1 and mark2 See the wiki Mark Rounding algorithm for more info
|
||||
*
|
||||
* @param mark1 One mark of the line
|
||||
* @param mark2 The second mark of the line
|
||||
* @param lastLocation The last location of the point crossing this line
|
||||
* @param location The current location of the point crossing this line
|
||||
* @return 0 if two line segment doesn't intersect, otherwise 1 if they intersect and
|
||||
* lastLocation is on RHS of the line segment (mark1 to mark2) or 2 if lastLocation on LHS of
|
||||
* the line segment (mark1 to mark2)
|
||||
*/
|
||||
public static Integer checkCrossedLine(GeoPoint mark1, GeoPoint mark2, GeoPoint lastLocation,
|
||||
GeoPoint location) {
|
||||
boolean enteredDirection = isClockwise(mark1, mark2, lastLocation);
|
||||
boolean exitedDirection = isClockwise(mark1, mark2, location);
|
||||
if (enteredDirection != exitedDirection) {
|
||||
if (!isPointInTriangle(mark1, lastLocation, location, mark2)
|
||||
&& !isPointInTriangle(mark2, lastLocation, location, mark1)) {
|
||||
|
||||
return enteredDirection ? 1 : 2;
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Define vector v1 = p1 - p0 to v2 = p2- p0. This function returns the difference of bearing
|
||||
* from v1 to v2. For example, if bearing of v1 is 30 deg and bearing of v2 is 90 deg, then the
|
||||
* difference is 60 deg.
|
||||
*
|
||||
* @param bearing1 the bearing of v1
|
||||
* @param bearing2 the bearing of v2
|
||||
* @return the difference of bearing from v1 to v2
|
||||
*/
|
||||
private static Double getBearingDiff(double bearing1, double bearing2) {
|
||||
return ((360 - bearing1) + bearing2) % 360;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a geo point ins on the right hand side of the line segment, which
|
||||
* formed by two geo points v1 to v2. (Algorithm: point is clockwise to the
|
||||
* line if the bearing difference is less than 180 deg.)
|
||||
*
|
||||
* @param v1 one end of the line segment
|
||||
* @param v2 another end of the line segment
|
||||
* @param point the point to be tested
|
||||
* @return true if the point is on the RHS of the line
|
||||
*/
|
||||
public static Boolean isClockwise(GeoPoint v1, GeoPoint v2, GeoPoint point) {
|
||||
return getBearingDiff(getBearing(v1, v2), getBearing(v1, point)) < 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given three geo points to form a triangle, the method returns true if the fourth point is
|
||||
* inside the triangle
|
||||
*
|
||||
* @param v1 the vertex of the triangle
|
||||
* @param v2 the vertex of the triangle
|
||||
* @param v3 the vertex of the triangle
|
||||
* @param point the point to be tested
|
||||
* @return true if the fourth point is inside the triangle
|
||||
*/
|
||||
public static Boolean isPointInTriangle(GeoPoint v1, GeoPoint v2, GeoPoint v3, GeoPoint point) {
|
||||
// true, if diff of bearing from (v1 to v2) to (v1 to p) is less than 180 deg
|
||||
boolean isCW = isClockwise(v1, v2, point);
|
||||
|
||||
if (isClockwise(v2, v3, point) != isCW) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isClockwise(v3, v1, point) != isCW) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param boatSpeedInKnots Speed in knots
|
||||
* @return The Boat speed in millimeters per second
|
||||
*/
|
||||
public static Double knotsToMMS(Double boatSpeedInKnots) {
|
||||
return boatSpeedInKnots / MS_TO_KNOTS * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param boatSpeedInMMS Speed in millimeters per second
|
||||
* @return The Boat speed in knots
|
||||
*/
|
||||
public static Double mmsToKnots(Double boatSpeedInMMS) {
|
||||
return boatSpeedInMMS / 1000 * MS_TO_KNOTS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package seng302.utilities;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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.model.stream.packets.PacketType;
|
||||
import seng302.model.stream.packets.StreamPacket;
|
||||
import seng302.model.stream.parser.*;
|
||||
import seng302.model.stream.parser.PositionUpdateData.DeviceType;
|
||||
|
||||
/**
|
||||
* StreamParser is a utilities class for taking byte data, formatted according to the AC35 streaming
|
||||
* protocol, and parsing it into basic data types or collections.
|
||||
*
|
||||
* Created by kre39 on 23/04/17.
|
||||
*/
|
||||
public class StreamParser {
|
||||
|
||||
/**
|
||||
* Extracts and returns the seq num used in the heartbeat packet.
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
* @return the packet sequence number if the packet is of type HEARTBEAT, null otherwise.
|
||||
*/
|
||||
public static Long extractHeartBeat(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.HEARTBEAT) {
|
||||
return null;
|
||||
}
|
||||
long heartbeat = bytesToLong(packet.getPayload());
|
||||
System.out.println("heartbeat = " + heartbeat);
|
||||
return heartbeat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return null if the packet type is not RACE_STATUS, otherwise an instance of RaceStatusData
|
||||
* containing the parsed packet data.
|
||||
*/
|
||||
public static RaceStatusData extractRaceStatus(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.RACE_STATUS) {
|
||||
return null;
|
||||
}
|
||||
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));
|
||||
|
||||
// DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
|
||||
// currentTime = format.format((new Date(currentTime)))
|
||||
|
||||
RaceStatusData data = new RaceStatusData(
|
||||
windDir, rawWindSpeed, raceStatus, currentTime, expectedStartTime
|
||||
);
|
||||
|
||||
// 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;
|
||||
// } else if (!raceStarted) {
|
||||
// raceStarted = true;
|
||||
// raceFinished = false;
|
||||
// }
|
||||
// timeSinceStart = timeTillStart;
|
||||
// }
|
||||
//
|
||||
|
||||
//
|
||||
int noBoats = payload[22];
|
||||
int raceType = payload[23];
|
||||
long boatID, estTimeAtNextMark, estTimeAtFinish;
|
||||
int leg, boatStatus;
|
||||
for (int i = 0; i < noBoats; i++) {
|
||||
boatID = bytesToLong(
|
||||
Arrays.copyOfRange(payload, 24 + (i * 20), 28 + (i * 20)));
|
||||
boatStatus = (int) payload[28 + (i * 20)];
|
||||
estTimeAtNextMark = bytesToLong(
|
||||
Arrays.copyOfRange(payload, 32 + (i * 20), 38 + (i * 20)));
|
||||
estTimeAtFinish = bytesToLong(
|
||||
Arrays.copyOfRange(payload, 38 + (i * 20), 44 + (i * 20)));
|
||||
leg = (int) payload[29 + (i * 20)];
|
||||
// boat.setEstimateTimeAtFinish(estTimeAtFinish);
|
||||
data.addBoatData(boatID, estTimeAtNextMark, estTimeAtFinish, leg, boatStatus);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// private static void setBoatLegPosition(Yacht updatingBoat, Integer leg){
|
||||
// Integer placing = 1;
|
||||
// if (leg != updatingBoat.getLegNumber() && (raceStarted || raceFinished)) {
|
||||
// for (Yacht boat : boats.values()) {
|
||||
// if (boat.getLegNumber() != null && leg <= boat.getLegNumber()){
|
||||
// placing += 1;
|
||||
// }
|
||||
// }
|
||||
// updatingBoat.setPlacing(placing.toString());
|
||||
// updatingBoat.setLegNumber(leg);
|
||||
// boatsPos.putIfAbsent(placing, updatingBoat);
|
||||
// boatsPos.replace(placing, updatingBoat);
|
||||
// } else if(updatingBoat.getLegNumber() == null){
|
||||
// updatingBoat.setPlacing("1");
|
||||
// updatingBoat.setLegNumber(leg);
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Parses and returns the text from a StreamPacket containing text data for display.
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
* @return A list containing all display message text. Is null if the packet is not of type
|
||||
* DISPLAY_TEXT_MESSAGE.
|
||||
*/
|
||||
public static List<String> extractDisplayMessage(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.DISPLAY_TEXT_MESSAGE) {
|
||||
return null;
|
||||
}
|
||||
List<String> message = new ArrayList<>();
|
||||
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);
|
||||
message.add(new String(messageTextBytes));
|
||||
totalLen += 2 + textLength;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and returns an XMLParser containing XML data sent in the given StreamPacket. XML data
|
||||
* can be for races, boats or the regatta.
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
* @return XMLParse containing xmldata. Returns null if the StreamPacket is not of type
|
||||
* XML_MESSAGE.
|
||||
*/
|
||||
public static Document extractXmlMessage(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.RACE_XML &&
|
||||
packet.getType() != PacketType.REGATTA_XML &&
|
||||
packet.getType() != PacketType.BOAT_XML) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
Document doc = null;
|
||||
try {
|
||||
db = dbf.newDocumentBuilder();
|
||||
doc = db.parse(new InputSource(new StringReader(xmlMessage)));
|
||||
} catch (ParserConfigurationException | IOException | SAXException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the race start status from the packet and returns it as a long array.
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
* @return An array of form [raceID, raceStartTime, notificationType, timeStamp] or null if the
|
||||
* packet type is not of RACE_START_STATUS.
|
||||
*/
|
||||
public static RaceStartData extractRaceStartStatus(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.RACE_START_STATUS) {
|
||||
return null;
|
||||
}
|
||||
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];
|
||||
return new RaceStartData(raceId, raceStartTime, notificationType, timeStamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the the byte array in a StreamPacket for yacht events to retrieve the necessary info
|
||||
* and returns it as YachtEventData.
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
* @return the event data in the form of YachtEventData. Returns null if the packet is not of
|
||||
* type YACHT_EVENT_CODE.
|
||||
*/
|
||||
public static YachtEventData extractYachtEventCode(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.YACHT_EVENT_CODE) {
|
||||
return null;
|
||||
}
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
|
||||
long ackNumber = bytesToLong(Arrays.copyOfRange(payload, 7, 9));
|
||||
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];
|
||||
return new YachtEventData(subjectId, incidentId, eventId, timeStamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data from a StreamPacket for yacht actions and returns it in a long array.
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
* @return long array of packet data in the form [subjectID, incidentID, eventID, timeStamp].
|
||||
* Returns null if the packet is not of type YACHT_ACTION_CODE.
|
||||
*/
|
||||
public static long[] extractYachtActionCode(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.YACHT_ACTION_CODE) {
|
||||
return null;
|
||||
}
|
||||
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];
|
||||
return new long[]{subjectId, incidentId, eventId, timeStamp};
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the message from the chatter text type packets.
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
* @return Chatter text message as a string. Returns null if the packet is not of type
|
||||
* CHATTER_TEXT.
|
||||
*/
|
||||
public static String extractChatterText(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.CHATTER_TEXT) {
|
||||
return null;
|
||||
}
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
int messageType = payload[1];
|
||||
int length = payload[2];
|
||||
return new String(Arrays.copyOfRange(payload, 3, 3 + length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the data from a bot location stream packet and parses the id, timeValid, lat, lon,
|
||||
* heading and groundspeed into a BoatPositionPacket which is returned.
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
* @return BoatPositionPacket containing important boat information. Returns null if the packet
|
||||
* is not of type BOAT_LOCATION.
|
||||
*/
|
||||
public static PositionUpdateData extractBoatLocation(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.BOAT_LOCATION) {
|
||||
return null;
|
||||
}
|
||||
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));
|
||||
double heading = bytesToLong(Arrays.copyOfRange(payload, 28, 30));
|
||||
heading = 360.0 / 0xffff * heading; //Convert to degrees.
|
||||
double groundSpeed = bytesToLong(Arrays.copyOfRange(payload, 38, 40)) / 1000.0;
|
||||
|
||||
DeviceType type;
|
||||
if (deviceType == 1) {
|
||||
type = DeviceType.YACHT_TYPE;
|
||||
} else {
|
||||
type = DeviceType.MARK_TYPE;
|
||||
}
|
||||
|
||||
return new PositionUpdateData((int) boatId, type, lat, lon, heading, groundSpeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a stream packet for a mark rounding and returns the boatID, markID and timestamp.
|
||||
*
|
||||
* @param packet The packet containing the payload
|
||||
* @return an array containing longs. The values are [boatID, markID, timeStamp]. Returns null
|
||||
* if packet is not of type MARK_ROUNDING.
|
||||
*/
|
||||
public static MarkRoundingData extractMarkRounding(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.MARK_ROUNDING) {
|
||||
return null;
|
||||
}
|
||||
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];
|
||||
|
||||
return new MarkRoundingData((int) subjectId, markId, roundingSide, timeStamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing the string value of data within the given stream packet for course
|
||||
* wind.
|
||||
*
|
||||
* @param packet The packet containing the payload
|
||||
* @return the string values of the wind packet. Returns null if the packet is not of type
|
||||
* COURSE_WIND.
|
||||
*/
|
||||
public static List<String> extractCourseWind(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.COURSE_WIND) {
|
||||
return null;
|
||||
}
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
int selectedWindId = payload[1];
|
||||
int loopCount = payload[2];
|
||||
List<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);
|
||||
}
|
||||
return windInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parsed data from a StreamPacket for average wind data.
|
||||
*
|
||||
* @param packet The packet containing the payload
|
||||
* @return The wind data in the form [rawPeriod, rawSamplePeriod, period2, speed2, period3,
|
||||
* speed3, period4, speed4, timestamp] or null if the packet is not of type AVG_WIND.
|
||||
*/
|
||||
public static long[] extractAvgWind(StreamPacket packet) {
|
||||
if (packet.getType() != PacketType.AVG_WIND) {
|
||||
return null;
|
||||
}
|
||||
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));
|
||||
return new long[]{
|
||||
rawPeriod, rawSamplePeriod, period2, speed2, period3, speed3, period4, speed4, timeStamp
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static void extractBoatAction(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long actionType = bytesToLong(Arrays.copyOfRange(payload, 0, 1));
|
||||
if (actionType == 1) {
|
||||
System.out.println("VMG");
|
||||
} else if (actionType == 2) {
|
||||
System.out.println("SAILS IN");
|
||||
} else if (actionType == 3) {
|
||||
System.out.println("SAILS OUT");
|
||||
} else if (actionType == 4) {
|
||||
System.out.println("TACK/GYBE");
|
||||
} else if (actionType == 5) {
|
||||
System.out.println("UPWIND");
|
||||
} else if (actionType == 6) {
|
||||
System.out.println("DOWNWIND");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* takes an array of up to 7 bytes and returns a positive long constructed from the input bytes
|
||||
*
|
||||
* @param bytes the byte array to conver to Long
|
||||
* @return a positive long if there is less than 7 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package seng302.utilities;
|
||||
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.Template;
|
||||
import freemarker.template.TemplateException;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import seng302.model.stream.xml.generator.Race;
|
||||
import seng302.model.stream.xml.generator.Regatta;
|
||||
import seng302.gameServer.messages.XMLMessageSubType;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package seng302.utilities;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import seng302.model.ClientYacht;
|
||||
import seng302.model.Limit;
|
||||
import seng302.model.mark.CompoundMark;
|
||||
import seng302.model.mark.Corner;
|
||||
import seng302.model.mark.Mark;
|
||||
import seng302.model.stream.xml.parser.RaceXMLData;
|
||||
import seng302.model.stream.xml.parser.RegattaXMLData;
|
||||
|
||||
/**
|
||||
* Utilities for parsing XML documents
|
||||
*/
|
||||
public class XMLParser {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static Integer getElementInt(Element ele, String tag) {
|
||||
NodeList tagList = ele.getElementsByTagName(tag);
|
||||
if (tagList.getLength() > 0) {
|
||||
return Integer.parseInt(tagList.item(0).getTextContent());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static String getElementString(Element ele, String tag) {
|
||||
NodeList tagList = ele.getElementsByTagName(tag);
|
||||
if (tagList.getLength() > 0) {
|
||||
return tagList.item(0).getTextContent();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static Double getElementDouble(Element ele, String tag) {
|
||||
NodeList tagList = ele.getElementsByTagName(tag);
|
||||
if (tagList.getLength() > 0) {
|
||||
return Double.parseDouble(tagList.item(0).getTextContent());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static String getNodeAttributeString(Node n, String attr) {
|
||||
Node attrItem = n.getAttributes().getNamedItem(attr);
|
||||
if (attrItem != null) {
|
||||
return attrItem.getTextContent();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static Integer getNodeAttributeInt(Node n, String attr) {
|
||||
Node attrItem = n.getAttributes().getNamedItem(attr);
|
||||
if (attrItem != null) {
|
||||
return Integer.parseInt(attrItem.getTextContent());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static Double getNodeAttributeDouble(Node n, String attr) {
|
||||
Node attrItem = n.getAttributes().getNamedItem(attr);
|
||||
if (attrItem != null) {
|
||||
return Double.parseDouble(attrItem.getTextContent());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a mapping of boat sourceIDS to boat objects created from the given xml document.
|
||||
* @param doc XML Document Object
|
||||
* @return Mapping of sourceIds to Boats.
|
||||
*/
|
||||
public static Map<Integer, ClientYacht> parseBoats(Document doc) {
|
||||
Map<Integer, ClientYacht> competingBoats = new HashMap<>();
|
||||
|
||||
Element docEle = doc.getDocumentElement();
|
||||
|
||||
NodeList boatsList = docEle.getElementsByTagName("Boats").item(0).getChildNodes();
|
||||
for (int i = 0; i < boatsList.getLength(); i++) {
|
||||
Node currentBoat = boatsList.item(i);
|
||||
if (currentBoat.getNodeName().equals("Boat")) {
|
||||
// Boat boat = new Boat(currentBoat);
|
||||
ClientYacht yacht = new ClientYacht(
|
||||
XMLParser.getNodeAttributeString(currentBoat, "Type"),
|
||||
XMLParser.getNodeAttributeInt(currentBoat, "SourceID"),
|
||||
XMLParser.getNodeAttributeString(currentBoat, "HullNum"),
|
||||
XMLParser.getNodeAttributeString(currentBoat, "ShortName"),
|
||||
XMLParser.getNodeAttributeString(currentBoat, "BoatName"),
|
||||
XMLParser.getNodeAttributeString(currentBoat, "Country"));
|
||||
yacht.setColour(Color.web(getNodeAttributeString(currentBoat, "Color")));
|
||||
if (yacht.getBoatType().equals("Yacht")) {
|
||||
competingBoats.put(yacht.getSourceId(), yacht);
|
||||
}
|
||||
}
|
||||
}
|
||||
return competingBoats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing the data extracted from the given xml formatted document
|
||||
*
|
||||
* @param doc XML Document Object
|
||||
* @return Object containing regatta data
|
||||
*/
|
||||
public static RegattaXMLData parseRegatta(Document doc) {
|
||||
|
||||
Element docEle = doc.getDocumentElement();
|
||||
Integer regattaID = XMLParser.getElementInt(docEle, "RegattaID");
|
||||
String regattaName = XMLParser.getElementString(docEle, "RegattaName");
|
||||
String courseName = XMLParser.getElementString(docEle, "CourseName");
|
||||
Double centralLat = XMLParser.getElementDouble(docEle, "CentralLatitude");
|
||||
Double centralLng = XMLParser.getElementDouble(docEle, "CentralLongitude");
|
||||
Integer utcOffset = XMLParser.getElementInt(docEle, "UtcOffset");
|
||||
return new RegattaXMLData(
|
||||
regattaID, regattaName, courseName, centralLat, centralLng, utcOffset
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing the data extracted from the given xml formatted document
|
||||
*
|
||||
* @param doc XML document
|
||||
* @return object containing race data
|
||||
*/
|
||||
public static RaceXMLData parseRace(Document doc) {
|
||||
Element docEle = doc.getDocumentElement();
|
||||
return new RaceXMLData(
|
||||
extractParticpantIDs(docEle),
|
||||
extractCompoundMarks(docEle),
|
||||
extractMarkOrder(docEle),
|
||||
extractCourseLimit(docEle)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts course limit data
|
||||
*/
|
||||
private static List<Limit> extractCourseLimit(Element docEle) {
|
||||
List<Limit> courseLimit = new ArrayList<>();
|
||||
NodeList limitList = docEle.getElementsByTagName("CourseLimit").item(0).getChildNodes();
|
||||
for (int i = 0; i < limitList.getLength(); i++) {
|
||||
Node limitNode = limitList.item(i);
|
||||
if (limitNode.getNodeName().equals("Limit")) {
|
||||
courseLimit.add(
|
||||
new Limit(
|
||||
XMLParser.getNodeAttributeInt(limitNode, "SeqID"),
|
||||
XMLParser.getNodeAttributeDouble(limitNode, "Lat"),
|
||||
XMLParser.getNodeAttributeDouble(limitNode, "Lon")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return courseLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts course order data
|
||||
*/
|
||||
private static List<Corner> extractMarkOrder (Element docEle) {
|
||||
List<Corner> compoundMarkSequence = new ArrayList<>();
|
||||
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")) {
|
||||
compoundMarkSequence.add(
|
||||
new Corner(
|
||||
XMLParser.getNodeAttributeInt(cornerNode, "SeqID"),
|
||||
XMLParser.getNodeAttributeInt(cornerNode, "CompoundMarkID"),
|
||||
XMLParser.getNodeAttributeString(cornerNode, "Rounding"),
|
||||
XMLParser.getNodeAttributeInt(cornerNode, "ZoneSize")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return compoundMarkSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts course participants data
|
||||
*/
|
||||
private static List<Integer> extractParticpantIDs (Element docEle) {
|
||||
List<Integer> boatIDs = new ArrayList<>();
|
||||
NodeList pList = docEle.getElementsByTagName("Participants").item(0).getChildNodes();
|
||||
for (int i = 0; i < pList.getLength(); i++) {
|
||||
Node pNode = pList.item(i);
|
||||
if (pNode.getNodeName().equals("Yacht")) {
|
||||
boatIDs.add(XMLParser.getNodeAttributeInt(pNode, "SourceID"));
|
||||
}
|
||||
}
|
||||
return boatIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts course mark data
|
||||
*/
|
||||
private static List<CompoundMark> extractCompoundMarks(Element docEle) {
|
||||
List<CompoundMark> allMarks = new ArrayList<>();
|
||||
NodeList cMarkList = docEle.getElementsByTagName("Course").item(0).getChildNodes();
|
||||
CompoundMark cMark;
|
||||
for (int i = 0; i < cMarkList.getLength(); i++) {
|
||||
Node cMarkNode = cMarkList.item(i);
|
||||
if (cMarkNode.getNodeName().equals("CompoundMark")) {
|
||||
cMark = new CompoundMark(
|
||||
XMLParser.getNodeAttributeInt(cMarkNode, "CompoundMarkID"),
|
||||
XMLParser.getNodeAttributeString(cMarkNode, "Name"),
|
||||
createMarks(cMarkNode)
|
||||
);
|
||||
allMarks.add(cMark);
|
||||
}
|
||||
}
|
||||
return allMarks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates marks objects from the given node
|
||||
*/
|
||||
private static List<Mark> createMarks(Node compoundMark) {
|
||||
List<Mark> subMarks = new ArrayList<>();
|
||||
Integer compoundMarkID = XMLParser.getNodeAttributeInt(compoundMark, "CompoundMarkID");
|
||||
String cMarkName = XMLParser.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 seqID = XMLParser.getNodeAttributeInt(markNode, "SeqID");
|
||||
Integer sourceID = XMLParser.getNodeAttributeInt(markNode, "SourceID");
|
||||
String markName = XMLParser.getNodeAttributeString(markNode, "Name");
|
||||
Double targetLat = XMLParser.getNodeAttributeDouble(markNode, "TargetLat");
|
||||
Double targetLng = XMLParser.getNodeAttributeDouble(markNode, "TargetLng");
|
||||
Mark mark = new Mark(markName, seqID, targetLat, targetLng, sourceID);
|
||||
subMarks.add(mark);
|
||||
}
|
||||
}
|
||||
return subMarks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package seng302.visualiser;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Checksum;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import seng302.gameServer.messages.BoatAction;
|
||||
import seng302.gameServer.messages.BoatActionMessage;
|
||||
import seng302.gameServer.messages.ClientType;
|
||||
import seng302.gameServer.messages.CustomizeRequestMessage;
|
||||
import seng302.gameServer.messages.CustomizeRequestType;
|
||||
import seng302.gameServer.messages.Message;
|
||||
import seng302.gameServer.messages.RegistrationRequestMessage;
|
||||
import seng302.gameServer.messages.RegistrationResponseStatus;
|
||||
import seng302.model.stream.packets.PacketType;
|
||||
import seng302.model.stream.packets.StreamPacket;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Functional interface for receiving packets from client socket.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ClientSocketListener {
|
||||
void newPacket();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DisconnectedFromHostListener {
|
||||
void notifYDisconnection (String message);
|
||||
}
|
||||
|
||||
private class ByteReadException extends Exception {
|
||||
private ByteReadException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private Queue<StreamPacket> streamPackets = new ConcurrentLinkedQueue<>();
|
||||
private List<ClientSocketListener> listeners = new ArrayList<>();
|
||||
private List<DisconnectedFromHostListener> disconnectionListeners = new ArrayList<>();
|
||||
private Thread thread;
|
||||
|
||||
private Socket socket;
|
||||
private InputStream is;
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(ClientToServerThread.class);
|
||||
|
||||
//Output stream
|
||||
private OutputStream os;
|
||||
private Timer upWindPacketTimer = new Timer();
|
||||
private Timer downWindPacketTimer = new Timer();
|
||||
private boolean upwindTimerFlag = false, downwindTimerFlag = false;
|
||||
static public final int PACKET_SENDING_INTERVAL_MS = 100;
|
||||
|
||||
private int clientId = -1;
|
||||
|
||||
private ByteArrayOutputStream crcBuffer;
|
||||
private boolean socketOpen = true;
|
||||
|
||||
/**
|
||||
* 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 IOException SocketConnection if fail to connect to ip address and port number
|
||||
* combination
|
||||
*/
|
||||
public ClientToServerThread(String ipAddress, Integer portNumber) throws IOException {
|
||||
socket = new Socket(ipAddress, portNumber);
|
||||
is = socket.getInputStream();
|
||||
os = socket.getOutputStream();
|
||||
|
||||
sendRegistrationRequest();
|
||||
|
||||
thread = new Thread(this);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(!socket.isClosed() && socket.isConnected() && socketOpen) {
|
||||
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) {
|
||||
if (streamPackets.size() > 0) {
|
||||
streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload));
|
||||
} else {
|
||||
if (PacketType.RACE_REGISTRATION_RESPONSE == PacketType.assignPacketType(type, payload)){
|
||||
processRegistrationResponse(new StreamPacket(type, payloadLength, timeStamp, payload));
|
||||
}
|
||||
else {
|
||||
if (clientId == -1) continue; // Do not continue if not registered
|
||||
streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload));
|
||||
for (ClientSocketListener csl : listeners)
|
||||
csl.newPacket();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn("Packet has been dropped", 1);
|
||||
}
|
||||
}
|
||||
} catch (ByteReadException e) {
|
||||
logger.warn("Byte read exception on ClientToServerThread", 1);
|
||||
notifyDisconnectListeners("Connection to server was interrupted");
|
||||
closeSocket();
|
||||
}
|
||||
}
|
||||
logger.warn("Closed connection to server", 1);
|
||||
notifyDisconnectListeners("Connection to server was terminated");
|
||||
closeSocket();
|
||||
}
|
||||
|
||||
public void sendCustomizationRequest(CustomizeRequestType reqType, byte[] payload) {
|
||||
CustomizeRequestMessage requestMessage = new CustomizeRequestMessage(reqType, this.clientId, payload);
|
||||
try {
|
||||
os.write(requestMessage.getBuffer());
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not send customization request");
|
||||
notifyDisconnectListeners("Could not communicate with server");
|
||||
closeSocket();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyDisconnectListeners (String message) {
|
||||
if (socketOpen) {
|
||||
for (DisconnectedFromHostListener listener : disconnectionListeners) {
|
||||
listener.notifYDisconnection(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the server asking for a source ID
|
||||
*/
|
||||
private void sendRegistrationRequest() {
|
||||
RegistrationRequestMessage requestMessage = new RegistrationRequestMessage(ClientType.PLAYER);
|
||||
|
||||
try {
|
||||
os.write(requestMessage.getBuffer());
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not send registration request. Exiting");
|
||||
notifyDisconnectListeners("Failed to register with server");
|
||||
closeSocket();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a response to the registration request message, and updates the client OR quits
|
||||
* @param packet The registration requests packet
|
||||
*/
|
||||
private void processRegistrationResponse(StreamPacket packet){
|
||||
int sourceId = (int) Message.bytesToLong(Arrays.copyOfRange(packet.getPayload(), 0, 3));
|
||||
int statusCode = (int) Message.bytesToLong(Arrays.copyOfRange(packet.getPayload(), 4,5));
|
||||
|
||||
RegistrationResponseStatus status = RegistrationResponseStatus.getResponseStatus(statusCode);
|
||||
|
||||
if (status.equals(RegistrationResponseStatus.SUCCESS_PLAYING)){
|
||||
clientId = sourceId;
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error("Server Denied Connection, Exiting");
|
||||
|
||||
final String alertErrorText;
|
||||
|
||||
if (status.equals(RegistrationResponseStatus.FAILURE_FULL)){
|
||||
alertErrorText = "Server is full";
|
||||
}
|
||||
else{
|
||||
alertErrorText = "Could not connect to server";
|
||||
}
|
||||
notifyDisconnectListeners(alertErrorText);
|
||||
closeSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends packets for the given boat action. Special cases are: \n
|
||||
* - DOWNWIND = Packets are sent every ClientToServerThread.PACKET_SENDING_INTERVAL_MS
|
||||
* - UPWIND = Packets are sent every ClientToServerThread.PACKET_SENDING_INTERVAL_MS
|
||||
* - MAINTAIN_HEADING = DOWNWIND and UPWIND packets stop being sent.
|
||||
* @param actionType The boat action that will dictate packets sent.
|
||||
*/
|
||||
public void sendBoatAction(BoatAction actionType) {
|
||||
switch (actionType) {
|
||||
case MAINTAIN_HEADING:
|
||||
if (upwindTimerFlag) {
|
||||
cancelTimer(upWindPacketTimer);
|
||||
upwindTimerFlag = false;
|
||||
upWindPacketTimer = new Timer();
|
||||
}
|
||||
if (downwindTimerFlag) {
|
||||
cancelTimer(downWindPacketTimer);
|
||||
downwindTimerFlag = false;
|
||||
downWindPacketTimer = new Timer();
|
||||
}
|
||||
break;
|
||||
case DOWNWIND:
|
||||
if (!downwindTimerFlag) {
|
||||
downwindTimerFlag = true;
|
||||
downWindPacketTimer.scheduleAtFixedRate(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendBoatActionMessage(new BoatActionMessage(BoatAction.DOWNWIND));
|
||||
}
|
||||
}, 0, PACKET_SENDING_INTERVAL_MS
|
||||
);
|
||||
}
|
||||
break;
|
||||
case UPWIND:
|
||||
if (!upwindTimerFlag) {
|
||||
upwindTimerFlag = true;
|
||||
upWindPacketTimer.scheduleAtFixedRate(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendBoatActionMessage(new BoatActionMessage(BoatAction.UPWIND));
|
||||
}
|
||||
}, 0, PACKET_SENDING_INTERVAL_MS
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sendBoatActionMessage(new BoatActionMessage(actionType));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a packet sending timer.
|
||||
* @param timer The timer to cancel.
|
||||
*/
|
||||
private void cancelTimer (Timer timer) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a boat action of the given message type.
|
||||
* @param message The given message type.
|
||||
*/
|
||||
private void sendBoatActionMessage(BoatActionMessage message) {
|
||||
if (clientId != -1) {
|
||||
try {
|
||||
os.write(message.getBuffer());
|
||||
} catch (IOException e) {
|
||||
logger.warn("IOException on attempting to sendBoatAction from Client");
|
||||
notifyDisconnectListeners("Cannot communicate with server");
|
||||
closeSocket();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void closeSocket() {
|
||||
try {
|
||||
socket.close();
|
||||
socketOpen = false;
|
||||
} catch (IOException e) {
|
||||
logger.warn("IOException on attempting to close ClientToServerSocket");
|
||||
}
|
||||
}
|
||||
|
||||
public void setSocketToClose () {
|
||||
socketOpen = false;
|
||||
}
|
||||
|
||||
public Queue<StreamPacket> getPacketQueue () {
|
||||
return streamPackets;
|
||||
}
|
||||
|
||||
public void addStreamObserver (ClientSocketListener streamListener) {
|
||||
listeners.add(streamListener);
|
||||
}
|
||||
|
||||
public void removeStreamObserver (ClientSocketListener streamListener) {
|
||||
listeners.remove(streamListener);
|
||||
}
|
||||
|
||||
public void addDisconnectionListener (DisconnectedFromHostListener listener) {
|
||||
disconnectionListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeDisconnectionListener (DisconnectedFromHostListener listener) {
|
||||
disconnectionListeners.remove(listener);
|
||||
}
|
||||
|
||||
private int readByte() throws ByteReadException {
|
||||
int currentByte = -1;
|
||||
try {
|
||||
currentByte = is.read();
|
||||
crcBuffer.write(currentByte);
|
||||
} catch (IOException e) {
|
||||
logger.warn("IOException on readByte Client side", 1);
|
||||
notifyDisconnectListeners("Cannot read from server.");
|
||||
closeSocket();
|
||||
}
|
||||
if (currentByte == -1) {
|
||||
notifyDisconnectListeners("Cannot read from server.");
|
||||
closeSocket();
|
||||
logger.warn("InputStream reach end of stream", 1);
|
||||
}
|
||||
return currentByte;
|
||||
}
|
||||
|
||||
private byte[] getBytes(int n) throws ByteReadException {
|
||||
byte[] bytes = new byte[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
bytes[i] = (byte) readByte();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private void skipBytes(long n) throws ByteReadException {
|
||||
for (int i = 0; i < n; i++) {
|
||||
readByte();
|
||||
}
|
||||
}
|
||||
|
||||
int getClientId () {
|
||||
return clientId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
package seng302.visualiser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import seng302.gameServer.GameState;
|
||||
import seng302.gameServer.MainServerThread;
|
||||
import seng302.gameServer.messages.BoatAction;
|
||||
import seng302.gameServer.messages.BoatStatus;
|
||||
import seng302.model.ClientYacht;
|
||||
import seng302.model.RaceState;
|
||||
import seng302.model.stream.packets.StreamPacket;
|
||||
import seng302.model.stream.parser.MarkRoundingData;
|
||||
import seng302.model.stream.parser.PositionUpdateData;
|
||||
import seng302.model.stream.parser.PositionUpdateData.DeviceType;
|
||||
import seng302.model.stream.parser.RaceStatusData;
|
||||
import seng302.model.stream.parser.YachtEventData;
|
||||
import seng302.model.stream.xml.parser.RaceXMLData;
|
||||
import seng302.model.stream.xml.parser.RegattaXMLData;
|
||||
import seng302.utilities.StreamParser;
|
||||
import seng302.utilities.XMLParser;
|
||||
import seng302.visualiser.controllers.FinishScreenViewController;
|
||||
import seng302.visualiser.controllers.LobbyController;
|
||||
import seng302.visualiser.controllers.LobbyController.CloseStatus;
|
||||
import seng302.visualiser.controllers.RaceViewController;
|
||||
|
||||
/**
|
||||
* This class is a client side instance of a yacht racing game in JavaFX. The game is instantiated
|
||||
* with a JavaFX Pane to insert itself into.
|
||||
*/
|
||||
public class GameClient {
|
||||
|
||||
private Pane holderPane;
|
||||
private ClientToServerThread socketThread;
|
||||
private MainServerThread server;
|
||||
|
||||
private RaceViewController raceView;
|
||||
|
||||
private Map<Integer, ClientYacht> allBoatsMap;
|
||||
private RegattaXMLData regattaData;
|
||||
private RaceXMLData courseData;
|
||||
private RaceState raceState = new RaceState();
|
||||
private LobbyController lobbyController;
|
||||
|
||||
private ObservableList<String> clientLobbyList = FXCollections.observableArrayList();
|
||||
|
||||
/**
|
||||
* Create an instance of the game client. Does not do anything until run with runAsClient()
|
||||
* runAsHost().
|
||||
* @param holder The JavaFX Pane that the visual elements for the race will be inserted into.
|
||||
*/
|
||||
public GameClient(Pane holder) {
|
||||
this.holderPane = holder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a game at the given address and starts the visualiser.
|
||||
* @param ipAddress IP to connect to.
|
||||
* @param portNumber Port to connect to.
|
||||
*/
|
||||
public void runAsClient(String ipAddress, Integer portNumber) {
|
||||
try {
|
||||
startClientToServerThread(ipAddress, portNumber);
|
||||
socketThread.addDisconnectionListener((cause) -> {
|
||||
showConnectionError(cause);
|
||||
tearDownConnection();
|
||||
Platform.runLater(this::loadStartScreen);
|
||||
});
|
||||
socketThread.addStreamObserver(this::parsePackets);
|
||||
LobbyController lobbyController = loadLobby();
|
||||
lobbyController.setSocketThread(socketThread);
|
||||
lobbyController.setPlayerID(socketThread.getClientId());
|
||||
lobbyController.setPlayerListSource(clientLobbyList);
|
||||
lobbyController.disableReadyButton();
|
||||
if (regattaData != null){
|
||||
lobbyController.setTitle(regattaData.getRegattaName());
|
||||
lobbyController.setCourseName(regattaData.getCourseName());
|
||||
}
|
||||
else{
|
||||
lobbyController.setTitle(ipAddress);
|
||||
lobbyController.setCourseName("");
|
||||
}
|
||||
|
||||
lobbyController.addCloseListener((exitCause) -> {
|
||||
this.tearDownConnection();
|
||||
this.loadStartScreen();
|
||||
});
|
||||
this.lobbyController = lobbyController;
|
||||
} catch (IOException ioe) {
|
||||
showConnectionError("Unable to find server");
|
||||
Platform.runLater(this::loadStartScreen);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a game as the host at the given address and starts the visualiser.
|
||||
* @param ipAddress IP to connect to.
|
||||
* @param portNumber Port to connect to.
|
||||
*/
|
||||
public void runAsHost(String ipAddress, Integer portNumber) {
|
||||
server = new MainServerThread();
|
||||
try {
|
||||
startClientToServerThread(ipAddress, portNumber);
|
||||
socketThread.addDisconnectionListener((cause) -> {
|
||||
this.tearDownConnection();
|
||||
Platform.runLater(this::loadStartScreen);
|
||||
});
|
||||
LobbyController lobbyController = loadLobby();
|
||||
lobbyController.setSocketThread(socketThread);
|
||||
lobbyController.setPlayerID(socketThread.getClientId());
|
||||
lobbyController.setPlayerListSource(clientLobbyList);
|
||||
if (regattaData != null) {
|
||||
lobbyController.setTitle("Hosting: " + regattaData.getRegattaName());
|
||||
lobbyController.setCourseName(regattaData.getCourseName());
|
||||
} else {
|
||||
lobbyController.setTitle("Hosting: " + ipAddress);
|
||||
lobbyController.setCourseName("");
|
||||
}
|
||||
|
||||
lobbyController.addCloseListener(exitCause -> {
|
||||
if (exitCause == CloseStatus.READY) {
|
||||
GameState.resetStartTime();
|
||||
lobbyController.disableReadyButton();
|
||||
server.startGame();
|
||||
} else if (exitCause == CloseStatus.LEAVE) {
|
||||
tearDownConnection();
|
||||
loadStartScreen();
|
||||
}
|
||||
});
|
||||
this.lobbyController = lobbyController;
|
||||
} catch (IOException ioe) {
|
||||
showConnectionError("Cannot connect to server as host");
|
||||
Platform.runLater(this::loadStartScreen);
|
||||
}
|
||||
}
|
||||
|
||||
private void tearDownConnection() {
|
||||
socketThread.setSocketToClose();
|
||||
if (server != null) {
|
||||
server.terminate();
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadStartScreen() {
|
||||
// socketThread.setSocketToClose();
|
||||
// if (server != null) {
|
||||
// server.terminate();
|
||||
// server = null;
|
||||
// }
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(
|
||||
getClass().getResource("/views/StartScreenView.fxml"));
|
||||
try {
|
||||
holderPane.getChildren().clear();
|
||||
holderPane.getChildren().add(fxmlLoader.load());
|
||||
} catch (IOException e) {
|
||||
showConnectionError("JavaFX crashed. Please restart the app");
|
||||
}
|
||||
}
|
||||
|
||||
private void showConnectionError (String message) {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(AlertType.ERROR);
|
||||
alert.setHeaderText("Connection Error");
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
private void startClientToServerThread (String ipAddress, int portNumber) throws IOException {
|
||||
socketThread = new ClientToServerThread(ipAddress, portNumber);
|
||||
socketThread.addStreamObserver(this::parsePackets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a view of the lobby into the clients pane
|
||||
*
|
||||
* @return the lobby controller.
|
||||
*/
|
||||
private LobbyController loadLobby() {
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(
|
||||
GameClient.class.getResource("/views/LobbyView.fxml"));
|
||||
try {
|
||||
holderPane.getChildren().clear();
|
||||
holderPane.getChildren().add(fxmlLoader.load());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return fxmlLoader.getController();
|
||||
}
|
||||
|
||||
private void loadRaceView() {
|
||||
FXMLLoader fxmlLoader = loadFXMLToHolder("/views/RaceView.fxml");
|
||||
holderPane.getScene().setOnKeyPressed(this::keyPressed);
|
||||
holderPane.getScene().setOnKeyReleased(this::keyReleased);
|
||||
raceView = fxmlLoader.getController();
|
||||
ClientYacht player = allBoatsMap.get(socketThread.getClientId());
|
||||
raceView.loadRace(allBoatsMap, courseData, raceState, player);
|
||||
}
|
||||
|
||||
private void loadFinishScreenView() {
|
||||
FXMLLoader fxmlLoader = loadFXMLToHolder("/views/FinishScreenView.fxml");
|
||||
FinishScreenViewController controller = fxmlLoader.getController();
|
||||
controller.setFinishers(raceState.getPlayerPositions());
|
||||
}
|
||||
|
||||
private FXMLLoader loadFXMLToHolder(String fxmlLocation) {
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(
|
||||
getClass().getResource(fxmlLocation)
|
||||
);
|
||||
try {
|
||||
final Node fxmlLoaderFX = fxmlLoader.load();
|
||||
Platform.runLater(() -> {
|
||||
holderPane.getChildren().clear();
|
||||
holderPane.getChildren().add(fxmlLoaderFX);
|
||||
});
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return fxmlLoader;
|
||||
}
|
||||
|
||||
private void parsePackets() {
|
||||
while (socketThread.getPacketQueue().peek() != null) {
|
||||
StreamPacket packet = socketThread.getPacketQueue().poll();
|
||||
switch (packet.getType()) {
|
||||
case RACE_STATUS:
|
||||
processRaceStatusUpdate(StreamParser.extractRaceStatus(packet));
|
||||
|
||||
if (raceState.getTimeTillStart() <= 5000) {
|
||||
startRaceIfAllDataReceived();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case REGATTA_XML:
|
||||
regattaData = XMLParser.parseRegatta(
|
||||
StreamParser.extractXmlMessage(packet)
|
||||
);
|
||||
|
||||
raceState.setTimeZone(
|
||||
TimeZone.getTimeZone(
|
||||
ZoneId.ofOffset("UTC", ZoneOffset.ofHours(regattaData.getUtcOffset()))
|
||||
)
|
||||
);
|
||||
break;
|
||||
|
||||
case RACE_XML:
|
||||
courseData = XMLParser.parseRace(
|
||||
StreamParser.extractXmlMessage(packet)
|
||||
);
|
||||
if (raceView != null) {
|
||||
raceView.updateRaceData(courseData);
|
||||
}
|
||||
break;
|
||||
|
||||
case BOAT_XML:
|
||||
allBoatsMap = XMLParser.parseBoats(
|
||||
StreamParser.extractXmlMessage(packet)
|
||||
);
|
||||
clientLobbyList.clear();
|
||||
allBoatsMap.forEach((id, boat) ->
|
||||
clientLobbyList.add(boat.getBoatName())
|
||||
);
|
||||
raceState.setBoats(allBoatsMap.values());
|
||||
break;
|
||||
|
||||
case RACE_START_STATUS:
|
||||
raceState.updateState(StreamParser.extractRaceStartStatus(packet));
|
||||
if (lobbyController != null) lobbyController.updateRaceState(raceState);
|
||||
break;
|
||||
|
||||
case BOAT_LOCATION:
|
||||
updatePosition(StreamParser.extractBoatLocation(packet));
|
||||
break;
|
||||
|
||||
case MARK_ROUNDING:
|
||||
updateMarkRounding(StreamParser.extractMarkRounding(packet));
|
||||
break;
|
||||
|
||||
case YACHT_EVENT_CODE:
|
||||
showCollisionAlert(StreamParser.extractYachtEventCode(packet));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startRaceIfAllDataReceived() {
|
||||
if (allXMLReceived() && raceView == null) {
|
||||
loadRaceView();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean allXMLReceived() {
|
||||
return courseData != null && allBoatsMap != null && regattaData != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of a boat. Boat and position are given in the provided data.
|
||||
*/
|
||||
private void updatePosition(PositionUpdateData positionData) {
|
||||
if (positionData.getType() == DeviceType.YACHT_TYPE) {
|
||||
if (allXMLReceived() && allBoatsMap.containsKey(positionData.getDeviceId())) {
|
||||
ClientYacht yacht = allBoatsMap.get(positionData.getDeviceId());
|
||||
yacht.updateLocation(positionData.getLat(),
|
||||
positionData.getLon(), positionData.getHeading(),
|
||||
positionData.getGroundSpeed());
|
||||
}
|
||||
} else if (positionData.getType() == DeviceType.MARK_TYPE) {
|
||||
//CompoundMark mark = courseData.getCompoundMarks().get(positionData.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the boat as having passed the mark. Boat and mark are given by the ids in the
|
||||
* provided data.
|
||||
*
|
||||
* @param roundingData Contains data for the rounding of a mark.
|
||||
*/
|
||||
private void updateMarkRounding(MarkRoundingData roundingData) {
|
||||
if (allXMLReceived()) {
|
||||
ClientYacht clientYacht = allBoatsMap.get(roundingData.getBoatId());
|
||||
clientYacht.roundMark(
|
||||
courseData.getCompoundMarks().get(roundingData.getMarkId()),
|
||||
roundingData.getTimeStamp(),
|
||||
raceState.getRaceTime() - roundingData.getTimeStamp()
|
||||
);
|
||||
}
|
||||
updatePlayerPositions();
|
||||
}
|
||||
|
||||
private void processRaceStatusUpdate(RaceStatusData data) {
|
||||
if (allXMLReceived()) {
|
||||
raceState.updateState(data);
|
||||
boolean raceFinished = true;
|
||||
for (ClientYacht yacht : allBoatsMap.values()) {
|
||||
if (yacht.getBoatStatus() != BoatStatus.FINISHED.getCode()) {
|
||||
raceFinished = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (long[] boatData : data.getBoatData()) {
|
||||
ClientYacht clientYacht = allBoatsMap.get((int) boatData[0]);
|
||||
clientYacht.setEstimateTimeTillNextMark(raceState.getRaceTime() - boatData[1]);
|
||||
clientYacht.setEstimateTimeAtFinish(boatData[2]);
|
||||
// int legNumber = (int) boatData[3];
|
||||
clientYacht.setBoatStatus((int) boatData[4]);
|
||||
// if (legNumber != clientYacht.getLegNumber()) {
|
||||
// clientYacht.setLegNumber(legNumber);
|
||||
// }
|
||||
}
|
||||
|
||||
if (raceFinished) {
|
||||
close();
|
||||
loadFinishScreenView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePlayerPositions() {
|
||||
raceState.sortPlayers();
|
||||
for (ClientYacht yacht : raceState.getPlayerPositions()) {
|
||||
yacht.setPosition(raceState.getPlayerPositions().indexOf(yacht) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void close() {
|
||||
socketThread.setSocketToClose();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle the key-pressed event from the text field.
|
||||
* @param e The key event triggering this call
|
||||
*/
|
||||
private void keyPressed(KeyEvent e) {
|
||||
switch (e.getCode()) {
|
||||
case SPACE: // align with vmg
|
||||
socketThread.sendBoatAction(BoatAction.VMG); break;
|
||||
case PAGE_UP: // upwind
|
||||
socketThread.sendBoatAction(BoatAction.UPWIND); break;
|
||||
case PAGE_DOWN: // downwind
|
||||
socketThread.sendBoatAction(BoatAction.DOWNWIND); break;
|
||||
case ENTER: // tack/gybe
|
||||
socketThread.sendBoatAction(BoatAction.TACK_GYBE); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private 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
|
||||
socketThread.sendBoatAction(BoatAction.SAILS_IN);
|
||||
allBoatsMap.get(socketThread.getClientId()).toggleSail();
|
||||
break;
|
||||
case PAGE_UP:
|
||||
case PAGE_DOWN:
|
||||
socketThread.sendBoatAction(BoatAction.MAINTAIN_HEADING); break;
|
||||
}
|
||||
}
|
||||
|
||||
public RaceXMLData getCourseData() {
|
||||
return courseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells race view to show a collision animation.
|
||||
*/
|
||||
private void showCollisionAlert(YachtEventData yachtEventData) {
|
||||
// 33 is the agreed code to show collision
|
||||
if (yachtEventData.getEventId() == 33) {
|
||||
raceState.storeCollision(
|
||||
allBoatsMap.get(
|
||||
yachtEventData.getSubjectId().intValue()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,847 @@
|
||||
package seng302.visualiser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.Polygon;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.util.Duration;
|
||||
import seng302.model.ClientYacht;
|
||||
import seng302.gameServer.messages.RoundingSide;
|
||||
import seng302.model.ClientYacht;
|
||||
import seng302.model.Colors;
|
||||
import seng302.model.GeoPoint;
|
||||
import seng302.model.Limit;
|
||||
import seng302.model.mark.CompoundMark;
|
||||
import seng302.model.mark.Corner;
|
||||
import seng302.model.mark.Mark;
|
||||
import seng302.utilities.GeoUtility;
|
||||
import seng302.visualiser.fxObjects.AnnotationBox;
|
||||
import seng302.visualiser.fxObjects.BoatObject;
|
||||
import seng302.visualiser.fxObjects.CourseBoundary;
|
||||
import seng302.visualiser.fxObjects.Gate;
|
||||
import seng302.visualiser.fxObjects.MarkArrowFactory;
|
||||
import seng302.visualiser.fxObjects.Marker;
|
||||
import seng302.visualiser.map.Boundary;
|
||||
import seng302.visualiser.map.CanvasMap;
|
||||
|
||||
/**
|
||||
* Created by cir27 on 20/07/17.
|
||||
*/
|
||||
public class GameView extends Pane {
|
||||
|
||||
private double bufferSize = 50;
|
||||
private double panelWidth = 1260; // it should be 1280 but, minors 40 to cancel the bias.
|
||||
private double panelHeight = 960;
|
||||
private double canvasWidth = 1100;
|
||||
private double canvasHeight = 920;
|
||||
private boolean horizontalInversion = false;
|
||||
|
||||
private double distanceScaleFactor;
|
||||
private ScaleDirection scaleDirection;
|
||||
private GeoPoint minLatPoint, minLonPoint, maxLatPoint, maxLonPoint;
|
||||
private double referencePointX, referencePointY;
|
||||
private double metersPerPixelX, metersPerPixelY;
|
||||
|
||||
final double SCALE_DELTA = 1.1;
|
||||
|
||||
private Text fpsDisplay = new Text();
|
||||
private Polygon raceBorder = new CourseBoundary();
|
||||
|
||||
/* Note that if either of these is null then values for it have not been added and the other
|
||||
should be used as the limits of the map. */
|
||||
private List<Limit> borderPoints;
|
||||
private Map<Mark, Marker> markerObjects;
|
||||
|
||||
private Map<ClientYacht, BoatObject> boatObjects = new HashMap<>();
|
||||
private Map<ClientYacht, AnnotationBox> annotations = new HashMap<>();
|
||||
private ObservableList<Node> gameObjects;
|
||||
private BoatObject selectedBoat = null;
|
||||
private Group annotationsGroup = new Group();
|
||||
private Group wakesGroup = new Group();
|
||||
private Group boatObjectGroup = new Group();
|
||||
private Group trails = new Group();
|
||||
private Group markers = new Group();
|
||||
private List<CompoundMark> course = new ArrayList<>();
|
||||
|
||||
private ImageView mapImage = new ImageView();
|
||||
|
||||
//FRAME RATE
|
||||
|
||||
private AnimationTimer timer;
|
||||
private int NUM_SAMPLES = 10;
|
||||
private final long[] frameTimes = new long[NUM_SAMPLES];
|
||||
private Double frameRate = 60.0;
|
||||
private int frameTimeIndex = 0;
|
||||
private boolean arrayFilled = false;
|
||||
private ClientYacht playerYacht;
|
||||
private double windDir = 0.0;
|
||||
|
||||
double scaleFactor = 1;
|
||||
|
||||
private void zoomOut() {
|
||||
scaleFactor = 0.1;
|
||||
if (this.getScaleX() > 0.5) {
|
||||
this.setScaleX(this.getScaleX() - scaleFactor);
|
||||
this.setScaleY(this.getScaleY() - scaleFactor);
|
||||
}
|
||||
}
|
||||
|
||||
private void zoomIn() {
|
||||
scaleFactor = 0.10;
|
||||
if (this.getScaleX() < 2.5) {
|
||||
this.setScaleX(this.getScaleX() + scaleFactor);
|
||||
this.setScaleY(this.getScaleY() + scaleFactor);
|
||||
}
|
||||
}
|
||||
|
||||
private enum ScaleDirection {
|
||||
HORIZONTAL,
|
||||
VERTICAL
|
||||
}
|
||||
|
||||
|
||||
private void trackBoat() {
|
||||
if (selectedBoat != null) {
|
||||
double x = selectedBoat.getBoatLayoutX();
|
||||
double y = selectedBoat.getBoatLayoutY();
|
||||
double displacementX = this.getWidth();
|
||||
double displacementY = this.getHeight();
|
||||
this.setLayoutX((-x + (displacementX / 2.0)) * this.getScaleX());
|
||||
this.setLayoutY((-y + (displacementY / 2.0)) * this.getScaleY());
|
||||
} else {
|
||||
this.setLayoutX(0);
|
||||
this.setLayoutY(0);
|
||||
}
|
||||
}
|
||||
|
||||
public GameView () {
|
||||
gameObjects = this.getChildren();
|
||||
// create image view for map, bind panel size to image
|
||||
gameObjects.add(mapImage);
|
||||
gameObjects.add(raceBorder);
|
||||
gameObjects.add(markers);
|
||||
initializeTimer();
|
||||
}
|
||||
|
||||
private void initializeTimer() {
|
||||
Arrays.fill(frameTimes, 1_000_000_000 / 60);
|
||||
timer = new AnimationTimer() {
|
||||
private long lastTime = 0;
|
||||
private int FPSCount = 30;
|
||||
private Double frameRate = 60.0;
|
||||
private int index = 0;
|
||||
private boolean arrayFilled = false;
|
||||
private long sum = 1_000_000_000 / 3;
|
||||
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
trackBoat();
|
||||
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;
|
||||
if (frameTimeIndex == 0) {
|
||||
arrayFilled = true;
|
||||
}
|
||||
long elapsedNanos;
|
||||
if (arrayFilled) {
|
||||
elapsedNanos = now - oldFrameTime;
|
||||
long elapsedNanosPerFrame = elapsedNanos / frameTimes.length;
|
||||
frameRate = 1_000_000_000.0 / elapsedNanosPerFrame;
|
||||
if (FPSCount-- == 0) {
|
||||
FPSCount = 30;
|
||||
drawFps(frameRate);
|
||||
}
|
||||
}
|
||||
lastTime = now;
|
||||
}
|
||||
}
|
||||
boatObjects.forEach((boat, boatObject) -> boatObject.updateLocation());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.getLat(), minLonPoint.getLng());
|
||||
// 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.getLat(), minLonPoint.getLng());
|
||||
GeoPoint originPos = GeoUtility
|
||||
.getGeoCoordinate(topLeftPos, bearingFromTopLeftToOrigin, distanceFromTopLeftToOrigin);
|
||||
|
||||
// distance from origin corner to bottom right corner of the panel
|
||||
double distanceFromOriginToBottomRight = Math.sqrt(
|
||||
Math.pow(panelHeight * metersPerPixelY, 2) + Math
|
||||
.pow(panelWidth * metersPerPixelX, 2));
|
||||
double bearingFromOriginToBottomRight = Math
|
||||
.toDegrees(Math.atan2(panelWidth, -panelHeight));
|
||||
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());
|
||||
mapImage.fitWidthProperty().bind(((AnchorPane) this.getParent()).heightProperty());
|
||||
mapImage.fitHeightProperty().bind(((AnchorPane) this.getParent()).heightProperty());
|
||||
}
|
||||
|
||||
// TODO: 16/08/17 Break up this function
|
||||
/**
|
||||
* Adds a course to the GameView. The view is scaled accordingly unless a border is set in which
|
||||
* case the course is added relative ot the border.
|
||||
*
|
||||
* @param newCourse the mark objects that make up the course.
|
||||
* @param sequence The sequence the marks travel through
|
||||
*/
|
||||
public void updateCourse(List<CompoundMark> newCourse, List<Corner> sequence) {
|
||||
markerObjects = new HashMap<>();
|
||||
|
||||
for (Corner corner : sequence) { //Makes course out of all compound marks.
|
||||
for (CompoundMark compoundMark : newCourse) {
|
||||
if (corner.getCompoundMarkID() == compoundMark.getId()) {
|
||||
course.add(compoundMark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 16/08/17 Updating mark roundings here. It should not happen here. Nor should it be done this way.
|
||||
for (Corner corner : sequence){
|
||||
CompoundMark compoundMark = course.get(corner.getSeqID() - 1);
|
||||
compoundMark.setRoundingSide(
|
||||
RoundingSide.getRoundingSide(corner.getRounding())
|
||||
);
|
||||
}
|
||||
|
||||
final List<Gate> gates = new ArrayList<>();
|
||||
Paint colour = Color.BLACK;
|
||||
//Creates new markers
|
||||
for (CompoundMark cMark : newCourse) {
|
||||
//Set start and end colour
|
||||
if (cMark.getId() == sequence.get(0).getCompoundMarkID()) {
|
||||
colour = Color.GREEN;
|
||||
} else if (cMark.getId() == sequence.get(sequence.size() - 1).getCompoundMarkID()) {
|
||||
colour = Color.RED;
|
||||
}
|
||||
//Create mark dots
|
||||
for (Mark mark : cMark.getMarks()) {
|
||||
makeAndBindMarker(mark, colour);
|
||||
}
|
||||
//Create gate line
|
||||
if (cMark.isGate()) {
|
||||
for (int i = 1; i < cMark.getMarks().size(); i++) {
|
||||
gates.add(
|
||||
makeAndBindGate(
|
||||
markerObjects.get(cMark.getSubMark(i)),
|
||||
markerObjects.get(cMark.getSubMark(i + 1)),
|
||||
colour
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
colour = Color.BLACK;
|
||||
}
|
||||
|
||||
createMarkArrows(sequence);
|
||||
|
||||
//Scale race to markers if there is no border.
|
||||
if (borderPoints == null) {
|
||||
rescaleRace(new ArrayList<>(markerObjects.keySet()));
|
||||
}
|
||||
//Move the Markers to initial position.
|
||||
markerObjects.forEach(((mark, marker) -> {
|
||||
Point2D p2d = findScaledXY(mark.getLat(), mark.getLng());
|
||||
marker.setLayoutX(p2d.getX());
|
||||
marker.setLayoutY(p2d.getY());
|
||||
}));
|
||||
Platform.runLater(() -> {
|
||||
markers.getChildren().clear();
|
||||
markers.getChildren().addAll(gates);
|
||||
markers.getChildren().addAll(markerObjects.values());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates all the data needed for to create mark arrows. Requires that a course has been
|
||||
* added to the gameview.
|
||||
* @param sequence The order in which marks are traversed.
|
||||
*/
|
||||
private void createMarkArrows (List<Corner> sequence) {
|
||||
for (int i=1; i < sequence.size()-1; i++) { //General case.
|
||||
double averageLat = 0;
|
||||
double averageLng = 0;
|
||||
int numMarks = 0;
|
||||
for (Mark mark : course.get(i-1).getMarks()) {
|
||||
numMarks += 1;
|
||||
averageLat += mark.getLat();
|
||||
averageLng += mark.getLng();
|
||||
}
|
||||
GeoPoint lastMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks);
|
||||
numMarks = 0;
|
||||
averageLat = 0;
|
||||
averageLng = 0;
|
||||
for (Mark mark : course.get(i+1).getMarks()) {
|
||||
numMarks += 1;
|
||||
averageLat += mark.getLat();
|
||||
averageLng += mark.getLng();
|
||||
}
|
||||
GeoPoint nextMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks);
|
||||
// TODO: 16/08/17 This comparison doesn't need to exist but the alternative is to user server enum client side.
|
||||
for (Mark mark : course.get(i).getMarks()) {
|
||||
markerObjects.get(mark).addArrows(
|
||||
mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT,
|
||||
GeoUtility.getBearing(lastMarkAv, mark),
|
||||
GeoUtility.getBearing(mark, nextMarkAv)
|
||||
);
|
||||
}
|
||||
}
|
||||
createStartLineArrows();
|
||||
createFinishLineArrows();
|
||||
}
|
||||
|
||||
private void createStartLineArrows () {
|
||||
double averageLat = 0;
|
||||
double averageLng = 0;
|
||||
int numMarks = 0;
|
||||
for (Mark mark : course.get(1).getMarks()) {
|
||||
numMarks += 1;
|
||||
averageLat += mark.getLat();
|
||||
averageLng += mark.getLng();
|
||||
}
|
||||
GeoPoint firstMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks);
|
||||
for (Mark mark : course.get(0).getMarks()) {
|
||||
markerObjects.get(mark).addArrows(
|
||||
mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT,
|
||||
0d, //90
|
||||
GeoUtility.getBearing(mark, firstMarkAv)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void createFinishLineArrows () {
|
||||
double numMarks = 0;
|
||||
double averageLat = 0;
|
||||
double averageLng = 0;
|
||||
for (Mark mark : course.get(course.size()-2).getMarks()) {
|
||||
numMarks += 1;
|
||||
averageLat += mark.getLat();
|
||||
averageLng += mark.getLng();
|
||||
}
|
||||
GeoPoint secondToLastMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks);
|
||||
for (Mark mark : course.get(course.size()-1).getMarks()) {
|
||||
markerObjects.get(mark).addArrows(
|
||||
mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT,
|
||||
GeoUtility.getBearing(secondToLastMarkAv, mark),
|
||||
GeoUtility.getBearing(mark, mark)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Marker and binds it's position to the given Mark.
|
||||
*
|
||||
* @param observableMark The mark to bind the marker to.
|
||||
* @param colour The desired colour of the mark
|
||||
*/
|
||||
private void makeAndBindMarker(Mark observableMark, Paint colour) {
|
||||
Marker marker = new Marker(colour);
|
||||
// marker.addArrows(MarkArrowFactory.RoundingSide.PORT, ThreadLocalRandom.current().nextDouble(91, 180), ThreadLocalRandom.current().nextDouble(1, 90));
|
||||
markerObjects.put(observableMark, marker);
|
||||
observableMark.addPositionListener((mark, lat, lon) -> {
|
||||
Point2D p2d = findScaledXY(lat, lon);
|
||||
markerObjects.get(mark).setLayoutX(p2d.getX());
|
||||
markerObjects.get(mark).setLayoutY(p2d.getY());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new gate connecting the given marks.
|
||||
*
|
||||
* @param m1 The first Mark of the gate.
|
||||
* @param m2 The second Mark of the gate.
|
||||
* @param colour The desired colour of the gate.
|
||||
* @return the new gate.
|
||||
*/
|
||||
private Gate makeAndBindGate(Marker m1, Marker m2, Paint colour) {
|
||||
Gate gate = new Gate(colour);
|
||||
gate.startXProperty().bind(
|
||||
m1.layoutXProperty()
|
||||
);
|
||||
gate.startYProperty().bind(
|
||||
m1.layoutYProperty()
|
||||
);
|
||||
gate.endXProperty().bind(
|
||||
m2.layoutXProperty()
|
||||
);
|
||||
gate.endYProperty().bind(
|
||||
m2.layoutYProperty()
|
||||
);
|
||||
return gate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a border to the GameView and rescales to the size of the border, does not rescale if a
|
||||
* border already exists. Assumes the border is larger than the course.
|
||||
*
|
||||
* @param border the race border to be drawn.
|
||||
*/
|
||||
public void updateBorder(List<Limit> border) {
|
||||
if (borderPoints == null) {
|
||||
borderPoints = border;
|
||||
rescaleRace(new ArrayList<>(borderPoints));
|
||||
}
|
||||
List<Double> boundaryPoints = new ArrayList<>();
|
||||
for (Limit limit : border) {
|
||||
Point2D location = findScaledXY(limit.getLat(), limit.getLng());
|
||||
boundaryPoints.add(location.getX());
|
||||
boundaryPoints.add(location.getY());
|
||||
}
|
||||
raceBorder.getPoints().setAll(boundaryPoints);
|
||||
}
|
||||
|
||||
// TODO: 16/08/17 initialize zooming internal to GameView only
|
||||
/**
|
||||
* Enables zoom. Has to be called after this is added to a scene.
|
||||
*/
|
||||
public void enableZoom () {
|
||||
if (this.getScene() != null) {
|
||||
this.getScene().addEventHandler(KeyEvent.KEY_PRESSED, (event) -> {
|
||||
if (event.getCode() == KeyCode.Z) {
|
||||
zoomIn();
|
||||
} else if (event.getCode() == KeyCode.X) {
|
||||
zoomOut();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Rescales the race to the size of the window.
|
||||
*
|
||||
* @param limitingCoordinates the set of geo points that contains the extremities of the race.
|
||||
*/
|
||||
private void rescaleRace(List<GeoPoint> limitingCoordinates) {
|
||||
//Check is called once to avoid unnecessarily change the course limits once the race is running
|
||||
findMinMaxPoint(limitingCoordinates);
|
||||
double minLonToMaxLon = scaleRaceExtremities();
|
||||
calculateReferencePointLocation(minLonToMaxLon);
|
||||
// drawGoogleMap();
|
||||
}
|
||||
|
||||
private void setSelectedBoat(BoatObject bo, Boolean isSelected) {
|
||||
if (this.selectedBoat == bo && !isSelected) {
|
||||
this.selectedBoat = null;
|
||||
boatObjects.forEach((boat, group) ->
|
||||
group.setIsSelected(false)
|
||||
);
|
||||
} else if (isSelected) {
|
||||
this.selectedBoat = bo;
|
||||
for (BoatObject group : boatObjects.values()) {
|
||||
if (group != bo) {
|
||||
group.setIsSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws all the boats.
|
||||
* @param yachts The yachts to set in the race
|
||||
*/
|
||||
public void setBoats(List<ClientYacht> yachts) {
|
||||
BoatObject newBoat;
|
||||
final List<Group> wakes = new ArrayList<>();
|
||||
for (ClientYacht clientYacht : yachts) {
|
||||
Paint colour = clientYacht.getColour();
|
||||
newBoat = new BoatObject();
|
||||
newBoat.addSelectedBoatListener(this::setSelectedBoat);
|
||||
newBoat.setFill(colour);
|
||||
boatObjects.put(clientYacht, newBoat);
|
||||
createAndBindAnnotationBox(clientYacht, colour);
|
||||
// wakesGroup.getChildren().add(newBoat.getWake());
|
||||
wakes.add(newBoat.getWake());
|
||||
boatObjectGroup.getChildren().add(newBoat);
|
||||
trails.getChildren().add(newBoat.getTrail());
|
||||
// TODO: 1/08/17 Make this less vile to look at.
|
||||
clientYacht.addLocationListener((boat, lat, lon, heading, sailIn, velocity) -> {
|
||||
BoatObject bo = boatObjects.get(boat);
|
||||
Point2D p2d = findScaledXY(lat, lon);
|
||||
bo.moveTo(p2d.getX(), p2d.getY(), heading, velocity, sailIn, windDir);
|
||||
annotations.get(boat).setLocation(p2d.getX(), p2d.getY());
|
||||
bo.setTrajectory(
|
||||
heading,
|
||||
velocity,
|
||||
metersPerPixelX,
|
||||
metersPerPixelY);
|
||||
});
|
||||
}
|
||||
annotationsGroup.getChildren().addAll(annotations.values());
|
||||
Platform.runLater(() -> {
|
||||
gameObjects.addAll(trails);
|
||||
gameObjects.addAll(wakes);
|
||||
gameObjects.addAll(annotationsGroup);
|
||||
gameObjects.addAll(boatObjectGroup);
|
||||
});
|
||||
}
|
||||
|
||||
private void createAndBindAnnotationBox(ClientYacht clientYacht, Paint colour) {
|
||||
AnnotationBox newAnnotation = new AnnotationBox();
|
||||
newAnnotation.setFill(colour);
|
||||
newAnnotation.addAnnotation(
|
||||
"name", "Player: " + clientYacht.getShortName()
|
||||
);
|
||||
// newAnnotation.addAnnotation(
|
||||
// "velocity",
|
||||
// yacht.getVelocityProperty(),
|
||||
// (velocity) -> String.format("Speed: %.2f ms", velocity.doubleValue())
|
||||
// );
|
||||
// newAnnotation.addAnnotation(
|
||||
// "nextMark",
|
||||
// yacht.timeTillNextProperty(),
|
||||
// (time) -> {
|
||||
// DateFormat format = new SimpleDateFormat("mm:ss");
|
||||
// return format.format(time);
|
||||
// }
|
||||
// );
|
||||
// newAnnotation.addAnnotation(
|
||||
// "lastMark",
|
||||
// yacht.timeTillNextProperty(),
|
||||
// (time) -> {
|
||||
// DateFormat format = new SimpleDateFormat("mm:ss");
|
||||
// return format.format(time);
|
||||
// }
|
||||
// );
|
||||
annotations.put(clientYacht, newAnnotation);
|
||||
}
|
||||
|
||||
private void drawFps(Double fps) {
|
||||
Platform.runLater(() -> fpsDisplay.setText(String.format("%d FPS", Math.round(fps))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the class variables minLatPoint, maxLatPoint, minLonPoint, maxLonPoint to the point with
|
||||
* the leftmost point, rightmost point, southern most point and northern most point
|
||||
* respectively.
|
||||
*/
|
||||
private void findMinMaxPoint(List<GeoPoint> points) {
|
||||
List<GeoPoint> sortedPoints = new ArrayList<>(points);
|
||||
sortedPoints.sort(Comparator.comparingDouble(GeoPoint::getLat));
|
||||
minLatPoint = new GeoPoint(sortedPoints.get(0).getLat(), sortedPoints.get(0).getLng());
|
||||
GeoPoint maxLat = sortedPoints.get(sortedPoints.size() - 1);
|
||||
maxLatPoint = new GeoPoint(maxLat.getLat(), maxLat.getLng());
|
||||
|
||||
sortedPoints.sort(Comparator.comparingDouble(GeoPoint::getLng));
|
||||
minLonPoint = new GeoPoint(sortedPoints.get(0).getLat(), sortedPoints.get(0).getLng());
|
||||
GeoPoint maxLon = sortedPoints.get(sortedPoints.size() - 1);
|
||||
maxLonPoint = new GeoPoint(maxLon.getLat(), maxLon.getLng());
|
||||
if (maxLonPoint.getLng() - minLonPoint.getLng() > 180) {
|
||||
horizontalInversion = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private void calculateReferencePointLocation(double minLonToMaxLon) {
|
||||
GeoPoint referencePoint = minLatPoint;
|
||||
double referenceAngle;
|
||||
|
||||
if (scaleDirection == ScaleDirection.HORIZONTAL) {
|
||||
referenceAngle = Math.abs(
|
||||
GeoUtility.getBearingRad(referencePoint, minLonPoint)
|
||||
);
|
||||
referencePointX =
|
||||
bufferSize + distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility
|
||||
.getDistance(referencePoint, minLonPoint);
|
||||
referenceAngle = Math.abs(GeoUtility.getDistance(referencePoint, maxLatPoint));
|
||||
referencePointY = canvasHeight - (bufferSize + bufferSize);
|
||||
referencePointY -= distanceScaleFactor * Math.cos(referenceAngle) * GeoUtility
|
||||
.getDistance(referencePoint, maxLatPoint);
|
||||
referencePointY = referencePointY / 2;
|
||||
referencePointY += bufferSize;
|
||||
referencePointY += distanceScaleFactor * Math.cos(referenceAngle) * GeoUtility
|
||||
.getDistance(referencePoint, maxLatPoint);
|
||||
} else {
|
||||
referencePointY = canvasHeight - bufferSize;
|
||||
referenceAngle = Math.abs(
|
||||
Math.toRadians(
|
||||
GeoUtility.getDistance(referencePoint, minLonPoint)
|
||||
)
|
||||
);
|
||||
referencePointX = bufferSize;
|
||||
referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility
|
||||
.getDistance(referencePoint, minLonPoint);
|
||||
referencePointX +=
|
||||
((canvasWidth - (bufferSize + bufferSize)) - (minLonToMaxLon * distanceScaleFactor))
|
||||
/ 2;
|
||||
}
|
||||
if (horizontalInversion) {
|
||||
referencePointX = canvasWidth - bufferSize - (referencePointX - bufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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(
|
||||
GeoUtility.getBearingRad(minLatPoint, maxLatPoint)
|
||||
);
|
||||
double vertDistance =
|
||||
Math.cos(vertAngle) * GeoUtility.getDistance(minLatPoint, maxLatPoint);
|
||||
double horiAngle = Math.abs(
|
||||
GeoUtility.getBearingRad(minLonPoint, maxLonPoint)
|
||||
);
|
||||
if (horiAngle <= (Math.PI / 2)) {
|
||||
horiAngle = (Math.PI / 2) - horiAngle;
|
||||
} else {
|
||||
horiAngle = horiAngle - (Math.PI / 2);
|
||||
}
|
||||
double horiDistance =
|
||||
Math.cos(horiAngle) * GeoUtility.getDistance(minLonPoint, maxLonPoint);
|
||||
|
||||
double vertScale = (canvasHeight - (bufferSize + bufferSize)) / vertDistance;
|
||||
|
||||
if ((horiDistance * vertScale) > (canvasWidth - (bufferSize + bufferSize))) {
|
||||
distanceScaleFactor = (canvasWidth - (bufferSize + bufferSize)) / horiDistance;
|
||||
scaleDirection = ScaleDirection.HORIZONTAL;
|
||||
} else {
|
||||
distanceScaleFactor = vertScale;
|
||||
scaleDirection = ScaleDirection.VERTICAL;
|
||||
}
|
||||
return horiDistance;
|
||||
}
|
||||
|
||||
private Point2D findScaledXY(GeoPoint unscaled) {
|
||||
return findScaledXY(unscaled.getLat(), unscaled.getLng());
|
||||
}
|
||||
|
||||
private Point2D findScaledXY(double unscaledLat, double unscaledLon) {
|
||||
double distanceFromReference;
|
||||
double angleFromReference;
|
||||
double xAxisLocation = referencePointX;
|
||||
double yAxisLocation = referencePointY;
|
||||
|
||||
angleFromReference = GeoUtility.getBearingRad(
|
||||
minLatPoint, new GeoPoint(unscaledLat, unscaledLon)
|
||||
);
|
||||
distanceFromReference = GeoUtility.getDistance(
|
||||
minLatPoint, new GeoPoint(unscaledLat, unscaledLon)
|
||||
);
|
||||
if (angleFromReference >= 0 && angleFromReference <= Math.PI / 2) {
|
||||
xAxisLocation += Math
|
||||
.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
|
||||
yAxisLocation -= Math
|
||||
.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
|
||||
} else if (angleFromReference >= 0) {
|
||||
angleFromReference = angleFromReference - Math.PI / 2;
|
||||
xAxisLocation += Math
|
||||
.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
|
||||
yAxisLocation += Math
|
||||
.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
|
||||
} else if (angleFromReference < 0 && angleFromReference >= -Math.PI / 2) {
|
||||
angleFromReference = Math.abs(angleFromReference);
|
||||
xAxisLocation -= Math
|
||||
.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
|
||||
yAxisLocation -= Math
|
||||
.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
|
||||
} else {
|
||||
angleFromReference = Math.abs(angleFromReference) - Math.PI / 2;
|
||||
xAxisLocation -= Math
|
||||
.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
|
||||
yAxisLocation += Math
|
||||
.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
|
||||
}
|
||||
if (horizontalInversion) {
|
||||
xAxisLocation = canvasWidth - bufferSize - (xAxisLocation - bufferSize);
|
||||
}
|
||||
return new Point2D(xAxisLocation, yAxisLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the number of meters per pixel.
|
||||
*/
|
||||
private void findMetersPerPixel() {
|
||||
Point2D p1, p2;
|
||||
GeoPoint g1, g2;
|
||||
double theta, distance, dx, dy, dHorizontal, dVertical;
|
||||
g1 = new GeoPoint(maxLatPoint.getLat(), minLonPoint.getLng());
|
||||
g2 = new GeoPoint(minLatPoint.getLat(), maxLatPoint.getLng());
|
||||
p1 = findScaledXY(new GeoPoint(maxLatPoint.getLat(), minLonPoint.getLng()));
|
||||
p2 = findScaledXY(new GeoPoint(minLatPoint.getLat(), maxLatPoint.getLng()));
|
||||
theta = GeoUtility.getBearingRad(g1, g2);
|
||||
distance = GeoUtility.getDistance(g1, g2);
|
||||
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;
|
||||
}
|
||||
|
||||
public void setAnnotationVisibilities(boolean teamName, boolean velocity, boolean estTime,
|
||||
boolean legTime, boolean trail, boolean wake) {
|
||||
for (BoatObject boatObject : boatObjects.values()) {
|
||||
boatObject.setVisibility(teamName, velocity, estTime, legTime, trail, wake);
|
||||
}
|
||||
for (AnnotationBox ag : annotations.values()) {
|
||||
ag.setAnnotationVisibility("name", teamName);
|
||||
ag.setAnnotationVisibility("velocity", velocity);
|
||||
ag.setAnnotationVisibility("nextMark", estTime);
|
||||
ag.setAnnotationVisibility("lastMark", legTime);
|
||||
}
|
||||
}
|
||||
|
||||
public void setFPSVisibility(boolean visibility) {
|
||||
fpsDisplay.setVisible(visibility);
|
||||
}
|
||||
|
||||
public void selectBoat(ClientYacht selectedClientYacht) {
|
||||
boatObjects.forEach((boat, group) ->
|
||||
group.setIsSelected(boat == selectedClientYacht)
|
||||
);
|
||||
}
|
||||
|
||||
public void pauseRace() {
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
public void setWindDir(double windDir) {
|
||||
this.windDir = windDir;
|
||||
}
|
||||
|
||||
public void startRace() {
|
||||
timer.start();
|
||||
}
|
||||
|
||||
public ClientYacht getPlayerYacht() {
|
||||
return playerYacht;
|
||||
}
|
||||
|
||||
public void setBoatAsPlayer (ClientYacht playerYacht) {
|
||||
this.playerYacht = playerYacht;
|
||||
playerYacht.toggleSail();
|
||||
boatObjects.get(playerYacht).setAsPlayer();
|
||||
CompoundMark currentMark = course.get(playerYacht.getLegNumber());
|
||||
for (Mark mark : currentMark.getMarks()) {
|
||||
markerObjects.get(mark).showNextExitArrow();
|
||||
}
|
||||
annotations.get(playerYacht).addAnnotation(
|
||||
"velocity",
|
||||
playerYacht.getVelocityProperty(),
|
||||
(velocity) -> String.format("Speed: %.2f ms", velocity.doubleValue())
|
||||
);
|
||||
Platform.runLater(() -> {
|
||||
boatObjectGroup.getChildren().remove(boatObjects.get(playerYacht));
|
||||
gameObjects.add(boatObjects.get(playerYacht));
|
||||
annotationsGroup.getChildren().remove(annotations.get(playerYacht));
|
||||
gameObjects.add(annotations.get(playerYacht));
|
||||
});
|
||||
playerYacht.addMarkRoundingListener(this::updateMarkArrows);
|
||||
}
|
||||
|
||||
private void updateMarkArrows (ClientYacht yacht, CompoundMark compoundMark, int legNumber) {
|
||||
//Only show arrows for this and next leg.
|
||||
if (compoundMark != null) {
|
||||
for (Mark mark : compoundMark.getMarks()) {
|
||||
markerObjects.get(mark).showNextExitArrow();
|
||||
}
|
||||
}
|
||||
CompoundMark nextMark = null;
|
||||
if (legNumber < course.size() - 1) {
|
||||
nextMark = course.get(legNumber);
|
||||
for (Mark mark : nextMark.getMarks()) {
|
||||
markerObjects.get(mark).showNextEnterArrow();
|
||||
}
|
||||
}
|
||||
if (legNumber - 2 >= 0) {
|
||||
CompoundMark lastMark = course.get(Math.max(0, legNumber - 2));
|
||||
if (lastMark != nextMark) {
|
||||
for (Mark mark : lastMark.getMarks()) {
|
||||
markerObjects.get(mark).hideAllArrows();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given yacht geopoint by race view controller, drawCollision will calculate canvas X and Y and
|
||||
* display a flashing red circle on collision point.
|
||||
*
|
||||
* @param collisionPoint yacht collision point
|
||||
*/
|
||||
public void drawCollision(GeoPoint collisionPoint) {
|
||||
Point2D point = findScaledXY(collisionPoint);
|
||||
double circleRadius = 0.0;
|
||||
Circle circle = new Circle(point.getX(), point.getY(), circleRadius, Color.RED);
|
||||
|
||||
circle.setFill(Color.TRANSPARENT);
|
||||
circle.setStroke(Color.RED);
|
||||
circle.setStrokeWidth(3);
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
timeline.setCycleCount(1);
|
||||
|
||||
KeyFrame keyframe1 = new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(circle.radiusProperty(), 0),
|
||||
new KeyValue(circle.strokeProperty(), Color.TRANSPARENT));
|
||||
KeyFrame keyFrame2 = new KeyFrame(new Duration(1000),
|
||||
new KeyValue(circle.radiusProperty(), 50),
|
||||
new KeyValue(circle.strokeProperty(), Color.RED));
|
||||
KeyFrame keyFrame3 = new KeyFrame(new Duration(1500),
|
||||
new KeyValue(circle.strokeProperty(), Color.TRANSPARENT));
|
||||
|
||||
timeline.getKeyFrames().addAll(keyframe1, keyFrame2, keyFrame3);
|
||||
|
||||
Platform.runLater(() -> gameObjects.add(circle));
|
||||
timeline.setOnFinished(event -> Platform.runLater(() -> gameObjects.remove(circle)));
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
public void setFrameRateFXText(Text fpsDisplay) {
|
||||
this.fpsDisplay = null;
|
||||
this.fpsDisplay = fpsDisplay;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package seng302.visualiser.controllers;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import seng302.gameServer.messages.CustomizeRequestType;
|
||||
import seng302.visualiser.ClientToServerThread;
|
||||
|
||||
public class CustomizationController {
|
||||
|
||||
@FXML
|
||||
private TextField nameField;
|
||||
|
||||
@FXML
|
||||
private ColorPicker boatColorPicker;
|
||||
|
||||
@FXML
|
||||
private Button customizeSubmit;
|
||||
|
||||
private LobbyController lc;
|
||||
private ClientToServerThread socketThread;
|
||||
private Stage windowStage;
|
||||
|
||||
public void initialize() {
|
||||
|
||||
}
|
||||
|
||||
public void setServerThread(ClientToServerThread ctsThread) {
|
||||
this.socketThread = ctsThread;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void submitCustomization() {
|
||||
System.out.println("Attempting to send");
|
||||
socketThread.sendCustomizationRequest(CustomizeRequestType.NAME, nameField.getText().getBytes());
|
||||
// TODO: 16/08/17 ajm412: Turn colors into byte array.
|
||||
Color color = boatColorPicker.getValue();
|
||||
|
||||
short red = (short) (color.getRed() * 255);
|
||||
short green = (short) (color.getGreen() * 255);
|
||||
short blue = (short) (color.getBlue() * 255);
|
||||
|
||||
byte[] colorArray = new byte[3];
|
||||
|
||||
colorArray[0] = (byte) red;
|
||||
colorArray[1] = (byte) green;
|
||||
colorArray[2] = (byte) blue;
|
||||
|
||||
socketThread.sendCustomizationRequest(CustomizeRequestType.COLOR, colorArray);
|
||||
lc.setPlayersColor(color);
|
||||
windowStage.close();
|
||||
}
|
||||
|
||||
public void setLobbyController(LobbyController lc) {
|
||||
this.lc = lc;
|
||||
}
|
||||
|
||||
public void setStage(Stage stage) {
|
||||
this.windowStage = stage;
|
||||
}
|
||||
|
||||
public void setPlayerName(String name) {
|
||||
this.nameField.setText(name);
|
||||
}
|
||||
|
||||
public void setPlayerColor(Color playerColor) {
|
||||
this.boatColorPicker.setValue(playerColor);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package seng302.visualiser.controllers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
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.model.ClientYacht;
|
||||
|
||||
public class FinishScreenViewController implements Initializable {
|
||||
|
||||
@FXML
|
||||
private GridPane finishScreenGridPane;
|
||||
@FXML
|
||||
private TableView<ClientYacht> finishOrderTable;
|
||||
@FXML
|
||||
private TableColumn<ClientYacht, String> posCol;
|
||||
@FXML
|
||||
private TableColumn<ClientYacht, String> boatNameCol;
|
||||
@FXML
|
||||
private TableColumn<ClientYacht, String> shortNameCol;
|
||||
@FXML
|
||||
private TableColumn<ClientYacht, String> countryCol;
|
||||
|
||||
ObservableList<ClientYacht> data = FXCollections.observableArrayList();
|
||||
|
||||
@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
|
||||
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")
|
||||
);
|
||||
finishOrderTable.refresh();
|
||||
}
|
||||
|
||||
public void setFinishers(Collection<ClientYacht> participants) {
|
||||
List<ClientYacht> sorted = new ArrayList<>(participants);
|
||||
sorted.sort(Comparator.comparingInt(ClientYacht::getPlacing));
|
||||
finishOrderTable.getItems().setAll(sorted);
|
||||
}
|
||||
|
||||
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,248 @@
|
||||
package seng302.visualiser.controllers;
|
||||
|
||||
import com.sun.media.jfxmedia.logging.Logger;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.Stage;
|
||||
import seng302.gameServer.GameStages;
|
||||
import seng302.gameServer.GameState;
|
||||
import seng302.model.Colors;
|
||||
import seng302.model.RaceState;
|
||||
import seng302.visualiser.ClientToServerThread;
|
||||
|
||||
/**
|
||||
* A class describing the actions of the lobby screen
|
||||
* Created by wmu16 on 10/07/17.
|
||||
*/
|
||||
public class LobbyController {
|
||||
|
||||
public enum CloseStatus {
|
||||
LEAVE,
|
||||
READY
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface LobbyCloseListener {
|
||||
void notify(CloseStatus exitCause);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private Text lobbyIpText;
|
||||
@FXML
|
||||
private Button readyButton;
|
||||
@FXML
|
||||
private Button customizeButton;
|
||||
@FXML
|
||||
private TextArea playerOneTxt;
|
||||
@FXML
|
||||
private TextArea playerTwoTxt;
|
||||
@FXML
|
||||
private TextArea playerThreeTxt;
|
||||
@FXML
|
||||
private TextArea playerFourTxt;
|
||||
@FXML
|
||||
private TextArea playerFiveTxt;
|
||||
@FXML
|
||||
private TextArea playerSixTxt;
|
||||
@FXML
|
||||
private TextArea playerSevenTxt;
|
||||
@FXML
|
||||
private TextArea playerEightTxt;
|
||||
@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;
|
||||
@FXML
|
||||
private Text timeUntilStart;
|
||||
@FXML
|
||||
private Text courseNameText;
|
||||
|
||||
private List<ImageView> imageViews = new ArrayList<>();
|
||||
private List<TextArea> listViews = new ArrayList<>();
|
||||
private RaceState raceState;
|
||||
|
||||
private ClientToServerThread socketThread;
|
||||
|
||||
private Stage customizeStage;
|
||||
|
||||
private Color playersColor;
|
||||
|
||||
private int MAX_NUM_PLAYERS = 8;
|
||||
private Integer playerID;
|
||||
|
||||
private List<LobbyCloseListener> lobbyListeners = new ArrayList<>();
|
||||
private ObservableList<String> players;
|
||||
|
||||
/**
|
||||
* Add all FXObjects to lists and initialize images.
|
||||
*/
|
||||
public void initialize() {
|
||||
Collections.addAll(listViews,
|
||||
playerOneTxt, playerTwoTxt, playerThreeTxt, playerFourTxt, playerFiveTxt, playerSixTxt,
|
||||
playerSevenTxt, playerEightTxt
|
||||
);
|
||||
Collections.addAll(imageViews,
|
||||
firstImageView, secondImageView, thirdImageView, fourthImageView,
|
||||
fifthImageView, sixthImageView, seventhImageView, eighthImageView
|
||||
);
|
||||
initialiseImageView();
|
||||
|
||||
timeUntilStart.setText("Waiting For Host...");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates player names.
|
||||
*/
|
||||
private void updatePlayers() {
|
||||
//Update players if one added.
|
||||
for (int i = 0; i < players.size(); i++) {
|
||||
listViews.get(i).setText(players.get(i));
|
||||
if (playerID == (i + 1)) {
|
||||
listViews.get(i).setText(listViews.get(i).getText() + " (YOU)");
|
||||
}
|
||||
imageViews.get(i).setVisible(true);
|
||||
}
|
||||
//Update empty text fields if player left.
|
||||
for (int i = MAX_NUM_PLAYERS-1; i >= players.size(); i--) {
|
||||
listViews.get(i).setText("");
|
||||
imageViews.get(i).setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all images and hides them till players join.
|
||||
*/
|
||||
private void initialiseImageView() {
|
||||
for (ImageView viewer : imageViews) {
|
||||
viewer.setImage(
|
||||
new Image(
|
||||
RaceViewController.class.getResourceAsStream(
|
||||
"/pics/sail.png")
|
||||
)
|
||||
);
|
||||
viewer.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void customize() {
|
||||
Parent root;
|
||||
try {
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(LobbyController.class.getResource("/views/customizeView.fxml"));
|
||||
root = fxmlLoader.load();
|
||||
root.getStylesheets().add("/css/master.css");
|
||||
customizeStage = new Stage();
|
||||
customizeStage.setTitle("Customize Boat");
|
||||
customizeStage.setScene(new Scene(root, 700, 450));
|
||||
CustomizationController cc = fxmlLoader.getController();
|
||||
cc.setServerThread(this.socketThread);
|
||||
cc.setPlayerName(this.players.get(playerID - 1));
|
||||
|
||||
if (this.playersColor == null) {
|
||||
this.playersColor = Colors.getColor(playerID - 1);
|
||||
}
|
||||
|
||||
cc.setPlayerColor(this.playersColor);
|
||||
cc.setStage(customizeStage); // pass the stage through so it can be closed later.
|
||||
cc.setLobbyController(this);
|
||||
customizeStage.show();
|
||||
} catch (IOException e) {
|
||||
Logger.logMsg(4, "Failed to load Customization View from resources.");
|
||||
}
|
||||
}
|
||||
|
||||
public void setSocketThread(ClientToServerThread thread) {
|
||||
this.socketThread = thread;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void leaveLobbyButtonPressed() {
|
||||
// TODO: 10/07/17 wmu16 - Finish function!
|
||||
GameState.setCurrentStage(GameStages.CANCELLED);
|
||||
// TODO: 20/07/17 wmu16 - Implement some way of terminating the game
|
||||
for (LobbyCloseListener readyListener : lobbyListeners)
|
||||
readyListener.notify(CloseStatus.LEAVE);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void readyButtonPressed() {
|
||||
GameState.setCurrentStage(GameStages.PRE_RACE);
|
||||
// Do countdown logic here
|
||||
|
||||
for (LobbyCloseListener readyListener : lobbyListeners)
|
||||
readyListener.notify(CloseStatus.READY);
|
||||
customizeButton.setDisable(true);
|
||||
}
|
||||
|
||||
public void setTitle (String title) {
|
||||
lobbyIpText.setText(title);
|
||||
}
|
||||
|
||||
public void setCourseName(String courseName){
|
||||
courseNameText.setText(courseName);
|
||||
}
|
||||
|
||||
public void addCloseListener(LobbyCloseListener listener) {
|
||||
lobbyListeners.add(listener);
|
||||
}
|
||||
|
||||
public void setPlayerListSource (ObservableList<String> players) {
|
||||
this.players = players;
|
||||
players.addListener((ListChangeListener<? super String>) (lcl) ->
|
||||
Platform.runLater(this::updatePlayers)
|
||||
);
|
||||
Platform.runLater(this::updatePlayers);
|
||||
}
|
||||
|
||||
public void setPlayerID(Integer id) {
|
||||
playerID = id;
|
||||
}
|
||||
|
||||
public void updateRaceState(RaceState raceState){
|
||||
this.raceState = raceState;
|
||||
/*if (this.customizeStage != null) {
|
||||
this.customizeStage.close();
|
||||
}*/ // TODO: 17/08/17 ajm412: close the customization window if the host starts the game while customizing
|
||||
if (!customizeButton.isDisabled()) {
|
||||
customizeButton.setDisable(true);
|
||||
}
|
||||
timeUntilStart.setText("Starting in: " + raceState.getRaceTimeStr());
|
||||
}
|
||||
|
||||
public void disableReadyButton () {
|
||||
readyButton.setDisable(true);
|
||||
readyButton.setVisible(false);
|
||||
}
|
||||
|
||||
public void setPlayersColor(Color playerColor) {
|
||||
this.playersColor = playerColor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
package seng302.visualiser.controllers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
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.Data;
|
||||
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.StringConverter;
|
||||
import seng302.gameServer.messages.BoatStatus;
|
||||
import seng302.model.ClientYacht;
|
||||
import seng302.model.RaceState;
|
||||
import seng302.model.mark.CompoundMark;
|
||||
import seng302.model.mark.Mark;
|
||||
import seng302.model.stream.xml.parser.RaceXMLData;
|
||||
import seng302.visualiser.GameView;
|
||||
import seng302.visualiser.controllers.annotations.Annotation;
|
||||
import seng302.visualiser.controllers.annotations.ImportantAnnotationController;
|
||||
import seng302.visualiser.controllers.annotations.ImportantAnnotationDelegate;
|
||||
import seng302.visualiser.controllers.annotations.ImportantAnnotationsState;
|
||||
import seng302.visualiser.fxObjects.BoatObject;
|
||||
|
||||
/**
|
||||
* Controller class that manages the display of a race
|
||||
*/
|
||||
public class RaceViewController extends Thread implements ImportantAnnotationDelegate {
|
||||
|
||||
@FXML
|
||||
private LineChart<String, Double> raceSparkLine;
|
||||
@FXML
|
||||
private NumberAxis sparklineYAxis;
|
||||
@FXML
|
||||
private VBox positionVbox;
|
||||
@FXML
|
||||
private CheckBox toggleFps;
|
||||
@FXML
|
||||
private Text timerLabel;
|
||||
@FXML
|
||||
private AnchorPane contentAnchorPane;
|
||||
@FXML
|
||||
private Text windArrowText, windDirectionText;
|
||||
@FXML
|
||||
private Slider annotationSlider;
|
||||
@FXML
|
||||
private Button selectAnnotationBtn;
|
||||
@FXML
|
||||
private ComboBox<ClientYacht> yachtSelectionComboBox;
|
||||
@FXML
|
||||
private Text fpsDisplay;
|
||||
@FXML
|
||||
private Text windSpeedText;
|
||||
|
||||
//Race Data
|
||||
private Map<Integer, ClientYacht> participants;
|
||||
private Map<Integer, CompoundMark> markers;
|
||||
private RaceXMLData courseData;
|
||||
private GameView gameView;
|
||||
private RaceState raceState;
|
||||
|
||||
private Timeline timerTimeline;
|
||||
private Timer timer = new Timer();
|
||||
private List<Series<String, Double>> sparkLineData = new ArrayList<>();
|
||||
private ImportantAnnotationsState importantAnnotations;
|
||||
|
||||
public void initialize() {
|
||||
// Load a default important annotation state
|
||||
importantAnnotations = new ImportantAnnotationsState();
|
||||
|
||||
//Formatting the y axis of the sparkline
|
||||
// raceSparkLine.getYAxis().setRotate(180);
|
||||
// raceSparkLine.getYAxis().setTickLabelRotation(180);
|
||||
// raceSparkLine.getYAxis().setTranslateX(-5);
|
||||
raceSparkLine.visibleProperty().setValue(false);
|
||||
raceSparkLine.getYAxis().setAutoRanging(false);
|
||||
sparklineYAxis.setTickMarkVisible(false);
|
||||
|
||||
positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString());
|
||||
|
||||
selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView());
|
||||
}
|
||||
|
||||
public void loadRace (
|
||||
Map<Integer, ClientYacht> participants, RaceXMLData raceData, RaceState raceState,
|
||||
ClientYacht player) {
|
||||
|
||||
this.participants = participants;
|
||||
this.courseData = raceData;
|
||||
this.markers = raceData.getCompoundMarks();
|
||||
this.raceState = raceState;
|
||||
|
||||
initializeUpdateTimer();
|
||||
initialiseFPSCheckBox();
|
||||
initialiseAnnotationSlider();
|
||||
initialiseBoatSelectionComboBox();
|
||||
initialiseSparkLine();
|
||||
|
||||
raceState.getPlayerPositions().addListener((ListChangeListener<ClientYacht>) c -> {
|
||||
while (c.next()) {
|
||||
if (c.wasPermutated()) {
|
||||
updateOrder(raceState.getPlayerPositions());
|
||||
updateSparkLine();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateOrder(raceState.getPlayerPositions());
|
||||
gameView = new GameView();
|
||||
gameView.setFrameRateFXText(fpsDisplay);
|
||||
Platform.runLater(() -> contentAnchorPane.getChildren().add(0, gameView));
|
||||
gameView.setBoats(new ArrayList<>(participants.values()));
|
||||
gameView.updateBorder(raceData.getCourseLimit());
|
||||
gameView.updateCourse(
|
||||
new ArrayList<>(raceData.getCompoundMarks().values()), raceData.getMarkSequence()
|
||||
);
|
||||
gameView.enableZoom();
|
||||
gameView.setBoatAsPlayer(player);
|
||||
gameView.startRace();
|
||||
|
||||
raceState.addCollisionListener(gameView::drawCollision);
|
||||
raceState.windDirectionProperty().addListener((obs, oldDirection, newDirection) -> {
|
||||
gameView.setWindDir(newDirection.doubleValue());
|
||||
updateWindDirection(newDirection.doubleValue());
|
||||
});
|
||||
raceState.windSpeedProperty().addListener((obs, oldSpeed, newSpeed) -> {
|
||||
updateWindSpeed(newSpeed.doubleValue());
|
||||
});
|
||||
updateWindDirection(raceState.windDirectionProperty().doubleValue());
|
||||
updateWindSpeed(raceState.getWindSpeed());
|
||||
gameView.setWindDir(raceState.windDirectionProperty().doubleValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void initialiseFPSCheckBox() {
|
||||
toggleFps.selectedProperty().addListener((obs, oldVal, newVal) ->
|
||||
gameView.setFPSVisibility(toggleFps.isSelected())
|
||||
);
|
||||
}
|
||||
|
||||
private void initialiseAnnotationSlider() {
|
||||
annotationSlider.setLabelFormatter(new StringConverter<Double>() {
|
||||
@Override
|
||||
public String toString(Double n) {
|
||||
if (n == 0) {
|
||||
return "None";
|
||||
}
|
||||
if (n == 1) {
|
||||
return "Important";
|
||||
}
|
||||
if (n == 2) {
|
||||
return "All";
|
||||
}
|
||||
return "All";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double fromString(String s) {
|
||||
switch (s) {
|
||||
case "None":
|
||||
return 0d;
|
||||
case "Important":
|
||||
return 1d;
|
||||
case "All":
|
||||
return 2d;
|
||||
|
||||
default:
|
||||
return 2d;
|
||||
}
|
||||
}
|
||||
});
|
||||
annotationSlider.setValue(2);
|
||||
annotationSlider.valueProperty().addListener((obs, oldVal, newVal) ->
|
||||
setAnnotations((int) annotationSlider.getValue())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to add any new yachts into the race that may have started late or not have had data received yet
|
||||
*/
|
||||
private void updateSparkLine(){
|
||||
// TODO: 2/08/17 there is about 0 chance of this working. Once we are keeping track of boat positions it can be fixed.
|
||||
// Collect the racing yachts that aren't already in the chart
|
||||
sparkLineData.clear();
|
||||
List<ClientYacht> sparkLineCandidates = new ArrayList<>(participants.values());
|
||||
// Create a new data series for new yachts
|
||||
sparkLineCandidates
|
||||
.stream()
|
||||
.filter(yacht -> yacht.getPosition() != null)
|
||||
.forEach(yacht -> {
|
||||
Series<String, Double> yachtData = new Series<>();
|
||||
yachtData.setName(yacht.getSourceId().toString());
|
||||
yachtData.getData().add(
|
||||
new Data<>(
|
||||
Integer.toString(yacht.getLegNumber()),
|
||||
1.0 + participants.size() - yacht.getPosition()
|
||||
)
|
||||
);
|
||||
sparkLineData.add(yachtData);
|
||||
});
|
||||
|
||||
// Lambda function to sort the series in order of leg (later legs shown more to the right)
|
||||
sparkLineData.sort((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)
|
||||
Platform.runLater(() -> {
|
||||
sparkLineData
|
||||
.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()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void initialiseSparkLine() {
|
||||
sparklineYAxis.setUpperBound(participants.size() + 1);
|
||||
raceSparkLine.setCreateSymbols(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the yachts sparkline of the desired yacht 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
|
||||
*/
|
||||
void updateYachtPositionSparkline(ClientYacht yacht, Integer legNumber){
|
||||
for (XYChart.Series<String, Double> positionData : sparkLineData) {
|
||||
positionData.getData().add(
|
||||
new Data<>(
|
||||
Integer.toString(legNumber),
|
||||
1.0 + participants.size() - yacht.getPlacing()
|
||||
)
|
||||
);
|
||||
}
|
||||
// XYChart.Series<String, Double> positionData = sparkLineData.get(yacht.getSourceID());
|
||||
// positionData.getData().add(
|
||||
// new XYChart.Data<>(
|
||||
// Integer.toString(legNumber),
|
||||
// 1.0 + participants.size() - yacht.getPlacing()
|
||||
// )
|
||||
// );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* gets the rgb string of the yachts colour to use for the chart via css
|
||||
* @param yachtId id of yacht passed in to get the yachts colour
|
||||
* @return the colour as an rgb string
|
||||
*/
|
||||
private String getBoatColorAsRGB(String yachtId){
|
||||
Color color = participants.get(Integer.valueOf(yachtId)).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 )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialises a timer which updates elements of the RaceView such as wind direction, yacht
|
||||
* 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() {
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateRaceTime();
|
||||
}
|
||||
}, 0, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all corners until ones SeqID matches with the yachts 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 Mark getNextMark(BoatObject bg) {
|
||||
// TODO: 1/08/17 Move to GameView
|
||||
//
|
||||
// Integer legNumber = bg.getClientYacht().getLegNumber();
|
||||
// List<Corner> markSequence = courseData.getMarkSequence();
|
||||
//
|
||||
// if (legNumber == 0) {
|
||||
// return null;
|
||||
// } else if (legNumber == markSequence.size() - 1) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// for (Corner corner : markSequence) {
|
||||
// if (legNumber + 2 == corner.getSeqID()) {
|
||||
// return courseData.getCompoundMarks().get(corner.getCompoundMarkID());
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the wind direction arrow and text as from info from the StreamParser
|
||||
* @param direction the from north angle of the wind.
|
||||
*/
|
||||
private void updateWindDirection(double direction) {
|
||||
windDirectionText.setText(String.format("%.1f°", direction));
|
||||
windArrowText.setRotate(direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the speed of the wind as displayed by the info pane.
|
||||
* @param windSpeed Windspeed in knots.
|
||||
*/
|
||||
private void updateWindSpeed(double windSpeed) {
|
||||
windSpeedText.setText("Speed: " + String.format("%.1f", windSpeed) + " Knots");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the clock for the race
|
||||
*/
|
||||
private void updateRaceTime() {
|
||||
// if (!raceState.isRaceStarted()) {
|
||||
// timerLabel.setFill(Color.RED);
|
||||
// timerLabel.setText("Race Finished!");
|
||||
// } else {
|
||||
timerLabel.setText(raceState.getRaceTimeStr());
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the order of the yachts as from the StreamParser and sets them in the yacht order
|
||||
* section
|
||||
*/
|
||||
private void updateOrder(ObservableList<ClientYacht> yachts) {
|
||||
List<Text> vboxEntries = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < yachts.size(); i++) {
|
||||
// System.out.println("yacht == null " + String.valueOf(yacht == null));
|
||||
if (yachts.get(i).getBoatStatus() == BoatStatus.FINISHED
|
||||
.getCode()) { // 3 is finish status
|
||||
Text textToAdd = new Text(i + 1 + ". " +
|
||||
yachts.get(i).getShortName() + " (Finished)");
|
||||
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
|
||||
vboxEntries.add(textToAdd);
|
||||
|
||||
} else {
|
||||
Text textToAdd = new Text(i + 1 + ". " +
|
||||
yachts.get(i).getShortName() + " ");
|
||||
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
|
||||
textToAdd.setStyle("");
|
||||
vboxEntries.add(textToAdd);
|
||||
}
|
||||
}
|
||||
Platform.runLater(() ->
|
||||
positionVbox.getChildren().setAll(vboxEntries)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private void updateLaylines(BoatObject bg) {
|
||||
// TODO: 1/08/17 move to GameView
|
||||
//
|
||||
// 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(gameViewController, nextMark)) {
|
||||
// isUpwind = true;
|
||||
// } else {
|
||||
// isUpwind = false;
|
||||
// }
|
||||
//
|
||||
// for(MarkObject mg : gameViewController.getMarkGroups()) {
|
||||
//
|
||||
// mg.removeLaylines();
|
||||
//
|
||||
// if (mg.getMainMark().getId() == nextMark.getId()) {
|
||||
//
|
||||
// SingleMark singleMark1 = ((GateMark) nextMark).getSingleMark1();
|
||||
// SingleMark singleMark2 = ((GateMark) nextMark).getSingleMark2();
|
||||
// Point2D markPoint1 = gameViewController
|
||||
// .findScaledXY(singleMark1.getLatitude(), singleMark1.getLongitude());
|
||||
// Point2D markPoint2 = gameViewController
|
||||
// .findScaledXY(singleMark2.getLatitude(), singleMark2.getLongitude());
|
||||
// HashMap<Double, Double> angleAndSpeed;
|
||||
// if (isUpwind) {
|
||||
// angleAndSpeed = PolarTable.getOptimalUpwindVMG(StreamParser.getWindSpeed());
|
||||
// } else {
|
||||
// angleAndSpeed = PolarTable.getOptimalDownwindVMG(StreamParser.getWindSpeed());
|
||||
// }
|
||||
//
|
||||
// Double resultingAngle = angleAndSpeed.keySet().iterator().next();
|
||||
//
|
||||
//
|
||||
// Point2D yachtCurrentPos = new Point2D(bg.getBoatLayoutX(), bg.getBoatLayoutY());
|
||||
// Point2D gateMidPoint = markPoint1.midpoint(markPoint2);
|
||||
// Integer lineFuncResult = GeoUtility.lineFunction(yachtCurrentPos, gateMidPoint, markPoint2);
|
||||
// Line rightLayline = new Line();
|
||||
// Line leftLayline = new Line();
|
||||
// if (lineFuncResult == 1) {
|
||||
// rightLayline = makeRightLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection());
|
||||
// leftLayline = makeLeftLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection());
|
||||
// } else if (lineFuncResult == -1) {
|
||||
// rightLayline = makeRightLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection());
|
||||
// leftLayline = makeLeftLayline(markPoint2, 180 - resultingAngle, StreamParser.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 yachts currently in the race and adds the required listener
|
||||
* for the combobox to take action upon selection
|
||||
*/
|
||||
private void initialiseBoatSelectionComboBox() {
|
||||
yachtSelectionComboBox.setItems(
|
||||
FXCollections.observableArrayList(participants.values())
|
||||
);
|
||||
//Null check is if the listener is fired but nothing selected
|
||||
yachtSelectionComboBox.valueProperty().addListener((obs, lastSelection, selectedBoat) -> {
|
||||
if (selectedBoat != null) {
|
||||
gameView.selectBoat(selectedBoat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the list of yachts in the order they finished the race
|
||||
*/
|
||||
private void loadRaceResultView() {
|
||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("/views/FinishView.fxml"));
|
||||
|
||||
try {
|
||||
contentAnchorPane.getChildren().removeAll();
|
||||
contentAnchorPane.getChildren().clear();
|
||||
contentAnchorPane.getChildren().addAll((Pane) loader.load());
|
||||
|
||||
} catch (javafx.fxml.LoadException e) {
|
||||
System.err.println(e.getCause().toString());
|
||||
} catch (IOException e) {
|
||||
System.err.println(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private String getMillisToFormattedTime(long milliseconds) {
|
||||
return String.format("%02d:%02d:%02d",
|
||||
TimeUnit.MILLISECONDS.toHours(milliseconds),
|
||||
TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60, //Modulus 60 minutes per hour
|
||||
TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60 //Modulus 60 seconds per minute
|
||||
);
|
||||
}
|
||||
|
||||
private void setAnnotations(Integer annotationLevel) {
|
||||
switch (annotationLevel) {
|
||||
// No Annotations
|
||||
case 0:
|
||||
gameView.setAnnotationVisibilities(
|
||||
false, false, false, false, false, false
|
||||
);
|
||||
break;
|
||||
// Important Annotations
|
||||
case 1:
|
||||
gameView.setAnnotationVisibilities(
|
||||
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:
|
||||
gameView.setAnnotationVisibilities(
|
||||
true, true, true, true, true, true
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets all the annotations of the selected yacht to be visible and all others to be hidden
|
||||
*
|
||||
* @param yacht The yacht for which we want to view all annotations
|
||||
*/
|
||||
private void setSelectedBoat(ClientYacht yacht) {
|
||||
// for (BoatObject bg : gameViewController.getBoatGroups()) {
|
||||
// //We need to iterate over all race groups to get the matching yacht group belonging to this yacht if we
|
||||
// //are to toggle its annotations, there is no other backwards knowledge of a yacht to its yachtgroup.
|
||||
// if (bg.getBoat().getHullID().equals(yacht.getHullID())) {
|
||||
//// updateLaylines(bg);
|
||||
// bg.setIsSelected(true);
|
||||
//// selectedBoat = yacht;
|
||||
// } else {
|
||||
// bg.setIsSelected(false);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
public void updateRaceData (RaceXMLData raceData) {
|
||||
this.courseData = raceData;
|
||||
gameView.updateBorder(raceData.getCourseLimit());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package seng302.visualiser.controllers;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.URL;
|
||||
import java.util.Enumeration;
|
||||
import java.util.ResourceBundle;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import seng302.gameServer.GameState;
|
||||
import seng302.visualiser.GameClient;
|
||||
|
||||
/**
|
||||
* A Class describing the actions of the start screen controller
|
||||
* Created by wmu16 on 10/07/17.
|
||||
*/
|
||||
public class StartScreenController implements Initializable {
|
||||
|
||||
@FXML
|
||||
private TextField ipTextField;
|
||||
@FXML
|
||||
private TextField portTextField;
|
||||
@FXML
|
||||
private GridPane startScreen2;
|
||||
@FXML
|
||||
private AnchorPane holder;
|
||||
|
||||
GameClient gameClient;
|
||||
|
||||
public void initialize(URL url, ResourceBundle resourceBundle) {
|
||||
// gameClient = new GameClient(holder);
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// * 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 (IOException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// 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() {
|
||||
// new GameState(getLocalHostIp());
|
||||
gameClient = new GameClient(holder);
|
||||
gameClient.runAsHost(getLocalHostIp(), 4942);
|
||||
// try {
|
||||
//// String ipAddress = InetAddress.getLocalHost().getHostAddress();
|
||||
//// new GameState(ipAddress);
|
||||
//// new MainServerThread();
|
||||
//// ClientToServerThread clientToServerThread = new ClientToServerThread("localhost", 4950);
|
||||
//// controller.setClientToServerThread(clientToServerThread);
|
||||
// // 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);
|
||||
// } catch (Exception e) {
|
||||
// Alert alert = new Alert(AlertType.ERROR);
|
||||
// alert.setHeaderText("Cannot host");
|
||||
// alert.setContentText("Oops, failed to host, try to restart.");
|
||||
// alert.showAndWait();
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
gameClient = new GameClient(holder);
|
||||
gameClient.runAsClient(ipTextField.getText().trim().toLowerCase(), 4942);
|
||||
|
||||
// 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);
|
||||
//
|
||||
//// controller.setClientToServerThread(clientToServerThread);
|
||||
//// setContentPane("/views/LobbyView.fxml");
|
||||
// } 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) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (ipAddress == null) {
|
||||
System.out.println("[HOST] Cannot obtain local host ip address.");
|
||||
}
|
||||
// ClientState.setHostIp(ipAddress);
|
||||
return ipAddress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package seng302.visualiser.controllers.annotations;
|
||||
|
||||
/**
|
||||
* Annotations the user can select as important
|
||||
*/
|
||||
public enum Annotation {
|
||||
SPEED,
|
||||
WAKE,
|
||||
TRACK,
|
||||
NAME,
|
||||
ESTTIMETONEXTMARK,
|
||||
LEGTIME
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package seng302.visualiser.controllers.annotations;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
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;
|
||||
|
||||
;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package seng302.visualiser.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);
|
||||
|
||||
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package seng302.visualiser.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,230 @@
|
||||
package seng302.visualiser.fxObjects;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.CacheHint;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
/**
|
||||
* Grouping of string objects over a semi transparent background.
|
||||
*/
|
||||
public class AnnotationBox extends Group {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AnnotationFormatter<T> {
|
||||
String transformString (T input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class stores a text object and relationship for updating the text object if needed
|
||||
*
|
||||
* @param <T> The type of observable value passed to the annotation, if there is one.
|
||||
*/
|
||||
public class Annotation<T> {
|
||||
private Text text;
|
||||
private ObservableValue<T> source;
|
||||
private AnnotationFormatter<T> format;
|
||||
|
||||
/**
|
||||
* Constructor for observing annotation
|
||||
* @param textObject the javaFX text object the annotation is displayed in
|
||||
* @param source observable value that the annotation is taken from
|
||||
* @param formatter interface describing how to format the source data if needed
|
||||
*/
|
||||
public Annotation (Text textObject, ObservableValue<T> source, AnnotationFormatter<T> formatter) {
|
||||
this.text = textObject;
|
||||
this.source = source;
|
||||
this.format = formatter;
|
||||
source.addListener((obs, oldVal, newVal) ->
|
||||
Platform.runLater(() -> text.setText(format.transformString(newVal)))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a static annotation
|
||||
* @param textObject the javaFX text object the annotation is displayed in
|
||||
* @param annotationText the static value of the test object
|
||||
*/
|
||||
public Annotation (Text textObject, String annotationText) {
|
||||
textObject.setText(annotationText);
|
||||
text = textObject;
|
||||
}
|
||||
|
||||
private Text getText () {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
//Text offset constants
|
||||
private static final double X_OFFSET_TEXT = 20d;
|
||||
private static final double Y_OFFSET_TEXT_INIT = -35d;
|
||||
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_ARC_SIZE = 10;
|
||||
|
||||
private int visibleAnnotations = 0;
|
||||
private double backgroundWidth = 145d;
|
||||
|
||||
private Rectangle background = new Rectangle();
|
||||
private Paint theme = Color.BLACK;
|
||||
|
||||
private Map<String, Annotation> annotationsByName = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates an empty annotation box. The box is offset from (0,0) by (17, -38).
|
||||
*/
|
||||
public AnnotationBox() {
|
||||
this.setCache(true);
|
||||
background.setX(BACKGROUND_X);
|
||||
background.setY(BACKGROUND_Y);
|
||||
background.setWidth(backgroundWidth);
|
||||
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.SCALE);
|
||||
this.getChildren().add(background);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an annotation to the box. Use the name to reference the annotation for removal or\
|
||||
* changing visibility.
|
||||
* @param annotationName the name of the annotation.
|
||||
* @param annotation the annotation.
|
||||
*/
|
||||
public void addAnnotation (String annotationName, Annotation annotation) {
|
||||
annotationsByName.put(annotationName, annotation);
|
||||
Platform.runLater(() -> {
|
||||
this.getChildren().add(annotation.getText());
|
||||
visibleAnnotations++;
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an annotation with a constant text.
|
||||
* @param annotationName The name of the annotation. Will be used to reference it later.
|
||||
* @param annotationText The desired text.
|
||||
*/
|
||||
public void addAnnotation (String annotationName, String annotationText) {
|
||||
Text text = getTextObject();
|
||||
addAnnotation(annotationName, new Annotation(text, annotationText));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an annotation with the given name. The annotation will contain the value of the given
|
||||
* ObservableValue. The formatter should return a String and takes an object of the same type as
|
||||
* the ObservableValue as a parameter. The String is how you want the annotation to look.
|
||||
* @param annotationName The annotation name.
|
||||
* @param observable The observable value the annotation will display.
|
||||
* @param formatter A formatting function for the observable value.
|
||||
* @param <E> The type of ObservableValue.
|
||||
*/
|
||||
public <E> void addAnnotation (String annotationName, ObservableValue<E> observable,
|
||||
AnnotationFormatter<E> formatter) {
|
||||
Text newText = getTextObject();
|
||||
addAnnotation(annotationName, new Annotation<>(newText, observable, formatter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the annotation with the given name if it exists.
|
||||
* @param annotationName The name of the annotation
|
||||
* @param visibility the desired visibility
|
||||
*/
|
||||
public void setAnnotationVisibility (String annotationName, boolean visibility) {
|
||||
if (annotationsByName.containsKey(annotationName)) {
|
||||
Text textField = annotationsByName.get(annotationName).text;
|
||||
boolean currentState = textField.visibleProperty().get();
|
||||
if (visibility != currentState) {
|
||||
if (visibility)
|
||||
visibleAnnotations++;
|
||||
else
|
||||
visibleAnnotations--;
|
||||
}
|
||||
textField.setVisible(visibility);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the annotation with the given name if it exits.
|
||||
* @param annotationName The name given when the annotation was created.
|
||||
*/
|
||||
public void removeAnnotation (String annotationName) {
|
||||
if (annotationName.contains(annotationName)) {
|
||||
Platform.runLater(() -> {
|
||||
this.getChildren().remove(annotationsByName.remove(annotationName).getText());
|
||||
visibleAnnotations--;
|
||||
update();
|
||||
});
|
||||
annotationsByName.remove(annotationName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the annotation.
|
||||
* @param x x location
|
||||
* @param y y location
|
||||
*/
|
||||
public void setLocation (double x, double y) {
|
||||
Platform.runLater(()-> this.relocate(x + BACKGROUND_X, y + BACKGROUND_Y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the width of the annotation box. Default is 145.
|
||||
* @param width new width.
|
||||
*/
|
||||
public void setWidth (double width) {
|
||||
backgroundWidth = width;
|
||||
Platform.runLater(() -> background.setWidth(backgroundWidth));
|
||||
}
|
||||
|
||||
private void update () {
|
||||
background.setVisible(visibleAnnotations != 0);
|
||||
background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * visibleAnnotations);
|
||||
for (int i = 1; i <= visibleAnnotations; i++) {
|
||||
Text text = (Text) this.getChildren().get(i);
|
||||
if (text.visibleProperty().get()) {
|
||||
text.setX(X_OFFSET_TEXT);
|
||||
text.setY(Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * i);
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a text object for an annotation.
|
||||
* @return The text object
|
||||
*/
|
||||
private Text getTextObject() {
|
||||
Text text = new Text();
|
||||
text.setFill(theme);
|
||||
text.setStrokeWidth(2);
|
||||
// text.setCacheHint(CacheHint.QUALITY);
|
||||
text.setCache(true);
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the colour of the annotation box's border and text colour.
|
||||
* @param value desired colour.
|
||||
*/
|
||||
public void setFill (Paint value) {
|
||||
theme = value;
|
||||
background.setStroke(theme);
|
||||
annotationsByName.forEach((name, annotation) -> annotation.getText().setFill(theme));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package seng302.visualiser.fxObjects;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.scene.shape.Polygon;
|
||||
import javafx.scene.shape.Polyline;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.scene.transform.Rotate;
|
||||
|
||||
/**
|
||||
* 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 BoatObject extends Group {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SelectedBoatListener {
|
||||
|
||||
void notifySelected(BoatObject boatObject, Boolean isSelected);
|
||||
}
|
||||
|
||||
//Constants for drawing
|
||||
private static final double BOAT_HEIGHT = 15d;
|
||||
private static final double BOAT_WIDTH = 10d;
|
||||
|
||||
private double xVelocity;
|
||||
private double yVelocity;
|
||||
private double lastHeading;
|
||||
private double sailState;
|
||||
//Graphical objects
|
||||
private Polyline trail = new Polyline();
|
||||
private Polygon boatPoly;
|
||||
private Polygon sail;
|
||||
private Wake wake;
|
||||
private Line leftLayLine;
|
||||
private Line rightLayline;
|
||||
private double distanceTravelled, lastRotation;
|
||||
private Point2D lastPoint;
|
||||
private Paint colour = Color.BLACK;
|
||||
private Boolean isSelected = false, destinationSet; //All boats are initialised as selected
|
||||
private boolean isPlayer = false;
|
||||
|
||||
private List<SelectedBoatListener> selectedBoatListenerListeners = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Creates a BoatGroup with the default triangular boat polygon.
|
||||
*/
|
||||
public BoatObject() {
|
||||
this(-BOAT_WIDTH / 2, BOAT_HEIGHT / 2,
|
||||
0.0, -BOAT_HEIGHT / 2,
|
||||
BOAT_WIDTH / 2, BOAT_HEIGHT / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BoatGroup with the boat being the default polygon. The head of the boat should be
|
||||
* at point (0,0).
|
||||
*
|
||||
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat
|
||||
* polygon.
|
||||
*/
|
||||
public BoatObject(double... points) {
|
||||
initChildren(points);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the javafx objects that will be the in the group by default.
|
||||
*
|
||||
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat
|
||||
* polygon.
|
||||
*/
|
||||
private void initChildren(double... points) {
|
||||
boatPoly = new Polygon(points);
|
||||
boatPoly.setFill(colour);
|
||||
boatPoly.setFill(this.colour);
|
||||
boatPoly.setOnMouseEntered(event -> {
|
||||
boatPoly.setFill(Color.FLORALWHITE);
|
||||
boatPoly.setStroke(Color.RED);
|
||||
});
|
||||
boatPoly.setOnMouseExited(event -> {
|
||||
boatPoly.setFill(colour);
|
||||
boatPoly.setFill(this.colour);
|
||||
boatPoly.setStroke(Color.BLACK);
|
||||
});
|
||||
boatPoly.setOnMouseClicked(event -> setIsSelected(!isSelected));
|
||||
boatPoly.setCache(true);
|
||||
// boatPoly.setCacheHint(CacheHint.SPEED);
|
||||
|
||||
// annotationBox = new AnnotationBox();
|
||||
// annotationBox.setFill(colour);
|
||||
|
||||
leftLayLine = new Line();
|
||||
rightLayline = new Line();
|
||||
trail.getStrokeDashArray().setAll(5d, 10d);
|
||||
trail.setCache(true);
|
||||
wake = new Wake(0, -BOAT_HEIGHT);
|
||||
wake.setVisible(true);
|
||||
|
||||
sail = new Polygon(0.0,BOAT_HEIGHT / 4,
|
||||
0.0, BOAT_HEIGHT);
|
||||
sailState = 0;
|
||||
sail.setStrokeWidth(2.0);
|
||||
sail.setStroke(Color.BLACK);
|
||||
sail.setFill(Color.TRANSPARENT);
|
||||
sail.setCache(true);
|
||||
super.getChildren().clear();
|
||||
super.getChildren().addAll(boatPoly, sail);
|
||||
}
|
||||
|
||||
public void setFill (Paint value) {
|
||||
this.colour = value;
|
||||
boatPoly.setFill(colour);
|
||||
trail.setStroke(colour);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 rotation by which the boat moves
|
||||
* @param velocity The velocity the boat is moving
|
||||
* @param sailIn Boolean to toggle sail state.
|
||||
*/
|
||||
public void moveTo(double x, double y, double rotation, double velocity, Boolean sailIn, double windDir) {
|
||||
Double dx = Math.abs(boatPoly.getLayoutX() - x);
|
||||
Double dy = Math.abs(boatPoly.getLayoutY() - y);
|
||||
Platform.runLater(() -> {
|
||||
rotateTo(rotation, sailIn, windDir);
|
||||
boatPoly.setLayoutX(x);
|
||||
boatPoly.setLayoutY(y);
|
||||
if (sailIn) {
|
||||
// sail.getPoints().clear();
|
||||
// sail.getPoints().addAll(0.0, 0.0, 4.0, 1.5, 8.0, 3.0, 12.0, 3.5, 16.0, 3.0, 20.0, 1.5, 24.0, 0.0);
|
||||
// sail.getPoints().addAll(0.0, 0.0, 24.0, 0.0);
|
||||
sail.setLayoutX(x);
|
||||
sail.setLayoutY(y);
|
||||
} else {
|
||||
animateSail();
|
||||
sail.setLayoutX(x);
|
||||
sail.setLayoutY(y);
|
||||
}
|
||||
wake.setLayoutX(x);
|
||||
wake.setLayoutY(y);
|
||||
});
|
||||
wake.setRotation(rotation, velocity);
|
||||
// rotateTo(rotation);
|
||||
// boatPoly.setLayoutX(x);
|
||||
// boatPoly.setLayoutY(y);
|
||||
// wake.setLayoutX(x);
|
||||
// wake.setLayoutY(y);
|
||||
// wake.rotate(rotation);
|
||||
|
||||
// wake.setRotation(rotation, groundSpeed);
|
||||
// isStopped = false;
|
||||
// destinationSet = true;
|
||||
lastRotation = rotation;
|
||||
|
||||
distanceTravelled += Math.sqrt((dx * dx) + (dy * dy));
|
||||
|
||||
if (distanceTravelled > 15 && isPlayer) {
|
||||
distanceTravelled = 0d;
|
||||
Platform.runLater(() -> trail.getPoints().addAll(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
private Double normalizeHeading(double heading, double windDirection) {
|
||||
Double normalizedHeading = heading - windDirection;
|
||||
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L);
|
||||
return normalizedHeading;
|
||||
}
|
||||
|
||||
|
||||
private void rotateTo(double heading, boolean sailsIn, double windDir) {
|
||||
boatPoly.getTransforms().setAll(new Rotate(heading));
|
||||
if (sailsIn) {
|
||||
Double sailWindOffset = 30.0;
|
||||
Double upwindAngleLimit = 15.0;
|
||||
Double downwindAngleLimit = 10.0; //Upwind from normalised horizontal
|
||||
Double normalizedHeading = normalizeHeading(heading, windDir);
|
||||
if (normalizedHeading < 180) {
|
||||
sail.getTransforms().setAll(new Rotate(windDir + 90 + sailWindOffset));
|
||||
sail.getPoints().clear();
|
||||
sail.getPoints().addAll(0.0, 0.0, 4.0, -1.5, 8.0, -3.0, 12.0, -3.5, 16.0, -3.0, 20.0, -1.5, 24.0, 0.0);
|
||||
if (normalizedHeading > 90 + sailWindOffset){
|
||||
sail.getTransforms().setAll(new Rotate(heading + downwindAngleLimit));
|
||||
}
|
||||
if (normalizedHeading < sailWindOffset + upwindAngleLimit){
|
||||
sail.getTransforms().setAll(new Rotate(heading + 90 - upwindAngleLimit));
|
||||
}
|
||||
} else {
|
||||
sail.getTransforms().setAll(new Rotate(windDir + 90 - sailWindOffset));
|
||||
sail.getPoints().clear();
|
||||
sail.getPoints().addAll(0.0, 0.0, 4.0, 1.5, 8.0, 3.0, 12.0, 3.5, 16.0, 3.0, 20.0, 1.5, 24.0, 0.0);
|
||||
if (normalizedHeading < 270 - sailWindOffset){
|
||||
sail.getTransforms().setAll(new Rotate(heading + 180 - downwindAngleLimit));
|
||||
}
|
||||
if (normalizedHeading > 360 - (sailWindOffset + upwindAngleLimit)){
|
||||
sail.getTransforms().setAll(new Rotate(heading + 90 + upwindAngleLimit));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sail.getTransforms().setAll(new Rotate(windDir));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void animateSail(){
|
||||
Double[] points = new Double[200];
|
||||
double amplitude = 2.0;
|
||||
double period = 10;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
points[i * 2] = amplitude * Math.sin(((Math.PI * i) / period + sailState));
|
||||
points[i * 2 + 1] = (BOAT_HEIGHT * i) / BOAT_HEIGHT / 2;
|
||||
points[199 - (i * 2)] = (BOAT_HEIGHT * i) / BOAT_HEIGHT / 2;
|
||||
points[199 - (i * 2 + 1)] = amplitude * Math.sin(((Math.PI * i) / period + sailState));
|
||||
}
|
||||
if (sailState == - 2 * Math.PI) {
|
||||
sailState = 0;
|
||||
} else {
|
||||
sailState = sailState - Math.PI / 5;
|
||||
}
|
||||
sail.getPoints().clear();
|
||||
sail.getPoints().addAll(points);
|
||||
|
||||
}
|
||||
|
||||
public void updateLocation() {
|
||||
// double dx = xVelocity / 60;
|
||||
// double dy = yVelocity / 60;
|
||||
//
|
||||
// distanceTravelled += Math.abs(dx) + Math.abs(dy);
|
||||
// moveGroupBy(dx, dy);
|
||||
//
|
||||
// if (distanceTravelled > 70) {
|
||||
// distanceTravelled = 0d;
|
||||
//
|
||||
// if (lastPoint != null) {
|
||||
// Line l = new Line(
|
||||
// lastPoint.getX(),
|
||||
// lastPoint.getY(),
|
||||
// boatPoly.getLayoutX(),
|
||||
// boatPoly.getLayoutY()
|
||||
// );
|
||||
// l.getStrokeDashArray().setAll(3d, 7d);
|
||||
// l.setStroke(colour);
|
||||
// l.setCache(true);
|
||||
// l.setCacheHint(CacheHint.SPEED);
|
||||
// lineGroup.getChildren().add(l);
|
||||
// }
|
||||
// lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
|
||||
// }
|
||||
// wake.updatePosition();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 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(GameViewController canvasController, Mark nextMark) {
|
||||
//
|
||||
// Double windAngle = StreamParser.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.
|
||||
// */
|
||||
// return boatLineFuncResult.equals(windLineFuncResult);
|
||||
// return true;
|
||||
// }
|
||||
|
||||
public void setIsSelected(Boolean isSelected) {
|
||||
updateListener(isSelected);
|
||||
this.isSelected = isSelected;
|
||||
setLineGroupVisible(isSelected);
|
||||
setWakeVisible(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.trail.setVisible(trail);
|
||||
}
|
||||
|
||||
public void setLineGroupVisible(Boolean visible) {
|
||||
trail.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 Group getWake () {
|
||||
return wake;
|
||||
}
|
||||
|
||||
public Node getTrail() {
|
||||
return trail;
|
||||
}
|
||||
|
||||
public Double getBoatLayoutX() {
|
||||
return boatPoly.getLayoutX();
|
||||
}
|
||||
|
||||
|
||||
public Double getBoatLayoutY() {
|
||||
return boatPoly.getLayoutY();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(2);
|
||||
boatPoly.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
isPlayer = true;
|
||||
animateSail();
|
||||
}
|
||||
|
||||
public void setTrajectory(double heading, double velocity, double windDir) {
|
||||
wake.setRotation(lastHeading - heading, velocity);
|
||||
rotateTo(heading, false, windDir);
|
||||
xVelocity = Math.cos(Math.toRadians(heading)) * velocity;
|
||||
yVelocity = Math.sin(Math.toRadians(heading)) * velocity;
|
||||
lastHeading = heading;
|
||||
}
|
||||
|
||||
public Boolean getSelected() {
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
public void setTrajectory(double heading, double velocity, double scaleFactorX, double scaleFactorY) {
|
||||
// wake.setRotation(lastHeading - heading, velocity);
|
||||
// rotateTo(heading);
|
||||
// xVelocity = Math.cos(Math.toRadians(heading)) * velocity * scaleFactorX;
|
||||
// yVelocity = Math.sin(Math.toRadians(heading)) * velocity * scaleFactorY;
|
||||
lastHeading = heading;
|
||||
}
|
||||
|
||||
private void updateListener(Boolean isSelected) {
|
||||
for (SelectedBoatListener sbl : selectedBoatListenerListeners) {
|
||||
sbl.notifySelected(this, isSelected);
|
||||
}
|
||||
}
|
||||
|
||||
public void addSelectedBoatListener(SelectedBoatListener sbl) {
|
||||
selectedBoatListenerListeners.add(sbl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package seng302.visualiser.fxObjects;
|
||||
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Polygon;
|
||||
|
||||
/**
|
||||
* Polygon with default course border settings.
|
||||
*/
|
||||
public class CourseBoundary extends Polygon {
|
||||
public CourseBoundary() {
|
||||
this.setStroke(new Color(0.0f, 0.0f, 0.74509807f, 1));
|
||||
this.setStrokeWidth(3);
|
||||
this.setFill(new Color(0,0,0,0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package seng302.visualiser.fxObjects;
|
||||
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Line;
|
||||
|
||||
/**
|
||||
* Visual object representing a gate, intended to connect two mark objects.
|
||||
*/
|
||||
public class Gate extends Line {
|
||||
|
||||
public Gate () {
|
||||
super.setStrokeWidth(2);
|
||||
super.getStrokeDashArray().setAll(2d, 5d);
|
||||
}
|
||||
|
||||
public Gate (Paint colour) {
|
||||
this();
|
||||
super.setStroke(colour);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package seng302.visualiser.fxObjects;
|
||||
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.*;
|
||||
import javafx.scene.transform.Rotate;
|
||||
|
||||
// TODO: 16/08/17 this class used to be well written... FeelsBadMan. Maybe lose the ternary operators.
|
||||
/**
|
||||
* Factory class for making rounding arrows for mark objects out of JavaFX objects.
|
||||
*/
|
||||
public class MarkArrowFactory {
|
||||
|
||||
/**
|
||||
* The side of the boat that will be closest to the mark.
|
||||
*/
|
||||
public enum RoundingSide {
|
||||
PORT,
|
||||
STARBOARD,
|
||||
}
|
||||
|
||||
public static final double MARK_ARROW_SEPARATION = 15;
|
||||
public static final double ARROW_LENGTH = 75;
|
||||
public static final double ARROW_HEAD_DEPTH = 10;
|
||||
public static final double ARROW_HEAD_WIDTH = 6;
|
||||
public static final double STROKE_WIDTH = 3;
|
||||
|
||||
/**
|
||||
* Creates an entry arrow group showing an arrow into and out of the rounding area. It is centered on (0, 0).
|
||||
* @param roundingSide The side of the boat that will be closest to the mark.
|
||||
* @param angleOfEntry The angle between this mark and the last one as a heading from north in degrees.
|
||||
* @param angleOfExit The angle between this mark and the next one as a heading from north in degrees.
|
||||
* @param colour The desired colour of the arrows.
|
||||
* @return The group containing all JavaFX objects.
|
||||
*/
|
||||
public static Group constructEntryArrow (RoundingSide roundingSide, double angleOfEntry,
|
||||
double angleOfExit, Paint colour) {
|
||||
if (roundingSide == RoundingSide.PORT && angleOfEntry < angleOfExit && Math.abs(angleOfExit - angleOfEntry) < 180) {
|
||||
return makeInteriorAngle(roundingSide, angleOfExit, angleOfEntry, colour);
|
||||
} else if (roundingSide == RoundingSide.STARBOARD && angleOfEntry > angleOfExit && -Math.abs(angleOfEntry - angleOfExit) > -180) {
|
||||
return makeInteriorAngle(roundingSide, angleOfExit, angleOfEntry, colour);
|
||||
}
|
||||
|
||||
angleOfEntry = 180 - angleOfEntry;
|
||||
Group arrow = new Group();
|
||||
Group exitSection = constructExitArrow(roundingSide, angleOfExit, colour);
|
||||
angleOfExit = 180 - angleOfExit;
|
||||
Arc roundSection = new Arc(
|
||||
0, 0, MARK_ARROW_SEPARATION, MARK_ARROW_SEPARATION,
|
||||
(roundingSide == RoundingSide.PORT ? -180 : 0) + angleOfEntry,
|
||||
roundingSide == RoundingSide.PORT ? Math.abs(angleOfExit - angleOfEntry) : -Math.abs(angleOfEntry - angleOfExit)
|
||||
);
|
||||
roundSection.setStrokeWidth(STROKE_WIDTH);
|
||||
roundSection.setType(ArcType.OPEN);
|
||||
roundSection.setStroke(colour);
|
||||
roundSection.setFill(new Color(0,0,0,0));
|
||||
Polygon entrySection = constructLineSegment(
|
||||
roundingSide == RoundingSide.PORT ? RoundingSide.STARBOARD : RoundingSide.PORT, 180 + angleOfEntry, colour
|
||||
);
|
||||
arrow.getChildren().addAll(exitSection, roundSection, entrySection);
|
||||
return arrow;
|
||||
}
|
||||
|
||||
private static Group makeInteriorAngle (RoundingSide roundingSide, double angleOfExit, double angleOfEntry, Paint colour) {
|
||||
Group arrow = new Group();
|
||||
Polygon lineSegment;
|
||||
angleOfEntry = Math.toRadians(360 - angleOfEntry);
|
||||
angleOfExit = Math.toRadians(180 - angleOfExit);
|
||||
int multiplier = roundingSide == RoundingSide.STARBOARD ? -1 : 1;
|
||||
double xStart = multiplier * MARK_ARROW_SEPARATION * Math.sin(angleOfEntry + Math.PI / 2);
|
||||
double yStart = multiplier * MARK_ARROW_SEPARATION * Math.cos(angleOfEntry + Math.PI / 2);
|
||||
xStart = xStart + (ARROW_LENGTH * Math.sin(angleOfEntry));
|
||||
yStart = yStart + (ARROW_LENGTH * Math.cos(angleOfEntry));
|
||||
multiplier = roundingSide == RoundingSide.STARBOARD ? 1 : -1;
|
||||
double xEnd = multiplier * MARK_ARROW_SEPARATION * Math.sin(angleOfExit + Math.PI / 2);
|
||||
double yEnd = multiplier * MARK_ARROW_SEPARATION * Math.cos(angleOfExit + Math.PI / 2);
|
||||
xEnd = xEnd + (ARROW_LENGTH * Math.sin(angleOfExit));
|
||||
yEnd = yEnd + (ARROW_LENGTH * Math.cos(angleOfExit));
|
||||
lineSegment = new Polygon(
|
||||
xStart, yStart,
|
||||
xEnd, yEnd
|
||||
);
|
||||
lineSegment.setStroke(colour);
|
||||
lineSegment.setFill(Color.BLUE);
|
||||
lineSegment.setStrokeWidth(STROKE_WIDTH);
|
||||
lineSegment.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
Polyline arrowHead = constructArrowHead(
|
||||
90 + Math.toDegrees(Math.atan2(yStart - yEnd, xEnd - xStart)),
|
||||
colour
|
||||
);
|
||||
arrowHead.setLayoutX(xEnd);
|
||||
arrowHead.setLayoutY(yEnd);
|
||||
arrow.getChildren().addAll(lineSegment, arrowHead);
|
||||
return arrow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an exit arrow group pointing towards the next mark.
|
||||
* @param roundingSide The side of the boat that will be closest to the mark.
|
||||
* @param angle The angle to the next mark as a heading from north in degrees.
|
||||
* @param colour The colour of the arrow.
|
||||
* @return The group containing all the JavaFX objects.
|
||||
*/
|
||||
public static Group constructExitArrow (RoundingSide roundingSide, double angle, Paint colour) {
|
||||
angle = 180 - angle;
|
||||
Group arrow = new Group();
|
||||
Polygon arrowBody = constructLineSegment(roundingSide, angle, colour);
|
||||
Polyline arrowHead = constructArrowHead(angle, colour);
|
||||
arrowHead.setLayoutX(arrowBody.getPoints().get(2));
|
||||
arrowHead.setLayoutY(arrowBody.getPoints().get(3));
|
||||
arrow.getChildren().addAll(arrowBody, arrowHead);
|
||||
return arrow;
|
||||
}
|
||||
|
||||
private static Polygon constructLineSegment (RoundingSide roundingSide, double angle, Paint colour) {
|
||||
Polygon lineSegment;
|
||||
angle = Math.toRadians(angle);
|
||||
int multiplier = roundingSide == RoundingSide.STARBOARD ? 1 : -1;
|
||||
double xStart = multiplier * MARK_ARROW_SEPARATION * Math.sin(angle + Math.PI / 2);
|
||||
double yStart = multiplier * MARK_ARROW_SEPARATION * Math.cos(angle + Math.PI / 2);
|
||||
double xEnd = xStart + (ARROW_LENGTH * Math.sin(angle));
|
||||
double yEnd = yStart + (ARROW_LENGTH * Math.cos(angle));
|
||||
lineSegment = new Polygon(
|
||||
xStart, yStart,
|
||||
xEnd, yEnd
|
||||
);
|
||||
lineSegment.setStroke(colour);
|
||||
lineSegment.setFill(Color.BLUE);
|
||||
lineSegment.setStrokeWidth(STROKE_WIDTH);
|
||||
lineSegment.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
return lineSegment;
|
||||
}
|
||||
|
||||
private static Polyline constructArrowHead (double rotation, Paint colour) {
|
||||
Polyline arrow = new Polyline(
|
||||
-ARROW_HEAD_WIDTH, -ARROW_HEAD_DEPTH,
|
||||
0, 0,
|
||||
ARROW_HEAD_WIDTH, -ARROW_HEAD_DEPTH
|
||||
);
|
||||
arrow.getTransforms().add(new Rotate(-rotation));
|
||||
arrow.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
arrow.setStroke(colour);
|
||||
arrow.setStrokeWidth(STROKE_WIDTH);
|
||||
return arrow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package seng302.visualiser.fxObjects;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Circle;
|
||||
|
||||
/**
|
||||
* Visual object for a mark. Contains a coloured circle and any specified arrows.
|
||||
*/
|
||||
public class Marker extends Group {
|
||||
|
||||
private Circle mark = new Circle();
|
||||
private Paint colour = Color.BLACK;
|
||||
private List<Group> enterArrows = new ArrayList<>();
|
||||
private List<Group> exitArrows = new ArrayList<>();
|
||||
private int enterArrowIndex = 0;
|
||||
private int exitArrowIndex = 0;
|
||||
|
||||
/**
|
||||
* Creates a new Marker containing only a circle. The default colour is black.
|
||||
*/
|
||||
public Marker() {
|
||||
mark.setRadius(5);
|
||||
mark.setCenterX(0);
|
||||
mark.setCenterY(0);
|
||||
Platform.runLater(() -> this.getChildren().addAll(mark, new Group())); //Empty group placeholder or arrows.
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Marker containing only a circle of the given colour.
|
||||
* @param colour the desired colour for the marker.
|
||||
*/
|
||||
public Marker(Paint colour) {
|
||||
this();
|
||||
this.colour = colour;
|
||||
mark.setFill(colour);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an exit and entry arrow pair to the mark. Arrows are hidden and shown in the order they
|
||||
* are created by calling showNextEnterArrow() or showNextExitArrow()
|
||||
* @param roundingSide the side the marker will be from the perspective of the arrow.
|
||||
* @param entryAngle The angle the arrow will point towards a marker
|
||||
* @param exitAngle The angle the arrow wil point from the marker.
|
||||
*/
|
||||
public void addArrows(MarkArrowFactory.RoundingSide roundingSide, double entryAngle,
|
||||
double exitAngle) {
|
||||
//Change Color.GRAY to this.colour to revert all gray arrows.
|
||||
enterArrows.add(
|
||||
MarkArrowFactory.constructEntryArrow(roundingSide, entryAngle, exitAngle, Color.GRAY)
|
||||
);
|
||||
exitArrows.add(
|
||||
MarkArrowFactory.constructExitArrow(roundingSide, exitAngle, Color.GRAY)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the next EnterArrow. Does nothing if there are no more enter arrows. Other arrows become hidden.
|
||||
*/
|
||||
public void showNextEnterArrow() {
|
||||
showArrow(enterArrows, enterArrowIndex);
|
||||
enterArrowIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the next ExitArrow. Does nothing if there are no more enter arrows. Other arrows become hidden.
|
||||
*/
|
||||
public void showNextExitArrow() {
|
||||
showArrow(exitArrows, exitArrowIndex);
|
||||
exitArrowIndex++;
|
||||
}
|
||||
|
||||
private void showArrow(List<Group> arrowList, int arrowListIndex) {
|
||||
if (arrowListIndex < arrowList.size()) {
|
||||
if (arrowListIndex == 1) {;
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
this.getChildren().remove(1);
|
||||
this.getChildren().add(arrowList.get(arrowListIndex));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides all arrows.
|
||||
*/
|
||||
public void hideAllArrows() {
|
||||
Platform.runLater(() -> this.getChildren().setAll(mark, new Group()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package seng302.visualiser.fxObjects;
|
||||
|
||||
import javafx.application.Platform;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
Platform.runLater(() -> {
|
||||
rotate(rotation);
|
||||
double rad = (14 / numWakes) + velocity;
|
||||
for (Arc arc : arcs) {
|
||||
arc.setRadiusX(rad);
|
||||
arc.setRadiusY(rad);
|
||||
rad += (14 / numWakes) + (velocity / 2.5);
|
||||
}
|
||||
});
|
||||
// } 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,44 @@
|
||||
package seng302.visualiser.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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user