diff --git a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties.xlf b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties.xlf index 8304d9ab338..03128dc78cc 100644 --- a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties.xlf +++ b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties.xlf @@ -1,12 +1,12 @@ - +
- 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 - +
@@ -2390,6 +2390,9 @@ Finish at + + Protest starts at + Protest ends at @@ -2885,6 +2888,9 @@ RRS26 start + + Sailing World Cup Start + Activate configuration @@ -3468,6 +3474,9 @@ Wind Chart + + Loading wind data... + No Videos @@ -4401,6 +4410,12 @@ Competitor Registrations are disabled for this race. Enable by checking the checkbox to the left. + + The following competitors cannot be registered to this race : {0} + + + {0} has already been assigned to fleet {2} in race {3} and therefore cannot be assigned to fleet {1} within the same race + Add Mark to Regatta diff --git a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties.xlf b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties.xlf index 0ed96b3769c..c84676f6049 100644 --- a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties.xlf +++ b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties.xlf @@ -1,12 +1,12 @@ - +
- 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 - +
@@ -3183,6 +3183,10 @@ Finish at フィニッシュ時刻 + + Protest starts at + 抗議開始時刻 + Protest ends at 抗議最終時刻 @@ -3843,6 +3847,10 @@ RRS26 start RRS26 開始 + + Sailing World Cup Start + セーリングワールドカップ開始 + Activate configuration 設定有効化 @@ -4619,6 +4627,10 @@ Wind Chart 風チャート + + Loading wind data... + 風データをロード中... + No Videos 動画なし @@ -4889,7 +4901,7 @@ The time passing of {0} waypoint can not be before the time of the previous waypoint or after the time of the following one - 変針点 {0} を通過する時刻は、前の変針点の時刻より前であってはならず、後の変針点の時刻より後であってはなりません。 + 変針点 {0} を通過する時刻は、前の変針点の時刻より前であってはならず、後の変針点の時刻より後であってはなりません。 Error removing fixed mark passing: {0} @@ -5863,6 +5875,14 @@ Competitor Registrations are disabled for this race. Enable by checking the checkbox to the left. 競技者登録はこのレースに対して無効化されています。左側のチェックボックスを選択して有効化します。 + + The following competitors cannot be registered to this race : {0} + 次の競技者はこのレースに登録することができません: {0} + + + {0} has already been assigned to fleet {2} in race {3} and therefore cannot be assigned to fleet {1} within the same race + {0} はすでにレース {3} のフリート {2} に割り当てられており、そのため同一レース内でフリート {1} に割り当てることはできません + Add Mark to Regatta マークをレガッタに追加 @@ -6425,7 +6445,7 @@ The current Foiling ride height - フォイリングでの現在の浮上高さ + フォイリングでの現在の浮上高さ Ride Height (∅ at end) @@ -6433,7 +6453,7 @@ Current Foiling ride height in meters while leg is still running; average ride height when leg is completed. - レグ航行中のフォイリングでの現在の浮上高さ、レグ完了時の平均浮上高さ + レグ航行中のフォイリングでの現在の浮上高さ、レグ完了時の平均浮上高さ Please create at least one mapping by first selecting a track on the left hand side and then selecting a competitor on the right hand side diff --git a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties.xlf b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties.xlf index 42f0aedef97..dbfe0575ee0 100644 --- a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties.xlf +++ b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties.xlf @@ -1,12 +1,12 @@ - +
- H4sIAAAAAAAAAMy9a3PcSJIl+h2/AtNtdyXZkqlHTffsqA1WN0VKKs6IjyapUveuro2BmSCJUSaQA2SKYrXVf79+jrtHBPJBqnr7rt2yUhKIFwKBCA8Pfxyfl015U726KOtZcRyvs66cVAftfF4vl1U1XiyKc0nItUSXSwJL/FxXd1WneV95nc3LxUHbLLt21kuDi3xiN9lCahZn8pMtpeql/Hv7tWqWfXFpt7neZ/1d3feX9bxubqzERUzxQmjjSzV9U123XVX8VPfLtrvP2+vcMvJKy91UTdWVs+K9/s1mUhLd7YsPcpXzUhOrm3K5LD3d7rK7upkWn+RHXqypVl+r7vJ+UWGoeMP8M7mZfSivqllxeVv3ufy/vK1y1MqZl9/d1pPbvG/z67JD9qSdL2bVsprd59V8sbwfZV113VX9bXGuf+V+3n6t5BZ/+JSLdtVNKvYl1+tsWs4XVSOjctQsq+6rvOahp+SehA+CD8HvxS/Fq77QV7fhKuyvDEUjf3yEcG1jtKznVSHfoOLVZdvOlvWCCXxZvFG1rOUr5L3MH6nWNpIh7zqrbkb5O0lHseu6k9GVJM3jUKykp9P8umvnMk5Vs97coux7yUdqvyw7qV031ShbzMr7i0UlXT+Tq7zHZUz8qZotio99lf/u5e9ymSJ5V92sZjL4KLF/Je+qNfby373SAtN2dTWr9pnKUixUymDLZ2sbNn3cTqsPtXwW/ISU8wpXhf7JkcIhOqyQyt94z359qMqvlX74fNlK11ixXOZld1XLh5CJjAr9n/IKHzHvK1lE0z5/Wo1uRtLjP77QLrdNlcuaWC2rXO5+Z6mNJD/DSpjigfkVV4gMmjxyWi7L/BoDxdc8av69abG8+M5PvzTPMnyFw+qmeIev8XRa3TzLmpYT4K/VUmZ3JU2V8q+RbiM1v6+Wo2zZLmXufV69+GH8QzYpu+6+GE8mq7mMuHzaLH7M4iBcJql9ktyn6RfVrJpIC8XfXvz6/G8vf02mRS+jopkj6eHFanIrozqtuqu27KbFSZv3kiQTLaRlybUQqOv6ZtWVy7ptiqRiPsjJmurusF3q/8VJdTcajbJqWi8vJjKkffFWLvOzthY6g5xJO1vNm5NSvtxR411Pe0Uyyfy8bnIvkSdFsnI6jU8cT6dot6saqROTz3mPHFu056TVTcPmLltvGMl4GgmsL+Q8FMTUC30IXSMBuGwP+C6kACint9mqQW7xkX/SAeVj0nFkU5OymQhNPOAfWS5V2VdvMaPHLH/GBJvjZY6XGhR605bLg5ms/vWS8ohlPkFONq17mW9KrzBnf2rvhDTfX4BQGBk7bYpDLaULBqsBc1epCUjlXb28zW/bu1xI/L0my5B0ujXEl/okxUDf0fnxrJOM+7ffZLPoB2/Oxkjc8EJ5qQXziiW9v5e3IPLtbNofr/rlm+pkNa+6ehI6ugz5+VwKyCLOGy2yq4VxP6maqbzNQ22UXshbScoj1UcEIylduqq602vdCi58rEimw1jpGOkEHcx0pqzPbJ1Fl21aUua4zS7Ms7RCMhUw4rKDyArUqc65uWX6KP2r7vT76nrM2i/F6b9n82pazlC7OMZVrhshu8lU6zGTW9lCt63fU0nfvmy7UpbFufzwyjfHSNWePOlz5GDdYyPr9OnkMc5ZV69zbSNmfEdTWjgTNkH4mxlbO9VrbS3JeLy1XuYYNnsQ9PNd/ZOBnZWLvir2s+rbQrbI4r/LOrkpPmBaLbHaj5pj4W6EsB9aQlaiFzfVRbrvfF69evXiDznTthUITIZ0zLJ1187xTvlNJ6zDNJ+uOkxM5zX2PjeTcjbRrcdXo2x99de6F5pOFpH32jHwi18r8CtX98zAzitzqJpiZ8F3Gv8jOoaG/sE9a768L+uGsy7HVUhKezetr69lz0Zz1vyi7WtsbrqSyWutOimxJK/DEl31tW5XyrllfbnAgUDeZNyUs/tlPRFqoAl5SEnJZEoN0/QBmRzUuKiWoC1DOuqJWe+5SQqW4HgpxfvlaSOU5+awWuIEo4sTrBSIw5IEH8zmlNmZvSk/JdbIe36owAadvn+4RDqwPmgbn104hF6+FL7xJRnlk+rb8lN5vwCfAL6AvFzx9nL8XQXTZ4byOhWWt/Kea8wypkevTGU50fnVSLP5nbW7l1+VvTPnaU0QAn8nmXXtpBbmdC6fI79pW2GTpTN9LR9HZzQm56rKcSyRKdbJmMuMGmU35cKpe3zT93IIlA7p995axN/xrb2fHp+exsXyTHjOrpbDjL64v7Z1N8xom+FkDzg4svDkwCVpMhXKW3moLMHw/r5wfz5+z30jbRJzBsuBi3RRdZI/55qs/muFXrTavj2Qu42+H1vqV1e9FLSG+tHnBl/vBuNQzWa9bp9gT6uym9VVt/4R79rVbJrf4pBwW5JPk737jrx3HzspZ8e6/yxnPWH+8WR8dNCCpIvDrtmwLbr2P3UH80H9nvEcjuMo8zlyLE2/lxnii0hGc1deOpW3TTE9JNvhGVPrTujA81lV4a8tNAzvaoHM59P2ruEExBjvyYSXEjIPpeX7vdy+Zzlrm5t9sslJA1wbmMj8Opk/yveqy/Z9GzawT5aZkOVW+vtgJX/RT+vvENqItHfrAgxfwL6iMG3+MTAz/zcGIfbge8Yh8gnv7POP8fXD4MjS/u21Hh8de9VkRdg4JCnD+ZjyVf8fDc/n5qRdbie622kNUmTaC/0VHrkvu1oWLFaiU1D2tb6WJS4s+USIA1YvMmseDxczyZezLU5KejCD1OAthAbCCh9W1+VqthyIEijF0HQ9GQq/TGmZHgBzuVWpmOaWV7NKmftLv3VRD+7xeawixV/GZ/Lk5twgj2xsksl8WJKoCXqqwBanpwjScE08veYDcArRLJff4TAikz7Jf2tLwHOtO94Vv++LILnTxw7bD9X7ZbtI8tpFzJroAVmWH+QS4Tq7VRnj6XWSmAoek3qZfIa6qWTAY9nikGk4qch3iIXTQ3wfjup6sJID9IUMwzk2E5wz5SQtHLqMRGcp3tkxCpXWhj2TGxgOn9bYou2kiTP8ZpAGHZYyfB9cLIQR6aopEy94qcnCw/bFx/OjPvvPvm0+drPi3y5OT/KP5x/YCJpjmzlbOowtxRxrjnmo87Gr9bnSrBVGkhVDospuE5GGzqQ+Pwhbnw1Udtv2S66Rn+xCxn4izCkFOAe31eTLVftNhqQDk6Ii2Tey7WKdCVt9GMuS179I54ydM/02N6FCtihXfTVeLVvhVeqJCWyLM6TmpSfnJtDNkHI+FO7GUnIuu88op4v8EuWE+dP+mWa8qZZ3VdXgeePpVxAnL3KlOWxtf7WYknOqGxcXvtb6JmBomxMsDAgveS5gCy4XuJFpsiQfIhPsl6prs8lt2/bVRzbqkuTT68hya2eLAxbL9eHybC2I1TBgwbVHWdV1bWdcPL6E0DGevd8iPTc+X5lK0l8cklnnXbUkJUalo4bMGD7ZsP61FdIG6lhsrbFPt/WsuuzupShkZctPQbY+bO8O5YQs3JNgt+hf/ju2TcrS2x7zu/x6Vt5kdaOkVsbfaGxbLFv5vRCuGVe9cH3O4E5JXC/AB0aeHo1BUAyKpLoOTkk92ciuieIqQrQkP9kPS/jZ3wvN/HDkBeS0FDJVRHIsDX1cFB8Xyf2h7JwFfkg3uJCTFSIU3FYHO+QrA+942szuXUaSiHX1bVvJDILbVJrLqluq4WtrVXxQYUW/KYe9rQm80oequVneFpdyKfMP11xJfSISFZ6OjKbKr5XXvm9X+az+QuZOeADI43/MtH5cmB+S9payjvtL+XhCtnCZ4zprTGbm2qE+6In6zbyUHdZMZ3xc09S7KqVOVCmbDR01eK3tze1sqiN3sRTSJpsOfrP/rK9kbP4Nv9mikpP98v6g7iYzSTzT29zuozDtqD9ZzWYqUiM/I6t/VZHtkTnY3lH4n8rah/L1VKWWjpVSn4NbmVVOYia4CfKlSxONBPlS7ikbRYbSkHXRSmDEwVereiny3J+bhPU01pwHUDtK15Elr/vmyRM5JdVNLUc0kxwd/n1dicLp394XZSrXO7N+IPsgHG/xsx/BkJojWbVCUezBYqocQmKuqekRniVwwL902W3G/sjk/tCW00KVIzz+y622ry2G7xRFhHUj1DOz3etjUy+L3u/OIANeccM8lmmsmc9PjmUm4kjB+7nsd7KRVXr3efXixdWLbDKTyVMc4JeKpbKrDtb0TlBnpWRkWvWTrl4YzxauMywgFYpT/mWLLKP+sruXpvg3a9ofPshOVnUHrbRUfGzIWMsIXNcmXzs6Pch/2J+x0P4EpUaQjkMUji1b3ggbvPwhUeQqMPLJRdBzbu1WRox1Eph8sVxu00OMHm5DKIqx19/TWKQpWmmU4fgjG8pPYD5Xi+xKGK7xXXlfgPPKyzsoRIXkkO7waiBPnWMgSQpBlrbTLdAqkipeba9Oera9+oC8Beo24e0wc3vTi0GNHQ+Zl92Xs7LvsZ8fyzUV2djVkcGUrJXp0DnZk6nypZENN9DsbNqubmalTP6VsLqdriXZk5kofWCqrv8+u65nMp10f3vHa2P7NePNPT7yaRd1asL98lsKWUn0aV7av7+UcgWDZgmpr0ttzZ8zR1Ju7elxJkh09QDpItys5JEHelMqT5P7dFVSr5ouyVgs1bz2vLZM6GbedO1dLwNJPY18DLu3EucDi4qkSao8tEm99goyfqGrOQUCHDGykrHIx1Qpup45eGia+fYb+Kq/HH8o9CqXTyp3ydsclv1tonc6aCHQDYn2OVKR+vCrpJL38HFKO7aO7Zxa92rvURz1uV5l9zIWf5UxaNripJXjlUxEEP8xp5+csrAR8KfUqWdiB+UnG1M+g3ZCB+83JDdH/b9jfgsbir3tI7ln5R+NoSaVUa4am6odAws3C7JJJFw6JoucK6Q/PtHYi7+9+FV6oMXfYatCH7SR/A7S02vuX1YkGBd4ocBVSol0XENTgyFNGuyFrE1u3VaJN+yPmSDJNmuGBnZBKwuaWGRCUOqZzB35zcAc41xsDYGRVrONtXZO2uX4qww3NpbQpvJcnqzCDzlxV4PCKgThWW1H8aQIBDBMguiFCSslULsJVuTA9dR5RjmmzH9qDBKePCiknVXdOIGOsmsSVWUkyGvGE13xTgmu8RXG9cT815wKkYKc0YQIJHesbynZblcEsxIhf3979WtG5tWml1YcL1kU+S9/fc1C0lE5tOuC5mnxsOXxQdPzu7JZqv6FKz7wZE+eSENPnvyYQ4r4GtyxyeZbm7xh55BlIJnlrG+hecDYUuBxVVmb038iX2BHOGUMfP3dymEz1Vj/JPcDI5i1Av1GCT2JpW3wAett6EbDyrrNoNZxudDS83LBUkhgGSQMFlW6+JK24xJMEsF2LobKwRsmbdJALbqTEFq9QA4nnHUn1d16E8UBczbrbvRrtykRs9cMiiBJxgG8d0WjXMtCF975JknZfTLEUUPWumpK1qTQqVpob8245eVGvze7OjC4kA0KMuWE/92wt2ibfZU7JwxzxkF6iDfdGNGHDWZGmU3t0+uNb2QrAGOz+Z2qwGP0cr4Sjh17/hVEGWP8Dmfje++1GppskvthVyvtKSiDbgDYD2H+SuELLrK7aiafRtjHDd35J82h9dX4LN9Upe+s+qad3heoc9Vxy+ut6nXZ9DkPOuGEKN/mjvRUN3wlqxV2bTUHHH3OPueYXthf9tMXrZpbyhjVWhO1vi3AFPGoeiujcHNLUrfPw6epLGS+kYfk+a43zWlDM1Rhdm/qpocFKAQC7ITaDbFYw07N674HP6EMcthPdLzlja270bS2r2E/C6023lheFbZ+2CoxJmg9mCpRbYq2QKPlqCeHk5bSYiz/WT35MpZdritlzf3Ptp0fNXJWrM3ucyrJrlhazLBfyovjeb9ISclQq1weaSlX7s2Mctk/y4S+Q0gmJ6mPlwfFeKaaax6VJIFCYZitkptWA1blooeSYTzonXzCtivQO/keuGZt3BcQCrM3IemyjYnS1SQdnH9f8DdNXbX3korfJJUi0arpW1OAQunQkxG3IqfX14X8S6pccjPib0jdJgbke1AMuGy3i/GuVDxfmJieg8BqwqVUi4v6l0qZi/fKMlzKfKUQ6kJyZWL8Um0Xa78I1aNEL1aJYnO3xA4m1S60D9JtCADXCm306C/+iI3aWzsHUuLPj2NhR/84fDlTNuxfYoHiUgYVO/D63sCW9mzAh8L5WOyBhoUrOmq+1kvd5vCUikJ2T8rf7h/j2z/6iGAtIq/S3ED6Y/fooySojMgyVfZkGSbdEQZDiLjcgHc24/WPfqUqD+ks+GhYYpuqBAwW6cNc0l4L8U+Y4pMBJ2zbjW8uUpSbQ8qv6F4QOBV5uau6UZm+6hEgziQHcmBZEBJ1ffUGpidbiyE3/1AKjTLlLIuaukAGZ0ulmKl0iCt0S7lj+QqrTvpwV11tyy6bVbnm7rGlmJkOWwGlnJyMVKFQuauE0w2Lbk18Ca2JG69lbtdQnNlF4tjQf+ydkFpCjhTV2bi6RhVSsX/9Zfv222S2mrrSJqprTCelChtrUJIrLR50Qlh6r8HdZ7CLh81/8c4u9FQE5TAIdFAiP6e6eKFJ2RXFV3QK+LfVfJHTawDnEsy2KKDNetn8ztw7gcqVC2yHi+C7kMNWIbdTY96tGjZ1pyw5CoTaHxe2MJLKMiHxQisqu2hNUd7AJhDEABx4cmKWlOdkynFGskWFbUFo+C2Hthg3NyqypEqK/P1Rs27Izr1sh329FN9gMlleeTM4PNR2Jj6P10Ndn2VQIb2m4LP6VFDrUc9sucZXfTtbLauDru17ip5YM9irmMHpXy7ffmeNbYaeExRVc4G80kmnTBcpuskEZd7fzCFTcEW/mpGYWJ98DW6FiAbD1/pGKMXjPc//+/P976qzre/RtMvt/0h5SnBfPRvbaqPEQvN6Op1V9L75U/5C+JWSDN6wpUGhxjTOqhXqWUVe+3q5f0u/GpmEe9altUJdfXOblBoY4v4dn6y0Kr/t2/WPfzySEZzlo9Sq17Oa2/zmMqptZzTJ3MCw+IKN0/qwrW/ddT+wQv7eL25f8++atP+IF3/g6+9+94dmwyjjyfNYeIqqC9YeaEuT8qPG9EhqZmDn34S+uJ2Bn4wTMuRuDcKvgBV7jm2CXEeU+9xWs8UH6WNfwIeL3TUbI6Sa3AypmerdmPqOl5pcwnpjKR+DOWO/00wdi2MOBfMPNgYnm8OMTA5TN1DGmybq2NNyCpFNWvD0RWoMBvV393VWLZN6F5qS1oIJVY2RrKZjOBn5DaVfL36NVFv9Q+xDnMdUHcS5fg1sqzJse/l82MO//SDpLyU9jvA//8pt6pOfpkwgT0nSXTxYqZWSFQxs40bhwGD2WuN6VlXL4h1+s6ug/cCGl/MScmUyibaLCztj1jG2i2+Ti/xcNasNB6avSBylxTkp5ID51iSpaxX6KI4FpcNh20StoyiR1aMFDu3eymXwhGRVP1CYo5/bi2ozFB0/II8xa5XHhDB8tYJvbe+nDPGmj5az/4lSaeTGeQ/qLM2k8JGOqH/KQw1picfa4ax4qBkW+K5Wep1cvfWtMB1SL+tiyRzh1uRSW4SFDd0g5VfNKGwCq1eS1Z2UjbZ5vmqOmrMSBj3VzJ6TT0rlD2EkallyTGGTOJ7wCcOv5Nq89W8UFLbldKpdhTuWdnTa/rVdnVOo/Uko1WVrIm9r6nGpt33Q3wkl+N2PKr2Jkm0riB1koFWJknCt/k9KzaEDEBJC7Z/Z8skKP+var/BJ6I2yz7SUKgZlHLwcTN61oHKL+v7nVS/EEd7el+3RHMQJZM1dSDrm5vAMx4vVLEAKl7XXRXut/Tq9klNis9kz71Hr+Rt96pVaes+47YAI6HKEUuJ6oJnFHpQvVlczmnSA6wIlJfGE1OyG2kYl7sIHRDc5mcvM1sOOUnru1O31dT2pZc7pm/bRR0MFgYkLXZ/p659aFR25vtBR22gJnqkmSZhFl5zZLCsXi9l9McZv1q8mMIu+Xsn00VPd1HS7F0mOneKmruvloLO4jznUgenRRMd9ZUWsnp7/k5cLh76BBZIayH26rZoo+Djqea6Vo6mrvfIlyvJsy00HLusDfi2faI2sr9UDetxjZ6Gy6MKSwHLziEiVsBcEQZJBlh2p0D9O1JXcy4JXd2+Zx3EvO2yr/qRdHkMIGlx/bZVyq3Ahmxq8AY9B5mQytUAtyZiqY7ee13vSuz46gOgsNDWWs3CxbaMk+Yk7hnOtX1VclDSI0hIksO9gBqo14waizuUyfAPRkZMR99AJT0z7P8qVUOWq0HHGxys/7Z9ZfViSoXuS8qP0CH0TDoa2SUICg9GLEEG7xlzWyR/wIWzW+z19wo2XUcGMMzbqA00+ztg5u1NBj7N4MxX7XK3aewpisxts8u+hcL1KpLPuwVW4ixh5IVlhG0aWIKoDgSoO9xRqketRuRZn1DugMUAtetnVJQ79CRurJktAa6DxipfIdJHrIHzsZpBwklIahfx4/sGWqUk+yYMPqoDk2jrtQhknuLEZ0l4j2JvE4ny9G1vJxvZGvX+yBW32rg9d0+zNNmwX2cIWwjx+jceT8iOdW4OncI5ttKwdk6zNblmvSuZt1nxtLPq8XVbg7eFdp8ehnsYnsig0ORyT1J5gpqk0I5hZkfXPZ3oozew3v9wgX4iUtb/z0621t/XD7Whz+OV2dMy+24PdknEc5PN7bK+RfpRhncEn2V5ZnzaQYSrPkOxZgMtRQ5c1EaZyD4ONa2JltV2hfmoRpSNbfADVZEKuKUI1OrAfZgn1selXC0wZ2c6UIauu5LxduatSu4D9+S/mJidkSTgQrTnKnXcUlhi651YdWSpFIaHttFAT2a2+5dRTCHMD76XqDtQD2BkkI5mhAakxEVKev6c1Cn2G0s1dORjSzrUt3RgTZO2cYlZzO02I9Y1Nubxr0RWwEu9pA6Msy/KuhfryC/d621nK/IamMZsfVLYvNnJ6LVeJG8HG51QLbzSbuo+mL7Sl9Y3h2NrsRksyDasuOBXrMTKoMo6YGVyDA6/hio1h5fE1DrS76pbXClRjVWVNcCSxqriTgQ8jy3ZbzasP7R0nwMV9v6zmQnvuFEUl14Rh4Z/qm9u0NO6/p/jbvjdXiM0q+duLi9wdJZ6+fPHs0TZevnq0lVff00pqUKELFac4hZCBd1Kjth5ywisnbuGYv3xlRfbyV3JE2M9fvgwJwn6M8nEDxKDKldXmF5K//bYUalgFGwM9XWKxy6r+ojaS1+VXLPNNl8tg6zCHVGw8wWEYevyjazUEngrbSaJxJSvji+l4qj07++n2pN0BlbIGpFPgCVc9OMLtowUa9gZN9pd1tTnoezmJHPVRWiyXctvnl47nezkz/8+qazfm2Z4POErkKDJsJlZ/B8HQlq6k9f9gSEC72riovz3WxB8faeIt5KCPNfI/tjYShpfHhHMaSGxridm55j/eQjqbL9Vpu0ndWJbmZpPfoGcv3VeBk4LmKkx/4TbMD732GJ5fQmMW7UyFubuHwUdgL481cBrbPkWWt29n8LZiqz0cKWHS08fHv/iXZOZwVaQVeGjXKsPv8GL0L1u/xG96cjrA4/xrCSY8yM9n7d2+Ul97gPzFE/ZkfeGuSrtJ/IaeG6hUpyMHfJrfwqhY4Ur621JPbNCRw7pStUJGMwZgM3sBK0TFZAQgQNN0huij5vJzM7RDQ5Vb+XBwkjZZtR/s9KyhEBqfG6EyJTwmgE4A9QJmS28Jy/JLpe7ReNNK5hWKvLLTat2F7ozUMU0f/UNyhJzX+9qUIXZs/UAqWLhZVUa8UylDmBB7IHqBnl7dk2zbR1nNOdUbJYCS8Zufk35+8O3wp7CPv+qB8sDxNjg73ZNAWlXcAe2yzQ48jlyM0Wrp8t4AS40jpMARofN97p4xZacgNHrt/utSJGLKqJvPbXWve5ZMHtU4YUrJm/u+IVuPzEEV+n2tML/CnsH9Kx3JsHVI/R2fSCg/9wBaS5w22Bl0j4Pt9M6P5AAgbNzmNyyS/o5n/B0f6Lu+DceX7IG1ptM1lW0HBD6zaJgqmKKitODumgxs+pWJKKJfWTmLXXsw5QKgQKdNuu/tOYFPyfljTVxWze4mtpP+rY2cduvb34628qcywv9jFzO2pcnv4cmcE3uxxon96xojtjaNH2Kw5GuQLspMJIvFr6mOfDVtvYWr75WAbF0bKWwXp7L6DXL5PfphL++q2dfq0TF99RvHNG31twzrowzubxjXjY2GD/mHD6yeRi5859WjiJ4S382qb/XVbGCJbwbl15Y1sJzRWiZQ3FLJZaCbdQwGZHfVNUnooAmYSG/rKt2dtnYUNbY8ixW2PWE66F5xOOxMJnvVlr0VqK1+qkobs1ONbOmEMOyDp3tIySCZLyDKN0cnOX6qowKlbYpViF839T7rqmthy11gDaoKsfeCydm8/FbPV/MdaGHH5bcciGEPlhoghg2gftT8s98CIAabB1rsrTFdBrMKW7sLur8cGrhNtGe9RAG1OnPkm0crpV1cxvpri8o8HAOeziNdU+eurR1Tg5FHKvymTpkJyiNdOjc0n62dcqifRyv9po4JZR9gCD3WxW1d21nmN/VknX0fgC6n/uY1pHZsbt153frk5mHbC212KpiTbe2JPSuFWIxrKOBDyip7oJg/c7wTiNF40cFzb2/n815dxW9vX8/nr/s+Eg1amJSmCVKrEpiS0CZJ7TEMleW4boQOKKIZkQ9kO3IVWbTLUJgWOppJU3979esobSqxGn+4oaHF+EtppeXuWs4Kvxjg5/6MhjaMVJA4ynCIXsJs9oNdZEDBsiS/igOi2CYYDYV0oiJaTaeo84i3VGNAygxUjOLCLrK61zJwNNXK2aoJxT6Gy1B1DTwkpOYpVDR9JoiwXoxX07rN/1v+s7C/bbaSTqEjcwArEq/96Phtzp1Bqs4AZL6E2+PqSm8v7CKbtIt7mpsVB36V3dVTwICcfSs+4Sp/uqi/VbNnmZw1JZsZP/Ey5CxLuD6XgKqsf4HS95cqm7qP2KFd5E9t3o3mcBiZTukMQqs+jjTf6Lm+j+T+tV0tV1dVzLeE7F7/Hk0LS8mPpgMvxaTdB6021ANFbQTdXXFoR2K2KutmJAYaOzQ5gQR8w94Eym7DV1FXPSANQHkjTKNs9MCCoFGz+enVIKDM1jO9TUj3qasw98INZ56jjaQMjZRKedFYYcCscG2msnTKMEG6oTaMOCFAvq7mV9VUjlzFW7vAB6p1CcrTgvf7OKSykQBgKm0vu3oC4/kaStUUsRv6VGbSwoDZQxW+Ye4O6rxl0lo56IfPI4IQvJDk3BkM34PP9gDo+ST4Gyq/NMjdwJIya8IBjVkOvBYDA90bjqV9fjhFENkFRuflvUxdfSGDMFPQFzQFHy/MWZtE5m3KG9jrjGczrWJKG34o964NULFrYNY+H5sdL4tD/sn6ePCQ5tAr4Y0wyPZFUItfYzyTTuNgzo+SHCFugaekTlyF+nUNlV5H/XGAqV7TelGgShsxbUqnkmp53Ijlg+xy0Uxl1mJL003xOCmxblMeKnx4pMI2y+qwveNpjngMZPC51h95g8D62PZ0pA+evF7wu59KuJDwPICDbHse0gfPWy/43c8jvoicB29L2V8u8JstSmC6NMWZ/oW7b9UAEBl/iH1X0KI0iXAy7oQ//Orw/TIv0lgnljn1oCcDBwm632xzj1Dfm+gcYWZxceNU5D8zlkuYRVTRQAaeZCc3VKD+j0Zm6hJ/7RAjtKPCmVnxms7VJTRrYG9MxJKTauliHpQ/8YztaORNKL0NLP23V7azphJD7yTtZHjOV1ypduMYGnEs/JiZwlpv0dBunmR3KvTbzdOtKfQdsqnwi2y1kK8BZY1fZMBzLoAVndWwgLwhB1XTyJHXmQIbXLaFR05p2fZNuVC3OtlzJsDMpnB8iNOsBR4rOjjVqq2TiWFuFOtZXdUJOtzX6jlsopTgTTD63Iyji4ARYlj79xFiFa3dlbLgKkg75FX+9Lkpo4vAZqV6yfLySC0vq1PYp8uWgG8K/+Av86nsqW6hYV/3RTUBHKzvqeMjAHC/6Njhmn21YAtuE9Ija31kOCghhgznttnuYEcZd9Uwr3fDHs7iQU1NGuSv1bdELfIF8gied77IQuSdHhD+9mJPR3Lv9y9GsrJD9jl9MNez92W9b1bQMEvL6qydld3FLUyJ31tSzrSciRmUcR1QSJF0WJc3XTnvNZUFdHlu1OClHLYgaQExAkd9hgNgubgtLJ3kyxb/goc/yXy8ejg+yoJkld4dXYTU46t1My7fpHW3k+R+nn+tIemf3RPmGzY1JcWIMVAOdo+uVG+aqb2y1RVm7DMFKuY5PzOoUeGpV7SqJS5/wJ0igX7wlei6t2tE1IDv0eo+IkdNThB5jor6bDOAUHkl5wxtzMYqWGxy1dZwYNROf26k19sH0e1DORAjmKXVN00t71yi2oKAQCEiAgeNhu9XkOsKy92YkpM6pRDqZMvg4CxPln8KDIMwNDzVT0K6nosfq+0jQ2ieaznr2fuzMZPnl2mrlELEQQn9xNT43NAMQi3jkzqQR5vI4AUPJS+3vFXsk/n3hhdLWjJB4nfUTin6fGdD6vTlmitOYNtlOUVUbVsvsRFfVTPzQl97872oMITqAziFaMkw8v1Bm2OgiAzg0ObwqMbgUe4O9/OeeAleuU+H66OcLOVj4TO+L1c3CnoMCTOnM7/bDdItNBbVB+5x9EgzPmZv7qGRVlfImWoQosMSpnB8CrYmWM5NJro47qrgj3yzUvHGnP7YiAvXqPdV7E/vEPlYB9flrK+v7200QS5tlWx5++gqPnz9KqTHNw/uU481k7y+r8QKb7bqgtv1WvvL2/WIEXscAllIw1ew3Rzks2uXNNqbrrjP1o2s01oH5XMTv1z60ir0OF0tEVqhd+i11u7zpwEyVBXB5UIYJyFzz3Y2EVa9N8GOKRYTVf7xfDBs0iY1nHQUp9IpWDn9TzlJozaWSdp5e4Yc2SvDkJvWq94fzQWgLi0NJE5XbScHZRz3UerRZtJVrlXUXMAW8dpb2Ih9buJDy9lN2wnBmz/UZaMxsptMAlFqH+6/bE6Tit6Q39du+iKxLtYam116u8l28zxw3THxcxPCpVxVuolRTFw2CeVScazPXyVcWiFRtksNe8V0ZFzqQJB4TGx3yYlChVvPclcbbtRcJR1Z8dga/SOX1WJBfYQqoRScxRLxHcm47ah0XH7zuQ/t1WgQ0sK8eL8iUNtjtQP3Cx1xKSTrm0+na8XNBv6d7Y66yauLkp5FAhng0wbUmo5ybyKZ7c11DgmG4fBQ6dAvZJDdSioqIDH6e0Xk1byPQH1X5TSl0rZu073FHLcaTBBZxLR7BNGWpS10Wrdxc5oBOZMinxsSeaX4Q4GiErse75DPkQYsI8UHekq/XQvatEw79Wyk/lPb2Ro6VYWte425eaBi4PWu5SRXMfTAMuggjC4AdFt4vq9V+JykGuYc5M+MhDnlHExWYscDBglCER4tMJ26+mqlUvHkxh8cl8EoCJfZ48t6EaTQtxGRkravC7V3k0EHEJf85Qv2Fj8nAL2kUg6PS6dDoQEa3YaxCg5zy1sL3KqVRgQEPpoOK0OwYNhZs/Im8XQHNnCwmusjfvCmAYPG9ho2e5lY9VJssdmfDc0a43AOmzE4ZuTUriMzN0QL+cUzp2nlgqxlsy+q20ukLbBZfqAzn5uLVs0Ag/UVXmOPxyizCeHEIIPyKWjcbhY9U96fXXiUinK5YjhC+ZM1LXIDYJvxcEBkMykzHbJcqUAng0Bwi0CVg9gXjbnMKnwcNmriK555xlA5Yng2ao2xsr7id64YKjx5/ylXzWkzaFItN3t6If4pP2grdaLUc97PMPQ0IMtXzA4Lyhj213D/pH/Y/2ybii5i+S9ylSkhDlJP3fjzp/NnjhBsQoEhRqKLCgIyYtN6wc24r16WoV8Z1hEOB+OuUn2pY4rIvfm7Iat3dzek90lGmr7NpyppfU2VmTxoNFQ+bdYJKqhBJYikPi5kx30ni7XglUZzQMaH9s4yeKUZSvvdruSy3bSGebNaglkOWSEkdLnNRGYENR7ZQnLkpdup6EZJyP+ooVK+GQCPg9WVM9pMrdTVNioaXIWwJ7wbZL2jE7rmqqEhNp5kAUPGWzdRwGuQ16cygaaUOBpq9nO7V5Khj9X5bmsG+5rajWuc0mvHRUiC+1wGnKSIDKsOJetRgUbr7QVohIebM98WD6QVWrMhBNKUDInMc4srY5o4Hd9gU0s71S5G6AmRzNXtCOrIEMzcHME0h1FtDN4QdqLdytzEDSCc/mKe6OByJ4j3YfBxTXvHmEWuKdKYRa4dUm/OJA+jFjJNZ6UgXUnogNc5+49/8v5d46GC2f2VpYQi6Old201jkYWlIF6qbO5t9xampX2hf0LqSbtkymVLqDXan2J+hwIfEEoD1g8fnP3eyLKWWQAQWcMnpBYU7xMjiW0FrCUvttnYgfpHM4wSL9IXCZl4k40CHHcal+knUOlQHB4a90smLjRLrfOFc1reC98Rrm1HWgNRvoyK1Z2Be0Yx/kULHJkxNZLsTYyVQWCThice5qzVgbeLSfrNY2x79T/0A/f09egR6aMvTt/Hp62V2/E4VHnkCfDUtwUU69Af39F/do+GczPpW8Xw8ujsmlnUdw3SI61eE6LUorkneC2PjV882GyYNm3287uGd3eLv6mP619gd7P8LGvGaa6fiq2zDRiqKAm7DCh8l2mUiFJxBreiIPcepKu1lhi5CKncQ0PjFmNaMhmgaViKSUmpxKhqm8WDGxKkZ4peOas0sn2uHFgGRkIZ2ALsRa48bWJCkiKd45qc8bv62wd14EmNWcgqv+NZ+6l60Pywp5o2+csYrQqk9SwXlu6TNlP1Kfglz8j72kRyOoqzPYwDN1l+GEzmjcWHOQ+Pm6v9CwM4wzrZuR6/s9ldK8rgwu1JyRqL0GZuPMM86uW+axIGozf7goTE0S8Xg0ZYnppKW6YPnub1RRhlK9G7eTjvfoIH9IF7FWsJRcts/U7tjfqlRhcM82hkrPxO6CKt/Qh2kUy3CPSqXTroavmtlY0foHBYkxMr4D0IFd4N893sZq3ZYHez1py1lh5IyHr4xln4hZpNFqqrSfZO3SiPLk7b7uj0ACF1dLOMJZKIA/CK0eA7udTIUTqnXgM6iipknR4wS1W2GgMdhBbH1oDF0Qcwjh4z2yNq2uQ41BSfH1Wzlv+2SbM7JfZCCdYiq4E2mAlHMNp4CmFsDkxRBRbGSTyGlDTRAMSYcJXrATduwDHPYN8RbOB/oiYNz6gM05OTBgSpyn86Ojx8e6I2lsAde0eaRJMGoUoq7iFvq8RqBLjWFF9pzYJzG9Szffd2CbuV8On1Vg4Ui5OkhTf354MPcSn8Lx62UYynD5bTc4gXNJAdmNj4lRnQ9m9KBI3h5VXZBTgeOcotAVbbyRDAe69arij0XC0YAI6R5i7sIn9K6euzrO0n9cx8VE/jdUyXNz2N11k5h7QSdrdjv8quGd+zmdwX7/xKyHfdg17XgC1Qh+4Qhz3ni8yVjzwNl+6bcLnqGjosUFDHO4tPZnoYCysYVD8Aw2+mhRrlZzD65737DuDtZXhuiHYWLrN23tQ9VdbFabiUnmGTXTWUpErnwl02lz1nWhzjV95gXi7Qx4VB07xFgERc7CP8iOSjwwMwHUnrV3Oh6/fFhf5lsQqyHsISlpLggiAXAsVZQBuDn9RIpTAbBdM/0VhIEiHMYxwLucafDOiVHIkPgLFUdFzoG5h2ThPgdHzudU69LNbu1/JfreW/Wsv/YS3/B9iVfSivqhksyzgweoevmLAgmhjXBqZVBefe4t+rauFhUPvs7rZcHl0busQnudk/us4NsCjx+NHNLsFF0mVuc1AtxDcsg22CEtUs1vlO+cwoERN5ZIUCN7kHXLACwhHVN00oYvNjytRY1s76oZgf70OByIFts+ANQawwOzSaAa3qooSLVmXXnpO+ylqlgCW1VsNFayGQJomycEeyGx7KJI4xNFVIBM4He/sUE9uqhBC55ytGYDCTV4aPCxFCOs0LRrKw39b4xL3iDslymQiDhfor2Mer46mGNe4d32EaCkmDiAsJVB01EQcHYWbh9lIPNB+CNj/6gD6csAHXoNIm61saQwonYRVrZQpHChmtQimpt4BBxiI513RD+TLEGEeO+dwoh0bEuM2AIAAWo0vHy0FmSH71q878kPDDrwkm2Wj4TMzLO3BEjHn6U0llWeuJqlPYmCl7tI2mXga6TLXmVxN+9YVXI7L1uT+MWuOj77lrQWt4rjin4dpJ29inA+tyAA3DO8UbTyUejA0zjeWocLjfN2RyRcMK0LuGI+tBq/kCEZfX0GR7BaYyFscL+/1lu9mIpgRkNuAFJ2C5IVh09U12hbp5mxQ7SaNJm9pIVUFeW3bkFW1UfBlB8GotJW/203po6qSxLe/4aHtvU5Q55W/nwfIywOmyc6P8KMo+lwmcX/CPoNZfn/zQsxjFm2r2tltGpZiPxPpTXdYdJKaucDd5LsXUQkvBz2577EUEKt4GUWxWntY+UJnSWaOAxbnBhypEvMZAn1QQajz08D7iHTukcR+aRFQXxS4wcK/eRu7Za28miQm/PhtdHVn6h15vq/TexuZO2m0NnrQJkz9s01qLn3eUazwGfK+NZZFfkbO1ETRvGDnnjbY+H24WPibrfVB3izDcVL5FnYZ/ns5H9CoxFhLiNJ7OOXR9OyPkcbuIjw1vTUl3Giag7D8g4OJYQxYi5iBID2Rf46aRDkyqKZRWZxW9gs3k1nbDwv5aquJz6+HDrgY5Mhv8wtLh+j5dwR8yXFnOUcN+LG7L3nEUeW22sbPzCkF9zCwWYKm41R3tTAgEwjMgYK5dKej7lcxfpocrjhKcXvyDHHYyPOnZr1X3Fx/yp4dYoLJb9f/0zOHgDCGwt0aK8eXl25PLo9OT17qC49xM6IVZyXjLo/ywzU9OL90CvGzueW7LHcH3znA+piqyoR4/r66vpZ/9P2VT6bY9Pn7z9ZdRZQ92bKV50OscKIJcwufqufEtZm5TBiyiIAWRXvRbYDEVnsgN11wUIVvlomvhmE11yZleq2ttmmOzoh8viwqC0i15U82TkaPET4V9vekcI1yfwtLJLD4WdkVhlS5bYeiw0uYQvnYazWqJxOx61cG2GGEAylk/lhcTDuidJhLzH7qVksmZ8Peze05EoiPizsSjFm+dskSLrqbotJrormGDICk6U8dLm9rl0pwVJMVQ7iXJRgEKDkn30eMISa7Fj/S4kXpyaW/09Dlrb5xLYZrrSplMJdi5FzedWBeqaYYCuVbT0F6eIgPmnWUrzcKmBFsEQ4E1NzJppTdA2GGIRhBGbEoarJUuAjPC/ttFRiTacTN9386uPy5UsBtc3fL9XB1ETQqTEwNHBZP7UfQooye0y+xfFWnQZJJs/dCb22ydQvvNxrNFuShjNf1yOAGqdco+6HW3rd4VDNEfqigPh9qM9trBwLMaTLEHmv9lNVsdNZArPfAI1TjXht783W3jldV+c1ujEGZqpY3X3V3p+152e9PhVXc330eQqkfeefsjODkwJfwRnB5uoLqfKIhoMU0ikX3rhMSgY1/r6SpsR1L6Vqb+8MF6ALhYXfVwFQc28WALszrLFmiZzf1mr/X94uR2KzLZSHi2kuFfVejzgxMcuo3tYvtr37o1TIa3tmM0qBz5nobyAExG6sYtAIhqZEC1VnYjq33Hc7h6awvMKIeepr8jKJmwJU3ZLBlTeSYMwgMvbQPnLIGkNNv2hG1N/ySUd/abmr7evomsN/79bW5U3TFQ69PDDvLwkwvTXbbHr7TE+55h43wvnWHaNWppi4+N1kaLOwbL2/zuprKrrvzaxuIDfsNVmSzjgzcoQfYio41EV1WxGd5uI44oepc87xIQt1uLnTZJe6db6Y6W8o6db6My2GVV60y7n4ICF+6/qll+Ht1IZRe/oxaSnlhqPvy0buAdQa7SjO+fBTh2gFrzWG/41oje8zq3bDrJasYHsF64TT04LTovNKpW42M3M62GRgQ0g1Y6/6fetYmcR1EKDGmXjbyvloPI4cQ8Kd6/vdysllOyL19S41zzISEAkeLx0XsUxlfa+AmtHc+r5aprqmjwSP9CS3QIK5OXmHyJN6q4+a6uaNhVPQOwExH9ufK2Y+DquUJ7BZW1i7WwsDphjy6IGK3CaE9TLaR7hAGaq6HVLY7r9dR489B91RxthHl2JP6AerAZ6XnQRFR3AilD0fBCENKhvnMz9G+IAZJNqxjc4rDy2jOzmzefRlcdpX6OGeVphhxDRB9618ozPRyizH9hIR3mhpoh2Itp5FAawX4ob/43m3GF+jlaCV5WSnDoq6t4BSozkXUiDw1R4xilyGF2rN0Q2TRzq7o/r1QR5fZ4+X9pAtQWBGP5UN0oFgstqA+pxsrVsMJk0YP44yrfTaHAug2ryyhCHsQyn8hdF125D3Cbh/usWxcKrJpxT0ZHtiuYXElGBkJdHMK2FiMT5AI8vAU3banJwrTiL/hL4jemG6TSPXWJdNwB9KVhsCuEH8Fh0xAKLMMXILLW6pir+bZK7qzOWujAMcPPvKmayS1MIDx0CzukeXnIzC0362On0l5Eu345KK5m0wCDVBysOVL2QRqdACAFb0bZSvB8uh/3JxXcqd9UtjqJ1Jz6r2p8Xms1ul646/goWG+bc42182otevqmo03q+zNXKyea4UqTttmo8TQ62KOHboh3sWR8Ytrkm1+Tub8M3InWHdVDs/Z2J4kH1NlkuaXVHT5Sg8eEh2Bs5/DqkQGWlzjtIE84C65n466iOXURkmTyVrNpOqYM5z5ScINPt7IVWNADLsDLVn2zLbARHP2wVaBYgnBQTqfw0W+qOwcrNWMc4iyM3E8cTOGGpziaSDy/oS7EBDiIK4hAStGXJKzF9yEprMh/Sh6VPCBTvlNoN58R3ANLSokYxiuUSPJc18numhkLi5PCDJfxtvXbJit2sEQnmyv6kaWsa3SwgDUp40b15r5Q1ubqPgsOTPLtytmENg1FuIr+TZns/t29LX5+ZcfDKj61nfoEaeR38CZclDITHRcayi+PXd20f0ZTabRkth0jA4DEpjHOQTXXop4TCZB77X98XDBEj/5JM4jf5QwAjeDIVb79tqimMbKwFUbYm6OpRr85mr5Os1IUpf6TSkd3oGRZsBXDGnQbM+c9MDLUtf34uTmNGlp1j19QznllXl5sguEKzTmOMlF6Ed7flfejtH8Ob/QbuxYAkP6xvdEpokYTNvjuoYzhJycs/aFTy4jqT32ut7mlsbdwjOLhQWs/larPADIhlfUQYC3/YC3/869bGiFvy1+zQNqMJxmsR9sbAhfQza3u66ua9mGm2kriTQ7UV+59uCP+5G9oPDg/b3/A9mjiCdrNFRXkqNfyCLoe4Brsq7bIaIM74o4H1Atz9QtuWeauFgNT5U+J36Du3X2/mtMGZS+itETxkk+wJokK++w74pmHt3PSZL7zSQTiIK5ha/3OkOfBXBT1J+ViyV1KA5O2V9irFCWgDpi9/WZgdG/k2H3xYZbJHikb8lC09EEH5mkD4W04YAyap4J4eApDvcHdRS0fQ6I35xo1qnwZmCd1Io0O+Mvq21I3dgXYodbZVrZjtEyT8jGK2ucGp7BWq0DCpZEJp+px46ZfqoMJhmDIcoxUzToPSKpypwZkmmHGZHh53McVE6FLDURU3ZtCAOUgU5DJ7fgQ60H3FhrGzgF31Cbzc/MUjE0w+k0m6rPQD3+Vh/ozRL21joUW/I0faiHAwG5tQYfmofopxK3Xpt5TN3DfxZQOnrRxa6eKN+z4VO3ZRty0BythiefkFqrugibllzJ9tJFLonowK1dzc9Qmhwi3S+ld01NL+165jo0uTL0IunBjLMlaD/zZ63X9wcBzXqtiIhAhusIeCa8iRwn1fgMpXu8C1mDnhQcNkSs5aZc/Q8jzppqUKxpXGrciTHUP8Os9WTbMSvivlPOycGR6ejwMRcgDMbbugFd26w7MRT6HtoX47FdlV/ykUFK84TZzsX4SNiuufuM8bPZim1Daai+2BUsb8m3D+gH5grTA41EXM9qzch+dCy9PG3oDCODG/nn14sXVi88Ndmja39+WPbbneR4iXO8KHqswdo/HjqXRVP/jg8083IqFUX3x64981wKuCFmbhtbc1XhqtPxoR/stVs07ux1bfrjhoRfmj25rn/TLze0TbbY57u4lFH0v9TPfc7yDZUkV/dEhbTuTPtG8M7EWTb1eIKHZ7vmSPGIQiI1go6kJuHBqODulGtlSy5hmdujlAJM5eftRVk8L6SuLqmLWcEy1Es0A6I96YBchxelpgLuR/U21uNz8h3H60rijnxtfqWRnJtzo8xfXV9d/fPEvL6dV+fLVH4Qu/HP5hz9Oq+oPf5j+8eXkX/91Wl3/c/nHV9XITlMrBDgoSApyvZGTpTTMFAVTVD22HDMRzVkpj8nDNccV3y66g5sDorjpQd7V4C5/o70XAZX8KG+nVpBpsPtuhA+arAnqvZ2aAsJuc2j9h8jSyX0/dAafDPJGHp+RJJfWsWnjHqDRc4e1E55PP84oK30SRRtbm1dD41rDoND4Se/gw0lbZhcjabQj8ttm1myG8RsVzER+s4KFNj6W2X5eTeoFrdztxCxsXk1lqCZnU3k58Ifpqx8ybWNoG2w6TTUwY666Y9hoD4yY1T8EUhNXkxVqKhL1ZtYG0Su8MoXqAZsvIPJBpLkMlqWQbWLWhAQ4OpwPnzZ8mYsqhnsYfML1Tm43Mx1WgU/V1xJWr66LNkOpsWXIBF/TUoc66REyFDfbYUgo7rOu61/9Uf3Tzs8vXv3RHQ2r5dq899rDzgXVesEYxYYB4Ik/VbMFOCYFwuFBJZSKJpCgYrkBGj95YoMdjH2fPKHYzB9PjCEfPgCpeQAPtCnEcG2QQg/gNhdaUW02FXKneswgebvSjZw6+2tidKyN68imOQf072laNwUPKB7QgKdG1oY6dhrHjdSsCOYQNI9S84hgHdHna/mbQx7c7tzRL9QNo451pt3Tjj40xkLDoLnXKcPYpBfBKxADcFYub+UdIAAJcyamAQHYLv+e8aP7323aILfzEVSt2iOEnNQOyYldGH8mvsGlJc8YSkzTNayYZURLvoiArkFVOA/XFny/Y3lsX/bf10oYETq+1Q7eqD5Yekq1QydAmsBkbH1akJs41M+qGVo1J9AY+/1CyLKM87AJ+KEs1b1UdRzL1IHa3CTghRrwEnDAHFQaRDcKeOIQe4eGAmQEjmsMgEZbfWP2/Fsn5WnQiyvFrrBXWRiwZsPqrrVM5jKBmwDSJMlqTej1ekUeCupbiPfuyiSqGKLVUZBDI04/nMSa+lX4TDx8xuWEsU/0vVGwEVCE2MBTBgUmLVR+/hmH3bWCGMR+OIrpgIBqrKF1DMczE/IRvXNgdoejPL9nmK3RoSeHpQ4P8vqNy601wwz9jhYMivCgnSX+kBPcmUHADWIpjxXqsS+OeJv7/RDK+2hYdg3PG+Nt1Q05MgaxHtQk9zZ8kAWvXi1v267+RVobk/++bLUYcD88jHUsg8cQiqP11oAGgkfyADjk861aNd3S8pDbDyV3tw8mZjaVc/mDzR6gEKGjQpuhhT8FjEimqItEF7FKOB7v1ZXLBs/aCNFMdDxutEwY+bRUjLgePgE7low/7ncdBAdn+kfPmOlB/8ch8rg2N5wD65jj1tJwAnEphUFPv+e2Ji+GZrRquPVQgxX4ZJxW5Ev6caXaRyIWFA5n6pyS6h0zxGWbjTVbSs+T0pl/vsJRaDI9zV4KZ3YRzrXfMd6/abhfxzAo82pgbnBAnembSlHSpwnGEwoOAZ6OlpDc2rlMjeWnOc5Qo0ztbS5bIX0n1V3ckNRuyHeJ4a7igMFlI3R5saiaKOCkk0+yn6BD5vWPE3EV9xS3a8bSgHs4GCSIcUoEM8GpPtmUzOVGg73DyxkYvIb4RkJfTy1G6w2hElHrWUTRJO+FNjDsgD3Vh4x2fazh1Hvwc61NwRtgMQ7Jl0Pw+sz80cjyp6BB2tQhaqsywawrclSWE6J1KDiGXlQ6XZ480VLg48sr5U2sP+4wGmCLKVIgaEk86to7uIw+Yoq6GRVBO6SkFtzZXdIFzQyUjW7JSg9qz4nhrNLqjiIPxj0+LBmly4H3YTSrMJhPSARq1wf6Ng3oCgPKsg1rScG/MqeqKrTz2NLNmGKeGiP1W/CJdqDrZFdVuRwAmKPTKr8MZk4DZVKfKOMcmpFKitBS8Uaucl5SxgoVMk+C1JL3atTmuply1jvoZALX6ODlPirxvBIldRpHpL0zQzGV2warsTWNeTDgWjVRlDxUoe/ZgQtLxylONboZOX9lugHrEaXttaKhbRNQhQcit0+fGU51S4qTlFG8wkCb5ArTmGUPA0TdmbL4LhqLGbnnRLuFUDZoOmKhoNWQr5wYNUhvMn+hUP29JcTaXUWO+ATGtIXQXovjrak5oSEhny97Ysm+Q5RrOB7JCUL4Wwv6aeC0DIHde9zjSSg1u6enq3qVKKyccgiWlFvaZql+W7E+m1ay5wCGIy1NjQQzOMO8kvPMenxer6A+CIMHoKwKmt/cp/uliZqv7gfxT7r1fdUgTcxiJjW0NXiTQUwZBkRQaWIUEOeDDFUbEyoQQEk2CI6HorIuyzNhl2eVtHgQtkM492qaYF6BZfIROHDERbAfLDhAGiPv5MOiTukmIlQ3dhML5h+oIgd2hQr4iGCR2zXIUwr/hBmGgCI9Je//AdgFFbobI6kp+PRKjDGUmmbDiuRUKr8ui2c+TavUXrM4w3uAmLoF53p8wHpKIzygQW7GCZR9PYJfB9sMhsgziWwzgHhBxLwgl20GOhEaJtBcwREHhudAPfz5OU/NJxuowDwkmBr3yQO5FC9bPIuAgnduvxvYGrXvYw80Oj29XBf3qWnDsnUoivP27kjF49qofeZwpDcQqY5IO5HW7SVRXzaaH5kI+Gjqct+jQ0ui/asl0gR2Dh5OWECNUsgbtDjIi+nWCu7JhVhLfp+Vy+OWFs4KiHmrCmpsAMelLIlTYReL8VJDmYEx8xM2nLq1DcjDNCQq/TXnJZcIgE/yFszmU17KPlAtnw1BV5ujZTWH1+5CerwG69HkNWKfQ4OPCEEAm9eeh1eyV1GLAgaeUXMCgqDThya8SnGAW+2OvUBSFqJdybeZRaRQzq7LIFdRgQfa6LWR+L5KLmXkwrAJw/qhjiijxWWQUNi4pYMF7nZWB7TRWrYFBP4yViAEa3LLPk/IKB8JuCRUbwYwBu2SkmwyCWOwz4S+MIWwMu0WSlv9xmRaUv6NOTswnAZxLfibsJcpU5kkBwVaQwnTVoHdtCt7YzoQ1d7thAYRa7Cwm/46ZWoPN1UkwZx/m6qkf6BesB4adJK9M7u48YcPPt+G+io3eOrUgySonXjWp02OCgfUKEeTz7oaMRDuPWth90awuRe7voNUO6bs2gSPa+LdUWwKX+/UE8Fm2SCyYy+U+MWvz4hDoFVTnXGXNj3KrVUz5/txHSiZztS6SjaheJipSLrZ8q7F5jWo8WofZuP7VlDLRdCEdG+I8AqDAK6x22fyFVNmAPcOIxf9NpwsJyPkdDyN7Ogydp826ldk84s3sJ5RbCLzhTc9MOGuT9rhjAha4gEkBOGv4/325tIX39GmsV1qGzGE5dY01+2/zlJR0Ntvk2rBw5rbvZDLLVLhUv60YikZEFLM6ziPyCM/i6CFm941Eb5ww2+qT3uiKafXwfyG1hmbdfJxEgRCwwiXm/HZ74ah2TeCsm+JR/sOvqE7gtKq4+hIw+ghfB4frFg1R81hec/QeSHGGayb7/sHSv8vmXH/TyFHrVaO1XcPFWyspGIzEWoLArAYKAByePmykHN5gcs7WBR2wzLQ1mu6F3snI3XVtl82ynlG9nv1JlDB+4XWkr2io4HX0zTzhEYjliIJq3g3pR4Ml8/+lG1tqTCUcJiAy+P/9urXrU9GQOm/78EhadXN1jrhjRZHT55Mc+oaGI4VQ7HRrVFuQAc5MPU/NwDP957qRNnRVc0c9FW7srXWtr7YPKQW2DqhUvStz9/4SLED6w8dfIUUeXbclLN72ZT7AfhsSFVERyK8LLFSB/Yci9t22RJjG+fkNoGEVXrHugUjSmdatjjjn0zLF4xXDboLU7Ij/Op1rzdWTosRF4PJH88/qKssy9Lp8qledvBx7cyy+OX/+Pby1eLbM21SSlmrqGAtM5Gta2K/ALxMdxQqXGhCnlQ0jUH0EwOT55F+gw2NbLeU4rIs8FO/gnxDIC18/ljvctyC2x+SKZUXmOfGBqHSXFObw+sFLCXDOpCtZVgH5RA1RsTsU3UlxzkOmifllsYg2CwMi2uUIGUCV48EHdnb5XLx+vlzCH4J2TLqy4Uh9Y1kHj2zdSYpMnGARpY80FJzJG8+1JnW8OCIKRbKfGhv2vDRcRM/BfnbM1LK7ks45JDJ9TsyuLiq/DDtkWFliZjKwYDZL2YlIFqC3Qm3kaB3mGihvNdSfPQgTDRZ7Ol6QGyT8wz1dxonqavK+azaGpJXjfS9RBKbNzgqeB7Ga+ZQf8NqPWWKM8P6G9SiFhLrfUYI7FABomjFz/MQTxrZjvalioQpM3vwzD4kx8eFJI1kHOSrisczXwg96oWwfm5o2FD3pki5yT3ovRtum8z6/dnFXh4xQr2GHc7ra3K+d9TyASkGG0Zn6EIafNIltdJKBBQlDdM2em9kWk2EFqgXGrBw5TCy6prECWCZBj4zpx5E2CVg9F7+ch9wqR70LXYyPAD963d3kGIIaj3w+F61IdKiYvDaFDtPJ1aSESISrU3BNZeI21LjAMkXhw9Hl9+0wi00Rloqs/XZCCcSQmX7rD9dmBNdf1Z1ipUnByBz940i/zYUY6wyBYNT8bicFDWmSKUhciyYDufVKuZZ+CADTk6tKs7N7XjNJCO4I28tnBoRGHyV2Qb160U3jlNJuA/XdJHIy5v14NthPME210ITQyenwjHX3Mgo+7gHsbix+vhAJhU3TAcNPcOvbFFYeJKF7o9+xfrhJiFLTQ621YEbCvntrdUmmkt5G3UviClCWZvZmzx5IvP3yZNs1VdhXIW0I/rYpKLpbKpP9Ayz0ozA6hbaBAc6HgVduMfjCXGjgDwPyy8V99kpMjHtiA1ScBcxV/rg4PtnYAzavOwv8NJxYSgko81FzACcWBmRpv7FA9LQe9Tj22tJ5voUp6NxMqWxaK7ul5XFttlcJsV6YKe4NNyI6LRzN/xh4AQL4BdkF85sbMRN8Jh+5mkLoooZThgOM1P+U/TJ3x52wSoFGvGdnWM4jafmYF4SxuuZQ27qbuOXynphJxD6Bg4daBU4xyqbF2mV6uo3QiPkpsPXDNPtD0C9owH6qQpmtEZ20162bwfSs/ctzaPIfARkXxTzEmN4jg1sArSKK+3IrWASAuwCYjolz4ZHk2pBmvZiNbnV8y6cblYQu/Fgm5DZyI9UMrDrx2yyminaBI8ufcqhqFHbJiIFfCw3zuHDp2nX1h9h1Xc8yDwn0LjjGiKa+KUj8dE42fd55GRXjM2xWegNUWhbLYTNUB3IqbrmpZ9E9MxwUCoJl5G0QwSzsXnw3vO5m+jEGhYIi0mBL1uF/ePUVGaLxeFnM2iPjjdJe6HARnuEAv+aPJm7yKAxjOqg97HIZvf0BJA0eFw2pE1y6Q1qUq4nrvVC0S3UQTzL1bRuOXvZbD6Z1Qs4gtw3k9uubUDhlKhSkjybmQR5NuPH/jm4rFoMgpiQUTFExN+gMqIaKUQTtEiAm3HE/KgUTknTjO6mp03hbqeZe2kxPpL7d2mMJFNM2Nm5sIOAn6WHGpE/dwcy81IoSLO6BPcWvR4DMsl4sVAEZervp9NUNWHPVbX+lbuw1xbi21/wufKJZuuhVpXERG002CkGe8+tC+S0AbSFIKT+8/kBQ0z8F7td6K2NdPB5ILmzoY5ODzzN4LCYlN4sOSgUDl1aJBy1Q4Gf7QBtBb76KToUuBgepq2cHbG3NAgykXSK6jnvUp+gEg1d5SKuhBzbYBtm5xeSKZPumqjeDVupoFC/2KZa3rXCkiSQqBx92CvcCJUbZSqHjU4KpUE9wJI7oJAdKOoDnkKYUgqXyc11eqsnQLBfcviYEFwCk1cL2flvkuZabA1FZoJBki5kcm+wFszjvZ9pLxgTTrpjVjh+kg3pDl6uIuRq+ua+8EsAQEDPbIDsVDTr9VA4EYTrDvO/Jp9IlRHWrTW931bof9tIXISfbrfrApBUQQUzFoCOrZmfW+qa2bkiklqe45LabWgpQvl7IxHIHzXedHKAq7p3q9lMjhEAs9GGrjQdW+J1yKN3QdYT0Uq3PEzxIaCF4l1plLSvppMeAlyAKKhG8TxESCODbXrD6KhMe7v+2dDq8321NEfKdVvPG+Hs1dY6Shf+8teBVdG3/ft9D3Px7d6zkOr2RjBLIkOogBm8TKB/Eqwfi2d0qO4TPtAwlgM+sYU4sBBH7mMRjOLcit+R7EETmtZUoypOT1BE3JpKuRM/vpHQpEOjDzup7nROro+P9peHt+AgqsLYybCidVruTVPRtDQ8sCAJ7JLZ/ah1iJzoUitaixoQ4zykFrQTCykQoj2s2fSqwohcJ2JSusnKpmGvAZ6rUACcx7UDJEgRA+GgVpQz8Tlsmiog2h2BH1iW32zwDqN5IuF3TPHDbd+emlowlkk2Dj0KRPA8//nszCPwRtRgHd0q4nOAVMmUb3HsgJoLdju8UQ8bM96XNaZSUYBsz6uf6ptbyhLeapgEmQkDz4GJQtB5JGEoshs9kZV0vXHl+MsXVmQvf9UAjfFfw71MpVE+bu5lxw6Br33Svf22lD1DemrRTAyRTDZwIRJfKsarvi6/YvfZiObsYbgqNbqQt4NcqKc7CKUJ07ZSLvxKpsUXMymAfcltcGWz7hCYTRuQTnkc7pGRExo4BnoyMM1hylFjUQCNvk0rUMnfWHcg+UuNKgcmEUryzKDSjIgfjLkWIBtGsuQU18sQ6VY3cpyXVXZ6cDHCpklX2KacjWc30LrfzimjqC11IIwQ6mJluA9CWGyB5Ww3REruSbGMmUoNCnmaKfmhQp6GxkzPf81ARmkPIHZJi0bJS9lvFLxYLei2nJS2lOFbBfs0dU7Wk7z1a7MN61sfMobPDXjwJMqDvl76t/MHy8clNKV9KoUjadPouuFz2zwI/kChDmh+NBpNikbbYmo71ItYur7eK3ch1qwtY64bHxvY9lZa37n1h6vbmIEsDmvHjKH0K6nsnZdGhh9k7QV2fJi0Kd8YTq9wSE+m8caO0LLEdzZk1p9Je4niYqPN4DAwmItRj0HnjeItfTwGRobNW+YMrQubXEvCofVrvRxgLhwxZRAmTgs5O7Cl8DYYhgxxumezpLQZ/2qHiPG6FhEgeB96/2RdMoYErh3M9UKT8mNFyGfioFzc64fbPv0vtN3e2GLyFrLH8zXct5pcsSrPsJPGvKD1jErPbDUASFMqYHpGW/pBJqn4WtiOtVLST62ewOVzl9ZUa41btVYMDY0+N0eN0AE1LdET3CI9kLnfR5enOz3PQPXEUYakf7PI2WND09fQx1cwLa78ZVYLx/CvzSI/2akAJjo1wQUhRY+ma0XA5OM8aoUSASRycmR5qMWAcKMolSHyYoS1cSwoFcIq5otBweS8s3n7ZtXeX6oW2+crknJLy9rueIVNdaYzsy+AqWspPl9kyBalYjdd3fNYWZoNdJTaXnYrYcVv5JgSt2kEgDVRwJpQeaolH2qlODh9H1wvjhyJ+b17Rx81x0SWjO4YT++e59Fl+ll02zAYo802Uk+NqIVz1KPgWAoBtukhVE4rQ/e5iZFeJ0lUc+Xo9qCf86qm03FoSEzbJJOdqoYBF/aGesHo4Vqryamj//leHBBy4QamDUoHae76ZehxMqG6lAYVOIKogN60QjhsT+trKln0ZfqRBWr8Bw1kcrL8zSPZqcx3OJT5O4ZO/f/nyBnWraPaUu5NcZQiZHK5cxWP+4kS8IKrPX8aEp5pAZw2hiViyrOsb7vlGzn4w7rw6j6blv0tpRkGlHjo9zHnhLBLb2SHcvMaK0vhuKS7aD6NyLy7sitrL9sQ7IqyhzVfL96aJh5ipnzV1Dj7CEFbdVVUbrLclXZCZR2KDahG/RSrVeFLXozPwsEo2BglvbW+hjeLOSA1AaSxuPx0sZkVI5RefjrcUfOjvETxBRF4d9ZmEaF4sQhENzR9xjjFZNOHMceUalszjxUzv4AdbgitO3xnajgY5PmyXGD0iIBLKYQGTAq42aHahQXHDJBMxgwkoEyxrJ7UZLz7qrfJowc7T0wmjPmWak6dhv8OrqWl5cUZl064wdPW5+xJmw+fvGPWbm8kzF2aodGHNm0NekuTl3AO07Y9krGITL/E7AT8fa3Yp5iPQfEe1O7r3SEQmsOW9W/uiYa24/0YFCQWfugtH2zW3xik073YXHfQhwcFMDWkd9UatrRt6nAPNsh/830BVlX1HT16X8H6q7v/vpf9Le+60fL/2ffdOf7pIln/lNImnR8eaCW+2O6G0I6Xim0hFvV4SStlnLYK3GPD0pg2OFAlpKsq5wV+YtLhwPlVuK6B/ys7gENf0ndQxaPm36HBp0EAbAhS+hj6G+Q6eZG8xFprWmJ8hcO8GnMVeb+te+cHFBgXedq/bqJ+rsNGz+oGYZ2LXC7yt+nAh0bOD3JcZTyjp6fep0yRXTccnQe5MfkZlc4fF9x69j8u8qd9MKpTvqRd4OAyLxfPgu9jdHh0KwlNSIIIGItkAeaCfSgOnAflAjP+9Doijo+h08MZbyqV6usayMlPnsxV8a7F0YmIVs7ZPnX0GLBFoaZrzYBSYdooHAI7+JdU9BLH1ncifMhtca6JCtmtDL29NQ4FDcqMsgl00ZetwhZ/0tGS7er5xHXUS+YEUR/Gbl9YJ9BaNoEbuHRZiJvNBoOJYj9smJ8CxmuEvQzIy6G0Kmcw4y/bsHQuu5r+3LbTSTNxDdEnZv4sQeoM9ij9T7KXv5H+a9w9deKCtGPfANQnoWTiwqxQOtOtDSImgKIRHtblrL1xEqe6fQsgRxm9Xpn4057RtHdmVaY+VBopd8B/x5BBGj21IXxoXgY9Cr2q5SSjT1SPOAAetJhYNOrN19/yTqHHTDtKGzG4iq9kcs3Wu5h6cuuRe2TgqzKVg1lJFw4ECPwVhP9Lh/BUm3GiIUBirkj+ibNFyFkro14WGwXv1Thyoxn1HIl5G9WJl7uMpn6bTVimNbVRdKPFtLyaNW+2qclp7kYzqiNNTjlbhij1lhrYBBxEa4DX+ZoPwK5hDIykm4Bd04btQ3XTMyxLLPz8by/9Ev6XDd0uAVVokCcGfZIxVI8QtWP9m8lMKMbNfbbqZu5t3xdCpfbV1sNUGgvPGuqSpVxw2l9XE+9sg9YEiEZjBuq4igg9fbm4gI68P20KnlV4A5sTlTlfeH6h4dnzWCjD4QAWkx8YxaO9y5RLptq2IH/6S2VgzNW3ZVCg1Ii2I7z1t+UAHL02jbVmE+MiZKip1dmsbBgFVv9a6gH20tkgCKwV3xI2Nj7BzygXIZ5aMhPmg85YUfNZ8nfgGwNUoQc6jcwPXkIp2vvZ2gKZ6gnbQ5nGWfappZiZ7yrUR4dqKJKDJumU4FvvStgSmmiOCiZ4PuGv5myt6KwGYUFxsVY/sCKhgIxdA/3oMYyMJmTGoAmRN1f4zfNB9mlT7Wt61naTs6oj7Bmes+qg/B+UFZYlKZGzCDVs+oLjJjBLw4rI3G+bffx97jdedEfPUv1n7OTrnHtlXk66tjczfDCIUDn+6bPsAvuckBiCPmL+Y+tFZPYgjOnzp4o4/tyhz59RdSME4GbfWPEguHnq4ObPHh2iQae3jdbr/KJd3BLpg8yL0JipXKohVk9dsIopAGM3NU/10j9t8kol1NHPVTXNqAWUNXXVvqnrIfrrnzxReJgQpAZvKsOUBkPo0/ZhyJX/rkbgK4t78Tsa1sa4BQoJYP4MgGj0ILocBUgek8pUoQH0J9YvdSs1c+uhNlVb7aKuLbafflju/d8x7QZm88kMzJ/n63MQ0wrqVWueMujYZzxKRs0hp5IcKafO4vQPR+CRe2Vwr7mifdjlZuWQ90+NwTH108vRi2cAIYdy4gqbrnLITfwoGCYLP0Pj7aZc0c5GSNys2pO6Cx1wF3BabAuCnFOBsXVJVN8WFgWl62qAK+O8hnjksrhwnnnSB1bNF4I8yieaZ8kke9IH6+yfj98///n4YJR8G6VshTDMTuSW28laOKnJkjmxNzyGYuSyPUxo23rLqb4+fF8d/D8Nhh0eDqpzqD0AIM8e9qHqZv2DY9rBK+i3dni9R4FAf98HlXkWZMu9oqTt6npoMJhfGuNAWmIPoIemzaxUUJ6OjT7nc/M0mcvDSR5mtqu18WbLONbPfC3vGsSYTjcNk9unAVEuiUy6tdiW8ClKF1Pnr7gpwxfQ6WpK6+oQATAYcwTS89S3iGeb2gWNhjLo2XhpoV6q6VjXzzgRa74rETy1X45vq3IazVLQxIgT9P+WxcCl9g9qeucALfPKG0sX+pq1iiPg2aqWr39tj8lLPCcl1DI/qxDlJtmOIgngWor0QYFOESExfxo/7zPM/LA14pyb0B60wHhlq6segTu1oZ7RFKFLvm7NS5NOmklycFMOXqOmGtnLLaxsLwTzpF1COfsG0pi93Fq6q0rsPa/1NPPnlRw6DQVZNdVJihuvpoXccHVYjM8eFlPk6EExHMIGhWjulhaRgwD0wWmhM00aFAuwYn3EEyNCBI05DIHEr2k+4BLM0+aYwYOirJruNPSf3tDU4HtrrCHZXqIxqFqW918gdPoK77K2+TG7aZftoSaaF8paESkBNwpkws3Czfb/Euz27eA3m5WLvvoLPEB5xeTgOfX+7EIhMBDNk5B9mU0o/byFrzetQMhBlleYQVZQKggS3nN6wWIFN5n8zlxPHg+be79/MaJuZJZdPVRAMmWoPSzlwBfdccTXXNCblqeRQdGGCG5RRq1WfdueKI/MAPqTGiQDFmnNsecO0FKZaXbGN21hip68vGmzW/AQnnjLpDtaHBZqeJi526uc4+qmOHR7ZdeaKuaFRy80pJJ+XfkzPP/C/Do5vhMgnzFn6WGpjpbhe6v/Mrywit9b88r3uHDaZQdRWO2GA1R9yXtplBd3G2aEhQ9VHCJzBCYS+y08YKnjVQ8QHhOi3WOz5r26GEQuWRNEFNsFEE37bjB2g4/vw2rtyyRRohFcwpRiBGcw4FK+I3q14jciCHSucNbh7IrokkmRg5jsJQlaWxDblhCNwQAkL838Q4/O527KX1JbHI7R0ZbfMrIFZguHlPMmx2WmaGbqOaVnW/OdmhDb/kxxXdV2yODu0zTA4XnMdkjvZJgBNHaggLQmZQwwqYy35IbjzWE1kY1spv67cpS2Wy6faC7/M0aYKygxk+c4E2JrraAKKtdLLhwZKyBiKZrEhjRMk9PcDRmXulMpNsVGfU1OczfqM8+EdpAbX5ZfKixkE2NohsGwIytfzKIMcFhB284RPV3FodBHppUwTTUMQJGEBFAp0y9BwpRIoIMzQxr+0xMzssFGSm0SQDCHVN/p85Cu022jtE69zdJdEh007NM6E5fl1VU13WjpksnbHoxT+eaDeVbfLJ1GPqVbmBAJvn24peV423yCALLgr6WM18eROJkeUnMTSBMUU/h+krRuOVktdUMmuGaIqBnM22cpzKYb42/DvBogXhH1WPGO1UlgbO4T5xsxrR382FGPzXsh6gCchCR1fkzgGRWQ0QLYaPIaWGPasX4u31Imf1NFmNCQlGCEzuqb2y3Cbksf5G+sLc3MsOb/PESFLUAd8nVI2HXsWBTahhyr6WrgY1Ya0aRHjX3cyTs17UnLJxY+axVSSx/WoOtls/4ATd1sX9M3mvfiaeu2xt7BDu+dO0sXqdjWWaJrmvFhJj558reXvz55sr2uEaCNBrbVXzWy5kw6FveymGabmrIj60XNUGWj9GJYzoJirRfz+FhToZIz2IOeamQPYEJqQm4pnMaycmb1QpcIpzFwVj0lm2jwpGkFdwFiNb+3+JrwFGDC6EZTnj5TdRV2PQ2VtFYlBAKlV1Z7RfRyOBH2FjFD/cL1vDjVKKp7+dVqGZP6pl7IGqOozwFnGRrI6vXyRoxn6fAiigdN3iZhaInPQKipegmnlN6wGYhhMrO0LW8zrJRaCW70zjsGoEB0R1+kNncT9U6pr4cv66NgnkRmJAvyDF/Iy+5+jNEyo/KZm9BK9ihXa3QdTp5o82uhAPMWhmk94JWm74XmyuwgvhQR4m40wbZoKXFBIwZCTSG/V/MFgk5J7iX45y5mL3kf8986GhFzdYcWot4Kt6lYVebsZZCACmPl/l5kM5kEDIkYM5MgEubUQBd7mmoHJ5/reqmK2bt6Cm/QW4LcXEUNOzDLIBJAdMXFtyC5ukUo+GUs9YOV+meUGoWwGe45feQdQ9/j41DFWrIuavcmIaDGVRUCZhrCoernAKmsBtkGGya7GJgd2Uw/4kh8Tqqf4Ahzrqu5djgph/g12ExXhNjoWA8TdyTb2bziUUEBWq2SIdBOuhYYuKNtO+2papm27bc4jJO1ekr8cyyQHqjd+woNrHZiz9Jd+XyzfbUjnN2fNjKUfNTazhxc8Tax5WUUDGQUFX9MnyRsORZ38qABKujOp0213k7Mz+EDzcVH3yENxxhrFBZd/J9yh88xByOLGjqUH/bRMoXywauqJnpZwLmk+mXh/t5UUowG34QN22wbeMamrQwesqfWvp5totZ5DFgtZ0i4oU+tK2CLRv7qxiZ917uza1BohXBwrLRriP1REMHv/h7R/NoYNj5l+OaB+va72hmOyGgXmKwuBptb05346txgbCatDfYof6vBpaDmiaC6w0BTaj5/vSSlpm1O6w7koKZ0m5NilrbFDVzhTy62OIErQAm0+qltwFAeImTgv1YypXuCmPuT+QkXChuo2QHJ3IZ+bwdGOLHhpL9ma5WYGJqtUqSx0X0qCURkblKl1omopSDnGFx9CoPa02A+zoRQks6yP/y6zaH5rZBFgDwfl4tdPs20ubFisMiygBvt4qS4bBd6AxjmRQlpy5sVbOVhXXXa/ATBCAD2PVP2feRSc8yZfkvfEdkxbqvZgqnZVWhAOIz2S/WJu0rxJtYEcyIZ+R1zXEbrglmNWCx7RErGr2UztpjFDV2CIslICIMMs5RTj/Ru/oFQ37EVOG32CLlHEzTwPu0Ng89T/JCucdiS0jfJJFCwhOpL7HvO2gWVYejH6HOzPSINwz40K6G3dF4qZ6fXWAK+DIm+Lhzb9KhRAda5lhJuovxqyu49mrH1BlWiLxpdu4e+9hvr6BNnImO0GfroxmlxvQ5nIgeArI8jQ9upNMBCGLLKm3tfgN5wCgzhWClCMdQiLwVL4SHZKSbh42+p0FVXOwdGSSrQnG8II+P0lrxLqO7GRwaez2sywJqBbS+EZTAkRgV3YeRbzSO5MvB5H+PN+K0GCLA1jKuM4yeo/TXzgizdoMQY6kHY4k5AWTDsFgXIMPANT24Y5dBYQ6oWrWPWi4xWAEf9WGM7jQfPAmKATHJ7t2hUb3GgtsVSJOHXvhhuy6GFL3C70wTDBVD+Id1w0aeQDocNdTy4Hdp3bczbB7BAdBtA+O4vSbXLdr2N02tDnJaCA6hqQE+to1CDiimiQ8KIffhNj1hH62LUxk18LXhEOpRWCPT16JOS0F+zHQ/bigQ2JA++H65hbzh8y2MyqO8VPP2YWcKHVpgYxkbBnacmALJ4b/yldJs+atYSX1KmHwneqpnB3V1GU+jvygDtNHxniDCaV99kbil+QtOGdjR1z3wHHJOxsVHELPc2e4dd4L5dfZMNbVKDTefJBMSsx7Hnv7qL+2ZS/Pl8H399yZ20cVFcVNEGFk9KlkVvZq8Jv+BhDL9UWKJrYQyde/C4ZZAQDwLqYR9QeCBbm1yRRo3ox2aG4ecfLBTl9nguidF4qKDh+WKOhePjZlQoxK4Qy5Pq7l39jcQStGodiv0UAxRCbGyAAWnsl6Xn8wgMAYdRJXr9SJOufA8F0yAb/YzGnr7xH8uUUCyabp7DOdgzrKee5b01DlSyHM3hzX1kP/HwYNEmu9i0K29CPFTZ3djjJrdHcHOiVWZUc12tlkvw1VfVDNzigOzR7WOd1HGXiy6uNbfShK9GvmphyUyrFhbqGuK4HJTzRVnfNMVqOf+PXnZxofYRxvm/IRVocat50QuvS5afiXasLl69ePnH0YtXmuZtxaILT5OZxKIJRHQoFXtz1PTYmWWjKwAu3b9+/hx5o5u2vZlVAJVW9Jnnsp31z6cK3fajtC05aHrkbcvO3rX1dOSC9ZFUiI8B76QuP8uq+t9/WJc2F54EL3U6bQOIuur+ce90JW2OfJJtvNubcnpT7XySrIXZ86p5foVS/XOVRT2n00g9kYx9VNhn7mjR3GToE1qFdTP84vX7FIb87bn6Ev5lWSTUZH8uhJ+XtfQfVROcueFlYrLxEGcqGHEp9J1JzkPQKSFj48t2NbmFFHeOoK4fjyi/FN4QyTQSZHr+8YhBgjwYJxbutAJoKZb0Ia+YKJm8A3r1NRge5OvGoqlKOWDBJit7YLwDgitd62sKVk0oeRwWmzv6z8Oiw0lULlIwB8bgKaRC3g9C3Gpky+tIY9JKGpCMtQKpGVTXQGQbD1Psgy2Ps1CsowzUSvZyHlviR5HExN00milSVrcfPHEAGjavmpUquzDCmWHcFMeGayO0CHvPK0wl5YSOY0JAyFJQsr9WayEFTGNKYaLFaqESQqHrdpr7O/aRBm1Jbf2vVvVs+rPax8iJU26CtUxv0VdCcGrf15IkKzTk763YwTq+XDOFNUNlqH4XIcFx/AD8QUgQiis0FoCGBDCObDp2NDEY92YWJDXulB+SqKkaQAehtQtabt1ts8NZ3rV/4X7y6hulLBnkHQyTV9H0KzF32fv973//634wo7AEAvm8GqatNfK/rstZn6ruHmqLwoGNBqGak7eQKSeMB4JKY4AiJqMwT5T+qKRReNiYlZR62yPYWt3fOtawteMBpS7UrVdO01GzEDIPHO6Y6uL9S0vOPX0vT7tnSIp5QHe20EFH6+/hkIsWJ6uUbPzUFp9cOR0aVNjUTfDSl26sYrU3g+6ECK6bbH6fhubC9piWpjk5N01Er4Ebhmo31h4APrwv6LWBWiyzvyEYAM8MBzwAjtzcW5wAeFws6XqnyXlId9DvYJzg6N4BaPGOzvi0NdGp7O6CyNCpwOQel1nvkZQ4RQxaQLPU6GWMwmcAB+2C0UupQ7DQ1KztJ3IGMJju0+QmzYGO6v1Kzg1pCeXomJzdMFOvPYJBcWwXBLF9UzEWS8HgGnajVqtUux/W19dq2mEGV0hgRUNYRDVFWBQKd+Wge2fxmn0gTDm6QfWSY5hrE66n1laIAP5zCT9JYQjV/ddvvV4EVfC6IUXrfxD+80KordXHbY57zT2up9OZ5+mNZnBfSerxXiva7qs9dgdaQ5bUOx9Iz/SxlMUzkXsiOPxfuPsJA1lAvXDbDblsioLTqbG+pegZKJ0niEKtDi7wgJPDGkC/GMVYzsCaQdta7mUnp5fE69Iox9jP8ou2A66shXyQ08vrZ/z6uqT0y+t11sZAFRjQIg1cgYS0AEduUIIpcAXScBRswWNTWHXP08ohU2vaKPvwboT0CHh62JmmpysGFsqLXF5sv73ev4K9QL9R6xie2HJphcv5lYa+VZvBplUyRZsEaFCVPqnxAYw+IdxMsChaE3dGBApjzhxGc9yY+Z/zaJ6hwXsTM0owqbzHEtfwJhRQxhiSdvIbIOMAZJ9+tDGko4ZAWVj8IeJvmzxTQ6rFKKC3lWX0t6VKH0BGZYJfh7gHZvIpfECEZ42Saw0prfLvRPJJ2JAQ8UXtr2JwGM2FnABm29eEGqYSaHZ/dA3oKOFUYVB31AvDBOGETOwVu72AufsETGTZS409BiIDcxxbcS1KwPEyaQKFxixeaut+cKW8wiT9qochEjmefK/ceFoLwkZVUqm+aZq5PqpwuXkGWE2IffDtB29YvPOc54P0UCM1dvKyA29aLxhieldJwTQ2d5YOyhhr/aS6c4tl8H5ro6bkAHIHM8rZbOAj4sZhZlvUwh1trKyYuSXbIj4XivoTtSoBggtJuabtLjWw/jAy+67V7bVDA7eDBkJ0iIeftxkh4tHq61EvtnSCeBEs/F2BL9KaW0NejB7WbrxrE33//zlFx+MKDmX5ZBHX6nokU6VvOyI1GsNahjwgDAKHnVzwHRQe8FtWi0jiQXFrclgd3Zwc9sewtD3vYoAJROt2qNLKRs8iNDud4U5Oht9kU5h7cJRDxTKAzIrpifW3wRz0mWwWRGtVZvSt3RnXuepmCiD9weW1cB4PaNFBimsFvVQoFETHT54IiX3yZLD2giVAcsozTIiPPZVclHcaBnFQ0g5VJnclTST2kngCv3tzr8HIfuf+hVO640JcaUYI2IQwS7d1wXhFq3LZflc3XHbqkm8YawVh5F3XEtZi0Bfnx0efm011YWv921UJZeJr/ggHBBIvh9bQGPRTOCXYXhohJNSEdrrVcnaUcQsL8CpEzMLP6/yvHpcBARhWfWXLKh6Lntqi+fB2fPj2/M3p+PwQ0bmfJcE11vS3Ca6G2+3dEkUK//npIsypcLTwSZUF0WdxEYSgV/I6xRv5yVqYb5cqIbHQeNHVSpVBd5qczSk0leP6sUJXy2lfY9PYAvxZg9AYApZHkB44rRg4dYqerzQumN0ZHYuhD0YwnwJTdYbXM5gS7NNqdGDuTYkV31pkhhCYwU356NQUYT/M+mLkLmAes8MNlU1IagNNXkbdEXSgDSEN+luEve9lmy0+yQU8FhtEwlZhJk2TI+qchQvyiECECgFXH6EYGOYVPzFzS5IeLH7qkiSHUJC/SbsOo11NBu2huWFrBX7WW9vWGNsChT4oFzXhOHA6KTRwDsg3QCiUjgOTQcOAKgyVcxKFX6gnwqY1+QcPHmaYDQwYpsjpmjKoyMgd7dLCoxEt166z61YWc/dv7VVf4MfuYYjLzauA59D4X/NXL16+3IfqYDtCoFULay0tVDeeK52ZFfwFkgMcSmQSWuaZMrCF/bXUd3KmhKdc4RfZLceAdp6bvdBMnTr02bhtZwT30oBUsr3VGt/iIo2XAyKn0s5YQKUj5txjgZvD6cIf/KntZlPWof33xepqyWrvK5ie9gnUO5sBG0DY+KsKTtDXK4Y0dxdFbZ7MuPOyuclOlNqM4pO0dzSlvenKxS0Yqxmku76IQkmNXhR6djSfV9gP7rktzK69h3d4E0aUticSHJKov+Bc0Sl7CZP/gkKA6pkPz2j9idpDBOoTMqkBXN8aOJAucOW4dda/obLNl/xmvnpt/RW92Fkm/dygQngzDqYW/VLdDyoB3VMIkryRdTJuBhdHF2gSVk+cx9JQoE2hzPhsUGZzKoaiWHtby5JjMzVwLH7UXGDdbdTILSOWjEqtrc0TlVjzYx0gy4TSuNlnL7Z0O8gDY+ObokIh4dKthIgnY2WqyYukyMuwZE0BYI6GVff/tnY1vW0bQfTuX8FjAzBC7NRtU0AHuQlqo2hrBAEC9MZIlE1EogSu6Mb99Z33Zma5JFfupTd/kMvdndmZ2fl4s4V2F1V+gl+fzIXRLFmPkNRxYpb2vDtqwj2tZoBp0fRp0brTAOwXGENuM28u3yGv/+mwe1KcVM/V56ss4qIzWb7sXQz82H07QjYjFCY3BJ2zGk+HXSjVVfBzbqFXy1tMMFh3XasAG1WBYxUeKCvcOZsb6+3yhhmyW3SsZBFeirRhHvbpS98vLRKjEW86OmhONkPkRFFINE4YLFzUPDG94IF3pc6eViR2bF3VZad4TdpjJ8W4ElM1WBHkaNe1X1IiXvB87SEBxFMKEGR72DVC9qM11nLom7raK+W9zUpp+MLyUJYEPwy22qd6/dgexKST8yBGhPUSId7CHuptu0ZBuUhzEXi5oX5ccgQGdpIBsLMfeiCeV5Hr4R586OuQG+anJYVibLBlQ2Drpi1JCBmZGeLdZFHZKWXbm2SP4xvU7uwbudclY/xag0Of/eBo4zVcR208kKIsVrLzo+n6e9kPXeY+FPWOf2pXdWgPMiT3/taI2W+EKb7Db8Xnuv76KvuNq/Pbe/3ntepqlDjuj5Aij80xJBZoFIAWFcmK9OGr07cul3KXsuOjOeuONlL5I0WlYO0psA9a+cFrTnddW/eIe5awDjCIHI7TelFyAWKJQ+LZxS0Qsmj7zPOwqffPIeY4q4fkm8m2QZ8vXpj8VX65sX9CFBHaeM0Ti9fPEWdpajyIsBRZbQG2X3aHXhsn37Wvf6/FaHhOTmSJz6yt/QbwDPDgDNlaA3DtaF/REzH0Hsaz5bGA60n2D25rMajwx02wVCykCHg7Py2KWuRYAOFH3OMYhyzwU/LUxyF167wyn+11+tanCcp2+qbzUIgSuOZ5524bXrd22lXvenHoHiq0gewIeus1CCZgnU/satfAy9sSB6cKX4ONF1vaiULYaBNt88dFcsaeXix1sviX5dmg5yJ5MwxsCZ6IYVIiBdiANICxUneCujWJbpxQmAkK8AkIpzqKnGmUrhIwNbelkWAifLt2RDEzmuZW1JhE/thn97hMnzcIEOTvKkLXPepoRQfdV8rbK80KKlhfi3MtL9PrjCUB7r5qdem1kcLdQEdca9fNcVDJYytBGZX/kF/A+wY2iqlYbD3mtucPsptOCk5D53zweVGcPLBkD5mv7Wzlr82rP0hlD5hgFbuDsY5cRGrGyqnm0yU0xCaiu4YmmnU17ooJ8lNpa1RTSYdPxllMSPoC0eH6i7bxWZt4dkiTd2Z8MHl5yg4vUt/obkwwOiVJ+lgEaLKygdnhHDqPIZUJXi9alv83fxBnK0T2jHM/HQbCFw+13jtpf8TsJ9W4TZeso1R+c+dSH+pUL587YWjPaqZzcjxoT47qtLIEzzGGTJF4nnHN2buPccT99OHL5cpTeVUTAaDH8ORO4zu6CG6R3yFOX0WhBX5P1JgMQoWB2UOG2QeJaEXUfkNAozqKduzcF/qVFmfnrYr9Bo5tfHFO8EHH23xLWVolxpb8wOsGBfZweeC9hyDu7KY5WgNgHg8dGUIxjtVwh0kYFYga8HAn825W3K7+WDnDsQBQ8+GFahowS8WIhfiRFI5OO+063YpDjFMGLT7mKatLAm2p2DM9yuZ07P7dbfQiFS9hSgVgo0zvaZEIMV0vpQcLwnr2b42acp5ucy7TBiJo9iw9qH5oNIjkA1KIONloJUSPOKILA+6+KfPKigBpdsym4OKoYSn00BWSKdCwxZ274+exWjewNGaNOs3hZv43zRaLGoLOxKDjk5RpFiR0rEyFHY5X8piRYKuuhFrWPJH1+mRkNIIc74eMJNZig6RLNHhheh67yjDtXibQMSPfYM+SKWcIlZMf5qbr98QUQ1ofujcip5J+V+RUKn78+7R9dXD0+XELasW2HrXw0OjduFHGBRKM/xJ98rHaNH24a297hAwAdBmW1EX/sIkv/4sNfOwZP+ADWiBdFldQtG9fXQgRFCLcu32bY/jmhW/c+TvaGytigX/5j29f/Avc+t4WhWQBAA== + H4sIAAAAAAAAAMy9a3PcSJIl+h2/AtNtdyXZkqlHTffsqA1WlyIlFWdEUk1Spe5dXRsDM0ESo0wgB8gUxWqr/379HHePCCAzKVVv37VbVkoC8UIgEOHh4Y/ji7Ipb6oXF2U9L07iddaV0+qwXSzq1aqqDpbL4lwSci3R5ZLAEj/X1V3Vad4XXmeLcnnYNquunffS4DKf2k22lJrFe/nJVlL1Uv69/lI1q764tNtc77P+ru77y3pRNzdW4iKmeCG08bmavaqu264qfqr7Vdvd5+11bhl5peVuqqbqynnxVv9mcymJ7vbFO7nKeamJ1U25WpWebnfZXd3Mio/yIy/WVOsvVXd5v6wwVLxh/nu5mb8rr6p5cXlb97n8v7qtctTKmZff3dbT27xv8+uyQ/a0XSzn1aqa3+fVYrm6n2Rddd1V/W1xrn/lftF+qeQWf/iUi3bdTSv2JdfrbFYullUjo3LcrKrui7zmkafknoQPgg/B78Uvxau+0Fe34SrsrwxFI398hHBtY7SqF1Uh36Di1WXbzlf1kgl8WbxRtarlK+S9zB+p1jaSIe86r24m+RtJR7HrupPRlSTN41Cspaez/LprFzJOVTNubln2veQjtV+VndSum2qSLefl/cWykq6/l6u8x2VM/KmaL4sPfZX/7vnvcpkieVfdrOcy+CixfyXvqjX28t+90AKzdn01r/aZylIsVMpgy2drGzZ90s6qd7V8FvyElPMKV4X+yZHCITqqkMrfeM9+vavKL5V++HzVStdYsVzlZXdVy4eQiYwK/Z/yCh8x7ytZRLM+f1xNbibS4z8+0y63TZXLmlivqlzufmepjSQ/wUqY4YH5FVeIDJo8clauyvwaA8XXPG7+vWmxvPjOjz83TzJ8haPqpniDr/F4Vt08yZqWE+Cv1UpmdyVNlfKvkW4jNb+vVpNs1a5k7n1aP/vh4IdsWnbdfXEwna4XMuLyabP4MYvDcJmk9klyn6ZfVPNqKi0Uf3v269O/Pf81mRa9jIpmTqSHF+vprYzqrOqu2rKbFadt3kuSTLSQliXXQqCu65t1V67qtimSivkgJ2uqu6N2pf8Xp9XdZDLJqlm9upjKkPbFa7nM37e10BnkTNv5etGclvLljhvvetorkknm53WTe4k8KZKVs1l84sFshna7qpE6Mfmc98ixRXtOWt00bO6y9YaRjKeRwPpCzkNBTL3Qh9A1EoDL9pDvQgqAcnqbrRvkFh/4Jx1QPiYdRzY1LZup0MRD/pHlUpV99Roz+oDl3zPB5niZ46UGhV615epwLqt/XFIescqnyMlmdS/zTekV5uxP7Z2Q5vsLEAojY2dNcaSldMFgNWDuKjUBqbyrV7f5bXuXC4m/12QZkk63hvhSH6UY6Ds6fzDvJOP+9VfZLPrBm7MxEje8UF5qwbxiSe/v5S2IfDuf9SfrfvWqOl0vqq6eho6uQn6+kAKyiPNGi+xq4aCfVs1M3uahNkov5K0k5ZHqI4KRlC5dVd3ZtW4FFz5WJNNhrHSMdIIOZjpTxjNbZ9Flm5aUOW6zC/MsrZBMBYy47CCyAnWqc25umT5K/6o7/b66HrP2c3H279mimpVz1C5OcJXrRshuMtV6zORWttBt6/dM0rcv266UZXEuP7zyzTFStUeP+hw5WPfYyDp9OnmMc9bV61zbiBnf0ZQWzoRNEP5mztbO9FpbSzK+3VovcwybPQj6+a7+ycDOy2VfFftZ9XUpW2Tx32Wd3BTvMK1WWO3HzYlwN0LYjywhK9GLm+oi3Xc+rV+8ePaHnGnbCgQmQzpm2bpr53in/KYT1mGWz9YdJqbzGnufmmk5n+rW46tRtr76S90LTSeLyHvtGPjFLxX4lat7ZmDnlTlUzbCz4Dsd/CM6hob+wT1rPr8t64azLsdVSEp7N6uvr2XPRnPW/LLta2xuupLJa607KbEir8MSXfWlbtfKuWV9ucSBQN7koCnn96t6KtRAE/KQkpLJlBqm6QMyOahxUa1AW4Z01BOz3nOTFCzBg5UU71dnjVCem6NqhROMLk6wUiAOKxJ8MJszZmf2pvyUWCNv+aECG3T29uES6cD6oG18duEQevlS+MaXZJRPq6+rj+X9EnwC+ALycsXry4PvKpg+M5TXqbC6lfccMcuYHr0yleVU51cjzeZ31u5eflX2zpynNUEI/J1k1rXTWpjThXyO/KZthU2WzvS1fByd0Zic6yrHsUSmWCdjLjNqkt2US6fu8U3fyiFQOqTfe2sRf8fX9n56fHocF8sT4Tm7Wg4z+uL+2tbdMKNthpM94ODIwpMDl6TJVChv5aGyBMP7+8L9+eQt9420ScwZLAcu0mXVSf6Ca7L6rzV60Wr79kDuNvp+bKlfX/VS0BrqJ58afL0bjEM1n/e6fYI9rcpuXlfd+CPetev5LL/FIeG2JJ8me/cdee8+dlLOjnX/Sc56wvzjyfjooAVJF4dds2Fbdu1/6g7mg/o94zkcx0nmc+REmn4rM8QXkYzmrrx0Km+bYnpItsMzptad0IGn86rCX1toGN71EplPZ+1dwwmIMd6TCS8lZB5Ky/d7uX3Pct42N/tkk5MGuDYwkfl1Mn+U71WX7ds2bGAfLTMhy63098FK/qIfx+8Q2oi0d+sCDF/AvqIwbf4xMDP/NwYh9uB7xiHyCW/s8x/g64fBkaX922t9e3TsVZMVYeOQpAznY8pX/X80PJ+a03a1nehupzVIkWkv9Fd45L7salmwWIlOQdnX+lqWuLDkUyEOWL3IrHk8XM4lX862OCnpwQxSg9cQGggrfFRdl+v5aiBKoBRD0/VkKPwypWV6AMzlVqVimltezStl7i/91kU9uMfnsYoUfxmfyZObc4M8srFJJvNhSaIm6KkCW5yeIkjDNfHsmg/AKUSzXH6Hw4hM+iT/tS0Bz7XueFf8vi+C5E4fO2w/VO9X7TLJa5cxa6oHZFl+kEuE6+xWZYxn10liKnhM6mXyGeqmkgGPZYsjpuGkIt8hFk4P8X04quvBSg7QFzIM59hMcM6Uk7Rw6DISnaV4Zw9QqLQ27JncwHD4tMaWbSdNvMdvBmnQUSnD987FQhiRrpox8YKXmiw8bF98OD/us//s2+ZDNy/+7eLsNP9w/o6NoDm2mbOlo9hSzLHmmIc6H7panyvNWmEkWTEkquw2EWnoTOrzw7D12UBlt22/4hr5yS5k7KfCnFKAc3hbTT9ftV9lSDowKSqSfSXbLtaZsNVHsSx5/Yt0ztg5029zEypky3LdVwfrVSu8Sj01gW3xHql56cm5CXQzpJwPhbuxlJzL7jPK6SK/RDlh/rh/ohmvqtVdVTV43sHsC4iTF7nSHLa2v17OyDnVjYsLX2p9EzC0zSkWBoSXPBewBZcL3Mg0WZEPkQn2S9W12fS2bfvqAxt1SfLZdWS5tbPFIYvl+nB5thbEahiw4NqjrOq6tjMuHl9C6BjP3q+Rnhufr0wl6S8OyazzplqREqPScUNmDJ9sWP/aCmkDdSw2auzjbT2vLrt7KQpZ2epjkK0P27tDOSEL9yTYLfqX/45tk7L0tsf8Lr+elzdZ3SiplfE3GtsWq1Z+L4RrxlUvXJ8zuDMS1wvwgZGnR2MQFIMiqa6DU1JPNrJroriKEC3JT/bDEn7290JzPxx5ATkthUwVkZxIQx+WxYdlcn8kO2eBH9INLuRkhQgFt9XBDvnKwDueNfN7l5EkYl1921Yyg+A2leay6pZq+NpaFR9UWNGvymFvawKv9K5qbla3xaVcyvzDNVdSn4hEhacjo6nya+W179t1Pq8/k7kTHgDy+B8zrR8X5rukvZWs4/5SPp6QLVzmuM4ak5m5dqgPeqJ+My9lhzXTGR/XNPWuSqkTVcpmQ8cNXmt7czub6shdrIS0yaaD3+w/6ysZm3/Db7as5GS/uj+su+lcEt/rbW73UZh23J+u53MVqZGfkdW/rsj2yBxs7yj8T2XtQ/l6qlJLx0qpz+GtzConMVPcBPnSpYlGgnwp95SNIkNpyFi0Ehhx8NWqXoo896cmYT2NNecB1I7SdWTJ67559EhOSXVTyxHNJEdHf19XonD6t/dFmcpxZ8YHsnfC8RY/+xEMqTmSVSsUxR4spsohJOaamh7hWQIH/EuX3Wbsj0zud205K1Q5wuO/3Gr72mL4TlFEWDdCPTPbvT409aro/e49ZMBrbpgnMo018+npicxEHCl4v5D9TjaySu8+rZ89u3qWTecyeYpD/FKxVHbV4UjvBHVWSkZmVT/t6qXxbOE6wwJSoTjlX7bIMuovu3tpin+zpv3hnexkVXfYSkvFh4aMtYzAdW3yteOzw/yH/TkL7U9RagLpOETh2LLljbDByx8SRa4CI59cBD3n1m5lxIFOApMvlqtteojJw20IRTH2+nsaizRFK00yHH9kQ/kJzOd6mV0Jw3VwV94X4Lzy8g4KUSE5pDu8GshTFxhIkkKQpe10C7SKpIpX26uTnm2vPiBvgbpNeTvM3N70clBjx0MWZff5fdn32M9P5JqKbOzqyGBK1sp06JzsyVT53MiGG2h2NmvXN/NSJv9aWN1O15LsyUyUPjBV13+fXddzmU66v73htbH9mvHqHh/5rIs6NeF++S2FrCT6NC/t319KuYJBs4TU16W25s9ZICm39vQ4EyS6eoB0EW5W8sgDvSmVp8l9uiqpV02XZCyWal57XlsmdDOvuvaul4GknkY+ht1bifOBRUXSJFUe2qReewUZv9DVnAIBjhhZyVjkQ6oUHWcOHppmvv4KvuovJ+8Kvcrlk8pd8jZHZX+b6J0OWwh0Q6J9jlSkPvwqqeQ9fJzSjq0Hdk6te7X3KI77XK+yexmLv8oYNG1x2srxSiYiiP8Bp5+csrAR8KfUqWdiB+UnG1M+g3ZCB+83JDfH/b9jfgsbir3tA7ln5R+NoSaVUa4am6odAws3C7JJJFw6JoucK6Q/PtHYi789+1V6oMXfYKtCH7SR/A7S02vuX1YkGBd4ocBVSol0XENTgyFNGuyFrE1v3VaJN+yPmSDJNmuGBnZBKwuaWGRCUOq5zB35zcAc41xsDYGRVrONUTun7ergiww3NpbQpvJcnqzCDzlxV4PCKgThWW1H8aQIBDBMguiFCWslULsJVuTA9dT5nnJMmf/UGCQ8eVBIO6u6cQKdZNckqspIkNeMJ7rijRJc4yuM64n5LzkVIgV5TxMikNwDfUvJdrsimJUI+fvbi18zMq82vbTiwYpFkf/815csJB2VQ7suaJ4Wj1oeHzQ9vyublepfuOIDT/bokTT06NGPOaSIL8Edm2y+tckbdg5ZBpJZzvsWmgeMLQUeV5W1Ofsn8gV2hFPGwNffrRw2U431T3I/MIIZFeg3SuhJLG2DDxi3oRsNK+s2g1on5VJLL8olSyGBZZAwWFTp4kvajkswSQTbuRwqB2+YtEkDtehOQmj1AjmcctadVnfjJopD5mzW3ejXblMiZo8MiiBJxgG8d0WjXMtCF975JknZfTLEUUPWumpKRlLoVC20NzJueb7R782uDgwuZIOCTDnhfzfsLdpmX+XOCcOccZAe4k03RvRhg5lJZlP77HrjG9kKwNhsfqcq8Bi9nK+EY8eefwVRxgF+h7PxrfdaDU02yf2wq5X2FJRBNwDshzB/pfAFF9ldNZdPI+zjhu78o+bQ+urgfb6pSt9Z9VU7uy9Q56rjltdb1euy6XMedMIJUb7NHempbvhKVivs2moOOPmUfcoxvbC/7KcvWjW3lDGqtSZqfV2CKeJR9VZG4eaWpG6fh09TWch8Iw/J811vmtOGZqjC7N7UTQ8LUAgE2Am1G2Kxhp1a1H0PfkIZ5LCf6HjLG1t3o2ltX8N+FlptvLG8Kmz9sFViTNB6MFWi2hRtgUbLUU8OJy2lxVj+83r6+UB2ua6UNfc/23Zx3MhZsTa7z5kku2JpOcd+KS+O5/0iJSVDrXJ5pKVcuTczylX/JBP6DiGZnKQ+XB4WB3PVXPOoJAkUCsNsldy0GrAqFz2UDONBb+QTtl2B3sn3wDVr476AUJi9CUmXbUyUribp4Pz7gr9p6rq9l1T8JqkUiVZN35oCFEqHnoy4FTm7vi7kX1LlkpsRf0PqNjEg34NiwFW7XYx3peL5wsT0HARWEy6lWl7Uv1TKXLxVluFS5iuFUBeSKxPjl2q7WPtZqB4lerFKFJu7JXYwqXahfZBuQwA4KrTRo7/4IzZqb+0cSIk/P46FHf3j8OVM2bB/iQWKSxlU7MDjvYEt7dmAD4XzsdgDDQtXdNx8qVe6zeEpFYXsnpS/3j/Bt//mI4K1iLxKcwPpj92jj5KgMiLLVNmTZZh0RxgMIeJyA97ZjNc/+JWqPKSz4KNhiW2qEjBYpA8LSXspxD9hik8HnLBtN765SFFuDim/ontB4FTk5a7qRmX6qkeAOJMcyKFlQUjU9dUrmJ5sLYbc/F0pNMqUsyxq6gIZnC2VYqbSIa7QLeVO5CusO+nDXXW1Lbts1uXI3WNLMTMdtgJKOTkZqUKhclcJpxsW3Zr4EloTN17L3K6heG8XiWND/6F3QmoJOVJUZ+PqGlVIxf71l+3rr9P5euZKm6iuMZ2UKmysQUmutHjQCWHpvQR3n8EuHjb/xRu70FMRlMMg0EGJ/JTq4qUmZVcUX9Ep4N/Wi2VOrwGcSzDbooA262Xze+/eCVSuXGA7XAbfhRy2CrmdGvNu3bCpO2XJUSDU/rC0hZFUlgmJF1pT2UVrivIGNoEgBuDAkxOzpDwlU44zki0qbAtCw285tMVBc6MiS6qkyN8fN2NDdu5lO+zrpfgGk8nyypvB4aG2M/F5vB7q+iyDCumRgs/qU0GtRz2z5Tq46tv5elUddm3fU/TEmsFexQxO/3L5+jtrbDP0nKKomgvklU46ZbpI0U0mKPP+ZgGZgiv61YzExPrka3ArRDQYvtY3Qim+3fP8vz/d/6462/oeTbvc/o+UpwT31bOxrTZKLLSoZ7N5Re+bP+XPhF8pyeANWxoUakzjrFqhnlXkta9X+7f0q5FJuGddGhXq6pvbpNTAEPfv+GSlVflt367/9scjGcFZPkqtej2ruc1vLqPadkaTzA0Miy/YOI2Hbbx11/3ACvl7v7h9zb9r0v4jXvyBr7/73R+aDZOMJ88T4SmqLlh7oC1Nyo8b0yOpmYGdfxP64nYGfjJOyJC7NQi/AlbsKbYJch1R7nNbzZfvpI99AR8udtdsjJBqcjOkZqp3Y+obXmpyCeuNlXwM5hz4nWbqWJxwKJh/uDE42QJmZHKYuoEy3jRRJ56WU4hs0oLHz1JjMKi/uy/zapXUu9CUtBZMqGqMZDU7gJOR31D69ezXSLXVP8Q+xHlM1UFc6NfAtirDtpcvhj382w+S/lzS4wj/86/cpj76acoE8pQk3cWDlVopWcHANm4UDgxmrzWu51W1Kt7gN7sK2g9seDkvIVcmk2i7uLAzZh1ju/g2ucjPVbPecGD6gsRJWpyTQg6Yr02SOqrQR3EsKB0O2yZqnUSJrB4tcGj3Vi6DJySr+oHCHP3cXlSboej4AXmMWat8SwjDVyv41vZ+yhBv+mg5+58olSZunPegztJMCr/REfVPeaghLfGtdjgrHmqGBb6rlV4nV299K0yH1Mu6WDFHuDW51BZhYUM3SPlVMwqbwOqVZHWnZaNtnq+b4+Z9CYOeam7Pyael8ocwErUsOaawSRxP+IThV3Jt3vgbBYVtOZtpV+GOpR2dtX9t1+cUan8USnXZmsjbmvq21Ns+6O+EEvzuR5XeRMm2FcQOMtCqREm4Vv8npebQAQgJofbPbPlkhb/v2i/wSeiNss+1lCoGZRy8HEzetaByi/r+51UvxBHe3pft8QLECWTNXUg65ubwDMeL1SxACpe110V7rf06u5JTYrPZM+9R6/kbfeqVWnrPuO2ACOhyhFLieqCZxR6UL9dXc5p0gOsCJSXxhNTshtpGJe7CB0Q3OZnLzNbDjlJ67tTt9XU9rWXO6Zv20UdDBYGJC12f6eufWRUdub7QUdtoCZ6pJkmYR5ec+Twrl8v5fXGA36xfT2EWfb2W6aOnupnpdi+SHDvFzVzXy0FncR9zqAPTo4mO+9qKWD09/ycvFw59AwskNZD7eFs1UfBx3PNcK0dTV3vlK5Tl2ZabDlzWB/xaPtUaWV+rB/RBj52FyqILSwLLzSMiVcJeEARJBll2pEL/OFFXci8LXt29ZR7HveyorfrTdnUCIWhw/bVVyq3ChWxq8AY8BpmTydQCtSRjqo7del7vSe/66ACis9DUWM7CxbaNkuSn7hjOtX5VcVHSIEpLkMC+gRmo1owbiDqXy/ANREdORtxDJzwx7f8kV0KVq0LHGR+v/Lh/YvVhSYbuScqP0iP0TTgY2iYJCQxGL0IE7RpzWSd/wIewWe/39Ak3XkYFM87YqA80+Thj5+xOBT3O4s1V7HO1bu8piM1usMm/hcL1KpHOugdX4S5i5IVkhW0YWYKoDgSqONxTqEWuR+VanFFvgMYAtehlV5c49CdsrJosAa2BxiteItNFroPwoZtDwklKaRTyw/k7W6Ym+SQPPqgCkmvrtAtlnODGZkh7jWBvEovzcTe2ko3tjXr/ZAva7F0fuqbZm23YLrKFLYR5/IjHk/ITnVuDp3CObbSsHZOszW5Zr0rmbdZ8aSz6ol1V4O3hXafHoZ7GJ7IoNDkck9SeYK6pNCOYW5Hx5zM9lGb2m19ukC9Eytrf+elG7W39cDvaHH65HR2z7/Zgt2QcB/n8HttrpB9lWGfwSbZX1qcNZJjKMyR7FuBy1NBlJMJU7mGwcU2trLYr1E8tonRki3egmkzINUWoRgf2wyyhPjT9eokpI9uZMmTVlZy3K3dVapewP//F3OSELAkHojUnufOOwhJD99yqI0ulKCS0nRZqIrvV15x6CmFu4L1U3YF6ADuDZCQzNCA1JkLK07e0RqHPULq5KwdD2jna0o0xQdbOKWY1t9OEWN/YlMu7Fl0BK/GWNjDKsqzuWqgvP3Ovt52lzG9oGrP5QWX7YiNn13KVuBFsfE618Eazqfto+kJbWt8Yjq3NbrQk07DqglOxHiODKuOYmcE1OPAartgYVj64xoF2V93yWoFqrKqsCY4kVhV3MvBhZNluq0X1rr3jBLi471fVQmjPnaKo5JowLPxTfXOblsb99xR/3ffmCrFZJX99cZG7o8Tj58+efLON5y++2cqL72klNajQhYpTnELIwDupUVsPOeGVU7dwzJ+/sCJ7+Qs5Iuznz5+HBGE/JvlBA8SgypXV5heSv/66EmpYBRsDPV1iscuq/qw2ktflFyzzTZfLYOuwgFTsYIrDMPT4x9dqCDwTtpNE40pWxmfT8VR7dvbT7Um7AyplDUinwBOue3CE20cLNOwVmuwv62pz0PdyEjnqo7RYLuW2zy8dz7dyZv6fVdduzLM9H3CUyFFk2Eys/gaCoS1dSev/wZCAdrVxUX/9VhN//EYTryEH/VYj/2NrI2F4eUw4p4HEtpaYnWv+t1tIZ/OlOm03qRvLytxs8hv07Ln7KnBS0FyF6c/chvmh1z6A55fQmGU7V2Hu7mHwEdjLYw2cxrZPkdXt6zm8rdhqD0dKmPT08fHP/iWZOVwVaQUe2rXK8Ds8m/zL1i/xm56cDvBB/qUEEx7k5/P2bl+prz1A/uIJe7K+cFel3SR+Q88NVKrTkQM+za9hVKxwJf1tqSc26MhhXalaIaMZA7CZvYAVomIyAhCgaTpD9FFz+akZ2qGhyq18ODhJm6zaD3Z61lAIjU+NUJkSHhNAJ4B6AbOlt4RV+blS92i8aSXzCkVe2Gm17kJ3JuqYpo/+ITlCLup9bcoQO7Z+IBUs3KwrI96plCFMiD0QvUBPr+5Jtu2jrBec6o0SQMn4zc9JPz/4dvhT2Mdf90B54HgbnJ3uSSCtKu6AdtlmBx5HLsZotXR5b4ClxhFS4IjQ+T53z5iyUxAavXb/dSkSMWXUzee2utc9SyaPapwwpeTNfd+QrUfmoAr9vlSYX2HP4P6VjmTYOqT+jk8klJ97AK0lzhrsDLrHwXZ650dyABA2bvMbFkl/xzP+jg/0Xd+G40v2wFrT6ZrKtgMCn1k0zBRMUVFacHdNBjb9ykQU0a+snMWuPZhyAVCgsybd9/acwKfk/FtNXFbN7ia2k/6tjZx14+1vR1v5Yxnh/7GLGdvS5PfwZM6JPRtxYv86YsRG0/ghBku+BumizESyWPya6shX09ZbuPpeCcjWtZHCdnEqq98gl983P+zlXTX/Un1zTF/8xjFNW/0tw/pNBvc3jOvGRsOH/MMHVk8jF77z6lFET4lv5tXX+mo+sMQ3g/JryxpYzmgtEyhuqeQy0M06BgOyu+pIEjpoAibS27pKd6etHUWNLc9ihW1PmA26VxwNO5PJXrVlbwVqq5+q0sbsVCNbOiEM++DpHlIySOYLiPLN0UmOn+qoQGmbYhXi102933fVtbDlLrAGVYXYe8nkbFF+rRfrxQ60sJPyaw7EsAdLDRDDBlA/av7ZbwEQg80DLfZGTJfBrMLW7oLuL0cGbhPtWS9RQK3OHPnmm5XSLq5i/dGiMg/HgKfzja6pc9fWjqnByDcq/KZOmQnKN7p0bmg+WzvlUD/frPSbOiaUfYAh9K0ubuvazjK/qSdj9n0Aupz6m9eQ2rG5sfO69cnNw7YX2uxUMCfb2hN7VgqxGNdQwIeUVfZAMX/mwU4gRuNFB8+9vV0senUVv719uVi87PtINGhhUpomSK1KYEpCmyS1xzBUlpO6ETqgiGZEPpDtyFVk0S5DYVroaCZN/e3Fr5O0qcRq/OGGhhbjz6WVlrtrOS/8YoCf+zMa2jBSQeIkwyF6BbPZd3aRAQXLkvwqDohim2A0FNKJimg1naLOI95SjQEpM1Axigu7yOpey8DRVCtn6yYU+xAuQ9UReEhIzVOoaPpMEGG9OFjP6jb/b/nPwv622Vo6hY4sAKxIvPbjk9c5dwapOgeQ+Qpuj+srvb2wi2zaLu9pblYc+lV2V88AA/L+a/ERV/njZf21mj/J5Kwp2cz4iZchZ1XC9bkEVGX9C5S+v1TZzH3Ejuwif2zzbrKAw8hsRmcQWvVxpPlGT/V9JPev7Xq1vqpiviVk9/r3eFZYSn48G3gpJu0+aLWhHihqI+juikM7ErNVGZuRGGjs0OQEEvANexMouw1fRV31gDQA5Y0wjbLRAwuCRs3mp1eDgDJbz/Q2Id2nrsLcCzeceY42kjI0UirlRWOFAbPCtZnK0inDBOmG2jDihAD5ulpcVTM5chWv7QIfqNYlKE8L3u8HIZWNBABTaXvV1VMYz9dQqqaI3dCnMpMWBsweqvANc3dQ5zWTRuWgHz6PCELwQpJzZzB8Dz7bA6Dn0+BvqPzSIHcDS8qsCQc0ZjXwWgwMdG84lvb54RRBZBcYnZf3MnX1hQzCTEFf0BR8vDBnbRKZtylvYK9zMJ9rFVPa8EO5d22Aih2BWft8bHa8LA75p+Px4CHNoVfCG2GQ7YugFr/GwVw6jYM5P0pyhLgFnpI6cRXq1zVUeh33JwGmeqT1okCVNmLalE4l1fK4Ecs72eWimcq8xZamm+JJUmJsUx4qvPtGhW2W1WF7x9Mc8RjI4AutP/EGgfWx7elIHzx5XPC7n0q4kPA8gINsex7SB88bF/zu5xFfRM6Dt6XsLxf4zZYlMF2a4r3+hbtv1QAQGX+IfVfQojSJcHLQCX/4xeH7ZV6ksU4sc+ZBTwYOEnS/2eYeob430TnCzOLixqnIf2YslzCLqKKBDDzJTm6oQP0fjczUJf7aIUZoR4Uzs+I1natLaNbA3piIJafVysU8KH/qGdvRyJtQehtY+m+vbGdNJYbeSdrJ8JyvuFLtxjE04lj4MTOFtd6iod08ye5U6Lebp1tT6DtkU+EX2XopXwPKGr/IgOdcACs6q2EBeUMOqqaRI68zBTa4bAuPnNKy7ZtyqW51sudMgZlN4fgQp1kLfKvo4FSrtk4mhrlRrGd1VSfocF+r57CJUoI3weRTcxBdBIwQw9q/jxCraO2ulAVXQdohr/KnT00ZXQQ2K9UrlpdHanlZncI+XbYEfFP4B3+Zj2VPdQsN+7rPqgngYH1PHR8BgPtFxw7X7KsFW3CbkB5Z6xPDQQkxZDi3zXYHO8pBVw3zejfs4Swe1NSkQf6oviVqkc+QR/C881kWIu/0gPC3Z3s6knu/fzaRlR2yz+mDOc7el/W+WUHDLK2q9+287C5uYUr81pJypuVMzKCM64BCiqSjurzpykWvqSygy3OjBi/lsAVJC4gROOr3OACWy9vC0km+bPEvefiTzG9XD8dHWZCs0ruji5B6fLVuzuWbtO52ktzP8y81JP3ze8J8w6ampBgxBsrB7tGV6k0zs1e2usKMfaJAxTzn5wY1Kjz1mla1xOUPuFMk0A++El33do2IGvB9s7qPyHGTE0Seo6I+2wwgVF7JOUMbs7EKFptctTUcGLXTnxrp9fZBdPtQDsQEZmn1TVPLO5eotiQgUIiIwEGj4fsV5LrCcjem5KROKYQ62TI4OMuT5Z8BwyAMDU/105Cu5+Jv1faRITTPtZz17P3ZmMnzy7RVSiHioIR+Ymp8amgGoZbxSR3Io01k8IyHkudb3ir2yfx7w4slLZkg8TtqpxR9sbMhdfpyzRUnsO2ynCKqtq1X2Iivqrl5oY/efC8qDKH6AE4hWjKMfH/Q5hgoIgM4tAU8qjF4lLvD/bwnXoJX7tPh+iAnS/lY+Ixvy/WNgh5DwszpzO92g3QLjUX1gXscfaMZH7NX99BIqyvkXDUI0WEJUzg+BVsTLOemU10cd1XwR75Zq3hjQX9sxIVr1Psq9qd3iHysg+ty3tfX9zaaIJe2Sra8fXQVH75+FdLjmwf3qW81k7y+r8QKb7bugtv1qP3V7ThixB6HQBbS8BVsNwf57NoVjfZma+6zdSPrtNZB+dTEL5e+tAo9ztYrhFboHXqttfv8cYAMVUVwuRTGScjck51NhFXvTbBjisVElX88HwybtEkNJx3FqXQKVs7+U07SqI1lknbeniFH9sow5Gb1uvdHcwGoS0sDidNV28lBGcd9lPpmM+kq1ypqLmCLePQWNmKfmvjQcn7TdkLwFg912WiM7CbTQJTah/svm9O0ojfk97Wbvkisi7XGZlfebrLdPA1cd0z81IRwKVeVbmIUE5dNQrlUHOvzVwmXVkiU7VLDXjEdGZc6ECQeE9tdcqJQ4daz3NWGGzVXSUdWPLZG/8hVtVxSH6FKKAVnsUR8RzJuOyqdlF997kN7NRmEtDAv3i8I1Pat2oH7hY64FJL11afTteJmA//Odkfd5NVFSc8igQzwaQNqTUe5V5HM9uY6hwTDcHiodOgXMshuJRUVkBj9vSLyat5HoL6rcpZSaVu36d5ijlsNJogsYto9gmjL0hY6rdu4Oc2AnEmRTw2JvFL8oUBRiV2Pd8gXSAOWkeIDPabfrgVtWqWdejJR/6ntbA2dqsLWPWJuHqgYeL1rOclVDD2wCjoIowsA3Rae70sVPiephjkH+TMjYU45B5OV2PGAQYJQhEcLTKeuvlqrVDy58QfHZTAJwmX2+LJeBin0bUSkpO3rUu3dZNABxCV/+YK9xc8JQC+plMPj0ulQaIBGt2GsgsPc6tYCt2qlCQGBj2fDyhAsGHbWvLxJPN2BDRys5vqIH7xpwKCxvYbNXiZWvRRbbPZnQ7PGOJzDZgyOGTm168jMDdFCfvHMaVq5IGvZ7Ivq9hJpC2yWH+jMp+aiVTPAYH2F19jjMcpsQjgxyKB8DBq3m2XPlLfvLzxKRblaMxyh/MmaFrkBsM14OCCymZSZDlmuVKCTQSC4RaDKQeyLxlxmFT4OGzXxFc88B1A5Yng2ah1gZX3B70IxVHjy/lOumtNm0KRabvb0QvxTfthW6kSp57yfYehpQJYvmB0WlDHsL+H+Sf+w/9k2FV3E8l/kKlNCHKSeuvHnjxdPHCHYhAJDjEQXFQRkxKb1gptxX70sQ78yrCMcDg66SvWljiki9+bvhqze3d2Q3icZafo2n6qk9ZEqM3nQZKh82qwTVFCDShBJfVjKjvtGFmvBK43mgIx37Z1l8EozlPa7Xcllu2kN82q9ArMcskJI6HKbicwEajyyheTIS7dT0Y2SkP9RQ6V8MwAeB6srZ7SZWqmrbVQ0uAphT3g3yHpDJ3TNVUNDbDzJAoaMt26igNcgr89kAs0ocTTU7Kd2ryRDH6vz3dYM9jW1G9c4pdeOi5AE97kMOEkRGVYdSsZRgSbj9gI0wsPNmW+LB9IKrdkQAmlKhkTmucWVMU2cjm+wqaWdahcj9IRI5up2BHVkCGZujmCaw6g2Bm8IO9FubW7iBhBOfzFPdHC5U8T7MPi4pr1jzCLXFGnMItcOqTdnkodRC5mms1KQriR0wMuc/cc/ef+u8VDB7P7aUkIR9PSu7WaxyNJSEC9VNve2ew3T0r7QPyH1tF0x5bIl1BrtTzG/Q4F3CKUB64d3zn5vZFnLLACIrOETUguKt4mRxLYC1pIX22zsUP2jGUaJF+mLhEy8yUYBjjuNy/QTqHQoDg+N+yUTF5ql1vnCOa3uhe8I17YjjUCUL6NidWfgnkmMf9ECR+aAGkn2JsbKILBJwxMPc0Z14O1ikn7zGNte/Q/9wD19HD0iffTF2dv4tFG5HY9DlW88AZ76toBiHfrjO/rP7tFwbiZ9qxheHp0dmUV91yB9o9VrQpRaNPcEr+Vb4xcPNhumTZv9/K7h3d3ib+rj+AvsbpafZWSc5vqp2DrbgKGKkrDLgMJ3mUaJKBVncCsKcu9BulpriZGLkMo9NDRuMaYlkwGahqWYlJRKjKq2WTy4IUF6puiVs0oj2+fKgWVgJJSBLcBe5MrTJiYkKdI5rskZv6m/vlMHntSYhazyG561H6sHzQ97qmmTv4zRqkBaT3Jh6T5qM1Wfgl/yjLyvTSSnozjbwzhwk+WHwWTeWHyY8/C4udq/MIAzrJOd6/E7m921ogwu3J6UrLEIbebGM8yjXu67JmEwerMvSEgc/XIxaITlqam0ZfrgaV5fhFG2Er2bh/PuJ3hAH7pXsZZQtMzW79TeqF9pdMEwjybGyu+ELtLa38AukukWgV61S4ddLb+1svEDFA5rcmoFvAehwpthvpvdjJoNdjej5qy19EBC1sM3zsIv1GyyUF1NsnfqRnl8cdZ2x2eHCKmjm2UskUQcgFeMBt/JpUaO0jn1GtBRVCHr7JBZqrLVGOggtDi2BiyOPoBx9JjZHlHTJseRpvj8qJpR/usmze6U2AslGEVWA20wE45gtPEYwtgcmKIKLIyTeAwpaaIBiDHhKtcDbtyAY57AviPYwP9ETRqeURmmJycNCFKV/3R8dPT6VG0sgTv2hjSJJg1ClVTcQ95WidUEcK0pvtLIgnMb1LN993YFu5Xw6fVWDhTL06SFV/fngw9xKfwvHrZRjKcPltNziBc0kB2Y2PiVGdD2r0oEjeHlVdkFOB45yq0AVtvJEMB7r1qtKfRcLxkAjpHmLuwif0zp65Os7af13HxUz+J1TJc3PYvXWbmAtBJ2twd+lV0zvmczvS/e+JWQ77oHva4BW6AO3SEOe84XWSgfeRYu3Tfhct01dFigoI53Fp/M9DAWVjCofgCG38wKNcrPYPTPe/cdwNvL8NwQ7SxcZu2iqXuqrIuzcCk9wya7bihJlc6Fu2whe86sOMGvvMGiXKKPS4OmeY0AibjYR/gRyUeHB2A6ktavF0LX74sL/ctiFWQ9hCUsJcEFQS4EirOANgY/qZFKYTYKpn+isZAkQpjHOBZyjT8Z0Cs5Eu8AY6nouNA3MO2cJsDp+NzrnHpejO5H+S9G+S9G+T+M8n+AXdm78qqaw7KMA6N3+IoJC6KJcW1gWlVw7i3+vaqWHga1z+5uy9XxtaFLfJSb/ePr3ACLEo8f3ewSXCRd5jYH1UJ8wzLYJihRzWKd75TPTBIxkUdWKHCTe8AFKyAcUX3ThCI2P2ZMjWXtrB+K+fE+FIgc2DYL3hDECrNDoxnQqi5KuGhVdu056auMKgUsqVENF62FQJokysIdyW54JJM4xtBUIRE4H+ztM0xsqxJC5J6vGYHBTF4ZPi5ECOk0LxjJwn5b4xP3ijsky2UqDBbqr2Efr46nGta4d3yHWSgkDSIuJFB11EQcHISZhdtLPdB8CNr8zQf04YQNuAaVNlnf0hhSOAmrWCtTOFLIaBVKSb0FDDIWybmmG8qXIcY4csynRjk0IsZtBgQBsBhdOp4PMkPyi1915oeEH35NMMkmw2diXt6BI2LM059KKstaT1SdwsZM2aNtNPUy0GWqNb+a8KsvvBqRjef+MGqNj77njoLW8FxxTsO107axTwfW5RAahjeKN55KPBgbZhbLUeFwv2/I5IqGFaB3DUfWg1bzBSIur6HJ9gpMZSyOF/b7y3azEU0JyGzAC07AckOw6Oqr7Ap18zopdppGkza1kaqCvLbsyGvaqPgyguDVWkre7KdxaOqksS3v+M32Xqcoc8rfLoLlZYDTZecm+XGUfa4SOL/gH0Gtvz75oWcxijfV7G23ikoxH4nxU13WHSSmrnA3eS7F1EJLwc9ue+xFBCreBlFsVp7WPlCZ0lmjgMW5wYcqRLzGQJ9WEGo89PA+4h07pHEfmkRUF8UuMHCv3kbuyUtvJokJP56Nro4s/UOP2yq9t7G503Zbg6dtwuQP27TW4ued5BqPAd9rY1nkV+RsbQTNG0bOeZOtz4ebhY/JuA/qbhGGm8q3qNPwz9P5iF4lxkJCnA5mCw5d384Jedwu42PDW1PSnYYJKPt3CLh4oCELEXMQpAeyr4OmkQ5MqxmUVu8regWbya3thoX9tVTF59bDh10NcmQ2+IWlw/V9toY/ZLiynOOG/Vjelr3jKPLabGPn5xWC+phZLMBScas72nshEAjPgIC5dqWg71cyf5kerjhKcHrxD3LUyfCkZ79W3V98yB8fYYHKbtX/0xOHgzOEwN4aKQ4uL1+fXh6fnb7UFRznZkIvzErGW57kR21+enbpFuBlc89zW+4IvneG8zFTkQ31+Hl1fS397P8pm0m37fHxm49fRpU92LGV5kGvc6gIcgmfq+fG15i5TRmwiIIURHrRb4HFVHgiN1xzUYRslcuuhWM21SXv9Vpda9McmxX9waqoICjdkjfTPBk5SvxU2NebzjHC9SksncziE2FXFFbpshWGDittAeFrp9GsVkjMrtcdbIsRBqCc9wfyYsIBvdFEYv5Dt1IyORP+fn7PiUh0RNyZeNTirVOWaNHVFJ1WE901bBAkRWfqwcqmdrkyZwVJMZR7SbJRUF5Acnz8jDmLJaACSfI5hpJrESY9sqSebdobPZ/O2xvnY5jm2lQmU0127sVNa9aFapqhUK/VLLSXp9iBeWfZStWwbcFawXBizdFMWukNMnYYxBGkE9uWhnOlE8GcgQHsIiNW7UEze9vOrz8sVfQbnOHy/VxdSE1OkxMlR0WX+1E4KaMn1M0sZBWL0KSWbP3Im9tsnWL9zcazZbksYzX9tjgjqv3KPih6t63eFUzVH6ooD4dijRbdwQS0GkzCB5r/ZT1fHzeQPD3wCNVJ14bv/N1t45XVwnNboxB3aqWN191d6ftednvT4VV3N99HGKtvvPP2R3ByYEr4Izg93IR1P1Eh0aaaZCT72gkRQse+1LN12LCk9K1M/eGD9Yhwsb7q4UwO9OLBJmd1Vi3wNJv7zV7r+8XJ7XZmstXw9CXDv67Q5wcnOLQf2wX71765ayANb23HaFB98j0N5QG6jPSPmwQw18iiaq3sRlb7judw9dYWulGORU1/R9gyYVyaslkx6vJcWIgHXtoGzpkGSWm27Rrbmv5JKO/8NzV9vX2bGTf+/W1uVN0xUOPpYbsJPOnCdJcN9Att9b5n2DjfS2epdo1a2uK3RmujxR2D5W1+d1PZVVd+aWPxAUfiyk6W8cEblCADktGKoquq2AxvtxFHFL1LnncJENytxc6apL2zrXRHS3nHzrdRGeyyqpemZVBBkQz3X9U9P42OprKL31FPSV8tNTB+XDfwnyDfaeb5TwJgO2CvefA3BGzE93mZWzbdaDXjHZgz3KY+nha/FzpXq/Ghm5veQ2MGmskr4QFS/9tEEqQ4BobFy0beVqtBbHGiohRvX19uVssp+5cvqZGw+ZAQokgR++hfCvMsbfyU9pDn1WrdNVU0iaQHoiU6yJVJVEwCxRtV7XxXVzQwq54S2ImID1152zG09ULBv4JS2wVfWFidsEcXxJRWcbWnqZ7SfcYA3tXQLhcH+npm3HvovuqWNgJBO1Z/wEXYjAU9aCIqRIGloXh5IUzpUCO6GRw4RAnJZlUMf3FUee25Wdab16Mrl1JPyIwSN8OWIeYP/W/lmR4wUea/sJAOhEPdESzKNLYozWTflTf/m824yv0crQQ/LCU49OZVRAOVqsg6kYeGuHKMY+RAPNZuiH2aud3dn9eqqnKLvfy/NAGKDcK1vKtuFK2FNtZHVHTlanph0upBhHKVAKdgYd2GXWYUMg+inU/lrovO3oe4zcN91o3FBuvmoCejI9sVjLIkIwOhLo5gfYuRCZIDHu+CI7fUZGHa+Rf8JfE7oKOk0j11mnRkAvSlYTgsBCjBcdQwDCzDFyCyRnXMGX1bJXdnZy104IQBal5VzfQWRhIe3IUd0rw8ZOaWm/WxU2kvouW/HCXX81kASioOR66WfZBXJxBJwd9RthI8nw7K/WkFh+tXla1OYjmnHq4awddajc4Z7lw+Cfbd5n5j7bwYxVffdMVJvYMWagdFQ11p0jYbNa9GB3v00E31LlaMYEyrffN8MgeZgcPR2JU9NGtvd5r4SL2frra0usOLavCY8BCM7QJ+PzLA8hJnHSQO74Nz2kFX0eC6CEkyeav5LB1TBnyfKPzBx1vZCiwsAhfgZave2xb6CK6A2CpQLMFAKGczePE31Z3DmZq5DpEYJu5JDqZww5ccTSS+4VAoYgIcxhVEqKXobRLW4tuQFFbkPyWPSh6QKd8ptJvPCA6EJeVIDPQVSiR5rg1ld83QhcVJYYbLeNv6bZMVO1ii080V/Y2lrGt0sIA1KeNG9eq+UNbm6j4LLk7y7cr5lFYPRbiKHlCZ7P7dvS1+fmVHzCo+tp16DWlsePAmXJQyEx05Guoxj27dtH9GU2k8ZbYdYweAxKZR0EE1R3HRiRXIvfY/PiwZxEf/pBlE+HIGgGZy5Cpff11Wsxh72AojMM7xTOPjHM9eplkpzlL/UeWnO3C0LByLoRG6FZrzHhgZauN+/NScRR2uOtAvKQm9Mj8wNsGAhuY+R6kp/Qzv78r7Sdo/B0D6jV0LEEn/2N7oFFGzCht892HG8JMTlv7Q7WVCBak+19vc0thruE7x8KC1H0vVJ4ChkMp6CLCWf7CW//nXLY2Qt+Wv2ShtRpwM9qXtDaEN6AhX9/VVTQsyU34lESkHCi73T9wRofI3NB7co7c/YHu88QQP54oqdNRreQQdh8AG+6otMh7hjsjkARfDnAGD45Y5tMXQVfljIjyoA3jfrxe0UtmLOC5RvOQTrEnixj75jojn4e2cNJl3fRKjOIhr2Fq/Myh6MChF/Wm5XHGX0tCl7RX2KsURqAOqb78ZOt0bOXFvfRhuskfKhjwUT33QgUXaQHgbDhjD6qmoHr7EUIBwd1HbyJDozbnOjUphhu5J3Uyji/6q+rrSjV0heKiXtpXtKC6zpHyMs/apwSms1SqQcGnswpn65LhxmGppgqkYshxFVbPOA9aq3KmJmWaYuRleHvdxxURwU4MZVQeoEGI5yBRkcjuCxDgs31ID3Zkk1aw2PzWPwdgEs+Bkoj4J/fBXeag/Q1xc61howd/4oRYCUOzWFnRoHqqfguB6bWpGdQP3XUzp4Gkbt3YqgcOOT+WfbcRNe7gWlnhBbqHqLmh0finTRxu5JO4Hs3I1SEdtcohwzJTeNT31uG+V69jowsyLoAs3xpKMeuDPHtf1BwPxeVTFRCBCdIU9El5FjhLqHwdSPO4C1mDnhQcNkSs5bVc/Q8jzqpqWa5pfGrciTHUPeOw9WTbMSvivlPOygGV6ejwKRcgDMfrugFd2+w/MRT6H1of47FdlV/ykYFO84TZzMT4Jm51Xv3EeNouyTbBttSjbgrYN+bahAYF8QVrgEauLOS1euY8uhJenlb1BCHBj/7R+9uzq2acGOzQt9G/LHtvzIg8xsHeFl1Wgu29Hl6VZVf/jg8083IoFWn3264981wLOClmbBt/c1Xhq1vzNjvZb7J53dju2/HDDQz/NH90aP+mXG+Qn+m5z7d1LKPpe6om+54gIq5JK/OMjWn8mfaIBaGJPmvrFQEKz3TcmecQgVBvhSFMjceHUcHZKNbKlljHN7NAPAkZ18vaTrJ4V0lcWVcWsIZ1qJRoK0GP10C5CitPTAIgj+5tqcbn5DyP5pZFJPzW+UsnOTLnR58+ur67/+Oxfns+q8vmLPwhd+OfyD3+cVdUf/jD74/Ppv/7rrLr+5/KPL6qJnabWCIFQkBTkeiMnS2mYKQq3qHpsOWYi3rNSHpOHa44rvl10B0cIxHnTg7yrwV3+RoswQi75Ud5OrSDTYPfdTB80WRPUvzs1FoRl59A+ELGnk/t+6C4+HeRNPIIjSS7tZ9PGPYSj5w5rJzyffpxJVvokila4Nq+G5reGUqERlt7Ay5PWzi5G0nhI5LfN8NlM5zcqmBH9ZgULfnwis/28mtZL2sHbiVnYvJrKUE3OZvJy4A/TVz9i2sbQNth0mmpg6Fx1J7DiHpg5qwcJpCauJivUmCTqzawN4lt4ZQrVA3pfwOyDSHMVbE8h28SsCQlwhTgfPm34MhdVDAgx+ITjTm43RB1WgdfVlxJ2sa6LNlOqA8uQCT7SUoc66REyFDfrYkgo7rOu61/8UT3Yzs8vXvzRXRHVcv1j281nh+ul+ZeaOTtTc0l2j9ZqNVok/qjhmwQ9fMGQxwYp4Ik/VfMl2CvF1eGpJpSKFpUgebnhIz96ZF8m2A4/ekQZmz+ekEU+1sBl83ggaFMo52hEQw/ghRdaUdU3tXdneiYhLbzSXZ8K/mtCfow+wsTWBEf/72ladxCPTx7AhWdGA4cKedraTdRKCbYTtLZSW4pgStHno/zNIQ9efO43GOqGUcei1O5pRx8aYyF4UPPr9GGo04vgZIgBeF+ubuUdIC0JcyamAVDYLv+e8aM34W3aIPf+CfSy2iNEsNQOyfFeTglMfIVLS54zMpmma5Qyy4iGgRFQXWO0cB6OqEO/Y3lspxHf10oYEfrR1Y4FqS5deqS1Eyown8CRbH1aELI4ctC6GRpJJ0gb+/1SaLiM87AJuLWozZspRFapP7Z5XcCpNcAv4DQ6qDQIlhTgySEjDw0FBAqc7RhPjab/xhn6t07K0z4YVwqFYa+yNJzOhtVdxZnMZeJAAfNJktU40ev1CmQUdL2QBd6VSZAyBL+j1Ic2oX6SiTX1q/CZePicywljnyiHoxQkgBKxgceMMUxaqMz/Ew67qxAxiP1wFNMBAdUYgX8MxzMT8hGdfWCjh3M/v2eYrdE/KIdZD0/9+o3LrTXDDP2OFgzZ8LCdJ+6VU9yZ9cANQjMfKHJkXxzzNvf7ITL48bDsCB4c423VDYgyxsQe1CSrN3yQxcJer27brv5FWjsgs37ZajHAiHhU7FgGjyGyR+utAVwEj+RpcXgosGrVbEvLw6NBKLm7fXA885kc4h9s9hCFiEQV2gwt/ClATjJFPS66CH3C8XirnmE2eNZGCI6i43GjZcLIp6ViAPfwCdixZPxxv+vUOBAAfPNAmkoFfhwCmWtzwzkwhjC3loYTiEspDHr6Pbc1eTG0uVUrr4carMBU42gjX9LPNtU+ErGgcJJTX5dUSZkhzNv8QLOl9CIpnfnnKxzUJtOj76VwZhfhEPwd4/2bhvtljKqyqAa2CYdUsL6qFHR9lkBGoeAQL+p4BTGvHeLU9n6W48A1ydQ457IV0nda3cUNSY2MfJcY7iqOP1w2QpeXy6qJ0lD6DCX7CTpkIAI4PldxT3EjaCwNeJuDQYLMp0RsFIgAkk3JPHg0djycpgHpawByJPT1zEK+3hB5EbWeRFBO8l5oA8MOFFV9yGTXxxpOvQc/12gK3gDacUi+HNHXZ+aPRpY/BnXTpsJRW5UJZl2Rc7UcJ61Dwc/0otLp8uiRlgIfX14pb2L9cf/TgIJM+QMxUOK52N7BBfoRotRtrogBIiW14M7uki5oZqBs9HJWelB7ToyOlVZ3UHow7vFhyShdDpwZow2GoYZCfFC78tC3aSBhGO6WbVgragmUOVW9oh3eVm7zFPPUcqnfAne0A6wnu6rK1QAPHZ1WYWewiRponvpEc+dIj9RohJaKV3KV85ICWeibeRKkSr1XCzhX5JTz3jEsE/RHx0L3UYnnlSjW07Ak7Z1ZlamQN5iYjdTrwdpr3US581DfvmcHLiwdpzjV5Gbi/JUpEqxHFM3XCq62TZoVHojcPn1mONWtKHtSRvEKA21iLkxjlj0KiHfvlcV3OVrMyD0nGjmEskEtEgsFFYh85cQCQnqT+QuF6m8tIdbuKnLEp7C8LYT2WlhwTc2JNAlhftkTmvYNgmbDj0lOEMLfWgxRw7plRO3ewyhPQ6n5PR1n1QVFUeqUQ7Ck3NI2S/XbivXZrJI9B6geaWmqL5jBGeaVnGfW4/O4gjosDB6AsiqVfnWf7pcml766H4RT6cb7qiGkmHlNapVraCmDEDWMr6CixyhNzgcZqmMm8iBwl2wQHF5FBWOWZ5IxzyppHiFsh3Du1SyB0ALL5CNw6ACOYD9YcABcRt7Jh0V93E2eqF7xJkPM31GfDigMlQYSECO3a5CnFE0KMwzxSXqK6f8DKA4qoTdGUlPw6ZUYYyg1zYYVyakIfyy4Zz7tsNS4s3iP9wAxdXPPcbjBekaLPYBLboYdlH09YmkHQw5G3DPxbTNAjEEAviDEbQYKFFox0LbBAQyG50A9/Pk5T20tG+jLPMKYWgLKA7kUL1s8i/iEd27sG9gaNQZkDzTYPZ1ml/epHcSqdWSL8/buWGXp2qh95nCkN0yqjsA9kdbtJUFkNpqfmLz4eOZC4uMjS6KxrCXSXnYBHk5YQA16yBu0OMiL6dYK7smFWEt+n5Wrk5bm0IqveavabGwAJ6UsiTNhF4uDlUZGA2PmJ2z4iGsbkIdphFW6fy5KLhHgqOQtmM3HvJR9oFo9GWK4NseragEn4KX0eIQS0uQ1QqlD3Y+AQ8Cu156HV7JXUfMDxrFR2wNiqtPhJrxKcYhb7Y69QFIWol3Jt5lF4FHOrssgV1GBB9rotZH4vkouZeTCsAnD+q6OoKXFZZBQ2LilgwXudl4H8NJatgXEETNWIMR+cjNAT8goHwkwJ9SFBmwH7VJhcmthDA7APhNJw7THyrRbZG51MpNpSWE55uzAyhrEteBvwl6mTGWSHLRtDSVMWwV2s67sjenI50TdJY8zCICDhd301ylTe7SpTwm2/9v0Kv0D9YKp0aCT7J0Z0R28e+fzbajccuuoTt1Ngo6KZ30a8KhwQC14NPl9VyOkwr1nLe3eCDb3YleOkGrHlF2b4ElN+DyKTeE6nrot2CwbBIrshRI/+/UJYQ20aqpg7tKmJ7m1arZ/P45xl+mbratkE9mHmQrMm63uWmxegxov9mFjvm8FtVzEYEj3hojWMIgHG7v9Xr5iygzg3lHpopOHk+VkhJyOp4EiXcbu00adkGx+8QamNqobMtd6UxoTPfu0Hc6IoFIeIEwQTTveb28uffEdbRrbpYYUQ5RvTXNDgJdZKgp6/XVaLXlYcyMZcrlFKlzKH1csJQNCinkd5xF55CcRA3HTFSeiIW44WfVpTzTl7DrY6tCUY7NOfpDElNCoxOVmuPe7YaT3jRjvW8LbvoEj6Y4Yt+plOtGofIjGxweru/txc1TeMxJfCJkGU+j7/oHS/0tm3P9TyFGrlWP13UMFGyupUE9E7oIALMYdgBxevizkXF7g8g7mh92wDFT7mu7F3shIXbXt541ynpH9Xl0PVPB+obVkr+hoDfY4zTylhYmlSMI63s2oB8Plkz9lW1sqDHQc9uLy+L+9+HXrkxGf+u97cEhad/NRJ7zR4vjRo1lOXQOju2IoNro1yQ03IQdE/6cGWPzeU50oO7qqmYO+ale21trWF5uH1AJbJ1SKvvX5Gx8pdmD80MFXSIFsD5pyfi+bcj/Asg2pChBJwJgVVurA+GN5265aQnbjnNwmCLNK71i3YIDqTMsW7/kn0/IFw1+D7sLu7Bi/et3rjZXTYoTZYPKH83fqV8uy9NB8rJcdHGI7M0N+/j++Pn+x/PpEm5RS1ioqWMtMZOua2C+BVtMdhwoXmpAnFU1jEJ3KwOR54OBgcCPbLaW4LAs41i8g3xBIC59/oHc5bsHtD8mUygvMzWODUGmuqc3hIgOWklEiyNYySoRyiBpyYv6xupLjHAfNk3JLY0xtFoZ5NkqQMoGrR4KO7O1qtXz59CkEv0SAmfTl0swnJjKPntg6kxSZOAA3Sx5oqTmSNx/qTGt4cIQoC2XetTdt+Oi4iZ+C/O17UsruczjkkMn1OzK4uKr8MO2BZmWJmMrBcN4v5iUQX4KRCreRoHeYaqG811J89CDqNFns2Ti+tsl5hvo7DbvUVeViXm2N8KsW/V4iCfUbvBo8D+M1d+TAYbWeMsW5QQcOalELifU+J6J2qABRtMLxecQoDZRHY1QF1pSZPXhmH5Lj40KSBkYO8lWF91kshR71Qlg/NTRsqHtTpNzABMQg09TK22TWb99f7OURctRr2OG8vibne0ctH2BlsGF0BlaksSxdUiutRHxS0jBto/dGZtVUaIG6rAFaVw4j665JPAZWaRw18wBCwF7iT+/lz/eBvuox5GInwwPQv353BymGoNYDj+9VGyItKqSvTbHzdGIlGSHA0WgKjvwnbksNKyRfHA4fXX7TCrfQGGmpzNZnIzpJiLzts/5saR53/fuqU+g9OQCZb3AU+behGEOfKbacisflpKghSiqNuGOxeTiv1jHPohEZDnNqVXFuPsojk4zgu7y1cGpEYGhYZhvUj4tuHKeS6CGu6SKRlzfrwbfDeIJtjiIdQyenwjHX3Mgo+7gHsbix+vhAJhU3AAiNZMOvbEFdeJKF7o9OyPrhpiFLTQ621YHPCvntrdWmmkt5G3UvCFFCWZvZmzx6JPP30aNs3VdhXIW0I5jZtKKdbapP9Awz6Yw47RYpBQc6HgVduMfjCWGoAGQPyy8V99kpMjHtiA1ScBcBWvrgDfxnQBbavOwv8NJxYSjCo81FzACcWBngpv7F49vQ1dTmvDXDXJ/i9EpOpjQWzdX9qrJQOZvLpBjHiYpLw42Izjr32R/GYbB4gEF24czGRhgGDxFobrkgqpjhxOwwm+Y/RQf+7VEcrFKgEd/ZOUbneGze6CUxv544gqfuNn6prBd2AqFv4NABbYFzrLJ5kVaprn4j0kJuOnzNMN3+ACM8WqufqWBGa2Q37WX7eiA9e9vSPIrMRwAKRjEvcQA3s4FNgFZxpR25FUxCIGNATKfk2cBrUi1I016sp7d63oWHzhpiNx5sEzIb+ZFKBnZ8zCarmUJT8OjSpxyKGrVtwlfAIXPjHD58mnZt/AirvuNB5maBxh0mEcHJLx3Yj5bMvs8jJ7tiqI/NQq8IattqIWyG6m1O1TUvY+K7EVx+UFKDHbbzip4sDksl9DLedtRgNrYY3ns+9xydfsMCYckp2marWIOcwMqSsThcdwbt0ZcnaS8U2GiP+ONfkidzrxk0hrEf9D4W2eyenhOSBk/KhhRMLr1BTcr1XDYuFD1NHTm0XM/qlnOczebTeb2Eb8l9M73t2gZ0UEkv5c3zucmZ53NOiZ+DF6wFPogJGdVHhBkOiiUqm0IIQws/uBm8zA9U4Sw1y+jBetYU7smaueMXgzK5y5gGZjL1hZ2wCzsu+Il7qDf5c3co8zPFnzTbTPB40ZEygJ0cLJcK20wt/2yWKjDsuar8v3Kv+NriivsLPlVu0ixC1PaSQKyNRljFYO+5DYKcSQDgEETZfz4/ZFyL/2K3C721kQ5uFCSKNtTRj4JnHqyhpPRmyUGhcDTTIuFAHgr8bMdsK/DFz9qhwMXwyG3l7CC+pUEQk6RTVOJ5l/oE6GjofRehKuRwBwsyO+WQmJkM2AT6bv5KNYa62jbV6q4VxiXBYeXow6rhRmjhJFNpbfR7KA09AvbeAdjsUIEk8BRio1IETZ6v01s9J4JJkyPKlHgVmLxayE6J0zTXAnoo2BPMlnQhk8eDTWEe7/3ke8FAdNIds9Xx825Id8R0FTRXs1f3hV8CUwLaaEOBpzpar4cijCCC99gCIylGqrKwbo20g1vjDdh244L+dFMei0lSNRaMXYBjNjJSt9SRcbrCoFqeg6HabWgpxg/wRmL0ANR41ckxr+rerOdzOWwAH0cbutJ0bJzXIY8+CFlPkCzdGDHFhxgZCqGlodm+mOZ6iJkBoqB6x/MQlo1suGkXo+8zrfL6J0Pb0LfVynwzxxahN8L/q0V2lEH85a8D26Ov+/f7Hlvj671nIdWtkmC8RLZRMTh4maAJJfBBFkTpSJ0sfKBhUgdQZIurYHGV3BMjmM65rb/D54MmNK0pUFXongCTuM2V8jB+yCOhSYdGH3Za3emcHI+P9pdHvOBzqiLb6bCidVruTZ/RtDRPsMgM7JJZB6kNiZz7UltbC1UQg0ukdrZTi2MQQkyMLH9VrUTeFIEw3bBl0/zXUNZVdADO49oxF6SI4XpQd8qZ+BSWTxVA8o7BD6zKrzZ4R9GIkYg+ph7itm9PTe0cyyQbRyPFNnia//z+vYf9jVDFOrpVhPwAqZIp3+JwAmUYrHt4o344ZuIva0xlp0D2XlQ/1Te3lDi81tgMMhMG/gVTRbXz8MVQdzd6bivpoOMq9OfPrMhe/qIBwOO/hnuZSpP8oLmXHTtE2/ZJ9/rrSvYM6an5nBnImWzgQiQ+VwySfV1+we6zEULaY39VapohbwcWuKfTCGUOs7ZSXv1KpsVnMzyAFcpt8I6z7hDrTRuQTnnw74mRE5pBBnoyMOBhynFjoQeNvs0qUMnfWHcgH0xNLweGE0ryzOzSTI0fDPQWUCAmsuQUKsxA7tY3cuiXVXZ2eDHBpknv2qacH8xvoJu/XVCSUVvqQGQh1MXKcB+ESNmi2dluiJTck2IZM6gaFPI0MwWAonkWGjNrgGtGT0p7AOFMWjTKZ8p+o+DFeklP6KS0pQzfKlixqb+znvetX5ttWN/6kDF8bgChJ1Ee9PXSv50/WD4u0S7tUynCSZuG9A2f2+ZB8BoKdUDzo2lpUjRaIFMnoo7J0vVxr9wrWbO2jLlufGxg21tpfefWH65uYwayOKwdM4YysqSyd14aGX6Q0Qvs+DBpU74xnF3hKJ9M440doWWJ72zIbEST9hL1xkabwa1gMBejtoMuHsVreoIMTBGb18wZ2iA2uZaE2+uXejWAcThmyiA2nRZydmBL4W3IDhmCg8/nSWkzEdYOETZ2FIYg+Ch6/2RdMnAFrh0f9kKT8hOF5WfioFzc64fbPr00tN3e2GLyFrLH8zXcXZtcsarYsJPGvKAbjarRbD3AXFMqYNpIW/pBcqmQXdiOtVLST62eIPBzl9ZUa41btVYMDU0+NceN0AE1QNET3DI9kLl3SJenOz3PQPXUgYukf/PI2WND09fQx1cwQK78ZdZLDwtQm91+slMBn3RmgguilB7PRkXA5OM8aoUSMSVycmR5fMcAmqPAl0F+FZFyHF5KRbUKI2PoMjnvbN6+Wrf3l6rr9vmKpNzSsrY7WWNTnevM7AvA9FqKzxcZsmWpcFBX9zxWlmYpHWW7l91aWPEbOabEbRpRZ00UMBI9z7TkQ60Uh2dvg4PGsYM7v3Uf6uPmhGCV0Wnj8d3TPDpWP4nOHYaMtNlG6s8RdXUOpBTcTyHmNm2FSnNl6D41MbzsNAmlrhzdHrR4XtU0P442iWmbZLJT1TCGw95Qexj9YGs1THVAQd+LA+gunMW0QekgjWI/D/1SplSq0uwCRxAV45vuCIftWX1NVYy+TD+x6JD/oIFMTpa/eSQ7lQwPhzJ/w3it//8cOYPPdaBcSscpjlLQTS53ruKDfqoEvOBqzx+HhCdaAKeNYYmY8iTr2271Sg7+sEG8us9mZX9LaYZhLx75fcw5JZLTK9mh3AjHylKELukuwE/DQO+u7CrdyzZE2KLsYeQRxlvT10PMlK+bGmcfIWjrrooqUJa70k6orEPhBtX0n2K1KnzJi4P34WAULJGS3lpfw5vFHJCagPtYXH682MyKYVEvPx7tqPlBXqL4jLC/O2uziFC8WASiGxpIY5xismnNmGOqt62ZJwrDX8BaN8TzHb4ztR+MLH1ZLjF6BNWlFEKjNAUo7lDtwiJyBpQnYwYSnKdYVk9qMt591dvk0YOdJyYTxjxQNadOY44HB9TS8uKMSyfc4GnjOXva5sMn75i12xsJc5fGavS0TVuDdtPkJZzDtICPZCyC3a8wO4GoXyucKuZjUM8H5fy4O8RWcyS0/tU9AdZ2vB/jjMTCD73lg836G4N0uq+b6w768KCAz4b0rhrBVdumDidiiyJgHjKAv6q+o0dvK9iIdfff97K/5V03Wv4/+747xz9dJONPKW3SReKBVuKL7W4I7Xip2BYCYB+saMuM01aBe2xYGiYHB6qEdFXlosBPTDoauMgK1zXwkmUHcOhL+g6qeNz8O/T8NBuApUFKH0N/g1wnL5KXGLWmJQ6ucJhXk68i77d17/yQAuMiT/vXTdUbdtjo+7pBLOkil4v8dTrwoZHzwxxXGc/o6an3MVNk1w1H50FuTH5CLfSHJbee/Q/L/HEfTO+UL2mXOLgsyuWT4CEZ3SLdlkITkrgExiJZVLtgRYoD52G5xIw/u44g5gfQ6eGMN5NK9XUNMOZHjxaqntfi6EQEQOdsnznGDNiiUNO1ZsCyMG0UDoEdvFAq+pJj6zsVPuS2ONdERQFXht7eGoeCBmUm2RS66MtWkZA/6mjJdvV06jrqFXOCqA9jty+sE2gtm8ANHL8sas5mg8GQsR82zE8BEzciaQYw51BalTOY8ZdtWDqXXU2vb9vppJm4hug5s3iSgH8Gq5X+J9nLX0n/NdifunpB2rFvmOzTUDJxdFbAndnWBhFmQAEOj+py3t44iVPdvsWko4xer0z8ac9o2juzPVNPKw3PO+C/YxQiDdnaEJE0L4Mehb7XcpLRJ6rfHGARWkwsmv7m47e8U4Ay047SkgwO5WuZXPNxF1N/bz1yTwzPVaZyMD7pwoEAscSC8H/lqKBqWU7MBEjMNThA4pIRckZl1Bdjo+C9mlBuNKP+JTFvozoheFfRIHCzCcu0pjaKbrSYllfj5802NTnN3WhGdaTJKWfLEKU+VQObgMNoDfAyH3kK7BrGwEi6odg1Ld3eVTc9I73Ewk//9twv4aXZ0DkT6IcGjGIAKRmj/whRO9G/mcyE4qC5z9bd3H3y+0Ko1L7aephKY+lZQ12ylAuu/WM18c42aE2AADdmxo6riOPTl8sL6Mj7s6bgWYU3sDlRmfOF5xcaEz6PhTIcDmBX+Y6BQdq7TLlkqm0L8qe/VIbvXH1dBQVKjQA+wlt/XQ3w1mvTWGs2kTBChhpkvZ+XDUPP6l9LPcReOh9EnrXiW2LVxif4GeUihGhLZsJi0Bkrap5N/g58Y0Av9MCwkfnBSyhFez9bW/RUPWF7/NQ4yz62FDPzXYX66FANRXLQJJ0RoutNCYtDE81RwQT/KPzVnK0VndUg0iguRvUDKxIKyNg10I+ewMhoSmYMmhB5c0X0PB9knzXVvqZnbTd9X3UER8Nz1h2U/4OywrIkJXIWoYZNX/CgCczSsCIy99tmH3+f+o0X3dGzVP8ZO/ky516Zl9Ou7c1YHwwiVI5/+iS7wD4nJIagj2EEsPUiHHwQxvT5YwUxf+po6k+ouhECcLNvrHgQ3Dx2vPQn3xyiQae3jdbL/KJd3hIPhMyL0JiZXKohVk9dsIopAHY3M3/20j9t8kol1NFPVTXNQAiUNXXVvqnrIfrrHz1SEJkQ9wZvKsOUxlfo0/ZhyJX/rkYsLQul8Tua38ZQCAocYF4PAHL0yL0cBUgek8pUoQEaKNYvdSs1o+yhNlVb7aKuLbafflju/d8x7QbG9ckMzJ/m4zmIaQX1qjVPGXTsMx4lo+bAVEmOlFOXcnqRI5bJvTK411zRPuxys3YU/cfG4Jj66fnk2RPgmkM5cYVNVznkJn4UDJNFtKGJd1OuaWcjJG5e7UndpQ64CzgtXAZx06nA2Lokqq9LC6zSdTXwmnFeQxB0WVw4zzzqA6vmC0Ee5RPNs2SSPeqDDffPJ2+f/nxyOEm+jVK2QhhmJ3Kr7WQtnNRkyZzaG55AMXLZHiW0bdxyqq8P31cH/0+DYYcfhOocao8pyLOHfai6GX9wTDv4Dv3WDo97FAj0931QmWdBttwrltqurocGg/mlMQ6kJfYA+nHazEoF5enY6HM+NY+TuTyc5GFmu1obb7aKY/3E1/KuQYzpdOYwuX0aY+WS+KVbi22JyKJ0MXURi5syPAadrqa0rg5BBYMxRyA9j32LeLKpXdAAK4OeHawsekw1O9D1c5CINd+UiMfarw5uq3IWzVLQxIQT9P+WxcCl9g9qeucArfLKG0sX+shaxXHybFXL17+2x+QlnpMSapmfVQick2xHkQRwLUX6oHCoCLqYP46f9wlmftgacc5NaA9aYAi09VWPWKDaUE+cbOiSr1vz5aQrZ5IcnJmDb6mpRvZyi1TbC8E8bVdQzr6CNGYvt5buqhJ7z0s9zfx5LYdOw0pWTXWS4saraSE3XB0W47OHxRRfelAMh7BBIZq7pUXkIAB9cFrovSYNigXwsT6ijhFHgsYchlPi1zQfcAnmWXPCeERRVk2nG3pZb2hq8L01fJFsL9EYVC3L+88QOn2BD1rb/JjdtKv2SBPNV2VURErA2QKZcMZws/2/BLt9O/jN5+Wyr/4CP1FeMTn4V719f6FAGQgQSmC/zCaUft7C15tWIDAhyysYISsoFQQJ7zm9YLGCm0x+564nj4fNvd8/m1A3Ms+uHiogmTLUHuly4LHuaOMjR/Wm5WlkULQhzluUUatV37YnyiMzQAOlBskATxq5/9wBgCozzc7BTVuYoicvb9rsFjyEJ94y6Y4Wh4UaHmbuHCvnuLopjtxe2bWmiozhARENz6QfK3+G51+YXyfHd2LuM4wt/TDVHTN8b/Vyhq9W8XtrXvkeF0677CAKq91wgKoveS8NHOPOxQza8K6KQ2TuwsRrv4WfLHW86gHCY0K0e2xGPq7LQTCUkSCi2C6AaNo3g7EbfHwfVmtfJokSjeA4phQjuIwBvfINMa4V5RFxpXMFvQ5nVwSsTIocxmQvSWjbggi4BHIMBiB5aeYfenQ+d1P+ktricIyOtvyWkS0xWziknDc5LjPFPFP/Kj3bmofVlAj47xX9VW2HDBQ/TQNonoeBh/ROhhlwZIcKW2tSxgCmyhBObjjeHFVT2cjm6uUrR2m75fKJ5vI/Y4S5ghIzeY4zgbhGBVVQOS65dPysgJulmBMb0jBNTnM3ZFzqTqUIFhv1NTnN3ajPPBPaQW58WX6usJBNjKEZBtaOrHw5jzLAYQVtO0dAdhWHQh+ZVsI01WABRRI4QKVMvwQJUyKBDs4MaURRT8zIBhsptUkAwRxSfafPQ7pOt43SOvU2S3dJwNGwT+tMXJVXV9Vso6VLJm97ME7lmw/mWX2zdBpMlW5hQiT49uGWluNt8xECyIK/lnIwHkeiaXqUzk24TVBM4ftJ0rrVdL3SDZkQnCFIZzBvn6dgnG6Mvw0Za4CLRWxkRUVWJ4EDc5843wiT7RDJjo1s3gtRB+AkJKnzYwLiqLCNFhNHk0eQjmnH+oV8S5n8TRXBRENSgiQ6r29utwi7LX2Qv7G2NDPDmv/zEDu2AHXIx8CxY4RZFNqGL6vpauBjVhrRpEeNfdwVPDXtScsnFj6jCqmlD2vQ9bIZP0BTN9vX9I3mvXjauq2xN7DDe+Mu1UUqtnWW6JpmfJiJjx797fmvjx5tr2sEaKOBbfXXjaw5k47FvSym2aam7Mi4qBmqbJReDstZnK1xMQ+5NRMqOYc96JnG/wBypCbklsJpLCtnXi91iXAaA43VU7KpxmOaVXAXIKLzWwvZCU8BJkxuNOXxE1VXYdfT6EujKiG2KL2y2itinMOJsLe4Guo9rufFmQZm3cuv1quY1Df1UtYYRX0OS8sAQlavlzdiiEwHIVHUaPI2CUNLFAcCUtUrOKX0huBApJO5pW15m2Gl1Epwo3feMcAJojv6IrW5m6h3Sn09fFkfBfMkMiNZkGf4Ql529wcYLTMqn7sJLby/c7VG1+HkiTa/FgqwaGGY1gOEafZWaK7MDqJQEUfuRhNsi5YSFzRiICAV8ns1XyA0leRegn/uYvaK9zH/tWMWMVd3aCHqrXCbimhlzl4GHKhgV+7vRTaTSUCaiGE4CTVhTg10xKepdnDyua5Xqpi9q2fwBr0lFM5V1LAD2QwiAQRsXH4NkqtbRJdfxVI/WKl/RqlJCK7hntPH3jH0PT4OVawl66J2bxrCblxVIQan4SCqfg7Ay2qQbeBisouB2ZHN9AOOxOek+gnaMOe6mmuHk3KIcoPNdE0gjo71MHEnsp0tKh4VFMbVKhlO7bRrgZQ72bbTnqmWadt+i8M4WavHREnHAumB7b2vAMJqJ/Yk3ZXPN9tXO8L5/VkjQ8lHjXbm4Iq3iUAvo2BQpKj4Y/okYcuxuJMHDbBDdz5tpvV2IoMOH2guPvoOaYTHWKOwgOX/lDvIjjkYWSDSofywj5YplA9eVTUxzgIaJtUvS/f3ppJiMvgmbNhm28AzNm1l8JA9tfb1bBO1LmIMbDlDwg19Zl0BWzTxVzc26bvenV2DQisEjWOlXUPsj4IIfvf3iObXxrDxKcM3D9S339XOcEQmuyBndTHY3JrtRGHnBmMzaTTYk/y1hqCCmidC7w7DUan5/PUqjLKsfX9oymdeDpy7BvbcjLSiFCd8dbbsM2ts1YDt67JeuhSO0pcyRkKhuUqZQJHqGgcldQ8mp5tRjxZ7saWm0GKIMeokCAtV2LJ50BypdZ95bCD0FJS6lrbF811xYS62+L0rcgsMGVJziKEISCjff61lFfdEd/cnc9YuFU9RswPEu822vR3g6QTNk/6aeVliVWnmWXFbiR5jSYQm8wwrtU6Ec8WoYT7F0aerdzL5Q0n6B//w6zYf7teyEwD9+qRc7nLjppmRFYMRmkUiaZenxWW71BvgUy9LCJhereEeAIOys+YnyIIQecAzhdVBLpXlXNy3dJeRTfK2mi+Zml2FBoSpaj9XH7mRFq9iTfBjkpHfMcfF0i6L1rjPsi2mO9e18B8W+bmhF1RcH8mqkGGWcuqE3y3eEQM9tgI/1R6xCGl1B3avvQEk5IwSl5SswXyW7lgmdIPxV19iq3duNmhJQz8mn5rtoXoYD6NZyxZDf61yfnaNJeBEgLD0wqTOjhuV2Z1rKWGgyi+m39+j5V5v6Cz6otGbfQgvsLGOPnImMnidwbJuHJDHdTgTOQDk9hwy2w7iAQnDwGRe3fsC9IZTLAyHhxEiqUaIKT4M5QK+SRBX/5Y6bPUudCyYpAItGIfIOb7FkF0L1d3eyqIK8Jo8v2Zgpw/xKgyiUvFsGD9Y80iuDJXfx3gzCq5hIGwNhivj+BGWDpp5QS52UOIAGlGYH09BWTDsFh7JggMY0N4w/KNxw9SmWsesFxkJ9nF/oKT+YPAsgCTIJLd3i34Eti1sCzLJPUb7YlA1RxbXwU1tE9gaxDgI6QYYP4NAPPAQB4PboUnbxrx9AP5EtwEEQf+cVLtsx22cXRsUtxQcYHgDk2sMzw0qpiAWCe/57jc9YgxjxnCWm8BjcAJ1jLEQAe2bT0pios13PGwrRNqQPPh+OIIbccSab4ndvlfW9mNmCe9a4dsYNAZ3npog6+K98ZcCfbrlWUt8SZl+JHjrZg4PfxlNob9rQ/rTuKYh9GpefZW5pZARTRva0dQ9c5dwsMrGRhGz3NvsHWmC+3b1VTa0aY2TCQ9jIGY9Tnr/1V3cN9Piz+f7+OtL7rSNi+Kiima/eFKyLHqz9E34BY/v+LnCEh3Fd3TuwQO6QSg+iDSIfUARkWxtckUaNaLrntnCn7+zGJ3bA90kdvKhgsYtjDkWp5CbUaHYw0IsT6u7N/VXEkvQqjFG/RkGKMQe2cA/0qA4K8/nqR8yHaNKdHSSJt3eIBRMo4/0c9q3+sZ/IlNC4Xe6RQ5/aM+wnnqW99Y4UMlyAItX95H9xMODEZ/sYrOuvAmBYmV3Y4+b3B7BzYmGqFGzd7VerXCUuKrm4BYHZI+eLmNSx10uevXW3EqTowTyVfFMZloVz9BQEbrmsFwsS2HRi/Vq8R+97OJC7SO+9X9DKgDy1ouiF16XpxwmmiShePHs+R8nz15omrcViy49TWYSiybY2aFU7M1x02Nnlo2uAOp2//LpU+RNbtr2Zl4BbVsBd57KdtY/nSla3Y/StuSg6Ym3LTt719aziesSJlIhPga8k3o5rarqf/9hXdpceBIc8+mnDoTuqvvHvdOVtDnxSbbxbq/KmRzjdj1J1sL8adU8vUKp/qmK357ST6aeSsY+Kuwzd7JsbjL0Ca3CoBtQAPp9CoNE91x9Cf+yLBJqsj8Xws/LWvqPqgn+63CsMXVACMAV7NYU7c+UBSEal5Cxg8t2Pb2F4HqBaLcfjimyFd4QybSLZHr+4ZjRkzxKKRburAKaK5b0Ea+YKJm8A6z3NRge5OvGoqlKOWC0Jyt7YK8Egitd62vKkk0OexIWm2MbLMKiw0lULlL8CgYnKqRC3g9i/2rIz+tIY9JKGqmNtQKpGVTXCG0bD1O4hy2Psxi1kwzUSvZyHlviR5HExMM2WmZSPLkfnI+Ak7aomrXq9zDCmcH6FCcG5SO0CHvPC0wl5YROYkIABVMctr9Wo1gLpiSm/NSC2FDvomh9Oz0cHO5Jo9mk7g1X63o++1lNguTEKTfBQKi3sDQharfva0mSFRry91bscAyp18xgwFEZkOFFSHDoQmCdEAWF4goNkqCxEowjmx04gBrsmTOLHht3yndJOFmNLISY4wWN1e62mR6t7tq/cD958ZVSlgzyDsYPrGjtllj47P3+97//dT9YjlgCsYteDNNGjfyv63Lep9rKh9qicGCjQWgj5S1kygnjgWjbGKAIQynME6U/KlwVHjZmJaVe94hCV/e3DsJs7XikrQv1ZJbTdFSmhMxDx4Gmhnz/0pJzT9/L0+4ZeGQeYK8tptLx+D0cZdICiJWSjZ/aArcrp0MbEpu6CZD8yu1zrPZmNKIQ2naTze/TmGXYHtPStKDnpomwPvA8UYXO6AHgw/uCjiqoxTL7G4IB8MzwOQTGys29BVCAk8mK3oaanId0R0MP9hgOex6wJe+IP0DzGp3K7iGJDJ0KTO5xmfUeYopTxNAUNEvtfA5Q+D3wULtg51PqECw1NWv7qZwBDL/8LLlJc6CWe7uWc0NaQjk6Jmc3zNRrD+1QnNgFcXtfVQxSUzDqiN2ooS4tDY7q62u1ZjEbMySwooFKopqCSgqFu3Kcwffxmn0gfju6QY2ag7trE66a11YIjf5zCddQYQjV49lvvV7EkfC6IUXrvxP+80KordXHbY57zT2pZ7O55+mNZnBfSerxXiva7qs9dp9hA9PUOx9Iz/SxlMUzlXuCVvxfuPsJA1lAo3LbDblsioLTqTHeUvQMlM4ThOdWnx44/clhDThnDO8sZ2DNoDkx97LTs0tClGn4Z+xn+UXbAUrXYmHI6eXlE359XVL65fU6a2MEDwxokUb0QEJagCM3KMEUeD9pnA624EE7rLrnaeWQqTVtlH14N2KdBAhB7EyzszUjLuVFLi+2317vX8FEot+odQLnc7m0wuXiSmMCq5lk0yqZohkGlMZKn9TeAnauEG4m8ButiTsj6IYxZ44cetCYxaPzaJ6hUY0Ty1EwqbzHEte4LxRQxuCadvIbgAEh+gBdh2OsS40Ns7TATIQcN3mmxpqL4VFvK8vob0uVPoCMygS/DgEhzMpV+ICISBsl1xprW+XfieSTSCkhFI6anMWoOZoLOQEs1a+Jrky91/z++BpoWcKpwobwuBeGCcIJmdhrdnsJC/8pmMiylxp7jNAG5ji24lqUAF1m0gQKjVm81Nb94Ep5hUn6VQ9D8HU8+V658bQWhI2ql1MV2yxzFVzhcvMMSKIQ++DbD96weOM5TwfpoUaqd/OyAwdiLxiCnVdJwTRoeZYOygHW+ml150ba4P1Go6bkAHIHs0PabOADAuphZls4xx1trK2YeWLbIj4XivoTtSoBdQxJuabtLjUweDEy+6bV7bVDA7eDBkLYjIeftxk645vVx+FAtnSCEBks/F0RQdKaW2OBTB7WbrxpExOH/3OKjm8rOJTlk0Vcq7eVTJW+7QhOaQxrGfIAqgjoeXLBd1B4wFVbjUAJgcWtyZGEdHNypCODD/e8iwEMEg36oUorGz2L0NJ2jjs5GX6VTWHhUWOOFL4BMiumJwbvhuzQZ7JZEKBWmdHXdmdc57qbK2b2O5fXwl8+AGQHKa4V9FKhUBAdP3okJPbRo8HaC8YPySnPYDA+9FRyUd5psMtBSTtUmdyVtArZS0Io/O7VvUZp+527VM7ogQxxpVkEYBPCLN3WBeMVrcpl+13dcNmpS75hnxaEkXddSwODQV+cH598ajbVha31b1cllImv+SN8Lki8HE2E4X/BjgbT4IiaoVbDs63GwpOMW1hAlCFIGH5e5n/1UBSIObHuK1tW8Vj02BbNu9cHR6/PX50dnB8hbPmTJJ7ISH+bQIm4qeItgbPwn58uwpwKRwufVFkQfRYXQQh6Ja9TvJKfrIXFeqkSEosZGL3LVBl0p8nZgkJTOa6fKFq3nPY1aI8twJ81Oo+Bfnlo7YGfjuFxpwEDlMYFS0OjYzHawwQWY2Cq3uP1DJkF+7QaHZgtSWK4OApGEWJRuPUi/bgi0olZX0zc683DlLhttglJbaDJy6gHhg60gcJBf3snHEcv22zxUS7gpNkgRLgKM2mNHYH2LI6Sh0oiOgq4+og+wfi3+ImZW5L0YPFTlyQ5aoT8Tdp15PBqOmgPzQ1bK/Azbm1bY2wLFPqwXNZEIMHppNCIQiDfwN1QOg4YCo2PqshbzkkUfqHOF5sG9O88qprBVDCSmoLFa8qgIoOVtCuLG0eAYLvOrltZzN2/tVd9gR+7h+0xN68CzlIH/5q/ePb8+T5UB9tBEa1aWGtpobrxXOnMvOAvwCvgQyOT0DLfKwNb2F9LfSNnSjgHFn6R3XIMaNq62QvN1KlDN5Xbdk48M43UJdtbrSE9LtIQQSByKu2MBVQ6Yv5MFtE6nC78wR/bbj5jHZq8X6yvVqz2toK1bZ+g27MZsAFEyr+q4Pd9vWasd/fK1ObJjDsvm5vsRKnNJD5Je0fr4ZuuXN6CsZpDuuuLKJTUgE2hZ8eLRYX94J7bwvzae3iHN2GobXsi8TAJdAzOFZ2ylzD5LygEqJ65LU3GT9QeIoKhkEmNbPva8JB0gSvHrbP+FZVtvuQ389VR7a/oxc4y6ecGFcKbcTC16OfqflAJgKb/b2tX09vGDUTv/hV7bIC1EDt12xTQQW6LWijaGnGAAL1tpJW8iLQSllo36q/vvDfDj92l3EtvcURySc5wOJyPNyKQZEU2yXgZPC2fMCSinsjHMlCQTaHN4nHQZsqKoSnOXrYtNTZzA8fmy/YJ527So7AfYsvo1MoOTyBm/T32AZhOaI0/rjmLzLSDPTAOPjUVigiXaSVCPNkrc00+JU1uwpE1B4DlVtbdBre7XOUn2PXJXBjNgvWIwh0mZpHeu6MGLlJrBn4YVZ8WNU0Ns3+GMeQ18/bmPVIZXg67F4WG9TGP7Mq8NRqT5cu+cIM/dl+PkM1whckLQeesytNh50o1FfyYW+jt/AETdFZ22JLeBonvWIV3lBXeOJsb6938nkHBG5TyZN5hCi5iFvZxp2/n5olRjzcNHVQnm+g5UeAV9RM6cxc1Lwwv2PKt1FlrBZ/H1lVddop3pD3jVQskyDrL+xzsupaISsQL2tfeJQB/SgGCbA67Rsh+tFpiHu2nrvZKeV9ZpjRIZWmUJcF3UVf7WK+e24OodHIeRImw8imEmNjjetuskEMv0lwEXm6o7+ccgY6dZADs7C89QN6rwPUwD2772uWG+WFOoRhqitkQ2LpxFRaiZGaGeD9aVHZK2You2eP4FulK+0bedckYv9bg0LM/OFprDs9RGw+kKIuF7Pxgur5f9kM3uQ+Fe8d/ald1qIgSg3t/a0TtN8IU3+Cv4lNdf3mT/cbt5e29+/NO72pkde6PkCLPzdElGmgQgOYVyYr0+NVxr5u5vKXs+GiYvgdYqXyTolJ8+hTLCNULYTWnua6te/g9S2gHGEQOx2k1K7kA0cQh8ezh5ojStDnzPKzr/dmFGGe1kHw12Rbv89krk7/NLzeUjAgiQmvN+cDi1TlAS42VBxGWIqvNwfbT7tBrRelle/17LUrDOTmRJT6zsoojgHBAwwmYtzrg2sG+ogyk670bz5bHnLUX2T+YrUWhwn+unYViIUTAVzDUPLBZjgXgfsQ7jn7IAv9KWn2IoVuXL/PJXqe9Po6AxdOenodckMA1zzt32yDKtQSxWteLQ7etUPmyI86vT7swAev5xJ52Day8LaF/KvfF2Xihip9cCGutLm72uEDOUMaM2V3m/7I4G5SZJG+6yJbgieAmJTiCDUgFGCv1RlCvTaIAKS7MBPj4BFBXHUXONLJ1iRGb29JAMBG+XTugmClNUy1qSCLf7JO3uIzbG+oJ4ncVlOwRqcNyBz1WytsLjQoqmFKMcy2daXXGkjQjRJdeGym8GeiIZ+2qOcYreaglKKPyB/kDvG/4qpiK+dZDbHv+IHvVSfF4aJx3fl4UJ1tmKSLytZ2s/Nqs+lEqe4cJVrE7GOvIQ6Smr5zXfLqEhnBMNNdQRbNyz10xArsqbY2qKunwyTizEUlfITpMf0E3vqgTTw5p0mfCB6POY3Z4lfpGd2OCwSlJwscCJpWlDUwOZyy2hlAmWL2oWf7f/EFoMRfYM8z9dIiEL7a1vjupf4ToJ71xmy5ZR6n85o1LvavTe/nSCUNFWlOdk+NBfXKQmpYleI4xZIqEMA1rzr59jCMex41v5gsfyqs3ETCJDELvNHyji+AW+e3C9FUUmuP3xBuTTigXmd1lmD1KRMsb9y8E1OajaMfOfaZdaXZx3nqx38OwjS9OCR7veJtvKUurRNmSf/C5QYEdHw989xC3ngVEB2sAsuWhI0MorLMq7lAJwwWiCjzMyXybFQ+LPxae4ZjzqPHwQjV1mKVixFz8CApHcaF2lW7FIfgpneZb85TVJbHFVOzZPcp6fCyL3q31IRUeYUoFwMGM32mBCCFcL6UHE8J6lqwNN+U03OZSpA1E0KQtLaj+0KgTyQ9IIeLJRi0hWMThXYilBuwyryzvkWrHZApeHDXM/o6FMBkCDV3cc3f4PFbrFSz1WSM1Nb7M/6baYl5D0Jmwe2xJmWZOQg8PqkjL4UkeIhJs1ZVQy+pFEqKAjIzal8P9kJFEW2wQdImaNgzPYyEdht3LBDpG5BvSWzLlDKFy8sPMdP2eMGoI60PBSsRU0u6KmEqFzP85rdjtPOD+sOq2wnkPqpao925YG+QKAcZ/yX3yoVo3vVu2Dz1cBsD2dHPeRf+wbjF/xQY+9/QfsIHmhJfFLS7ad2+uhAiKiu4LnJth+P6Vbyx9Hy0HFuDPP//Ht6/+BSMUq2jtZQEA - +
@@ -3183,6 +3183,10 @@ Finish at Финиш в
+ + Protest starts at + Протест начинается в + Protest ends at Протест заканчивается в @@ -3843,6 +3847,10 @@ RRS26 start RRS26 старт + + Sailing World Cup Start + Старт Кубка мира по парусному спорту + Activate configuration Активировать конфигурацию @@ -4619,6 +4627,10 @@ Wind Chart Диаграмма ветра + + Loading wind data... + Загрузка данных о ветре... + No Videos Нет видео @@ -4889,7 +4901,7 @@ The time passing of {0} waypoint can not be before the time of the previous waypoint or after the time of the following one - Время прохождения отметки {0} не может быть раньше времени предыдущей отметки или позже времени следующей + Время прохождения отметки {0} не может быть раньше времени предыдущей отметки или позже времени следующей Error removing fixed mark passing: {0} @@ -5863,6 +5875,14 @@ Competitor Registrations are disabled for this race. Enable by checking the checkbox to the left. Регистрации участника отключены для данной гонки. Чтобы включить, установите флажок слева. + + The following competitors cannot be registered to this race : {0} + Регистрация следующих участников на эту гонку невозможна: {0} + + + {0} has already been assigned to fleet {2} in race {3} and therefore cannot be assigned to fleet {1} within the same race + {0} уже присвоен флоту {2} в гонке {3} и поэтому не может быть присвоен флоту {1} в той же самой гонке + Add Mark to Regatta Добавить отметку к регате diff --git a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties.xlf b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties.xlf index e7b2b6b28bd..8a94a8fa43f 100644 --- a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties.xlf +++ b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties.xlf @@ -1,12 +1,12 @@ - +
- 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 - +
@@ -3183,6 +3183,10 @@ Finish at 结束时间
+ + Protest starts at + 声明开始时间 + Protest ends at 声明结束时间 @@ -3843,6 +3847,10 @@ RRS26 start RRS26 开始 + + Sailing World Cup Start + 世界杯帆船赛开赛 + Activate configuration 激活配置 @@ -4619,6 +4627,10 @@ Wind Chart 风力记录 + + Loading wind data... + 正在加载风力数据... + No Videos 无视频 @@ -5863,6 +5875,14 @@ Competitor Registrations are disabled for this race. Enable by checking the checkbox to the left. 参赛队注册已针对此比赛轮次禁用。选中左侧的复选框可启用。 + + The following competitors cannot be registered to this race : {0} + 以下参赛队无法注册此比赛轮次:{0} + + + {0} has already been assigned to fleet {2} in race {3} and therefore cannot be assigned to fleet {1} within the same race + {0}已分配到比赛轮次{3}中的船队{2},因此,无法分配到同一比赛轮次中的船队{1} + Add Mark to Regatta 向比赛添加标志 diff --git a/configuration/git-blame-stats b/configuration/git-blame-stats new file mode 100755 index 00000000000..16371852203 --- /dev/null +++ b/configuration/git-blame-stats @@ -0,0 +1,4 @@ +#!/bin/sh +# Counts the lines per committer currently in the tree, for all files with type +# .java, .MF, .properties, .swift and .xml +git ls-tree -r --name-only HEAD -- | grep "\(\.java$\)\|\(\.MF$\)\|\(\.properties$\)\|\(\.swift$\)\|\(\.xml$\)" | sed -e 's/^/"/' -e 's/$/"/' | xargs -n1 git blame HEAD | recode ISO-8859-1..UTF-8 | sed -e 's/^.* (\(.*\) [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9] [+-][0-9][0-9][0-9][0-9] *[0-9][0-9]*).*$/\1/' | sed -e 's/ *$//' | sort | uniq -c | sort -rn | awk 'BEGIN {s=0;} /.*/ {s=s+$1; print $0;} END { print "\n Sum: " s; }' diff --git a/configuration/git-stats b/configuration/git-stats new file mode 100755 index 00000000000..6546b548d58 --- /dev/null +++ b/configuration/git-stats @@ -0,0 +1,2 @@ +#!/bin/sh +git log --shortstat --pretty="%cE" | sed 's/\(.*\)@.*/\1/' | grep -v "^$" | awk 'BEGIN { line=""; } !/^ / { if (line=="" || !match(line, $0)) {line = $0 "," line }} /^ / { print line " # " $0; line=""}' | tr '[:upper:]' '[:lower:]' | sort | sed -E 's/# //;s/ files? changed,//;s/([0-9]+) ([0-9]+ deletion)/\1 0 insertions\(+\), \2/;s/\(\+\)$/\(\+\), 0 deletions\(-\)/;s/insertions?\(\+\), //;s/ deletions?\(-\)//' | awk 'BEGIN {name=""; files=0; insertions=0; deletions=0;} {if ($1 != name && name != "") { print insertions " " name ":" files " files changed, " insertions " insertions(+), " deletions " deletions(-), " insertions-deletions " net"; files=0; insertions=0; deletions=0; name=$1; } name=$1; files+=$2; insertions+=$3; deletions+=$4} END {print name ": " files " files changed, " insertions " insertions(+), " deletions " deletions(-), " insertions-deletions " net";}' | grep -v "^[^,:]*,.*,: " | sort -n diff --git a/java/com.sap.sailing.autoload/.gitignore b/java/com.sap.sailing.autoload/.gitignore deleted file mode 100644 index 092357e47d3..00000000000 --- a/java/com.sap.sailing.autoload/.gitignore +++ /dev/null @@ -1 +0,0 @@ -bin diff --git a/java/com.sap.sailing.autoload/META-INF/MANIFEST.MF b/java/com.sap.sailing.autoload/META-INF/MANIFEST.MF deleted file mode 100644 index cb8d57d083b..00000000000 --- a/java/com.sap.sailing.autoload/META-INF/MANIFEST.MF +++ /dev/null @@ -1,23 +0,0 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: AutoLoad Races -Bundle-SymbolicName: com.sap.sailing.autoload -Bundle-Version: 1.0.0.qualifier -Bundle-Vendor: SAP -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Bundle-ActivationPolicy: lazy -Export-Package: com.sap.sailing.autoload -Import-Package: org.json.simple, - org.json.simple.parser, - org.osgi.framework;version="1.7.0", - org.osgi.util.tracker;version="1.5.1" -Require-Bundle: com.sap.sailing.gwt.ui, - com.sap.sse.common, - com.sap.sailing.domain.shared.android, - com.sap.sailing.domain.tractracadapter, - com.sap.sailing.server, - com.sap.sailing.domain, - com.sap.sailing.domain.common, - com.sap.sailing.domain.persistence, - com.sap.sse.replication -Bundle-Activator: com.sap.sailing.autoload.Activator diff --git a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/Activator.java b/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/Activator.java deleted file mode 100644 index 0c1a601cab3..00000000000 --- a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/Activator.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sap.sailing.autoload; - -import java.util.logging.Logger; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.util.tracker.ServiceTracker; - -import com.sap.sailing.server.RacingEventService; - - -public class Activator implements BundleActivator { - Logger logger = Logger.getLogger(Activator.class.getName()); - - @Override - public void start(BundleContext context) throws Exception { - RacingEventService racingEventService = createAndOpenRacingEventServiceTracker(context).getService(); - new TracTrac(context, racingEventService); - } - - @Override - public void stop(BundleContext context) throws Exception { - } - - protected ServiceTracker createAndOpenRacingEventServiceTracker( - BundleContext context) { - ServiceTracker result = new ServiceTracker( - context, RacingEventService.class.getName(), null); - result.open(); - return result; - } -} diff --git a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java b/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java deleted file mode 100644 index e461b040993..00000000000 --- a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.sap.sailing.autoload; - -import java.io.IOException; -import java.io.Serializable; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Logger; - -import org.osgi.framework.BundleContext; -import org.osgi.util.tracker.ServiceTracker; - -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; -import com.sap.sailing.domain.persistence.MongoRegattaLogStoreFactory; -import com.sap.sailing.domain.racelog.RaceLogStore; -import com.sap.sailing.domain.regattalog.RegattaLogStore; -import com.sap.sailing.domain.tracking.RaceTracker; -import com.sap.sailing.domain.tracking.TrackerManager; -import com.sap.sailing.domain.tractracadapter.RaceRecord; -import com.sap.sailing.domain.tractracadapter.TracTracAdapter; -import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory; -import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants; -import com.sap.sailing.gwt.ui.shared.TracTracRaceRecordDTO; -import com.sap.sailing.server.RacingEventService; -import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.impl.MillisecondsTimePoint; - -/** - * Auto load TracTrac races from a parameter called tractrac.autoload.json_urls. This - * parameter contains a list of all event json urls from which you want to load races from. Only - * races in state REPLAY will be loaded. - * - * @author Simon Marcel Pamies - */ -public class TracTrac { - Logger logger = Logger.getLogger(TracTrac.class.getName()); - - private final TracTracAdapter tracTracAdapter; - private final RacingEventService racingEventService; - - public TracTrac(BundleContext context, RacingEventService racingEventService) { - this.racingEventService = racingEventService; - this.tracTracAdapter = createAndOpenTracTracAdapterTracker(context).getService().getOrCreateTracTracAdapter(this.racingEventService.getBaseDomainFactory()); - try { - autoLoadTrackTrackRaces(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private TracTracAdapter getTracTracAdapter() { - return this.tracTracAdapter; - } - - private RacingEventService getService() { - return this.racingEventService; - } - - private void autoLoadTrackTrackRaces() throws Exception { - String jsonUrls = System.getProperty("tractrac.autoload.json_urls"); - if (jsonUrls != null) { - String[] splitJsonUrls = jsonUrls.split(","); - for (String jsonUrl : splitJsonUrls) { - logger.info("Loading races from " + jsonUrl); - Pair> racesThatCanBeTracked = listTracTracRacesInEvent(jsonUrl, /*listHiddenRaces*/false); - - for (TracTracRaceRecordDTO raceRecord : racesThatCanBeTracked.getB()) { - RaceRecord record = getTracTracAdapter().getSingleTracTracRaceRecord(new URL(raceRecord.jsonURL), raceRecord.id, /*loadClientParams*/true); - if (record.getRaceStatus().equals(TracTracConnectionConstants.REPLAY_STATUS) || record.getRaceVisibility().equals(TracTracConnectionConstants.REPLAY_VISIBILITY)) { - RegattaIdentifier regattaForRaceRecord = null; - if (raceRecord.hasRememberedRegatta()) { - Regatta regatta = getService().getRememberedRegattaForRace(raceRecord.id); - if (regatta != null) { - regattaForRaceRecord = regatta.getRegattaIdentifier(); - } - } - RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(getService() - .getMongoObjectFactory(), getService().getDomainObjectFactory()); - RegattaLogStore regattaLogStore = MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore( - getService().getMongoObjectFactory(), getService().getDomainObjectFactory()); - getTracTracAdapter().addTracTracRace( - (TrackerManager) getService(), - regattaForRaceRecord, - record.getParamURL(), /* effectiveLiveURI */ - null, - record.getStoredURI(), - new URI(""), - new MillisecondsTimePoint(record.getTrackingStartTime().asMillis()), - new MillisecondsTimePoint(record.getTrackingEndTime().asMillis()), - raceLogStore, regattaLogStore, - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, /* offsetToStartTimeOfSimulatedRace */ - null, /*useTracTracMarkPassings*/ false, - /* tracTracUsername */"", /* tracTracPassword */"", record.getRaceStatus(), - record.getRaceVisibility()); - } else { - logger.info("Ignoring race " + record.getName() + " because it is in status " - + record.getRaceStatus() + " and visibility " + record.getRaceVisibility()); - } - } - } - } - } - - public com.sap.sse.common.Util.Pair> listTracTracRacesInEvent(String eventJsonURL, boolean listHiddenRaces) throws MalformedURLException, IOException, ParseException, org.json.simple.parser.ParseException, URISyntaxException { - com.sap.sse.common.Util.Pair> raceRecords; - raceRecords = getTracTracAdapter().getTracTracRaceRecords(new URL(eventJsonURL), /*loadClientParam*/ false); - List result = new ArrayList(); - for (RaceRecord raceRecord : raceRecords.getB()) { - if (listHiddenRaces == false && raceRecord.getRaceVisibility().equals(TracTracConnectionConstants.HIDDEN_VISIBILITY)) { - continue; - } - - result.add(new TracTracRaceRecordDTO(raceRecord.getID(), raceRecord.getEventName(), raceRecord.getName(), - raceRecord.getTrackingStartTime().asDate(), - raceRecord - .getTrackingEndTime().asDate(), raceRecord.getRaceStartTime().asDate(), - raceRecord.getBoatClassNames(), raceRecord.getRaceStatus(), raceRecord.getRaceVisibility(), raceRecord.getJsonURL().toString(), - hasRememberedRegatta(raceRecord.getID()))); - } - return new com.sap.sse.common.Util.Pair>(raceRecords.getA(), result); - } - - private boolean hasRememberedRegatta(Serializable raceID) { - return racingEventService.getRememberedRegattaForRace(raceID) != null; - } - - private ServiceTracker createAndOpenTracTracAdapterTracker(BundleContext context) { - ServiceTracker result = new ServiceTracker( - context, TracTracAdapterFactory.class.getName(), null); - result.open(); - result.getService().getOrCreateTracTracAdapter(racingEventService.getBaseDomainFactory()); - return result; - } - -} diff --git a/java/com.sap.sailing.autoload/.classpath b/java/com.sap.sailing.competitorimport/.classpath similarity index 98% rename from java/com.sap.sailing.autoload/.classpath rename to java/com.sap.sailing.competitorimport/.classpath index 4f83b2397ec..b862a296d38 100644 --- a/java/com.sap.sailing.autoload/.classpath +++ b/java/com.sap.sailing.competitorimport/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/java/com.sap.sailing.autoload/.project b/java/com.sap.sailing.competitorimport/.project old mode 100755 new mode 100644 similarity index 89% rename from java/com.sap.sailing.autoload/.project rename to java/com.sap.sailing.competitorimport/.project index c38dee8ba08..c43491b2887 --- a/java/com.sap.sailing.autoload/.project +++ b/java/com.sap.sailing.competitorimport/.project @@ -1,6 +1,6 @@ - com.sap.sailing.autoload + com.sap.sailing.competitorimport diff --git a/java/com.sap.sailing.autoload/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.competitorimport/.settings/org.eclipse.jdt.core.prefs similarity index 54% rename from java/com.sap.sailing.autoload/.settings/org.eclipse.jdt.core.prefs rename to java/com.sap.sailing.competitorimport/.settings/org.eclipse.jdt.core.prefs index a698e59674f..295926d9641 100644 --- a/java/com.sap.sailing.autoload/.settings/org.eclipse.jdt.core.prefs +++ b/java/com.sap.sailing.competitorimport/.settings/org.eclipse.jdt.core.prefs @@ -1,12 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.8 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/java/com.sap.sailing.autoload/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sailing.competitorimport/.settings/org.eclipse.pde.core.prefs old mode 100755 new mode 100644 similarity index 71% rename from java/com.sap.sailing.autoload/.settings/org.eclipse.pde.core.prefs rename to java/com.sap.sailing.competitorimport/.settings/org.eclipse.pde.core.prefs index 42ee077170e..b7e72d019c2 --- a/java/com.sap.sailing.autoload/.settings/org.eclipse.pde.core.prefs +++ b/java/com.sap.sailing.competitorimport/.settings/org.eclipse.pde.core.prefs @@ -1,4 +1,3 @@ -#Mon Aug 29 13:20:38 CEST 2011 eclipse.preferences.version=1 pluginProject.extensions=false resolve.requirebundle=false diff --git a/java/com.sap.sailing.competitorimport/META-INF/MANIFEST.MF b/java/com.sap.sailing.competitorimport/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..d4ff74b71ae --- /dev/null +++ b/java/com.sap.sailing.competitorimport/META-INF/MANIFEST.MF @@ -0,0 +1,11 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Competitorimport +Bundle-SymbolicName: com.sap.sailing.competitorimport +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Export-Package: com.sap.sailing.competitorimport +Require-Bundle: com.sap.sse.common, + com.sap.sailing.domain.shared.android, + com.sap.sailing.domain.common diff --git a/java/com.sap.sailing.autoload/build.properties b/java/com.sap.sailing.competitorimport/build.properties old mode 100755 new mode 100644 similarity index 100% rename from java/com.sap.sailing.autoload/build.properties rename to java/com.sap.sailing.competitorimport/build.properties diff --git a/java/com.sap.sailing.autoload/pom.xml b/java/com.sap.sailing.competitorimport/pom.xml similarity index 88% rename from java/com.sap.sailing.autoload/pom.xml rename to java/com.sap.sailing.competitorimport/pom.xml index b68132bd8ba..95d28619b86 100644 --- a/java/com.sap.sailing.autoload/pom.xml +++ b/java/com.sap.sailing.competitorimport/pom.xml @@ -1,12 +1,12 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sailing.autoload - eclipse-plugin - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.competitorimport + eclipse-plugin + diff --git a/java/com.sap.sailing.competitorimport/src/com/sap/sailing/competitorimport/CompetitorProvider.java b/java/com.sap.sailing.competitorimport/src/com/sap/sailing/competitorimport/CompetitorProvider.java new file mode 100644 index 00000000000..e12c953be5b --- /dev/null +++ b/java/com.sap.sailing.competitorimport/src/com/sap/sailing/competitorimport/CompetitorProvider.java @@ -0,0 +1,47 @@ +package com.sap.sailing.competitorimport; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import javax.xml.bind.JAXBException; + +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sse.common.Named; + +/** + * Competitor data may come from various sources, such as external regatta management systems (RMS), + * CSV files, Excel spreadsheets and the like. When creating a regatta that uses "RaceLog Tracking" + * (also known as Smartphone Tracking)---and be it only in order to manage results for the regatta + * with the race committee app---it is tedious having to enter all competitor data manually when + * that data is already available in electronic form.

+ * + * In its simplest form, a provider will just produce an otherwise unstructured list of competitor + * entries with no hint as to the event, regatta or race that the competitor pertains to. More + * sophisticated providers may structure competitor data into events, regattas or even individual + * races so that importing these competitors into a single fleet of one race column becomes possible. + * This will be particularly useful for split-fleet racing. + * + * @author Axel Uhl (d043530) + * + */ +public interface CompetitorProvider extends Named { + /** + * @return keys are event names, values are the names of the regatta in the event for which the connector has + * competitor names. Should the value for a key be null, competitors for the event may still + * be available, only they may not be keyed per regatta in that case. + */ + Map> getHasCompetitorsForRegattasInEvent() throws IOException; + + /** + * Obtains competitor records from the source of import + * + * @param eventName + * an event name as obtained as a key in {@link #getHasCompetitorsForRegattasInEvent()}'s result + * @param regattaName + * null to get the competitors for all regattas in the event specified by + * eventName, or a regatta name as provided in the value set for the key + * eventName as returned by {@link #getHasCompetitorsForRegattasInEvent()}. + */ + Iterable getCompetitorDescriptors(String eventName, String regattaName) throws JAXBException, IOException; +} diff --git a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch index f7d00da3fd0..c2d9abb8f9c 100644 --- a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch +++ b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch @@ -1,46 +1,46 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch index 25eb0c93742..f1fab25cd8b 100644 --- a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch +++ b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch @@ -1,45 +1,45 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_ja.properties.xlf b/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_ja.properties.xlf index a6231731fd3..a388689ee7e 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_ja.properties.xlf +++ b/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_ja.properties.xlf @@ -1,6 +1,6 @@ - +

H4sIAAAAAAAAAIVWS28jKRC+8yu4jJQcRopn9qkVKznJ2vHKeci2Ntoj7i53o7TBApys59dvVdEP7Ew8F7rq4wOKetHLHUCpljQKHldgl1A4W4Zr2DgPaU6OroJcMyBuTYjaFrDy+hUaXN0BskPEg96CokHMoVoddqDwK0kQS22a2a2ij3zYb9fgxb+gvaKB2Aljfjv9oKNxVjcmHlQmi3HhnT1sVfsVC6h0jFq1X3Hj9j7A2INWSZQki2un402jQ1AkSRbFpAGIikex0AUoGnCH7Q6iic6rQeQNeK1Ygd4qGjLmEu9R1CtdZUtkAiWieEVdgl877UvygbHVrdtqYxcQvYFX8Dc1aiqjBbboHJnmAznv/I5VENOn5cT8d46GjM9Iwf3utX95Qucg89wCosmWl0xdQNg38UcGy0RLSx43mQd/dFPpNnKgk6EW9kg5b2Xi5P6ferff5Z6WjOSUfJYcTF7MHXN0eyLkF+EkRluLIXemu4DO7ZzcWz6Y92xsuYwebBXrmb0Gvceai4pg2eHSWNnNiOtNvFI3utmSNFJzU9VRauNJ/dKqaw/wDQj5qqZgYwMZ9JO6d3hFHXPwZzXxEOoM+UXh8c5WGfSrusPt5RvaRupvaqobnvi941YtMLpCxPlk40j9Y1yDZsjQY1/U3d57U6AbsI4brPJXWBbUfjpNsiovrrANhfiHHMk3jH28PKbP7BP4Avc+XYcu+3RKpWQ6t4CzlBeO18E1+wgLbV9Up0jSUsCoRb6LFvdNCtUm9j1zHJdRI6NvmdFJRiS2IhZSF36usQ2TSjmWdjomnLbptO3QqxPariBDx3Fi0F+Ur4p02o8RSZBIvfZx0yViUAmh/O2xnrXSxUvOYL2f/dtgiLJZ1vvZJ8D+HQ83xhfNEa2dkO0MWz3F2g2Pfu5CQC7bzZDEpprA4TlydNnUWE35oadToRNDXnyiY+dcU5eDfa40++3EWBPq3DqGZcKz62Cs80uQ+t7ya4hvALaPwNiW7QHfu5K8GLVhkZ//bE+8FLOAz2vxgu/tLMhW7K0Ymg737syg7BVKbX2ItH85JfOZXfdPQefXu1PSE07RVjSkVDz6T2jzru9njI43EXtcIrA8zN8aDwW96dimbQWq12UCqIBuocKeM5hEXhpMIk0868POGaziThDdy4WBVq3MUWfru+Qg+cPCYktP6yqZP5TVqjY+Ht6v4Lh+PVlAfeEN029I2XH5qm3UFbh9+MuWj5u5saA6nsyTN6dK5HLuGuqXVN10yp1uNlibNXCe0WuVCp0tiDXIGgm0jOQN136Dz9n3bfjwuJXZAvrDxlnnKEWQZIzC1aLiHkrd8M8US9xOxf+VN8i0cAoAAA== diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_ru.properties.xlf b/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_ru.properties.xlf index d4790886c72..b324dfe30fe 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_ru.properties.xlf +++ b/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_ru.properties.xlf @@ -1,6 +1,6 @@ - +
H4sIAAAAAAAAAIVWS28jKRC+8yu4jJQcRopn9qkVKznJ2vHKeci2Ntoj7i53o7TBApys59dvVdEP7Ew8F7rq4wOKetHLHUCpljQKHldgl1A4W4Zr2DgPaU6OroJcMyBuTYjaFrDy+hUaXN0BskPEg96CokHMoVoddqDwK0kQS22a2a2ij3zYb9fgxb+gvaKB2Aljfjv9oKNxVjcmHlQmi3HhnT1sVfsVC6h0jFq1X3Hj9j7A2INWSZQki2un402jQ1AkSRbFpAGIikex0AUoGnCH7Q6iic6rQeQNeK1Ygd4qGjLmEu9R1CtdZUtkAiWieEVdgl877UvygbHVrdtqYxcQvYFX8Dc1aiqjBbboHJnmAznv/I5VENOn5cT8d46GjM9Iwf3utX95Qucg89wCosmWl0xdQNg38UcGy0RLSx43mQd/dFPpNnKgk6EW9kg5b2Xi5P6ferff5Z6WjOSUfJYcTF7MHXN0eyLkF+EkRluLIXemu4DO7ZzcWz6Y92xsuYwebBXrmb0Gvceai4pg2eHSWNnNiOtNvFI3utmSNFJzU9VRauNJ/dKqaw/wDQj5qqZgYwMZ9JO6d3hFHXPwZzXxEOoM+UXh8c5WGfSrusPt5RvaRupvaqobnvi941YtMLpCxPlk40j9Y1yDZsjQY1/U3d57U6AbsI4brPJXWBbUfjpNsiovrrANhfiHHMk3jH28PKbP7BP4Avc+XYcu+3RKpWQ6t4CzlBeO18E1+wgLbV9Up0jSUsCoRb6LFvdNCtUm9j1zHJdRI6NvmdFJRiS2IhZSF36usQ2TSjmWdjomnLbptO3QqxPariBDx3Fi0F+Ur4p02o8RSZBIvfZx0yViUAmh/O2xnrXSxUvOYL2f/dtgiLJZ1vvZJ8D+HQ83xhfNEa2dkO0MWz3F2g2Pfu5CQC7bzZDEpprA4TlydNnUWE35oadToRNDXnyiY+dcU5eDfa40++3EWBPq3DqGZcKz62Cs80uQ+t7ya4hvALaPwNiW7QHfu5K8GLVhkZ//bE+8FLOAz2vxgu/tLMhW7K0Ymg737syg7BVKbX2ItH85JfOZXfdPQefXu1PSE07RVjSkVDz6T2jzru9njI43EXtcIrA8zN8aDwW96dimbQWq12UCqIBuocKeM5hEXhpMIk0868POGaziThDdy4WBVq3MUWfru+Qg+cPCYktP6yqZP5TVqjY+Ht6v4Lh+PVlAfeEN029I2XH5qm3UFbh9+MuWj5u5saA6nsyTN6dK5HLuGuqXVN10yp1uNlibNXCe0WuVCp0tiDXIGgm0jOQN136Dz9n3bfjwuJXZAvrDxlnnKEWQZIzC1aLiHkrd8M8US9xOxf+VN8i0cAoAAA== diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_zh_CN.properties.xlf b/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_zh_CN.properties.xlf index a5419aed3fd..b7349273236 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sailing.datamining/resources/stringmessages/XLF/Sailing_StringMessages_zh_CN.properties.xlf @@ -1,6 +1,6 @@ - +
H4sIAAAAAAAAAIVWS28jKRC+8yu4jJQcRopn9qkVKznJ2vHKeci2Ntoj7i53o7TBApys59dvVdEP7Ew8F7rq4wOKetHLHUCpljQKHldgl1A4W4Zr2DgPaU6OroJcMyBuTYjaFrDy+hUaXN0BskPEg96CokHMoVoddqDwK0kQS22a2a2ij3zYb9fgxb+gvaKB2Aljfjv9oKNxVjcmHlQmi3HhnT1sVfsVC6h0jFq1X3Hj9j7A2INWSZQki2un402jQ1AkSRbFpAGIikex0AUoGnCH7Q6iic6rQeQNeK1Ygd4qGjLmEu9R1CtdZUtkAiWieEVdgl877UvygbHVrdtqYxcQvYFX8Dc1aiqjBbboHJnmAznv/I5VENOn5cT8d46GjM9Iwf3utX95Qucg89wCosmWl0xdQNg38UcGy0RLSx43mQd/dFPpNnKgk6EW9kg5b2Xi5P6ferff5Z6WjOSUfJYcTF7MHXN0eyLkF+EkRluLIXemu4DO7ZzcWz6Y92xsuYwebBXrmb0Gvceai4pg2eHSWNnNiOtNvFI3utmSNFJzU9VRauNJ/dKqaw/wDQj5qqZgYwMZ9JO6d3hFHXPwZzXxEOoM+UXh8c5WGfSrusPt5RvaRupvaqobnvi941YtMLpCxPlk40j9Y1yDZsjQY1/U3d57U6AbsI4brPJXWBbUfjpNsiovrrANhfiHHMk3jH28PKbP7BP4Avc+XYcu+3RKpWQ6t4CzlBeO18E1+wgLbV9Up0jSUsCoRb6LFvdNCtUm9j1zHJdRI6NvmdFJRiS2IhZSF36usQ2TSjmWdjomnLbptO3QqxPariBDx3Fi0F+Ur4p02o8RSZBIvfZx0yViUAmh/O2xnrXSxUvOYL2f/dtgiLJZ1vvZJ8D+HQ83xhfNEa2dkO0MWz3F2g2Pfu5CQC7bzZDEpprA4TlydNnUWE35oadToRNDXnyiY+dcU5eDfa40++3EWBPq3DqGZcKz62Cs80uQ+t7ya4hvALaPwNiW7QHfu5K8GLVhkZ//bE+8FLOAz2vxgu/tLMhW7K0Ymg737syg7BVKbX2ItH85JfOZXfdPQefXu1PSE07RVjSkVDz6T2jzru9njI43EXtcIrA8zN8aDwW96dimbQWq12UCqIBuocKeM5hEXhpMIk0868POGaziThDdy4WBVq3MUWfru+Qg+cPCYktP6yqZP5TVqjY+Ht6v4Lh+PVlAfeEN029I2XH5qm3UFbh9+MuWj5u5saA6nsyTN6dK5HLuGuqXVN10yp1uNlibNXCe0WuVCp0tiDXIGgm0jOQN136Dz9n3bfjwuJXZAvrDxlnnKEWQZIzC1aLiHkrd8M8US9xOxf+VN8i0cAoAAA== diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java index 0bf539a6b6a..cb7839f8336 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java @@ -16,7 +16,7 @@ public enum BoatClassMasterdata { _49ER ("49er", true, 4.88, 1.93, BoatHullType.MONOHULL, true), _49ERFX ("49erFX", true, 4.88, 1.93, BoatHullType.MONOHULL, true, "49FX"), _420 ("420", true, 4.20, 1.65, BoatHullType.MONOHULL, true, "420er", "420M", "420W"), - _470 ("470", true, 4.70, 1.68, BoatHullType.MONOHULL, true, "470er", "470M", "470W"), + _470 ("470", true, 4.70, 1.68, BoatHullType.MONOHULL, true, "470er", "470M", "470W", "470 - M", "470 - W"), _5O5 ("5O5", true, 5.03, 1.88, BoatHullType.MONOHULL, true, "505", "5o5", "505er", "5o5er"), _5_5MR ("5.5mR", true, 9.50, 1.92, BoatHullType.MONOHULL, true, "5.5 Meter", "5.5 Metre", "5.5", "5.5M", "5.5-metre", "5.5 metre"), A_CAT ("A-Catamaran", true, 5.49, 2.30, BoatHullType.CATAMARAN, false, "A-Cat", "ACat", "A-Class Catamaran"), @@ -85,7 +85,7 @@ public enum BoatClassMasterdata { RS500 ("RS 500", true, 4.34, 1.58, BoatHullType.MONOHULL, true, "RS500", "RS_500"), RS800 ("RS 800", true, 4.80, 1.88, BoatHullType.MONOHULL, true, "RS800", "RS_800"), RS_AERO ("RS Aero", true, 4.00, 1.40, BoatHullType.MONOHULL, false, "RSAERO", "RS_Aero"), - RS_X ("RS:X", true, 2.86, 0.93, BoatHullType.SURFERBOARD, false, "RS-X", "RSX", "RS:X"), + RS_X ("RS:X", true, 2.86, 0.93, BoatHullType.SURFERBOARD, false, "RS-X", "RSX", "RS:X", "RS:X Men", "RS:X Woman"), RS_FEVA ("RS Feva", true, 3.64, 1.42, BoatHullType.MONOHULL, true, "RSFeva"), SONAR ("Sonar", true, 7.01, 2.39, BoatHullType.MONOHULL, true), SOLING ("Soling", true, 8.15, 1.91, BoatHullType.MONOHULL, true), @@ -100,6 +100,7 @@ public enum BoatClassMasterdata { TORNADO ("Tornado Catamaran", true, 6.10, 3.02, BoatHullType.CATAMARAN, true, "Tornado", "Tornado Cat"), TRIAS ("Trias", true, 9.20, 2.12, BoatHullType.MONOHULL, true), VIPER_640 ("Viper 640", true, 6.43, 2.49, BoatHullType.MONOHULL, true), + VO60 ("VO60", true, 19.5, 5.25, BoatHullType.MONOHULL, true, "Volvo Ocean 60", "VolvoOcean60", "W60", "Whitbread60", "Whitbread 60"), VX_ONE ("VX ONE", true, 5.79, 2.19, BoatHullType.MONOHULL, true, "VX-ONE"), X_99 ("X-99", true, 9.96, 2.95, BoatHullType.MONOHULL, true, "X99"), diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/CompetitorDescriptor.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/CompetitorDescriptor.java new file mode 100644 index 00000000000..70d8a742547 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/CompetitorDescriptor.java @@ -0,0 +1,111 @@ +package com.sap.sailing.domain.common; + +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.common.dto.PersonDTO; +import com.sap.sse.common.CountryCode; +import com.sap.sse.common.Duration; +import com.sap.sse.common.Named; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.NamedImpl; + + +public class CompetitorDescriptor extends NamedImpl implements Named { + private static final long serialVersionUID = -6645726854552711646L; + + private final String eventName; + private final String regattaName; + private final String boatClassName; + private final String boatName; + private final String teamName; + private final String raceName; + private final String fleetName; + private final String sailNumber; + private final String name; + private final CountryCode countryCode; + private final List persons; + private final Double timeOnTimeFactor; + private final Duration timeOnDistanceAllowancePerNauticalMile; + + public CompetitorDescriptor(String eventName, String regattaName, String boatClassName, String raceName, + String fleetName, String sailNumber, String name, String teamName, String boatName, CountryCode countryCode, + Iterable persons, Double timeOnTimeFactor, Duration timeOnDistanceAllowancePerNauticalMile) { + super(name); + this.eventName = eventName; + this.regattaName = regattaName; + this.boatClassName = boatClassName; + this.boatName = boatName; + this.raceName = raceName; + this.fleetName = fleetName; + this.sailNumber = sailNumber; + this.name = name; + this.teamName = teamName; + this.countryCode = countryCode; + this.persons = new ArrayList<>(); + Util.addAll(persons, this.persons); + this.timeOnTimeFactor = timeOnTimeFactor; + this.timeOnDistanceAllowancePerNauticalMile = timeOnDistanceAllowancePerNauticalMile; + } + + public String getEventName() { + return eventName; + } + + public String getRegattaName() { + return regattaName; + } + + public String getBoatClassName() { + return boatClassName; + } + + public String getRaceName() { + return raceName; + } + + public String getFleetName() { + return fleetName; + } + + public String getSailNumber() { + return sailNumber; + } + + public String getName() { + return name; + } + + public CountryCode getCountryCode() { + return countryCode; + } + + public Iterable getPersons() { + return persons; + } + + public Double getTimeOnTimeFactor() { + return timeOnTimeFactor; + } + + public String getBoatName() { + return boatName; + } + + public String getTeamName() { + return teamName; + } + + public Duration getTimeOnDistanceAllowancePerNauticalMile() { + return timeOnDistanceAllowancePerNauticalMile; + } + + @Override + public String toString() { + return "CompetitorDescriptor [eventName=" + eventName + ", regattaName=" + regattaName + ", boatClassName=" + + boatClassName + ", boatName=" + boatName + ", teamName=" + teamName + ", raceName=" + raceName + + ", fleetName=" + fleetName + ", sailNumber=" + sailNumber + ", name=" + name + ", countryCode=" + + countryCode + ", persons=" + persons + ", timeOnTimeFactor=" + timeOnTimeFactor + + ", timeOnDistanceAllowancePerNauticalMile=" + timeOnDistanceAllowancePerNauticalMile + "]"; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java index b2fb909baec..f6e565fbbfc 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Position.java @@ -20,7 +20,10 @@ public interface Position extends Serializable { Distance getDistance(Position p); /** - * The bearing from this position towards p on a great circle + * The bearing from this position towards p on a great circle. + * + * @return {@code null} if {@code p} is {@code null}, otherwise the bearing from {@code this} {@link Position} to + * {@code p} along a great circle */ Bearing getBearingGreatCircle(Position p); diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/TargetTimeInfo.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/TargetTimeInfo.java new file mode 100755 index 00000000000..2c1af7b0cc1 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/TargetTimeInfo.java @@ -0,0 +1,92 @@ +package com.sap.sailing.domain.common; + +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; + +/** + * Information about the time competitors of a race are expected to need for the legs of a race. This is based on a + * {@link TrackedRace} with {@link TrackedLeg}s and + * {@link TrackedRace#getWind(com.sap.sailing.domain.common.Position, com.sap.sse.common.TimePoint) wind information}. + * Furthermore, a valid {@link PolarDataService} instance must be bound to the {@link TrackedRace} to make predictions + * possible. + *

+ * + * In order to give a rationale for the response, in addition to the total expected duration the expected durations for + * each leg are specified, together with the {@link Wind} data used for the leg, leg distance, the true wind angle for + * the leg's direction and the leg type that this leads to. + * + * @author Axel Uhl (d043530) + * + */ +public interface TargetTimeInfo { + public interface LegTargetTimeInfo { + /** + * The distance between the waypoints delimiting the leg, at the {@link #getExpectedStartTimePoint() expected + * time point} at which the competitor starts the leg. + */ + Distance getDistance(); + + /** + * True wind at the position in the middle between the two positions at which the waypoints delimiting + * the leg are at {@link #getExpectedStartTimePoint()}. + */ + Wind getWind(); + + /** + * The bearing from the leg's start waypoint to the leg's end waypoint at {@link #getExpectedStartTimePoint()}/ + */ + Bearing getLegBearing(); + + /** + * The angle between the wind direction (from) and the bearing from the leg's start waypoint position at + * {@link #getExpectedStartTimePoint()} and the leg's end waypoint position at + * {@link #getExpectedStartTimePoint()}. For example, if a windward mark is a few meters off to the left, + * viewed from the leeward start line, then the wind comes a little more from the starboard side compared + * to the course middle line, therefore would have a true wind angle to the leg of, say 5deg. + */ + Bearing getTrueWindAngleToLeg(); + + /** + * The time a competitor is expected to need to sail the leg in the {@link #getWind() wind conditions expected}. + */ + Duration getExpectedDuration(); + + /** + * The time point when the competitor is expected to enter this leg. For the race's first leg + * this equals the {@link TargetTimeInfo#getExpectedStartTimePoint()} result. + */ + TimePoint getExpectedStartTimePoint(); + + /** + * The type of the leg at {@link #getExpectedStartTimePoint()}, given the {@link #getWind() wind conditions} + * and the {@link #getLegBearing() leg bearing} at that point in time. + */ + LegType getLegType(); + } + + /** + * The time point when a competitor whose hypothetical race is described + * by this object starts. This time is the basis for determining wind angles + * as input for the polar / VPP calculations. Wind is taken on each leg + * during the expected time at which the competitor starts that leg. + */ + TimePoint getExpectedStartTimePoint(); + + /** + * The expected total duration; equals the sum of the {@link LegTargetTimeInfo#getExpectedDuration() leg durations} + * as aggregated across {@link #getLegTargetTimes()}. + */ + Duration getExpectedDuration(); + + /** + * The per-leg information about the expected hypothetical sail around the course. The element + * order matches that of the {@link Course#getLegs() legs in the race course}. + */ + Iterable getLegTargetTimes(); + + /** + * The {@link Duration#plus(Duration) sum} of {@link LegTargetTimeInfo#getExpectedDuration() expected leg durations} + * across all legs of type {@code spentInLegsOfType}. + */ + Duration getExpectedDuration(LegType spentInLegsOfType); +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BoatClassDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BoatClassDTO.java index b4e6fdc1cd4..02a33457d7b 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BoatClassDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BoatClassDTO.java @@ -14,24 +14,28 @@ public class BoatClassDTO extends NamedDTO implements Serializable, Comparable legInfos; + + public static class LegTargetTimeInfoImpl implements LegTargetTimeInfo { + private final Distance distance; + private final Wind wind; + private final Bearing legBearing; + private final Duration expectedDuration; + private final TimePoint expectedStartTimePoint; + private final LegType legType; + + public LegTargetTimeInfoImpl(Distance distance, Wind wind, Bearing legBearing, Duration expectedDuration, + TimePoint expectedStartTimePoint, LegType legType) { + super(); + this.distance = distance; + this.wind = wind; + this.legBearing = legBearing; + this.expectedDuration = expectedDuration; + this.expectedStartTimePoint = expectedStartTimePoint; + this.legType = legType; + } + + @Override + public Distance getDistance() { + return distance; + } + + @Override + public Wind getWind() { + return wind; + } + + @Override + public Bearing getLegBearing() { + return legBearing; + } + + @Override + public Bearing getTrueWindAngleToLeg() { + return getLegBearing().getDifferenceTo(getWind().getBearing().reverse()); + } + + @Override + public Duration getExpectedDuration() { + return expectedDuration; + } + + @Override + public TimePoint getExpectedStartTimePoint() { + return expectedStartTimePoint; + } + + @Override + public LegType getLegType() { + return legType; + } + + @Override + public String toString() { + return "LegTargetTimeInfoImpl [distance=" + distance + ", wind=" + wind + ", legBearing=" + legBearing + + ", expectedDuration=" + expectedDuration + ", expectedStartTimePoint=" + expectedStartTimePoint + + ", legType=" + legType + "]"; + } + } + + public TargetTimeInfoImpl(Iterable legInfos) { + super(); + this.legInfos = legInfos; + } + + @Override + public TimePoint getExpectedStartTimePoint() { + return Util.isEmpty(legInfos) ? null : legInfos.iterator().next().getExpectedStartTimePoint(); + } + + @Override + public Duration getExpectedDuration() { + Duration result = Duration.NULL; + for (final LegTargetTimeInfo legInfo : legInfos) { + result = result.plus(legInfo.getExpectedDuration()); + } + return result; + } + + @Override + public Iterable getLegTargetTimes() { + return legInfos; + } + + @Override + public Duration getExpectedDuration(final LegType spentInLegsOfType) { + Duration result = Duration.NULL; + for (final LegTargetTimeInfo legInfo : legInfos) { + if (legInfo.getLegType() == spentInLegsOfType) { + result = result.plus(legInfo.getExpectedDuration()); + } + } + return result; + } + + @Override + public String toString() { + return "TargetTimeInfoImpl [legInfos=" + legInfos + "]"; + } +} diff --git a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/websocket/WebSocketConnectionManager.java b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/websocket/WebSocketConnectionManager.java index fdfb8fce645..662cece766a 100755 --- a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/websocket/WebSocketConnectionManager.java +++ b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/websocket/WebSocketConnectionManager.java @@ -170,9 +170,7 @@ public class WebSocketConnectionManager implements LiveDataConnection { @Override public void onWebSocketText(String message) { - if (logger.isLoggable(Level.FINEST)) { - logger.finest("Received "+message+" in "+this); - } + logger.finest(()->"Received "+message+" in "+this); if (message.equals("1")) { logger.fine("Received server heartbeat for "+this); receivedServerHeartbeatInInterval = true; @@ -193,9 +191,7 @@ public class WebSocketConnectionManager implements LiveDataConnection { } } } - if (logger.isLoggable(Level.FINEST)) { - logger.finest("Received fixes"+fixes+" for "+this); - } + logger.finest(()->"Received fixes"+fixes+" for "+this); notifyListeners(fixes); } catch (ParseException e) { logger.log(Level.SEVERE, "Error trying to parse a web socket data package coming from Igtimi "+this, e); diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/DomainObjectFactory.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/DomainObjectFactory.java index 2278bf700a4..751457a70e2 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/DomainObjectFactory.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/DomainObjectFactory.java @@ -1,10 +1,13 @@ package com.sap.sailing.domain.persistence; +import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.util.Collection; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.function.Consumer; import com.mongodb.DBObject; import com.sap.sailing.domain.abstractlog.race.RaceLog; @@ -28,8 +31,14 @@ import com.sap.sailing.domain.leaderboard.LeaderboardGroupResolver; import com.sap.sailing.domain.leaderboard.LeaderboardRegistry; import com.sap.sailing.domain.racelog.RaceLogIdentifier; import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; +import com.sap.sailing.domain.tracking.RaceHandle; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRegatta; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.WindTrack; +import com.sap.sse.common.NoCorrespondingServiceRegisteredException; import com.sap.sse.common.Util.Pair; /** @@ -135,4 +144,33 @@ public interface DomainObjectFactory { Iterable> loadAllDeviceConfigurations(); Map> loadResultUrls(); + + /** + * Loads all {@link RaceTrackingConnectivityParameters} objects from the database telling how to re-load those races + * that were {@link MongoObjectFactory#addConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters) + * marked as to be restored} before the server got re-started. Callers that would like to use the result to + * re-connect to those races should take care not to flood servers with requests. Instead, ideally, callers would + * monitor the loading {@link TrackedRace#getStatus() status} or the resulting {@link TrackedRace}s (see + * {@link RaceTracker#getRaceHandle()}, {@link RaceHandle#getRace(long)}, {@link RaceHandle#getTrackedRegatta()} and + * {@link TrackedRegatta#getTrackedRace(RaceDefinition)}. + * + * @param callback + * invoked for each connectivity params object successfully resolved; this pattern is preferred over a + * non-{@code void} return type because resolving the connectivity parameters objects itself happens + * through a callback pattern that may be triggered asynchronously by the services for handling the + * respective parameter types becoming available only later. For example, during the OSGi startup phase, + * when the {@code RacingEventService} is launched, not all persistence bundles will have run their + * activators; some may even depend on the {@code RacingEventService} and therefore won't start their + * activators before the activator of {@code RacingEventService} has completed. This would either result + * in a {@link NoCorrespondingServiceRegisteredException} or would have to be handled by not treating + * those connectivity parameter objects which defeats the whole purpose of this method. + * @return the number of connectivity parameter objects found in the database; callers may use this to understand + * the progress with the {@code callback} invocations of which eventually there should be this many, at + * least if all handlers have ultimately become available. + * + * @see MongoObjectFactory#addConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters) + * @see MongoObjectFactory#removeConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters) + */ + int loadConnectivityParametersForRacesToRestore(Consumer callback) + throws MalformedURLException, URISyntaxException; } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/MongoObjectFactory.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/MongoObjectFactory.java index 71f6cd549e0..18638c6300f 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/MongoObjectFactory.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/MongoObjectFactory.java @@ -19,6 +19,7 @@ import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sailing.domain.leaderboard.LeaderboardGroup; import com.sap.sailing.domain.racelog.RaceLogIdentifier; import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRegatta; @@ -125,6 +126,12 @@ public interface MongoObjectFactory { */ void storeCompetitor(Competitor competitor); + /** + * Like {@link #storeCompetitor(Competitor)}, but for a collection of competitors that are all + * expected to be new, having a unique {@link Competitor#getId() ID}. + */ + void storeCompetitors(Iterable competitors); + void removeAllCompetitors(); void removeCompetitor(Competitor competitor); @@ -143,4 +150,31 @@ public interface MongoObjectFactory { void removeResultUrl(String resultProviderName, URL url); + + /** + * Updates the database such that the next call to + * {@link DomainObjectFactory#loadConnectivityParametersForRacesToRestore(Consumer)} won't return an object equivalent to + * {@code params} anymore; in other words, the race whose connectivity parameters are described by {@code params} + * will no longer be considered as to be restored. + * + * @see #addConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters) + */ + void removeConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters params); + + /** + * Updates the database such that the next call to + * {@link DomainObjectFactory#loadConnectivityParametersForRacesToRestore(Consumer)} will return an object equivalent to + * {@code params}; in other words, the race whose connectivity parameters are described by {@code params} will be + * considered as to be restored. + * + * @see #removeConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters) + */ + void addConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters params); + + /** + * Removes all {@link RaceTrackingConnectivityParameters} objects from those to restore; short for calling + * {@link #removeConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters)} for all parameter + * objects obtained through {@link DomainObjectFactory#loadConnectivityParametersForRacesToRestore(Consumer)}. + */ + void removeAllConnectivityParametersForRacesToRestore(); } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java index eeacdadee93..2e96c76cc15 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java @@ -2,6 +2,7 @@ package com.sap.sailing.domain.persistence.impl; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.common.tracking.GPSFix; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; /** * Defines literals providing the names for MongoDB collections. The literal documentation described the semantics @@ -100,4 +101,10 @@ public enum CollectionNames { * URLs providing race results */ RESULT_URLS, + + /** + * DB representations of {@link RaceTrackingConnectivityParameters} objects, serialized in a type-specific + * way by corresponding implementations of the RaceTrackingConnectivityParametersMongoHandler} interface. + */ + CONNECTIVITY_PARAMS_FOR_RACES_TO_BE_RESTORED, } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java index 1e4a99334b7..cdce9ed5653 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java @@ -2,6 +2,7 @@ package com.sap.sailing.domain.persistence.impl; import java.io.Serializable; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -14,6 +15,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -199,6 +201,8 @@ import com.sap.sailing.domain.ranking.RankingMetricConstructor; import com.sap.sailing.domain.ranking.RankingMetricsFactory; import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParametersHandler; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.impl.WindTrackImpl; @@ -235,8 +239,9 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { private final DB database; private final DomainFactory baseDomainFactory; - private final TypeBasedServiceFinder deviceIdentifierServiceFinder; private final TypeBasedServiceFinderFactory serviceFinderFactory; + private final TypeBasedServiceFinder deviceIdentifierServiceFinder; + private final TypeBasedServiceFinder raceTrackingConnectivityParamsServiceFinder; /** * Uses null as the {@link TypeBasedServiceFinder}, meaning that no {@link DeviceIdentifier}s can be loaded @@ -252,8 +257,10 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { if (serviceFinderFactory != null) { this.deviceIdentifierServiceFinder = serviceFinderFactory.createServiceFinder(DeviceIdentifierMongoHandler.class); this.deviceIdentifierServiceFinder.setFallbackService(new PlaceHolderDeviceIdentifierMongoHandler()); + this.raceTrackingConnectivityParamsServiceFinder = serviceFinderFactory.createServiceFinder(RaceTrackingConnectivityParametersHandler.class); } else { this.deviceIdentifierServiceFinder = null; + this.raceTrackingConnectivityParamsServiceFinder = null; } this.baseDomainFactory = baseDomainFactory; this.competitorDeserializer = CompetitorJsonDeserializer.create(baseDomainFactory); @@ -2276,4 +2283,29 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { } return modified; } + + @Override + public int loadConnectivityParametersForRacesToRestore(Consumer callback) throws MalformedURLException, URISyntaxException { + final DBCollection collection = database.getCollection(CollectionNames.CONNECTIVITY_PARAMS_FOR_RACES_TO_BE_RESTORED.name()); + int count = 0; + for (final DBObject o : collection.find()) { + count++; + final String type = (String) o.get(TypeBasedServiceFinder.TYPE); + raceTrackingConnectivityParamsServiceFinder.applyServiceWhenAvailable(type, connectivityParamsPersistenceService -> { + final Map map = new HashMap<>(); + for (final String key : o.keySet()) { + if (!key.equals(TypeBasedServiceFinder.TYPE)) { + map.put(key, o.get(key)); + } + } + try { + callback.accept(connectivityParamsPersistenceService.mapTo(map)); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception trying to load race restore connectivity parameters " + + o + " with handler " + connectivityParamsPersistenceService, e); + } + }); + } + return count; + } } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java index 2125d11ec08..54bbdb16a17 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java @@ -2,6 +2,7 @@ package com.sap.sailing.domain.persistence.impl; import java.io.Serializable; import java.net.URL; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -104,6 +105,8 @@ import com.sap.sailing.domain.persistence.racelog.tracking.impl.PlaceHolderDevic import com.sap.sailing.domain.racelog.RaceLogIdentifier; import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParametersHandler; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRegatta; import com.sap.sailing.domain.tracking.WindTrack; @@ -118,6 +121,7 @@ import com.sap.sse.common.TimeRange; import com.sap.sse.common.Timed; import com.sap.sse.common.TypeBasedServiceFinder; import com.sap.sse.common.TypeBasedServiceFinderFactory; +import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.shared.media.ImageDescriptor; import com.sap.sse.shared.media.VideoDescriptor; @@ -127,6 +131,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { private final DB database; private final CompetitorJsonSerializer competitorSerializer = CompetitorJsonSerializer.create(); private final TypeBasedServiceFinder deviceIdentifierServiceFinder; + private final TypeBasedServiceFinder raceTrackingConnectivityParamsServiceFinder; /** * Uses null for the device type service finder and hence will be unable to store device identifiers. @@ -142,8 +147,10 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { if (serviceFinderFactory != null) { this.deviceIdentifierServiceFinder = serviceFinderFactory.createServiceFinder(DeviceIdentifierMongoHandler.class); this.deviceIdentifierServiceFinder.setFallbackService(new PlaceHolderDeviceIdentifierMongoHandler()); + this.raceTrackingConnectivityParamsServiceFinder = serviceFinderFactory.createServiceFinder(RaceTrackingConnectivityParametersHandler.class); } else { this.deviceIdentifierServiceFinder = null; + this.raceTrackingConnectivityParamsServiceFinder = null; } } @@ -1245,6 +1252,20 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { collection.update(query, entry, /* upsrt */true, /* multi */false, WriteConcern.SAFE); } + @Override + public void storeCompetitors(Iterable competitors) { + if (competitors != null && !Util.isEmpty(competitors)) { + DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); + List competitorsDB = new ArrayList<>(); + for (Competitor competitor : competitors) { + JSONObject json = competitorSerializer.serialize(competitor); + DBObject entry = (DBObject) JSON.parse(json.toString()); + competitorsDB.add(entry); + } + collection.insert(competitorsDB); + } + } + @Override public void removeAllCompetitors() { logger.info("Removing all persistent competitor info"); @@ -1500,4 +1521,50 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { result.put(FieldNames.SAILORS_INFO_LOCALE.name(), locale != null ? locale.toLanguageTag() : null); return result; } + + @Override + public void removeConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters params) { + if (raceTrackingConnectivityParamsServiceFinder != null) { + final String typeIdentifier = params.getTypeIdentifier(); + final RaceTrackingConnectivityParametersHandler paramsPersistenceService = raceTrackingConnectivityParamsServiceFinder.findService(typeIdentifier); + if (paramsPersistenceService != null) { + final DBCollection collection = database.getCollection(CollectionNames.CONNECTIVITY_PARAMS_FOR_RACES_TO_BE_RESTORED.name()); + DBObject key = new BasicDBObject(); + key.putAll(paramsPersistenceService.getKey(params)); + collection.remove(key, WriteConcern.SAFE); + } else { + logger.warning("Couldn't find a persistence service for connectivity parameters of type "+typeIdentifier); + } + } else { + logger.warning("No connectivity parameters service finder set; unable to remove connectivity parameters from persistent store for " + + params + " of type " + params.getTypeIdentifier()); + } + } + + @Override + public void addConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters params) { + final String typeIdentifier = params.getTypeIdentifier(); + if (raceTrackingConnectivityParamsServiceFinder == null) { + logger.warning("No service finder has been configured to find connectivity parameter persistence services. Can't add connectivity parameters to DB for restore."); + } else { + try { + final RaceTrackingConnectivityParametersHandler paramsPersistenceService = raceTrackingConnectivityParamsServiceFinder.findService(typeIdentifier); + final DBCollection collection = database.getCollection(CollectionNames.CONNECTIVITY_PARAMS_FOR_RACES_TO_BE_RESTORED.name()); + DBObject key = new BasicDBObject(); + key.putAll(paramsPersistenceService.getKey(params)); + DBObject dbObject = new BasicDBObject(); + dbObject.putAll(paramsPersistenceService.mapFrom(params)); + collection.update(key, dbObject, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); + } catch (NoCorrespondingServiceRegisteredException e) { + logger.log(Level.WARNING, "Couldn't find a persistence service for connectivity parameters of type "+typeIdentifier+ + ". Couldn't store race "+params.getTrackerID()+" for restoring.", e); + } + } + } + + @Override + public void removeAllConnectivityParametersForRacesToRestore() { + database.getCollection(CollectionNames.CONNECTIVITY_PARAMS_FOR_RACES_TO_BE_RESTORED.name()).drop(); + } + } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java index cef4a6b47c7..fcd7855eda0 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java @@ -2,7 +2,9 @@ package com.sap.sailing.domain.racelogtracking.test; import static com.sap.sse.common.Util.size; import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import java.util.Collections; import java.util.UUID; import org.junit.After; @@ -11,14 +13,20 @@ import org.junit.Before; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogImpl; import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogDefineMarkEventImpl; import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogDeviceCompetitorMappingEventImpl; import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogDeviceMarkMappingEventImpl; import com.sap.sailing.domain.abstractlog.regatta.impl.RegattaLogImpl; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.DomainFactory; import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; @@ -26,12 +34,19 @@ import com.sap.sailing.domain.common.tracking.GPSFixMoving; import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; import com.sap.sailing.domain.persistence.racelog.tracking.impl.MongoSensorFixStoreImpl; +import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; import com.sap.sailing.domain.racelog.tracking.SensorFixStore; import com.sap.sailing.domain.racelog.tracking.test.mock.MockDeviceAndSessionIdentifierWithGPSFixesDeserializer; import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory; import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.Track; +import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; +import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl; +import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.gateway.deserialization.impl.DeviceAndSessionIdentifierWithGPSFixesDeserializer; import com.sap.sailing.server.impl.RacingEventServiceImpl; @@ -61,13 +76,17 @@ public class AbstractGPSFixStoreTest { service = new RacingEventServiceImpl(null, null, serviceFinderFactory); raceLog = new RaceLogImpl("racelog"); regattaLog = new RegattaLogImpl("regattalog"); - + dropPersistedData(); store = new MongoSensorFixStoreImpl(service.getMongoObjectFactory(), service.getDomainObjectFactory(), serviceFinderFactory); } @After public void after() { + dropPersistedData(); + } + + private void dropPersistedData() { MongoObjectFactoryImpl mongoOF = (MongoObjectFactoryImpl) service.getMongoObjectFactory(); mongoOF.getGPSFixCollection().drop(); mongoOF.getGPSFixMetadataCollection().drop(); @@ -76,8 +95,9 @@ public class AbstractGPSFixStoreTest { } protected void map(RegattaLog regattaLog, Competitor comp, DeviceIdentifier device, long from, long to) { - regattaLog.add(new RegattaLogDeviceCompetitorMappingEventImpl(MillisecondsTimePoint.now(), MillisecondsTimePoint.now(), author, UUID.randomUUID(), - comp, device, new MillisecondsTimePoint(from), new MillisecondsTimePoint(to))); + regattaLog.add(new RegattaLogDeviceCompetitorMappingEventImpl(MillisecondsTimePoint.now(), + MillisecondsTimePoint.now(), author, UUID.randomUUID(), comp, device, new MillisecondsTimePoint(from), + new MillisecondsTimePoint(to))); } protected void map(Competitor comp, DeviceIdentifier device, long from, long to) { @@ -85,8 +105,25 @@ public class AbstractGPSFixStoreTest { } protected void map(Mark mark, DeviceIdentifier device, long from, long to) { - regattaLog.add(new RegattaLogDeviceMarkMappingEventImpl(MillisecondsTimePoint.now(), MillisecondsTimePoint.now(), author, UUID.randomUUID(), - mark, device, new MillisecondsTimePoint(from), new MillisecondsTimePoint(to))); + regattaLog.add(new RegattaLogDeviceMarkMappingEventImpl(MillisecondsTimePoint.now(), + MillisecondsTimePoint.now(), author, UUID.randomUUID(), mark, device, new MillisecondsTimePoint(from), + new MillisecondsTimePoint(to))); + } + + protected void defineMarksOnRegattaLog(Mark... marks) { + for (int i = 0; i < marks.length; i++) { + regattaLog.add(new RegattaLogDefineMarkEventImpl(new MillisecondsTimePoint(i + 1), author, + new MillisecondsTimePoint(1), 0, marks[i])); + } + } + + protected DynamicTrackedRaceImpl createDynamicTrackedRace(BoatClass boatClass, RaceDefinition raceDefinition) { + DynamicTrackedRegatta regatta = new DynamicTrackedRegattaImpl(new RegattaImpl(EmptyRaceLogStore.INSTANCE, + EmptyRegattaLogStore.INSTANCE, RegattaImpl.getDefaultName("regatta", boatClass.getName()), boatClass, + /* startDate */ null, /* endDate */null, null, null, "a", null)); + return new DynamicTrackedRaceImpl(regatta, raceDefinition, Collections. emptyList(), + EmptyWindStore.INSTANCE, 0, 0, 0, /* useMarkPassingCalculator */ false, OneDesignRankingMetric::new, + mock(RaceLogResolver.class)); } protected void testNumberOfRawFixes(Track track, long expected) { diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java index ff6a03d662b..81bf91073eb 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java @@ -78,7 +78,7 @@ public class CreateAndTrackWithRaceLogTest { @Before public void setup() { - service = new RacingEventServiceImpl(true, new MockSmartphoneImeiServiceFinderFactory()); + service = new RacingEventServiceImpl(/* clearPersistentCompetitorStore */ true, new MockSmartphoneImeiServiceFinderFactory(), /* restoreTrackedRaces */ false); sensorFixStore = service.getSensorFixStore(); service.getMongoObjectFactory().getDatabase().dropDatabase(); author = service.getServerAuthor(); @@ -113,7 +113,7 @@ public class CreateAndTrackWithRaceLogTest { Exception { RaceColumn column = leaderboard.getRaceColumnByName(columnName); exception.expect(NotDenotedForRaceLogTrackingException.class); - adapter.startTracking(service, leaderboard, column, fleet); + adapter.startTracking(service, leaderboard, column, fleet, /* trackWind */ false, /* correctWindDirectionByMagneticDeclination */ false); } private void testSize(Track track, int expected) { @@ -184,7 +184,7 @@ public class CreateAndTrackWithRaceLogTest { raceLog.add(new RaceLogRegisterCompetitorEventImpl(t(), author, 0, comp1)); raceLog.add(new RaceLogStartOfTrackingEventImpl(t(0), author, /* passId */ 0)); // start tracking - adapter.startTracking(service, leaderboard, column, fleet); + adapter.startTracking(service, leaderboard, column, fleet, /* trackWind */ false, /* correctWindDirectionByMagneticDeclination */ false); // now there is a tracked race @@ -226,7 +226,7 @@ public class CreateAndTrackWithRaceLogTest { raceLog.add(new RaceLogStartOfTrackingEventImpl(t(0), author, /* passId */ 0)); // start tracking - adapter.startTracking(service, leaderboard, column, fleet); + adapter.startTracking(service, leaderboard, column, fleet, /* trackWind */ false, /* correctWindDirectionByMagneticDeclination */ false); // now there is a trackedrace TrackedRace race = column.getTrackedRace(fleet); diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/RaceLogFixTrackerManagerTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/RaceLogFixTrackerManagerTest.java new file mode 100644 index 00000000000..bddfc87bf0b --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/RaceLogFixTrackerManagerTest.java @@ -0,0 +1,145 @@ +package com.sap.sailing.domain.racelogtracking.test.impl; + +import static org.mockito.Mockito.mock; + +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collections; +import java.util.ConcurrentModificationException; + +import org.junit.Before; +import org.junit.Test; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; +import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; +import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogImpl; +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMappingEvent; +import com.sap.sailing.domain.abstractlog.regatta.impl.RegattaLogImpl; +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.CourseImpl; +import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.WaypointImpl; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.domain.persistence.PersistenceFactory; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.MongoSensorFixStoreImpl; +import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.racelog.tracking.EmptySensorFixStore; +import com.sap.sailing.domain.racelog.tracking.SensorFixMapper; +import com.sap.sailing.domain.racelog.tracking.SensorFixStore; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.domain.racelogsensortracking.SensorFixMapperFactory; +import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; +import com.sap.sailing.domain.racelogtracking.impl.fixtracker.RaceLogFixTrackerManager; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; +import com.sap.sailing.domain.tracking.DynamicTrack; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; +import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl; +import com.sap.sailing.domain.tracking.impl.EmptyWindStore; +import com.sap.sse.common.Timed; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class RaceLogFixTrackerManagerTest { + protected final MockSmartphoneImeiServiceFinderFactory serviceFinderFactory = new MockSmartphoneImeiServiceFinderFactory(); + protected final DeviceIdentifier device = new SmartphoneImeiIdentifier("a"); + protected final DeviceIdentifier deviceTest = new SmartphoneImeiIdentifier("b"); + protected RaceLog raceLog; + protected RaceLog raceLog2; + protected RegattaLog regattaLog; + protected SensorFixStore store; + protected final Competitor comp = DomainFactory.INSTANCE.getOrCreateCompetitor("comp", "comp", null, null, null, + null, null, /* timeOnTimeFactor */ null, /* timeOnDistanceAllowanceInSecondsPerNauticalMile */ null, null); + protected final Mark mark = DomainFactory.INSTANCE.getOrCreateMark("mark"); + protected final Mark mark2 = DomainFactory.INSTANCE.getOrCreateMark("mark2"); + private final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("49er"); + + protected final AbstractLogEventAuthor author = new LogEventAuthorImpl("author", 0); + private DynamicTrackedRace trackedRace; + + protected GPSFixMoving createFix(long millis, double lat, double lng, double knots, double degrees) { + return new GPSFixMovingImpl(new DegreePosition(lat, lng), new MillisecondsTimePoint(millis), + new KnotSpeedWithBearingImpl(knots, new DegreeBearingImpl(degrees))); + } + + @Before + public void setUp() throws UnknownHostException, MongoException { + raceLog = new RaceLogImpl("racelog"); + raceLog2 = new RaceLogImpl("racelog2"); + + regattaLog = new RegattaLogImpl("regattalog"); + + store = new MongoSensorFixStoreImpl(PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(), + PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory(), serviceFinderFactory); + + Course course = new CourseImpl("course", + Arrays.asList(new Waypoint[] { new WaypointImpl(mark), new WaypointImpl(mark2) })); + RaceDefinition race = new RaceDefinitionImpl("race", course, boatClass, Arrays.asList(comp)); + DynamicTrackedRegatta regatta = new DynamicTrackedRegattaImpl(new RegattaImpl(EmptyRaceLogStore.INSTANCE, + EmptyRegattaLogStore.INSTANCE, RegattaImpl.getDefaultName("regatta", boatClass.getName()), boatClass, + /* startDate */ null, /* endDate */null, null, null, "a", null)); + trackedRace = new DynamicTrackedRaceImpl(regatta, race, Collections. emptyList(), + EmptyWindStore.INSTANCE, 0, 0, 0, /* useMarkPassingCalculator */ false, OneDesignRankingMetric::new, + mock(RaceLogResolver.class)); + } + + /** + * Bug 4001: A {@link ConcurrentModificationException} was thrown when stopping tracking for a race with multiple + * attached RaceLogs. + */ + @Test + public void testThatNoExceptionIsThrownWhenStoppingTrackingForRaceWithMultipleRaceLogs_bug4001() { + trackedRace.attachRegattaLog(regattaLog); + trackedRace.attachRaceLog(raceLog); + trackedRace.attachRaceLog(raceLog2); + + RaceLogFixTrackerManager raceLogFixTrackerManager = new RaceLogFixTrackerManager(trackedRace, + EmptySensorFixStore.INSTANCE, new SensorFixMapperFactory() { + @Override + public > SensorFixMapper createCompetitorMapper( + Class> eventType) { + throw new IllegalArgumentException("Unknown event type"); + } + }); + raceLogFixTrackerManager.stop(false); + } + + /** + * Bug 4001: A {@link ConcurrentModificationException} was thrown when stopping tracking for a race with multiple + * attached RaceLogs. + */ + @Test + public void testThatNoExceptionIsThrownWhenStoppingTrackingWhenAddingSecondRaceLogWhileAlreadyTracking_bug4001() { + trackedRace.attachRegattaLog(regattaLog); + trackedRace.attachRaceLog(raceLog); + + RaceLogFixTrackerManager raceLogFixTrackerManager = new RaceLogFixTrackerManager(trackedRace, + EmptySensorFixStore.INSTANCE, new SensorFixMapperFactory() { + @Override + public > SensorFixMapper createCompetitorMapper( + Class> eventType) { + throw new IllegalArgumentException("Unknown event type"); + } + }); + trackedRace.attachRaceLog(raceLog2); + raceLogFixTrackerManager.stop(false); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java index 044b3b0c676..872aa882636 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java @@ -1,50 +1,50 @@ package com.sap.sailing.domain.racelogtracking.test.impl; import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.mock; import java.util.Arrays; import java.util.Collections; import java.util.Map; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; -import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; -import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogDefineMarkEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartOfTrackingEventImpl; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Course; import com.sap.sailing.domain.base.DomainFactory; import com.sap.sailing.domain.base.Mark; import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Sideline; -import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.base.impl.CourseImpl; import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.base.impl.WaypointImpl; import com.sap.sailing.domain.common.racelog.tracking.TransformationException; -import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; import com.sap.sailing.domain.racelogtracking.impl.fixtracker.FixLoaderAndTracker; import com.sap.sailing.domain.racelogtracking.test.AbstractGPSFixStoreTest; -import com.sap.sailing.domain.ranking.OneDesignRankingMetric; -import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; import com.sap.sailing.domain.tracking.DynamicTrackedRace; -import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; -import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl; -import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sse.common.NoCorrespondingServiceRegisteredException; import com.sap.sse.common.Timed; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.TimeRangeImpl; public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest { + private static final Logger logger = Logger.getLogger(TrackedRaceLoadsFixesTest.class.getName()); private final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("49er"); + + private final Mark mark2 = DomainFactory.INSTANCE.getOrCreateMark("mark2"); + private final Competitor comp2 = DomainFactory.INSTANCE.getOrCreateCompetitor("comp2", "comp2", null, null, null, + null, null, /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null); + private final Course course = new CourseImpl("course", Arrays.asList(new WaypointImpl(mark), new WaypointImpl(mark2))); + private final RaceDefinition raceDefinition = new RaceDefinitionImpl("race", course, boatClass, Arrays.asList(comp, comp2)); @Rule public Timeout TrackedRaceLoadsFixesTestTimeout = new Timeout(3 * 60 * 1000); @@ -52,15 +52,8 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest { @Test public void doesRaceLoadOnlyBetweenStartAndEndOfTracking() throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { - Competitor comp2 = DomainFactory.INSTANCE.getOrCreateCompetitor("comp2", "comp2", null, null, null, null, null, - /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null); - Mark mark2 = DomainFactory.INSTANCE.getOrCreateMark("mark2"); DeviceIdentifier markDevice = new SmartphoneImeiIdentifier("imei2"); - regattaLog.add(new RegattaLogDefineMarkEventImpl(new MillisecondsTimePoint(1), author, new MillisecondsTimePoint(1), 0, mark)); - regattaLog.add(new RegattaLogDefineMarkEventImpl(new MillisecondsTimePoint(2), author, new MillisecondsTimePoint(1), 0, mark2)); - Course course = new CourseImpl("course", Arrays.asList(new Waypoint[] { new WaypointImpl(mark), - new WaypointImpl(mark2) })); - RaceDefinition race = new RaceDefinitionImpl("race", course, boatClass, Arrays.asList(new Competitor[] { comp, comp2 })); + defineMarksOnRegattaLog(mark, mark2); map(comp, device, 0, 10000); map(mark, markDevice, 0, 10000); @@ -71,13 +64,7 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest { store.storeFix(markDevice, createFix(1100, 10, 20, 30, 40)); store.storeFix(markDevice, createFix(2100, 10, 20, 30, 40)); - DynamicTrackedRegatta regatta = new DynamicTrackedRegattaImpl(new RegattaImpl(EmptyRaceLogStore.INSTANCE, - EmptyRegattaLogStore.INSTANCE, RegattaImpl.getDefaultName("regatta", boatClass.getName()), boatClass, /* startDate */ - null, /* endDate */null, null, null, "a", null)); - final DynamicTrackedRaceImpl trackedRace = new DynamicTrackedRaceImpl(regatta, race, - Collections. emptyList(), EmptyWindStore.INSTANCE, 0, 0, 0, - /* useMarkPassingCalculator */ false, - OneDesignRankingMetric::new, mock(RaceLogResolver.class)); + final DynamicTrackedRaceImpl trackedRace = createDynamicTrackedRace(boatClass, raceDefinition); trackedRace.setStartOfTrackingReceived(new MillisecondsTimePoint(1000)); trackedRace.setEndOfTrackingReceived(new MillisecondsTimePoint(2000)); new FixLoaderAndTracker(trackedRace, store, null); @@ -100,16 +87,9 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest { @Test public void areFixesStoredInDb() throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { - Competitor comp2 = DomainFactory.INSTANCE.getOrCreateCompetitor("comp2", "comp2", null, null, null, null, null, - /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null); - Mark mark2 = DomainFactory.INSTANCE.getOrCreateMark("mark2"); - regattaLog.add(new RegattaLogDefineMarkEventImpl(new MillisecondsTimePoint(1), author, new MillisecondsTimePoint(1), 0, mark)); - regattaLog.add(new RegattaLogDefineMarkEventImpl(new MillisecondsTimePoint(2), author, new MillisecondsTimePoint(1), 0, mark2)); + defineMarksOnRegattaLog(mark, mark2); DeviceIdentifier device2 = new SmartphoneImeiIdentifier("imei2"); DeviceIdentifier device3 = new SmartphoneImeiIdentifier("imei3"); - Course course = new CourseImpl("course", Arrays.asList(new Waypoint[] { new WaypointImpl(mark), - new WaypointImpl(mark2) })); - RaceDefinition race = new RaceDefinitionImpl("race", course, boatClass, Arrays.asList(comp, comp2)); map(comp, device, 0, 20000); map(comp2, device2, 0, 600); @@ -127,12 +107,7 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest { store.storeFix(device, createFix(i + 1000, 10, 20, 30, 40)); } - DynamicTrackedRegatta regatta = new DynamicTrackedRegattaImpl(new RegattaImpl(EmptyRaceLogStore.INSTANCE, - EmptyRegattaLogStore.INSTANCE, RegattaImpl.getDefaultName("regatta", boatClass.getName()), boatClass, - /* startDate */ null, /* endDate */null, null, null, "a", null)); - DynamicTrackedRace trackedRace = new DynamicTrackedRaceImpl(regatta, race, - Collections. emptyList(), EmptyWindStore.INSTANCE, 0, 0, 0, - /*useMarkPassingCalculator*/ false, OneDesignRankingMetric::new, mock(RaceLogResolver.class)); + DynamicTrackedRace trackedRace = createDynamicTrackedRace(boatClass, raceDefinition); new FixLoaderAndTracker(trackedRace, store, null); @@ -145,17 +120,228 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest { testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 1); testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark2), 1); } + + @Test + public void testFixesForCompetitorsAreNotLoadedIfMappingDoesNotIntersectWithTrackingInterval() + throws InterruptedException { + testFixes(/* start of tracking */ 200, /* end of tracking */ 300, /* mappings and fixes */ () -> { + map(comp, device, 0, 100); + store.storeFix(device, createFix(10, 10, 20, 30, 40)); + store.storeFix(device, createFix(20, 10, 20, 30, 40)); + }, /* tests and expectations */ trackedRace -> testNumberOfRawFixes(trackedRace.getTrack(comp), 0)); + } + + @Test + public void testFixesForCompetitorsAreLoadedIfMappingDoesIntersectWithTrackingIntervalFixesWithinIntersectionOnly() + throws InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings and fixes */ () -> { + map(comp, device, 0, 200); + store.storeFix(device, createFix(50, 10, 20, 30, 40)); + store.storeFix(device, createFix(150, 10, 20, 30, 40)); + store.storeFix(device, createFix(250, 10, 20, 30, 40)); + map(comp, device, 350, 500); + store.storeFix(device, createFix(400, 10, 20, 30, 40)); + store.storeFix(device, createFix(450, 10, 20, 30, 40)); + }, /* tests and expectations */ trackedRace -> testNumberOfRawFixes(trackedRace.getTrack(comp), 1)); + } + @Test + public void testAddingMappingLoadsFixesPreviouslyNotCovered() throws InterruptedException { + testFixes(/* start of tracking */ 10, /* end of tracking */ 500, /* mappings and fixes */ () -> { + map(comp, device, 0, 200); + map(comp, device, 300, 400); + store.storeFix(device, createFix(50, 10, 20, 30, 40)); // in first mapping + store.storeFix(device, createFix(150, 10, 20, 30, 40)); // in first mapping + store.storeFix(device, createFix(250, 10, 20, 30, 40)); // outside both mappings + store.storeFix(device, createFix(350, 10, 20, 30, 40)); // in second mapping + }, /* tests and expectations */ trackedRace -> { + testNumberOfRawFixes(trackedRace.getTrack(comp), 3); + map(comp, device, 0, 500); // covers fix at 250ms; assert that only that fix is loaded + try { + trackedRace.waitForLoadingToFinish(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception waiting for loading to finish", e); + } + testNumberOfRawFixes(trackedRace.getTrack(comp), 4); + }); + } + + @Test + public void testAddingMappingDoesNotLoadFixesAlreadyCoveredByExistingMapping() throws InterruptedException { + testFixes(/* start of tracking */ 10, /* end of tracking */ 500, /* mappings and fixes */ () -> { + map(comp, device, 0, 200); + map(comp, device, 300, 400); + store.storeFix(device, createFix(50, 10, 20, 30, 40)); // in first mapping + store.storeFix(device, createFix(150, 10, 20, 30, 40)); // in first mapping + store.storeFix(device, createFix(250, 10, 20, 30, 40)); // outside both mappings + store.storeFix(device, createFix(350, 10, 20, 30, 40)); // in second mapping + }, /* tests and expectations */ trackedRace -> { + map(comp, device, 0, 500); // covers fix at 250ms + try { + trackedRace.waitForLoadingToFinish(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception waiting for loading to finish", e); + } + store.storeFix(device, createFix(400, 10, 20, 30, 40)); // in third mapping + testNumberOfRawFixes(trackedRace.getTrack(comp), 5); + }); + } + + /** Regression test for bug 4008 - https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4008 */ + @Test + public void testFixesForMarkAreLoadedIfMappingDoesNotIntersectWithTrackingInterval() throws InterruptedException { + testFixes(/* start of tracking */ 200, /* end of tracking */ 300, /* mappings and fixes */ () -> { + map(mark, device, 0, 100); + store.storeFix(device, createFix(10, 10, 20, 30, 40)); + store.storeFix(device, createFix(20, 10, 20, 30, 40)); + }, /* tests and expectations */ trackedRace -> testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 2)); + } + + /** Regression test for bug 4008 - https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4008 */ + @Test + public void testFixesForTwoMarksAreLoadedIfMappingsDoNotIntersectWithTrackingInterval() + throws InterruptedException { + testFixes(/* start of tracking */ 400, /* end of tracking */ 500, /* mappings and fixes */ () -> { + map(mark, device, 0, 100); + store.storeFix(device, createFix(50, 10, 20, 30, 40)); + map(mark2, device, 200, 300); + store.storeFix(device, createFix(250, 10, 20, 30, 40)); + }, /* tests and expectations */ trackedRace -> { + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 1); + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark2), 1); + }); + } + + /** Regression test for bug 4008 - https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4008 */ + @Test + public void testFixesForMarkAreLoadedIfMappingDoesIntersectWithTrackingIntervalFixesWithinIntersectionOnly() + throws InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings and fixes */ () -> { + map(mark, device, 0, 200); + store.storeFix(device, createFix(50, 10, 20, 30, 40)); + store.storeFix(device, createFix(150, 10, 20, 30, 40)); + store.storeFix(device, createFix(250, 10, 20, 30, 40)); + + map(mark, device, 350, 500); + store.storeFix(device, createFix(400, 10, 20, 30, 40)); + store.storeFix(device, createFix(450, 10, 20, 30, 40)); + }, /* tests and expectations */ trackedRace -> { + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 1); + }); + } + + /** Regression test for bug 4008 - https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4008 */ + @Test + public void testFixesForMarkAreLoadedIfMappingDoesIntersectWithTrackingIntervalAllFixesBecauseOfNoFixesWithIntersection() + throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings and fixes */ () -> { + map(mark, device, 0, 200); + store.storeFix(device, createFix(50, 10, 20, 30, 40)); + store.storeFix(device, createFix(250, 10, 20, 30, 40)); + map(mark, device, 350, 500); + store.storeFix(device, createFix(400, 10, 20, 30, 40)); + store.storeFix(device, createFix(450, 10, 20, 30, 40)); + }, /* tests and expectations */ trackedRace -> { + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 3); + }); + } + + @Test + public void testFixForMarkIsAddedIfBeforeTrackingIntervalAndNoOtherFixExists() + throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings */ () -> map(mark, device, 0, 200), + /* fixes and tests/expectations */ trackedRace -> { + store.storeFix(device, createFix(50, 10, 20, 30, 40)); + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 1); + }); + } + + @Test + public void testFixForMarkIsAddedIfAfterTrackingIntervalAndNoOtherFixExists() + throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings */ () -> map(mark, device, 0, 400), /* fixes and tests/expectations */ trackedRace -> { + store.storeFix(device, createFix(350, 10, 20, 30, 40)); + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 1); + }); + } + + @Test + public void testFixForMarkIsAddedIfBeforeTrackingIntervalAndBetterThanAnExistingOne() + throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings */ () -> map(mark, device, 0, 400), + /* fixes and tests/expectations */ trackedRace -> { + store.storeFix(device, createFix(50, 10, 20, 30, 40)); + store.storeFix(device, createFix(75, 10, 20, 30, 40)); + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 2); + }); + } + + @Test + public void testFixForMarkIsNotAddedIfBeforeTrackingIntervalAndWorseThanAnExistingOne() + throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings */ () -> map(mark, device, 0, 400), + /* fixes and tests/expectations */ trackedRace -> { + store.storeFix(device, createFix(75, 10, 20, 30, 40)); + store.storeFix(device, createFix(50, 10, 20, 30, 40)); + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 1); + }); + } + + @Test + public void testFixForMarkIsAddedIfAfterTrackingIntervalAndBetterThanAnExistingOne() + throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings */ () -> map(mark, device, 0, 400), + /* fixes and tests/expectations */ trackedRace -> { + store.storeFix(device, createFix(375, 10, 20, 30, 40)); + store.storeFix(device, createFix(350, 10, 20, 30, 40)); + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 2); + }); + } + + @Test + public void testFixForMarkIsNotAddedIfAfterTrackingIntervalAndWorseThanAnExistingOne() + throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings */ () -> map(mark, device, 0, 400), + /* fixes and tests/expectations */ trackedRace -> { + store.storeFix(device, createFix(350, 10, 20, 30, 40)); + store.storeFix(device, createFix(375, 10, 20, 30, 40)); + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 1); + }); + } + + @Test + public void testFixForMarkIsNotAddedIfNotWithinTrackingIntervalAndFixWithinTrackingIntervalExists() + throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { + testFixes(/* start of tracking */ 100, /* end of tracking */ 300, /* mappings */ () -> map(mark, device, 0, 400), + /* fixes and tests/expectations */ trackedRace -> { + store.storeFix(device, createFix(200, 10, 20, 30, 40)); + store.storeFix(device, createFix(50, 10, 20, 30, 40)); + store.storeFix(device, createFix(350, 10, 20, 30, 40)); + testNumberOfRawFixes(trackedRace.getOrCreateTrack(mark), 1); + }); + } + + private void testFixes(long startOfTracking, long endOfTracking, Runnable beforeTrackingStarted, + Consumer afterTrackingStarted) throws InterruptedException { + defineMarksOnRegattaLog(mark, mark2); + raceLog.add(new RaceLogStartOfTrackingEventImpl(new MillisecondsTimePoint(startOfTracking), author, 0)); + raceLog.add(new RaceLogEndOfTrackingEventImpl(new MillisecondsTimePoint(endOfTracking), author, 0)); + beforeTrackingStarted.run(); + DynamicTrackedRace trackedRace = createDynamicTrackedRace(boatClass, raceDefinition); + trackedRace.attachRaceLog(raceLog); + trackedRace.attachRegattaLog(regattaLog); + new FixLoaderAndTracker(trackedRace, store, null); + trackedRace.waitForLoadingToFinish(); + afterTrackingStarted.accept(trackedRace); + } + @Test public void metadataStoredInDb() throws TransformationException, NoCorrespondingServiceRegisteredException { assertEquals(0, store.getNumberOfFixes(device)); assertEquals(null, store.getTimeRangeCoveredByFixes(device)); - map(comp, device, 0, 600); - store.storeFix(device, createFix(100, 10, 20, 30, 40)); store.storeFix(device, createFix(200, 10, 20, 30, 40)); - assertEquals(2, store.getNumberOfFixes(device)); assertEquals(TimeRangeImpl.create(100, 200), store.getTimeRangeCoveredByFixes(device)); } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/AbstractTypeBasedServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/AbstractTypeBasedServiceFinder.java new file mode 100644 index 00000000000..05f76a3f1f4 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/AbstractTypeBasedServiceFinder.java @@ -0,0 +1,11 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sse.common.TypeBasedServiceFinder; + +public abstract class AbstractTypeBasedServiceFinder implements TypeBasedServiceFinder { + @Override + public void applyServiceWhenAvailable(String type, com.sap.sse.common.TypeBasedServiceFinder.Callback callback) { + final ServiceT service = findService(type); + callback.withService(service); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinder.java index 354a654036f..b3d626619a8 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinder.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinder.java @@ -4,9 +4,8 @@ import java.util.Collections; import java.util.Set; import com.sap.sse.common.NoCorrespondingServiceRegisteredException; -import com.sap.sse.common.TypeBasedServiceFinder; -public class MockEmptyServiceFinder implements TypeBasedServiceFinder { +public class MockEmptyServiceFinder extends AbstractTypeBasedServiceFinder { private ServiceType fallback; @Override diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java index 587b128c16d..241b5fff5f3 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java @@ -15,9 +15,8 @@ import com.sap.sailing.domain.persistence.racelog.tracking.FixMongoHandler; import com.sap.sailing.domain.persistence.racelog.tracking.impl.DoubleVectorFixMongoHandlerImpl; import com.sap.sailing.domain.persistence.racelog.tracking.impl.GPSFixMongoHandlerImpl; import com.sap.sailing.domain.persistence.racelog.tracking.impl.GPSFixMovingMongoHandlerImpl; -import com.sap.sse.common.TypeBasedServiceFinder; -public class MockGPSFixMongoServiceFinder implements TypeBasedServiceFinder> { +public class MockGPSFixMongoServiceFinder extends AbstractTypeBasedServiceFinder> { private final MongoObjectFactoryImpl mof = new MongoObjectFactoryImpl(null); private final DomainObjectFactoryImpl dof = new DomainObjectFactoryImpl(null, null); private final FixMongoHandler movingHandler = new GPSFixMovingMongoHandlerImpl(mof, dof); diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockServiceFinder.java index 94a016826a8..88aa2ff2ce3 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockServiceFinder.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockServiceFinder.java @@ -3,9 +3,7 @@ package com.sap.sailing.domain.racelog.tracking.test.mock; import java.util.Collections; import java.util.Set; -import com.sap.sse.common.TypeBasedServiceFinder; - -public class MockServiceFinder implements TypeBasedServiceFinder { +public class MockServiceFinder extends AbstractTypeBasedServiceFinder { private final T handler; private T fallback; diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.racelogtrackingadapter/META-INF/MANIFEST.MF index bf0d461cbab..e478d0a6104 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/META-INF/MANIFEST.MF @@ -32,5 +32,5 @@ Import-Package: com.google.zxing;version="3.1.0", org.osgi.framework;version="1.6.0", org.osgi.util.tracker;version="1.5.1" Export-Package: com.sap.sailing.domain.racelogtracking, - com.sap.sailing.domain.racelogtracking.impl;x-friends:="com.sap.sailing.domain.racelogtrackingadapter.testsupport,com.sap.sailing.gwt.ui.test" + com.sap.sailing.domain.racelogtracking.impl;x-friends:="com.sap.sailing.domain.racelogtrackingadapter.testsupport,com.sap.sailing.gwt.ui.test,com.sap.sailing.mongodb.test" Bundle-Activator: com.sap.sailing.domain.racelogtracking.impl.Activator diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/TrackingStringMessages_zh.properties b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/TrackingStringMessages_zh.properties index 54b457fbd21..6cbdd73bdfc 100755 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/TrackingStringMessages_zh.properties +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/TrackingStringMessages_zh.properties @@ -5,7 +5,7 @@ iOSUsers=启动 iOS 应用 androidUsers=启动 Android 应用 alternativelyVisitThisLink=如果在已安装该应用的设备中读取邮件,只需点击以下链接即可启动: buoyTender=浮标供应船 -appStoreInstallText=如果尚无该应用,请安装: +appStoreInstallText=如果您尚无该应用,请安装: appIos=安装 iOS 应用 appAndroid=安装 Android 应用 buoyPingerAppName=SAP Sailing Buoy Pinger diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/TrackingStringMessages_zh_CN.properties b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/TrackingStringMessages_zh_CN.properties index 54b457fbd21..6cbdd73bdfc 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/TrackingStringMessages_zh_CN.properties +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/TrackingStringMessages_zh_CN.properties @@ -5,7 +5,7 @@ iOSUsers=启动 iOS 应用 androidUsers=启动 Android 应用 alternativelyVisitThisLink=如果在已安装该应用的设备中读取邮件,只需点击以下链接即可启动: buoyTender=浮标供应船 -appStoreInstallText=如果尚无该应用,请安装: +appStoreInstallText=如果您尚无该应用,请安装: appIos=安装 iOS 应用 appAndroid=安装 Android 应用 buoyPingerAppName=SAP Sailing Buoy Pinger diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages.properties.xlf b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages.properties.xlf index 593b1894376..909e04c6336 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages.properties.xlf +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages.properties.xlf @@ -1,12 +1,12 @@ - +

- H4sIAAAAAAAAAF2RTU/DMAyG7/0VvnGZEGekHgYSUiVgY+3gnLXeajWNo9jbqBD/naQtbOLm+PXH+zgaTN2ROxTuRGqU2D1xyKs5C5c07DlkUhv3tnnkBlfhnYR0G2zFGzyQKIal5C+mQ5BjQBj4CK05IWiL8HX3DUvvgZyosRabW6hadJDmjQV7tpbPaeM0HjhAbanuRtWS6+AoSY5jA0hvgvqWXZzOEOb1YCQ7o625x4rzjymKBRmtyq1gkLzU2AfxmcxkxjWBqbmWllNqkm2c6SL7Ce0wwlYtyXO0khf7Ec9EzICmSb40atAbssATUYMnqhHOpO34TvjGpvLhcoYFCPXeDjPrDuMREpIkN/fZ7shDha7BkKcQdIwz432pHLCYxlT4qclSw+5GLzdPCwfUBXiLRvB36X1qL1jyuftyDu9n/D/p+hzJwDqSxm/2/tX0mJfLNZSROOE/JHuTnEnMFa6kQ6v/S2HOZz95rIJYdwIAAA== + H4sIAAAAAAAAAF1RQU7DQAy85xW+cakQ50o5FCSkSEBLk8J5m7iN1c16tXZbIsTf2U1SWnHzzjjjmYkGUx/I7Qt3IjVK7J455NWEwhWGHYdMauPe10/c4DJ8kJBugq14jXsSxbCQ/NUcEOQYEHo+QmtOCNoifD/8wMJ7ICdqrMXmHqoWHSS9YWHH1vI5XRzlgQPUlurDwFpyBzhKoqNsAOlMUN+yi+oMYToPRrIz2po7rDj/HKe4kNGy3AgGyUuN30F8JjOZcU1gam6pxQiNtI2aLmY/oe2HsFVL8hKt5MVuiGdizICmSb40ctAZssBjogZPVCOcSdvhneIbm9b7aw0zEOq87aesW4wlpEiS3Myz7ZH7Cl2DIU8j6DBnxvtSOWAxylT4pRdLDbs7vfaejvaoM/AWjeDl8DxJFCz5pHCtxPupgj/qtpJkYhXTxl/t/ZvpMC8XKyhj6lTBY7I40plErHAl7Vv9vwoTnv0Cs+RzW3sCAAA= - +
@@ -33,7 +33,7 @@ buoy tender - If don't have the App yet, please install: + If you don't have the App yet, please install: Install iOS App diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_ja.properties.xlf b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_ja.properties.xlf index 7971f39fcd6..9ce996e6326 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_ja.properties.xlf +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_ja.properties.xlf @@ -1,12 +1,12 @@ - +
- H4sIAAAAAAAAAF2RTU/DMAyG7/0VvnGZEGekHgYSUiVgY+3gnLXeajWNo9jbqBD/naQtbOLm+PXH+zgaTN2ROxTuRGqU2D1xyKs5C5c07DlkUhv3tnnkBlfhnYR0G2zFGzyQKIal5C+mQ5BjQBj4CK05IWiL8HX3DUvvgZyosRabW6hadJDmjQV7tpbPaeM0HjhAbanuRtWS6+AoSY5jA0hvgvqWXZzOEOb1YCQ7o625x4rzjymKBRmtyq1gkLzU2AfxmcxkxjWBqbmWllNqkm2c6SL7Ce0wwlYtyXO0khf7Ec9EzICmSb40atAbssATUYMnqhHOpO34TvjGpvLhcoYFCPXeDjPrDuMREpIkN/fZ7shDha7BkKcQdIwz432pHLCYxlT4qclSw+5GLzdPCwfUBXiLRvB36X1qL1jyuftyDu9n/D/p+hzJwDqSxm/2/tX0mJfLNZSROOE/JHuTnEnMFa6kQ6v/S2HOZz95rIJYdwIAAA== + H4sIAAAAAAAAAF1RQU7DQAy85xW+cakQ50o5FCSkSEBLk8J5m7iN1c16tXZbIsTf2U1SWnHzzjjjmYkGUx/I7Qt3IjVK7J455NWEwhWGHYdMauPe10/c4DJ8kJBugq14jXsSxbCQ/NUcEOQYEHo+QmtOCNoifD/8wMJ7ICdqrMXmHqoWHSS9YWHH1vI5XRzlgQPUlurDwFpyBzhKoqNsAOlMUN+yi+oMYToPRrIz2po7rDj/HKe4kNGy3AgGyUuN30F8JjOZcU1gam6pxQiNtI2aLmY/oe2HsFVL8hKt5MVuiGdizICmSb40ctAZssBjogZPVCOcSdvhneIbm9b7aw0zEOq87aesW4wlpEiS3Myz7ZH7Cl2DIU8j6DBnxvtSOWAxylT4pRdLDbs7vfaejvaoM/AWjeDl8DxJFCz5pHCtxPupgj/qtpJkYhXTxl/t/ZvpMC8XKyhj6lTBY7I40plErHAl7Vv9vwoTnv0Cs+RzW3sCAAA= - +
@@ -17,7 +17,7 @@
Make sure you have the {0} App installed. Then scan the following QRCode or click the link using your smartphone to register as - {0} アプリがインストールされていることを確認してください。その上で、以下の QR コードをスキャンするか、スマートフォンを使用してリンクをクリックして登録します: + {0} アプリがインストールされていることを確認してください。その上で、以下の QR コードをスキャンするか、スマートフォンを使用してリンクをクリックして登録します: Welcome to @@ -40,8 +40,8 @@ 設標船 - If don't have the App yet, please install: - まだアプリがない場合は、インストールしてください: + If you don't have the App yet, please install: + まだアプリがない場合は、インストールしてください: Install iOS App diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_ru.properties.xlf b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_ru.properties.xlf index c9cec6e733d..dc102932e7b 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_ru.properties.xlf +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_ru.properties.xlf @@ -1,12 +1,12 @@ - +
- H4sIAAAAAAAAAF2RTU/DMAyG7/0VvnGZEGekHgYSUiVgY+3gnLXeajWNo9jbqBD/naQtbOLm+PXH+zgaTN2ROxTuRGqU2D1xyKs5C5c07DlkUhv3tnnkBlfhnYR0G2zFGzyQKIal5C+mQ5BjQBj4CK05IWiL8HX3DUvvgZyosRabW6hadJDmjQV7tpbPaeM0HjhAbanuRtWS6+AoSY5jA0hvgvqWXZzOEOb1YCQ7o625x4rzjymKBRmtyq1gkLzU2AfxmcxkxjWBqbmWllNqkm2c6SL7Ce0wwlYtyXO0khf7Ec9EzICmSb40atAbssATUYMnqhHOpO34TvjGpvLhcoYFCPXeDjPrDuMREpIkN/fZ7shDha7BkKcQdIwz432pHLCYxlT4qclSw+5GLzdPCwfUBXiLRvB36X1qL1jyuftyDu9n/D/p+hzJwDqSxm/2/tX0mJfLNZSROOE/JHuTnEnMFa6kQ6v/S2HOZz95rIJYdwIAAA== + H4sIAAAAAAAAAF1RQU7DQAy85xW+cakQ50o5FCSkSEBLk8J5m7iN1c16tXZbIsTf2U1SWnHzzjjjmYkGUx/I7Qt3IjVK7J455NWEwhWGHYdMauPe10/c4DJ8kJBugq14jXsSxbCQ/NUcEOQYEHo+QmtOCNoifD/8wMJ7ICdqrMXmHqoWHSS9YWHH1vI5XRzlgQPUlurDwFpyBzhKoqNsAOlMUN+yi+oMYToPRrIz2po7rDj/HKe4kNGy3AgGyUuN30F8JjOZcU1gam6pxQiNtI2aLmY/oe2HsFVL8hKt5MVuiGdizICmSb40ctAZssBjogZPVCOcSdvhneIbm9b7aw0zEOq87aesW4wlpEiS3Myz7ZH7Cl2DIU8j6DBnxvtSOWAxylT4pRdLDbs7vfaejvaoM/AWjeDl8DxJFCz5pHCtxPupgj/qtpJkYhXTxl/t/ZvpMC8XKyhj6lTBY7I40plErHAl7Vv9vwoTnv0Cs+RzW3sCAAA= - +
@@ -40,8 +40,8 @@ лоцмейстерское судно
- If don't have the App yet, please install: - Если у вас еще нет приложения, установите его: + If you don't have the App yet, please install: + Если у вас еще нет приложения, установите его: Install iOS App diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_zh_CN.properties.xlf b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_zh_CN.properties.xlf index 0037c51bbaa..29f86428c5b 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/resources/stringmessages/XLF/TrackingStringMessages_zh_CN.properties.xlf @@ -1,63 +1,63 @@ - +
- H4sIAAAAAAAAAF2RTU/DMAyG7/0VvnGZEGekHgYSUiVgY+3gnLXeajWNo9jbqBD/naQtbOLm+PXH+zgaTN2ROxTuRGqU2D1xyKs5C5c07DlkUhv3tnnkBlfhnYR0G2zFGzyQKIal5C+mQ5BjQBj4CK05IWiL8HX3DUvvgZyosRabW6hadJDmjQV7tpbPaeM0HjhAbanuRtWS6+AoSY5jA0hvgvqWXZzOEOb1YCQ7o625x4rzjymKBRmtyq1gkLzU2AfxmcxkxjWBqbmWllNqkm2c6SL7Ce0wwlYtyXO0khf7Ec9EzICmSb40atAbssATUYMnqhHOpO34TvjGpvLhcoYFCPXeDjPrDuMREpIkN/fZ7shDha7BkKcQdIwz432pHLCYxlT4qclSw+5GLzdPCwfUBXiLRvB36X1qL1jyuftyDu9n/D/p+hzJwDqSxm/2/tX0mJfLNZSROOE/JHuTnEnMFa6kQ6v/S2HOZz95rIJYdwIAAA== + H4sIAAAAAAAAAF1RQU7DQAy85xW+cakQ50o5FCSkSEBLk8J5m7iN1c16tXZbIsTf2U1SWnHzzjjjmYkGUx/I7Qt3IjVK7J455NWEwhWGHYdMauPe10/c4DJ8kJBugq14jXsSxbCQ/NUcEOQYEHo+QmtOCNoifD/8wMJ7ICdqrMXmHqoWHSS9YWHH1vI5XRzlgQPUlurDwFpyBzhKoqNsAOlMUN+yi+oMYToPRrIz2po7rDj/HKe4kNGy3AgGyUuN30F8JjOZcU1gam6pxQiNtI2aLmY/oe2HsFVL8hKt5MVuiGdizICmSb40ctAZssBjogZPVCOcSdvhneIbm9b7aw0zEOq87aesW4wlpEiS3Myz7ZH7Cl2DIU8j6DBnxvtSOWAxylT4pRdLDbs7vfaejvaoM/AWjeDl8DxJFCz5pHCtxPupgj/qtpJkYhXTxl/t/ZvpMC8XKyhj6lTBY7I40plErHAl7Vv9vwoTnv0Cs+RzW3sCAAA= - +
Tracking Invitation for - 跟踪邀请 - + 跟踪邀请 - Make sure you have the {0} App installed. Then scan the following QRCode or click the link using your smartphone to register as - 确保已安装 {0} 应用。然后,使用智能手机扫描以下二维码或点击链接注册 + 确保已安装 {0} 应用。然后,使用智能手机扫描以下二维码或点击链接注册 Welcome to - 欢迎使用 + 欢迎使用 Start iOS App - 启动 iOS 应用 + 启动 iOS 应用 Start Android App - 启动 Android 应用 + 启动 Android 应用 If you are reading this mail on the device with the App already installed, simply click below to start: - 如果在已安装该应用的设备中读取邮件,只需点击以下链接即可启动: + 如果在已安装该应用的设备中读取邮件,只需点击以下链接即可启动: buoy tender - 浮标供应船 + 浮标供应船 - If don't have the App yet, please install: - 如果尚无该应用,请安装: + If you don't have the App yet, please install: + 如果您尚无该应用,请安装: Install iOS App - 安装 iOS 应用 + 安装 iOS 应用 Install Android App - 安装 Android 应用 + 安装 Android 应用 SAP Sailing Buoy Pinger - SAP Sailing Buoy Pinger + SAP Sailing Buoy Pinger SAP Sail InSight - SAP Sail InSight + SAP Sail InSight
diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapter.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapter.java index d728bf31f88..cbeada5a0f1 100755 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapter.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapter.java @@ -39,8 +39,10 @@ public interface RaceLogTrackingAdapter { *
  • Is a {@link RaceLogRaceTracker} already listening for this racelog? If not, add one.
  • *
  • Is a {@link RaceLogStartTrackingEvent} present in the racelog? If not, add one
  • * + * @param trackWind TODO + * @param correctWindDirectionByMagneticDeclination TODO */ - RaceHandle startTracking(RacingEventService service, Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet) + RaceHandle startTracking(RacingEventService service, Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) throws NotDenotedForRaceLogTrackingException, Exception; RaceLogTrackingState getRaceLogTrackingState(RacingEventService service, RaceColumn raceColumn, Fleet fleet); diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/Activator.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/Activator.java index 6dc817c4c0a..c2b99f36dc3 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/Activator.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/Activator.java @@ -22,6 +22,7 @@ import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; import com.sap.sailing.domain.racelogtracking.SmartphoneUUIDIdentifier; import com.sap.sailing.domain.racelogtracking.impl.fixtracker.RegattaLogFixTrackerRegattaListener; import com.sap.sailing.domain.trackfiles.TrackFileImportDeviceIdentifier; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParametersHandler; import com.sap.sailing.domain.tracking.TrackedRegattaListener; import com.sap.sailing.server.MasterDataImportClassLoaderService; import com.sap.sailing.server.RacingEventService; @@ -97,7 +98,14 @@ public class Activator implements BundleActivator { regattaLogSensorDataTrackerTrackedRegattaListener, null)); registrations.add(context.registerService(Replicable.class, regattaLogSensorDataTrackerTrackedRegattaListener, null)); - + new Thread(()->{ + try { + registrations.add(context.registerService(RaceTrackingConnectivityParametersHandler.class, + new RaceLogConnectivityParamsHandler(racingEventServiceTracker.waitForService(0)), getDict(RaceLogConnectivityParams.TYPE))); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception trying to register race log tracking connectivity params persistence handler", e); + } + }, "RaceLog tracking activator registering connectivity params persistence handler").start(); logger.log(Level.INFO, "Started "+context.getBundle().getSymbolicName()); } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParams.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParams.java index 95e833a6835..e96a9b2faa8 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParams.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParams.java @@ -22,12 +22,17 @@ import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sailing.domain.regattalike.HasRegattaLike; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.RaceTracker; -import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tracking.impl.AbstractRaceTrackingConnectivityParameters; import com.sap.sailing.server.RacingEventService; -public class RaceLogConnectivityParams implements RaceTrackingConnectivityParameters { +public class RaceLogConnectivityParams extends AbstractRaceTrackingConnectivityParameters { + /** + * A type identifier that needs to be unique for the + */ + public static final String TYPE = "RACE_LOG_TRACKING"; + private final RacingEventService service; private final RaceColumn raceColumn; private final Fleet fleet; @@ -37,7 +42,9 @@ public class RaceLogConnectivityParams implements RaceTrackingConnectivityParame private final DomainFactory domainFactory; public RaceLogConnectivityParams(RacingEventService service, Regatta regatta, RaceColumn raceColumn, Fleet fleet, - Leaderboard leaderboard, long delayToLiveInMillis, DomainFactory domainFactory) throws RaceNotCreatedException { + Leaderboard leaderboard, long delayToLiveInMillis, DomainFactory domainFactory, boolean trackWind, + boolean correctWindDirectionByMagneticDeclination) throws RaceNotCreatedException { + super(trackWind, correctWindDirectionByMagneticDeclination); this.service = service; this.regatta = regatta; this.raceColumn = raceColumn; @@ -45,12 +52,16 @@ public class RaceLogConnectivityParams implements RaceTrackingConnectivityParame this.leaderboard = leaderboard; this.delayToLiveInMillis = delayToLiveInMillis; this.domainFactory = domainFactory; - if (!new RaceLogTrackingStateAnalyzer(getRaceLog()).analyze().isForTracking()) { throw new RaceNotCreatedException(String.format("Racelog (%s) is not denoted for tracking", getRaceLog())); } } + @Override + public String getTypeIdentifier() { + return TYPE; + } + @Override public RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, RaceLogResolver raceLogResolver) { @@ -70,7 +81,7 @@ public class RaceLogConnectivityParams implements RaceTrackingConnectivityParame throw new RaceNotCreatedException("No regatta for race-log tracked race"); } DynamicTrackedRegatta trackedRegatta = trackedRegattaRegistry.getOrCreateTrackedRegatta(regatta); - return new RaceLogRaceTracker(trackedRegatta, this, windStore, raceLogResolver); + return new RaceLogRaceTracker(trackedRegatta, this, windStore, raceLogResolver, this); } @Override diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParamsHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParamsHandler.java new file mode 100644 index 00000000000..e3ef23fa681 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParamsHandler.java @@ -0,0 +1,71 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.impl.AbstractRaceTrackingConnectivityParametersHandler; +import com.sap.sailing.server.RacingEventService; +import com.sap.sse.common.TypeBasedServiceFinder; + +/** + * Handles mapping TracTrac connectivity parameters from and to a map with {@link String} keys. The + * "param URL" is considered the {@link #getKey(RaceTrackingConnectivityParameters) key} for these objects.

    + * + * Lives in the same package as {@link RaceTrackingConnectivityParameters} for package-private access to + * its members. + * + * @author Axel Uhl (d043530) + * + */ +public class RaceLogConnectivityParamsHandler extends AbstractRaceTrackingConnectivityParametersHandler { + private static final String FLEET_NAME = "fleetName"; + private static final String RACE_COLUMN_NAME = "raceColumnName"; + private static final String LEADERBOARD_NAME = "leaderboardName"; + private static final String DELAY_TO_LIVE_IN_MILLIS = "delayToLiveInMillis"; + private final DomainFactory domainFactory; + private final RacingEventService racingEventService; + + public RaceLogConnectivityParamsHandler(RacingEventService racingEventService) { + super(); + this.racingEventService = racingEventService; + this.domainFactory = racingEventService.getBaseDomainFactory(); + } + + @Override + public Map mapFrom(RaceTrackingConnectivityParameters params) { + assert params instanceof RaceLogConnectivityParams; + final RaceLogConnectivityParams rlParams = (RaceLogConnectivityParams) params; + final Map result = getKey(params); + result.put(DELAY_TO_LIVE_IN_MILLIS, rlParams.getDelayToLiveInMillis()); + addWindTrackingParameters(rlParams, result); + return result; + } + + @Override + public RaceTrackingConnectivityParameters mapTo(Map map) throws MalformedURLException, URISyntaxException { + final RegattaLeaderboard leaderboard = (RegattaLeaderboard) racingEventService.getLeaderboardByName((String) map.get(LEADERBOARD_NAME)); + final RaceColumn raceColumn = leaderboard.getRaceColumnByName((String) map.get(RACE_COLUMN_NAME)); + return new RaceLogConnectivityParams(racingEventService, leaderboard.getRegatta(), raceColumn, + raceColumn.getFleetByName((String) map.get(FLEET_NAME)), leaderboard, + ((Number) map.get(DELAY_TO_LIVE_IN_MILLIS)).longValue(), domainFactory, isTrackWind(map), + isCorrectWindDirectionByMagneticDeclination(map)); + } + + @Override + public Map getKey(RaceTrackingConnectivityParameters params) { + assert params instanceof RaceLogConnectivityParams; + final RaceLogConnectivityParams rlParams = (RaceLogConnectivityParams) params; + final Map result = new HashMap<>(); + result.put(TypeBasedServiceFinder.TYPE, params.getTypeIdentifier()); + result.put(LEADERBOARD_NAME, rlParams.getLeaderboard().getName()); + result.put(RACE_COLUMN_NAME, rlParams.getRaceColumn().getName()); + result.put(FLEET_NAME, rlParams.getFleet().getName()); + return result; + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRaceTracker.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRaceTracker.java index 9d0b8d79ec2..eaed45e622f 100755 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRaceTracker.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRaceTracker.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -98,7 +97,8 @@ public class RaceLogRaceTracker extends AbstractRaceTrackerBaseImpl { private EndOfTrackingController endOfTrackingController; public RaceLogRaceTracker(DynamicTrackedRegatta regatta, RaceLogConnectivityParams params, WindStore windStore, - RaceLogResolver raceLogResolver) { + RaceLogResolver raceLogResolver, RaceLogConnectivityParams connectivityParams) { + super(connectivityParams); this.params = params; this.windStore = windStore; this.trackedRegatta = regatta; @@ -194,17 +194,17 @@ public class RaceLogRaceTracker extends AbstractRaceTrackerBaseImpl { } @Override - public Set getRaces() { - return trackedRace == null ? null : Collections.singleton(trackedRace.getRace()); + public RaceDefinition getRace() { + return trackedRace == null ? null : trackedRace.getRace(); } @Override - public Set getRaceIdentifiers() { - return trackedRace == null ? null : Collections.singleton(trackedRace.getRaceIdentifier()); + public RegattaAndRaceIdentifier getRaceIdentifier() { + return trackedRace == null ? null : trackedRace.getRaceIdentifier(); } @Override - public RaceHandle getRacesHandle() { + public RaceHandle getRaceHandle() { return new RaceLogRacesHandle(this); } @@ -312,6 +312,7 @@ public class RaceLogRaceTracker extends AbstractRaceTrackerBaseImpl { trackedRace.addStartTimeChangedListener(startOfTrackingController); endOfTrackingController = new EndOfTrackingController(trackedRace, raceLog, raceLogEventAuthor); trackedRace.addListener(endOfTrackingController); + notifyRaceCreationListeners(); logger.info(String.format("Started tracking race-log race (%s)", raceLog)); // this wakes up all waiting race handles synchronized (this) { diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRacesHandle.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRacesHandle.java index ff375ff8e66..2d0c26c79ac 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRacesHandle.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRacesHandle.java @@ -1,7 +1,5 @@ package com.sap.sailing.domain.racelogtracking.impl; -import java.util.Set; - import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; @@ -28,12 +26,7 @@ public class RaceLogRacesHandle implements RaceHandle { @Override public RaceDefinition getRace(long timeoutInMilliseconds) { long start = System.currentTimeMillis(); - Set raceDefs = tracker.getRaces(); - - RaceDefinition result = null; - if (raceDefs != null && !raceDefs.isEmpty()) { - result = tracker.getRaces().iterator().next(); - } + RaceDefinition result = tracker.getRace(); boolean interrupted = false; synchronized (tracker) { while ((timeoutInMilliseconds == -1 || System.currentTimeMillis() - start < timeoutInMilliseconds) @@ -47,13 +40,13 @@ public class RaceLogRacesHandle implements RaceHandle { tracker.wait(timeToWait); } } - result = tracker.getRaces().isEmpty() ? null : tracker.getRaces().iterator().next(); + result = tracker.getRace(); } catch (InterruptedException e) { interrupted = true; } } } - return result == null ? null : result; + return result; } @Override diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java index 394e1b5a893..f3fbae990b0 100755 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java @@ -96,7 +96,8 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter { @Override public RaceHandle startTracking(RacingEventService service, Leaderboard leaderboard, RaceColumn raceColumn, - Fleet fleet) throws NotDenotedForRaceLogTrackingException, Exception { + Fleet fleet, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) + throws NotDenotedForRaceLogTrackingException, Exception { RaceLog raceLog = raceColumn.getRaceLog(fleet); RaceLogTrackingState raceLogTrackingState = new RaceLogTrackingStateAnalyzer(raceLog).analyze(); if (!raceLogTrackingState.isForTracking()) { @@ -110,7 +111,7 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter { } final RaceHandle result; if (!isRaceLogRaceTrackerAttached(service, raceLog)) { - result = addTracker(service, regatta, leaderboard, raceColumn, fleet, -1); + result = addTracker(service, regatta, leaderboard, raceColumn, fleet, -1, trackWind, correctWindDirectionByMagneticDeclination); } else { result = null; } @@ -123,16 +124,15 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter { * and tracking begins. Otherwise, the {@code RaceLogRaceTracker} waits until a {@code StartTrackingEvent} is added * to perform these actions. The race first has to be denoted for racelog tracking. */ - private RaceHandle addTracker(RacingEventService service, RegattaIdentifier regattaToAddTo, - Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet, long timeoutInMilliseconds) - throws RaceLogRaceTrackerExistsException, Exception { + private RaceHandle addTracker(RacingEventService service, RegattaIdentifier regattaToAddTo, Leaderboard leaderboard, + RaceColumn raceColumn, Fleet fleet, long timeoutInMilliseconds, boolean trackWind, + boolean correctWindDirectionByMagneticDeclination) throws RaceLogRaceTrackerExistsException, Exception { RaceLog raceLog = raceColumn.getRaceLog(fleet); assert !isRaceLogRaceTrackerAttached(service, raceLog) : new RaceLogRaceTrackerExistsException( leaderboard.getName() + " - " + raceColumn.getName() + " - " + fleet.getName()); - Regatta regatta = regattaToAddTo == null ? null : service.getRegatta(regattaToAddTo); RaceLogConnectivityParams params = new RaceLogConnectivityParams(service, regatta, raceColumn, fleet, - leaderboard, delayToLiveInMillis, domainFactory); + leaderboard, delayToLiveInMillis, domainFactory, trackWind, correctWindDirectionByMagneticDeclination); return service.addRace(regattaToAddTo, params, timeoutInMilliseconds); } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/FixLoaderAndTracker.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/FixLoaderAndTracker.java index 107eb1aeae4..ff1e2096f1f 100755 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/FixLoaderAndTracker.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/FixLoaderAndTracker.java @@ -135,13 +135,56 @@ public class FixLoaderAndTracker implements TrackingDataLoader { final DynamicGPSFixTrack markTrack = trackedRace.getOrCreateTrack(mark); final GPSFix firstFixAtOrAfter; final boolean forceFix; - markTrack.lockForRead(); - try { - forceFix = Util.isEmpty(markTrack.getRawFixes()) - || (firstFixAtOrAfter = markTrack.getFirstFixAtOrAfter(timePoint)) != null - && firstFixAtOrAfter.getTimePoint().equals(timePoint); - } finally { - markTrack.unlockAfterRead(); + if (trackedRace.isWithinStartAndEndOfTracking(fix.getTimePoint())) { + forceFix = false; + } else { + markTrack.lockForRead(); + try { + if (Util.isEmpty(markTrack.getRawFixes()) + || (firstFixAtOrAfter = markTrack.getFirstFixAtOrAfter(timePoint)) != null + && firstFixAtOrAfter.getTimePoint().equals(timePoint)) { + // either the first fix or overwriting an existing one + forceFix = true; + } else { + // checking if the given fix is "better" than an existing one + TimePoint startOfTracking = trackedRace.getStartOfTracking(); + TimePoint endOfTracking = trackedRace.getStartOfTracking(); + if (startOfTracking != null) { + GPSFix fixAfterStartOfTracking = markTrack + .getFirstFixAtOrAfter(startOfTracking); + if (fixAfterStartOfTracking == null + || !trackedRace.isWithinStartAndEndOfTracking( + fixAfterStartOfTracking.getTimePoint())) { + // There is no fix in the tracking interval, so this fix could be "better" + // than ones already available in the track + // Better means closer before/after the beginning/end of the tracking + // interval + if (timePoint.before(startOfTracking)) { + // check if it is closer to the beginning of the tracking interval + GPSFix fixBeforeStartOfTracking = markTrack + .getLastFixAtOrBefore(startOfTracking); + forceFix = (fixBeforeStartOfTracking == null + || fixBeforeStartOfTracking.getTimePoint().before(timePoint)); + } else if (endOfTracking != null && timePoint.after(endOfTracking)) { + // check if it is closer to the end of the tracking interval + GPSFix fixAfterEndOfTracking = markTrack + .getFirstFixAtOrAfter(endOfTracking); + forceFix = (fixAfterEndOfTracking == null + || fixAfterEndOfTracking.getTimePoint().after(timePoint)); + } else { + forceFix = false; + } + } else { + // there is already a fix in the tracking interval + forceFix = false; + } + } else { + forceFix = false; + } + } + } finally { + markTrack.unlockAfterRead(); + } } trackedRace.recordFix(mark, (GPSFix) fix, /* only when in tracking interval */ !forceFix); } @@ -160,6 +203,57 @@ public class FixLoaderAndTracker implements TrackingDataLoader { startTracking(); } + /** + * Loading fixes for {@link Mark}s needs special handling compared with {@link Competitor}s. If there are no fixes + * available in the tracking interval, it is necessary to load other available fixes before/after the tracking + * interval. this is due to the buoy pinger app can be used in the morning to ping some marks. The resulting fixes + * need to also be available for races later on the day. This method ensures that if available, fixes are initially + * loaded when starting tracking. In contrast to that, mappings for {@link Competitor}s have no special handling. + * + * @param mappings + * the added mappings + * @param item + * the item the mappings belong to + */ + private void loadFixesForNewMappings(Iterable> mappings, WithID item) { + TimeRange trackingTimeRange = getTrackingTimeRange(); + if (item instanceof Mark) { + Mark mark = (Mark) item; + if (containsMappingThatIntersectsTimeRange(mappings, trackingTimeRange)) { + loadFixesInTimeRange(mappings, trackingTimeRange); + } + DynamicGPSFixTrack track = trackedRace.getOrCreateTrack(mark); + // load all mapped fixes if there was no fix in the tracking TimeRange + final boolean loadAllMappedFixes = (track.getFirstRawFix() == null); + if (loadAllMappedFixes) { + // either got an empty track of there is no mapping for the TimeRange of the race at all. + // try again without constraining the mapping interval by start/end of tracking to at + // least attempt to get fixes at all in case there were any within the device mapping interval specified + mappings.forEach(mapping -> loadFixes(mapping.getTimeRange(), mapping)); + } + } else { + loadFixesInTimeRange(mappings, trackingTimeRange); + } + } + + private void loadFixesInTimeRange(Iterable> mappings, TimeRange trackingTimeRange) { + mappings.forEach(mapping -> loadFixes(trackingTimeRange.intersection(mapping.getTimeRange()), mapping)); + } + + /** + * Calls the given callback for every known mapping of the given item. + * + * @param callback the callback to call for every known mapping + */ + public boolean containsMappingThatIntersectsTimeRange(Iterable> mappings, TimeRange timeRange) { + for (DeviceMappingWithRegattaLogEvent mapping : mappings) { + if(timeRange.intersects(mapping.getTimeRange())) { + return true; + } + } + return false; + } + private void loadFixes(TimeRange timeRangeToLoad, DeviceMappingWithRegattaLogEvent mapping) { if(timeRangeToLoad == null) { return; @@ -212,15 +306,6 @@ public class FixLoaderAndTracker implements TrackingDataLoader { @SuppressWarnings("unchecked") DeviceMapping markMapping = (DeviceMapping) mapping; gpsFixStore.loadMarkTrack(track, markMapping, timeRangeToLoad.from(), timeRangeToLoad.to()); - if (track.getFirstRawFix() == null) { - logger.fine("Loading mark positions from outside of start/end of tracking interval (" + timeRangeToLoad.from() - + ".." + timeRangeToLoad.to() + ") because no fixes were found in that interval"); - // got an empty track for the mark; try again without constraining the mapping interval - // by start/end of tracking to at least attempt to get fixes at all in case there were any - // within the device mapping interval specified - gpsFixStore.loadMarkTrack(track, markMapping, /* startOfTimeWindowToLoad */ null, - /* endOfTimeWindowToLoad */ null); - } } catch (TransformationException | NoCorrespondingServiceRegisteredException e) { logger.log(Level.WARNING, "Could not load mark track " + mapping.getMappedTo()); } @@ -355,26 +440,40 @@ public class FixLoaderAndTracker implements TrackingDataLoader { } @Override - protected void mappingAdded(DeviceMappingWithRegattaLogEvent mapping) { - // The listener is first added to not lose any fix after loading the initial fixes and adding the listener. - sensorFixStore.addListener(listener, mapping.getDevice()); - loadFixes(getTrackingTimeRange().intersection(mapping.getTimeRange()), mapping); + protected void mappingsAdded(Iterable> mappings, WithID item) { + // The listener is first added. This causes that no fix is lost but potentially fixes are being processes twice. + // There are the following cases: + // 1. A fix is added before the mapping update started + // -> the fix is dropped but during mapping update the whole mapping is being loaded including this fix. + // 2. A fix is added while the write lock is being held for the mappings + // -> The fix will be processed after the write lock is removed. In this state + // the mappings are up-to-date and in a consequence the fix is loaded into the track. + // In addition, the fix is loaded on mapping update, but both fix instances consistently have the same state. + // 3. A fix is added after a successful mapping update + // -> The fix is only added once, because the initial loading during mapping update is finished + mappings.forEach(mapping -> sensorFixStore.addListener(listener, mapping.getDevice())); + loadFixesForNewMappings(mappings, item); } + /** + * {@link #loadFixes loads fixes} that are captured by {@code newMapping} but not by {@code oldMapping}. This + * may require up to two {@link #loadFixes} calls: one for the fixes after and one for the fixes before the old + * mapping. + */ @Override protected void mappingChanged(DeviceMappingWithRegattaLogEvent oldMapping, DeviceMappingWithRegattaLogEvent newMapping) { final TimeRange newTimeRange = newMapping.getTimeRange(); final TimeRange oldTimeRange = oldMapping.getTimeRange(); - if(newTimeRange.endsAfter(oldTimeRange)) { - final TimePoint oldTo= oldTimeRange.to(); + if (newTimeRange.endsAfter(oldTimeRange)) { + final TimePoint oldTo = oldTimeRange.to(); final TimePoint newFrom = newTimeRange.from(); final TimePoint from = oldTo.after(newFrom) ? oldTo : newFrom; final TimeRange rangeToLoad = new TimeRangeImpl(from, newTimeRange.to()); loadFixes(rangeToLoad, newMapping); } - if(newTimeRange.startsBefore(oldTimeRange)) { - final TimePoint oldFrom= oldTimeRange.from(); + if (newTimeRange.startsBefore(oldTimeRange)) { + final TimePoint oldFrom = oldTimeRange.from(); final TimePoint newTo = newTimeRange.to(); final TimePoint to = oldFrom.after(newTo) ? oldFrom : newTo; final TimeRange rangeToLoad = new TimeRangeImpl(newTimeRange.from(), to); diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RaceLogFixTrackerManager.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RaceLogFixTrackerManager.java index d0edf58d240..dfef30b0ae9 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RaceLogFixTrackerManager.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RaceLogFixTrackerManager.java @@ -120,7 +120,8 @@ public class RaceLogFixTrackerManager implements TrackingDataLoader { stopTrackerIfStillRunning(preemptive); trackedRace.removeListener(raceChangeListener); synchronized (knownRaceLogs) { - for (RaceLog raceLog : knownRaceLogs) { + final Set tempSet = new HashSet<>(knownRaceLogs); + for (RaceLog raceLog : tempSet) { removeRaceLogUnlocked(raceLog); } } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java index e61d9abd4a9..f6e489441e6 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java @@ -242,7 +242,6 @@ public abstract class RegattaLogDeviceMappings { LockUtil.lockForWrite(mappingsLock); try { oldMappings.putAll(mappings); - mappings.clear(); mappings.putAll(newMappings); mappingsByDevice.clear(); @@ -266,26 +265,27 @@ public abstract class RegattaLogDeviceMappings { * Calculates and applies the mapping changes by removing listeners no longer needed for the mappings removed, * and by loading and adding the fixes for extended or added mappings. */ - private void calculateDiff(Map>> previousMappings, - Map>> newMappings) { + private void calculateDiff(Map>> previousMappings, + Map>> newMappings) { Set itemsToProcess = new HashSet(previousMappings.keySet()); itemsToProcess.addAll(newMappings.keySet()); for (ItemT item : itemsToProcess) { if (!newMappings.containsKey(item)) { previousMappings.get(item).forEach(this::mappingRemovedInternal); } else { - final List> oldMappings = previousMappings.containsKey(item) + final Iterable> oldMappings = previousMappings.containsKey(item) ? previousMappings.get(item) : Collections.emptyList(); - + final List> addedMappings = new ArrayList<>(); for (DeviceMappingWithRegattaLogEvent newMapping : newMappings.get(item)) { - DeviceMappingWithRegattaLogEvent oldMapping = findAndRemoveMapping(newMapping, - oldMappings); + DeviceMappingWithRegattaLogEvent oldMapping = findAndRemoveMapping(newMapping, oldMappings); if (oldMapping == null) { - mappingAddedInternal(newMapping); + addedMappings.add(newMapping); } else if (!newMapping.getTimeRange().equals(oldMapping.getTimeRange())) { mappingChangedInternal(oldMapping, newMapping); } } + mappingsAddedInternal(addedMappings, item); + // oldMappings now still contains those mappings that were not entirely substituted by the new mappings oldMappings.forEach(this::mappingRemovedInternal); } } @@ -309,21 +309,24 @@ public abstract class RegattaLogDeviceMappings { /** * Called when a {@link DeviceMapping} was added. * - * @param mapping the new mapping + * @param mappings the new mapping */ - protected abstract void mappingAdded(DeviceMappingWithRegattaLogEvent mapping); + protected abstract void mappingsAdded(Iterable> mappings, ItemT item); - private void mappingAddedInternal(DeviceMappingWithRegattaLogEvent mapping) { + private void mappingsAddedInternal(Iterable> mappings, ItemT item) { try { - mappingAdded(mapping); + mappingsAdded(mappings, item); } catch(Exception e) { - logger.log(Level.SEVERE, "error while adding mapping " + mapping, e); + logger.log(Level.SEVERE, "error while adding mapping " + mappings, e); } } /** * Called when a {@link DeviceMapping} was changed regarding its mapped time range. - * This can occur if an open ended mapping is being closed or a close event gets revoked. + * This can occur if an open ended mapping is being closed or a close event gets revoked + * or a new mapping is added that may partly overlap with an old mapping such that only + * parts of the fixes covered by the new mapping need to be loaded (the ones not already + * covered by the old mapping). * * @param oldMapping the old mapping * @param newMapping the new mapping @@ -342,11 +345,10 @@ public abstract class RegattaLogDeviceMappings { private DeviceMappingWithRegattaLogEvent findAndRemoveMapping( DeviceMappingWithRegattaLogEvent mappingToFind, - List> newItemsToProcess) { - for (Iterator> iterator = newItemsToProcess.iterator(); iterator - .hasNext();) { + Iterable> newItemsToProcess) { + for (Iterator> iterator = newItemsToProcess.iterator(); iterator.hasNext();) { DeviceMappingWithRegattaLogEvent deviceMapping = iterator.next(); - if(isSame(mappingToFind, deviceMapping)) { + if (isSame(mappingToFind, deviceMapping)) { iterator.remove(); return deviceMapping; } @@ -354,9 +356,15 @@ public abstract class RegattaLogDeviceMappings { return null; } + /** + * Compares two device mappings based on their device, the item mapped to and the race/regatta log event type + * that usually corresponds with the type of item to which the device is mapped. Note that in particular the + * mappings' time ranges are ignored for this comparison. + */ private boolean isSame(DeviceMappingWithRegattaLogEvent mapping1, DeviceMappingWithRegattaLogEvent mapping2) { return mapping1.getDevice().equals(mapping2.getDevice()) && mapping1.getMappedTo().equals(mapping2.getMappedTo()) - && mapping1.getEventType().equals(mapping2.getEventType()); + && mapping1.getEventType().equals(mapping2.getEventType()) + && mapping1.getRegattaLogEvent().getId().equals(mapping2.getRegattaLogEvent().getId()); } } diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImplTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImplTest.java index 6c8b7394dac..a1acdfca3ab 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImplTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImplTest.java @@ -413,7 +413,7 @@ public class CurrentRaceFilterImplTest { for (int i=0; i < numberOfRaces; i++) { _505RaceCells.add(new RaceCellImpl(raceColumnNamePrefix+(i+firstRaceColumnNumber), /* race log */ null, /* factor */ firstRaceColumnNumber, /* explicitFactor */ null, - /* zeroBasedIndexInFleet */ i)); + /* zeroBasedIndexInFleet */ i, /* targetTime */ null)); } final RaceRowImpl _505RaceRow = new RaceRowImpl(new FleetImpl(fleetName), _505RaceCells); return _505RaceRow; diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/CompetitorStore.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/CompetitorStore.java index 7ccc5f81527..2b4f3eebe6e 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/CompetitorStore.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/CompetitorStore.java @@ -68,6 +68,8 @@ public interface CompetitorStore extends CompetitorFactory { String newSailId, Nationality newNationality, URI newTeamImageUri, URI newFlagImageUri, Double timeOnTimeFactor, Duration timeOnDistanceAllowancePerNauticalMile, String searchTag); + void addCompetitors(Iterable competitors); + CompetitorDTO convertToCompetitorDTO(Competitor c); /** diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SharedDomainFactory.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SharedDomainFactory.java index 1af85ef3f12..90900c5011e 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SharedDomainFactory.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SharedDomainFactory.java @@ -110,4 +110,6 @@ public interface SharedDomainFactory extends CompetitorFactory { ControlPointWithTwoMarks getOrCreateControlPointWithTwoMarks(String id, String name, Mark left, Mark right); RaceLogResolver getRaceLogResolver(); + + Mark getOrCreateMark(String name, MarkType markType); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/BoatClassImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/BoatClassImpl.java index 657f8a55a67..dc19f20d65a 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/BoatClassImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/BoatClassImpl.java @@ -48,7 +48,9 @@ public class BoatClassImpl extends NamedImpl implements BoatClass { public BoatClassImpl(String name, boolean typicallyStartsUpwind) { this(name, typicallyStartsUpwind, /* displayName */ null, - /* hull length */ new MeterDistance(5), /* hullBeam */ null, /* hullType */ null); + // use the typical dinghy parameters as default + /* hull length */ new MeterDistance(5), /* hullBeam */ new MeterDistance(1.8), + /* hullType */ BoatHullType.MONOHULL); } public BoatClassImpl(String name, BoatClassMasterdata masterData) { diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/SharedDomainFactoryImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/SharedDomainFactoryImpl.java index f35232cf6f6..0dd1ae120b8 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/SharedDomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/SharedDomainFactoryImpl.java @@ -154,14 +154,23 @@ public class SharedDomainFactoryImpl implements SharedDomainFactory { return getOrCreateMark(name, name); } + @Override + public Mark getOrCreateMark(String name, MarkType markType) { + return getOrCreateMark(name, name, markType); + } + + private Mark getOrCreateMark(Serializable id, String name, MarkType markType) { + return getOrCreateMark(id, name, markType, /* color */ null, /* shape */ null, /* pattern */ null); + } + @Override public Mark getOrCreateMark(Serializable id, String name) { - return getOrCreateMark(id, name, null, null, null, null); + return getOrCreateMark(id, name, /* type */ null, /* color */ null, /* shape */ null, /* pattern */ null); } @Override public Mark getOrCreateMark(String toStringRepresentationOfID, String name) { - return getOrCreateMark(toStringRepresentationOfID, name, null, null, null, null); + return getOrCreateMark(toStringRepresentationOfID, name, /* type */ null, /* color */ null, /* shape */ null, /* pattern */ null); } @Override diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/TransientCompetitorStoreImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/TransientCompetitorStoreImpl.java index bba44934b60..75a5e22baab 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/TransientCompetitorStoreImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/TransientCompetitorStoreImpl.java @@ -250,7 +250,8 @@ public class TransientCompetitorStoreImpl implements CompetitorStore, Serializab c.getFlagImage() == null ? null : c.getFlagImage().toString(), new BoatDTO(c.getBoat().getName(), c.getBoat().getSailID(), c.getBoat().getColor()), new BoatClassDTO(c.getBoat().getBoatClass() - .getName(), c.getBoat().getBoatClass().getDisplayName(), c.getBoat().getBoatClass().getHullLength()), + .getName(), c.getBoat().getBoatClass().getDisplayName(), c.getBoat().getBoatClass().getHullLength(), + c.getBoat().getBoatClass().getHullBeam()), c.getTimeOnTimeFactor(), c.getTimeOnDistanceAllowancePerNauticalMile(), c.getSearchTag()); weakCompetitorDTOCache.put(c, competitorDTO); } @@ -274,4 +275,17 @@ public class TransientCompetitorStoreImpl implements CompetitorStore, Serializab LockUtil.unlockAfterWrite(lock); } } + + @Override + public void addCompetitors(Iterable competitors) { + LockUtil.lockForWrite(lock); + try { + for (Competitor competitor: competitors) { + competitorCache.put(competitor.getId(), competitor); + competitorsByIdAsString.put(competitor.getId().toString(), competitor); + } + } finally { + LockUtil.unlockAfterWrite(lock); + } + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceCell.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceCell.java index 833f0a6fac2..f55fef33e8c 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceCell.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceCell.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.base.racegroup; import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sse.common.Named; import com.sap.sse.common.Util; @@ -33,4 +34,11 @@ public interface RaceCell extends Named { * index 0. */ int getZeroBasedIndexInFleet(); + + /** + * If there is a tracked race for this race and we have sufficient information about wind and the boat class's + * polar / VPP data this method will return an estimated duration for how long the race will take based on the + * current course. + */ + TargetTimeInfo getTargetTime(); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceCellImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceCellImpl.java index cb69b6d7b83..c645efb79e7 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceCellImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceCellImpl.java @@ -2,6 +2,7 @@ package com.sap.sailing.domain.base.racegroup.impl; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.base.racegroup.RaceCell; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sse.common.Util; import com.sap.sse.common.impl.NamedImpl; @@ -21,6 +22,8 @@ public class RaceCellImpl extends NamedImpl implements RaceCell { private final Double explicitFactor; private final int zeroBasedIndexInFleet; + + private final TargetTimeInfo targetTime; /** * @param zeroBasedIndexInFleet @@ -34,12 +37,13 @@ public class RaceCellImpl extends NamedImpl implements RaceCell { * except in case the first race column is a "virtual" one that holds a non-discardable carry-forward * result. In this case, the second Race Column, which is the first "non-virtual" one, receives index 0. */ - public RaceCellImpl(String raceColumnName, RaceLog raceLog, double factor, Double explicitFactor, int zeroBasedIndexInFleet) { + public RaceCellImpl(String raceColumnName, RaceLog raceLog, double factor, Double explicitFactor, int zeroBasedIndexInFleet, TargetTimeInfo targetTime) { super(raceColumnName); this.raceLog = raceLog; this.factor = factor; this.explicitFactor = explicitFactor; this.zeroBasedIndexInFleet = zeroBasedIndexInFleet; + this.targetTime = targetTime; } @Override @@ -70,9 +74,14 @@ public class RaceCellImpl extends NamedImpl implements RaceCell { return zeroBasedIndexInFleet; } + @Override + public TargetTimeInfo getTargetTime() { + return targetTime; + } + @Override public String toString() { return "RaceCellImpl [factor=" + factor + ", explicitFactor=" + explicitFactor + ", zeroBasedIndexInFleet=" - + zeroBasedIndexInFleet + ", getName()=" + getName() + "]"; + + zeroBasedIndexInFleet + ", name=" + getName() + ", targetTime: " + getTargetTime() + "]"; } } diff --git a/java/com.sap.sailing.domain.swisstimingadapter.persistence/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.swisstimingadapter.persistence/META-INF/MANIFEST.MF index e327c07a9c4..faaf538f3ae 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter.persistence/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.swisstimingadapter.persistence/META-INF/MANIFEST.MF @@ -10,8 +10,12 @@ Require-Bundle: com.sap.sailing.domain.swisstimingadapter, org.mongodb.mongo-java-driver;bundle-version="2.13.0", com.sap.sse.mongodb, com.sap.sailing.domain, - com.sap.sailing.domain.common + com.sap.sailing.domain.common, + com.sap.sse.common, + com.sap.sailing.domain.persistence, + com.sap.sse Export-Package: com.sap.sailing.domain.swisstimingadapter.persistence, - com.sap.sailing.domain.swisstimingadapter.persistence.impl; x-friends:="com.sap.sailing.domain.swisstimingadapter.test" -Import-Package: org.osgi.framework + com.sap.sailing.domain.swisstimingadapter.persistence.impl;x-friends:="com.sap.sailing.domain.swisstimingadapter.test,com.sap.sailing.mongodb.test" +Import-Package: org.osgi.framework, + org.osgi.util.tracker;version="1.5.1" Bundle-Activator: com.sap.sailing.domain.swisstimingadapter.persistence.impl.Activator diff --git a/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/impl/Activator.java b/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/impl/Activator.java index d2714ee33bc..5514e93770a 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/impl/Activator.java +++ b/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/impl/Activator.java @@ -1,16 +1,52 @@ package com.sap.sailing.domain.swisstimingadapter.persistence.impl; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.logging.Level; +import java.util.logging.Logger; + import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.util.tracker.ServiceTracker; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; +import com.sap.sailing.domain.persistence.MongoRegattaLogStoreFactory; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingAdapterFactoryImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParametersHandler; +import com.sap.sse.common.TypeBasedServiceFinder; import com.sap.sse.mongodb.MongoDBService; +import com.sap.sse.util.ServiceTrackerFactory; public class Activator implements BundleActivator { + private static final Logger logger = Logger.getLogger(Activator.class.getName()); + @Override public void start(BundleContext context) throws Exception { for (CollectionNames name : CollectionNames.values()) { MongoDBService.INSTANCE.registerExclusively(CollectionNames.class, name.name()); } + new Thread(() -> { + final ServiceTracker mongoObjectFactoryServiceTracker = ServiceTrackerFactory.createAndOpen(context, MongoObjectFactory.class); + final ServiceTracker domainObjectFactoryServiceTracker = ServiceTrackerFactory.createAndOpen(context, DomainObjectFactory.class); + try { + final MongoObjectFactory mongoObjectFactory = mongoObjectFactoryServiceTracker.waitForService(0); + final DomainObjectFactory domainObjectFactory = domainObjectFactoryServiceTracker.waitForService(0); + final Dictionary properties = new Hashtable(); + final com.sap.sailing.domain.swisstimingadapter.DomainFactory domainFactory = new SwissTimingAdapterFactoryImpl().getOrCreateSwissTimingAdapter( + domainObjectFactory.getBaseDomainFactory()).getSwissTimingDomainFactory(); + final SwissTimingConnectivityParamsHandler paramsHandler = new SwissTimingConnectivityParamsHandler( + MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory), + MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore(mongoObjectFactory, domainObjectFactory), + domainFactory); + properties.put(TypeBasedServiceFinder.TYPE, SwissTimingTrackingConnectivityParameters.TYPE); + context.registerService(RaceTrackingConnectivityParametersHandler.class, paramsHandler, properties); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception trying to register SwissTiming RaceTrackingConnectivityParametersHandler implementation", e); + } + }, getClass().getName() + " registering connectivity handler").start(); } @Override diff --git a/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/impl/SwissTimingConnectivityParamsHandler.java b/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/impl/SwissTimingConnectivityParamsHandler.java new file mode 100644 index 00000000000..a643a9db27f --- /dev/null +++ b/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/impl/SwissTimingConnectivityParamsHandler.java @@ -0,0 +1,192 @@ +package com.sap.sailing.domain.swisstimingadapter.persistence.impl; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.swisstimingadapter.Competitor; +import com.sap.sailing.domain.swisstimingadapter.CrewMember; +import com.sap.sailing.domain.swisstimingadapter.DomainFactory; +import com.sap.sailing.domain.swisstimingadapter.StartList; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; +import com.sap.sailing.domain.swisstimingadapter.impl.CompetitorWithID; +import com.sap.sailing.domain.swisstimingadapter.impl.CompetitorWithoutID; +import com.sap.sailing.domain.swisstimingadapter.impl.CrewMemberImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.StartListImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.impl.AbstractRaceTrackingConnectivityParametersHandler; +import com.sap.sse.common.TypeBasedServiceFinder; + +/** + * Handles mapping TracTrac connectivity parameters from and to a map with {@link String} keys. The + * "param URL" is considered the {@link #getKey(RaceTrackingConnectivityParameters) key} for these objects.

    + * + * Lives in the same package as {@link RaceTrackingConnectivityParameters} for package-private access to + * its members. + * + * @author Axel Uhl (d043530) + * + */ +public class SwissTimingConnectivityParamsHandler extends AbstractRaceTrackingConnectivityParametersHandler { + private static final String CREW_MEMBER_POSITION = "crewMemberPosition"; + private static final String CREW_MEMBER_NATIONALITY = "crewMemberNationality"; + private static final String CREW_MEMBER_NAME = "crewMemberName"; + private static final String COMPETITOR_CREW = "competitorCrew"; + private static final String COMPETITOR_THREE_LETTER_IOC_CODE = "competitorThreeLetterIocCode"; + private static final String COMPETITOR_NAME = "competitorName"; + private static final String COMPETITOR_BOAT_ID = "competitorBoatId"; + private static final String COMPETITOR_ID = "competitorId"; + private static final String USE_INTERNAL_MARK_PASSING_ALGORITHM = "useInternalMarkPassingAlgorithm"; + private static final String DELAY_TO_LIVE_IN_MILLIS = "delayToLiveInMillis"; + private static final String START_LIST = "startList"; + private static final String RACE_NAME = "raceName"; + private static final String RACE_ID = "raceID"; + private static final String RACE_DESCRIPTION = "raceDescription"; + private static final String PORT = "port"; + private static final String BOAT_CLASS_NAME = "boatClassName"; + private static final String HOSTNAME = "hostname"; + private final RaceLogStore raceLogStore; + private final RegattaLogStore regattaLogStore; + private final DomainFactory domainFactory; + private final SwissTimingFactory swissTimingFactory; + + public SwissTimingConnectivityParamsHandler(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, DomainFactory domainFactory) { + super(); + this.raceLogStore = raceLogStore; + this.regattaLogStore = regattaLogStore; + this.domainFactory = domainFactory; + this.swissTimingFactory = SwissTimingFactory.INSTANCE; + } + + @Override + public Map mapFrom(RaceTrackingConnectivityParameters params) { + assert params instanceof SwissTimingTrackingConnectivityParameters; + final SwissTimingTrackingConnectivityParameters stParams = (SwissTimingTrackingConnectivityParameters) params; + final Map result = getKey(params); + result.put(BOAT_CLASS_NAME, stParams.getBoatClass()==null?null:stParams.getBoatClass().getName()); + result.put(RACE_DESCRIPTION, stParams.getRaceDescription()); + result.put(RACE_NAME, stParams.getRaceName()); + result.put(START_LIST, createStartListDBObject(stParams)); + result.put(DELAY_TO_LIVE_IN_MILLIS, stParams.getDelayToLiveInMillis()); + result.put(USE_INTERNAL_MARK_PASSING_ALGORITHM, stParams.isUseInternalMarkPassingAlgorithm()); + addWindTrackingParameters(stParams, result); + return result; + } + + private BasicDBList createStartListDBObject(final SwissTimingTrackingConnectivityParameters stParams) { + final BasicDBList startListDbObject; + if (stParams.getStartList() != null) { + startListDbObject = new BasicDBList(); + for (final Competitor competitor : stParams.getStartList().getCompetitors()) { + DBObject competitorDBObject = createCompetitorDBObject(competitor); + startListDbObject.add(competitorDBObject); + } + } else { + startListDbObject = null; + } + return startListDbObject; + } + + private StartList createStartListFromDBObject(String raceId, BasicDBList startListDBObject) { + List competitors = new ArrayList<>(); + for (final Object competitorObject : startListDBObject) { + final DBObject competitorDBObject = (DBObject) competitorObject; + competitors.add(createCompetitorFromDBObject(competitorDBObject)); + } + return new StartListImpl(raceId, competitors); + } + + private DBObject createCompetitorDBObject(Competitor competitor) { + final DBObject result = new BasicDBObject(); + result.put(COMPETITOR_ID, competitor.getID()); + result.put(COMPETITOR_BOAT_ID, competitor.getBoatID()); + result.put(COMPETITOR_NAME, competitor.getName()); + result.put(COMPETITOR_THREE_LETTER_IOC_CODE, competitor.getThreeLetterIOCCode()); + result.put(COMPETITOR_CREW, createCrewDbObject(competitor.getCrew())); + return result; + } + + private BasicDBList createCrewDbObject(List crew) { + final BasicDBList result; + if (crew == null) { + result = null; + } else { + result = new BasicDBList(); + for (final CrewMember crewMember : crew) { + final DBObject crewMemberDBObject = new BasicDBObject(); + crewMemberDBObject.put(CREW_MEMBER_NAME, crewMember.getName()); + crewMemberDBObject.put(CREW_MEMBER_NATIONALITY, crewMember.getNationality()); + crewMemberDBObject.put(CREW_MEMBER_POSITION, crewMember.getPosition()); + result.add(crewMemberDBObject); + } + } + return result; + } + + private List createCrewFromDBObject(BasicDBList crewDBObject) { + final List result; + if (crewDBObject == null) { + result = null; + } else { + result = new ArrayList<>(); + for (final Object o : crewDBObject) { + final DBObject crewMemberDBObject = (DBObject) o; + result.add(new CrewMemberImpl((String) crewMemberDBObject.get(CREW_MEMBER_NAME), + (String) crewMemberDBObject.get(CREW_MEMBER_NATIONALITY), + (String) crewMemberDBObject.get(CREW_MEMBER_POSITION))); + } + } + return result; + } + + private Competitor createCompetitorFromDBObject(DBObject competitorDBObject) { + final Competitor result; + final String competitorId = (String) competitorDBObject.get(COMPETITOR_ID); + final String boatID = (String) competitorDBObject.get(COMPETITOR_BOAT_ID); + final String threeLetterIOCCode = (String) competitorDBObject.get(COMPETITOR_THREE_LETTER_IOC_CODE); + final String name = (String) competitorDBObject.get(COMPETITOR_NAME); + final BasicDBList crewDBObject = (BasicDBList) competitorDBObject.get(COMPETITOR_CREW); + if (competitorId == null) { + result = new CompetitorWithoutID(boatID, threeLetterIOCCode, name); + } else { + result = new CompetitorWithID(competitorId, boatID, threeLetterIOCCode, name, createCrewFromDBObject(crewDBObject)); + } + return result; + } + + @Override + public RaceTrackingConnectivityParameters mapTo(Map map) throws MalformedURLException, URISyntaxException { + return new SwissTimingTrackingConnectivityParameters( + (String) map.get(HOSTNAME), + ((Number) map.get(PORT)).intValue(), + (String) map.get(RACE_ID), + (String) map.get(RACE_NAME), + (String) map.get(RACE_DESCRIPTION), + domainFactory.getBaseDomainFactory().getOrCreateBoatClass((String) map.get(BOAT_CLASS_NAME)), + map.get(START_LIST) == null ? null : createStartListFromDBObject((String) map.get(RACE_ID), (BasicDBList) map.get(START_LIST)), + ((Number) map.get(DELAY_TO_LIVE_IN_MILLIS)).longValue(), + swissTimingFactory, domainFactory, raceLogStore, regattaLogStore, + (boolean) map.get(USE_INTERNAL_MARK_PASSING_ALGORITHM), isTrackWind(map), isCorrectWindDirectionByMagneticDeclination(map)); + } + + @Override + public Map getKey(RaceTrackingConnectivityParameters params) { + assert params instanceof SwissTimingTrackingConnectivityParameters; + final SwissTimingTrackingConnectivityParameters stParams = (SwissTimingTrackingConnectivityParameters) params; + final Map result = new HashMap<>(); + result.put(TypeBasedServiceFinder.TYPE, params.getTypeIdentifier()); + result.put(HOSTNAME, stParams.getHostname()); + result.put(PORT, stParams.getPort()); + result.put(RACE_ID, stParams.getRaceID()); + return result; + } +} diff --git a/java/com.sap.sailing.domain.swisstimingadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.swisstimingadapter/META-INF/MANIFEST.MF index ce94c7a2bf0..b382124711f 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.swisstimingadapter/META-INF/MANIFEST.MF @@ -16,7 +16,7 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sailing.xrr.schema, com.sap.sailing.domain.shared.android Export-Package: com.sap.sailing.domain.swisstimingadapter, - com.sap.sailing.domain.swisstimingadapter.impl;x-friends:="com.sap.sailing.server.test" + com.sap.sailing.domain.swisstimingadapter.impl;x-friends:="com.sap.sailing.server.test,com.sap.sailing.mongodb.test,com.sap.sailing.domain.swisstimingadapter.persistence" Import-Package: com.sap.sailing.xrr.schema, org.osgi.framework Bundle-Activator: com.sap.sailing.domain.swisstimingadapter.impl.Activator diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/CrewMember.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/CrewMember.java index 0406a38b8ff..73c926d3717 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/CrewMember.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/CrewMember.java @@ -1,9 +1,9 @@ package com.sap.sailing.domain.swisstimingadapter; public interface CrewMember { - public String getName(); - - public String getNationality(); + public String getName(); - public String getPosition(); + public String getNationality(); + + public String getPosition(); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java index 929614077f3..95cfd8093db 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java @@ -10,6 +10,7 @@ import com.sap.sailing.domain.base.Nationality; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.common.MarkType; import com.sap.sailing.domain.common.tracking.GPSFixMoving; import com.sap.sailing.domain.racelog.RaceLogStore; import com.sap.sailing.domain.regattalog.RegattaLogStore; @@ -31,6 +32,9 @@ public interface DomainFactory { com.sap.sailing.domain.base.DomainFactory getBaseDomainFactory(); + /** + * @param boatClass if {@code null}, the boat class will be inferred from the Race ID + */ Regatta getOrCreateDefaultRegatta(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, String raceID, BoatClass boatClass, TrackedRegattaRegistry trackedRegattaRegistry); Nationality getOrCreateNationality(String threeLetterIOCCode); @@ -62,11 +66,10 @@ public interface DomainFactory { RaceTrackingConnectivityParameters createTrackingConnectivityParameters(String hostname, int port, String raceID, String raceName, String raceDescription, BoatClass boatClass, StartList startList, long delayToLiveInMillis, SwissTimingFactory swissTimingFactory, DomainFactory domainFactory, - RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, boolean useInternalMarkPassingAlgorithm); + RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, boolean useInternalMarkPassingAlgorithm, boolean trackWind, boolean correctWindDirectionByMagneticDeclination); - ControlPoint getOrCreateControlPoint(Iterable devices); - - RaceDefinition createRaceDefinition(Regatta regatta, String raceID, Iterable competitors, - List courseDefinition); + ControlPoint getOrCreateControlPoint(Iterable devices, MarkType markType); + RaceDefinition createRaceDefinition(Regatta regatta, String swissTimingRaceID, Iterable competitors, + List courseDefinition, String raceName, String raceIdForRaceDefinition); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingAdapter.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingAdapter.java index 4d75dbecd80..e275b3573c8 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingAdapter.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingAdapter.java @@ -26,10 +26,12 @@ public interface SwissTimingAdapter { * is specified, a regatta lookup is performed with that identifier; if the regatta is found, it is used * to add the races to. Otherwise, a default regatta as described above will be created and used. * @param useInternalMarkPassingAlgorithm use our own instead of the SwissTiming-provided mark passing / split times + * @param trackWind TODO + * @param correctWindDirectionByMagneticDeclination TODO */ RaceHandle addSwissTimingRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, - StartList startList, RaceLogStore logStore, RegattaLogStore regattaLogStore, long timeoutInMilliseconds, boolean useInternalMarkPassingAlgorithm) + StartList startList, RaceLogStore logStore, RegattaLogStore regattaLogStore, long timeoutInMilliseconds, boolean useInternalMarkPassingAlgorithm, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) throws InterruptedException, UnknownHostException, IOException, ParseException, Exception; StartList readStartListForRace(String raceId, RegattaResults regattaResults); diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java index 8f3780103c4..4c99a0b2c3e 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java @@ -10,6 +10,7 @@ import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.racelog.RaceLogStore; import com.sap.sailing.domain.regattalog.RegattaLogStore; import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingFactoryImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingTrackingConnectivityParameters; import com.sap.sailing.domain.tracking.RaceTracker; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.WindStore; @@ -54,14 +55,15 @@ public interface SwissTimingFactory { BoatClass boatClass, String hostname, int port, StartList startList, long delayToLiveInMillis, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, boolean useInternalMarkPassingAlgorithm, DomainFactory domainFactory, - TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws InterruptedException, UnknownHostException, + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) throws InterruptedException, UnknownHostException, IOException, ParseException; RaceTracker createRaceTracker(Regatta regatta, String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, StartList startList, long delayToLiveInMillis, - WindStore windStore, boolean useInternalMarkPassingAlgorithm, - DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws UnknownHostException, - InterruptedException, IOException, ParseException; + WindStore windStore, boolean useInternalMarkPassingAlgorithm, DomainFactory domainFactory, + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceLogStore raceLogStore, + RegattaLogStore regattaLogStore, SwissTimingTrackingConnectivityParameters connectivityParams) + throws UnknownHostException, InterruptedException, IOException, ParseException; Race createRace(String raceId, String raceName, String description, BoatClass boatClass); diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/AbstractCompetitor.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/AbstractCompetitor.java new file mode 100644 index 00000000000..a36fdf74a26 --- /dev/null +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/AbstractCompetitor.java @@ -0,0 +1,72 @@ +package com.sap.sailing.domain.swisstimingadapter.impl; + +import com.sap.sailing.domain.swisstimingadapter.Competitor; + +public abstract class AbstractCompetitor implements Competitor { + private final String boatID; + private final String threeLetterIOCCode; + private final String name; + + public AbstractCompetitor(String boatID, String threeLetterIOCCode, String name) { + super(); + this.boatID = boatID; + this.threeLetterIOCCode = threeLetterIOCCode; + this.name = name; + } + + @Override + public String getBoatID() { + return boatID; + } + + @Override + public String getThreeLetterIOCCode() { + return threeLetterIOCCode; + } + + @Override + public String getName() { + return name; + } + + public String getID() { + return null; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((boatID == null) ? 0 : boatID.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((threeLetterIOCCode == null) ? 0 : threeLetterIOCCode.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AbstractCompetitor other = (AbstractCompetitor) obj; + if (boatID == null) { + if (other.boatID != null) + return false; + } else if (!boatID.equals(other.boatID)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (threeLetterIOCCode == null) { + if (other.threeLetterIOCCode != null) + return false; + } else if (!threeLetterIOCCode.equals(other.threeLetterIOCCode)) + return false; + return true; + } +} diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithID.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithID.java index 717d4f38716..a90b0d09e91 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithID.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithID.java @@ -2,54 +2,61 @@ package com.sap.sailing.domain.swisstimingadapter.impl; import java.util.List; -import com.sap.sailing.domain.swisstimingadapter.Competitor; import com.sap.sailing.domain.swisstimingadapter.CrewMember; -public class CompetitorWithID implements Competitor { - private final String boatID; - private final String threeLetterIOCCode; - private final String name; +public class CompetitorWithID extends AbstractCompetitor { private final String id; - private List crew; public CompetitorWithID(String id, String boatID, String threeLetterIOCCode, String name, List crew) { - super(); - this.boatID = boatID; - this.threeLetterIOCCode = threeLetterIOCCode; - this.name = name; + super(boatID, threeLetterIOCCode, name); this.id = id; this.crew = crew; } - @Override - public String getBoatID() { - return boatID; - } - - @Override - public String getThreeLetterIOCCode() { - return threeLetterIOCCode; - } - - @Override - public String getName() { - return name; - } - public String getID() { return id; } - @Override - public String toString() { - return "CompetitorWithID [boatID=" + boatID + ", threeLetterIOCCode=" + threeLetterIOCCode + ", name=" + name - + ", id=" + id + "]"; - } - @Override public List getCrew() { return crew; } + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((crew == null) ? 0 : crew.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + CompetitorWithID other = (CompetitorWithID) obj; + if (crew == null) { + if (other.crew != null) + return false; + } else if (!crew.equals(other.crew)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + @Override + public String toString() { + return "CompetitorWithID [boatID=" + getBoatID() + ", threeLetterIOCCode=" + getThreeLetterIOCCode() + ", name=" + getName() + + ", id=" + id + ", crew=" + getCrew() + "]"; + } } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithoutID.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithoutID.java index 80b484c798f..ba734b1fdf2 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithoutID.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithoutID.java @@ -2,49 +2,25 @@ package com.sap.sailing.domain.swisstimingadapter.impl; import java.util.List; -import com.sap.sailing.domain.swisstimingadapter.Competitor; import com.sap.sailing.domain.swisstimingadapter.CrewMember; -public class CompetitorWithoutID implements Competitor { - private final String boatID; - private final String threeLetterIOCCode; - private final String name; - +public class CompetitorWithoutID extends AbstractCompetitor { public CompetitorWithoutID(String boatID, String threeLetterIOCCode, String name) { - super(); - this.boatID = boatID; - this.threeLetterIOCCode = threeLetterIOCCode; - this.name = name; + super(boatID, threeLetterIOCCode, name); } @Override - public String getBoatID() { - return boatID; - } - - @Override - public String getThreeLetterIOCCode() { - return threeLetterIOCCode; - } - - @Override - public String getName() { - return name; - } - public String getID() { return null; } - @Override - public String toString() { - return "CompetitorWithoutID [boatID=" + boatID + ", threeLetterIOCCode=" - + threeLetterIOCCode + ", name=" + name + "]"; - } - - @Override - public List getCrew() { - return null; - } + @Override + public List getCrew() { + return null; + } + @Override + public String toString() { + return "CompetitorWithoutID [boatID=" + getBoatID() + ", threeLetterIOCCode=" + getThreeLetterIOCCode() + ", name=" + getName() + "]"; + } } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CrewMemberImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CrewMemberImpl.java index edd06e5ca15..eebe63cbbc8 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CrewMemberImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CrewMemberImpl.java @@ -3,29 +3,70 @@ package com.sap.sailing.domain.swisstimingadapter.impl; import com.sap.sailing.domain.swisstimingadapter.CrewMember; public class CrewMemberImpl implements CrewMember { - private String name; - private String nationality; - private String position; + private String name; + private String nationality; + private String position; - public CrewMemberImpl(String name, String nationality, String position) { - super(); - this.name = name; - this.nationality = nationality; - this.position = position; - } + public CrewMemberImpl(String name, String nationality, String position) { + super(); + this.name = name; + this.nationality = nationality; + this.position = position; + } - @Override - public String getName() { - return name; - } + @Override + public String getName() { + return name; + } - @Override - public String getNationality() { - return nationality; - } + @Override + public String getNationality() { + return nationality; + } - public String getPosition() { - return position; - } + public String getPosition() { + return position; + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((nationality == null) ? 0 : nationality.hashCode()); + result = prime * result + ((position == null) ? 0 : position.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CrewMemberImpl other = (CrewMemberImpl) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (nationality == null) { + if (other.nationality != null) + return false; + } else if (!nationality.equals(other.nationality)) + return false; + if (position == null) { + if (other.position != null) + return false; + } else if (!position.equals(other.position)) + return false; + return true; + } + + @Override + public String toString() { + return "CrewMemberImpl [name=" + name + ", nationality=" + nationality + ", position=" + position + "]"; + } } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java index fd218645037..57c5daaa79f 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java @@ -28,6 +28,7 @@ import com.sap.sailing.domain.base.impl.PersonImpl; import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.base.impl.TeamImpl; +import com.sap.sailing.domain.common.MarkType; import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.domain.common.ScoringSchemeType; import com.sap.sailing.domain.common.tracking.GPSFixMoving; @@ -176,17 +177,17 @@ public class DomainFactoryImpl implements DomainFactory { } @Override - public RaceDefinition createRaceDefinition(Regatta regatta, String raceID, Iterable competitors, - List courseDefinition) { + public RaceDefinition createRaceDefinition(Regatta regatta, String swissTimingRaceID, Iterable competitors, + List courseDefinition, String raceName, String raceIdForRaceDefinition) { List waypoints = new ArrayList<>(); for (ControlPoint controlPoint : courseDefinition) { Waypoint waypoint = baseDomainFactory.createWaypoint(controlPoint, /* passingInstruction */ PassingInstruction.None); waypoints.add(waypoint); } com.sap.sailing.domain.base.Course domainCourse = new CourseImpl("Course", waypoints); - BoatClass boatClass = getRaceTypeFromRaceID(raceID).getBoatClass(); - logger.info("Creating RaceDefinitionImpl for race "+raceID); - RaceDefinition result = new RaceDefinitionImpl(regatta.getId().toString()+"/"+raceID, domainCourse, boatClass, competitors); + BoatClass boatClass = getRaceTypeFromRaceID(swissTimingRaceID).getBoatClass(); + logger.info("Creating RaceDefinitionImpl for race "+swissTimingRaceID); + RaceDefinition result = new RaceDefinitionImpl(raceName, domainCourse, boatClass, competitors, raceIdForRaceDefinition); regatta.addRace(result); return result; } @@ -264,7 +265,7 @@ public class DomainFactoryImpl implements DomainFactory { private com.sap.sailing.domain.base.Course createCourse(String courseName, Course course) { List waypoints = new ArrayList(); for (Mark mark : course.getMarks()) { - ControlPoint controlPoint = getOrCreateControlPoint(mark.getDevices()); + ControlPoint controlPoint = getOrCreateControlPoint(mark.getDevices(), getMarkType(mark.getMarkType())); Waypoint waypoint = baseDomainFactory.createWaypoint(controlPoint, /* passingInstruction */ PassingInstruction.None); waypoints.add(waypoint); } @@ -272,15 +273,28 @@ public class DomainFactoryImpl implements DomainFactory { return result; } + /** + * Converts a mark type as defined by SwissTiming into a {@link MarkType} as defined by the domain model + */ + private MarkType getMarkType(com.sap.sailing.domain.swisstimingadapter.Mark.MarkType markType) { + final MarkType result; + if (markType == null) { + result = null; + } else { + result = MarkType.BUOY; + } + return result; + } + @Override - public ControlPoint getOrCreateControlPoint(Iterable devices) { + public ControlPoint getOrCreateControlPoint(Iterable devices, MarkType markType) { ControlPoint result; synchronized (controlPointCache) { result = controlPointCache.get(devices); if (result == null) { switch (Util.size(devices)) { case 1: - result = getOrCreateMark(devices.iterator().next()); + result = getOrCreateMark(devices.iterator().next(), markType); break; case 2: Iterator markNameIter = devices.iterator(); @@ -299,6 +313,10 @@ public class DomainFactoryImpl implements DomainFactory { return result; } + private ControlPoint getOrCreateMark(String name, MarkType markType) { + return baseDomainFactory.getOrCreateMark(name, markType); + } + /** * @param trackerID * the "device name" and the "sail number" in case of an {@link MessageType#RPD RPD} message, used as the mark's @@ -321,7 +339,7 @@ public class DomainFactoryImpl implements DomainFactory { List> newDomainControlPoints = new ArrayList>(); for (Mark mark : marks) { // TODO bug 1043: propagate the mark names to the waypoint names - com.sap.sailing.domain.base.ControlPoint domainControlPoint = getOrCreateControlPoint(mark.getDevices()); + com.sap.sailing.domain.base.ControlPoint domainControlPoint = getOrCreateControlPoint(mark.getDevices(), getMarkType(mark.getMarkType())); newDomainControlPoints.add(new com.sap.sse.common.Util.Pair<>(domainControlPoint, PassingInstruction.None)); } courseToUpdate.update(newDomainControlPoints, baseDomainFactory); @@ -359,10 +377,10 @@ public class DomainFactoryImpl implements DomainFactory { public RaceTrackingConnectivityParameters createTrackingConnectivityParameters(String hostname, int port, String raceID, String raceName, String raceDescription, BoatClass boatClass, StartList startList, long delayToLiveInMillis, SwissTimingFactory swissTimingFactory, DomainFactory domainFactory, - RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, boolean useInternalMarkPassingAlgorithm) { + RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, boolean useInternalMarkPassingAlgorithm, + boolean trackWind, boolean correctWindDirectionByMagneticDeclination) { return new SwissTimingTrackingConnectivityParameters(hostname, port, raceID, raceName, raceDescription, boatClass, startList, delayToLiveInMillis, swissTimingFactory, domainFactory, raceLogStore, - regattaLogStore, useInternalMarkPassingAlgorithm); + regattaLogStore, useInternalMarkPassingAlgorithm, trackWind, correctWindDirectionByMagneticDeclination); } - } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java index 5068ea0a08e..d43f6119d75 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java @@ -59,94 +59,96 @@ public class SwissTimingAdapterImpl implements SwissTimingAdapter { @Override public List getSwissTimingRaceRecords(String hostname, - int port) throws InterruptedException, UnknownHostException, IOException, - ParseException { + int port) throws InterruptedException, UnknownHostException, IOException, ParseException { List result = new ArrayList(); // TODO --> Frank: this needs to come from Manage2Sail and its JSON document somehow... return result; } @Override - public RaceHandle addSwissTimingRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, - String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, + public RaceHandle addSwissTimingRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, String raceID, + String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, StartList startList, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, long timeoutInMilliseconds, - boolean useInternalMarkPassingAlgorithm) - throws Exception { - return trackerManager.addRace(regattaToAddTo, swissTimingDomainFactory.createTrackingConnectivityParameters( - hostname, port, raceID, raceName, raceDescription, boatClass, startList, - DEFAULT_SWISSTIMING_LIVE_DELAY_IN_MILLISECONDS, swissTimingFactory, swissTimingDomainFactory, - raceLogStore, regattaLogStore, useInternalMarkPassingAlgorithm), timeoutInMilliseconds); + boolean useInternalMarkPassingAlgorithm, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) throws Exception { + return trackerManager.addRace(regattaToAddTo, + swissTimingDomainFactory.createTrackingConnectivityParameters(hostname, port, raceID, raceName, + raceDescription, boatClass, startList, DEFAULT_SWISSTIMING_LIVE_DELAY_IN_MILLISECONDS, + swissTimingFactory, swissTimingDomainFactory, raceLogStore, regattaLogStore, + useInternalMarkPassingAlgorithm, trackWind, correctWindDirectionByMagneticDeclination), + timeoutInMilliseconds); } @Override public StartList readStartListForRace(String raceId, RegattaResults regattaResults) { - StartList result = null; + StartList result = null; List competitors = new ArrayList(); - Map persons = new HashMap(); - Map boats = new HashMap(); - Map teams = new HashMap(); - // collect boats, persons and teams + Map persons = new HashMap(); + Map boats = new HashMap(); + Map teams = new HashMap(); + // collect boats, persons and teams for (Object o : regattaResults.getPersonOrBoatOrTeam()) { - if(o instanceof Person) { - Person p = (Person) o; - persons.put(p.getPersonID(), p); - } else if (o instanceof Boat) { - Boat b = (Boat) o; - boats.put(b.getBoatID(), b); - } else if (o instanceof Team) { - Team t = (Team) o; - teams.put(t.getTeamID(), t); - } + if (o instanceof Person) { + Person p = (Person) o; + persons.put(p.getPersonID(), p); + } else if (o instanceof Boat) { + Boat b = (Boat) o; + boats.put(b.getBoatID(), b); + } else if (o instanceof Team) { + Team t = (Team) o; + teams.put(t.getTeamID(), t); + } } - + for (Object o : regattaResults.getPersonOrBoatOrTeam()) { if (o instanceof Event) { - Event event = (Event) o; + Event event = (Event) o; for (Object eventO : event.getRaceOrDivisionOrRegattaSeriesResult()) { if (eventO instanceof Division) { Division division = (Division) eventO; - for(Object seriesResultOrRaceResultOrTRResult: division.getSeriesResultOrRaceResultOrTRResult()) { - if(seriesResultOrRaceResultOrTRResult instanceof RaceResult) { - RaceResult raceResult = (RaceResult) seriesResultOrRaceResultOrTRResult; - if(raceResult.getRaceID().equals(raceId)) { - Team team = teams.get(raceResult.getTeamID()); - if(team != null) { - Boat boat = boats.get(team.getBoatID()); - if(boat != null) { - List crew = new ArrayList(); - for(Crew crewMember: team.getCrew()) { - Person person = persons.get(crewMember.getPersonID()); - crew.add(new CrewMemberImpl(person.getGivenName() + " " + person.getFamilyName(), - person.getNOC().name(), crewMember.getPosition().name())); - } - Crew firstCrewMember = team.getCrew().get(0); - Person person = persons.get(firstCrewMember.getPersonID()); - String nationality = person.getNOC().name(); - CompetitorWithID competitor = new CompetitorWithID(team.getTeamID(), boat.getSailNumber(), nationality, team.getTeamName(), crew); - competitors.add(competitor); - } - } - } - } + for (Object seriesResultOrRaceResultOrTRResult : division.getSeriesResultOrRaceResultOrTRResult()) { + if (seriesResultOrRaceResultOrTRResult instanceof RaceResult) { + RaceResult raceResult = (RaceResult) seriesResultOrRaceResultOrTRResult; + if (raceResult.getRaceID().equals(raceId)) { + Team team = teams.get(raceResult.getTeamID()); + if (team != null) { + Boat boat = boats.get(team.getBoatID()); + if (boat != null) { + List crew = new ArrayList(); + for (Crew crewMember : team.getCrew()) { + Person person = persons.get(crewMember.getPersonID()); + crew.add(new CrewMemberImpl( + person.getGivenName() + " " + person.getFamilyName(), + person.getNOC().name(), crewMember.getPosition().name())); + } + Crew firstCrewMember = team.getCrew().get(0); + Person person = persons.get(firstCrewMember.getPersonID()); + String nationality = person.getNOC().name(); + CompetitorWithID competitor = new CompetitorWithID(team.getTeamID(), + boat.getSailNumber(), nationality, team.getTeamName(), crew); + competitors.add(competitor); + } + } + } + } } } } } } - if(!competitors.isEmpty()) { - result = new StartListImpl(raceId, competitors); + if (!competitors.isEmpty()) { + result = new StartListImpl(raceId, competitors); } - return result; } - + @Override public RegattaResults readRegattaEntryListFromXrrUrl(String xrrEntryListUrl) throws IOException, JAXBException { - // try to read the entry list from manage2sail + // try to read the entry list from manage2sail URL regattaEntryListUrl = new URL(xrrEntryListUrl); URLConnection regattaEntryListConn = regattaEntryListUrl.openConnection(); - RegattaResults regattaResults = ParserFactory.INSTANCE.createParser((InputStream) regattaEntryListConn.getContent(), xrrEntryListUrl).parse(); + RegattaResults regattaResults = ParserFactory.INSTANCE + .createParser((InputStream) regattaEntryListConn.getContent(), xrrEntryListUrl).parse(); return regattaResults; } } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java index b0473ba08e5..81a626ed55a 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java @@ -92,22 +92,23 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory { BoatClass boatClass, String hostname, int port, StartList startList, long delayToLiveInMillis, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, boolean useInternalMarkPassingAlgorithm, DomainFactory domainFactory, - TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) throws InterruptedException, UnknownHostException, IOException, ParseException { return new SwissTimingRaceTrackerImpl(raceID, raceName, raceDescription, boatClass, hostname, port, startList, raceLogStore, regattaLogStore, windStore, domainFactory, this, - trackedRegattaRegistry, raceLogResolver, delayToLiveInMillis, useInternalMarkPassingAlgorithm); + trackedRegattaRegistry, raceLogResolver, delayToLiveInMillis, useInternalMarkPassingAlgorithm, connectivityParams); } @Override public RaceTracker createRaceTracker(Regatta regatta, String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, StartList startList, long delayToLiveInMillis, - WindStore windStore, boolean useInternalMarkPassingAlgorithm, - DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) + WindStore windStore, boolean useInternalMarkPassingAlgorithm, DomainFactory domainFactory, + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceLogStore raceLogStore, + RegattaLogStore regattaLogStore, SwissTimingTrackingConnectivityParameters connectivityParams) throws UnknownHostException, InterruptedException, IOException, ParseException { return new SwissTimingRaceTrackerImpl(regatta, raceID, raceName, raceDescription, boatClass, hostname, port, - startList, windStore, domainFactory, this, trackedRegattaRegistry, raceLogResolver, delayToLiveInMillis, - useInternalMarkPassingAlgorithm); + startList, windStore, domainFactory, this, trackedRegattaRegistry, raceLogStore, regattaLogStore, + raceLogResolver, delayToLiveInMillis, useInternalMarkPassingAlgorithm, connectivityParams); } @Override diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java index d0e7098d876..ffe677df1c0 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java @@ -10,7 +10,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NavigableSet; -import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -112,25 +111,34 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl protected SwissTimingRaceTrackerImpl(String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, StartList startList, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, DomainFactory domainFactory, SwissTimingFactory factory, - TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, long delayToLiveInMillis, boolean useInternalMarkPassingAlgorithm) throws InterruptedException, + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, long delayToLiveInMillis, + boolean useInternalMarkPassingAlgorithm, SwissTimingTrackingConnectivityParameters connectivityParams) throws InterruptedException, UnknownHostException, IOException, ParseException { - this(domainFactory.getOrCreateDefaultRegatta(raceLogStore, regattaLogStore, raceID, - boatClass, trackedRegattaRegistry), raceID, raceName, - raceDescription, boatClass, hostname, port, startList, windStore, domainFactory, factory, - trackedRegattaRegistry, - raceLogResolver, delayToLiveInMillis, useInternalMarkPassingAlgorithm); + this(/* regatta */ null, raceID, raceName, raceDescription, boatClass, hostname, port, startList, windStore, + domainFactory, factory, trackedRegattaRegistry, raceLogStore, regattaLogStore, raceLogResolver, + delayToLiveInMillis, useInternalMarkPassingAlgorithm, connectivityParams); } protected SwissTimingRaceTrackerImpl(Regatta regatta, String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, StartList startList, WindStore windStore, - DomainFactory domainFactory, SwissTimingFactory factory, - TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, - long delayToLiveInMillis, boolean useInternalMarkPassingAlgorithm) throws InterruptedException, UnknownHostException, IOException, - ParseException { - super(); + DomainFactory domainFactory, SwissTimingFactory factory, TrackedRegattaRegistry trackedRegattaRegistry, + RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, RaceLogResolver raceLogResolver, + long delayToLiveInMillis, boolean useInternalMarkPassingAlgorithm, SwissTimingTrackingConnectivityParameters connectivityParams) + throws InterruptedException, UnknownHostException, IOException, ParseException { + super(connectivityParams); this.raceLogResolver = raceLogResolver; this.tmdMessageQueue = new TMDMessageQueue(this); - this.regatta = regatta; + final Regatta effectiveRegatta; + // Try to find a pre-associated event based on the Race ID + if (regatta == null) { + effectiveRegatta = trackedRegattaRegistry.getRememberedRegattaForRace(raceID); + } else { + effectiveRegatta = regatta; + } + // if regatta is still null, no previous assignment of any of the races in this TracTrac event to a Regatta was + // found; in this case, create a default regatta based on the TracTrac event data + this.regatta = effectiveRegatta == null ? domainFactory.getOrCreateDefaultRegatta(raceLogStore, regattaLogStore, + raceID, boatClass, trackedRegattaRegistry) : effectiveRegatta; this.connector = factory.getOrCreateSailMasterConnector(hostname, port, raceID, raceName, raceDescription, boatClass); this.domainFactory = domainFactory; this.raceID = raceID; @@ -142,11 +150,10 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl this.windStore = windStore; this.id = createID(raceID, hostname, port); connector.addSailMasterListener(this); - trackedRegatta = trackedRegattaRegistry.getOrCreateTrackedRegatta(regatta); + trackedRegatta = trackedRegattaRegistry.getOrCreateTrackedRegatta(this.regatta); this.delayToLiveInMillis = delayToLiveInMillis; this.competitorsByBoatId = new HashMap(); this.useInternalMarkPassingAlgorithm = useInternalMarkPassingAlgorithm; - } @Override @@ -168,13 +175,12 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl } @Override - public Set getRaces() { - final Set emptySet = Collections.emptySet(); - return race==null?emptySet:Collections.singleton(race); + public RaceDefinition getRace() { + return race; } @Override - public RaceHandle getRacesHandle() { + public RaceHandle getRaceHandle() { return new RaceHandle() { @Override public Regatta getRegatta() { @@ -479,6 +485,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl assert SwissTimingRaceTrackerImpl.this.race == race; } }, useInternalMarkPassingAlgorithm, raceLogResolver); + notifyRaceCreationListeners(); logger.info("Created SwissTiming RaceDefinition and TrackedRace for "+race.getName()); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingTrackingConnectivityParameters.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingTrackingConnectivityParameters.java index ee8935c98fa..3c54e7020ef 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingTrackingConnectivityParameters.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingTrackingConnectivityParameters.java @@ -10,11 +10,13 @@ import com.sap.sailing.domain.swisstimingadapter.DomainFactory; import com.sap.sailing.domain.swisstimingadapter.StartList; import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; import com.sap.sailing.domain.tracking.RaceTracker; -import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tracking.impl.AbstractRaceTrackingConnectivityParameters; -public class SwissTimingTrackingConnectivityParameters implements RaceTrackingConnectivityParameters { +public class SwissTimingTrackingConnectivityParameters extends AbstractRaceTrackingConnectivityParameters { + public static final String TYPE = "SWISS_TIMING"; + private final String hostname; private final int port; private final String raceID; @@ -32,8 +34,8 @@ public class SwissTimingTrackingConnectivityParameters implements RaceTrackingCo public SwissTimingTrackingConnectivityParameters(String hostname, int port, String raceID, String raceName, String raceDescription, BoatClass boatClass, StartList startList, long delayToLiveInMillis, SwissTimingFactory swissTimingFactory, DomainFactory domainFactory, RaceLogStore raceLogStore, - RegattaLogStore regattaLogStore, boolean useInternalMarkPassingAlgorithm) { - super(); + RegattaLogStore regattaLogStore, boolean useInternalMarkPassingAlgorithm, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) { + super(trackWind, correctWindDirectionByMagneticDeclination); this.hostname = hostname; this.port = port; this.raceID = raceID; @@ -49,13 +51,18 @@ public class SwissTimingTrackingConnectivityParameters implements RaceTrackingCo this.useInternalMarkPassingAlgorithm = useInternalMarkPassingAlgorithm; } + @Override + public String getTypeIdentifier() { + return TYPE; + } + @Override public RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, RaceLogResolver raceLogResolver) throws Exception { return swissTimingFactory.createRaceTracker(raceID, raceName, raceDescription, boatClass, hostname, port, startList, delayToLiveInMillis, raceLogStore, regattaLogStore, windStore, useInternalMarkPassingAlgorithm, - domainFactory, trackedRegattaRegistry, raceLogResolver); + domainFactory, trackedRegattaRegistry, raceLogResolver, this); } @Override @@ -63,7 +70,7 @@ public class SwissTimingTrackingConnectivityParameters implements RaceTrackingCo WindStore windStore, RaceLogResolver raceLogResolver) throws Exception { return swissTimingFactory.createRaceTracker(regatta, raceID, raceName, raceDescription, boatClass, hostname, port, startList, delayToLiveInMillis, windStore, useInternalMarkPassingAlgorithm, - domainFactory, trackedRegattaRegistry, raceLogResolver); + domainFactory, trackedRegattaRegistry, raceLogResolver, raceLogStore, regattaLogStore, this); } @Override @@ -75,4 +82,36 @@ public class SwissTimingTrackingConnectivityParameters implements RaceTrackingCo public long getDelayToLiveInMillis() { return delayToLiveInMillis; } + + public String getHostname() { + return hostname; + } + + public int getPort() { + return port; + } + + public String getRaceID() { + return raceID; + } + + public String getRaceName() { + return raceName; + } + + public String getRaceDescription() { + return raceDescription; + } + + public BoatClass getBoatClass() { + return boatClass; + } + + public StartList getStartList() { + return startList; + } + + public boolean isUseInternalMarkPassingAlgorithm() { + return useInternalMarkPassingAlgorithm; + } } diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingRaceConfigurationTest.java b/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingRaceConfigurationTest.java index 0de4deffcde..2f261e29719 100644 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingRaceConfigurationTest.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingRaceConfigurationTest.java @@ -17,9 +17,6 @@ import java.util.Map; import org.junit.Ignore; import org.junit.Test; -import static org.mockito.Mockito.mock; - -import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.swisstimingadapter.DomainFactory; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayRace; import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingRaceConfig; @@ -33,13 +30,13 @@ public class SwissTimingRaceConfigurationTest { @Test public void testLoadConfigurations() throws IOException, ParseException, org.json.simple.parser.ParseException { InputStream inputStream = getClass().getResourceAsStream(JSON_URL); - List races = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE, mock(RaceLogResolver.class)).parseJSONObject(inputStream , JSON_URL); + List races = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE).parseJSONObject(inputStream , JSON_URL); Map configsById = new HashMap(); for (SwissTimingReplayRace race : races) { URL configFileURL = new URL(MessageFormat.format(SwissTimingReplayServiceImpl.RACE_CONFIG_URL_TEMPLATE, race.getRaceId())); URLConnection connection = configFileURL.openConnection(); InputStream configDataStream = connection.getInputStream(); - SwissTimingRaceConfig raceConfig = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE, mock(RaceLogResolver.class)).loadRaceConfig(configDataStream); + SwissTimingRaceConfig raceConfig = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE).loadRaceConfig(configDataStream); configsById.put(race.getRaceId(), raceConfig); } } @@ -49,7 +46,7 @@ public class SwissTimingRaceConfigurationTest { URL configFileURL = new URL(MessageFormat.format(SwissTimingReplayServiceImpl.RACE_CONFIG_URL_TEMPLATE, "446483")); URLConnection connection = configFileURL.openConnection(); InputStream configDataStream = connection.getInputStream(); - SwissTimingRaceConfig config_446483 = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE, mock(RaceLogResolver.class)).loadRaceConfig(configDataStream); + SwissTimingRaceConfig config_446483 = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE).loadRaceConfig(configDataStream); assertNotNull(config_446483); assertEquals("GB", config_446483.country_code); assertNull(config_446483.event_name); @@ -66,7 +63,7 @@ public class SwissTimingRaceConfigurationTest { URL configFileURL = new URL(MessageFormat.format(SwissTimingReplayServiceImpl.RACE_CONFIG_URL_TEMPLATE, "6260")); URLConnection connection = configFileURL.openConnection(); InputStream configDataStream = connection.getInputStream(); - SwissTimingRaceConfig config_446483 = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE, mock(RaceLogResolver.class)).loadRaceConfig(configDataStream); + SwissTimingRaceConfig config_446483 = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE).loadRaceConfig(configDataStream); assertNotNull(config_446483); assertEquals("GB", config_446483.country_code); assertEquals("London 2012 Olympic Games", config_446483.event_name); diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingReplayAdapterServiceTest.java b/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingReplayAdapterServiceTest.java index e5d6c133ebd..1cdb1ec1218 100644 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingReplayAdapterServiceTest.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingReplayAdapterServiceTest.java @@ -20,6 +20,8 @@ import org.junit.Test; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; import com.sap.sailing.domain.swisstimingadapter.DomainFactory; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayRace; import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingReplayParserImpl; @@ -35,7 +37,7 @@ public class SwissTimingReplayAdapterServiceTest { public void testLoadRaceJson() throws Exception { String swissTimingUrlText = "/2012_OSG.json"; InputStream inputStream = getClass().getResourceAsStream(swissTimingUrlText); - final SwissTimingReplayServiceImpl swissTimingReplayService = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE, mock(RaceLogResolver.class)); + final SwissTimingReplayServiceImpl swissTimingReplayService = new SwissTimingReplayServiceImpl(DomainFactory.INSTANCE); List races = swissTimingReplayService.parseJSONObject(inputStream , swissTimingUrlText); assertEquals(201, races.size()); assertEquals("446483", races.get(0).getRaceId()); @@ -179,8 +181,9 @@ public class SwissTimingReplayAdapterServiceTest { @Test public void testRaceData_SAW010955_20120802_WithDomainAdapter() throws Exception { - SwissTimingReplayToDomainAdapter replayListener = new SwissTimingReplayToDomainAdapter(null, DomainFactory.INSTANCE, - new DummyTrackedRegattaRegistry(), /* useInternalMarkPassingAlgorithm */ false, mock(RaceLogResolver.class)); + SwissTimingReplayToDomainAdapter replayListener = new SwissTimingReplayToDomainAdapter(null, /* raceName */ "SAW005906", + "SAW005906", /* boatClass */ null, DomainFactory.INSTANCE, new DummyTrackedRegattaRegistry(), /* useInternalMarkPassingAlgorithm */ false, + mock(RaceLogResolver.class), EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, /* tracker creator */ null); new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAW005906.20120805.replay"), replayListener); Iterable trackedRaces = replayListener.getTrackedRaces(); assertFalse(Util.isEmpty(trackedRaces)); @@ -193,8 +196,9 @@ public class SwissTimingReplayAdapterServiceTest { @Test public void testStartPerformanceDetection() throws Exception { - SwissTimingReplayToDomainAdapter replayListener = new SwissTimingReplayToDomainAdapter(null, DomainFactory.INSTANCE, - new DummyTrackedRegattaRegistry(), /* useInternalMarkPassingAlgorithm */ false, mock(RaceLogResolver.class)); + SwissTimingReplayToDomainAdapter replayListener = new SwissTimingReplayToDomainAdapter(null, /* raceName */ "SAW005905", + "SAW005905", /* boatClass */ null, DomainFactory.INSTANCE, new DummyTrackedRegattaRegistry(), /* useInternalMarkPassingAlgorithm */ false, + mock(RaceLogResolver.class), EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, /* tracker creator */ null); new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAW005905.20120805.replay"), replayListener); Iterable trackedRaces = replayListener.getTrackedRaces(); TrackedRace trackedRace = trackedRaces.iterator().next(); diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.swisstimingreplayadapter/META-INF/MANIFEST.MF index 0507cad7850..aa480ccf2a8 100644 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/META-INF/MANIFEST.MF @@ -13,7 +13,10 @@ Require-Bundle: com.sap.sailing.domain.common, com.googlecode.java-diff-utils;bundle-version="1.3.0", com.sap.sse, com.sap.sse.common, - com.sap.sailing.domain.shared.android -Export-Package: com.sap.sailing.domain.swisstimingreplayadapter -Import-Package: org.osgi.framework;version="1.7.0" + com.sap.sailing.domain.shared.android, + com.sap.sailing.domain.persistence +Export-Package: com.sap.sailing.domain.swisstimingreplayadapter, + com.sap.sailing.domain.swisstimingreplayadapter.impl;x-friends:="com.sap.sailing.mongodb.test" +Import-Package: org.osgi.framework;version="1.7.0", + org.osgi.util.tracker;version="1.5.1" Bundle-Activator: com.sap.sailing.domain.swisstimingreplayadapter.impl.Activator diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/SwissTimingReplayService.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/SwissTimingReplayService.java index 7000cd9e137..2553cc2d0d0 100755 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/SwissTimingReplayService.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/SwissTimingReplayService.java @@ -1,14 +1,20 @@ package com.sap.sailing.domain.swisstimingreplayadapter; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.text.DateFormat; import java.text.ParseException; import java.util.List; -import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.regattalog.RegattaLogStore; import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingRaceConfig; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.TrackerManager; public interface SwissTimingReplayService { @@ -22,6 +28,30 @@ public interface SwissTimingReplayService { DateFormat getStartTimeFormat(); + /** + * @param regattaToAddTo + * the regatta to which the race shall be added; if null, a default regatta will be + * created/used based on the {@code regattaName} and {@code boatClassName} parameter values + * @param link + * the URL without the implicit "http://" prefix, as obtained, e.g., from + * {@link SwissTimingReplayRace#getLink()}. + * @param raceName TODO + * @param raceID + * the SwissTiming ID for the race + * @param trackerManager TODO + * @param useInternalMarkPassingAlgorithm + * use our own instead of the SwissTiming-provided mark rounding / split times + * @param raceLogStore TODO + * @param regattaLogStore TODO + * @param boatClassNameName + * only required if {@code regattaToAddTo} is {@code null}; used for the retrieval/creation of a default + * regatta + */ + void loadRaceData(RegattaIdentifier regattaToAddTo, String link, String raceName, String raceID, + String boatClassName, TrackerManager trackerManager, TrackedRegattaRegistry trackedRegattaRegistry, + boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore) + throws MalformedURLException, FileNotFoundException, URISyntaxException, Exception; + /** * @param link * the URL without the implicit "http://" prefix, as obtained, e.g., from @@ -30,17 +60,6 @@ public interface SwissTimingReplayService { * @param replayListener * the listener to receive all persing events */ - void loadRaceData(String link, SwissTimingReplayListener replayListener); - - /** - * @param link - * the URL without the implicit "http://" prefix, as obtained, e.g., from - * {@link SwissTimingReplayRace#getLink()}. - * @param regatta - * the regatta to which the race shall be added; if null, a default regatta will be - * created/used - * @param useInternalMarkPassingAlgorithm use our own instead of the SwissTiming-provided mark rounding / split times - */ - void loadRaceData(String link, Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, boolean useInternalMarkPassingAlgorithm); + void loadRaceData(String link, SwissTimingReplayListener listener); } diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/Activator.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/Activator.java index 28bb91703a6..14c4ad44cc1 100755 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/Activator.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/Activator.java @@ -1,12 +1,24 @@ package com.sap.sailing.domain.swisstimingreplayadapter.impl; +import java.util.Dictionary; +import java.util.Hashtable; import java.util.logging.Level; import java.util.logging.Logger; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.util.tracker.ServiceTracker; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; +import com.sap.sailing.domain.persistence.MongoRegattaLogStoreFactory; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapterFactory; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayServiceFactory; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParametersHandler; +import com.sap.sse.common.TypeBasedServiceFinder; +import com.sap.sse.util.ServiceTrackerFactory; public class Activator implements BundleActivator { private static final Logger logger = Logger.getLogger(Activator.class.getName()); @@ -21,6 +33,30 @@ public class Activator implements BundleActivator { public void start(BundleContext context) throws Exception { // register the racing service in the OSGi registry context.registerService(SwissTimingReplayServiceFactory.class.getName(), swissTimingReplayServiceFactory, null); + new Thread(() -> { + final ServiceTracker mongoObjectFactoryServiceTracker = ServiceTrackerFactory.createAndOpen(context, MongoObjectFactory.class); + final ServiceTracker domainObjectFactoryServiceTracker = ServiceTrackerFactory.createAndOpen(context, DomainObjectFactory.class); + final ServiceTracker swissTimingAdapterFactoryServiceTracker = ServiceTrackerFactory.createAndOpen(context, SwissTimingAdapterFactory.class); + final ServiceTracker raceLogResolverServiceTracker = ServiceTrackerFactory.createAndOpen(context, RaceLogResolver.class); + try { + final MongoObjectFactory mongoObjectFactory = mongoObjectFactoryServiceTracker.waitForService(0); + final DomainObjectFactory domainObjectFactory = domainObjectFactoryServiceTracker.waitForService(0); + final SwissTimingAdapterFactory swissTimingAdapterFactory = swissTimingAdapterFactoryServiceTracker.waitForService(0); + final RaceLogResolver raceLogResolver = raceLogResolverServiceTracker.waitForService(0); + final Dictionary properties = new Hashtable(); + final com.sap.sailing.domain.swisstimingadapter.DomainFactory domainFactory = swissTimingAdapterFactory + .getOrCreateSwissTimingAdapter(domainObjectFactory.getBaseDomainFactory()) + .getSwissTimingDomainFactory(); + final SwissTimingReplayConnectivityParamsHandler paramsHandler = new SwissTimingReplayConnectivityParamsHandler( + MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory), + MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore(mongoObjectFactory, domainObjectFactory), + domainFactory, swissTimingReplayServiceFactory.createSwissTimingReplayService(domainFactory, raceLogResolver)); + properties.put(TypeBasedServiceFinder.TYPE, SwissTimingReplayConnectivityParameters.TYPE); + context.registerService(RaceTrackingConnectivityParametersHandler.class, paramsHandler, properties); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception trying to register SwissTiming Replay RaceTrackingConnectivityParametersHandler implementation", e); + } + }, getClass().getName() + " registering connectivity handler").start(); logger.log(Level.INFO, "Started "+context.getBundle().getSymbolicName()); } diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayAdapter.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayAdapter.java index 648886923ef..07b1ecfee16 100755 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayAdapter.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayAdapter.java @@ -3,7 +3,7 @@ package com.sap.sailing.domain.swisstimingreplayadapter.impl; import com.sap.sailing.domain.swisstimingreplayadapter.CompetitorStatus; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayListener; -public class SwissTimingReplayAdapter implements SwissTimingReplayListener { +public abstract class SwissTimingReplayAdapter implements SwissTimingReplayListener { @Override public void keyFrameIndex(int keyFrameIndex) {} diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayConnectivityParameters.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayConnectivityParameters.java new file mode 100644 index 00000000000..613c36896c7 --- /dev/null +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayConnectivityParameters.java @@ -0,0 +1,192 @@ +package com.sap.sailing.domain.swisstimingreplayadapter.impl; + +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.swisstimingadapter.DomainFactory; +import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayService; +import com.sap.sailing.domain.tracking.AbstractRaceTrackerImpl; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.RaceHandle; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tracking.impl.AbstractRaceTrackingConnectivityParameters; +import com.sap.sse.common.Util; + +public class SwissTimingReplayConnectivityParameters extends AbstractRaceTrackingConnectivityParameters { + public static final String TYPE = "SWISS_TIMING_REPLAY"; + + private final boolean useInternalMarkPassingAlgorithm; + private final DomainFactory domainFactory; + private final String boatClassName; + private final RaceLogStore raceLogStore; + private final RegattaLogStore regattaLogStore; + private final String raceName; + private final String raceID; + private final String link; + private final SwissTimingReplayService replayService; + + class SwissTimingReplayRaceTracker extends AbstractRaceTrackerImpl { + private final WindStore windStore; + private SwissTimingReplayToDomainAdapter listener; + + public SwissTimingReplayRaceTracker(WindStore windStore, SwissTimingReplayToDomainAdapter listener, + SwissTimingReplayConnectivityParameters connectivityParams) { + super(connectivityParams); + this.windStore = windStore; + this.listener = listener; + } + + @Override + public Regatta getRegatta() { + return listener.getRegatta(); + } + + @Override + public void notifyRaceCreationListeners() { + super.notifyRaceCreationListeners(); + } + + @Override + public RaceDefinition getRace() { + RaceDefinition race; + try { + race = listener.getRaceDefinition(raceID, 1 /* very short timeout */); + return race; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public DynamicTrackedRegatta getTrackedRegatta() { + return listener.getTrackedRegatta(); + } + + @Override + public RaceHandle getRaceHandle() { + return new RaceHandle() { + @Override + public Regatta getRegatta() { + return listener.getRegatta(); + } + + @Override + public RaceDefinition getRace() { + try { + return listener.getRaceDefinition(raceID); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public RaceDefinition getRace(long timeoutInMilliseconds) { + try { + return listener.getRaceDefinition(raceID, timeoutInMilliseconds); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public DynamicTrackedRegatta getTrackedRegatta() { + return SwissTimingReplayRaceTracker.this.getTrackedRegatta(); + } + + @Override + public RaceTracker getRaceTracker() { + return SwissTimingReplayRaceTracker.this; + } + }; + } + + @Override + public WindStore getWindStore() { + return windStore; + } + + @Override + public Object getID() { + return getTrackerID(); + } + } + + public SwissTimingReplayConnectivityParameters(String link, String raceName, String raceID, String boatClassName, + boolean useInternalMarkPassingAlgorithm, DomainFactory domainFactory, + SwissTimingReplayService replayService, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore) { + super(/* trackWind: never track live wind with a SwissTiming Replay race */ false, /* correctWindDirectionByMagneticDeclination */ false); + this.link = link; + this.raceName = raceName; + this.raceID = raceID; + this.boatClassName = boatClassName; + this.useInternalMarkPassingAlgorithm = useInternalMarkPassingAlgorithm; + this.domainFactory = domainFactory; + this.replayService = replayService; + this.raceLogStore = raceLogStore; + this.regattaLogStore = regattaLogStore; + } + + @Override + public RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, final WindStore windStore, + RaceLogResolver raceLogResolver) throws Exception { + SwissTimingReplayToDomainAdapter listener = new SwissTimingReplayToDomainAdapter(/* regatta */ null, raceName, + raceID, domainFactory.getBaseDomainFactory().getOrCreateBoatClass(boatClassName), domainFactory, + trackedRegattaRegistry, useInternalMarkPassingAlgorithm, raceLogResolver, raceLogStore, + regattaLogStore, l->new SwissTimingReplayRaceTracker(windStore, l, this)); + replayService.loadRaceData(link, listener); + return listener.getTracker(); + } + + @Override + public RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, + WindStore windStore, RaceLogResolver raceLogResolver) throws Exception { + SwissTimingReplayToDomainAdapter listener = new SwissTimingReplayToDomainAdapter(regatta, raceName, + raceID, domainFactory.getBaseDomainFactory().getOrCreateBoatClass(boatClassName), + domainFactory, trackedRegattaRegistry, useInternalMarkPassingAlgorithm, raceLogResolver, + raceLogStore, regattaLogStore, l->new SwissTimingReplayRaceTracker(windStore, l, this)); + replayService.loadRaceData(link, listener); + return listener.getTracker(); + } + + @Override + public Object getTrackerID() { + return new Util.Pair<>(link, raceID); + } + + /** + * There is no live tracking with this SwissTiming replay connector + */ + @Override + public long getDelayToLiveInMillis() { + return 0; + } + + @Override + public String getTypeIdentifier() { + return TYPE; + } + + public boolean isUseInternalMarkPassingAlgorithm() { + return useInternalMarkPassingAlgorithm; + } + + public String getBoatClassName() { + return boatClassName; + } + + public String getRaceName() { + return raceName; + } + + public String getRaceID() { + return raceID; + } + + public String getLink() { + return link; + } +} diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayConnectivityParamsHandler.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayConnectivityParamsHandler.java new file mode 100644 index 00000000000..8ce4bf01492 --- /dev/null +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayConnectivityParamsHandler.java @@ -0,0 +1,78 @@ +package com.sap.sailing.domain.swisstimingreplayadapter.impl; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.swisstimingadapter.DomainFactory; +import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayService; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.impl.AbstractRaceTrackingConnectivityParametersHandler; +import com.sap.sse.common.TypeBasedServiceFinder; + +/** + * Handles mapping TracTrac connectivity parameters from and to a map with {@link String} keys. The + * "param URL" is considered the {@link #getKey(RaceTrackingConnectivityParameters) key} for these objects.

    + * + * Lives in the same package as {@link RaceTrackingConnectivityParameters} for package-private access to + * its members. + * + * @author Axel Uhl (d043530) + * + */ +public class SwissTimingReplayConnectivityParamsHandler extends AbstractRaceTrackingConnectivityParametersHandler { + private static final String BOAT_CLASS_NAME = "boatClassName"; + private static final String RACE_ID = "raceId"; + private static final String RACE_NAME = "raceName"; + private static final String LINK = "link"; + private static final String USE_INTERNAL_MARK_PASSING_ALGORITHM = "useInternalMarkPassingAlgorithm"; + private static final String DELAY_TO_LIVE_IN_MILLIS = "delayToLiveInMillis"; + private final RaceLogStore raceLogStore; + private final RegattaLogStore regattaLogStore; + private final DomainFactory domainFactory; + private final SwissTimingReplayService replayService; + + public SwissTimingReplayConnectivityParamsHandler(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + DomainFactory domainFactory, SwissTimingReplayService replayService) { + super(); + this.raceLogStore = raceLogStore; + this.regattaLogStore = regattaLogStore; + this.domainFactory = domainFactory; + this.replayService = replayService; + } + + @Override + public Map mapFrom(RaceTrackingConnectivityParameters params) { + assert params instanceof SwissTimingReplayConnectivityParameters; + final SwissTimingReplayConnectivityParameters stParams = (SwissTimingReplayConnectivityParameters) params; + final Map result = getKey(params); + result.put(DELAY_TO_LIVE_IN_MILLIS, stParams.getDelayToLiveInMillis()); + result.put(USE_INTERNAL_MARK_PASSING_ALGORITHM, stParams.isUseInternalMarkPassingAlgorithm()); + result.put(RACE_NAME, stParams.getRaceName()); + result.put(RACE_ID, stParams.getRaceID()); + result.put(BOAT_CLASS_NAME, stParams.getBoatClassName()); + addWindTrackingParameters(stParams, result); + return result; + } + + @Override + public RaceTrackingConnectivityParameters mapTo(Map map) throws MalformedURLException, URISyntaxException { + return new SwissTimingReplayConnectivityParameters((String) map.get(LINK), (String) map.get(RACE_NAME), + (String) map.get(RACE_ID), (String) map.get(BOAT_CLASS_NAME), + (boolean) map.get(USE_INTERNAL_MARK_PASSING_ALGORITHM), domainFactory, replayService, raceLogStore, + regattaLogStore); + } + + @Override + public Map getKey(RaceTrackingConnectivityParameters params) { + assert params instanceof SwissTimingReplayConnectivityParameters; + final SwissTimingReplayConnectivityParameters stParams = (SwissTimingReplayConnectivityParameters) params; + final Map result = new HashMap<>(); + result.put(TypeBasedServiceFinder.TYPE, params.getTypeIdentifier()); + result.put(LINK, stParams.getLink()); + return result; + } +} diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayParserImpl.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayParserImpl.java index bdb1d70be5c..115fa596777 100644 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayParserImpl.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayParserImpl.java @@ -15,7 +15,6 @@ public class SwissTimingReplayParserImpl implements SwissTimingReplayParser { private static final long serialVersionUID = 1L; public UnknownMessageIdentificationCode(byte messageIdentificationCode, long position) { super("Unknown message identification code: " + messageIdentificationCode+" at position "+position); - } } diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceFactoryImpl.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceFactoryImpl.java index 5334e7ea2f0..df45fcb2d30 100755 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceFactoryImpl.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceFactoryImpl.java @@ -19,7 +19,7 @@ public class SwissTimingReplayServiceFactoryImpl implements SwissTimingReplaySer public SwissTimingReplayService createSwissTimingReplayService(DomainFactory domainFactory, RaceLogResolver raceLogResolver) { SwissTimingReplayService result = servicesForDomainFactories.get(domainFactory); if (result == null) { - result = new SwissTimingReplayServiceImpl(domainFactory, raceLogResolver); + result = new SwissTimingReplayServiceImpl(domainFactory); servicesForDomainFactories.put(domainFactory, result); } return result; diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceImpl.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceImpl.java index 3d5fe7ff094..b8994dcef23 100644 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceImpl.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceImpl.java @@ -1,9 +1,12 @@ package com.sap.sailing.domain.swisstimingreplayadapter.impl; import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.text.DateFormat; import java.text.MessageFormat; @@ -18,13 +21,15 @@ import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; -import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; -import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.regattalog.RegattaLogStore; import com.sap.sailing.domain.swisstimingadapter.DomainFactory; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayListener; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayRace; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayService; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.TrackerManager; import com.sap.sse.util.ByteArrayOutputStreamWithVisibleBuffer; public class SwissTimingReplayServiceImpl implements SwissTimingReplayService { @@ -35,11 +40,9 @@ public class SwissTimingReplayServiceImpl implements SwissTimingReplayService { private static final TimeZone DEFAULT_TIMEZONE = TimeZone.getTimeZone("GMT"); private final DomainFactory domainFactory; - private final RaceLogResolver raceLogResolver; - public SwissTimingReplayServiceImpl(DomainFactory domainFactory, RaceLogResolver raceLogResolver) { + public SwissTimingReplayServiceImpl(DomainFactory domainFactory) { this.domainFactory = domainFactory; - this.raceLogResolver = raceLogResolver; } @Override @@ -146,10 +149,11 @@ public class SwissTimingReplayServiceImpl implements SwissTimingReplayService { } @Override - public void loadRaceData(String link, Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, - boolean useInternalMarkPassingAlgorithm) { - SwissTimingReplayListener listener = new SwissTimingReplayToDomainAdapter(regatta, domainFactory, - trackedRegattaRegistry, useInternalMarkPassingAlgorithm, raceLogResolver); - loadRaceData(link, listener); + public void loadRaceData(RegattaIdentifier regattaToAddTo, String link, String raceName, String raceID, + String boatClassName, TrackerManager trackerManager, TrackedRegattaRegistry trackedRegattaRegistry, + boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore) + throws MalformedURLException, FileNotFoundException, URISyntaxException, Exception { + trackerManager.addRace(regattaToAddTo, new SwissTimingReplayConnectivityParameters(link, raceName, raceID, + boatClassName, useInternalMarkPassingAlgorithm, domainFactory, this, raceLogStore, regattaLogStore), /* timeout */ 0); } } diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java index da048fee42f..8f11fb7c975 100755 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java @@ -16,6 +16,7 @@ import java.util.TimeZone; import java.util.logging.Logger; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.ControlPoint; import com.sap.sailing.domain.base.Course; @@ -37,14 +38,18 @@ import com.sap.sailing.domain.common.impl.WindImpl; import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; import com.sap.sailing.domain.common.tracking.GPSFixMoving; import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; -import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; -import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.regattalog.RegattaLogStore; import com.sap.sailing.domain.swisstimingadapter.DomainFactory; import com.sap.sailing.domain.swisstimingreplayadapter.CompetitorStatus; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayListener; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayParser; +import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingReplayConnectivityParameters.SwissTimingReplayRaceTracker; +import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.RaceTracker; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.TrackingDataLoader; @@ -77,11 +82,14 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i private final DomainFactory domainFactory; - private final Map racePerRaceID; + /** + * Can be waited on; additions are {@link Object#notifyAll() notified}. + */ + private final Map racePerRaceIdForRaceDefinition; private final Map trackedRacePerRaceID; /** - * The last race ID received from {@link #raceID(String)}. Used as key into {@link #racePerRaceID} and + * The last race ID received from {@link #raceID(String)}. Used as key into {@link #racePerRaceIdForRaceDefinition} and * {@link #trackedRacePerRaceID} for storing data from subsequent messages. */ private String currentRaceID; @@ -140,18 +148,57 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i private final RaceLogResolver raceLogResolver; + private final String raceName; + + private final String raceIdForRaceDefinition; + + /** + * Optional callback to which to announce the arrival of the {@link RaceDefinition} + */ + private final DynamicRaceDefinitionSet dynamicRaceDefinitionSet; + + private final SwissTimingReplayRaceTracker tracker; + + @FunctionalInterface + public static interface TrackerConstructor { + SwissTimingReplayRaceTracker createTracker(SwissTimingReplayToDomainAdapter adapter); + } + /** * @param regatta * the regatta to associate the race(s) received by the listener with, or null to force the * use / creation of a default regatta per race - * @param useInternalMarkPassingAlgorithm use our own instead of the SwissTiming-provided mark rounding / split times + * @param useInternalMarkPassingAlgorithm + * use our own instead of the SwissTiming-provided mark rounding / split times + * @param trackerConstructor + * if not {@code null} this object is used to construct the {@link SwissTimingReplayRaceTracker} that can + * be obtained from {@link #getTracker()}. This tracker's {@link RaceTracker.RaceCreationListener}s will + * be {@link SwissTimingReplayRaceTracker#notifyRaceCreationListeners() notified} when this adapter has + * created the race. */ - public SwissTimingReplayToDomainAdapter(Regatta regatta, DomainFactory domainFactory, - TrackedRegattaRegistry trackedRegattaRegistry, boolean useInternalMarkPassingAlgorithm, RaceLogResolver raceLogResolver) { + public SwissTimingReplayToDomainAdapter(Regatta regatta, String raceName, String raceIdForRaceDefinition, + BoatClass boatClass, DomainFactory domainFactory, + TrackedRegattaRegistry trackedRegattaRegistry, boolean useInternalMarkPassingAlgorithm, RaceLogResolver raceLogResolver, + RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, TrackerConstructor trackerConstructor) { + this.tracker = trackerConstructor == null ? null : trackerConstructor.createTracker(this); this.raceLogResolver = raceLogResolver; - this.regatta = regatta; + // when the race is created, notify the tracker's race creation listeners + this.dynamicRaceDefinitionSet = trackerConstructor == null ? null : (race, trackedRace)->this.tracker.notifyRaceCreationListeners(); + this.raceName = raceName; + this.raceIdForRaceDefinition = raceIdForRaceDefinition; + final Regatta effectiveRegatta; + // Try to find a pre-associated event based on the Race ID + if (regatta == null) { + effectiveRegatta = trackedRegattaRegistry.getRememberedRegattaForRace(raceIdForRaceDefinition); + } else { + effectiveRegatta = regatta; + } + // if regatta is still null, no previous assignment of any of the races in this TracTrac event to a Regatta was + // found; in this case, create a default regatta based on the TracTrac event data + this.regatta = effectiveRegatta == null ? domainFactory.getOrCreateDefaultRegatta(raceLogStore, regattaLogStore, + raceIdForRaceDefinition, boatClass, trackedRegattaRegistry) : effectiveRegatta; this.trackedRegattaRegistry = trackedRegattaRegistry; - racePerRaceID = new HashMap<>(); + racePerRaceIdForRaceDefinition = new HashMap<>(); trackedRacePerRaceID = new HashMap<>(); bestStartTimePerRaceID = new HashMap<>(); raceTimePerRaceID = new HashMap<>(); @@ -165,6 +212,14 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i this.useInternalMarkPassingAlgorithm = useInternalMarkPassingAlgorithm; } + public RaceTracker getTracker() { + return tracker; + } + + public Regatta getRegatta() { + return regatta; + } + public Iterable getTrackedRaces() { return trackedRacePerRaceID.values(); } @@ -273,7 +328,7 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i if (id2 != null && !id2.trim().isEmpty()) { markNames.add(id2.trim()); } - final ControlPoint controlPoint = domainFactory.getOrCreateControlPoint(markNames); + final ControlPoint controlPoint = domainFactory.getOrCreateControlPoint(markNames, getMarkType(markType)); if (index == 0) { currentCourseDefinition = new ArrayList<>(); } @@ -290,9 +345,32 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i } } + /** + * Produces a domain {@link com.sap.sailing.domain.common.MarkType} from a SwissTiming replay {@link MarkType mark type}. + */ + private com.sap.sailing.domain.common.MarkType getMarkType(MarkType markType) { + final com.sap.sailing.domain.common.MarkType result; + if (markType == null) { + result = null; + } else { + if (markType.isBoat()) { + if (markType.isStart()) { + result = com.sap.sailing.domain.common.MarkType.STARTBOAT; + } else if (markType.isFinish()) { + result = com.sap.sailing.domain.common.MarkType.FINISHBOAT; + } else { + result = com.sap.sailing.domain.common.MarkType.BUOY; + } + } else { + result = com.sap.sailing.domain.common.MarkType.BUOY; + } + } + return result; + } + @Override public void trackersCount(short trackersCount) { - RaceDefinition race = racePerRaceID.get(currentRaceID); + RaceDefinition race = racePerRaceIdForRaceDefinition.get(raceIdForRaceDefinition); if (race == null) { createRace(); } else { @@ -311,13 +389,13 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i } private void createRace() { - final Regatta myRegatta = regatta != null ? regatta : domainFactory.getOrCreateDefaultRegatta( - EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, - currentRaceID, domainFactory.getRaceTypeFromRaceID(currentRaceID).getBoatClass(), trackedRegattaRegistry); - RaceDefinition race = domainFactory.createRaceDefinition(myRegatta, - currentRaceID, competitorsPerRaceID.get(currentRaceID), currentCourseDefinition); - racePerRaceID.put(currentRaceID, race); - DynamicTrackedRace trackedRace = trackedRegattaRegistry.getOrCreateTrackedRegatta(myRegatta). + RaceDefinition race = domainFactory.createRaceDefinition(regatta, + currentRaceID, competitorsPerRaceID.get(currentRaceID), currentCourseDefinition, raceName, raceIdForRaceDefinition); + synchronized (racePerRaceIdForRaceDefinition) { + racePerRaceIdForRaceDefinition.put(raceIdForRaceDefinition, race); + racePerRaceIdForRaceDefinition.notifyAll(); + } + DynamicTrackedRace trackedRace = getTrackedRegatta(). createTrackedRace(race, Collections. emptyList(), EmptyWindStore.INSTANCE, TrackedRace.DEFAULT_LIVE_DELAY_IN_MILLISECONDS, WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, @@ -329,6 +407,46 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i trackedRace.setStartTimeReceived(bestStartTimeKnownSoFar); } trackedRacePerRaceID.put(currentRaceID, trackedRace); + if (dynamicRaceDefinitionSet != null) { + dynamicRaceDefinitionSet.addRaceDefinition(race, trackedRace); + } + } + + public DynamicTrackedRegatta getTrackedRegatta() { + return trackedRegattaRegistry.getOrCreateTrackedRegatta(regatta); + } + + /** + * Waits synchronously until a race with ID {@code raceID} was added to {@link #racePerRaceIdForRaceDefinition}. + */ + public RaceDefinition getRaceDefinition(String raceID) throws InterruptedException { + return getRaceDefinition(raceID, 0); + } + + /** + * Waits synchronously until a race with ID {@code raceID} was added to {@link #racePerRaceIdForRaceDefinition} + * + * @param timeoutInMillis + * if 0, waits until a race for {@code raceID} was added to {@link #racePerRaceIdForRaceDefinition}; otherwise the call + * returns after so many milliseconds at the latest, even if no race with that ID was added, in which + * case the method returns {@code null} + */ + public RaceDefinition getRaceDefinition(String raceIdForRaceDefinition, long timeoutInMillis) throws InterruptedException { + final RaceDefinition result; + long targetTime = 0; + if (timeoutInMillis != 0) { + targetTime = System.currentTimeMillis() + timeoutInMillis; + } + synchronized (racePerRaceIdForRaceDefinition) { + while (!racePerRaceIdForRaceDefinition.containsKey(raceIdForRaceDefinition)) { + racePerRaceIdForRaceDefinition.wait(timeoutInMillis == 0 ? 0 : Math.max(1, targetTime-System.currentTimeMillis())); + if (timeoutInMillis != 0 && System.currentTimeMillis() > targetTime) { + break; // if a real timeout has been specified, don't wait again + } + } + result = racePerRaceIdForRaceDefinition.get(raceIdForRaceDefinition); + } + return result; } @Override diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/LeaderboardDTODiffingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/LeaderboardDTODiffingTest.java index 080fde075c1..8113344b2a3 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/LeaderboardDTODiffingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/LeaderboardDTODiffingTest.java @@ -23,6 +23,7 @@ import java.util.Set; import org.junit.Before; import org.junit.Test; +import com.sap.sailing.domain.common.BoatClassMasterdata; import com.sap.sailing.domain.common.dto.BoatClassDTO; import com.sap.sailing.domain.common.dto.BoatDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; @@ -35,7 +36,6 @@ import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; import com.sap.sailing.domain.common.dto.LegEntryDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.domain.common.impl.MeterDistance; import com.sap.sailing.domain.test.StoredTrackBasedTest; import com.sap.sse.common.Cloner; import com.sap.sse.common.Color; @@ -235,7 +235,8 @@ public class LeaderboardDTODiffingTest { public void testCompetitorListChange() { newVersion.competitors = new ArrayList(newVersion.competitors); // clone competitor list so it's not identical to that of previous version CompetitorDTO somebodyNew = new CompetitorDTOImpl("Someone New", Color.RED, "someone@nobody.de", "DE", "GER", "Germany", "912p09871203987", - /* imageURL */ null, /* flagImageURL */ null, new BoatDTO("LSC", "GER 1234"), new BoatClassDTO("505", new MeterDistance(5.05)), /* timeOnTimeFactor */ null, + /* imageURL */ null, /* flagImageURL */ null, new BoatDTO("LSC", "GER 1234"), new BoatClassDTO("505", + BoatClassMasterdata._5O5.getHullLength(), BoatClassMasterdata._5O5.getHullBeam()), /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null); newVersion.competitors.add(13, somebodyNew); // insert a competitor; this should mess up all others' indexes; check if this works CompetitorDTO wolfgang = getPreviousCompetitorByName("HUNGER +JESS"); @@ -256,7 +257,8 @@ public class LeaderboardDTODiffingTest { newVersion.setSuppressedCompetitors(newSuppressedCompetitors); newVersion.competitors = new ArrayList(newVersion.competitors); // clone competitor list so it's not identical to that of previous version CompetitorDTO somebodyNew = new CompetitorDTOImpl("Someone New", Color.RED, "someone@nobody.de", "DE", "GER", "Germany", "912p09871203987", - /* imageURL */ null, /* flagImageURL */ null, new BoatDTO("LSC", "GER 1234"), new BoatClassDTO("505", new MeterDistance(5.05)), /* timeOnTimeFactor */ null, + /* imageURL */ null, /* flagImageURL */ null, new BoatDTO("LSC", "GER 1234"), new BoatClassDTO("505", + BoatClassMasterdata._5O5.getHullLength(), BoatClassMasterdata._5O5.getHullBeam()), /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null); newVersion.setSuppressed(newVersion.competitors.get(13), true); // suppress an existing competitor; compaction should reduce this to a single number only newVersion.setSuppressed(somebodyNew, true); // check that mixed mode with existing and new competitors works as well @@ -279,7 +281,8 @@ public class LeaderboardDTODiffingTest { public void testDisplayNameChange() { newVersion.competitors = new ArrayList(newVersion.competitors); // clone competitor list so it's not identical to that of previous version CompetitorDTO somebodyNew = new CompetitorDTOImpl("Someone New", Color.RED, "someone@nobody.de", "DE", "GER", "Germany", "912p09871203987", - /* imageURL */ null, /* flagImageURL */ null, new BoatDTO("LSC", "GER 1234"), new BoatClassDTO("505", new MeterDistance(5.05)), /* timeOnTimeFactor */ null, + /* imageURL */ null, /* flagImageURL */ null, new BoatDTO("LSC", "GER 1234"), new BoatClassDTO("505", + BoatClassMasterdata._5O5.getHullLength(), BoatClassMasterdata._5O5.getHullBeam()), /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null); newVersion.competitors.add(somebodyNew); newVersion.competitorDisplayNames = new HashMap(newVersion.competitorDisplayNames); @@ -309,7 +312,8 @@ public class LeaderboardDTODiffingTest { newVersion.setCompetitorsFromBestToWorst(r9, newOrdering); newVersion.competitors = new ArrayList(newVersion.competitors); // clone competitor list so it's not identical to that of previous version CompetitorDTO somebodyNew = new CompetitorDTOImpl("Someone New", Color.RED, "someone@nobody.de", "DE", "GER", "Germany", "912p09871203987", - /* imageURL */ null, /* flagImageURL */ null, new BoatDTO("LSC", "GER 1234"), new BoatClassDTO("505", new MeterDistance(5.05)), /* timeOnTimeFactor */ null, + /* imageURL */ null, /* flagImageURL */ null, new BoatDTO("LSC", "GER 1234"), new BoatClassDTO("505", + BoatClassMasterdata._5O5.getHullLength(), BoatClassMasterdata._5O5.getHullBeam()), /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null); newVersion.competitors.add(somebodyNew); newOrdering.add(somebodyNew); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimeRangeTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimeRangeTest.java index 72cb878624b..31e3f62c538 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimeRangeTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimeRangeTest.java @@ -135,4 +135,47 @@ public class TimeRangeTest { assertTrue(TimePoint.EndOfTime.after(TimePoint.BeginningOfTime)); assertTrue(TimePoint.EndOfTime.after(MillisecondsTimePoint.now())); } + + @Test + public void testSubtractionWithMinuendCompletelyWithin() { + testSubtraction(5, 10, 7, 8, /* expected */ 5, 6, 9, 10); + } + + @Test + public void testSubtractionWithMinuendStartingAtFrom() { + testSubtraction(5, 10, 5, 8, /* expected */ 9, 10); + } + + @Test + public void testSubtractionWithMinuendEndingAtTo() { + testSubtraction(5, 10, 7, 10, /* expected */ 5, 6); + } + + @Test + public void testSubtractionWithMinuendContainingAll() { + testSubtraction(5, 10, 4, 11 /* expected empty */); + } + + @Test + public void testSubtractionWithMinuendOverlappingAtFrom() { + testSubtraction(5, 10, 4, 7, /* expected */ 8, 10); + } + + @Test + public void testSubtractionWithMinuendOverlappingAtto() { + testSubtraction(5, 10, 7, 12, /* expected */ 5, 6); + } + + private void testSubtraction(long allFrom, long allTo, long minuendFrom, long minuendTo, long... expected) { + TimeRange all = create(allFrom, allTo); + TimeRange minuend = create(minuendFrom, minuendTo); + TimeRange[] diff = all.subtract(minuend); + assertEquals("Expected to obtain "+expected.length/2+" time ranges but got "+diff.length, expected.length/2, diff.length); + for (int i=0; i emptyCompetitorList = Collections.emptyList(); + competitor = createCompetitor("Test"); + final Iterable competitorListWithOneCompetitor = Collections.singletonList(competitor); now = MillisecondsTimePoint.now(); - trackedRace = createTestTrackedRace("Test Regatta", "Test Race", "505", emptyCompetitorList, now, /* useMarkPassingCalculator */ false); + trackedRace = createTestTrackedRace("Test Regatta", "Test Race", "505", competitorListWithOneCompetitor, now, /* useMarkPassingCalculator */ false); } @Test @@ -56,6 +61,37 @@ public class LineAnalysisTest extends TrackBasedTest { assertEquals(advantage.getMeters(), finishLine.getAdvantage().getMeters(), 0.1); assertEquals(NauticalSide.PORT, finishLine.getAdvantageousSideWhileApproachingLine()); } + + @Test + public void testDistanceToLineWithOrthogonalProjectionOnLine() { + Mark left = getMark("Left lee gate buoy"); + Mark right = getMark("Right lee gate buoy"); + final Position leftPos = trackedRace.getOrCreateTrack(left).getEstimatedPosition(now, /* extrapolate */ false); + final Position rightPos = trackedRace.getOrCreateTrack(right).getEstimatedPosition(now, /* extrapolate */ false); + final Bearing lineBearingFromLeftToRight = leftPos.getBearingGreatCircle(rightPos); + final Bearing awayFromCourseSideOrthogonalToLine = lineBearingFromLeftToRight.add(new DegreeBearingImpl(90)); + final Position lineMiddle = leftPos.translateGreatCircle(lineBearingFromLeftToRight, leftPos.getDistance(rightPos).scale(0.5)); + final Distance distanceBehindLine = new MeterDistance(10); + final Position behindMiddleOfLine = lineMiddle.translateGreatCircle(awayFromCourseSideOrthogonalToLine, distanceBehindLine); + trackedRace.getTrack(competitor).add(new GPSFixMovingImpl(behindMiddleOfLine, now, new KnotSpeedWithBearingImpl(5, new DegreeBearingImpl(45)))); + assertEquals(distanceBehindLine.getMeters(), trackedRace.getDistanceToStartLine(competitor, now).getMeters(), 0.000001); + } + + @Test + public void testDistanceToLineWithOrthogonalProjectionOutsideLine() { + Mark left = getMark("Left lee gate buoy"); + Mark right = getMark("Right lee gate buoy"); + final Position leftPos = trackedRace.getOrCreateTrack(left).getEstimatedPosition(now, /* extrapolate */ false); + final Position rightPos = trackedRace.getOrCreateTrack(right).getEstimatedPosition(now, /* extrapolate */ false); + final Bearing lineBearingFromLeftToRight = leftPos.getBearingGreatCircle(rightPos); + final Bearing awayFromCourseSideOrthogonalToLine = lineBearingFromLeftToRight.add(new DegreeBearingImpl(90)); + final Position outsideOfLine = rightPos.translateGreatCircle(lineBearingFromLeftToRight, new MeterDistance(30)); // 30m right of start boat + final Distance distanceBehindLine = new MeterDistance(10); + final Position behindOutsideOfLine = outsideOfLine.translateGreatCircle(awayFromCourseSideOrthogonalToLine, distanceBehindLine); + trackedRace.getTrack(competitor).add(new GPSFixMovingImpl(behindOutsideOfLine, now, new KnotSpeedWithBearingImpl(5, new DegreeBearingImpl(45)))); + // the distance is that of the hypotenuse formed by the two cathesuses of 10m and 30m length, respectively: + assertEquals(Math.sqrt(10.*10.+30.*30.), trackedRace.getDistanceToStartLine(competitor, now).getMeters(), 0.001); + } private Mark getMark(String name) { for (Mark mark : trackedRace.getMarks()) { diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java index 02f80b054c4..c634c147beb 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java @@ -62,7 +62,7 @@ public class MultipleClassesInRegattaTest { false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, - new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class)); + new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class), /* connectivityParams */ null); kiwotest2 = domainFactory .createRaceTracker( new URL( @@ -73,7 +73,7 @@ public class MultipleClassesInRegattaTest { false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, - new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class)); + new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class), /* connectivityParams */ null); kiwotest3 = domainFactory .createRaceTracker( new URL( @@ -84,7 +84,7 @@ public class MultipleClassesInRegattaTest { false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, - new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class)); + new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class), /* connectivityParams */ null); weym470may112014_2 = domainFactory .createRaceTracker( new URL( @@ -95,7 +95,7 @@ public class MultipleClassesInRegattaTest { false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, - new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class)); + new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class), /* connectivityParams */ null); assertEquals("STG", kiwotest1.getRegatta().getBoatClass().getName()); assertEquals("5O5", kiwotest2.getRegatta().getBoatClass().getName()); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TargetTimeEstimationTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TargetTimeEstimationTest.java index d1b4b9ca77f..64df6e5594d 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TargetTimeEstimationTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TargetTimeEstimationTest.java @@ -48,6 +48,7 @@ import com.sap.sse.common.impl.MillisecondsTimePoint; public class TargetTimeEstimationTest { + @SuppressWarnings("unchecked") // the problem here is any(Set.class) which cannot infer the type arguments @Test public void simpleReachTargetTimeEstimation() throws NotEnoughDataHasBeenAddedException, NoWindException { // Setup mock objects @@ -63,11 +64,11 @@ public class TargetTimeEstimationTest { Bearing legBearing = startOfLeg.getBearingGreatCircle(endOfLeg); SpeedWithConfidence boatSpeedWithConfidence = new SpeedWithConfidenceImpl(new KnotSpeedImpl(12), 1, null); DynamicTrackedRaceImpl trackedRace = mock(DynamicTrackedRaceImpl.class); - when(trackedRace.getCenterOfCourse(timepoint)).thenReturn(centerOfCourse); WindSource source = mock(WindSource.class); Set sources = Collections.singleton(source); when(trackedRace.getWindSources(WindSourceType.TRACK_BASED_ESTIMATION)).thenReturn(sources); - when(trackedRace.getWind(any(Position.class), eq(timepoint), eq(sources))).thenReturn(wind); + when(trackedRace.getWind(any(Position.class), eq(timepoint))).thenReturn(wind); + when(trackedRace.getWind(any(Position.class), eq(timepoint), any(Set.class))).thenReturn(wind); RaceDefinition race = mock(RaceDefinition.class); when(race.getBoatClass()).thenReturn(mockedBoatClass); @@ -79,20 +80,24 @@ public class TargetTimeEstimationTest { Waypoint to = mock(Waypoint.class); when(leg.getFrom()).thenReturn(from); when(leg.getTo()).thenReturn(to); + when(trackedRace.getApproximatePosition(from, timepoint)).thenReturn(startOfLeg); + when(trackedRace.getApproximatePosition(to, timepoint)).thenReturn(endOfLeg); when(trackedRace.getApproximatePosition(from, timepoint)).thenReturn(startOfLeg); when(trackedRace.getApproximatePosition(to, timepoint)).thenReturn(endOfLeg); - when(mockedPolars.getSpeed(mockedBoatClass, wind, windBearing.getDifferenceTo(legBearing))).thenReturn(boatSpeedWithConfidence); + when(mockedPolars.getSpeed(mockedBoatClass, wind, legBearing.getDifferenceTo(windBearing.reverse()))).thenReturn(boatSpeedWithConfidence); HashSet competitors = new HashSet(); TrackedLeg trackedLeg = new TrackedLegImpl(trackedRace, leg, competitors); //Actual test of functionality - Duration duration = trackedLeg.getEstimatedTimeToComplete(mockedPolars, timepoint, new MarkPositionAtTimePointCacheImpl(trackedRace, timepoint)); + Duration duration = trackedLeg.getEstimatedTimeToComplete(mockedPolars, timepoint, + new MarkPositionAtTimePointCacheImpl(trackedRace, timepoint)).getExpectedDuration(); assertEquals(75494, duration.asMillis(), 100); } + @SuppressWarnings("unchecked") // the problem here is any(Set.class) which cannot infer the type arguments @Test public void simpleUpwindTargetTimeEstimation() throws NotEnoughDataHasBeenAddedException, NoWindException { // Setup mock objects @@ -108,10 +113,8 @@ public class TargetTimeEstimationTest { DynamicTrackedRaceImpl trackedRace = mock(DynamicTrackedRaceImpl.class); when(trackedRace.getCenterOfCourse(timepoint)).thenReturn(centerOfCourse); - WindSource source = mock(WindSource.class); - Set sources = Collections.singleton(source); - when(trackedRace.getWindSources(WindSourceType.TRACK_BASED_ESTIMATION)).thenReturn(sources); - when(trackedRace.getWind(any(Position.class), eq(timepoint), eq(sources))).thenReturn(wind); + when(trackedRace.getWind(any(Position.class), eq(timepoint))).thenReturn(wind); + when(trackedRace.getWind(any(Position.class), eq(timepoint), any(Set.class))).thenReturn(wind); RaceDefinition race = mock(RaceDefinition.class); when(race.getBoatClass()).thenReturn(mockedBoatClass); @@ -141,10 +144,12 @@ public class TargetTimeEstimationTest { TrackedLeg trackedLeg = new TrackedLegImpl(trackedRace, leg, competitors); //Actual test of functionality - Duration duration = trackedLeg.getEstimatedTimeToComplete(mockedPolars, timepoint, new MarkPositionAtTimePointCacheImpl(trackedRace, timepoint)); + Duration duration = trackedLeg.getEstimatedTimeToComplete(mockedPolars, timepoint, + new MarkPositionAtTimePointCacheImpl(trackedRace, timepoint)).getExpectedDuration(); assertEquals(213513, duration.asMillis(), 100); } + @SuppressWarnings("unchecked") // the problem here is any(Set.class) which cannot infer the type arguments @Test public void simpleDownwindTargetTimeEstimation() throws NotEnoughDataHasBeenAddedException, NoWindException { // Setup mock objects @@ -160,10 +165,8 @@ public class TargetTimeEstimationTest { DynamicTrackedRaceImpl trackedRace = mock(DynamicTrackedRaceImpl.class); when(trackedRace.getCenterOfCourse(timepoint)).thenReturn(centerOfCourse); - WindSource source = mock(WindSource.class); - Set sources = Collections.singleton(source); - when(trackedRace.getWindSources(WindSourceType.TRACK_BASED_ESTIMATION)).thenReturn(sources); - when(trackedRace.getWind(any(Position.class), eq(timepoint), eq(sources))).thenReturn(wind); + when(trackedRace.getWind(any(Position.class), eq(timepoint))).thenReturn(wind); + when(trackedRace.getWind(any(Position.class), eq(timepoint), any(Set.class))).thenReturn(wind); RaceDefinition race = mock(RaceDefinition.class); when(race.getBoatClass()).thenReturn(mockedBoatClass); @@ -193,7 +196,8 @@ public class TargetTimeEstimationTest { TrackedLeg trackedLeg = new TrackedLegImpl(trackedRace, leg, competitors); //Actual test of functionality - Duration duration = trackedLeg.getEstimatedTimeToComplete(mockedPolars, timepoint, new MarkPositionAtTimePointCacheImpl(trackedRace, timepoint)); + Duration duration = trackedLeg.getEstimatedTimeToComplete(mockedPolars, timepoint, + new MarkPositionAtTimePointCacheImpl(trackedRace, timepoint)).getExpectedDuration(); assertEquals(95090, duration.asMillis(), 100); } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java index e1803615e5e..9ec655d660f 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java @@ -70,9 +70,9 @@ public class UnicodeCharactersInCompetitorNamesTest { /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, "tracTest", "tracTest", "", "", - new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class)); + new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class), /* connectivityParams */ null); - Iterable competitors = fourtyninerYellow_2.getRacesHandle().getRace().getCompetitors(); + Iterable competitors = fourtyninerYellow_2.getRaceHandle().getRace().getCompetitors(); for (Competitor competitor : competitors) { System.out.println(competitor.getName()); } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java index 23b7ebb5e21..bb4298e6ecd 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java @@ -36,6 +36,7 @@ import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; @@ -1035,7 +1036,7 @@ public class MockedTrackedRace implements DynamicTrackedRace { } @Override - public Duration getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, + public TargetTimeInfo getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException { return null; } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRaceWithStartTimeAndRanks.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRaceWithStartTimeAndRanks.java index 9f596ddc595..ecb3dab6078 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRaceWithStartTimeAndRanks.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRaceWithStartTimeAndRanks.java @@ -27,6 +27,7 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; @@ -59,7 +60,6 @@ import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.WindWithConfidence; import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl; -import com.sap.sse.common.Duration; import com.sap.sse.common.IsManagedByCache; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -644,7 +644,7 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace { } @Override - public Duration getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, + public TargetTimeInfo getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException { return null; } diff --git a/java/com.sap.sailing.domain.tractracadapter.persistence/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.tractracadapter.persistence/META-INF/MANIFEST.MF index 682a42bbc63..0f63097dbe2 100644 --- a/java/com.sap.sailing.domain.tractracadapter.persistence/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.tractracadapter.persistence/META-INF/MANIFEST.MF @@ -11,7 +11,11 @@ Require-Bundle: org.mongodb.mongo-java-driver;bundle-version="2.13.0", com.sap.sailing.domain.tractracadapter, com.sap.sailing.domain, com.sap.sailing.domain.common, - com.sap.sse.common -Export-Package: com.sap.sailing.domain.tractracadapter.persistence -Import-Package: org.osgi.framework + com.sap.sse.common, + com.sap.sailing.domain.persistence, + com.sap.sse +Export-Package: com.sap.sailing.domain.tractracadapter.persistence, + com.sap.sailing.domain.tractracadapter.persistence.impl;x-friends:="com.sap.sailing.mongodb.test" +Import-Package: org.osgi.framework, + org.osgi.util.tracker;version="1.5.1" Bundle-Activator: com.sap.sailing.domain.tractracadapter.persistence.impl.Activator diff --git a/java/com.sap.sailing.domain.tractracadapter.persistence/src/com/sap/sailing/domain/tractracadapter/persistence/impl/Activator.java b/java/com.sap.sailing.domain.tractracadapter.persistence/src/com/sap/sailing/domain/tractracadapter/persistence/impl/Activator.java index 46c4c82cd96..32dc23b47bd 100755 --- a/java/com.sap.sailing.domain.tractracadapter.persistence/src/com/sap/sailing/domain/tractracadapter/persistence/impl/Activator.java +++ b/java/com.sap.sailing.domain.tractracadapter.persistence/src/com/sap/sailing/domain/tractracadapter/persistence/impl/Activator.java @@ -1,16 +1,61 @@ package com.sap.sailing.domain.tractracadapter.persistence.impl; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.logging.Level; +import java.util.logging.Logger; + import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.util.tracker.ServiceTracker; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; +import com.sap.sailing.domain.persistence.MongoRegattaLogStoreFactory; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParametersHandler; +import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory; +import com.sap.sailing.domain.tractracadapter.impl.RaceTrackingConnectivityParametersImpl; +import com.sap.sse.common.TypeBasedServiceFinder; import com.sap.sse.mongodb.MongoDBService; +import com.sap.sse.util.ServiceTrackerFactory; public class Activator implements BundleActivator { + private static final Logger logger = Logger.getLogger(Activator.class.getName()); + + /** + * Requires a base {@link DomainFactory} as well as a {@link RaceLogStore} and a {@link RegattaLogStore}. In order + * to instantiate those two stores, the method requires a {@link MongoObjectFactory} and a {@link DomainObjectFactory} + * which are waited for as services from the OSGi registry. This happens in a separate thread that the {@link #start(BundleContext)} + * method launches. + */ @Override public void start(BundleContext context) throws Exception { for (CollectionNames name : CollectionNames.values()) { MongoDBService.INSTANCE.registerExclusively(CollectionNames.class, name.name()); } + new Thread(() -> { + final ServiceTracker mongoObjectFactoryServiceTracker = ServiceTrackerFactory.createAndOpen(context, MongoObjectFactory.class); + final ServiceTracker domainObjectFactoryServiceTracker = ServiceTrackerFactory.createAndOpen(context, DomainObjectFactory.class); + try { + final MongoObjectFactory mongoObjectFactory = mongoObjectFactoryServiceTracker.waitForService(0); + final DomainObjectFactory domainObjectFactory = domainObjectFactoryServiceTracker.waitForService(0); + final Dictionary properties = new Hashtable(); + final com.sap.sailing.domain.tractracadapter.DomainFactory domainFactory = TracTracAdapterFactory.INSTANCE.getOrCreateTracTracAdapter( + domainObjectFactory.getBaseDomainFactory()).getTracTracDomainFactory(); + final TracTracConnectivityParamsHandler paramsHandler = new TracTracConnectivityParamsHandler( + MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory), + MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore(mongoObjectFactory, domainObjectFactory), + domainFactory); + properties.put(TypeBasedServiceFinder.TYPE, RaceTrackingConnectivityParametersImpl.TYPE); + context.registerService(RaceTrackingConnectivityParametersHandler.class, paramsHandler, properties); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception trying to register TracTrac RaceTrackingConnectivityParametersHandler implementation", e); + } + }, getClass().getName() + " registering connectivity handler").start(); } @Override diff --git a/java/com.sap.sailing.domain.tractracadapter.persistence/src/com/sap/sailing/domain/tractracadapter/persistence/impl/TracTracConnectivityParamsHandler.java b/java/com.sap.sailing.domain.tractracadapter.persistence/src/com/sap/sailing/domain/tractracadapter/persistence/impl/TracTracConnectivityParamsHandler.java new file mode 100644 index 00000000000..a1183d78cae --- /dev/null +++ b/java/com.sap.sailing.domain.tractracadapter.persistence/src/com/sap/sailing/domain/tractracadapter/persistence/impl/TracTracConnectivityParamsHandler.java @@ -0,0 +1,105 @@ +package com.sap.sailing.domain.tractracadapter.persistence.impl; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.impl.AbstractRaceTrackingConnectivityParametersHandler; +import com.sap.sailing.domain.tractracadapter.DomainFactory; +import com.sap.sailing.domain.tractracadapter.impl.RaceTrackingConnectivityParametersImpl; +import com.sap.sse.common.TypeBasedServiceFinder; +import com.sap.sse.common.impl.MillisecondsDurationImpl; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +/** + * Handles mapping TracTrac connectivity parameters from and to a map with {@link String} keys. The + * "param URL" is considered the {@link #getKey(RaceTrackingConnectivityParameters) key} for these objects.

    + * + * Lives in the same package as {@link RaceTrackingConnectivityParameters} for package-private access to + * its members. + * + * @author Axel Uhl (d043530) + * + */ +public class TracTracConnectivityParamsHandler extends AbstractRaceTrackingConnectivityParametersHandler { + private static final String USE_INTERNAL_MARK_PASSING_ALGORITHM = "useInternalMarkPassingAlgorithm"; + private static final String TRAC_TRAC_USERNAME = "tracTracUsername"; + private static final String TRAC_TRAC_PASSWORD = "tracTracPassword"; + private static final String STORED_URI = "storedURI"; + private static final String START_OF_TRACKING_MILLIS = "startOfTrackingMillis"; + private static final String RACE_VISIBILITY = "raceVisibility"; + private static final String RACE_STATUS = "raceStatus"; + private static final String PARAM_URL = "paramURL"; + private static final String OFFSET_TO_START_TIME_OF_SIMULATED_RACE_MILLIS = "offsetToStartTimeOfSimulatedRaceMillis"; + private static final String LIVE_URI = "liveURI"; + private static final String END_OF_TRACKING_MILLIS = "endOfTrackingMillis"; + private static final String DELAY_TO_LIVE_IN_MILLIS = "delayToLiveInMillis"; + private static final String COURSE_DESIGN_UPDATE_URI = "courseDesignUpdateURI"; + private final RaceLogStore raceLogStore; + private final RegattaLogStore regattaLogStore; + private final DomainFactory domainFactory; + + public TracTracConnectivityParamsHandler(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, DomainFactory domainFactory) { + super(); + this.raceLogStore = raceLogStore; + this.regattaLogStore = regattaLogStore; + this.domainFactory = domainFactory; + } + + @Override + public Map mapFrom(RaceTrackingConnectivityParameters params) { + assert params instanceof RaceTrackingConnectivityParametersImpl; + final RaceTrackingConnectivityParametersImpl ttParams = (RaceTrackingConnectivityParametersImpl) params; + final Map result = getKey(params); + result.put(COURSE_DESIGN_UPDATE_URI, ttParams.getCourseDesignUpdateURI()==null?null:ttParams.getCourseDesignUpdateURI().toString()); + result.put(DELAY_TO_LIVE_IN_MILLIS, ttParams.getDelayToLiveInMillis()); + result.put(END_OF_TRACKING_MILLIS, ttParams.getEndOfTracking()==null?null:ttParams.getEndOfTracking().asMillis()); + result.put(LIVE_URI, ttParams.getLiveURI()==null?null:ttParams.getLiveURI().toString()); + result.put(OFFSET_TO_START_TIME_OF_SIMULATED_RACE_MILLIS, ttParams.getOffsetToStartTimeOfSimulatedRace()==null?null:ttParams.getOffsetToStartTimeOfSimulatedRace().asMillis()); + result.put(RACE_STATUS, ttParams.getRaceStatus()); + result.put(RACE_VISIBILITY, ttParams.getRaceVisibility()); + result.put(START_OF_TRACKING_MILLIS, ttParams.getStartOfTracking()==null?null:ttParams.getStartOfTracking().asMillis()); + result.put(STORED_URI, ttParams.getStoredURI()==null?null:ttParams.getStoredURI().toString()); + result.put(TRAC_TRAC_PASSWORD, ttParams.getTracTracPassword()); + result.put(TRAC_TRAC_USERNAME, ttParams.getTracTracUsername().toString()); + result.put(USE_INTERNAL_MARK_PASSING_ALGORITHM, ttParams.isUseInternalMarkPassingAlgorithm()); + addWindTrackingParameters(ttParams, result); + return result; + } + + @Override + public RaceTrackingConnectivityParameters mapTo(Map map) throws MalformedURLException, URISyntaxException { + return new RaceTrackingConnectivityParametersImpl( + new URL(map.get(PARAM_URL).toString()), + map.get(LIVE_URI) == null ? null : new URI(map.get(LIVE_URI).toString()), + map.get(STORED_URI) == null ? null : new URI(map.get(STORED_URI).toString()), + map.get(COURSE_DESIGN_UPDATE_URI) == null ? null : new URI(map.get(COURSE_DESIGN_UPDATE_URI).toString()), + map.get(START_OF_TRACKING_MILLIS) == null ? null : new MillisecondsTimePoint(((Number) map.get(START_OF_TRACKING_MILLIS)).longValue()), + map.get(END_OF_TRACKING_MILLIS) == null ? null : new MillisecondsTimePoint(((Number) map.get(END_OF_TRACKING_MILLIS)).longValue()), + ((Number) map.get(DELAY_TO_LIVE_IN_MILLIS)).longValue(), + map.get(OFFSET_TO_START_TIME_OF_SIMULATED_RACE_MILLIS) == null ? null : new MillisecondsDurationImpl(((Number) map.get(OFFSET_TO_START_TIME_OF_SIMULATED_RACE_MILLIS)).longValue()), + (Boolean) map.get(USE_INTERNAL_MARK_PASSING_ALGORITHM), + raceLogStore, regattaLogStore, domainFactory, + map.get(TRAC_TRAC_USERNAME)==null?null:map.get(TRAC_TRAC_USERNAME).toString(), + map.get(TRAC_TRAC_PASSWORD)==null?null:map.get(TRAC_TRAC_PASSWORD).toString(), + map.get(RACE_STATUS)==null?null:map.get(RACE_STATUS).toString(), + map.get(RACE_VISIBILITY)==null?null:map.get(RACE_VISIBILITY).toString(), isTrackWind(map), + isCorrectWindDirectionByMagneticDeclination(map)); + } + + @Override + public Map getKey(RaceTrackingConnectivityParameters params) { + assert params instanceof RaceTrackingConnectivityParametersImpl; + final RaceTrackingConnectivityParametersImpl ttParams = (RaceTrackingConnectivityParametersImpl) params; + final Map result = new HashMap<>(); + result.put(TypeBasedServiceFinder.TYPE, params.getTypeIdentifier()); + result.put(PARAM_URL, ttParams.getParamURL().toString()); + return result; + } +} diff --git a/java/com.sap.sailing.domain.tractracadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.tractracadapter/META-INF/MANIFEST.MF index bb6a0a88bc7..74dac9cbc66 100644 --- a/java/com.sap.sailing.domain.tractracadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.tractracadapter/META-INF/MANIFEST.MF @@ -19,7 +19,8 @@ Export-Package: com.sap.sailing.domain.tractracadapter, x-friends:="com.sap.sailing.domain.test, com.sap.sailing.domain.swisstimingadapter.test, com.sap.sailing.server.test, - com.sap.sailing.mongodb.test" + com.sap.sailing.mongodb.test, + com.sap.sailing.domain.tractracadapter.persistence" Bundle-ActivationPolicy: lazy Import-Package: org.apache.http, org.apache.http.client, diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java index 863458c7fc9..d3449b01978 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java @@ -42,6 +42,7 @@ import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sailing.domain.tractracadapter.impl.DomainFactoryImpl; import com.sap.sailing.domain.tractracadapter.impl.RaceCourseReceiver; +import com.sap.sailing.domain.tractracadapter.impl.RaceTrackingConnectivityParametersImpl; import com.sap.sailing.domain.tractracadapter.impl.Simulator; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; @@ -137,26 +138,27 @@ public interface DomainFactory { * Provides the capability to obtain the {@link WindTrack}s for the different wind sources. A trivial * implementation is {@link EmptyWindStore} which simply provides new, empty tracks. This is always * available but loses track of the wind, e.g., during server restarts. + * @param connectivityParams TODO */ TracTracRaceTracker createRaceTracker(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, - String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) + String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceTrackingConnectivityParametersImpl connectivityParams) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException; /** * Same as {@link #createRaceTracker(URL, URI, URI, URI, TimePoint, TimePoint, WindStore, TrackedRegattaRegistry)}, * only that a predefined {@link Regatta} is used to hold the resulting races. - * * @param offsetToStartTimeOfSimulatedRace * if non-null, the {@link Simulator} will be used with this duration as start offset + * @param connectivityParams TODO */ RaceTracker createRaceTracker(Regatta regatta, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, - String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) + String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceTrackingConnectivityParametersImpl connectivityParams) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException; @@ -264,11 +266,13 @@ public interface DomainFactory { /** * @param offsetToStartTimeOfSimulatedRace * if non-null, the {@link Simulator} will be used with this duration as start offset + * @param trackWind TODO + * @param correctWindDirectionByMagneticDeclination TODO */ RaceTrackingConnectivityParameters createTrackingConnectivityParameters(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, - String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility); + String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, boolean trackWind, boolean correctWindDirectionByMagneticDeclination); /** * Removes all knowledge about tractracRace which includes removing it from the race cache, from the * {@link com.sap.sailing.domain.base.Regatta} and, if a {@link TrackedRace} for the corresponding diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java index ca159e2ca8e..73c4570a572 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java @@ -48,11 +48,13 @@ public interface TracTracAdapter { * @param timeoutInMilliseconds * if the race definition is not received for the race within this time, the race tracker for that race * is stopped; use -1 to wait forever + * @param trackWind TODO + * @param correctWindDirectionByMagneticDeclination TODO */ RaceHandle addTracTracRace(TrackerManager trackerManager, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, long timeoutInMilliseconds, String tracTracUsername, String tracTracPassword, String raceStatus, - String raceVisibility) throws MalformedURLException, FileNotFoundException, URISyntaxException, Exception; + String raceVisibility, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) throws MalformedURLException, FileNotFoundException, URISyntaxException, Exception; /** * Same as {@link #addTracTracRace(URL, URI, URI, WindStore, long)}, only that start and end of tracking are @@ -62,43 +64,25 @@ public interface TracTracAdapter { * @param trackerManager * the race will be added to that manager * @param regattaToAddTo - * if null, an existing regatta by the name of the TracTrac event with the boat class name - * appended in parentheses will be looked up; if not found, a default regatta with that name will be - * created, with a single default series and a single default fleet. If a valid {@link RegattaIdentifier} - * is specified, a regatta lookup is performed with that identifier; if the regatta is found, it is used - * to add the races to. Otherwise, a default regatta as described above will be created and used. + * if null, the regatta into which the race has previously been loaded will be looked up; if + * found, the race will be loaded into that regatta; otherwise, an existing regatta by the name of the + * TracTrac event with the boat class name appended in parentheses will be looked up; if not found, a + * default regatta with that name will be created, with a single default series and a single default + * fleet. If a valid {@link RegattaIdentifier} is specified, a regatta lookup is performed with that + * identifier; if the regatta is found, it is used to add the races to. Otherwise, a default regatta as + * described above will be created and used. * @param offsetToStartTimeOfSimulatedRace * if non-null, the {@link Simulator} will be used with this duration as start offset + * @param trackWind TODO + * @param correctWindDirectionByMagneticDeclination TODO */ RaceHandle addTracTracRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint trackingStartTime, TimePoint trackingEndTime, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, long timeoutForReceivingRaceDefinitionInMilliseconds, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, - String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility) + String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) throws MalformedURLException, FileNotFoundException, URISyntaxException, Exception; - /** - * Defines the regatta and for each race listed in the JSON document that is not already being tracked by this service - * creates a {@link TracTracRaceTracker} that starts tracking the respective race. The {@link RaceDefinition}s obtained this - * way are all grouped into the single {@link Regatta} produced for the event listed in the JSON response. Note that - * the many race trackers will have their TracTrac Event each, all with the same name, meaning the same - * event but being distinct. - * @param trackerManager TODO - * @param jsonURL - * URL of a JSON response that contains an "event" object telling the event's name and ID, as well as a - * JSON array named "races" which tells ID and replay URL for the race. From those replay URLs the - * paramURL for the Java client can be derived. - * @param timeoutInMilliseconds - * if a race definition is not received for a race of this event within this time, the race tracker for - * that race is stopped; use -1 to wait forever - * @param raceLogStore TODO - */ - Regatta addRegatta(TrackerManager trackerManager, URL jsonURL, URI liveURI, URI storedURI, - URI courseDesignUpdateURI, WindStore windStore, long timeoutInMilliseconds, String tracTracUsername, - String tracTracPassword, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore) - throws MalformedURLException, FileNotFoundException, URISyntaxException, IOException, ParseException, - org.json.simple.parser.ParseException, Exception; - /** * For the JSON URL of an account / event, lists the paramURLs that can be used for * {@link #addTracTracRace(URL, URI, URI, WindStore, long)} calls to individually start tracking races of this diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java index afadc3c4379..9cd6990be8f 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java @@ -654,15 +654,16 @@ public class DomainFactoryImpl implements DomainFactory { @Override public TracTracRaceTracker createRaceTracker(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, - RegattaLogStore regattaLogStore, WindStore windStore, String tracTracUsername, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, + RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, - TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, - URISyntaxException, CreateModelException, SubscriberInitializationException { + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, + RaceTrackingConnectivityParametersImpl connectivityParams) throws MalformedURLException, + FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException { return new TracTracRaceTrackerImpl(this, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, - endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, - tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, - raceLogResolver); + endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, + raceLogStore, regattaLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, + raceVisibility, trackedRegattaRegistry, raceLogResolver, connectivityParams); } @Override @@ -671,13 +672,13 @@ public class DomainFactoryImpl implements DomainFactory { Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, - TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceTrackingConnectivityParametersImpl connectivityParams) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException { return new TracTracRaceTrackerImpl(regatta, this, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, - raceVisibility, trackedRegattaRegistry, raceLogResolver); + raceVisibility, trackedRegattaRegistry, raceLogResolver, connectivityParams); } @Override @@ -695,14 +696,15 @@ public class DomainFactoryImpl implements DomainFactory { URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, String tracTracUsername, String tracTracPassword, String raceStatus, - String raceVisibility) { + String raceVisibility, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) { return new RaceTrackingConnectivityParametersImpl(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, - regattaLogStore, this, tracTracUsername, tracTracPassword, raceStatus, raceVisibility); + regattaLogStore, this, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackWind, correctWindDirectionByMagneticDeclination); } @Override - public JSONService parseJSONURLForOneRaceRecord(URL jsonURL, String raceId, boolean loadClientParams) throws IOException, ParseException, org.json.simple.parser.ParseException, URISyntaxException { + public JSONService parseJSONURLForOneRaceRecord(URL jsonURL, String raceId, boolean loadClientParams) + throws IOException, ParseException, org.json.simple.parser.ParseException, URISyntaxException { return new JSONServiceImpl(jsonURL, raceId, loadClientParams); } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java index 91d819e7da0..e7fc260f00a 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java @@ -11,16 +11,18 @@ import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.racelog.RaceLogStore; import com.sap.sailing.domain.regattalog.RegattaLogStore; import com.sap.sailing.domain.tracking.RaceTracker; -import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tracking.impl.AbstractRaceTrackingConnectivityParameters; import com.sap.sailing.domain.tractracadapter.DomainFactory; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.tractrac.model.lib.api.event.CreateModelException; import com.tractrac.subscription.lib.api.SubscriberInitializationException; -public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConnectivityParameters { +public class RaceTrackingConnectivityParametersImpl extends AbstractRaceTrackingConnectivityParameters { + public static final String TYPE = "TRAC_TRAC"; + private final URL paramURL; private final URI liveURI; private final URI storedURI; @@ -42,8 +44,8 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, DomainFactory domainFactory, String tracTracUsername, String tracTracPassword, String raceStatus, - String raceVisibility) { - super(); + String raceVisibility, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) { + super(trackWind, correctWindDirectionByMagneticDeclination); this.paramURL = paramURL; this.liveURI = liveURI; this.storedURI = storedURI; @@ -62,6 +64,11 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne this.useInternalMarkPassingAlgorithm = useInternalMarkPassingAlgorithm; } + @Override + public String getTypeIdentifier() { + return TYPE; + } + @Override public RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, @@ -69,7 +76,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne RaceTracker tracker = domainFactory.createRaceTracker(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, - raceVisibility, trackedRegattaRegistry, raceLogResolver); + raceVisibility, trackedRegattaRegistry, raceLogResolver, this); return tracker; } @@ -79,7 +86,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne RaceTracker tracker = domainFactory.createRaceTracker(regatta, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, - raceVisibility, trackedRegattaRegistry, raceLogResolver); + raceVisibility, trackedRegattaRegistry, raceLogResolver, this); return tracker; } @@ -93,4 +100,52 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne return delayToLiveInMillis; } + public URL getParamURL() { + return paramURL; + } + + public URI getLiveURI() { + return liveURI; + } + + public URI getStoredURI() { + return storedURI; + } + + public URI getCourseDesignUpdateURI() { + return courseDesignUpdateURI; + } + + public TimePoint getStartOfTracking() { + return startOfTracking; + } + + public TimePoint getEndOfTracking() { + return endOfTracking; + } + + public Duration getOffsetToStartTimeOfSimulatedRace() { + return offsetToStartTimeOfSimulatedRace; + } + + public String getTracTracUsername() { + return tracTracUsername; + } + + public String getTracTracPassword() { + return tracTracPassword; + } + + public String getRaceStatus() { + return raceStatus; + } + + public String getRaceVisibility() { + return raceVisibility; + } + + public boolean isUseInternalMarkPassingAlgorithm() { + return useInternalMarkPassingAlgorithm; + } + } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java index 0d7fd26a6ec..552a8325d6e 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java @@ -8,14 +8,12 @@ import java.text.ParseException; import java.util.List; import java.util.logging.Logger; -import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.racelog.RaceLogStore; import com.sap.sailing.domain.regattalog.RegattaLogStore; import com.sap.sailing.domain.tracking.RaceHandle; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackerManager; -import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.domain.tractracadapter.DomainFactory; import com.sap.sailing.domain.tractracadapter.JSONService; import com.sap.sailing.domain.tractracadapter.RaceRecord; @@ -51,14 +49,14 @@ public class TracTracAdapterImpl implements TracTracAdapter { public RaceHandle addTracTracRace(TrackerManager trackerManager, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, long timeoutInMilliseconds, String tracTracUsername, String tracTracPassword, String raceStatus, - String raceVisibility) throws Exception { + String raceVisibility, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) throws Exception { return trackerManager.addRace( /* regattaToAddTo */null, getTracTracDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, /* startOfTracking */null, /* endOfTracking */null, delayToLiveInMillis, /* offsetToStartTimeOfSimulatedRace */null, /* ignoreTracTracMarkPassings */ false, - raceLogStore, regattaLogStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility), + raceLogStore, regattaLogStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackWind, correctWindDirectionByMagneticDeclination), timeoutInMilliseconds); } @@ -66,14 +64,15 @@ public class TracTracAdapterImpl implements TracTracAdapter { public RaceHandle addTracTracRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, long timeoutInMilliseconds, - Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword, String raceStatus, - String raceVisibility) throws Exception { + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, + String tracTracPassword, String raceStatus, String raceVisibility, boolean trackWind, + boolean correctWindDirectionByMagneticDeclination) throws Exception { return trackerManager.addRace( regattaToAddTo, getTracTracDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, tracTracUsername, tracTracPassword, - raceStatus, raceVisibility), timeoutInMilliseconds); + raceStatus, raceVisibility, trackWind, correctWindDirectionByMagneticDeclination), timeoutInMilliseconds); } @Override @@ -85,21 +84,6 @@ public class TracTracAdapterImpl implements TracTracAdapter { return new Util.Pair>(jsonService.getEventName(), jsonService.getRaceRecords()); } - @Override - public Regatta addRegatta(TrackerManager trackerManager, URL jsonURL, URI liveURI, URI storedURI, - URI courseDesignUpdateURI, WindStore windStore, long timeoutInMilliseconds, String tracTracUsername, - String tracTracPassword, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore) throws Exception { - JSONService jsonService = getTracTracDomainFactory().parseJSONURLWithRaceRecords(jsonURL, true); - Regatta regatta = null; - for (RaceRecord rr : jsonService.getRaceRecords()) { - URL paramURL = rr.getParamURL(); - regatta = addTracTracRace(trackerManager, paramURL, liveURI, storedURI, courseDesignUpdateURI, - raceLogStore, regattaLogStore, timeoutInMilliseconds, tracTracUsername, "", "", - tracTracPassword).getRegatta(); - } - return regatta; - } - @Override public RaceRecord getSingleTracTracRaceRecord(URL jsonURL, String raceId, boolean loadClientParams) throws Exception { diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java index 48061878442..2656e92091a 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java @@ -195,7 +195,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl private final Set receivers; private final DomainFactory domainFactory; private final WindStore windStore; - private final Set races; + private RaceDefinition race; private final DynamicTrackedRegatta trackedRegatta; private TrackedRaceStatus lastStatus; private Map> lastProgressPerID; @@ -257,14 +257,14 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, - String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) + String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceTrackingConnectivityParametersImpl connectivityParams) throws URISyntaxException, MalformedURLException, FileNotFoundException, CreateModelException, SubscriberInitializationException { this(ModelLocator.getEventFactory().createRace(new URI(paramURL.toString())), domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, tracTracUsername, - tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); + tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver, connectivityParams); } private TracTracRaceTrackerImpl(IRace tractracRace, DomainFactory domainFactory, URL paramURL, URI liveURI, @@ -272,12 +272,12 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, - TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceTrackingConnectivityParametersImpl connectivityParams) throws URISyntaxException, MalformedURLException, FileNotFoundException, SubscriberInitializationException { - this(tractracRace, null, domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, + this(tractracRace, /* regatta */ null, domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, - raceLogResolver); + raceLogResolver, connectivityParams); } /** @@ -291,13 +291,13 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean ignoreTracTracMarkPassings, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, - TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceTrackingConnectivityParametersImpl connectivityParams) throws URISyntaxException, MalformedURLException, FileNotFoundException, CreateModelException, SubscriberInitializationException { this(ModelLocator.getEventFactory().createRace(new URI(paramURL.toString())), regatta, domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, ignoreTracTracMarkPassings, raceLogStore, regattaLogStore, windStore, tracTracUsername, - tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); + tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver, connectivityParams); } /** @@ -316,15 +316,15 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl TimePoint endOfTracking, long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, - TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, + TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceTrackingConnectivityParametersImpl connectivityParams) throws URISyntaxException, MalformedURLException, FileNotFoundException, SubscriberInitializationException { - super(); + super(connectivityParams); this.trackedRegattaRegistry = trackedRegattaRegistry; this.tractracRace = tractracRace; this.tractracEvent = tractracRace.getEvent(); this.id = createID(paramURL, liveURI, storedURI); isLiveTracking = liveURI != null; - this.races = new HashSet(); + this.race = null; // no race received yet this.domainFactory = domainFactory; this.lastProgressPerID = new HashMap<>(); if (offsetToStartTimeOfSimulatedRace != null) { @@ -375,8 +375,8 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl public void updateRace(IRace race) { if (Util.equalsWithNull(race, TracTracRaceTrackerImpl.this.tractracRace)) { int delayToLiveInMillis = race.getLiveDelay()*1000; - for (RaceDefinition raceDefinition : getRaces()) { - DynamicTrackedRace trackedRace = getTrackedRegatta().getExistingTrackedRace(raceDefinition); + if (getRace() != null) { + DynamicTrackedRace trackedRace = getTrackedRegatta().getExistingTrackedRace(getRace()); if (trackedRace != null) { trackedRace.setDelayToLiveInMillis(delayToLiveInMillis); } @@ -385,13 +385,15 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl } }); // Start live and stored data streams - Regatta effectiveRegatta = regatta; + final Regatta effectiveRegatta; raceSubscriber = subscriberFactory.createRaceSubscriber(tractracRace, liveURI, storedURI); raceSubscriber.subscribeConnectionStatus(this); // Try to find a pre-associated event based on the Race ID - if (effectiveRegatta == null) { + if (regatta == null) { Serializable raceID = domainFactory.getRaceID(tractracRace); effectiveRegatta = trackedRegattaRegistry.getRememberedRegattaForRace(raceID); + } else { + effectiveRegatta = regatta; } // removeRace may detach the domain regatta from the domain factory if that // removed the last race; therefore, it's important to getOrCreate the @@ -517,13 +519,13 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl } @Override - public RaceHandle getRacesHandle() { + public RaceHandle getRaceHandle() { return new RaceHandleImpl(domainFactory, tractracRace, getTrackedRegatta(), this); } @Override - public Set getRaces() { - return races; + public RaceDefinition getRace() { + return race; } protected void addListenersForStoredDataAndStartController(Iterable listenersForStoredData) { @@ -579,8 +581,8 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl * @see #updateStatusOfTrackedRace(DynamicTrackedRace) */ private void updateStatusOfTrackedRaces() { - for (RaceDefinition race : getRaces()) { - DynamicTrackedRace trackedRace = getTrackedRegatta().getExistingTrackedRace(race); + if (getRace() != null) { + DynamicTrackedRace trackedRace = getTrackedRegatta().getExistingTrackedRace(getRace()); if (trackedRace != null) { updateStatusOfTrackedRace(trackedRace); } @@ -635,7 +637,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl } } } - logger.info("Stored data progress in tracker "+getID()+" for race(s) "+getRaces()+": "+progress); + logger.info("Stored data progress in tracker "+getID()+" for race(s) "+getRace()+": "+progress); lastStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.LOADING, progress); if (progress==1.0) { new AbstractLoadingQueueDoneCallBack(receivers) { @@ -651,27 +653,28 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl } @Override - public void addRaceDefinition(RaceDefinition race, DynamicTrackedRace trackedRace) { - races.add(race); + public void addRaceDefinition(final RaceDefinition race, final DynamicTrackedRace trackedRace) { + this.race = race; updateStatusOfTrackedRace(trackedRace); + notifyRaceCreationListeners(); } @Override public void gotLiveDataEvent(ILiveDataEvent liveDataEvent) { - logger.info("Status change in tracker "+getID()+" for race(s) "+getRaces()+": "+liveDataEvent); + logger.info("Status change in tracker "+getID()+" for race(s) "+getRace()+": "+liveDataEvent); } @Override public void gotStoredDataEvent(IStoredDataEvent storedDataEvent) { - logger.info("Status change in tracker "+getID()+" for race(s) "+getRaces()+": "+storedDataEvent); + logger.info("Status change in tracker "+getID()+" for race(s) "+getRace()+": "+storedDataEvent); switch (storedDataEvent.getType()) { case Begin: - logger.info("Stored data begin in tracker "+getID()+" for race(s) "+getRaces()); + logger.info("Stored data begin in tracker "+getID()+" for race(s) "+getRace()); lastStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.LOADING, 0); updateStatusOfTrackedRaces(); break; case End: - logger.info("Stored data end in tracker "+getID()+" for race(s) "+getRaces()); + logger.info("Stored data end in tracker "+getID()+" for race(s) "+getRace()); if (isLiveTracking) { lastStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, 1); updateStatusOfTrackedRaces(); @@ -681,14 +684,14 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl storedDataProgress(storedDataEvent.getProgress()); break; case Error: - logger.warning("Error with stored data in tracker "+getID()+" for race(s) "+getRaces()+": "+storedDataEvent.getError()); + logger.warning("Error with stored data in tracker "+getID()+" for race(s) "+getRace()+": "+storedDataEvent.getError()); break; } } @Override public void stopped(Object o) { - logger.info("stopped TracTrac tracking in tracker " + getID() + " for " + getRaces() + " while in status " + logger.info("stopped TracTrac tracking in tracker " + getID() + " for " + getRace() + " while in status " + lastStatus); new AbstractLoadingQueueDoneCallBack(receivers) { @Override @@ -697,13 +700,13 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl updateStatusOfTrackedRaces(); if (!stopped) { try { - for (RaceDefinition race : getRaces()) { + if (getRace() != null) { // See also bug 1517; with TracAPI we assume that when stopped(IEvent) is called by the // TracAPI then // all subscriptions have received all their data and it's therefore safe to stop all // subscriptions // at this point without missing any data. - trackedRegattaRegistry.stopTracking(regatta, race); + trackedRegattaRegistry.stopTracking(regatta, getRace()); } } catch (InterruptedException | IOException e) { logger.log(Level.INFO, "Interrupted while trying to stop tracker " + this, e); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LiveLeaderboardUpdater.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LiveLeaderboardUpdater.java index c1b211c7d3b..f5e3a3409b1 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LiveLeaderboardUpdater.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LiveLeaderboardUpdater.java @@ -169,9 +169,7 @@ public class LiveLeaderboardUpdater implements Runnable { result = currentLiveLeaderboard; } if (result == null) { - if (logger.isLoggable(Level.FINEST)) { - logger.finest("waiting for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails); - } + logger.finest(()->"waiting for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails); ensureRunning(); try { this.wait(); @@ -184,18 +182,12 @@ public class LiveLeaderboardUpdater implements Runnable { if (columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.containsAll(namesOfRaceColumnsForWhichToLoadLegDetails) && (!addOverallDetails || currentLiveLeaderboardHasOverallDetails)) { result = currentLiveLeaderboard; - if (logger.isLoggable(Level.FINEST)) { - logger.finest("successfully waited for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails); - } + logger.finest(()->"successfully waited for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails); } else { - if (logger.isLoggable(Level.FINEST)) { - logger.finest("waiting for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails+" unsuccessful. Need to try again..."); - } + logger.finest(()->"waiting for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails+" unsuccessful. Need to try again..."); } } else { - if (logger.isLoggable(Level.FINEST)) { - logger.finest("leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails+" was provided in the meantime"); - } + logger.finest(()->"leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails+" was provided in the meantime"); } // now we either have a result (good, we're done), or the thread stopped running (then we need to renew the request) // or the thread still runs but the result may have expired and therefore be null (renew the request) diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java index 8539ef86d47..68cb1c53595 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java @@ -368,15 +368,11 @@ public class MarkPassingCalculator { @Override public void run() { try { - if (logger.isLoggable(Level.FINER)) { - logger.finer("Calculating MarkPassings for race "+raceName+", competitor " + c + " (" + Util.size(fixes) + " new fixes)"); - } + logger.finer(()->"Calculating MarkPassings for race "+raceName+", competitor " + c + " (" + Util.size(fixes) + " new fixes)"); Util.Pair, Iterable> candidateDeltas = finder.getCandidateDeltas(c, fixes); - if (logger.isLoggable(Level.FINER)) { - logger.finer("Received " + Util.size(candidateDeltas.getA()) + " new Candidates for race "+raceName+ - " and competitor "+c+" and will remove " - + Util.size(candidateDeltas.getB()) + " old Candidates for " + c); - } + logger.finer(()->"Received " + Util.size(candidateDeltas.getA()) + " new Candidates for race "+raceName+ + " and competitor "+c+" and will remove " + + Util.size(candidateDeltas.getB()) + " old Candidates for " + c); chooser.calculateMarkPassDeltas(c, candidateDeltas.getA(), candidateDeltas.getB()); } catch (Exception e) { // make sure the exception is logged and not only "swallowed" and suppressed by an invokeAll(...) statement diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java index 272e6db711b..b40cefa97b3 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java @@ -13,7 +13,6 @@ import java.util.NavigableSet; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; -import java.util.logging.Level; import java.util.logging.Logger; import com.sap.sailing.domain.base.Competitor; @@ -370,9 +369,7 @@ public class CandidateChooserImpl implements CandidateChooser { // The most likely edge taking us to currentMostLikelyEdge.getEnd() is found. Remember it. candidateWithParentAndHighestTotalProbability.put(currentMostLikelyEdge.getEnd(), new Util.Pair( currentMostLikelyEdge.getStart(), currentHighestProbability)); - if (logger.isLoggable(Level.FINEST)) { - logger.finest("Added "+ currentMostLikelyEdge + " as most likely edge for " + c); - } + logger.finest(()->"Added "+ currentMostLikelyEdge + " as most likely edge for " + c); endFound = currentMostLikelyEdge.getEnd() == endOfFixedInterval; if (!endFound) { // the end of the segment was not yet found; add edges leading away from @@ -426,9 +423,7 @@ public class CandidateChooserImpl implements CandidateChooser { newMarkPassings.add(newMarkPassing); } } - if (logger.isLoggable(Level.FINE)) { - logger.fine("Updating MarkPasses for " + c + " in case "+race.getRace().getName()); - } + logger.fine(()->"Updating MarkPasses for " + c + " in case "+race.getRace().getName()); race.updateMarkPassings(c, newMarkPassings); } } @@ -542,9 +537,7 @@ public class CandidateChooserImpl implements CandidateChooser { private void removeCandidates(Competitor c, Iterable wrongCandidates) { for (Candidate can : wrongCandidates) { - if (logger.isLoggable(Level.FINEST)) { - logger.finest("Removing all edges containing " + can + "of "+ c); - } + logger.finest(()->"Removing all edges containing " + can + "of "+ c); candidates.get(c).remove(can); Map> edges = allEdges.get(c); edges.remove(can); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polars/PolarDataService.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polars/PolarDataService.java index 5329886f015..3843f2bcc65 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polars/PolarDataService.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polars/PolarDataService.java @@ -170,14 +170,15 @@ public interface PolarDataService { void unregisterListener(BoatClass boatClass, PolarsChangedListener listener); + /** + * Announces a base {@link DomainFactory} to this polar data service that it now can start using. Calling this + * method with a non-{@code null} parameter will unblock all {@link #runWithDomainFactory} calls. + */ void registerDomainFactory(DomainFactory domainFactory); - void unregisterDomainFactory(DomainFactory domainFactory); - /** * When called, the method blocks until a {@link DomainFactory} has been {@link #registerDomainFactory(DomainFactory) registered} * with this service, then lets the {@code consumer} accept that domain factory. - * @throws InterruptedException */ void runWithDomainFactory(Consumer consumer) throws InterruptedException; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/RaceStateOfSameDayHelper.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/RaceStateOfSameDayHelper.java index 30bcbf35bca..55fb9044184 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/RaceStateOfSameDayHelper.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/RaceStateOfSameDayHelper.java @@ -2,13 +2,15 @@ package com.sap.sailing.domain.racelog; import java.util.Calendar; import java.util.Date; +import java.util.TimeZone; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; public class RaceStateOfSameDayHelper { public static boolean isRaceStateOfSameDay(TimePoint raceStartTimePoint, TimePoint raceFinishedTimePoint, - TimePoint abortingTimePoint, Calendar now) { + TimePoint abortingTimePoint, Calendar now, Duration clientTimeZoneOffset) { Date raceStartTime = null; Date raceFinishedTime = null; Date abortingTime = null; @@ -22,35 +24,46 @@ public class RaceStateOfSameDayHelper { if (abortingTimePoint != null) { abortingTime = abortingTimePoint.asDate(); } - - return isRaceStateOfSameDay(raceStartTime, raceFinishedTime, abortingTime, now); + return isRaceStateOfSameDay(raceStartTime, raceFinishedTime, abortingTime, now, clientTimeZoneOffset); } - public static boolean isRaceStateOfSameDay(Date raceStartTime, Date raceFinishedTime, Calendar now) { - boolean result = false; - if (raceFinishedTime != null) { - Calendar finishedTimeCal = Calendar.getInstance(); - finishedTimeCal.setTime(raceFinishedTime); - if(isSameDay(now, finishedTimeCal)) { - result = true; - } - } else if (raceStartTime != null) { - Calendar startTimeCal = Calendar.getInstance(); - startTimeCal.setTime(raceStartTime); - if(isSameDay(now, startTimeCal)) { - result = true; + public static boolean isRaceStateOfSameDay(Date raceStartTime, Date raceFinishedTime, Calendar now, Duration clientTimeZoneOffset) { + final boolean result; + final Calendar timeToCheck = Calendar.getInstance(); + // see if we shall and can adjust the time zone offset for determining the day boundaries: + if (clientTimeZoneOffset != null) { + final String[] availableTimeZoneIDsForOffset = TimeZone.getAvailableIDs((int) -clientTimeZoneOffset.asMillis()); + if (availableTimeZoneIDsForOffset != null && availableTimeZoneIDsForOffset.length > 0) { + // found a time zone with the offset requested; note that this may not be the time zone the client really is in + // but at least for the current time (which may be different from the "now" parameter) that time zone has + // the clientTimeZoneOffset from UTC. + final TimeZone tz = TimeZone.getTimeZone(availableTimeZoneIDsForOffset[0]); + timeToCheck.setTimeZone(tz); + now.setTimeZone(tz); } } - + if (raceFinishedTime != null || raceStartTime != null) { + if (raceFinishedTime != null) { + timeToCheck.setTime(raceFinishedTime); + } else if (raceStartTime != null) { + timeToCheck.setTime(raceStartTime); + } + result = isSameDay(now, timeToCheck); + } else { + result = false; + } return result; } - public static boolean isRaceStateOfSameDay(Date raceStartTime, Date raceFinishedTime, Date abortingTime, Calendar now) { - boolean result = isRaceStateOfSameDay(raceStartTime, raceFinishedTime, now); + /** + * The client's day starts at 00:00:00Z - clientTimeZoneOffset and ends at 23:59:59Z - clientTimeZoneOffset. + */ + public static boolean isRaceStateOfSameDay(Date raceStartTime, Date raceFinishedTime, Date abortingTime, Calendar now, Duration clientTimeZoneOffset) { + boolean result = isRaceStateOfSameDay(raceStartTime, raceFinishedTime, now, clientTimeZoneOffset); if (!result && (abortingTime != null)) { Calendar abortingTimeCal = Calendar.getInstance(); abortingTimeCal.setTime(abortingTime); - if(isSameDay(now, abortingTimeCal)) { + if (isSameDay(now, abortingTimeCal)) { result = true; } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/impl/GPSFixStoreImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/impl/GPSFixStoreImpl.java index 73d4e067ee2..2cc9e41a089 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/impl/GPSFixStoreImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/impl/GPSFixStoreImpl.java @@ -40,7 +40,7 @@ public class GPSFixStoreImpl implements GPSFixStore { private void loadTrack(DynamicGPSFixTrack track, DeviceIdentifier device, TimePoint from, TimePoint to, boolean inclusive) throws NoCorrespondingServiceRegisteredException, TransformationException { - sensorFixStore.loadFixes(track::add, device, from, to, inclusive); + sensorFixStore.loadFixes(fix->track.add(fix, /* replace */ true), device, from, to, inclusive); } @Override diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/AbstractRaceTrackerBaseImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/AbstractRaceTrackerBaseImpl.java index 20a899d1b3d..9fe94a359a2 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/AbstractRaceTrackerBaseImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/AbstractRaceTrackerBaseImpl.java @@ -2,12 +2,27 @@ package com.sap.sailing.domain.tracking; import java.io.IOException; import java.net.MalformedURLException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.sap.sailing.domain.base.RaceDefinition; /** * Base class for all {@link RaceTracker}s that must implement listener notifications */ public abstract class AbstractRaceTrackerBaseImpl implements RaceTracker { private final RaceTrackerListeners listeners = new RaceTrackerListeners(); + private final Set raceCreationListeners = Collections + .newSetFromMap(new ConcurrentHashMap()); + + private final RaceTrackingConnectivityParameters connectivityParams; + + public AbstractRaceTrackerBaseImpl(RaceTrackingConnectivityParameters connectivityParams) { + super(); + this.connectivityParams = connectivityParams; + } /** * Ensure stop method does notify all listeners after tracker stopped. @@ -23,11 +38,6 @@ public abstract class AbstractRaceTrackerBaseImpl implements RaceTracker { /** * Template stop method for subclasses. - * - * @param preemptive - * @throws MalformedURLException - * @throws IOException - * @throws InterruptedException */ protected void onStop(boolean preemptive) throws MalformedURLException, IOException, InterruptedException { } @@ -41,4 +51,44 @@ public abstract class AbstractRaceTrackerBaseImpl implements RaceTracker { public void remove(Listener listener) { listeners.removeListener(listener); } + + @Override + public void add(RaceCreationListener listener) { + final RaceDefinition race; + synchronized (raceCreationListeners) { + race = getRace(); + if (race == null) { + raceCreationListeners.add(listener); + } + } + if (race != null) { // listener hasn't been added above; notify immediately + listener.onRaceCreated(this); + } + } + + @Override + public void remove(RaceCreationListener listener) { + synchronized (raceCreationListeners) { + raceCreationListeners.remove(listener); + } + } + + /** + * Notifies all {@link RaceCreationListener}s registered and removes them. Must be called + * after {@link #getRace()} has started returning a valid, non-{@code null} race. + */ + protected void notifyRaceCreationListeners() { + assert getRace() != null; + final Set listenersToNotify; + synchronized (raceCreationListeners) { + listenersToNotify = new HashSet<>(raceCreationListeners); + raceCreationListeners.clear(); + } + listenersToNotify.forEach(l->l.onRaceCreated(this)); + } + + @Override + public RaceTrackingConnectivityParameters getConnectivityParams() { + return connectivityParams; + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/AbstractRaceTrackerImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/AbstractRaceTrackerImpl.java index 158b784f312..a2ce0922d8c 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/AbstractRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/AbstractRaceTrackerImpl.java @@ -1,25 +1,28 @@ package com.sap.sailing.domain.tracking; -import java.util.HashSet; -import java.util.Set; - import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; public abstract class AbstractRaceTrackerImpl extends AbstractRaceTrackerBaseImpl { + public AbstractRaceTrackerImpl(RaceTrackingConnectivityParameters connectivityParams) { + super(connectivityParams); + } + @Override - public Set getRaceIdentifiers() { - Set result = new HashSet(); - final Set races = getRaces(); - if (races != null) { - for (RaceDefinition race : races) { - TrackedRace trackedRace = getTrackedRegatta().getTrackedRace(race); - if (trackedRace != null) { - result.add(trackedRace.getRaceIdentifier()); - } + public RegattaAndRaceIdentifier getRaceIdentifier() { + final RegattaAndRaceIdentifier result; + final RaceDefinition race = getRace(); + if (race != null) { + TrackedRace trackedRace = getTrackedRegatta().getTrackedRace(race); + if (trackedRace != null) { + result = trackedRace.getRaceIdentifier(); + } else { + result = null; } + } else { + result = null; } return result; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicRaceDefinitionSet.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicRaceDefinitionSet.java index 1c6d11f66ac..a9497e47c14 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicRaceDefinitionSet.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicRaceDefinitionSet.java @@ -6,7 +6,7 @@ import com.sap.sailing.domain.base.RaceDefinition; * Particularly the way the TracTrac connector was originally designed, the set of {@link RaceDefinition} objects * received from connecting to a particular TracTrac event is not known up-front. We even wait (for historic reasons, * because the course definition used to be immutable) for the course to be received before creating the - * {@link RaceDefinition} object. Therefore, a {@link RaceTracker#getRaces()} call needs to understand which + * {@link RaceDefinition} object. Therefore, a {@link RaceTracker#getRace()} call needs to understand which * {@link RaceDefinition} objects were created so far.

    * * This interface is used particularly by the race course receiver in the TracTrac case to update a tracker's set @@ -15,6 +15,7 @@ import com.sap.sailing.domain.base.RaceDefinition; * @author Axel Uhl (d043530) * */ +@FunctionalInterface public interface DynamicRaceDefinitionSet { void addRaceDefinition(RaceDefinition race, DynamicTrackedRace trackedRace); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTracker.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTracker.java index 347b8ec2a4f..02b4b3b6258 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTracker.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTracker.java @@ -3,7 +3,6 @@ package com.sap.sailing.domain.tracking; import java.io.IOException; import java.net.MalformedURLException; import java.util.Map; -import java.util.Set; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; @@ -13,8 +12,8 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; * Centerpiece of a tracking adapter. A tracker is responsible for receiving tracking data for one or more * {@link RaceDefinition races} that are {@link Regatta#getAllRaces() part of} a common {@link #getRegatta() Event}. Some * tracker architectures may not be able to deliver all data for the {@link RaceDefinition} when created or started. - * Therefore, {@link #getRaces()} may return null if the race information hasn't been received by the - * tracker yet. Through the {@link RaceHandle} returned by {@link #getRacesHandle()} it is also possible to perform a + * Therefore, {@link #getRace()} may return null if the race information hasn't been received by the + * tracker yet. Through the {@link RaceHandle} returned by {@link #getRaceHandle()} it is also possible to perform a * {@link RaceHandle#getRace() blocking get} for the race tracked by this tracker. *

    * @@ -56,17 +55,17 @@ public interface RaceTracker { com.sap.sailing.domain.base.Regatta getRegatta(); /** - * Returns the races being tracked by this tracker. Non-blocking call that returns null or an empty set - * if the {@link RaceDefinition} for a TracTrac Event hasn't been created yet, e.g., because the course definition + * Returns the races being tracked by this tracker. Non-blocking call that returns null + * if the {@link RaceDefinition} hasn't been created yet, e.g., because the course definition * hasn't been received yet or the listener for receiving course information hasn't been registered (yet). Also - * returns races that have been removed from containing structures which may lead this tracker to no longer update + * returns a race that may have been removed from containing structures which may lead this tracker to no longer update * their {@link TrackedRace} with new data. */ - Set getRaces(); + RaceDefinition getRace(); - Set getRaceIdentifiers(); + RegattaAndRaceIdentifier getRaceIdentifier(); - RaceHandle getRacesHandle(); + RaceHandle getRaceHandle(); DynamicTrackedRegatta getTrackedRegatta(); @@ -80,6 +79,7 @@ public interface RaceTracker { /** * Listener interface for race tracker related events */ + @FunctionalInterface interface Listener { /** * Tracker has stopped event, see {@link RaceTracker#stop(boolean)} method @@ -88,20 +88,55 @@ public interface RaceTracker { */ void onTrackerWillStop(boolean preemptive); } + + @FunctionalInterface + interface RaceCreationListener { + /** + * Tracker has received its {@link RaceDefinition}, so that now {@link RaceTracker#getRace} no longer returns + * {@code null} but a valid {@link RaceDefinition} + * + * @param tracker + * this tracker is passed to the listener which can then obtain the {@link RaceTracker#getRace() + * race} and the {@link RaceTracker#getConnectivityParams() connectivity parameters}, etc. If the + * {@link RaceTracker} already has created its race, this method is called immediately upon + * {@link RaceTracker#add(Listener) adding} this listener so that the listener will always receive + * the call for a valid race if the race is created at any point in time, regardless the point in + * time of the listener registration. This helps avoid race conditions. + */ + void onRaceCreated(RaceTracker tracker); + } /** * Register a new RaceTracker.Listener for this race tracker. * - * @param newListener * @return true if listener has been added */ boolean add(RaceTracker.Listener newListener); /** - * Remove listener from racetracker + * Remove listener from race tracker * - * @param newListener + * @param listener * @return the listener registration for listener removal */ - void remove(RaceTracker.Listener newListener); + void remove(RaceTracker.Listener listener); + + /** + * Adds a listener that will be {@link RaceCreationListener#onRaceCreated(RaceTracker) notified} when the + * {@link #getRace()} method starts returning a valid, non-{@code null} race. The listener will be notified + * immediately if {@link #getRace()} already yields a valid {@link RaceDefinition} when calling this method so + * that the listener will be notified at least once. The listener will automatically be removed after it has been + * notified. + */ + void add(RaceTracker.RaceCreationListener listener); + + void remove(RaceTracker.RaceCreationListener listener); + + /** + * The connectivity parameters used to create this tracker. Can be used, e.g., to add or remove those parameters to + * the set of trackers to restore after a server restart. May return {@code null}, e.g., in case the tracker was + * created by a test case that did not use a {@link RaceTrackingConnectivityParameters} object to describe what to + * track. + */ + RaceTrackingConnectivityParameters getConnectivityParams(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParameters.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParameters.java index 05376e6f043..34690d54ff4 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParameters.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParameters.java @@ -5,21 +5,26 @@ import com.sap.sailing.domain.base.Fleet; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Series; - - - /** * Different tracking providers require different sets of arguments to start tracking a race. * This interface represents the functionality required to start tracking a race, agnostic of the * particular parameters and ways of launching the connector. Use {@link #createRaceTracker()} to - * create the tracker that starts tracking the race identified by these tracking parameters. + * create the tracker that starts tracking the race identified by these tracking parameters.

    + * + * The parameters also tell whether wind tracking shall be activated for this race (see {@link #isTrackWind()}). + * This property can be {@link #setTrackWind(boolean) updated}, making sense in case tracking a live race + * is stopped in a running server, stopping the wind tracker and therefore not requiring wind tracking + * to be activated anymore when the race is restored during server restart. * * @author Axel Uhl (d043530) * */ public interface RaceTrackingConnectivityParameters { /** - * Starts a {@link RaceTracker} using the connectivity parameters provided by this object. + * Starts a {@link RaceTracker} using the connectivity parameters provided by this object. As no specific + * {@link Regatta} is provided, this will first look up a regatta for the race from + * {@link TrackedRegattaRegistry#getRememberedRegattaForRace(java.io.Serializable)} and if not found + * will look up or create a default regatta based on race data such as an event name and the boat class. */ RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, RaceLogResolver raceLogResolver) throws Exception; @@ -43,4 +48,16 @@ public interface RaceTrackingConnectivityParameters { * Gets the configured delay time to the 'live' timepoint for this tracker */ public long getDelayToLiveInMillis(); + + /** + * A non-{@code null} unique type identifier that helps finding services that need to deal with connectivity + * parameters in a type-specific way + */ + public String getTypeIdentifier(); + + boolean isTrackWind(); + + void setTrackWind(boolean trackWind); + + boolean isCorrectWindDirectionByMagneticDeclination(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParametersHandler.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParametersHandler.java new file mode 100644 index 00000000000..a47f15c71d7 --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParametersHandler.java @@ -0,0 +1,48 @@ +package com.sap.sailing.domain.tracking; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.UUID; + +import org.json.simple.JSONObject; + +import com.sap.sse.common.TypeBasedServiceFinder; + +/** + * Handler for mapping objects of classes that implement the {@link RaceTrackingConnectivityParameters} interface + * to / from maps using {@link String}s as keys, such as {@link JSONObject} or {@link BasicDBObject}. This can be + * used to save and restore the races currently being tracked by a server, based on the tracking connectivity + * data provided when last tracked. + * + * @author Axel Uhl (d043530) + * + */ +public interface RaceTrackingConnectivityParametersHandler { + /** + * Produces a map that has the value for the {@link TypeBasedServiceFinder#TYPE} key set such that this handler + * can be {@link TypeBasedServiceFinder#findService(String) found} based on that value again; furthermore, all + * other key/value pairs in the map returned allow the {@link #mapTo(Map)} method of this handler to restore + * a {@link RaceTrackingConnectivityParameters} object that is equivalent to {@code params}.

    + * + * The types of the value objects must be restricted to thos types that MongoDB can handle as field values, + * in particular {@link String}, {@link Number} and {@link UUID}. + */ + Map mapFrom(RaceTrackingConnectivityParameters params); + + /** + * Produces a {@link RaceTrackingConnectivityParameters} object from the contents of {@code map}, ignoring the + * {@link TypeBasedServiceFinder#TYPE} key which has enabled {@link TypeBasedServiceFinder#findService(String) + * finding} this service instance in the first place. The object returned is equivalent to one that has + * produced {@code map} by passing it to this service's {@link #mapFrom(RaceTrackingConnectivityParameters)} + * method. + */ + RaceTrackingConnectivityParameters mapTo(Map map) throws MalformedURLException, URISyntaxException; + + /** + * @return a map that contains the {@link TypeBasedServiceFinder#TYPE} identifier as taken from + * {@link RaceTrackingConnectivityParameters#getTypeIdentifier()} as well as a non-empty set of further + * entries that uniquely identify the {@code params} object in the database, in particular for removing. + */ + Map getKey(RaceTrackingConnectivityParameters params); +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLeg.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLeg.java index da4592948ff..dcd9c41478f 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLeg.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLeg.java @@ -10,10 +10,10 @@ import com.sap.sailing.domain.common.Distance; import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.TargetTimeInfo.LegTargetTimeInfo; import com.sap.sailing.domain.polars.NotEnoughDataHasBeenAddedException; import com.sap.sailing.domain.polars.PolarDataService; import com.sap.sailing.domain.tracking.impl.TrackedLegImpl; -import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util.Pair; @@ -164,7 +164,7 @@ public interface TrackedLeg extends Serializable { * is not available * @throws NoWindException no wind available. unable to determine legtypes for given timepoint */ - Duration getEstimatedTimeToComplete(PolarDataService polarDataService, TimePoint timepoint, MarkPositionAtTimePointCache markPositionCache) + LegTargetTimeInfo getEstimatedTimeToComplete(PolarDataService polarDataService, TimePoint timepoint, MarkPositionAtTimePointCache markPositionCache) throws NotEnoughDataHasBeenAddedException, NoWindException; /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index c9f4e7ec66d..a456b3f8d49 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -32,6 +32,7 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.domain.common.TimingConstants; import com.sap.sailing.domain.common.TrackedRaceStatusEnum; import com.sap.sailing.domain.common.Wind; @@ -923,13 +924,13 @@ public interface TrackedRace extends Serializable, IsManagedByCachenull or no regatta by that identifier is found, an existing regatta by the name of the - * TracTrac event with the boat class name appended in parentheses will be looked up; if not found, a - * default regatta with that name will be created, with a single default series and a single default - * fleet. If a valid {@link RegattaIdentifier} is specified, a regatta lookup is performed with that - * identifier; if the regatta is found, it is used to add the races to, and + * if null or no regatta by that identifier is found, the regatta into which the race has + * previously been loaded will be looked up; if found, the race will be loaded into that regatta; + * otherwise, an existing default regatta will be looked up; if not found, a default regatta with that + * name will be created, with a single default series and a single default fleet. If a valid + * {@link RegattaIdentifier} is specified, a regatta lookup is performed with that identifier; if the + * regatta is found, it is used to add the races to, and * {@link #setRegattaForRace(Regatta, RaceDefinition)} is called to remember the association * persistently. Otherwise, a default regatta as described above will be created and used. */ diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/AbstractRaceTrackingConnectivityParameters.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/AbstractRaceTrackingConnectivityParameters.java new file mode 100644 index 00000000000..cc90c62286b --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/AbstractRaceTrackingConnectivityParameters.java @@ -0,0 +1,29 @@ +package com.sap.sailing.domain.tracking.impl; + +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; + +public abstract class AbstractRaceTrackingConnectivityParameters implements RaceTrackingConnectivityParameters { + private boolean trackWind; + private final boolean correctWindDirectionByMagneticDeclination; + + public AbstractRaceTrackingConnectivityParameters(boolean trackWind, boolean correctWindDirectionByMagneticDeclination) { + super(); + this.trackWind = trackWind; + this.correctWindDirectionByMagneticDeclination = correctWindDirectionByMagneticDeclination; + } + + @Override + public boolean isTrackWind() { + return trackWind; + } + + @Override + public void setTrackWind(boolean trackWind) { + this.trackWind = trackWind; + } + + @Override + public boolean isCorrectWindDirectionByMagneticDeclination() { + return correctWindDirectionByMagneticDeclination; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/AbstractRaceTrackingConnectivityParametersHandler.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/AbstractRaceTrackingConnectivityParametersHandler.java new file mode 100644 index 00000000000..447873b891b --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/AbstractRaceTrackingConnectivityParametersHandler.java @@ -0,0 +1,25 @@ +package com.sap.sailing.domain.tracking.impl; + +import java.util.Map; + +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParametersHandler; + +public abstract class AbstractRaceTrackingConnectivityParametersHandler + implements RaceTrackingConnectivityParametersHandler { + private static final String CORRECT_WIND_DIRECTION_BY_MAGNETIC_DECLINATION = "correctWindDirectionByMagneticDeclination"; + private static final String TRACK_WIND = "trackWind"; + + protected void addWindTrackingParameters(RaceTrackingConnectivityParameters params, Map map) { + map.put(TRACK_WIND, params.isTrackWind()); + map.put(CORRECT_WIND_DIRECTION_BY_MAGNETIC_DECLINATION, params.isCorrectWindDirectionByMagneticDeclination()); + } + + protected boolean isTrackWind(Map from) { + return from.containsKey(TRACK_WIND) ? (Boolean) from.get(TRACK_WIND) : false; + } + + protected boolean isCorrectWindDirectionByMagneticDeclination(Map from) { + return from.containsKey(CORRECT_WIND_DIRECTION_BY_MAGNETIC_DECLINATION) ? (Boolean) from.get(CORRECT_WIND_DIRECTION_BY_MAGNETIC_DECLINATION) : true; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java index ccd012a2176..35375bc834e 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java @@ -1062,15 +1062,13 @@ public class GPSFixTrackImpl extends MappedTra FixType last; lockForRead(); try { - if (logger.isLoggable(Level.FINEST)) { - logger.finest("GPS fix "+fix+" for "+getTrackedItem()+", isValid="+isValid(getInternalRawFixes(), fix)+ - ", time/distance/speed from last: "+ - ((last=getInternalRawFixes().lower(fix))==null - ? "null" - : (fix.getTimePoint().asMillis()-last.getTimePoint().asMillis()+"ms/"+ - fix.getPosition().getDistance(last.getPosition())) + "/"+ - fix.getPosition().getDistance(last.getPosition()).inTime(fix.getTimePoint().asMillis()-last.getTimePoint().asMillis()))); - } + logger.finest("GPS fix "+fix+" for "+getTrackedItem()+", isValid="+isValid(getInternalRawFixes(), fix)+ + ", time/distance/speed from last: "+ + ((last=getInternalRawFixes().lower(fix))==null + ? "null" + : (fix.getTimePoint().asMillis()-last.getTimePoint().asMillis()+"ms/"+ + fix.getPosition().getDistance(last.getPosition())) + "/"+ + fix.getPosition().getDistance(last.getPosition()).inTime(fix.getTimePoint().asMillis()-last.getTimePoint().asMillis()))); } finally { unlockAfterRead(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java index 85ee7d7ca06..a6d5692757c 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java @@ -31,10 +31,12 @@ import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.impl.MeterDistance; +import com.sap.sailing.domain.common.impl.TargetTimeInfoImpl; import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; import com.sap.sailing.domain.polars.NotEnoughDataHasBeenAddedException; import com.sap.sailing.domain.polars.PolarDataService; @@ -513,21 +515,22 @@ public class TrackedLegImpl implements TrackedLeg { } @Override - public Duration getEstimatedTimeToComplete(PolarDataService polarDataService, TimePoint timepoint, MarkPositionAtTimePointCache markPositionCache) + public TargetTimeInfo.LegTargetTimeInfo getEstimatedTimeToComplete(PolarDataService polarDataService, TimePoint timepoint, MarkPositionAtTimePointCache markPositionCache) throws NotEnoughDataHasBeenAddedException, NoWindException { assert timepoint.equals(markPositionCache.getTimePoint()); assert getTrackedRace() == markPositionCache.getTrackedRace(); - Position centralPosition = trackedRace.getCenterOfCourse(timepoint); - Wind wind = trackedRace.getWind(centralPosition, timepoint, getTrackedRace().getWindSources(WindSourceType.TRACK_BASED_ESTIMATION)); + Position centralPosition = getMiddleOfLeg(timepoint); + Wind wind = trackedRace.getWind(centralPosition, timepoint); Position from = trackedRace.getApproximatePosition(leg.getFrom(), timepoint); Position to = trackedRace.getApproximatePosition(leg.getTo(), timepoint); LegType legType = getLegType(timepoint); BoatClass boatClass = trackedRace.getRace().getBoatClass(); - Duration result; + final Bearing legBearing = from.getBearingGreatCircle(to); + Distance distance = from.getDistance(to); + Bearing trueWindAngleToLeg = legBearing.getDifferenceTo(wind.getBearing().reverse()); + final Duration result; if (legType == LegType.REACHING) { - Bearing trueWindAngle = wind.getBearing().getDifferenceTo(from.getBearingGreatCircle(to)); - SpeedWithConfidence reachSpeed = polarDataService.getSpeed(boatClass, wind, trueWindAngle); - Distance distance = from.getDistance(to); + SpeedWithConfidence reachSpeed = polarDataService.getSpeed(boatClass, wind, trueWindAngleToLeg); result = reachSpeed.getObject().getDuration(distance); } else { SpeedWithBearingWithConfidence portSpeedAndBearing = polarDataService.getAverageSpeedWithBearing( @@ -536,7 +539,7 @@ public class TrackedLegImpl implements TrackedLeg { boatClass, wind, legType, Tack.STARBOARD); result = estimateTargetTimeTacking(from, to, portSpeedAndBearing, starboardSpeedAndBearing, wind); } - return result; + return new TargetTimeInfoImpl.LegTargetTimeInfoImpl(distance, wind, legBearing, result, timepoint, legType); } private Duration estimateTargetTimeTacking(Position from, Position to, diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index 2d40b49208c..8f1cf87f9f9 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -82,11 +82,13 @@ import com.sap.sailing.domain.common.RegattaNameAndRaceName; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.domain.common.TimingConstants; import com.sap.sailing.domain.common.TrackedRaceStatusEnum; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.TargetTimeInfo.LegTargetTimeInfo; import com.sap.sailing.domain.common.abstractlog.TimePointSpecificationFoundInLog; import com.sap.sailing.domain.common.confidence.BearingWithConfidence; import com.sap.sailing.domain.common.confidence.BearingWithConfidenceCluster; @@ -101,6 +103,7 @@ import com.sap.sailing.domain.common.impl.CourseChangeImpl; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.KnotSpeedImpl; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.TargetTimeInfoImpl; import com.sap.sailing.domain.common.impl.WindImpl; import com.sap.sailing.domain.common.impl.WindSourceImpl; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; @@ -913,22 +916,17 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl TimePoint newStartTime = null; TimePoint newStartTimeWithoutInferenceFromStartMarkPassings = null; for (RaceLog raceLog : attachedRaceLogs.values()) { - if (logger.isLoggable(Level.FINEST)) { - logger.finest("Analyzing race log "+raceLog+" for race "+this.getRace().getName()); - } + logger.finest(()->"Analyzing race log "+raceLog+" for race "+this.getRace().getName()); newStartTime = new StartTimeFinder(raceLogResolver, raceLog).analyze().getStartTime(); if (newStartTime != null) { newStartTimeWithoutInferenceFromStartMarkPassings = newStartTime; - if (logger.isLoggable(Level.FINEST)) { - logger.finest("Found start time "+newStartTime+" in race log "+raceLog+" for race "+this.getRace().getName()); - } + final TimePoint finalNewStartTime = newStartTime; + logger.finest(()->"Found start time "+finalNewStartTime+" in race log "+raceLog+" for race "+this.getRace().getName()); break; } } if (newStartTime == null) { - if (logger.isLoggable(Level.FINEST)) { - logger.finest("No start time found in race logs for race "+getRace().getName()); - } + logger.finest(()->"No start time found in race logs for race "+getRace().getName()); newStartTime = getStartTimeReceived(); if (newStartTime != null) { newStartTimeWithoutInferenceFromStartMarkPassings = newStartTime; @@ -951,14 +949,12 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl if (startTimeReceived2timeOfFirstMarkPassingFirstMark > MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS) { newStartTime = new MillisecondsTimePoint(timeOfFirstMarkPassing.asMillis() - MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS); - if (logger.isLoggable(Level.FINEST)) { - logger.finest("Using start mark passings for start time of race "+this.getRace().getName()+": "+newStartTime); - } + final TimePoint finalNewStartTime = newStartTime; + logger.finest(()->"Using start mark passings for start time of race "+this.getRace().getName()+": "+finalNewStartTime); } else { newStartTime = startTimeReceived; - if (logger.isLoggable(Level.FINEST)) { - logger.finest("Using start mark received for race "+this.getRace().getName()+": "+newStartTime); - } + final TimePoint finalNewStartTime = newStartTime; + logger.finest(()->"Using start mark received for race "+this.getRace().getName()+": "+finalNewStartTime); } } } else { @@ -3455,29 +3451,46 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl if (startWaypoint == null) { result = null; } else { - Position competitorPosition = getTrack(competitor).getEstimatedPosition(timePoint, /* extrapolate */false); + Position competitorPosition = getTrack(competitor).getEstimatedPosition(timePoint, /* extrapolate */ false); if (competitorPosition == null) { result = null; } else { Iterable marks = startWaypoint.getControlPoint().getMarks(); Iterator marksIterator = marks.iterator(); Mark first = marksIterator.next(); - Position firstPosition = getOrCreateTrack(first).getEstimatedPosition(timePoint, /* extrapolate */false); + Position firstPosition = getOrCreateTrack(first).getEstimatedPosition(timePoint, /* extrapolate */ false); if (firstPosition == null) { result = null; } else { if (marksIterator.hasNext()) { // it's a line / gate Mark second = marksIterator.next(); - Position secondPosition = getOrCreateTrack(second).getEstimatedPosition(timePoint, /* extrapolate */ - false); - final Bearing bearingGreatCircle = firstPosition.getBearingGreatCircle(secondPosition); - if (bearingGreatCircle == null) { + Position secondPosition = getOrCreateTrack(second).getEstimatedPosition(timePoint, /* extrapolate */ false); + if (secondPosition == null) { result = null; } else { - Position competitorProjectedOntoStartLine = competitorPosition.projectToLineThrough( - firstPosition, bearingGreatCircle); - result = competitorPosition.getDistance(competitorProjectedOntoStartLine); + final Bearing lineBearingGreatCircleFromFirstToSecond = firstPosition.getBearingGreatCircle(secondPosition); + // if the competitor is outside of the line when projected orthogonally, compute the distance to + // the nearest of the line's marks (see also bug 1952): + final Bearing bearingFromFirstToCompetitor = firstPosition.getBearingGreatCircle(competitorPosition); + final Bearing angleBetweenFromFirstToCompetitorAndLine = lineBearingGreatCircleFromFirstToSecond.getDifferenceTo(bearingFromFirstToCompetitor); + if (angleBetweenFromFirstToCompetitorAndLine.getDegrees() < -90 || angleBetweenFromFirstToCompetitorAndLine.getDegrees() > 90) { + // competitor's orthogonal projection onto the line's extension is outside of the line's ends on the side + // of the first mark; use distance between competitor and first mark: + result = competitorPosition.getDistance(firstPosition); + } else { + final Bearing bearingFromSecondToCompetitor = secondPosition.getBearingGreatCircle(competitorPosition); + final Bearing angleBetweenFromSecondToCompetitorAndReversedLine = lineBearingGreatCircleFromFirstToSecond.reverse().getDifferenceTo(bearingFromSecondToCompetitor); + if (angleBetweenFromSecondToCompetitorAndReversedLine.getDegrees() < -90 || angleBetweenFromSecondToCompetitorAndReversedLine.getDegrees() > 90) { + // competitor's orthogonal projection onto the line's extension is outside of the line's ends on the side + // of the first mark; use distance between competitor and first mark: + result = competitorPosition.getDistance(secondPosition); + } else { + Position competitorProjectedOntoStartLine = competitorPosition.projectToLineThrough( + firstPosition, lineBearingGreatCircleFromFirstToSecond); + result = competitorPosition.getDistance(competitorProjectedOntoStartLine); + } + } } } else { result = competitorPosition.getDistance(firstPosition); @@ -3920,18 +3933,22 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } @Override - public Duration getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, + public TargetTimeInfo getEstimatedTimeToComplete(final TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException { if (polarDataService == null) { throw new NotEnoughDataHasBeenAddedException("Target time estimation failed. No polar service available."); } Duration durationOfAllLegs = Duration.NULL; - final MarkPositionAtTimePointCache markPositionCache = new MarkPositionAtTimePointCacheImpl(this, timepoint); + TimePoint current = timepoint; + final List legTargetTimes = new ArrayList<>(); for (TrackedLeg leg : trackedLegs.values()) { - Duration durationOfLeg = leg.getEstimatedTimeToComplete(polarDataService, timepoint, markPositionCache); - durationOfAllLegs = durationOfAllLegs.plus(durationOfLeg); + final MarkPositionAtTimePointCache markPositionCache = new MarkPositionAtTimePointCacheImpl(this, current); + LegTargetTimeInfo legTargetTime = leg.getEstimatedTimeToComplete(polarDataService, current, markPositionCache); + legTargetTimes.add(legTargetTime); + durationOfAllLegs = durationOfAllLegs.plus(legTargetTime.getExpectedDuration()); + current = current.plus(legTargetTime.getExpectedDuration()); // simulate the next leg with the wind as of the projected finishing time of the previous leg } - return durationOfAllLegs; + return new TargetTimeInfoImpl(legTargetTimes); } @Override diff --git a/java/com.sap.sailing.feature/feature.xml b/java/com.sap.sailing.feature/feature.xml index 65f3933509f..ef4aa3fbfad 100755 --- a/java/com.sap.sailing.feature/feature.xml +++ b/java/com.sap.sailing.feature/feature.xml @@ -269,13 +269,6 @@ Computes leaderboard information for sailing races and offers RESTful APIs to al version="0.0.0" unpack="false"/> - - + + diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java index c948deb2535..a97e253a59a 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java @@ -29,6 +29,7 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; @@ -69,7 +70,6 @@ import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.WindWithConfidence; import com.sap.sailing.domain.tracking.impl.WindTrackImpl; -import com.sap.sse.common.Duration; import com.sap.sse.common.IsManagedByCache; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -768,7 +768,7 @@ public class MockedTrackedRace implements DynamicTrackedRace { } @Override - public Duration getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, + public TargetTimeInfo getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException { return null; } diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java index b46db9f5858..af633c042ae 100755 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java @@ -47,7 +47,7 @@ public class SailingServiceImplMock extends SailingServiceImpl { public SailingServiceImplMock() { super(); - service = new RacingEventServiceImpl(true, new MockSmartphoneImeiServiceFinderFactory()); + service = new RacingEventServiceImpl(/* clearPersistentCompetitorStore */ true, new MockSmartphoneImeiServiceFinderFactory(), /* restoreTrackedRaces */ false); try { replicationService = new ReplicationServiceImpl("test exchange", "localhost", 0, new ReplicationInstancesManager(), new AbstractReplicablesProvider() { @@ -75,7 +75,7 @@ public class SailingServiceImplMock extends SailingServiceImpl { public RaceHandle addSwissTimingRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, StartList startList, RaceLogStore logStore, - RegattaLogStore regattaLogStore, long timeoutInMilliseconds, boolean useInternalMarkPassingAlgorithm) throws InterruptedException, + RegattaLogStore regattaLogStore, long timeoutInMilliseconds, boolean useInternalMarkPassingAlgorithm, boolean trackWind, boolean correctWindDirectionByMagneticDeclination) throws InterruptedException, UnknownHostException, IOException, ParseException, Exception { // TODO Auto-generated method stub return null; diff --git a/java/com.sap.sailing.gwt.ui/AdminConsole.css b/java/com.sap.sailing.gwt.ui/AdminConsole.css index 899f9763c16..15c8f269e5a 100644 --- a/java/com.sap.sailing.gwt.ui/AdminConsole.css +++ b/java/com.sap.sailing.gwt.ui/AdminConsole.css @@ -112,7 +112,6 @@ fieldset legend { .dialogMessageLabel { font-weight: bold; - width: 300px; } select { diff --git a/java/com.sap.sailing.gwt.ui/Leaderboard.css b/java/com.sap.sailing.gwt.ui/Leaderboard.css index 79ed640da2c..35a50bf6ce7 100755 --- a/java/com.sap.sailing.gwt.ui/Leaderboard.css +++ b/java/com.sap.sailing.gwt.ui/Leaderboard.css @@ -1,13 +1,4 @@ /** Add css rules here for your application. */ -/* - body #calendar:after - content: " "; - display: block; - height: 0; - clear: both; - visibility: hidden; -*/ - .clear { clear: both; font-size: 1px; diff --git a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF index b56587edf47..b18857a8cd7 100644 --- a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF @@ -52,7 +52,8 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sailing.polars.datamining.shared, com.sap.sailing.polars.datamining, com.sap.sse.datamining.annotations, - com.sap.sse.shared.android + com.sap.sse.shared.android, + com.sap.sailing.competitorimport Bundle-Activator: com.sap.sailing.gwt.ui.server.Activator Bundle-ActivationPolicy: lazy Import-Package: javax.servlet;version="3.1.0", diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch index 4c3271a52ac..4a15fa5eb4b 100644 --- a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch @@ -1,78 +1,78 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT.launch b/java/com.sap.sailing.gwt.ui/SailingGWT.launch index 0c4449938c3..05d195e0e53 100644 --- a/java/com.sap.sailing.gwt.ui/SailingGWT.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT.launch @@ -1,75 +1,75 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/CompetitorDescriptor_CustomFieldSerializer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/CompetitorDescriptor_CustomFieldSerializer.java new file mode 100755 index 00000000000..29b37f9a2e6 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/CompetitorDescriptor_CustomFieldSerializer.java @@ -0,0 +1,71 @@ +package com.google.gwt.user.client.rpc.core.com.sap.sailing.domain.common; + +import java.util.List; + +import com.google.gwt.user.client.rpc.CustomFieldSerializer; +import com.google.gwt.user.client.rpc.SerializationException; +import com.google.gwt.user.client.rpc.SerializationStreamReader; +import com.google.gwt.user.client.rpc.SerializationStreamWriter; +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sailing.domain.common.dto.PersonDTO; +import com.sap.sse.common.CountryCode; +import com.sap.sse.common.Duration; + +public class CompetitorDescriptor_CustomFieldSerializer extends CustomFieldSerializer { + + @Override + public void serializeInstance(SerializationStreamWriter streamWriter, CompetitorDescriptor instance) + throws SerializationException { + serialize(streamWriter, instance); + } + + public static void serialize(SerializationStreamWriter streamWriter, CompetitorDescriptor instance) + throws SerializationException { + streamWriter.writeObject(instance.getPersons()); + streamWriter.writeString(instance.getEventName()); + streamWriter.writeString(instance.getRegattaName()); + streamWriter.writeString(instance.getBoatClassName()); + streamWriter.writeString(instance.getRaceName()); + streamWriter.writeString(instance.getFleetName()); + streamWriter.writeString(instance.getSailNumber()); + streamWriter.writeString(instance.getName()); + streamWriter.writeString(instance.getTeamName()); + streamWriter.writeString(instance.getBoatName()); + streamWriter.writeObject(instance.getCountryCode()); + streamWriter.writeObject(instance.getTimeOnTimeFactor()); + streamWriter.writeObject(instance.getTimeOnDistanceAllowancePerNauticalMile()); + } + + @Override + public boolean hasCustomInstantiateInstance() { + return true; + } + + @Override + public CompetitorDescriptor instantiateInstance(SerializationStreamReader streamReader) + throws SerializationException { + return instantiate(streamReader); + } + + public static CompetitorDescriptor instantiate(SerializationStreamReader streamReader) + throws SerializationException { + @SuppressWarnings("unchecked") + final List persons = (List) streamReader.readObject(); + return new CompetitorDescriptor(streamReader.readString(), streamReader.readString(), + streamReader.readString(), streamReader.readString(), streamReader.readString(), + streamReader.readString(), streamReader.readString(), streamReader.readString(), + streamReader.readString(), (CountryCode) streamReader.readObject(), + persons, (Double) streamReader.readObject(), (Duration) streamReader.readObject()); + } + + @Override + public void deserializeInstance(SerializationStreamReader streamReader, CompetitorDescriptor instance) + throws SerializationException { + deserialize(streamReader, instance); + } + + public static void deserialize(SerializationStreamReader streamReader, CompetitorDescriptor instance) { + // Done by instantiateInstance + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/dto/PersonDTO_CustomFieldSerializer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/dto/PersonDTO_CustomFieldSerializer.java new file mode 100755 index 00000000000..b151b22a6a5 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sailing/domain/common/dto/PersonDTO_CustomFieldSerializer.java @@ -0,0 +1,54 @@ +package com.google.gwt.user.client.rpc.core.com.sap.sailing.domain.common.dto; + +import java.util.Date; + +import com.google.gwt.user.client.rpc.CustomFieldSerializer; +import com.google.gwt.user.client.rpc.SerializationException; +import com.google.gwt.user.client.rpc.SerializationStreamReader; +import com.google.gwt.user.client.rpc.SerializationStreamWriter; +import com.sap.sailing.domain.common.dto.PersonDTO; + +public class PersonDTO_CustomFieldSerializer extends CustomFieldSerializer { + + @Override + public void serializeInstance(SerializationStreamWriter streamWriter, PersonDTO instance) + throws SerializationException { + serialize(streamWriter, instance); + } + + public static void serialize(SerializationStreamWriter streamWriter, PersonDTO instance) + throws SerializationException { + streamWriter.writeString(instance.getName()); + streamWriter.writeObject(instance.getDateOfBirth()); + streamWriter.writeString(instance.getDescription()); + streamWriter.writeString(instance.getNationalityThreeLetterIOCAcronym()); + } + + @Override + public boolean hasCustomInstantiateInstance() { + return true; + } + + @Override + public PersonDTO instantiateInstance(SerializationStreamReader streamReader) + throws SerializationException { + return instantiate(streamReader); + } + + public static PersonDTO instantiate(SerializationStreamReader streamReader) + throws SerializationException { + return new PersonDTO(streamReader.readString(), (Date) streamReader.readObject(), + streamReader.readString(), streamReader.readString()); + } + + @Override + public void deserializeInstance(SerializationStreamReader streamReader, PersonDTO instance) + throws SerializationException { + deserialize(streamReader, instance); + } + + public static void deserialize(SerializationStreamReader streamReader, PersonDTO instance) { + // Done by instantiateInstance + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/common/impl/CountryCodeImpl_CustomFieldSerializer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/common/impl/CountryCodeImpl_CustomFieldSerializer.java new file mode 100755 index 00000000000..7c3d61579ee --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/common/impl/CountryCodeImpl_CustomFieldSerializer.java @@ -0,0 +1,57 @@ +package com.google.gwt.user.client.rpc.core.com.sap.sse.common.impl; + +import com.google.gwt.user.client.rpc.CustomFieldSerializer; +import com.google.gwt.user.client.rpc.SerializationException; +import com.google.gwt.user.client.rpc.SerializationStreamReader; +import com.google.gwt.user.client.rpc.SerializationStreamWriter; +import com.sap.sse.common.impl.CountryCodeImpl; + +public class CountryCodeImpl_CustomFieldSerializer extends CustomFieldSerializer { + + @Override + public void serializeInstance(SerializationStreamWriter streamWriter, CountryCodeImpl instance) + throws SerializationException { + serialize(streamWriter, instance); + } + + public static void serialize(SerializationStreamWriter streamWriter, CountryCodeImpl instance) + throws SerializationException { + streamWriter.writeString(instance.getName()); + streamWriter.writeString(instance.getTwoLetterISOCode()); + streamWriter.writeString(instance.getThreeLetterISOCode()); + streamWriter.writeString(instance.getIANAInternet()); + streamWriter.writeString(instance.getUNVehicle()); + streamWriter.writeString(instance.getThreeLetterIOCCode()); + streamWriter.writeString(instance.getUNISONumeric()); + streamWriter.writeString(instance.getITUCallPrefix()); + } + + @Override + public boolean hasCustomInstantiateInstance() { + return true; + } + + @Override + public CountryCodeImpl instantiateInstance(SerializationStreamReader streamReader) + throws SerializationException { + return instantiate(streamReader); + } + + public static CountryCodeImpl instantiate(SerializationStreamReader streamReader) + throws SerializationException { + return new CountryCodeImpl(streamReader.readString(), streamReader.readString(), + streamReader.readString(), streamReader.readString(), streamReader.readString(), streamReader.readString(), + streamReader.readString(), streamReader.readString()); + } + + @Override + public void deserializeInstance(SerializationStreamReader streamReader, CountryCodeImpl instance) + throws SerializationException { + deserialize(streamReader, instance); + } + + public static void deserialize(SerializationStreamReader streamReader, CountryCodeImpl instance) { + // Done by instantiateInstance + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/race/SimpleRaceMetadataDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/race/SimpleRaceMetadataDTO.java index a7f1ccbd619..1d2170b153d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/race/SimpleRaceMetadataDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/race/SimpleRaceMetadataDTO.java @@ -37,6 +37,12 @@ public class SimpleRaceMetadataDTO implements DTO, Comparabletrue if the {@link #getViewState() view state} is {@link RaceViewState#RUNNING}, - * false otherwise + * @return true if the {@link #getViewState() view state} is {@link RaceViewState#RUNNING} or + * {@link RaceViewState#FINISHING}, false otherwise */ public boolean isRunning() { - return getViewState() == RaceViewState.RUNNING; + return getViewState() == RaceViewState.RUNNING || getViewState() == RaceViewState.FINISHING; } + /** + * Convenience method to check if this {@link SimpleRaceMetadataDTO race} is scheduled. + * + * @return true if the {@link #getViewState() view state} is {@link RaceViewState#SCHEDULED}, + * false otherwise + */ + public boolean isScheduled() { + return getViewState() == RaceViewState.SCHEDULED; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetFavoritesAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetFavoritesAction.java index b4539d6d405..58029acce59 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetFavoritesAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetFavoritesAction.java @@ -34,7 +34,7 @@ public class GetFavoritesAction implements SailingAction { if (preferences != null) { for (BoatClassNotificationPreference pref : preferences.getBoatClasses()) { String name = pref.getBoatClass().getName(), displayName = pref.getBoatClass().getDisplayName(); - selected.add(new BoatClassDTO(name, displayName, pref.getBoatClass().getHullLength())); + selected.add(new BoatClassDTO(name, displayName, pref.getBoatClass().getHullLength(), pref.getBoatClass().getHullBeam())); notifyAboutUpcomingRaces |= pref.isNotifyAboutUpcomingRaces(); notifyAboutResults |= pref.isNotifyAboutResults(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/header/Header.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/header/Header.ui.xml index 81ceb49710d..756f962e43e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/header/Header.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/header/Header.ui.xml @@ -9,7 +9,7 @@

    - + {i18n.headerLogo}

    diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/header/Header.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/header/Header.ui.xml index 20f15cca5b9..680f867dce5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/header/Header.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/header/Header.ui.xml @@ -18,7 +18,7 @@

    diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java index be4ba5cb018..16347a6dbe0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java @@ -591,6 +591,11 @@ public class RaceContext { if (startTime != null && now.before(startTime)) { return RaceViewState.SCHEDULED; } + if (state != null && state.getStatus() == RaceLogRaceStatus.FINISHING) { + // someone pulled up the blue flag; it's pretty likely that we'll also see the blue flag down + // event for the transition into the FINISHED state, so we can report FINISHING for now: + return RaceViewState.FINISHING; + } if (finishTime != null && now.after(finishTime)) { return RaceViewState.FINISHED; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionBoatClassDataProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionBoatClassDataProvider.java index 6245a0eab63..6a4c4a99d48 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionBoatClassDataProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionBoatClassDataProvider.java @@ -40,7 +40,7 @@ public abstract class AbstractSuggestedMultiSelectionBoatClassDataProvider exten List boatClasses = Arrays.asList(BoatClassMasterdata.values()); List suggestionItems = new ArrayList<>(); for (BoatClassMasterdata bcm : filter.applyFilter(queryTokens, boatClasses)) { - suggestionItems.add(new BoatClassDTO(bcm.getDisplayName(), bcm.getHullLength())); + suggestionItems.add(new BoatClassDTO(bcm.getDisplayName(), bcm.getHullLength(), bcm.getHullBeam())); } callback.setSuggestionItems(suggestionItems); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/regattacompetition/AbstractRegattaCompetitionFleetRace.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/regattacompetition/AbstractRegattaCompetitionFleetRace.java index f88a27148e2..e4074b902e0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/regattacompetition/AbstractRegattaCompetitionFleetRace.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/regattacompetition/AbstractRegattaCompetitionFleetRace.java @@ -17,7 +17,6 @@ import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.common.client.DateUtil; import com.sap.sailing.gwt.home.communication.race.SimpleRaceMetadataDTO; import com.sap.sailing.gwt.home.communication.race.SimpleRaceMetadataDTO.RaceTrackingState; -import com.sap.sailing.gwt.home.communication.race.SimpleRaceMetadataDTO.RaceViewState; import com.sap.sailing.gwt.home.desktop.partials.raceviewerlaunchpad.RaceviewerLaunchPad; import com.sap.sailing.gwt.home.shared.partials.regattacompetition.RegattaCompetitionView.RegattaCompetitionRaceView; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -36,7 +35,7 @@ public abstract class AbstractRegattaCompetitionFleetRace extends Widget impleme this.race = race; this.presenter = presenter; this.mainElement = getMainUiElement(); - setupRaceState(race.getTrackingState(), race.getViewState()); + setupRaceState(race); getRaceNameUiElement().setInnerText(race.getRaceName()); setupRaceStart(race.getStart()); setElement(mainElement); @@ -82,17 +81,20 @@ public abstract class AbstractRegattaCompetitionFleetRace extends Widget impleme }); } - private void setupRaceState(RaceTrackingState trackingState, RaceViewState viewState) { - boolean isUntrackedRace = trackingState != RaceTrackingState.TRACKED_VALID_DATA; - if (viewState == RaceViewState.RUNNING) { + private void setupRaceState(SimpleRaceMetadataDTO race) { + boolean isUntrackedRace = race.getTrackingState() != RaceTrackingState.TRACKED_VALID_DATA; + if (race.isRunning()) { mainElement.addClassName(getRaceLiveStyleName()); getRaceStateUiElement().setInnerText(isUntrackedRace ? I18N.live() : I18N.actionWatch()); - } else if (viewState == RaceViewState.FINISHED) { + } else if (race.isFinished()) { getRaceStateUiElement().setInnerText(isUntrackedRace ? I18N.raceIsFinished() : I18N.actionAnalyze()); } else { mainElement.addClassName(getRacePlannedStyleName()); - if (viewState == RaceViewState.SCHEDULED) getRaceStateUiElement().setInnerText(I18N.raceIsPlanned()); - else getRaceStateUiElement().setInnerText(viewState.getLabel()); + if (race.isScheduled()) { + getRaceStateUiElement().setInnerText(I18N.raceIsPlanned()); + } else { + getRaceStateUiElement().setInnerText(race.getViewState().getLabel()); + } } setStyleName(mainElement, getRaceUntrackedStyleName(), isUntrackedRace); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java index 0bdbf0014d8..72d9eece5c3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java @@ -63,8 +63,10 @@ import com.sap.sailing.gwt.ui.shared.RaceGroupSeriesDTO; import com.sap.sailing.gwt.ui.shared.RaceInfoDTO; import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; import com.sap.sailing.gwt.ui.shared.WaypointDTO; +import com.sap.sse.common.Duration; import com.sap.sse.common.Util; import com.sap.sse.common.util.NaturalComparator; +import com.sap.sse.gwt.client.DateTimeUtil; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.celltable.BaseCelltable; @@ -359,10 +361,11 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component return; } final long clientTimeWhenRequestWasSent = System.currentTimeMillis(); + final Duration clientTimeZoneOffset = DateTimeUtil.getClientTimezoneOffsetFromUTC(); sailingService.getRaceStateEntriesForRaceGroup(eventId, Util.asList(settings.getVisibleCourseAreas()), Util.asList(settings.getVisibleRegattas()), settings.isShowOnlyCurrentlyRunningRaces(), settings .isShowOnlyRacesOfSameDay(), - new MarkedAsyncCallback>( + clientTimeZoneOffset, new MarkedAsyncCallback>( new AsyncCallback>() { @Override public void onFailure(Throwable cause) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorRegistrationsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorRegistrationsDialog.java index 9f4c898603e..30984537d4f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorRegistrationsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorRegistrationsDialog.java @@ -7,6 +7,7 @@ import com.google.gwt.core.client.Callback; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CaptionPanel; import com.google.gwt.user.client.ui.CheckBox; @@ -15,16 +16,21 @@ import com.google.gwt.user.client.ui.HasVerticalAlignment; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.CompetitorDescriptor; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.CompetitorDTOImpl; +import com.sap.sailing.gwt.ui.adminconsole.CompetitorImportProviderSelectionDialog.MatchImportedCompetitorsDialogFactory; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.common.Util; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; +import com.sap.sse.gwt.client.controls.busyindicator.BusyDisplay; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; import com.sap.sse.gwt.client.dialog.DataEntryDialog; -public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDialog> { +public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDialog> implements BusyDisplay { protected CompetitorTableWrapper> allCompetitorsTable; protected CompetitorTableWrapper> registeredCompetitorsTable; protected final boolean filterByLeaderBoardInitially = false; @@ -40,6 +46,8 @@ public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDia private String boatClass; protected String leaderboardName; + private final BusyIndicator busyIndicator; + private final ImportCompetitorCallback importCompetitorCallback; /** * @param boatClass @@ -47,7 +55,7 @@ public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDia * boatClass parameter is null, if you want to edit a competitor or there is no * boat class for the new competitor. */ - public AbstractCompetitorRegistrationsDialog(final SailingServiceAsync sailingService, + protected AbstractCompetitorRegistrationsDialog(final SailingServiceAsync sailingService, final StringMessages stringMessages, final ErrorReporter errorReporter, boolean editable, DialogCallback> callback, String leaderboardName, String boatClass, com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator> validator) { @@ -58,6 +66,8 @@ public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDia this.editable = editable; this.leaderboardName = leaderboardName; this.boatClass = boatClass; + this.busyIndicator = new SimpleBusyIndicator(); + this.importCompetitorCallback = new RaceOrRegattaImportCompetitorCallback(this, sailingService, errorReporter, stringMessages); } @Override @@ -82,6 +92,29 @@ public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDia helper.inviteCompetitors(competitors, leaderboardName); } }); + final Button competitorImportButton = new Button(stringMessages.importCompetitors()); + competitorImportButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + sailingService.getCompetitorProviderNames(new AsyncCallback>() { + @Override + public void onSuccess(Iterable providerNames) { + MatchImportedCompetitorsDialogFactory matchCompetitorsDialogFactory = getMatchCompetitorsDialogFactory( + sailingService, stringMessages, errorReporter); + CompetitorImportProviderSelectionDialog dialog = new CompetitorImportProviderSelectionDialog( + matchCompetitorsDialogFactory, AbstractCompetitorRegistrationsDialog.this, providerNames, sailingService, + stringMessages, errorReporter); + dialog.show(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter + .reportError(stringMessages.errorLoadingCompetitorImportProviders(caught.getMessage())); + } + }); + } + }); final HorizontalPanel competitorRegistrationPanel = new HorizontalPanel(); final CaptionPanel allCompetitorsPanel = new CaptionPanel(stringMessages.competitorPool()); final CaptionPanel registeredCompetitorsPanel = new CaptionPanel(stringMessages.registeredCompetitors()); @@ -116,6 +149,8 @@ public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDia competitorRegistrationPanel.add(allCompetitorsPanel); buttonPanel.add(addCompetitorButton); buttonPanel.add(inviteCompetitorsButton); + buttonPanel.add(competitorImportButton); + buttonPanel.add(busyIndicator); mainPanel.add(buttonPanel); showOnlyCompetitorsOfLogCheckBox = new CheckBox(stringMessages.showOnlyCompetitorsOfLog()); showOnlyCompetitorsOfLogCheckBox.setValue(false); @@ -135,6 +170,25 @@ public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDia refreshCompetitors(); return mainPanel; } + + private MatchImportedCompetitorsDialogFactory getMatchCompetitorsDialogFactory( + final SailingServiceAsync sailingService, final StringMessages stringMessages, + final ErrorReporter errorReporter) { + return new MatchImportedCompetitorsDialogFactory() { + @Override + public MatchImportedCompetitorsDialog createMatchImportedCompetitorsDialog( + final Iterable competitorDescriptors, + final Iterable competitors) { + return new MatchImportedCompetitorsDialog(competitorDescriptors, competitors, stringMessages, + sailingService, errorReporter, importCompetitorCallback); + } + }; + } + + @Override + public void setBusy(boolean isBusy) { + busyIndicator.setBusy(isBusy); + } public abstract void addAdditionalWidgets(FlowPanel mainPanel); @@ -142,6 +196,11 @@ public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDia from.getFilterField().removeAll(toMove); to.getFilterField().addAll(toMove); } + + protected void addImportedCompetitorsToRegisteredCompetitorsTableAndRemoveFromAllCompetitorsTable(Iterable competitorsImported) { + allCompetitorsTable.getFilterField().removeAll(competitorsImported); + registeredCompetitorsTable.getFilterField().addAll(competitorsImported); + } private void moveSelected(CompetitorTableWrapper> from, CompetitorTableWrapper> to) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java index 06eacf8ad37..ffbfc751e8c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractTrackedRacesListComposite.java @@ -545,9 +545,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel listBoxRegattas.clear(); listBoxRegattas.addItem(stringMessages.all(), ""); - for (String regatta : regattaNames) { - listBoxRegattas.addItem(regatta, regatta); - } + regattaNames.stream().sorted().forEach(regatta->listBoxRegattas.addItem(regatta, regatta)); allRaces = newAllRaces; filterablePanelRaces.updateAll(allRaces); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java index 226aaa74264..f45175e8c0d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java @@ -35,6 +35,7 @@ import com.sap.sailing.gwt.ui.shared.SecurityStylesheetResources; import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; import com.sap.sse.gwt.adminconsole.AdminConsolePanel; import com.sap.sse.gwt.adminconsole.DefaultRefreshableAdminConsolePanel; +import com.sap.sse.gwt.adminconsole.ReplicationPanel; import com.sap.sse.gwt.client.EntryPointHelper; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; @@ -83,7 +84,7 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint implements private Widget createAdminConsolePanel() { AdminConsolePanel panel = new AdminConsolePanel(getUserService(), SailingPermissionsForRoleProvider.INSTANCE, - sailingService, getStringMessages().releaseNotes(), "/release_notes_admin.html", /* error reporter */ this, SecurityStylesheetResources.INSTANCE.css(), false); + sailingService, getStringMessages().releaseNotes(), "/release_notes_admin.html", /* error reporter */ this, SecurityStylesheetResources.INSTANCE.css(), getStringMessages()); panel.addStyleName("adminConsolePanel"); BetterDateTimeBox.initialize(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorDescriptorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorDescriptorTableWrapper.java new file mode 100644 index 00000000000..fed2d6c135a --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorDescriptorTableWrapper.java @@ -0,0 +1,240 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import com.google.gwt.cell.client.FieldUpdater; +import com.google.gwt.cell.client.SafeHtmlCell; +import com.google.gwt.core.client.GWT; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.user.cellview.client.Column; +import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.user.client.ui.ImageResourceRenderer; +import com.google.gwt.user.client.ui.Label; +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.gwt.ui.client.FlagImageResolver; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.controls.ImagesBarCell; +import com.sap.sse.common.util.NaturalComparator; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; +import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; + +public class CompetitorDescriptorTableWrapper> + extends TableWrapper { + + private final LabeledAbstractFilterablePanel filterablePanelCompetitorDescriptor; + + private final CompetitorImportMatcher competitorImportMatcher; + + private static class CompetitorImportTableActionIcons extends ImagesBarCell { + static final String ACTION_UNLINK = "ACTION_UNLINK"; + private final StringMessages stringMessages; + private static AdminConsoleResources resources = GWT.create(AdminConsoleResources.class); + + public CompetitorImportTableActionIcons(StringMessages stringMessages) { + super(); + this.stringMessages = stringMessages; + } + + @Override + protected Iterable getImageSpecs() { + return Arrays.asList( + new ImageSpec(ACTION_UNLINK, stringMessages.actionEdit(), makeImagePrototype(resources.unlinkIcon())) + ); + } + + /** + * An unlink button is rendered if an only if the {@code data} value is not an empty string. + */ + @Override + protected void render(Context context, SafeHtml data, SafeHtmlBuilder sb) { + if (!data.asString().isEmpty()) { + super.render(context, data, sb); + } + } + } + + /** + * An instance implementing this interface may be passed to the constructor of the table wrapper. It will then receive + * a callback whenever the unlink action has been triggered. This way the table of importable competitors doesn't need + * to know about how the selection models are used outside of the table itself to establish the link between the + * importable and existing competitor.

    + * + * Furthermore, the {@link #getExigetExistingCompetitorToUseInsteadOf} method tells the existing competitor to which + * a competitor that can be imported is mapped. If {@code null} is returned, no such mapping exists for an importable + * competitor, and calling {@link #unlinkCompetitor} will not have any effect. + * + * @author Axel Uhl (d043530) + * + */ + public static interface CompetitorsToImportToExistingLinking { + void unlinkCompetitor(CompetitorDescriptor competitor); + CompetitorDTO getExistingCompetitorToUseInsteadOf(CompetitorDescriptor competitor); + } + + public CompetitorDescriptorTableWrapper(CompetitorImportMatcher competitorImportMatcherParam, + SailingServiceAsync sailingService, final StringMessages stringMessages, ErrorReporter errorReporter, + boolean multiSelection, boolean enablePager, final CompetitorsToImportToExistingLinking unlinkCallback) { + super(sailingService, stringMessages, errorReporter, multiSelection, enablePager, + new EntityIdentityComparator() { + @Override + public boolean representSameEntity(CompetitorDescriptor dto1, CompetitorDescriptor dto2) { + return dto1.equals(dto2); + } + + @Override + public int hashCode(CompetitorDescriptor dto) { + return dto.hashCode(); + } + }); + this.competitorImportMatcher = competitorImportMatcherParam; + filterablePanelCompetitorDescriptor = new LabeledAbstractFilterablePanel( + new Label(stringMessages.filterImportedCompetitorsByNameSailRaceFleet()), + new ArrayList(), table, dataProvider) { + @Override + public Iterable getSearchableStrings(CompetitorDescriptor competitorDescriptor) { + List string = new ArrayList(); + string.add(competitorDescriptor.getName()); + string.add(competitorDescriptor.getSailNumber()); + string.add(competitorDescriptor.getBoatClassName()); + string.add(competitorDescriptor.getRaceName()); + string.add(competitorDescriptor.getFleetName()); + return string; + } + }; + registerSelectionModelOnNewDataProvider(filterablePanelCompetitorDescriptor.getAllListDataProvider()); + + TextColumn competitorNameColumn = new TextColumn() { + @Override + public String getValue(CompetitorDescriptor competitor) { + return competitor.getName(); + } + }; + competitorNameColumn.setSortable(true); + + Column sailIdColumn = new Column( + new SafeHtmlCell()) { + @Override + public SafeHtml getValue(CompetitorDescriptor competitor) { + SafeHtmlBuilder sb = new SafeHtmlBuilder(); + ImageResourceRenderer renderer = new ImageResourceRenderer(); + final String twoLetterIsoCountryCode = competitor.getCountryCode().getTwoLetterISOCode(); + final ImageResource flagImageResource; + if (twoLetterIsoCountryCode == null || twoLetterIsoCountryCode.isEmpty()) { + flagImageResource = FlagImageResolver.getEmptyFlagImageResource(); + } else { + flagImageResource = FlagImageResolver.getFlagImageResource(twoLetterIsoCountryCode); + } + if (flagImageResource != null) { + sb.append(renderer.render(flagImageResource)); + sb.appendHtmlConstant(" "); + } + + sb.appendEscaped(competitor.getSailNumber()); + return sb.toSafeHtml(); + } + }; + sailIdColumn.setSortable(true); + + TextColumn boatClassNameColumn = new TextColumn() { + @Override + public String getValue(CompetitorDescriptor competitorDescriptor) { + return competitorDescriptor.getBoatClassName(); + } + }; + boatClassNameColumn.setSortable(true); + + TextColumn raceNameColumn = new TextColumn() { + @Override + public String getValue(CompetitorDescriptor competitorDescriptor) { + return competitorDescriptor.getRaceName(); + } + }; + raceNameColumn.setSortable(true); + + TextColumn fleetNameColumn = new TextColumn() { + @Override + public String getValue(CompetitorDescriptor competitorDescriptor) { + return competitorDescriptor.getFleetName(); + } + }; + fleetNameColumn.setSortable(true); + + TextColumn isHasMatchesColumn = new TextColumn() { + @Override + public String getValue(CompetitorDescriptor competitorDescriptor) { + return competitorImportMatcher.getMatchesCompetitors(competitorDescriptor).isEmpty() + ? stringMessages.no() : stringMessages.yes(); + } + }; + isHasMatchesColumn.setSortable(true); + // alters the getValue method such that it returns an empty string if the competitor to import is not linked to an existing one, non-null otherwise + ImagesBarColumn unlinkColumn = + new ImagesBarColumn(new CompetitorImportTableActionIcons(stringMessages)) { + @Override + public String getValue(CompetitorDescriptor competitor) { + return unlinkCallback.getExistingCompetitorToUseInsteadOf(competitor)==null?"":"linked"; + } + }; + unlinkColumn.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, final CompetitorDescriptor competitor, String value) { + if (CompetitorImportTableActionIcons.ACTION_UNLINK.equals(value)) { + if (unlinkCallback != null) { + unlinkCallback.unlinkCompetitor(competitor); + } + } + } + }); + unlinkColumn.setSortable(true); + mainPanel.insert(filterablePanelCompetitorDescriptor, 0); + table.addColumn(sailIdColumn, stringMessages.sailNumber()); + table.addColumn(competitorNameColumn, stringMessages.name()); + table.addColumn(boatClassNameColumn, stringMessages.boatClass()); + table.addColumn(raceNameColumn, stringMessages.race()); + table.addColumn(fleetNameColumn, stringMessages.fleet()); + table.addColumn(isHasMatchesColumn, stringMessages.hasMatches()); + table.addColumn(unlinkColumn, stringMessages.unlink()); + table.addColumnSortHandler(getCompetitorDescriptorTableColumnListSortHandler(competitorNameColumn, boatClassNameColumn, + sailIdColumn, raceNameColumn, fleetNameColumn, isHasMatchesColumn, unlinkColumn, unlinkCallback)); + } + + private ListHandler getCompetitorDescriptorTableColumnListSortHandler( + TextColumn competitorNameColumn, + TextColumn boatClassNameColumn, Column sailIdColumn, + TextColumn raceNameColumn, + TextColumn fleetNameColumn, TextColumn isHasMatchesColumn, + ImagesBarColumn unlinkColumn, CompetitorsToImportToExistingLinking unlinkCallback) { + ListHandler competitorColumnListHandler = getColumnSortHandler(); + final NaturalComparator caseInsensitiveNaturalComparator = new NaturalComparator(/* case sensitive */ false); + final Comparator nullFirstComparator = Comparator.nullsFirst(/* real comparator */ null); + competitorColumnListHandler.setComparator(competitorNameColumn, (cd1, cd2)->caseInsensitiveNaturalComparator.compare(cd1.getName(), cd2.getName())); + competitorColumnListHandler.setComparator(boatClassNameColumn, (cd1, cd2)->caseInsensitiveNaturalComparator.compare(cd1.getBoatClassName(), cd2.getBoatClassName())); + competitorColumnListHandler.setComparator(sailIdColumn, (cd1, cd2)->caseInsensitiveNaturalComparator.compare(cd1.getSailNumber(), cd2.getSailNumber())); + competitorColumnListHandler.setComparator(raceNameColumn, (cd1, cd2)->caseInsensitiveNaturalComparator.compare(cd1.getRaceName(), cd2.getRaceName())); + competitorColumnListHandler.setComparator(fleetNameColumn, (cd1, cd2)->caseInsensitiveNaturalComparator.compare(cd1.getFleetName(), cd2.getFleetName())); + competitorColumnListHandler.setComparator(isHasMatchesColumn, (cd1, cd2)->((Boolean) competitorImportMatcher.getMatchesCompetitors(cd1).isEmpty()).compareTo( + competitorImportMatcher.getMatchesCompetitors(cd2).isEmpty())); + competitorColumnListHandler.setComparator(unlinkColumn, (cd1, cd2)->(nullFirstComparator.compare( + unlinkCallback.getExistingCompetitorToUseInsteadOf(cd1), unlinkCallback.getExistingCompetitorToUseInsteadOf(cd2)))); + return competitorColumnListHandler; + } + + public Iterable getAllCompetitorDescriptors() { + return filterablePanelCompetitorDescriptor.getAll(); + } + + public void refreshCompetitorDescriptorList(Iterable competitors) { + filterablePanelCompetitorDescriptor.updateAll(competitors); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java index ca595671334..72c63417a2a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java @@ -188,7 +188,7 @@ public class CompetitorEditDialog extends DataEntryDialog { color = new InvalidColor(iae); } } - BoatClassDTO boatClass = new BoatClassDTO(boatClassName.getValue(), Distance.NULL); + BoatClassDTO boatClass = new BoatClassDTO(boatClassName.getValue(), Distance.NULL, Distance.NULL); BoatDTO boat = new BoatDTO(name.getText(), sailId.getText()); CompetitorDTO result = new CompetitorDTOImpl(name.getText(), color, email.getText(), /* twoLetterIsoCountryCode */ null, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorImportMatcher.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorImportMatcher.java new file mode 100644 index 00000000000..1de72511f3f --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorImportMatcher.java @@ -0,0 +1,52 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.HashSet; +import java.util.Set; + +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sse.common.Util; + +/** + * Used to find existing competitors which match with imported competitor. Currently use the simple exact match + * comparison. + * + * @author Alexander Tatarinovich + * + */ +public class CompetitorImportMatcher { + //FIXME: Not sure about the amount of data. May be need to implement some cache for that. + private final Iterable existingCompetitorDTOs; + + public CompetitorImportMatcher(Iterable existingCompetitors) { + this.existingCompetitorDTOs = existingCompetitors; + } + + public Set getMatchesCompetitors(CompetitorDescriptor competitorDescriptor) { + Set matchesCompetitor = new HashSet<>(); + if (competitorDescriptor == null) { + return matchesCompetitor; + } + for (CompetitorDTO existingCompetitor : existingCompetitorDTOs) { + if (isEqual(competitorDescriptor, existingCompetitor)) { + matchesCompetitor.add(existingCompetitor); + } + } + return matchesCompetitor; + } + + private boolean isEqual(CompetitorDescriptor competitorDescriptor, CompetitorDTO existingCompetitor) { + return Util.equalsWithNull(competitorDescriptor.getName(), existingCompetitor.getName(), /* ignoreCase */ true) + && Util.equalsWithNull(removeSpaces(competitorDescriptor.getSailNumber()), removeSpaces(existingCompetitor.getSailID()), /* ignoreCase */ true) + && compareCountryCode(competitorDescriptor, existingCompetitor); + } + + private String removeSpaces(String s) { + return s==null?null:s.replace(" ", "").replace("\t", ""); + } + + private boolean compareCountryCode(CompetitorDescriptor competitorDescriptor, CompetitorDTO existingCompetitor) { + return Util.equalsWithNull(competitorDescriptor.getCountryCode().getThreeLetterIOCCode(), + existingCompetitor.getThreeLetterIocCountryCode(), /* ignoreCase */ true); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorImportProviderSelectionDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorImportProviderSelectionDialog.java new file mode 100644 index 00000000000..0c724caf927 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorImportProviderSelectionDialog.java @@ -0,0 +1,278 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.FocusWidget; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.base.CompetitorStore; +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.gwt.ui.client.ParallelExecutionCallback; +import com.sap.sailing.gwt.ui.client.ParallelExecutionHolder; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.CompetitorProviderDTO; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.controls.busyindicator.BusyDisplay; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +/** + * Defines the dialog for displaying competitor provider names and list box of pair "event, regatta" names where we have + * competitors available for importing. Also defines which dialog would be created for matching imported competitors + * using the factory {@link MatchImportedCompetitorsDialogFactory} + * + * @author Alexander Tatarinovich + * + */ +public class CompetitorImportProviderSelectionDialog extends DataEntryDialog { + + private final ListBox competitorListBox; + private final ListBox competitorProviderListBox; + private final BusyIndicator busyIndicator; + + private final SailingServiceAsync sailingService; + private final StringMessages stringMessages; + private final ErrorReporter errorReporter; + + /** + * a unique and human-readable string key for a eventNameRegattaName pair + */ + private final LinkedHashMap> eventRegattaNamesByCompetitorListItem; + + public CompetitorImportProviderSelectionDialog(MatchImportedCompetitorsDialogFactory matchCompetitorsDialogFactory, + BusyDisplay busyDisplay, Iterable competitorProviderNames, SailingServiceAsync sailingService, + StringMessages stringMessages, ErrorReporter errorReporter) { + super(stringMessages.importCompetitors(), null, stringMessages.ok(), stringMessages.cancel(), null, + new Callback(matchCompetitorsDialogFactory, sailingService, busyDisplay, errorReporter, + stringMessages)); + this.sailingService = sailingService; + this.stringMessages = stringMessages; + this.errorReporter = errorReporter; + + competitorProviderListBox = createListBox(/* isMultipleSelect */ false); + competitorListBox = createListBox(/* isMultipleSelect */ false); + competitorListBox.setVisible(false); + busyIndicator = new SimpleBusyIndicator(); + eventRegattaNamesByCompetitorListItem = new LinkedHashMap<>(); + competitorProviderListBox.addChangeHandler(new ChangeHandler() { + @Override + public void onChange(ChangeEvent event) { + int selectedIndex = competitorProviderListBox.getSelectedIndex(); + if (selectedIndex > 0) { + String selectedProviderName = competitorProviderListBox.getItemText(selectedIndex); + competitorImportProviderChanged(selectedProviderName); + } else { + competitorImportProviderChanged(null); + } + } + }); + addProviderNamesToListBox(competitorProviderNames, stringMessages); + } + + private void addProviderNamesToListBox(Iterable competitorProviderNames, StringMessages stringMessages) { + List sortedProviderNames = getSortedProviderNames(competitorProviderNames); + competitorProviderListBox.addItem(stringMessages.selectCompetitorImportProvider()); + for (String providerName : sortedProviderNames) { + competitorProviderListBox.addItem(providerName); + } + } + + private List getSortedProviderNames(Iterable competitorProviderNames) { + List sortedProviderNames = new ArrayList<>(); + for (String providerName : competitorProviderNames) { + sortedProviderNames.add(providerName); + } + Collections.sort(sortedProviderNames); + return sortedProviderNames; + } + + private void competitorImportProviderChanged(String selectedProviderName) { + if (selectedProviderName != null) { + busyIndicator.setBusy(true); + competitorProviderListBox.setEnabled(false); + sailingService.getCompetitorProviderDTOByName(selectedProviderName, + new AsyncCallback() { + @Override + public void onSuccess(CompetitorProviderDTO result) { + updateCompetitorListBox(result); + competitorListBox.setVisible(true); + busyIndicator.setBusy(false); + competitorProviderListBox.setEnabled(true); + } + + @Override + public void onFailure(Throwable caught) { + updateCompetitorListBox(null); + competitorListBox.setVisible(false); + busyIndicator.setBusy(false); + competitorProviderListBox.setEnabled(true); + errorReporter.reportError( + stringMessages.errorLoadingCompetitorImportProviders(caught.getMessage())); + } + }); + } else { + competitorListBox.setVisible(false); + updateCompetitorListBox(null); + } + } + + private void updateCompetitorListBox(CompetitorProviderDTO competitorProvider) { + eventRegattaNamesByCompetitorListItem.clear(); + competitorListBox.clear(); + if (competitorProvider != null) { + competitorListBox.addItem(stringMessages.pleaseSelectAScoringResult()); + List> eventAndRegattaNames = getEventAndRegattaNamesWhichHasCompetitors(competitorProvider); + for (Pair pair : eventAndRegattaNames) { + String eventName = pair.getA(); + String ragattaName = pair.getB(); + String competitorImportSourceName = eventName + ", " + ragattaName; + eventRegattaNamesByCompetitorListItem.put(competitorImportSourceName, pair); + competitorListBox.addItem(competitorImportSourceName); + } + } + } + + private List> getEventAndRegattaNamesWhichHasCompetitors(CompetitorProviderDTO competitorProvider) { + List> eventAndRegattaNames = new ArrayList<>(); + for (Entry> entry : competitorProvider.getHasCompetitorsForRegattasInEvent().entrySet()) { + for (String ragattaName : entry.getValue()) { + eventAndRegattaNames.add(new Pair<>(entry.getKey(), ragattaName)); + } + } + return eventAndRegattaNames; + } + + private static class Callback implements DialogCallback { + private final BusyDisplay busyDisplay; + private final SailingServiceAsync sailingService; + private final ErrorReporter errorReporter; + private final StringMessages stringMessages; + private final MatchImportedCompetitorsDialogFactory matchCompetitorsDialogFactory; + + public Callback(MatchImportedCompetitorsDialogFactory matchCompetitorsDialogFactory, + SailingServiceAsync sailingService, BusyDisplay busyDisplay, ErrorReporter errorReporter, + StringMessages stringMessages) { + this.sailingService = sailingService; + this.busyDisplay = busyDisplay; + this.errorReporter = errorReporter; + this.stringMessages = stringMessages; + this.matchCompetitorsDialogFactory = matchCompetitorsDialogFactory; + } + + @Override + public void cancel() { + // don't do anything; dialog was canceled + } + + @Override + public void ok(final CompetitorImportSelectionDialogResult competitorImportDialogResult) { + if (competitorImportDialogResult != null) { + final String competitorProviderName = competitorImportDialogResult.getProviderName(); + final String eventName = competitorImportDialogResult.getEventName(); + final String regattaName = competitorImportDialogResult.getRegattaName(); + busyDisplay.setBusy(true); + @SuppressWarnings("unchecked") + final Iterable[] competitorDescriptors = (Iterable[]) new Iterable[1]; + @SuppressWarnings("unchecked") + final Iterable[] competitors = (Iterable[]) new Iterable[1]; + final ParallelExecutionCallback> getCompetitorDescriptorsCallback = + new ParallelExecutionCallback>() { + @Override + public void onSuccess(List myCompetitorDescriptors) { + competitorDescriptors[0] = myCompetitorDescriptors; + super.onSuccess(myCompetitorDescriptors); + } + }; + final ParallelExecutionCallback> getCompetitorsCallback = + new ParallelExecutionCallback>() { + @Override + public void onSuccess(Iterable myCompetitors) { + competitors[0] = myCompetitors; + super.onSuccess(myCompetitors); + } + }; + // the following parallel execution holder fires its handleSuccess method after both callbacks have succeeded + new ParallelExecutionHolder(getCompetitorDescriptorsCallback, getCompetitorsCallback) { + @Override + protected void handleSuccess() { + busyDisplay.setBusy(false); + matchCompetitorsDialogFactory.createMatchImportedCompetitorsDialog(competitorDescriptors[0], competitors[0]).show(); + } + + @Override + protected void handleFailure(Throwable t) { + busyDisplay.setBusy(false); + errorReporter.reportError( + stringMessages.errorLoadingCompetitorImportDescriptors(t.getMessage())); + } + }; + // trigger both calls, allowing for parallel execution, synchronizing with the parallel execution holder above: + sailingService.getCompetitorDescriptors(competitorProviderName, eventName, regattaName, new MarkedAsyncCallback<>(getCompetitorDescriptorsCallback)); + sailingService.getCompetitors(new MarkedAsyncCallback<>(getCompetitorsCallback)); + } + } + } + + /** + * Factory for creating specific dialog {@link MatchImportedCompetitorsDialog} where we will be match imported + * competitors. + * + * @param competitorDescriptors + * imported competitor descriptors {@link CompetitorDescriptor} + * @param competitors + * existing competitors from {@link CompetitorStore} + * @author Alexander Tatarinovich + * + */ + public interface MatchImportedCompetitorsDialogFactory { + MatchImportedCompetitorsDialog createMatchImportedCompetitorsDialog( + Iterable competitorDescriptors, Iterable competitors); + } + + @Override + protected Widget getAdditionalWidget() { + VerticalPanel vPanel = new VerticalPanel(); + vPanel.add(busyIndicator); + vPanel.add(competitorProviderListBox); + vPanel.add(competitorListBox); + return vPanel; + } + + @Override + protected FocusWidget getInitialFocusWidget() { + return competitorProviderListBox; + } + + @Override + protected CompetitorImportSelectionDialogResult getResult() { + CompetitorImportSelectionDialogResult competitorImportDialogResult = null; + int selectedProviderIndex = competitorProviderListBox.getSelectedIndex(); + if (selectedProviderIndex > 0) { + String selectedProviderName = competitorProviderListBox.getItemText(selectedProviderIndex); + int selectedScoreCorrectionIndex = competitorListBox.getSelectedIndex(); + if (selectedScoreCorrectionIndex > 0) { + Pair pair = eventRegattaNamesByCompetitorListItem + .get(competitorListBox.getValue(selectedScoreCorrectionIndex)); + String eventName = pair.getA(); + String regattaName = pair.getB(); + competitorImportDialogResult = new CompetitorImportSelectionDialogResult(selectedProviderName, + eventName, regattaName); + } + } + return competitorImportDialogResult; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorImportSelectionDialogResult.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorImportSelectionDialogResult.java new file mode 100644 index 00000000000..771e4dede89 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorImportSelectionDialogResult.java @@ -0,0 +1,25 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +public class CompetitorImportSelectionDialogResult { + private final String providerName; + private final String eventName; + private final String regattaName; + + public CompetitorImportSelectionDialogResult(String providerName, String eventName, String regattaName) { + this.providerName = providerName; + this.eventName = eventName; + this.regattaName = regattaName; + } + + public String getProviderName() { + return providerName; + } + + public String getEventName() { + return eventName; + } + + public String getRegattaName() { + return regattaName; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java index 6df107211ff..5d4d3c0724b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java @@ -4,18 +4,24 @@ import java.util.Set; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.sap.sailing.domain.common.CompetitorDescriptor; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.CompetitorDTOImpl; +import com.sap.sailing.gwt.ui.adminconsole.CompetitorImportProviderSelectionDialog.MatchImportedCompetitorsDialogFactory; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; +import com.sap.sse.gwt.client.controls.busyindicator.BusyDisplay; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; /** * Allows an administrator to view and edit the set of competitors currently maintained by the server. @@ -23,11 +29,13 @@ import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; * @author Axel Uhl (d043530) * */ -public class CompetitorPanel extends SimplePanel { +public class CompetitorPanel extends SimplePanel implements BusyDisplay { private final CompetitorTableWrapper> competitorTable; private final RefreshableMultiSelectionModel refreshableCompetitorSelectionModel; private final String leaderboardName; + private final BusyIndicator busyIndicator; + public CompetitorPanel(final SailingServiceAsync sailingService, final StringMessages stringMessages, final ErrorReporter errorReporter) { this(sailingService, null, stringMessages, errorReporter); @@ -37,8 +45,11 @@ public class CompetitorPanel extends SimplePanel { final StringMessages stringMessages, final ErrorReporter errorReporter) { super(); this.leaderboardName = leaderboardName; - this.competitorTable = new CompetitorTableWrapper<>(sailingService, stringMessages, errorReporter, /* multiSelection */ true, /* enablePager */ false); + this.competitorTable = new CompetitorTableWrapper<>(sailingService, stringMessages, errorReporter, /* multiSelection */ true, /* enablePager */ true); this.refreshableCompetitorSelectionModel = (RefreshableMultiSelectionModel) competitorTable.getSelectionModel(); + + busyIndicator = new SimpleBusyIndicator(false, 0.8f); + VerticalPanel mainPanel = new VerticalPanel(); this.setWidget(mainPanel); mainPanel.setWidth("100%"); @@ -85,6 +96,31 @@ public class CompetitorPanel extends SimplePanel { }); buttonPanel.add(selectAllButton); + Button competitorImportButton = new Button(stringMessages.importCompetitors()); + competitorImportButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + sailingService.getCompetitorProviderNames(new AsyncCallback>() { + @Override + public void onSuccess(Iterable providerNames) { + MatchImportedCompetitorsDialogFactory matchCompetitorsDialogFactory = getMatchCompetitorsDialogFactory( + sailingService, stringMessages, errorReporter); + CompetitorImportProviderSelectionDialog dialog = new CompetitorImportProviderSelectionDialog( + matchCompetitorsDialogFactory, CompetitorPanel.this, providerNames, sailingService, + stringMessages, errorReporter); + dialog.show(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter + .reportError(stringMessages.errorLoadingCompetitorImportProviders(caught.getMessage())); + } + }); + } + }); + buttonPanel.add(competitorImportButton); + //only if this competitor panel is connected to a leaderboard, we want to enable invitations if (leaderboardName != null) { final Button inviteCompetitorsButton = new Button(stringMessages.inviteSelectedCompetitors()); @@ -92,7 +128,6 @@ public class CompetitorPanel extends SimplePanel { @Override public void onClick(ClickEvent event) { Set competitors = refreshableCompetitorSelectionModel.getSelectedSet(); - CompetitorInvitationHelper helper = new CompetitorInvitationHelper(sailingService, stringMessages, errorReporter); helper.inviteCompetitors(competitors, leaderboardName); } @@ -101,6 +136,7 @@ public class CompetitorPanel extends SimplePanel { } competitorsPanel.add(buttonPanel); + mainPanel.add(busyIndicator); mainPanel.add(competitorTable); refreshableCompetitorSelectionModel.addSelectionChangeHandler(new Handler() { @@ -116,6 +152,22 @@ public class CompetitorPanel extends SimplePanel { } } + private MatchImportedCompetitorsDialogFactory getMatchCompetitorsDialogFactory( + final SailingServiceAsync sailingService, final StringMessages stringMessages, + final ErrorReporter errorReporter) { + return new MatchImportedCompetitorsDialogFactory() { + @Override + public MatchImportedCompetitorsDialog createMatchImportedCompetitorsDialog( + final Iterable competitorDescriptors, + final Iterable competitors) { + ImportCompetitorCallback importCompetitorCallback = new ImportCompetitorCallback(sailingService, + errorReporter, stringMessages); + return new MatchImportedCompetitorsDialog(competitorDescriptors, competitors, stringMessages, + sailingService, errorReporter, importCompetitorCallback); + } + }; + } + private void openAddCompetitorDialog() { competitorTable.openEditCompetitorDialog(new CompetitorDTOImpl(), /* boat class to be used from CompetitorDTO */ null); } @@ -123,4 +175,11 @@ public class CompetitorPanel extends SimplePanel { public void refreshCompetitorList() { competitorTable.refreshCompetitorList(leaderboardName); } + + @Override + public void setBusy(boolean isBusy) { + if (busyIndicator.isBusy() != isBusy) { + busyIndicator.setBusy(isBusy); + } + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java index 4dd55101d3a..4736c4d705f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java @@ -333,23 +333,26 @@ public class CompetitorTableWrapper() { @Override public void ok(final CompetitorDTO competitor) { - sailingService.addOrUpdateCompetitor(competitor, new AsyncCallback() { + final List competitors = new ArrayList<>(); + competitors.add(competitor); + sailingService.addOrUpdateCompetitor(competitors, new AsyncCallback>() { @Override public void onFailure(Throwable caught) { errorReporter.reportError("Error trying to update competitor: " + caught.getMessage()); } @Override - public void onSuccess(CompetitorDTO updatedCompetitor) { + public void onSuccess(List updatedCompetitor) { + assert updatedCompetitor.size() == 1; //only reload selected competitors reloading with refreshCompetitorList(leaderboardName) //would not work in case the list is not based on a leaderboard e.g. AbstractCompetitorRegistrationDialog int editedCompetitorIndex = getFilterField().indexOf(originalCompetitor); getFilterField().remove(originalCompetitor); if (editedCompetitorIndex >= 0){ - getFilterField().add(editedCompetitorIndex, updatedCompetitor); + getFilterField().add(editedCompetitorIndex, updatedCompetitor.iterator().next()); } else { //in case competitor was not present --> not edit, but create - getFilterField().add(updatedCompetitor); + getFilterField().add(updatedCompetitor.iterator().next()); } getDataProvider().refresh(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ImportCompetitorCallback.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ImportCompetitorCallback.java new file mode 100644 index 00000000000..57f2e5cd61d --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ImportCompetitorCallback.java @@ -0,0 +1,98 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; + +/** + * Callback is responsible for creation competitors for which no {@link CompetitorDTO} that represents an + * already existing competitor has been provided, and save those newly created competitors to + * the competitor store using {@link SailingServiceAsync#addCompetitors(Iterable, AsyncCallback)}. The combined + * set of competitors as represented by the {@link CompetitorDTO}s provided as values representing the existing + * competitors to use, plus those {@link CompetitorDTO}s returned from the service representing the newly + * created competitors (those actually "imported") are then passed to {@link #registerCompetitors(Set)} which + * in this class does nothing but may be overridden by subclasses. + * + * @author Alexander Tatarinovich + * + */ +public class ImportCompetitorCallback implements DialogCallback, String>> { + protected final SailingServiceAsync sailingService; + protected final ErrorReporter errorReporter; + protected final StringMessages stringMessages; + + public ImportCompetitorCallback(SailingServiceAsync sailingService, ErrorReporter errorReporter, + StringMessages stringMessages) { + this.sailingService = sailingService; + this.errorReporter = errorReporter; + this.stringMessages = stringMessages; + } + + @Override + public void cancel() { + } + + @Override + public void ok(final Pair, String> competitorsForRegisteringAndSearchTag) { + final Set existingCompetitorsSelected = new HashSet<>(); + for (final CompetitorDTO existingCompetitor : competitorsForRegisteringAndSearchTag.getA().values()) { + if (existingCompetitor != null) { + existingCompetitor.addToSearchTag(competitorsForRegisteringAndSearchTag.getB()); + existingCompetitorsSelected.add(existingCompetitor); + } + } + sailingService.addOrUpdateCompetitor(new ArrayList<>(existingCompetitorsSelected), new MarkedAsyncCallback<>( + new AsyncCallback>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(caught.getMessage()); + } + @Override + public void onSuccess(List result) { + // TODO trigger an update of any client-side competitor display + } + })); + registerCompetitorsAfterSaving(competitorsForRegisteringAndSearchTag.getA().entrySet().stream().filter((e->e.getValue() == null)).map(e->e.getKey()). + collect(Collectors.toList()), + competitorsForRegisteringAndSearchTag.getA().values().stream().filter(e->e!=null).collect(Collectors.toList()), competitorsForRegisteringAndSearchTag.getB()); + } + + private void registerCompetitorsAfterSaving(final List competitorsForSaving, + final Iterable competitorsForRegistration, String searchTag) { + sailingService.addCompetitors(competitorsForSaving, searchTag, new AsyncCallback>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(caught.getMessage()); + } + + @Override + public void onSuccess(List result) { + final Set competitorsToAddWithNewOnesReplacedBySavedOnesWithId = new HashSet<>(); + Util.addAll(competitorsForRegistration, competitorsToAddWithNewOnesReplacedBySavedOnesWithId); + // remove the locally constructed CompetitorDTOs that had null as their ID... + competitorsToAddWithNewOnesReplacedBySavedOnesWithId.removeAll(competitorsForSaving); + // ...and replace by those returned by the server after saving to the competitor store where they received an ID: + competitorsToAddWithNewOnesReplacedBySavedOnesWithId.addAll(result); + registerCompetitors(competitorsToAddWithNewOnesReplacedBySavedOnesWithId); + } + }); + } + + protected void registerCompetitors(Set competitorDTOs) { + // Don't register by default + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MatchImportedCompetitorsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MatchImportedCompetitorsDialog.java new file mode 100644 index 00000000000..0a35dd94399 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MatchImportedCompetitorsDialog.java @@ -0,0 +1,211 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.user.client.ui.CaptionPanel; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ScrollPanel; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.gwt.ui.adminconsole.CompetitorDescriptorTableWrapper.CompetitorsToImportToExistingLinking; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +/** + * Defines dialog where we can match and choose imported competitors. It mainly consists of two tables: one showing + * competitor records coming from an source for import, such as an external regatta management system, and another table + * that---upon selecting a single record in the table with the importable records--- displays potentially matching + * existing competitors from the server's competitor base. The user can then assemble a set of competitors by first + * optionally defining mappings from the importable competitors to existing competitors where instead of creating a new + * competitor by means of import the existing one shall be used. Then, the user can make a selection in the table with + * the importable competitors, including those for which a mapping to an existing competitor was defined. Pressing OK + * will produce a set of {@link CompetitorDTO}s where the ones coming from the set of already existing competitors have + * a non-{@code null} {@link CompetitorDTO#getIdAsString() ID} whereas the ones to import have all but an + * {@link CompetitorDTO#getIdAsString() ID}. + *

    + * + * The result is a {@link Map} whose keys are all competitors selected for import and whose values are {@code null} + * in case the competitor shall really be imported and hence be created anew, or a valid {@link CompetitorDTO} with + * a valid {@link CompetitorDTO#getIdAsString() ID}, representing an already existing competitor that shall be used + * instead of the key competitor descriptor.

    + * + * Use an {@link ImportCompetitorCallback} or subclass thereof as the {@link DialogCallback} passed to the constructor + * for conveniently saving imported competitors to the store using + * {@link SailingServiceAsync#addCompetitors(Iterable, com.google.gwt.user.client.rpc.AsyncCallback)}. Define a subclass + * thereof and override {@link ImportCompetitorCallback#registerCompetitors(Set)} to determine what should happen with + * the set of competitors the user has assembled through this dialog. + * + * @author Alexander Tatarinovich + * + */ +public class MatchImportedCompetitorsDialog extends DataEntryDialog, String>> { + + private CompetitorDescriptorTableWrapper> importedCompetitorsTable; + private CompetitorTableWrapper> existingCompetitorsTable; + + private TextBox searchTagField; + + private final StringMessages stringMessages; + private final SailingServiceAsync sailingService; + private final ErrorReporter errorReporter; + + private final Iterable CompetitorDescriptors; + private final CompetitorImportMatcher competitorImportMatcher; + + private final Map existingCompetitorsByImported = new HashMap<>(); + + public MatchImportedCompetitorsDialog(final Iterable CompetitorDescriptors, + final Iterable existingCompetitor, StringMessages stringMessages, + SailingServiceAsync sailingService, ErrorReporter errorReporter, + DialogCallback, String>> callback) { + super(stringMessages.importCompetitors(), stringMessages.chooseWhichCompetitorsShouldBeImported(), + stringMessages.ok(), stringMessages.cancel(), /* validator */ null, callback); + this.stringMessages = stringMessages; + this.sailingService = sailingService; + this.errorReporter = errorReporter; + this.CompetitorDescriptors = CompetitorDescriptors; + competitorImportMatcher = new CompetitorImportMatcher(existingCompetitor); + } + + protected void refreshInImportTable(CompetitorDescriptor competitor) { + final List importedCompetitors = importedCompetitorsTable.getDataProvider().getList(); + importedCompetitors.set(importedCompetitors.indexOf(competitor), competitor); // force refresh of line item in table, showing new linkage state + } + + @Override + protected Widget getAdditionalWidget() { + existingCompetitorsTable = new CompetitorTableWrapper<>(sailingService, stringMessages, + errorReporter, /* multiSelection */ + false, /* enablePager */true); + final CompetitorsToImportToExistingLinking linker = new CompetitorsToImportToExistingLinking() { + @Override + public void unlinkCompetitor(CompetitorDescriptor competitor) { + final CompetitorDTO existingCompetitor = existingCompetitorsByImported.remove(competitor); + if (existingCompetitor != null) { + existingCompetitorsTable.getSelectionModel().setSelected(existingCompetitor, false); + refreshInImportTable(competitor); + } + } + + @Override + public CompetitorDTO getExistingCompetitorToUseInsteadOf(CompetitorDescriptor competitor) { + return existingCompetitorsByImported.get(competitor); + } + }; + importedCompetitorsTable = new CompetitorDescriptorTableWrapper<>(competitorImportMatcher, sailingService, + stringMessages, errorReporter, /* multiSelection */ true, /* enablePager */false, /* unlinkCallback */ linker); + importedCompetitorsTable.refreshCompetitorDescriptorList(CompetitorDescriptors); + + final RefreshableMultiSelectionModel importedCompetitorSelectionModel = importedCompetitorsTable + .getSelectionModel(); + importedCompetitorSelectionModel + .addSelectionChangeHandler(getHandlerForImportedCompetitorsModel(importedCompetitorSelectionModel)); + + final RefreshableSingleSelectionModel existingCompetitorSelectionModel = existingCompetitorsTable + .getSelectionModel(); + existingCompetitorSelectionModel.addSelectionChangeHandler(getHandlerForExistingCompetitorsModel( + importedCompetitorSelectionModel, existingCompetitorSelectionModel)); + + final FlowPanel mainPanel = new FlowPanel(); + final HorizontalPanel competitorImportPanel = new HorizontalPanel(); + final CaptionPanel existingCompetitorsPanel = new CaptionPanel(stringMessages.existingCompetitors()); + final CaptionPanel importedCompetitorsPanel = new CaptionPanel(stringMessages.importedCompetitors()); + existingCompetitorsPanel.add(existingCompetitorsTable); + importedCompetitorsPanel.add(importedCompetitorsTable); + + ScrollPanel scrollCompetitorRegistrationPanel = new ScrollPanel(importedCompetitorsPanel); + scrollCompetitorRegistrationPanel.setSize("100%", "500px"); + ScrollPanel scrollExistingCompetitorRegistrationPanel = new ScrollPanel(existingCompetitorsPanel); + scrollCompetitorRegistrationPanel.setSize("100%", "500px"); + competitorImportPanel.add(scrollCompetitorRegistrationPanel); + competitorImportPanel.add(scrollExistingCompetitorRegistrationPanel); + searchTagField = createTextBox("", 40); + searchTagField.getElement().setPropertyString("placeholder", stringMessages.searchTagForImportedCompetitorPlaceholder()); + final Label searchTagLabel = new Label(stringMessages.searchTag()); + final HorizontalPanel searchTagPanel = new HorizontalPanel(); + searchTagPanel.add(searchTagLabel); + searchTagPanel.add(searchTagField); + mainPanel.add(searchTagPanel); + mainPanel.add(competitorImportPanel); + return mainPanel; + } + + private Handler getHandlerForExistingCompetitorsModel( + final RefreshableMultiSelectionModel importedCompetitorSelectionModel, + final RefreshableSingleSelectionModel existingCompetitorSelectionModel) { + return new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + Set competitorDescriptors = importedCompetitorSelectionModel.getSelectedSet(); + if (competitorDescriptors.size() == 1) { + final CompetitorDTO selectedCompetitor = existingCompetitorSelectionModel.getSelectedObject(); + final CompetitorDescriptor competitorFromImport = competitorDescriptors.iterator().next(); + final boolean changed; + if (selectedCompetitor == null) { + changed = existingCompetitorsByImported.remove(competitorFromImport) != null; + } else { + changed = !Util.equalsWithNull( + existingCompetitorsByImported.put(competitorFromImport, selectedCompetitor), selectedCompetitor); + } + if (changed) { + refreshInImportTable(competitorFromImport); + } + } + } + }; + } + + private Handler getHandlerForImportedCompetitorsModel( + final RefreshableMultiSelectionModel importedCompetitorSelectionModel) { + return new Handler() { + private boolean refreshing = false; + @Override + public void onSelectionChange(SelectionChangeEvent event) { + if (!refreshing) { + Set selectedCompetitorDescriptors = importedCompetitorSelectionModel.getSelectedSet(); + if (selectedCompetitorDescriptors.size() > 1) { + existingCompetitorsTable.getFilterField().removeAll(); + return; + } + CompetitorDescriptor selectedCompetitorDescriptor = selectedCompetitorDescriptors.isEmpty() ? null + : importedCompetitorSelectionModel.getSelectedSet().iterator().next(); + refreshing = true; + try { + existingCompetitorsTable.refreshCompetitorList( + competitorImportMatcher.getMatchesCompetitors(selectedCompetitorDescriptor)); + CompetitorDTO competitor = existingCompetitorsByImported.get(selectedCompetitorDescriptor); + if (competitor != null && !existingCompetitorsTable.getSelectionModel().isSelected(competitor)) { + existingCompetitorsTable.getSelectionModel().setSelected(competitor, true); + } + } finally { + refreshing = false; + } + } + } + }; + } + + @Override + protected Pair, String> getResult() { + final Map result = new HashMap<>(); + for (CompetitorDescriptor competitorDescriptor : importedCompetitorsTable.getSelectionModel().getSelectedSet()) { + result.put(competitorDescriptor, existingCompetitorsByImported.get(competitorDescriptor)); + } + return new Pair<>(result, searchTagField.getText()); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogCompetitorRegistrationDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogCompetitorRegistrationDialog.java index e9930f06b8a..0ff4b05162c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogCompetitorRegistrationDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogCompetitorRegistrationDialog.java @@ -130,21 +130,24 @@ public class RaceLogCompetitorRegistrationDialog extends AbstractCompetitorRegis }); } } - new ParallelExecutionHolder(callbacksForFleetNames.values().toArray(new ParallelExecutionCallback[0])) { - @Override - protected void handleSuccess() { - // if the data was gained completely then gray out specific rows - grayOutRows(); - } + if (!callbacksForFleetNames.isEmpty()) { + new ParallelExecutionHolder(callbacksForFleetNames.values().toArray(new ParallelExecutionCallback[0])) { + @Override + protected void handleSuccess() { + // if the data was gained completely then gray out specific rows + grayOutRows(); + } - @Override - protected void handleFailure(Throwable t) { - errorReporter.reportError( - "Could not load already registered competitors: " + t.getMessage()); + @Override + protected void handleFailure(Throwable t) { + errorReporter.reportError("Could not load already registered competitors: " + t.getMessage()); + } + }; + for (final Entry>> fleetNameAndCallback : callbacksForFleetNames + .entrySet()) { + sailingService.getCompetitorRegistrationsForRace(leaderboardName, raceColumnName, + fleetNameAndCallback.getKey(), fleetNameAndCallback.getValue()); } - }; - for (final Entry>> fleetNameAndCallback : callbacksForFleetNames.entrySet()) { - sailingService.getCompetitorRegistrationsForRace(leaderboardName, raceColumnName, fleetNameAndCallback.getKey(), fleetNameAndCallback.getValue()); } return result; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceOrRegattaImportCompetitorCallback.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceOrRegattaImportCompetitorCallback.java new file mode 100644 index 00000000000..f1341662365 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceOrRegattaImportCompetitorCallback.java @@ -0,0 +1,23 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.Set; + +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.client.ErrorReporter; + +public class RaceOrRegattaImportCompetitorCallback extends ImportCompetitorCallback { + private final AbstractCompetitorRegistrationsDialog registrationsDialog; + + public RaceOrRegattaImportCompetitorCallback(AbstractCompetitorRegistrationsDialog registrationsDialog, SailingServiceAsync sailingService, ErrorReporter errorReporter, + StringMessages stringMessages) { + super(sailingService, errorReporter, stringMessages); + this.registrationsDialog = registrationsDialog; + } + + @Override + protected void registerCompetitors(Set competitors) { + registrationsDialog.addImportedCompetitorsToRegisteredCompetitorsTableAndRemoveFromAllCompetitorsTable(competitors); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaWithSeriesAndFleetsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaWithSeriesAndFleetsDialog.java index e3192b400f0..34611a7d115 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaWithSeriesAndFleetsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaWithSeriesAndFleetsDialog.java @@ -48,7 +48,7 @@ public abstract class RegattaWithSeriesAndFleetsDialog extends AbstractRegattaWi protected RegattaDTO getResult() { RegattaDTO result = getRegattaDTO(); result.setName(nameEntryField.getText().trim()); // trim to particularly avoid trailing blanks - result.boatClass = new BoatClassDTO(boatClassEntryField.getText(), Distance.NULL); + result.boatClass = new BoatClassDTO(boatClassEntryField.getText(), Distance.NULL, Distance.NULL); return result; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java index e9aea45d22e..dcd634590ca 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java @@ -342,7 +342,7 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat */ private RegattaDTO createRegattaDefaults(RegattaDTO regatta) { RegattaDTO result = new RegattaDTO("Default", ScoringSchemeType.LOW_POINT); - result.boatClass = new BoatClassDTO(BoatClassDTO.DEFAULT_NAME, /* hull length */ new MeterDistance(5)); + result.boatClass = new BoatClassDTO(BoatClassDTO.DEFAULT_NAME, /* hull length */ new MeterDistance(5), /* hull beam */ new MeterDistance(1.8)); result.series = regatta.series; result.rankingMetricType = RankingMetrics.ONE_DESIGN; return result; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java index afcb3c08683..0a1e5326b1c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java @@ -196,7 +196,7 @@ public class SwissTimingReplayConnectorPanel extends AbstractEventManagementPane @Override public List getSearchableStrings(SwissTimingReplayRaceDTO t) { List strings = new ArrayList(); - strings.addAll(Arrays.asList(t.boat_class, t.flight_number, t.getName(), t.race_id, t.rsc)); + strings.addAll(Arrays.asList(t.boat_class, t.flight_number, t.getName(), t.rsc)); return strings; } }; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index 7b921543829..1e12553c092 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -455,12 +455,10 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { private String getBoatClassNamesAsString(TracTracRaceRecordDTO object) { StringBuilder boatClassNames = new StringBuilder(); - for (String boatClassName : object.boatClassNames) { boatClassNames.append(boatClassName); boatClassNames.append(", "); } - return boatClassNames.substring(0, boatClassNames.length() - 2); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/LogoAndTitlePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/LogoAndTitlePanel.java index da871c8459b..0117adc9294 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/LogoAndTitlePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/LogoAndTitlePanel.java @@ -38,7 +38,7 @@ public class LogoAndTitlePanel extends FlowPanel implements RequiresResize { lastIsSmallWidth = windowSizeDetector.isSmallWidth(); sapLogo = new Anchor(new SafeHtmlBuilder().appendHtmlConstant( "").toSafeHtml()); - sapLogo.setHref("http://www.sap.com"); + sapLogo.setHref("https://www.sap.com/sponsorships"); sapLogo.addStyleName("sapLogo"); this.add(sapLogo); renderUI(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/ParallelExecutionCallback.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/ParallelExecutionCallback.java index 041ac3696cf..d86a5db2383 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/ParallelExecutionCallback.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/ParallelExecutionCallback.java @@ -1,10 +1,27 @@ package com.sap.sailing.gwt.ui.client; import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; /** - * The class is used together with the {@link ParallelExecutionHolder} when two or more asynchronous service calls should run in parallel - * and only in case all of them has been executed successfully the next action makes sense. + * The class is used together with the {@link ParallelExecutionHolder} when two or more asynchronous service calls + * should run in parallel and only in case all of them have been executed successfully the next action makes sense. + * Instances of this class are first created, then passed to a + * {@link ParallelExecutionHolder#ParallelExecutionHolder(ParallelExecutionCallback...)} constructor and can then be + * passed as callbacks to GWT RPC method invocations like the regular {@link AsyncCallback} and may also be wrapped by + * {@link MarkedAsyncCallback} objects. The {@link #onSuccess(Object)} method tells the {@link ParallelExecutionHolder} + * to which this callback is submitted that the call succeeded. If that was the last outstanding callback then the + * {@link ParallelExecutionHolder} will subsequently invoke its {@link ParallelExecutionHolder#handleSuccess()} method. + *

    + * + * Should this callback receive an {@link #onFailure(Throwable)} call then the + * {@link ParallelExecutionHolder#handleFailure(Throwable)} method will be called. When this happens for at least one of + * the callbacks registered with the {@link ParallelExecutionHolder} then the + * {@link ParallelExecutionHolder#handleSuccess()} method will not be called. + *

    + * + * In case of subclassing and overriding this class's default implementations of {@link #onSuccess(Object)} the subclass + * implementation must invoke {@code super.onSuccess(t)} at some point during method execution. */ public class ParallelExecutionCallback implements AsyncCallback { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/ParallelExecutionHolder.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/ParallelExecutionHolder.java index 4305c12246e..174c500ed24 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/ParallelExecutionHolder.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/ParallelExecutionHolder.java @@ -1,5 +1,18 @@ package com.sap.sailing.gwt.ui.client; +import com.google.gwt.user.client.rpc.AsyncCallback; + +/** + * Triggers the {@link #handleSuccess()} method after all {@link ParallelExecutionCallback}s that were passed to the + * constructor have received their {@link AsyncCallback#onSuccess(Object)} call. All {@link ParallelExecutionCallback#onFailure(Throwable)} + * callback will lead to the {@link #handleFailure(Throwable)} method being called.

    + * + * After creating the {@link ParallelExecutionCallback} objects and passing them to this class's constructor, + * use them as regular callback objects in the GWT RPC service method invocations. + * + * @author Axel Uhl (d043530) + * + */ public abstract class ParallelExecutionHolder { /** The number of successfully completed services */ diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 4b17e2bcd66..e8a6bc89cb5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -11,6 +11,7 @@ import java.util.UUID; import com.google.gwt.user.client.rpc.RemoteService; import com.sap.sailing.domain.abstractlog.Revokable; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; +import com.sap.sailing.domain.common.CompetitorDescriptor; import com.sap.sailing.domain.common.DataImportProgress; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.LeaderboardType; @@ -34,6 +35,7 @@ import com.sap.sailing.domain.common.dto.BoatDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.FleetDTO; import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO; +import com.sap.sailing.domain.common.dto.PersonDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; import com.sap.sailing.domain.common.dto.RaceDTO; @@ -52,6 +54,7 @@ import com.sap.sailing.gwt.ui.client.shared.charts.MarkPositionService.MarkTrack import com.sap.sailing.gwt.ui.shared.BulkScoreCorrectionDTO; import com.sap.sailing.gwt.ui.shared.CompactBoatPositionsDTO; import com.sap.sailing.gwt.ui.shared.CompactRaceMapDataDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorProviderDTO; import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; import com.sap.sailing.gwt.ui.shared.ControlPointDTO; import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; @@ -77,7 +80,6 @@ import com.sap.sailing.gwt.ui.shared.RegattaLogDTO; import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO; import com.sap.sailing.gwt.ui.shared.RemoteSailingServerReferenceDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; import com.sap.sailing.gwt.ui.shared.ServerConfigurationDTO; import com.sap.sailing.gwt.ui.shared.SimulatorResultsDTO; @@ -94,6 +96,7 @@ import com.sap.sailing.gwt.ui.shared.TypedDeviceMappingDTO; import com.sap.sailing.gwt.ui.shared.VenueDTO; import com.sap.sailing.gwt.ui.shared.WindDTO; import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; +import com.sap.sse.common.CountryCode; import com.sap.sse.common.Duration; import com.sap.sse.common.NoCorrespondingServiceRegisteredException; import com.sap.sse.common.TimePoint; @@ -105,12 +108,13 @@ import com.sap.sse.gwt.client.ServerInfoDTO; import com.sap.sse.gwt.client.filestorage.FileStorageManagementGwtService; import com.sap.sse.gwt.client.media.ImageDTO; import com.sap.sse.gwt.client.media.VideoDTO; +import com.sap.sse.gwt.client.replication.RemoteReplicationService; /** * The client side stub for the RPC service. Usually, when a null date is passed to * the time-dependent service methods, an empty (non-null) result is returned. */ -public interface SailingService extends RemoteService, FileStorageManagementGwtService { +public interface SailingService extends RemoteService, FileStorageManagementGwtService, RemoteReplicationService { List getPreviousTracTracConfigurations() throws Exception; List getRegattas(); @@ -135,8 +139,6 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS void storeTracTracConfiguration(String name, String jsonURL, String liveDataURI, String storedDataURI, String courseDesignUpdateURI, String tracTracUsername, String tracTracPassword) throws Exception; - void stopTrackingEvent(RegattaIdentifier eventIdentifier) throws Exception; - void stopTrackingRaces(Iterable racesToStopTracking) throws Exception; void removeAndUntrackRaces(Iterable regattaNamesAndRaceNames); @@ -306,10 +308,6 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS void setWindSourcesToExclude(RegattaAndRaceIdentifier raceIdentifier, Iterable windSourcesToExclude); - ReplicationStateDTO getReplicaInfo(); - - void startReplicatingFromMaster(String messagingHost, String masterHost, String exchangeName, int servletPort, int messagingPort) throws Exception; - void updateRaceDelayToLive(RegattaAndRaceIdentifier regattaAndRaceIdentifier, long delayToLiveInMs); void updateRacesDelayToLive(List regattaAndRaceIdentifiers, long delayToLiveInMs); @@ -342,6 +340,13 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS void updateLeaderboardScoreCorrectionsAndMaxPointsReasons(BulkScoreCorrectionDTO updates) throws NoWindException; + Iterable getCompetitorProviderNames(); + + CompetitorProviderDTO getCompetitorProviderDTOByName(String providerName) throws Exception; + + List getCompetitorDescriptors(String competitorProviderName, String eventName, + String regattaName) throws Exception; + WindInfoForRaceDTO getWindSourcesInfo(RegattaAndRaceIdentifier raceIdentifier); ServerInfoDTO getServerInfo(); @@ -405,18 +410,12 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS List getRegattaStructureOfEvent(UUID eventId); List getRaceStateEntriesForRaceGroup(UUID eventId, List visibleCourseAreas, - List visibleRegattas, boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay) - throws Exception; + List visibleRegattas, boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, + Duration clientTimeZoneOffset) throws Exception; List getRaceStateEntriesForLeaderboard(String leaderboardName, - boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, List visibleRegattas) - throws Exception; - - void stopReplicatingFromMaster(); - - void stopAllReplicas(); - - void stopSingleReplicaInstance(String identifier); + boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, Duration clientTimeZoneOffset, + List visibleRegattas) throws Exception; void reloadRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet); @@ -434,8 +433,10 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS Iterable getCompetitorsOfLeaderboard(String leaderboardName); - CompetitorDTO addOrUpdateCompetitor(CompetitorDTO competitor) throws Exception; + List addOrUpdateCompetitor(List competitors) throws Exception; + List addCompetitors(List competitorsForSaving, String searchTag) throws Exception; + void allowCompetitorResetToDefaults(Iterable competitors); List getDeviceConfigurationMatchers(); @@ -666,4 +667,6 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS void editMarkFix(String leaderboardName, String raceColumnName, String fleetName, String markIdAsString, GPSFixDTO oldFix, Position newPosition) throws NotRevokableException; Map, Pair> getTrackingTimes(Collection> raceColumnsAndFleets); + + Pair makePersonDTOSerializableAndDeserializableDummy(PersonDTO dummy, CountryCode ccDummy); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index 28533bf2032..48f3df41ef8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -9,6 +9,7 @@ import java.util.Set; import java.util.UUID; import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sailing.domain.common.CompetitorDescriptor; import com.sap.sailing.domain.common.DataImportProgress; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.LeaderboardType; @@ -29,6 +30,7 @@ import com.sap.sailing.domain.common.dto.BoatDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.FleetDTO; import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO; +import com.sap.sailing.domain.common.dto.PersonDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; import com.sap.sailing.domain.common.dto.RaceDTO; @@ -41,6 +43,7 @@ import com.sap.sailing.gwt.ui.client.shared.charts.MarkPositionService.MarkTrack import com.sap.sailing.gwt.ui.shared.BulkScoreCorrectionDTO; import com.sap.sailing.gwt.ui.shared.CompactBoatPositionsDTO; import com.sap.sailing.gwt.ui.shared.CompactRaceMapDataDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorProviderDTO; import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; import com.sap.sailing.gwt.ui.shared.ControlPointDTO; import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; @@ -66,7 +69,6 @@ import com.sap.sailing.gwt.ui.shared.RegattaLogDTO; import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO; import com.sap.sailing.gwt.ui.shared.RemoteSailingServerReferenceDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; import com.sap.sailing.gwt.ui.shared.SailingServiceConstants; import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; import com.sap.sailing.gwt.ui.shared.ServerConfigurationDTO; @@ -84,6 +86,7 @@ import com.sap.sailing.gwt.ui.shared.TypedDeviceMappingDTO; import com.sap.sailing.gwt.ui.shared.VenueDTO; import com.sap.sailing.gwt.ui.shared.WindDTO; import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; +import com.sap.sse.common.CountryCode; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -93,11 +96,12 @@ import com.sap.sse.gwt.client.ServerInfoRetriever; import com.sap.sse.gwt.client.filestorage.FileStorageManagementGwtServiceAsync; import com.sap.sse.gwt.client.media.ImageDTO; import com.sap.sse.gwt.client.media.VideoDTO; +import com.sap.sse.gwt.client.replication.RemoteReplicationServiceAsync; /** * The async counterpart of {@link SailingService} */ -public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageManagementGwtServiceAsync { +public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageManagementGwtServiceAsync, RemoteReplicationServiceAsync { void getRegattas(AsyncCallback> callback); @@ -113,19 +117,22 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan /** * @param regattaToAddTo - * if null, an existing regatta by the name of the TracTrac event with the boat class name - * appended in parentheses will be looked up; if not found, a default regatta with that name will be - * created, with a single default series and a single default fleet. If a valid {@link RegattaIdentifier} - * is specified, a regatta lookup is performed with that identifier; if the regatta is found, it is used - * to add the races to. Otherwise, a default regatta as described above will be created and used. + * if null, the regatta into which the race has previously been loaded will be looked up; if + * found, the race will be loaded into that regatta; otherwise, an existing regatta by the name of the + * TracTrac event with the boat class name appended in parentheses will be looked up; if not found, a + * default regatta with that name will be created, with a single default series and a single default + * fleet. If a valid {@link RegattaIdentifier} is specified, a regatta lookup is performed with that + * identifier; if the regatta is found, it is used to add the races to. Otherwise, a default regatta as + * described above will be created and used. * @param liveURI * may be null or the empty string in which case the server will use the * {@link TracTracRaceRecordDTO#liveURI} from the rr race record. * @param offsetToStartTimeOfSimulatedRace - * if not null, the connector will adjust the time stamps of all events received such that the - * first mark passing for the first waypoint will be set to "now." It will delay the forwarding of all - * events received such that they seem to be sent in "real-time" plus the offsetToStartTimeOfSimulatedRace. So, more or less the time points - * attached to the events sent to the receivers will again approximate the wall time. + * if not null, the connector will adjust the time stamps of all events received such that + * the first mark passing for the first waypoint will be set to "now." It will delay the forwarding of + * all events received such that they seem to be sent in "real-time" plus the + * offsetToStartTimeOfSimulatedRace. So, more or less the time points attached to the events + * sent to the receivers will again approximate the wall time. * @param useInternalMarkPassingAlgorithm * whether or not to ignore the TracTrac-provided mark passings; if true, a separate mark * passing calculator is used, and the TracTrac-provided ones are ignored. @@ -151,8 +158,6 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void storeTracTracConfiguration(String name, String jsonURL, String liveDataURI, String storedDataURI, String courseDesignUpdateURI, String tracTracUsername, String tracTracPassword, AsyncCallback callback); - void stopTrackingEvent(RegattaIdentifier eventIdentifier, AsyncCallback callback); - void stopTrackingRaces(Iterable racesToStopTracking, AsyncCallback asyncCallback); /** @@ -384,11 +389,6 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan Map toPerCompetitorIdAsString, boolean extrapolate, AsyncCallback callback); - void getReplicaInfo(AsyncCallback callback); - - void startReplicatingFromMaster(String messagingHost, String masterName, String exchangeName, int servletPort, - int messagingPort, AsyncCallback callback); - void getEvents(AsyncCallback> callback); void getPublicEventsOfAllSailingServers(AsyncCallback> callback); @@ -463,6 +463,12 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void getScoreCorrections(String scoreCorrectionProviderName, String eventName, String boatClassName, Date timePointWhenResultPublished, AsyncCallback asyncCallback); + void getCompetitorProviderNames(AsyncCallback> callback); + + void getCompetitorProviderDTOByName(String providerName, AsyncCallback callback); + + void getCompetitorDescriptors(String competitorProviderName, String eventName, String regattaName, AsyncCallback> callback); + void getWindSourcesInfo(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); void getRaceCourse(RegattaAndRaceIdentifier raceIdentifier, Date date, AsyncCallback callback); @@ -547,24 +553,18 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void checkLeaderboardName(String leaderboardName, AsyncCallback> callback); - void stopReplicatingFromMaster(AsyncCallback asyncCallback); - void getRegattaStructureForEvent(UUID eventId, AsyncCallback> asyncCallback); void getRegattaStructureOfEvent(UUID eventId, AsyncCallback> callback); void getRaceStateEntriesForRaceGroup(UUID eventId, List visibleCourseAreas, List visibleRegattas, boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, - AsyncCallback> markedAsyncCallback); + Duration clientTimeZoneOffset, AsyncCallback> markedAsyncCallback); void getRaceStateEntriesForLeaderboard(String leaderboardName, boolean showOnlyCurrentlyRunningRaces, - boolean showOnlyRacesOfSameDay, List visibleRegattas, + boolean showOnlyRacesOfSameDay, Duration clientTimeZoneOffset, List visibleRegattas, AsyncCallback> callback); - void stopAllReplicas(AsyncCallback asyncCallback); - - void stopSingleReplicaInstance(String identifier, AsyncCallback asyncCallback); - void reloadRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet, AsyncCallback asyncCallback); @@ -586,7 +586,10 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void getCompetitorsOfLeaderboard(String leaderboardName, AsyncCallback> asyncCallback); - void addOrUpdateCompetitor(CompetitorDTO competitor, AsyncCallback asyncCallback); + void addOrUpdateCompetitor(List competitors, AsyncCallback> asyncCallback); + + void addCompetitors(List competitorsForSaving, String searchTag, + AsyncCallback> asyncCallback); void allowCompetitorResetToDefaults(Iterable competitors, AsyncCallback asyncCallback); @@ -812,4 +815,7 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void getTrackingTimes(Collection> raceColumnsAndFleets, AsyncCallback, Pair>> asyncCallback); + + void makePersonDTOSerializableAndDeserializableDummy(PersonDTO dummy, CountryCode ccDummy, + AsyncCallback> callback); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index dc68c455752..8f198e98cad 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -334,7 +334,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String messagingPortNumber(); String servletPortNumber(); String registeredAt(String string); - String replicatingFromMaster(String hostname, int jmsPort, int servletPort, String messagingHostname, String exchangeName); + String replicatingFromMaster(String hostname, int jmsPort, int servletPort, String messagingHostname, String exchangeName, String replicableIdsAsString); String showWindSpeedSeries(); String showWindDirectionSeries(); String fleet(); @@ -1576,6 +1576,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String mapControls(); String manage2SailEvent(); String noEventsYet(); + String selectCompetitorImportProvider(); String selectResultImportProvider(); String buildVersion(); String serverInformation(); @@ -1749,4 +1750,16 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String externalLinks(); String buoyZoneRadiusInHullLengths(); String incorrectValueForRegattaBuoyZoneRadiusInHullLengths(); + String importCompetitors(); + String chooseWhichCompetitorsShouldBeImported(); + String existingCompetitors(); + String importedCompetitors(); + String hasMatches(); + String filterImportedCompetitorsByNameSailRaceFleet(); + String errorLoadingCompetitorImportProviders(String message); + String errorLoadingCompetitorImportDescriptors(String message); + String couldNotSaveCompetitorRegistrations(String message); + String searchTagForImportedCompetitorPlaceholder(); + String version(String version); + String replicables(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index dd657678647..f84faa7e7b0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -346,7 +346,7 @@ courseMiddleLine=Course middle line messagingPortNumber=Messaging port number (0 for default) servletPortNumber=Servlet port number registeredAt=Registered at {0} -replicatingFromMaster=Replicating from master {0}:{2}, messaging port {3}:{1}, exchange {4} +replicatingFromMaster=Replicating from master {0}:{2}, messaging port {3}:{1}, exchange {4}, replicating {5} showWindSpeedSeries=Show wind speed data showWindDirectionSeries=Show wind directions data fleet=Fleet @@ -651,8 +651,8 @@ distanceToLineFiveSecondsBeforeStart=Distance to line 5s before start speedOverGroundAtRaceStart=SOG at Start speedOverGroundFiveSecondsBeforeStart=SOG 5s before start speedOverGroundWhenPassingStart=SOG when Starting -distanceToLineAtRaceStartTooltip=Distance to start line at race start time -distanceToLineFiveSecondsBeforeStartTooltip=Distance to start line five seconds before the start +distanceToLineAtRaceStartTooltip=Distance to start line at race start time; distance to nearest line end in case the competitor projects beyond the line''s ends +distanceToLineFiveSecondsBeforeStartTooltip=Distance to start line five seconds before the start; distance to nearest line end in case the competitor projects beyond the line''s ends speedOverGroundAtRaceStartTooltip=Speed over ground at race start time speedOverGroundFiveSecondsBeforeStartTooltip=Speed over ground five seconds before the start speedOverGroundWhenPassingStartTooltip=Speed over ground when the competitor passed the start @@ -1522,7 +1522,7 @@ playstoreCampaign=utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02 playstoreInsightApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app playstoreRacecommitteeApp=https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app playStoreBuoyPingerApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app -playstoreBadge=https://play.google.com/intl/en/badges/images/generic/en-play-badge.png +playstoreBadge=https://play.google.com/intl/en/badges/images/badge_new.png appstoreSapSailInsight=http://appstore.com/sapsailinsight appstoreBadgeSuffix=_en distanceFromAveragePosition=Distance from average position @@ -1537,6 +1537,7 @@ dragToChangePosition=Drag to change position\nRight-click to get menu to delete moveFix=Move fix manage2SailEvent=Manage2Sail Event noEventsYet=There are no events yet. +selectCompetitorImportProvider=Please select a competitor import provider... selectResultImportProvider=Please select a result import provider... buildVersion=Build version serverInformation=Server Information @@ -1659,7 +1660,7 @@ seriesHeaderUpcoming=Upcoming eventRegattaLeaderboardLastScoreUpdate=Last Score Update eventRegattaRaceNotTracked=Not Tracked footerJobs=Jobs -footerCopyright=\u00A9 2011-2016 SAP Sailing Analytics +footerCopyright=\u00A9 2011-2017 SAP Sailing Analytics footerLanguage=SAP Sailing in footerLegal=Legal Disclosure footerPrivacy=Privacy @@ -1721,3 +1722,15 @@ filterDeviceMappings=Filter device mappings raceStartAnalysis=Race Start Analysis buoyZoneRadiusInHullLengths=Buoy zone radius in hull lengths (e.g., 2 or 3) incorrectValueForRegattaBuoyZoneRadiusInHullLengths=Incorrect value for the buoy zone radius in hull lengths +importCompetitors=Import Competitors +chooseWhichCompetitorsShouldBeImported=Select competitors to be imported on the left. Selecting a single competitor shows any existing competitors that may represent the same entity on the right. To use an existing one, select it. When done mapping, select all competitors you want to import in the table on the left and press OK. Note that the tables can be sorted by clicking or tapping on the column headers. +existingCompetitors=Existing Competitors +importedCompetitors=Competitors to Import +hasMatches=Has matches +filterImportedCompetitorsByNameSailRaceFleet=Filter competitors to import by name, sail number, race or fleet +errorLoadingCompetitorImportProviders=Error loading competitor import providers: {0} +errorLoadingCompetitorImportDescriptors=Error loading competitor import descriptors: {0} +couldNotSaveCompetitorRegistrations=Could not save competitor registrations: {0} +searchTagForImportedCompetitorPlaceholder=Use, e.g., the race+fleet name +version=Version: {0} +replicables=Replicating objects \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index e34a901a375..fcc8802eb16 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -1,3 +1,4 @@ + manage2Sail=Manage2Sail raceCommitteeApp=Race Manager App raceViewer=Race viewer @@ -348,7 +349,7 @@ courseMiddleLine=Kursmittellinie messagingPortNumber=Messaging-Portnummer (0 für Default) servletPortNumber=Servlet Portnummer registeredAt=Registriert {0} -replicatingFromMaster=Repliziert von master {0}:{2}, Messaging Port {3}:{1}, Exchange {4} +replicatingFromMaster=Repliziert von master {0}:{2}, Messaging Port {3}:{1}, Exchange {4}, replizierte Objekte {5} showWindSpeedSeries=Daten für Windgeschwindigkeit zeigen showWindDirectionSeries=Daten für Windrichtung zeigen fleet=Flotte @@ -642,9 +643,9 @@ operatorEndsWith=Endet mit nationality=Nationalität numberMustBePositive=Die Zahl muß größer als Null sein. distanceToLineAtRaceStart=Abstand zur Linie bei Start -distanceToLineAtRaceStartTooltip=Abstand zur Linie zur Startzeit des Rennens +distanceToLineAtRaceStartTooltip=Abstand zur Linie zur Startzeit des Rennens; Abstand zum näheren Ende der Linie falls Projektion des Teilnehmers außerhalb der Linie liegt distanceToLineFiveSecondsBeforeStart=Abstand zur Linie 5s vor Start -distanceToLineFiveSecondsBeforeStartTooltip=Abstand zur Linie fünf Sekunden vor dem Start +distanceToLineFiveSecondsBeforeStartTooltip=Abstand zur Linie fünf Sekunden vor dem Start; Abstand zum näheren Ende der Linie falls Projektion des Teilnehmers außerhalb der Linie liegt speedOverGroundAtRaceStart=FüG bei Rennstart speedOverGroundAtRaceStartTooltip=Fahrt über Grund zur Startzeit des Rennens speedOverGroundFiveSecondsBeforeStart=FüG 5s vor Start @@ -1501,7 +1502,7 @@ raceManagerDeviceName=Gerätename serverURLInvalid=Bitte gültige Server-URL eingeben serverURLEmpty=Server URL darf nicht leer sein marks=Bahnmarken -playstoreBadge=https://play.google.com/intl/de-de/badges/images/generic/de-play-badge.png +playstoreBadge=https://play.google.com/intl/de-de/badges/images/badge_new.png appstoreBadgeSuffix=_de addNewFix=Neuen Fix hinzufügen pleaseSelectOtherTimepoint=Bitte wählen Sie einen anderen Zeitpunkt. Am gewählten Zeitpunkt ist bereits ein Fix. @@ -1523,6 +1524,7 @@ moveFix=Position durch Ziehen ändern manage2SailEvent=Manage2Sail Veranstaltung noEventsYet=Es sind keine Veranstaltungen vorhanden. selectResultImportProvider=Bitte wählen Sie einen Ergebnisdienst aus... +selectCompetitorImportProvider=Bitte wählen Sie einen Dienst für den Import von Teilnehmerdaten aus... buildVersion=Build-Version serverInformation=Server-Information serverConfiguration=Server-Konfiguration @@ -1644,7 +1646,7 @@ seriesHeaderUpcoming=Demnächst eventRegattaLeaderboardLastScoreUpdate=Letztes Ergebnisupdate eventRegattaRaceNotTracked=Nicht getrackt footerJobs=Jobs -footerCopyright=\u00A9 2011-2016 SAP Sailing Analytics +footerCopyright=\u00A9 2011-2017 SAP Sailing Analytics footerLanguage=SAP Sailing in footerLegal=Impressum footerPrivacy=Datenschutz @@ -1705,4 +1707,16 @@ lastFix=Letzter Fix filterDeviceMappings=Gerätezuordnungen filtern raceStartAnalysis=Startanalyse buoyZoneRadiusInHullLengths=Anzahl Bootslängen für Bahnmarkenkreis (z.B. 2 oder 3) -incorrectValueForRegattaBuoyZoneRadiusInHullLengths=Ungültiger WErt für den Bootslängen-Bahnmarkenkreis \ No newline at end of file +incorrectValueForRegattaBuoyZoneRadiusInHullLengths=Ungültiger Wert für den Bootslängen-Bahnmarkenkreis +importCompetitors=Teilnehmerdaten importieren +chooseWhichCompetitorsShouldBeImported=Zu importierende Teilnehmer links auswählen. Einzeln auswählen, falls ähnliche Teilnehmer bereits vorhanden sind. Diese werden dann rechts angezeigt, wovon einer ausgewählt werden kann, um diesen zu verwenden statt einen neuen anzulegen. Wenn die Auswahl vorhandener Teilnehmer abgeschlossen ist, links alle zu importierenden Teilnehmer auswählen und OK wählen. Tipp: Beide Tabellen können über die Spaltenköpfe sortiert werden. +existingCompetitors=Bestehende Teilnehmer +importedCompetitors=Zu importierende Teilnehmer +hasMatches=ähnliche vorhanden +filterImportedCompetitorsByNameSailRaceFleet=Zu importierende Teilnehmer filtern nach Name, Segelnummer, Wettfahrt oder Gruppe +errorLoadingCompetitorImportProviders=Fehler beim Laden der Quellen für den Teilnehmerdaten-Import: {0} +errorLoadingCompetitorImportDescriptors=Fehler beim Laden der zu importierenden Teilnehmerdaten: {0} +couldNotSaveCompetitorRegistrations=Teilnehmerregistrierung konnte nicht gespeichert werden: {0} +searchTagForImportedCompetitorPlaceholder=Z.B. Renn- und Gruppenname +version=Version: {0} +replicables=Replizierte Objekte \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties index 49732b9db08..343f03e6bba 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties @@ -344,7 +344,7 @@ courseMiddleLine=コース中央航路 messagingPortNumber=メッセージングポート番号 (初期値は 0) servletPortNumber=サーブレットポート番号 registeredAt=登録時刻 {0} -replicatingFromMaster=マスタ {0}:{2}、メッセージングポート {3}:{1}、エクスチェンジ {4} から複製 +replicatingFromMaster=マスタ {0}:{2}、メッセージングポート {3}:{1}、エクスチェンジ {4} から複製 TODO {5} showWindSpeedSeries=風速データ表示 showWindDirectionSeries=風向データ表示 fleet=フリート @@ -791,6 +791,7 @@ earlyStarters=フライング艇 showingDetailsOfRace=レース {0} の詳細表示 startAt=スタート時刻 finishAt=フィニッシュ時刻 +protestStartsAt=抗議開始時刻 protestEndsAt=抗議最終時刻 unknown=不明 raceLog=レースログ @@ -956,7 +957,7 @@ racingProcedureConfiguration=レガッタ設定 activateIndividualRecall=個別リコール有効化 activateResultEntry=結果エントリ有効化 rrs26Start=RRS26 開始 -sailingWorldCupStart=Sailing World Cup Start +sailingWorldCupStart=セーリングワールドカップ開始 setConfiguration=設定有効化 classFlag=クラス旗 classFlagHelpText={0} に使用するクラス旗を選定します。手順にある初期値の旗を有効化するには、''上書きしない'' を選択してください。 @@ -1154,6 +1155,7 @@ leaderboardGroupNotContainedInEvent=リーダーボードグループ {0} はイ raceIsInLiveTimePanelMode=レースがライブ中 backToLiveTimePanelMode=ライブに戻る windChart=風チャート +windChartLoading=風データをロード中... mediaNoVideosCaption=動画なし mediaShowVideoCaption=動画表示 mediaShowVideoTooltip=クリックして動画を表示: {0} @@ -1466,6 +1468,8 @@ doYouWantToDisableCompetitorsRegistrationsOnTheRace=レースへの競技者登 warningDirectCompetitorRegistration=警告: この設定を行うと、このレースの競技者がレースごとに登録されることにつながります。競技者は、該当するレガッタに登録されるときに、このレースに対して自動的には登録されません。 warningRegattaCompetitorRegistration=警告: これにより、レガッタへの競技者登録が再有効化されます。レガッタからのすべての競技者登録が、このレースに対する競技者登録として自動的に使用されます。 competitorRegistrationsOnRaceDisabled=競技者登録はこのレースに対して無効化されています。左側のチェックボックスを選択して有効化します。 +warningForDisabledCompetitors=次の競技者はこのレースに登録することができません: {0} +competitorToolTipMessage={0} はすでにレース {3} のフリート {2} に割り当てられており、そのため同一レース内でフリート {1} に割り当てることはできません addMarkToRegatta=マークをレガッタに追加 selectALeaderboardGroup=リーダーボードグループを選択... pleaseSelect=選択: @@ -1516,7 +1520,7 @@ playstoreCampaign=utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02 playstoreInsightApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app playstoreRacecommitteeApp=https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app playStoreBuoyPingerApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app -playstoreBadge=https://play.google.com/intl/en/badges/images/generic/en-play-badge.png +playstoreBadge=https://play.google.com/intl/en/badges/images/badge_new.png appstoreSapSailInsight=http://appstore.com/sapsailinsight appstoreBadgeSuffix=_en distanceFromAveragePosition=平均位置からの距離 @@ -1653,7 +1657,7 @@ seriesHeaderUpcoming=予定 eventRegattaLeaderboardLastScoreUpdate=前回スコア更新 eventRegattaRaceNotTracked=未追跡 footerJobs=採用 -footerCopyright=© 2011-2016 SAP Sailing Analytics +footerCopyright=© 2011-2017 SAP Sailing Analytics footerLanguage=SAP Sailing - footerLegal=利用規約 footerPrivacy=個人情報保護 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index 1a13b518344..58d1d59ce44 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -344,7 +344,7 @@ courseMiddleLine=Средняя линия курса messagingPortNumber=Номер порта обмена сообщениями (0 по умолчанию) servletPortNumber=Номер порта сервлета registeredAt=Зарегистрировано в {0} -replicatingFromMaster=Репликация с главного экземпляра {0}:{2}, порт обмена сообщениями {3}:{1}, Exchange {4} +replicatingFromMaster=Репликация с главного экземпляра {0}:{2}, порт обмена сообщениями {3}:{1}, Exchange {4} TODO {5} showWindSpeedSeries=Показать данные о скорости ветра showWindDirectionSeries=Показать данные о направлениях ветра fleet=Флот @@ -791,6 +791,7 @@ earlyStarters=Рано стартовавшие showingDetailsOfRace=Отображение подробностей для гонки {0} startAt=Старт в finishAt=Финиш в +protestStartsAt=Протест начинается в protestEndsAt=Протест заканчивается в unknown=Неизвестно raceLog=Журнал гонки @@ -956,7 +957,7 @@ racingProcedureConfiguration=Конфигурация регаты activateIndividualRecall=Активировать индивидуальный отзыв activateResultEntry=Активировать запись результата rrs26Start=RRS26 старт -sailingWorldCupStart=Sailing World Cup Start +sailingWorldCupStart=Старт Кубка мира по парусному спорту setConfiguration=Активировать конфигурацию classFlag=Флаг класса classFlagHelpText=Установка флага класса для {0}. Выберите ''Не перезаписывать'', чтобы активировать соответствующий флаг процедуры по умолчанию. @@ -1154,6 +1155,7 @@ leaderboardGroupNotContainedInEvent=Группа таблиц лидеров {0} raceIsInLiveTimePanelMode=Гонка активна backToLiveTimePanelMode=Вернуться в оперативный режим windChart=Диаграмма ветра +windChartLoading=Загрузка данных о ветре... mediaNoVideosCaption=Нет видео mediaShowVideoCaption=Показать видео mediaShowVideoTooltip=Щелкните, чтобы показать видео: {0} @@ -1466,6 +1468,8 @@ doYouWantToDisableCompetitorsRegistrationsOnTheRace=Отключить реги warningDirectCompetitorRegistration=Внимание! Этот параметр приведет к регистрации участников этой гонки только в данной гонке. При регистрации в соответствующей регате они не будут автоматически зарегистрированы в данной гонке. warningRegattaCompetitorRegistration=Внимание! Это приведет к повторной активации регистраций участника в регате. Все регистрации участника из регаты будут автоматически использованы как регистрация участника для данной гонки. competitorRegistrationsOnRaceDisabled=Регистрации участника отключены для данной гонки. Чтобы включить, установите флажок слева. +warningForDisabledCompetitors=Регистрация следующих участников на эту гонку невозможна: {0} +competitorToolTipMessage={0} уже присвоен флоту {2} в гонке {3} и поэтому не может быть присвоен флоту {1} в той же самой гонке addMarkToRegatta=Добавить отметку к регате selectALeaderboardGroup=Выбрать группу таблиц лидеров... pleaseSelect=Выберите @@ -1516,7 +1520,7 @@ playstoreCampaign=utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02 playstoreInsightApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app playstoreRacecommitteeApp=https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app playStoreBuoyPingerApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app -playstoreBadge=https://play.google.com/intl/en/badges/images/generic/en-play-badge.png +playstoreBadge=https://play.google.com/intl/en/badges/images/badge_new.png appstoreSapSailInsight=http://appstore.com/sapsailinsight appstoreBadgeSuffix=_en distanceFromAveragePosition=Дистанция от средней позиции @@ -1653,7 +1657,7 @@ seriesHeaderUpcoming=Скоро eventRegattaLeaderboardLastScoreUpdate=Последнее обновление оценок eventRegattaRaceNotTracked=Не отслеживается footerJobs=Работа -footerCopyright=© SAP Sailing Analytics, 2011–2016. +footerCopyright=© SAP Sailing Analytics, 2011–2017. footerLanguage=Язык SAP Sailing footerLegal=Раскрытие юридической информации footerPrivacy=Конфиденциальность diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index 3c255a3118b..67f902c19bf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -344,7 +344,7 @@ courseMiddleLine=场地中线 messagingPortNumber=消息传送端口编号(默认为 0) servletPortNumber=Servlet 端口编号 registeredAt=注册日期{0} -replicatingFromMaster=正在从主服务器{0}:{2}、消息传送端口{3}:{1}、交换机{4}复制 +replicatingFromMaster=正在从主服务器{0}:{2}、消息传送端口{3}:{1}、交换机{4}复制 TODO {5} showWindSpeedSeries=显示风速数据 showWindDirectionSeries=显示风向数据 fleet=船队 @@ -791,6 +791,7 @@ earlyStarters=抢航者 showingDetailsOfRace=显示比赛轮次{0}的详情 startAt=开始时间 finishAt=结束时间 +protestStartsAt=声明开始时间 protestEndsAt=声明结束时间 unknown=未知 raceLog=比赛轮次日志 @@ -956,7 +957,7 @@ racingProcedureConfiguration=比赛配置 activateIndividualRecall=激活个别召回 activateResultEntry=激活结果条目 rrs26Start=RRS26 开始 -sailingWorldCupStart=Sailing World Cup Start +sailingWorldCupStart=世界杯帆船赛开赛 setConfiguration=激活配置 classFlag=级别旗标 classFlagHelpText=设置用于{0}的级别旗标。 选择“不要覆盖”,激活程序的默认旗标。 @@ -1154,6 +1155,7 @@ leaderboardGroupNotContainedInEvent=积分榜分组{0}未包含在活动{1}中 raceIsInLiveTimePanelMode=正在直播比赛轮次 backToLiveTimePanelMode=返回实况 windChart=风力记录 +windChartLoading=正在加载风力数据... mediaNoVideosCaption=无视频 mediaShowVideoCaption=显示视频 mediaShowVideoTooltip=点击显示视频: {0} @@ -1466,6 +1468,8 @@ doYouWantToDisableCompetitorsRegistrationsOnTheRace=是否要禁用参赛队在 warningDirectCompetitorRegistration=警告! 此设置会导致此比赛轮次的参赛队在每个比赛轮次注册。 在相应的比赛中注册参赛队时,将无法自动注册此比赛轮次。 warningRegattaCompetitorRegistration=警告! 此操作将重新激活参赛队注册比赛。 比赛中的所有参赛队注册将自动用作此比赛轮次的参赛队注册。 competitorRegistrationsOnRaceDisabled=参赛队注册已针对此比赛轮次禁用。选中左侧的复选框可启用。 +warningForDisabledCompetitors=以下参赛队无法注册此比赛轮次:{0} +competitorToolTipMessage={0}已分配到比赛轮次{3}中的船队{2},因此,无法分配到同一比赛轮次中的船队{1} addMarkToRegatta=向比赛添加标志 selectALeaderboardGroup=选择积分榜分组... pleaseSelect=请选择 @@ -1516,7 +1520,7 @@ playstoreCampaign=utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02 playstoreInsightApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app playstoreRacecommitteeApp=https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app playStoreBuoyPingerApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app -playstoreBadge=https://play.google.com/intl/en/badges/images/generic/en-play-badge.png +playstoreBadge=https://play.google.com/intl/en/badges/images/badge_new.png appstoreSapSailInsight=http://appstore.com/sapsailinsight appstoreBadgeSuffix=_en distanceFromAveragePosition=与平均位置的距离 @@ -1653,7 +1657,7 @@ seriesHeaderUpcoming=即将到来 eventRegattaLeaderboardLastScoreUpdate=上次得分更新 eventRegattaRaceNotTracked=未跟踪 footerJobs=招贤纳士 -footerCopyright=版权所有 © 2011-2016 SAP Sailing Analytics +footerCopyright=版权所有 © 2011-2017 SAP Sailing Analytics footerLanguage=SAP Sailing 语言 footerLegal=法律声明 footerPrivacy=隐私 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties index 3c255a3118b..67f902c19bf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties @@ -344,7 +344,7 @@ courseMiddleLine=场地中线 messagingPortNumber=消息传送端口编号(默认为 0) servletPortNumber=Servlet 端口编号 registeredAt=注册日期{0} -replicatingFromMaster=正在从主服务器{0}:{2}、消息传送端口{3}:{1}、交换机{4}复制 +replicatingFromMaster=正在从主服务器{0}:{2}、消息传送端口{3}:{1}、交换机{4}复制 TODO {5} showWindSpeedSeries=显示风速数据 showWindDirectionSeries=显示风向数据 fleet=船队 @@ -791,6 +791,7 @@ earlyStarters=抢航者 showingDetailsOfRace=显示比赛轮次{0}的详情 startAt=开始时间 finishAt=结束时间 +protestStartsAt=声明开始时间 protestEndsAt=声明结束时间 unknown=未知 raceLog=比赛轮次日志 @@ -956,7 +957,7 @@ racingProcedureConfiguration=比赛配置 activateIndividualRecall=激活个别召回 activateResultEntry=激活结果条目 rrs26Start=RRS26 开始 -sailingWorldCupStart=Sailing World Cup Start +sailingWorldCupStart=世界杯帆船赛开赛 setConfiguration=激活配置 classFlag=级别旗标 classFlagHelpText=设置用于{0}的级别旗标。 选择“不要覆盖”,激活程序的默认旗标。 @@ -1154,6 +1155,7 @@ leaderboardGroupNotContainedInEvent=积分榜分组{0}未包含在活动{1}中 raceIsInLiveTimePanelMode=正在直播比赛轮次 backToLiveTimePanelMode=返回实况 windChart=风力记录 +windChartLoading=正在加载风力数据... mediaNoVideosCaption=无视频 mediaShowVideoCaption=显示视频 mediaShowVideoTooltip=点击显示视频: {0} @@ -1466,6 +1468,8 @@ doYouWantToDisableCompetitorsRegistrationsOnTheRace=是否要禁用参赛队在 warningDirectCompetitorRegistration=警告! 此设置会导致此比赛轮次的参赛队在每个比赛轮次注册。 在相应的比赛中注册参赛队时,将无法自动注册此比赛轮次。 warningRegattaCompetitorRegistration=警告! 此操作将重新激活参赛队注册比赛。 比赛中的所有参赛队注册将自动用作此比赛轮次的参赛队注册。 competitorRegistrationsOnRaceDisabled=参赛队注册已针对此比赛轮次禁用。选中左侧的复选框可启用。 +warningForDisabledCompetitors=以下参赛队无法注册此比赛轮次:{0} +competitorToolTipMessage={0}已分配到比赛轮次{3}中的船队{2},因此,无法分配到同一比赛轮次中的船队{1} addMarkToRegatta=向比赛添加标志 selectALeaderboardGroup=选择积分榜分组... pleaseSelect=请选择 @@ -1516,7 +1520,7 @@ playstoreCampaign=utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02 playstoreInsightApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app playstoreRacecommitteeApp=https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app playStoreBuoyPingerApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app -playstoreBadge=https://play.google.com/intl/en/badges/images/generic/en-play-badge.png +playstoreBadge=https://play.google.com/intl/en/badges/images/badge_new.png appstoreSapSailInsight=http://appstore.com/sapsailinsight appstoreBadgeSuffix=_en distanceFromAveragePosition=与平均位置的距离 @@ -1653,7 +1657,7 @@ seriesHeaderUpcoming=即将到来 eventRegattaLeaderboardLastScoreUpdate=上次得分更新 eventRegattaRaceNotTracked=未跟踪 footerJobs=招贤纳士 -footerCopyright=版权所有 © 2011-2016 SAP Sailing Analytics +footerCopyright=版权所有 © 2011-2017 SAP Sailing Analytics footerLanguage=SAP Sailing 语言 footerLegal=法律声明 footerPrivacy=隐私 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/BoatOverlay.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/BoatOverlay.java index 37471e031d8..b15e2eb9be6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/BoatOverlay.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/BoatOverlay.java @@ -7,7 +7,7 @@ import com.google.gwt.maps.client.MapWidget; import com.google.gwt.maps.client.base.LatLng; import com.google.gwt.maps.client.base.Point; import com.google.gwt.maps.client.base.Size; -import com.sap.sailing.domain.common.BoatClassMasterdata; +import com.sap.sailing.domain.common.Distance; import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.Tack; import com.sap.sailing.domain.common.dto.BoatClassDTO; @@ -149,43 +149,38 @@ public class BoatOverlay extends CanvasOverlayV3 { } public Util.Pair getBoatScaleAndSize(BoatClassDTO boatClass) { - BoatClassMasterdata boatClassMasterdata = BoatClassMasterdata.resolveBoatClass(boatClass.getName()); - Size boatSizeInPixel = getCorrelatedBoatSize(boatClassMasterdata); - + Size boatSizeInPixel = getCorrelatedBoatSize(boatClass.getHullLength(), boatClass.getHullBeam()); double boatHullScaleFactor = boatSizeInPixel.getWidth() / (boatVectorGraphics.getHullLengthInPx()); double boatBeamScaleFactor = boatSizeInPixel.getHeight() / (boatVectorGraphics.getBeamInPx()); - // as the canvas contains the whole boat the canvas size relates to the overall length, not the hull length double scaledWidthSize = (boatVectorGraphics.getOverallLengthInPx()) * boatHullScaleFactor; double scaledBeamSize = (boatVectorGraphics.getOverallLengthInPx()) * boatBeamScaleFactor; - return new Util.Pair(Size.newInstance(boatHullScaleFactor, boatBeamScaleFactor), Size.newInstance(scaledWidthSize + scaledWidthSize / 2.0, scaledBeamSize + scaledBeamSize / 2.0)); } - private Size getCorrelatedBoatSize(BoatClassMasterdata boatClassMasterdata) { - Size boatSizeInPixel = calculateBoundingBox(mapProjection, boatFix.position, - boatClassMasterdata.getHullLength(), boatClassMasterdata.getHullBeam()); - changeBoatSizeIfTooShortHull(boatSizeInPixel, boatClassMasterdata); - changeBoatSizeIfTooNarrowBeam(boatSizeInPixel, boatClassMasterdata); + private Size getCorrelatedBoatSize(Distance hullLength, Distance hullBeam) { + Size boatSizeInPixel = calculateBoundingBox(mapProjection, boatFix.position, hullLength, hullBeam); + changeBoatSizeIfTooShortHull(boatSizeInPixel, hullLength, hullBeam); + changeBoatSizeIfTooNarrowBeam(boatSizeInPixel, hullLength, hullBeam); return boatSizeInPixel; } - private void changeBoatSizeIfTooShortHull(Size boatSizeInPixel, BoatClassMasterdata boatClassMasterdata) { + private void changeBoatSizeIfTooShortHull(Size boatSizeInPixel, Distance hullLength, Distance hullBeam) { // the minimum boat length is related to the hull of the boat, not the overall length double minBoatHullLengthInPx = boatVectorGraphics.getMinHullLengthInPx(); if (boatSizeInPixel.getWidth() < minBoatHullLengthInPx) { - double ratioBeanHullLength = boatClassMasterdata.getHullBeam().divide(boatClassMasterdata.getHullLength()); + double ratioBeanHullLength = hullBeam.divide(hullLength); boatSizeInPixel.setHeight(minBoatHullLengthInPx * ratioBeanHullLength); boatSizeInPixel.setWidth(minBoatHullLengthInPx); } } - private void changeBoatSizeIfTooNarrowBeam(Size boatSizeInPixel, BoatClassMasterdata boatClassMasterdata) { + private void changeBoatSizeIfTooNarrowBeam(Size boatSizeInPixel, Distance hullLength, Distance hullBeam) { // if the boat gets too narrow, use the minimum beam and scale the hull length according to aspect double minBoatBeamLengthInPx = boatVectorGraphics.getMinBeamLengthInPx(); if (boatSizeInPixel.getHeight() < minBoatBeamLengthInPx) { - double ratioHullBeanLength = boatClassMasterdata.getHullLength().divide(boatClassMasterdata.getHullBeam()); + double ratioHullBeanLength = hullLength.divide(hullBeam); boatSizeInPixel.setWidth(minBoatBeamLengthInPx * ratioHullBeanLength); boatSizeInPixel.setHeight(minBoatBeamLengthInPx); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 87a3e132728..bd658050de6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -2766,7 +2766,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe sapLogo.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - Window.open("http://www.sap.com", "_blank", null); + Window.open("https://www.sap.com/sponsorships", "_blank", null); } }); sapLogo.setStyleName("raceBoard-Logo"); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/masterdataimport/MasterDataImportPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/masterdataimport/MasterDataImportPanel.java index ec91acc62f1..2003f3ee495 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/masterdataimport/MasterDataImportPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/masterdataimport/MasterDataImportPanel.java @@ -71,7 +71,7 @@ public class MasterDataImportPanel extends VerticalPanel { HorizontalPanel serverAddressPanel = new HorizontalPanel(); serverAddressPanel.add(new Label(stringMessages.importRemoteHost())); hostBox = new TextBox(); - hostBox.setText("http://www.sapsailing.com/"); + hostBox.setText("https://www.sapsailing.com/"); hostBox.setWidth("300px"); serverAddressPanel.add(hostBox); fetchIdsButton = new Button(stringMessages.importFetchRemoteLgs()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index f2ff5d8cf5a..645f7d52a50 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -44,6 +44,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.StreamSupport; import java.util.zip.GZIPInputStream; import javax.servlet.ServletContext; @@ -55,6 +56,7 @@ import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; +import com.sap.sailing.competitorimport.CompetitorProvider; import com.sap.sailing.domain.abstractlog.AbstractLog; import com.sap.sailing.domain.abstractlog.AbstractLogEvent; import com.sap.sailing.domain.abstractlog.impl.AllEventsOfTypeFinder; @@ -132,18 +134,17 @@ import com.sap.sailing.domain.base.configuration.impl.RegattaConfigurationImpl; import com.sap.sailing.domain.base.configuration.impl.SWCStartConfigurationImpl; import com.sap.sailing.domain.base.configuration.procedures.ConfigurableStartModeFlagRacingProcedureConfiguration; import com.sap.sailing.domain.base.impl.BoatImpl; +import com.sap.sailing.domain.base.impl.CompetitorImpl; import com.sap.sailing.domain.base.impl.CourseDataImpl; import com.sap.sailing.domain.base.impl.CourseImpl; import com.sap.sailing.domain.base.impl.DynamicBoat; import com.sap.sailing.domain.base.impl.DynamicPerson; import com.sap.sailing.domain.base.impl.DynamicTeam; -import com.sap.sailing.domain.base.impl.FleetImpl; import com.sap.sailing.domain.base.impl.PersonImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.base.impl.SailingServerConfigurationImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; import com.sap.sailing.domain.base.impl.TeamImpl; import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.CompetitorDescriptor; import com.sap.sailing.domain.common.CourseDesignerMode; import com.sap.sailing.domain.common.DataImportProgress; import com.sap.sailing.domain.common.DataImportSubProgress; @@ -191,6 +192,7 @@ import com.sap.sailing.domain.common.dto.FullLeaderboardDTO; import com.sap.sailing.domain.common.dto.IncrementalLeaderboardDTO; import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO; import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.PersonDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTOFactory; import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; @@ -256,7 +258,6 @@ import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; import com.sap.sailing.domain.racelogtracking.impl.DeviceMappingImpl; import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; -import com.sap.sailing.domain.ranking.RankingMetricsFactory; import com.sap.sailing.domain.regattalike.HasRegattaLike; import com.sap.sailing.domain.regattalike.LeaderboardThatHasRegattaLike; import com.sap.sailing.domain.regattalog.RegattaLogStore; @@ -282,7 +283,6 @@ import com.sap.sailing.domain.tracking.LineDetails; import com.sap.sailing.domain.tracking.Maneuver; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.MarkPassingManeuver; -import com.sap.sailing.domain.tracking.RaceHandle; import com.sap.sailing.domain.tracking.RaceTracker; import com.sap.sailing.domain.tracking.Track; import com.sap.sailing.domain.tracking.TrackedLeg; @@ -305,6 +305,7 @@ import com.sap.sailing.gwt.ui.client.shared.charts.MarkPositionService.MarkTrack import com.sap.sailing.gwt.ui.shared.BulkScoreCorrectionDTO; import com.sap.sailing.gwt.ui.shared.CompactBoatPositionsDTO; import com.sap.sailing.gwt.ui.shared.CompactRaceMapDataDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorProviderDTO; import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; import com.sap.sailing.gwt.ui.shared.ControlPointDTO; @@ -353,9 +354,6 @@ import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO; import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO.ScoreCorrectionEntryDTO; import com.sap.sailing.gwt.ui.shared.RemoteSailingServerReferenceDTO; -import com.sap.sailing.gwt.ui.shared.ReplicaDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationMasterDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; import com.sap.sailing.gwt.ui.shared.SailingServiceConstants; import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; import com.sap.sailing.gwt.ui.shared.SeriesDTO; @@ -421,7 +419,6 @@ import com.sap.sailing.server.operationaltransformation.SetRaceIsKnownToStartUpw import com.sap.sailing.server.operationaltransformation.SetSuppressedFlagForCompetitorInLeaderboard; import com.sap.sailing.server.operationaltransformation.SetWindSourcesToExclude; import com.sap.sailing.server.operationaltransformation.StopTrackingRace; -import com.sap.sailing.server.operationaltransformation.StopTrackingRegatta; import com.sap.sailing.server.operationaltransformation.UpdateCompetitor; import com.sap.sailing.server.operationaltransformation.UpdateCompetitorDisplayNameInLeaderboard; import com.sap.sailing.server.operationaltransformation.UpdateEvent; @@ -476,12 +473,17 @@ import com.sap.sse.gwt.dispatch.servlets.ProxiedRemoteServiceServlet; import com.sap.sse.gwt.server.filestorage.FileStorageServiceDTOUtils; import com.sap.sse.gwt.shared.filestorage.FileStorageServiceDTO; import com.sap.sse.gwt.shared.filestorage.FileStorageServicePropertyErrorsDTO; +import com.sap.sse.gwt.shared.replication.ReplicaDTO; +import com.sap.sse.gwt.shared.replication.ReplicationMasterDTO; +import com.sap.sse.gwt.shared.replication.ReplicationStateDTO; import com.sap.sse.i18n.ResourceBundleStringMessages; import com.sap.sse.replication.OperationWithResult; +import com.sap.sse.replication.Replicable; import com.sap.sse.replication.ReplicationFactory; import com.sap.sse.replication.ReplicationMasterDescriptor; import com.sap.sse.replication.ReplicationService; import com.sap.sse.replication.impl.ReplicaDescriptor; +import com.sap.sse.security.SecurityService; import com.sap.sse.security.SessionUtils; import com.sap.sse.shared.media.ImageDescriptor; import com.sap.sse.shared.media.MediaUtils; @@ -510,6 +512,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S private final ServiceTracker scoreCorrectionProviderServiceTracker; + private final ServiceTracker competitorProviderServiceTracker; + private final MongoObjectFactory mongoObjectFactory; private final SwissTimingAdapterPersistence swissTimingAdapterPersistence; @@ -586,6 +590,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S /* raceLogResolver */ getService()); scoreCorrectionProviderServiceTracker = ServiceTrackerFactory.createAndOpen(context, ScoreCorrectionProvider.class); + competitorProviderServiceTracker = ServiceTrackerFactory.createAndOpen(context, CompetitorProvider.class); tractracDomainObjectFactory = com.sap.sailing.domain.tractracadapter.persistence.PersistenceFactory.INSTANCE .createDomainObjectFactory(mongoObjectFactory.getDatabase(), getTracTracAdapter() .getTracTracDomainFactory()); @@ -662,11 +667,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } private Iterable getAllScoreCorrectionProviders() { - final Object[] services = scoreCorrectionProviderServiceTracker.getServices(); + final ScoreCorrectionProvider[] services = scoreCorrectionProviderServiceTracker.getServices(new ScoreCorrectionProvider[0]); List result = new ArrayList(); if (services != null) { - for (Object service : services) { - result.add((ScoreCorrectionProvider) service); + for (final ScoreCorrectionProvider service : services) { + result.add(service); } } return result; @@ -686,6 +691,53 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return new ScoreCorrectionProviderDTO(scoreCorrectionProvider.getName(), hasResultsForBoatClassFromDateByEventName); } + @Override + public Iterable getCompetitorProviderNames() { + List result = new ArrayList<>(); + for (CompetitorProvider competitorProvider : getAllCompetitorProviders()) { + result.add(competitorProvider.getName()); + } + return result; + } + + private Iterable getAllCompetitorProviders() { + final CompetitorProvider[] services = competitorProviderServiceTracker.getServices(new CompetitorProvider[0]); + List result = new ArrayList<>(); + if (services != null) { + for (final CompetitorProvider service : services) { + result.add(service); + } + } + return result; + } + + @Override + public CompetitorProviderDTO getCompetitorProviderDTOByName(String providerName) throws Exception { + for (CompetitorProvider competitorProvider : getAllCompetitorProviders()) { + if (competitorProvider.getName().equals(providerName)) { + return new CompetitorProviderDTO(competitorProvider.getName(), + new HashMap<>(competitorProvider.getHasCompetitorsForRegattasInEvent())); + } + } + return null; + } + + @Override + public List getCompetitorDescriptors(String competitorProviderName, String eventName, + String regattaName) throws Exception { + for (CompetitorProvider cp : getAllCompetitorProviders()) { + if (cp.getName().equals(competitorProviderName)) { + final List result = new ArrayList<>(); + Util.addAll(cp.getCompetitorDescriptors(eventName, regattaName), result); + return result; + } + } + return Collections.emptyList(); + } + + @Override + public Pair makePersonDTOSerializableAndDeserializableDummy(PersonDTO dummy, CountryCode ccDummy) { return null; } + /** * If date is null, the {@link LiveLeaderboardUpdater} for the * leaderboardName requested is obtained or created if it doesn't exist yet. The request is then passed @@ -827,7 +879,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S regattaDTO.endDate = regatta.getEndDate() != null ? regatta.getEndDate().asDate() : null; BoatClass boatClass = regatta.getBoatClass(); if (boatClass != null) { - regattaDTO.boatClass = new BoatClassDTO(boatClass.getName(), boatClass.getDisplayName(), boatClass.getHullLength()); + regattaDTO.boatClass = new BoatClassDTO(boatClass.getName(), boatClass.getDisplayName(), boatClass.getHullLength(), boatClass.getHullBeam()); } if (regatta.getDefaultCourseArea() != null) { regattaDTO.defaultCourseAreaUuid = regatta.getDefaultCourseArea().getId(); @@ -1037,8 +1089,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S result.add(new TracTracRaceRecordDTO(raceRecord.getID(), raceRecord.getEventName(), raceRecord.getName(), raceRecord.getTrackingStartTime().asDate(), - raceRecord - .getTrackingEndTime().asDate(), raceRecord.getRaceStartTime() == null ? null : raceRecord.getRaceStartTime().asDate(), + raceRecord.getTrackingEndTime().asDate(), raceRecord.getRaceStartTime() == null ? null : raceRecord.getRaceStartTime().asDate(), raceRecord.getBoatClassNames(), raceRecord.getRaceStatus(), raceRecord.getRaceVisibility(), raceRecord.getJsonURL().toString(), hasRememberedRegatta(raceRecord.getID()))); } @@ -1078,25 +1129,15 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } else { effectiveStoredURI = storedURI; } - final RaceHandle raceHandle = getTracTracAdapter().addTracTracRace(getService(), regattaToAddTo, - record.getParamURL(), effectiveLiveURI == null ? null : new URI(effectiveLiveURI), new URI(effectiveStoredURI), - new URI(courseDesignUpdateURI), new MillisecondsTimePoint(record.getTrackingStartTime().asMillis()), - new MillisecondsTimePoint(record.getTrackingEndTime().asMillis()), - getRaceLogStore(), getRegattaLogStore(), - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, offsetToStartTimeOfSimulatedRace, - useInternalMarkPassingAlgorithm, tracTracUsername, tracTracPassword, record.getRaceStatus(), record.getRaceVisibility()); - if (trackWind) { - new Thread("Wind tracking starter for race " + record.getEventName() + "/" + record.getName()) { - public void run() { - try { - startTrackingWind(raceHandle, correctWindByDeclination, - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }.start(); - } + getTracTracAdapter().addTracTracRace(getService(), regattaToAddTo, + record.getParamURL(), effectiveLiveURI == null ? null : new URI(effectiveLiveURI), + new URI(effectiveStoredURI), new URI(courseDesignUpdateURI), + new MillisecondsTimePoint(record.getTrackingStartTime().asMillis()), + new MillisecondsTimePoint(record.getTrackingEndTime().asMillis()), getRaceLogStore(), + getRegattaLogStore(), RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, + offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, tracTracUsername, + tracTracPassword, record.getRaceStatus(), record.getRaceVisibility(), trackWind, + correctWindByDeclination); } } @@ -1118,11 +1159,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S courseDesignUpdateURI, tracTracUsername, tracTracPassword)); } - @Override - public void stopTrackingEvent(RegattaIdentifier regattaIdentifier) throws Exception { - getService().apply(new StopTrackingRegatta(regattaIdentifier)); - } - private RaceDefinition getRaceByName(Regatta regatta, String raceName) { if (regatta != null) { return regatta.getRaceByName(raceName); @@ -1145,24 +1181,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } } - /** - * @param timeoutInMilliseconds eventually passed to {@link RaceHandle#getRace(long)}. If the race definition - * can be obtained within this timeout, wind for the race will be tracked; otherwise, the method returns without - * taking any effect. - */ - private void startTrackingWind(RaceHandle raceHandle, boolean correctByDeclination, long timeoutInMilliseconds) { - Regatta regatta = raceHandle.getRegatta(); - if (regatta != null) { - RaceDefinition race = raceHandle.getRace(timeoutInMilliseconds); - if (race != null) { - getService().startTrackingWind(regatta, race, correctByDeclination); - } else { - log("RaceDefinition wasn't received within " + timeoutInMilliseconds + "ms for a race in regatta " - + regatta.getName() + ". Aborting wait; no wind tracking for this race."); - } - } - } - @Override public WindInfoForRaceDTO getRawWindFixes(RegattaAndRaceIdentifier raceIdentifier, Collection windSources) { WindInfoForRaceDTO result = null; @@ -1472,23 +1490,22 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public boolean getPolarResults(RegattaAndRaceIdentifier raceIdentifier) { final boolean result; - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace == null) { + final TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + final PolarDataService polarData = getService().getPolarDataService(); + if (trackedRace == null || polarData == null) { result = false; } else { BoatClass boatClass = trackedRace.getRace().getBoatClass(); - PolarDataService polarData = getService().getPolarDataService(); PolarDiagram polarDiagram; try { polarDiagram = new PolarDiagramGPS(boatClass, polarData); } catch (SparseSimulationDataException e) { polarDiagram = null; - // TODO: raise a UI message, to inform user about missing polar data resulting in unability to simulate } result = polarDiagram != null; } return result; - } + } @Override public SimulatorResultsDTO getSimulatorResults(LegIdentifier legIdentifier) { @@ -1762,9 +1779,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S GPSFixMoving fix = fixIter.next(); if (fix.getTimePoint().before(toTimePointExcluding) || (extrapolate && fix.getTimePoint().equals(toTimePointExcluding))) { - if (logger.isLoggable(Level.FINEST)) { - logger.finest(""+competitor.getName()+": " + fix); - } + logger.finest(()->""+competitor.getName()+": " + fix); fixes.add(fix); } else { break; @@ -2706,15 +2721,15 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S if (eventResult != null) { for (RegattaResultDescriptor regattaResult : eventResult.getRegattaResults()) { for(RaceResultDescriptor race: regattaResult.getRaceResults()) { - // add only the tracked races - if(race.isTracked() != null && race.isTracked() == true) { + // add only the tracked races + if (race.isTracked() != null && race.isTracked() == true) { SwissTimingRaceRecordDTO swissTimingRaceRecordDTO = new SwissTimingRaceRecordDTO(race.getId(), race.getName(), - regattaResult.getName(), race.getSeriesName(), race.getFleetName(), race.getStatus(), race.getStartTime(), - regattaResult.getXrrEntriesUrl() != null ? regattaResult.getXrrEntriesUrl().toExternalForm() : null, hasRememberedRegatta(race.getId())); + regattaResult.getName(), race.getSeriesName(), race.getFleetName(), race.getStatus(), race.getStartTime(), + regattaResult.getXrrEntriesUrl() != null ? regattaResult.getXrrEntriesUrl().toExternalForm() : null, hasRememberedRegatta(race.getId())); swissTimingRaceRecordDTO.boatClass = regattaResult.getIsafId() != null && !regattaResult.getIsafId().isEmpty() ? regattaResult.getIsafId() : regattaResult.getClassName(); swissTimingRaceRecordDTO.gender = regattaResult.getCompetitorGenderType().name(); swissTimingRaces.add(swissTimingRaceRecordDTO); - } + } } } result = new SwissTimingEventRecordDTO(eventResult.getId(), eventResult.getName(), eventResult.getTrackingDataHost(), @@ -2764,22 +2779,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S startList = getSwissTimingAdapter().readStartListForRace(rr.raceId, regattaResults); } // now read the entry list for the race from the result - final RaceHandle raceHandle = getSwissTimingAdapter().addSwissTimingRace(getService(), regattaToAddTo, + getSwissTimingAdapter().addSwissTimingRace(getService(), regattaToAddTo, rr.raceId, rr.getName(), raceDescription, boatClass, hostname, port, startList, getRaceLogStore(), getRegattaLogStore(), - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, useInternalMarkPassingAlgorithm); - if (trackWind) { - new Thread("Wind tracking starter for race " + rr.raceId + "/" + rr.getName()) { - public void run() { - try { - startTrackingWind(raceHandle, correctWindByDeclination, - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }.start(); - } + RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, useInternalMarkPassingAlgorithm, trackWind, correctWindByDeclination); } } @@ -2792,7 +2795,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S List replayRaces = getSwissTimingReplayService().listReplayRaces(swissTimingUrl); List result = new ArrayList(replayRaces.size()); for (SwissTimingReplayRace replayRace : replayRaces) { - result.add(new SwissTimingReplayRaceDTO(replayRace.getFlightNumber(), replayRace.getRaceId(), replayRace.getRsc(), replayRace.getName(), replayRace.getBoatClass(), replayRace.getStartTime(), replayRace.getLink(), hasRememberedRegatta(replayRace.getRaceId()))); + result.add(new SwissTimingReplayRaceDTO(replayRace.getFlightNumber(), replayRace.getRaceId(), + replayRace.getRsc(), replayRace.getName(), replayRace.getBoatClass(), replayRace.getStartTime(), + replayRace.getLink(), hasRememberedRegatta(replayRace.getRaceId()))); } return result; } @@ -2801,35 +2806,24 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S public void replaySwissTimingRace(RegattaIdentifier regattaIdentifier, Iterable replayRaceDTOs, boolean trackWind, boolean correctWindByDeclination, boolean useInternalMarkPassingAlgorithm) { logger.info("replaySwissTimingRace for regatta "+regattaIdentifier+" for races "+replayRaceDTOs); - Regatta regatta; for (SwissTimingReplayRaceDTO replayRaceDTO : replayRaceDTOs) { try { + String boatClassName; if (regattaIdentifier == null) { - String boatClass = replayRaceDTO.boat_class; + boatClassName = replayRaceDTO.boat_class; for (String genderIndicator : new String[] { "Man", "Woman", "Men", "Women", "M", "W" }) { Pattern p = Pattern.compile("(( - )|-| )" + genderIndicator + "$"); - Matcher m = p.matcher(boatClass.trim()); + Matcher m = p.matcher(boatClassName.trim()); if (m.find()) { - boatClass = boatClass.trim().substring(0, m.start(1)); + boatClassName = boatClassName.trim().substring(0, m.start(1)); break; } } - regatta = getService().createRegatta( - RegattaImpl.getDefaultName(replayRaceDTO.rsc, boatClass.trim()), - boatClass.trim(), - /* startDate*/ null, /*endDate*/ null, - RegattaImpl.getDefaultName(replayRaceDTO.rsc, replayRaceDTO.boat_class), - Collections.singletonList(new SeriesImpl(LeaderboardNameConstants.DEFAULT_SERIES_NAME, - /* isMedal */false, /* isFleetsCanRunInParallel */ true, Collections.singletonList(new FleetImpl( - LeaderboardNameConstants.DEFAULT_FLEET_NAME)), - /* race column names */new ArrayList(), getService())), false, - baseDomainFactory.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /*buoyZoneRadiusInHullLengths*/2.0, /* useStartTimeInference */ true, - /* controlTrackingFromStartAndFinishTimes */ false, RankingMetricsFactory.getRankingMetricConstructor(RankingMetrics.ONE_DESIGN)); - // TODO: is course area relevant for swiss timing replay? } else { - regatta = getService().getRegatta(regattaIdentifier); + boatClassName = null; } - getSwissTimingReplayService().loadRaceData(replayRaceDTO.link, regatta, getService(), useInternalMarkPassingAlgorithm); + getSwissTimingReplayService().loadRaceData(regattaIdentifier, replayRaceDTO.link, replayRaceDTO.getName(), + replayRaceDTO.race_id, boatClassName, getService(), getService(), useInternalMarkPassingAlgorithm, getRaceLogStore(), getRegattaLogStore()); } catch (Exception e) { logger.log(Level.SEVERE, "Error trying to load SwissTimingReplay race " + replayRaceDTO, e); } @@ -3378,10 +3372,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S for (Entry>, Integer> e : statistics.entrySet()) { replicationCountByOperationClassName.put(e.getKey().getName(), e.getValue()); } - replicaDTOs.add(new ReplicaDTO(replicaDescriptor.getIpAddress().getHostName(), replicaDescriptor - .getRegistrationTime().asDate(), replicaDescriptor.getUuid().toString(), - replicationCountByOperationClassName, service.getAverageNumberOfOperationsPerMessage(replicaDescriptor), - service.getNumberOfMessagesSent(replicaDescriptor), service.getNumberOfBytesSent(replicaDescriptor), service.getAverageNumberOfBytesPerMessage(replicaDescriptor))); + replicaDTOs.add(new ReplicaDTO(replicaDescriptor.getIpAddress().getHostName(), + replicaDescriptor.getRegistrationTime().asDate(), replicaDescriptor.getUuid().toString(), + replicaDescriptor.getReplicableIdsAsStrings(), replicationCountByOperationClassName, + service.getAverageNumberOfOperationsPerMessage(replicaDescriptor), + service.getNumberOfMessagesSent(replicaDescriptor), service.getNumberOfBytesSent(replicaDescriptor), + service.getAverageNumberOfBytesPerMessage(replicaDescriptor))); } ReplicationMasterDTO master; ReplicationMasterDescriptor replicatingFromMaster = service.getReplicatingFromMaster(); @@ -3389,18 +3385,28 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S master = null; } else { master = new ReplicationMasterDTO(replicatingFromMaster.getHostname(), replicatingFromMaster.getServletPort(), - replicatingFromMaster.getMessagingHostname(), replicatingFromMaster.getMessagingPort(), replicatingFromMaster.getExchangeName()); + replicatingFromMaster.getMessagingHostname(), replicatingFromMaster.getMessagingPort(), replicatingFromMaster.getExchangeName(), + StreamSupport.stream(replicatingFromMaster.getReplicables().spliterator(), /* parallel */ false).map(r->r.getId()).toArray(s->new String[s])); } return new ReplicationStateDTO(master, replicaDTOs, service.getServerIdentifier().toString()); } + /** + * A warning shall be issued to the administration user if the {@link RacingEventService} is a replica. For all + * other {@link Replicable}s such as the {@link SecurityService} we don't care. + */ + @Override + public String[] getReplicableIdsAsStringThatShallLeadToWarningAboutInstanceBeingReplica() { + return new String[] { getService().getId().toString() }; + } + @Override public void startReplicatingFromMaster(String messagingHost, String masterHost, String exchangeName, int servletPort, int messagingPort) throws IOException, ClassNotFoundException, InterruptedException { - // the queue name must be always the same for this server. in order to achieve + // The queue name must always be the same for this server. In order to achieve // this we're using the unique server identifier getReplicationService().startToReplicateFrom( ReplicationFactory.INSTANCE.createReplicationMasterDescriptor(messagingHost, masterHost, exchangeName, servletPort, messagingPort, - getReplicationService().getServerIdentifier().toString())); + /* use local server identifier as queue name */ getReplicationService().getServerIdentifier().toString(), getReplicationService().getAllReplicables())); } @Override @@ -3654,10 +3660,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S private Map convertToLocalesAndUrls(Map sailorsInfoWebsiteURLsByLocaleName) { Map eventURLs = new HashMap<>(); for (Map.Entry entry : sailorsInfoWebsiteURLsByLocaleName.entrySet()) { - try { - eventURLs.put(toLocale(entry.getKey()), new URL(entry.getValue())); - } catch(Exception e) { - // broken URLs or Locales are not being stored + if (entry.getValue() != null) { + try { + eventURLs.put(toLocale(entry.getKey()), new URL(entry.getValue())); + } catch(Exception e) { + // broken URLs or Locales are not being stored + } } } return eventURLs; @@ -4226,14 +4234,17 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public List getRaceStateEntriesForLeaderboard(String leaderboardName, - boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, final List visibleRegattas) + boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, Duration clientTimeZoneOffset, final List visibleRegattas) throws NoWindException, InterruptedException, ExecutionException { Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); - return getRaceStateEntriesForLeaderboard(leaderboard, showOnlyCurrentlyRunningRaces, showOnlyRacesOfSameDay, visibleRegattas); + return getRaceStateEntriesForLeaderboard(leaderboard, showOnlyCurrentlyRunningRaces, showOnlyRacesOfSameDay, clientTimeZoneOffset, visibleRegattas); } + /** + * The client's day starts at 00:00:00Z - clientTimeZoneOffset and ends at 23:59:59Z - clientTimeZoneOffset. + */ private List getRaceStateEntriesForLeaderboard(Leaderboard leaderboard, - boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, final List visibleRegattas) + boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, Duration clientTimeZoneOffset, final List visibleRegattas) throws NoWindException, InterruptedException, ExecutionException { List result = new ArrayList(); Calendar dayToCheck = Calendar.getInstance(); @@ -4252,8 +4263,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S for (Series series : regatta.getSeries()) { Map> entriesPerFleet = new HashMap>(); for (RaceColumn raceColumn : series.getRaceColumns()) { - getRegattaOverviewEntries(showOnlyRacesOfSameDay, dayToCheck, - usedCourseArea, leaderboard, boatClass.getName(), regattaName, buyZoneRadius, series.getName(), raceColumn, entriesPerFleet); + getRegattaOverviewEntries(showOnlyRacesOfSameDay, clientTimeZoneOffset, dayToCheck, + usedCourseArea, leaderboard, boatClass.getName(), regattaName, buyZoneRadius, + series.getName(), raceColumn, entriesPerFleet); } result.addAll(getRegattaOverviewEntriesToBeShown(showOnlyCurrentlyRunningRaces, entriesPerFleet)); } @@ -4267,8 +4279,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S Distance buyZoneRadius = RegattaUtil.getCalculatedRegattaBuoyZoneRadius(null, boatClass); Map> entriesPerFleet = new HashMap>(); for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - getRegattaOverviewEntries(showOnlyRacesOfSameDay, dayToCheck, usedCourseArea, - leaderboard, boatClass == null ? "" : boatClass.getName(), regattaName, buyZoneRadius, LeaderboardNameConstants.DEFAULT_SERIES_NAME, raceColumn, entriesPerFleet); + getRegattaOverviewEntries(showOnlyRacesOfSameDay, clientTimeZoneOffset, dayToCheck, usedCourseArea, + leaderboard, boatClass == null ? "" : boatClass.getName(), regattaName, buyZoneRadius, + LeaderboardNameConstants.DEFAULT_SERIES_NAME, raceColumn, entriesPerFleet); } result.addAll(getRegattaOverviewEntriesToBeShown(showOnlyCurrentlyRunningRaces, entriesPerFleet)); } @@ -4384,13 +4397,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public List getRaceStateEntriesForRaceGroup(UUID eventId, List visibleCourseAreaIds, - List visibleRegattas, boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay) + List visibleRegattas, boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, Duration clientTimeZoneOffset) throws NoWindException, InterruptedException, ExecutionException { List result = new ArrayList(); - Calendar dayToCheck = Calendar.getInstance(); dayToCheck.setTime(new Date()); - Event event = getService().getEvent(eventId); if (event != null) { for (CourseArea courseArea : event.getVenue().getCourseAreas()) { @@ -4399,7 +4410,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final CourseArea leaderboardDefaultCourseArea = leaderboard.getDefaultCourseArea(); if (leaderboardDefaultCourseArea != null && leaderboardDefaultCourseArea.equals(courseArea)) { result.addAll(getRaceStateEntriesForLeaderboard(leaderboard.getName(), - showOnlyCurrentlyRunningRaces, showOnlyRacesOfSameDay, visibleRegattas)); + showOnlyCurrentlyRunningRaces, showOnlyRacesOfSameDay, clientTimeZoneOffset, visibleRegattas)); } } } @@ -4408,14 +4419,18 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return result; } - private void getRegattaOverviewEntries(boolean showOnlyRacesOfSameDay, Calendar dayToCheck, - CourseArea courseArea, Leaderboard leaderboard, String boatClassName, String regattaName, Distance buyZoneRadius, String seriesName, RaceColumn raceColumn, + /** + * The client's day starts at 00:00:00Z - clientTimeZoneOffset and ends at 23:59:59Z - clientTimeZoneOffset. + */ + private void getRegattaOverviewEntries(boolean showOnlyRacesOfSameDay, Duration clientTimeZoneOffset, + Calendar dayToCheck, CourseArea courseArea, Leaderboard leaderboard, String boatClassName, + String regattaName, Distance buyZoneRadius, String seriesName, RaceColumn raceColumn, Map> entriesPerFleet) { if (!raceColumn.isCarryForward()) { for (Fleet fleet : raceColumn.getFleets()) { RegattaOverviewEntryDTO entry = createRegattaOverviewEntryDTO(courseArea, leaderboard, boatClassName, regattaName, buyZoneRadius, seriesName, raceColumn, fleet, - showOnlyRacesOfSameDay, dayToCheck); + showOnlyRacesOfSameDay, clientTimeZoneOffset, dayToCheck); if (entry != null) { addRegattaOverviewEntryToEntriesPerFleet(entriesPerFleet, fleet, entry); } @@ -4463,9 +4478,13 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } entriesPerFleet.get(fleet.getName()).add(entry); } - - private RegattaOverviewEntryDTO createRegattaOverviewEntryDTO(CourseArea courseArea, Leaderboard leaderboard, String boatClassName, - String regattaName, Distance buyZoneRadius, String seriesName, RaceColumn raceColumn, Fleet fleet, boolean showOnlyRacesOfSameDay, Calendar dayToCheck) { + + /** + * The client's day starts at 00:00:00Z - clientTimeZoneOffset and ends at 23:59:59Z - clientTimeZoneOffset. + */ + private RegattaOverviewEntryDTO createRegattaOverviewEntryDTO(CourseArea courseArea, Leaderboard leaderboard, + String boatClassName, String regattaName, Distance buyZoneRadius, String seriesName, RaceColumn raceColumn, + Fleet fleet, boolean showOnlyRacesOfSameDay, Duration clientTimeZoneOffset, Calendar dayToCheck) { RegattaOverviewEntryDTO entry = new RegattaOverviewEntryDTO(); if (courseArea != null) { entry.courseAreaName = courseArea.getName(); @@ -4481,7 +4500,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S entry.currentServerTime = new Date(); entry.buyZoneRadius = buyZoneRadius; if (showOnlyRacesOfSameDay) { - if (!RaceStateOfSameDayHelper.isRaceStateOfSameDay(entry.raceInfo.startTime, entry.raceInfo.finishedTime, entry.raceInfo.abortingTimeInPassBefore, dayToCheck)) { + if (!RaceStateOfSameDayHelper.isRaceStateOfSameDay(entry.raceInfo.startTime, entry.raceInfo.finishedTime, + entry.raceInfo.abortingTimeInPassBefore, dayToCheck, clientTimeZoneOffset)) { entry = null; } } @@ -4493,7 +4513,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S try { getReplicationService().stopToReplicateFromMaster(); } catch (IOException e) { - e.printStackTrace(); + logger.log(Level.SEVERE, "Exception trying to stop replicating from master", e); throw new RuntimeException(e); } } @@ -4501,9 +4521,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void stopAllReplicas() { try { - getReplicationService().stopAllReplica(); + getReplicationService().stopAllReplicas(); } catch (IOException e) { - e.printStackTrace(); + logger.log(Level.SEVERE, "Exception trying to stop all replicas from receiving updates from this master", e); throw new RuntimeException(e); } } @@ -4511,11 +4531,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void stopSingleReplicaInstance(String identifier) { UUID uuid = UUID.fromString(identifier); - ReplicaDescriptor replicaDescriptor = new ReplicaDescriptor(null, uuid, ""); try { - getReplicationService().unregisterReplica(replicaDescriptor); + getReplicationService().unregisterReplica(uuid); } catch (IOException e) { - e.printStackTrace(); + logger.log(Level.SEVERE, "Exception trying to unregister replica with UUID "+uuid, e); throw new RuntimeException(e); } } @@ -4748,35 +4767,74 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } @Override - public CompetitorDTO addOrUpdateCompetitor(CompetitorDTO competitor) throws URISyntaxException { - Competitor existingCompetitor = getService().getCompetitorStore().getExistingCompetitorByIdAsString(competitor.getIdAsString()); - Nationality nationality = (competitor.getThreeLetterIocCountryCode() == null || competitor.getThreeLetterIocCountryCode().isEmpty()) ? null : - getBaseDomainFactory().getOrCreateNationality(competitor.getThreeLetterIocCountryCode()); - final CompetitorDTO result; - // new competitor - if (competitor.getIdAsString() == null || competitor.getIdAsString().isEmpty() || existingCompetitor == null) { - BoatClass boatClass = getBaseDomainFactory().getOrCreateBoatClass(competitor.getBoatClass().getName()); - DynamicPerson sailor = new PersonImpl(competitor.getName(), nationality, null, null); - DynamicTeam team = new TeamImpl(competitor.getName() + " team", Collections.singleton(sailor), null); - DynamicBoat boat = new BoatImpl(competitor.getName() + " boat", boatClass, competitor.getSailID()); - result = getBaseDomainFactory().convertToCompetitorDTO( - getBaseDomainFactory().getOrCreateCompetitor(UUID.randomUUID(), competitor.getName(), - competitor.getColor(), competitor.getEmail(), - competitor.getFlagImageURL() == null ? null : new URI(competitor.getFlagImageURL()), team, boat, - competitor.getTimeOnTimeFactor(), - competitor.getTimeOnDistanceAllowancePerNauticalMile(), competitor.getSearchTag())); - } else { - result = getBaseDomainFactory().convertToCompetitorDTO( - getService().apply( - new UpdateCompetitor(competitor.getIdAsString(), competitor.getName(), competitor - .getColor(), competitor.getEmail(), competitor.getSailID(), nationality, - competitor.getImageURL() == null ? null : new URI(competitor.getImageURL()), - competitor.getFlagImageURL() == null ? null : new URI(competitor.getFlagImageURL()), - competitor.getTimeOnTimeFactor(), - competitor.getTimeOnDistanceAllowancePerNauticalMile(), - competitor.getSearchTag()))); + public List addOrUpdateCompetitor(List competitors) throws URISyntaxException { + final List results = new ArrayList<>(); + for (final CompetitorDTO competitor : competitors) { + Competitor existingCompetitor = getService().getCompetitorStore().getExistingCompetitorByIdAsString(competitor.getIdAsString()); + Nationality nationality = (competitor.getThreeLetterIocCountryCode() == null + || competitor.getThreeLetterIocCountryCode().isEmpty()) ? null + : getBaseDomainFactory().getOrCreateNationality(competitor.getThreeLetterIocCountryCode()); + final CompetitorDTO result; + // new competitor + if (competitor.getIdAsString() == null || competitor.getIdAsString().isEmpty() || existingCompetitor == null) { + BoatClass boatClass = getBaseDomainFactory().getOrCreateBoatClass(competitor.getBoatClass().getName()); + DynamicPerson sailor = new PersonImpl(competitor.getName(), nationality, null, null); + DynamicTeam team = new TeamImpl(competitor.getName() + " team", Collections.singleton(sailor), null); + DynamicBoat boat = new BoatImpl(competitor.getName() + " boat", boatClass, competitor.getSailID()); + result = getBaseDomainFactory().convertToCompetitorDTO( + getBaseDomainFactory().getOrCreateCompetitor(UUID.randomUUID(), competitor.getName(), + competitor.getColor(), competitor.getEmail(), + competitor.getFlagImageURL() == null ? null : new URI(competitor.getFlagImageURL()), team, boat, + competitor.getTimeOnTimeFactor(), + competitor.getTimeOnDistanceAllowancePerNauticalMile(), competitor.getSearchTag())); + } else { + result = getBaseDomainFactory().convertToCompetitorDTO( + getService().apply( + new UpdateCompetitor(competitor.getIdAsString(), competitor.getName(), competitor + .getColor(), competitor.getEmail(), competitor.getSailID(), nationality, + competitor.getImageURL() == null ? null : new URI(competitor.getImageURL()), + competitor.getFlagImageURL() == null ? null : new URI(competitor.getFlagImageURL()), + competitor.getTimeOnTimeFactor(), + competitor.getTimeOnDistanceAllowancePerNauticalMile(), + competitor.getSearchTag()))); + } + results.add(result); } - return result; + return results; + } + + @Override + public List addCompetitors(List competitorDescriptors, String searchTag) throws URISyntaxException { + List competitorsForSaving = new ArrayList<>(); + for (final CompetitorDescriptor competitorDescriptor : competitorDescriptors) { + Competitor competitor = convertCompetitorDescriptorToCompetitor(competitorDescriptor, searchTag); + competitorsForSaving.add(competitor); + } + getBaseDomainFactory().getCompetitorStore().addCompetitors(competitorsForSaving); + return convertToCompetitorDTOs(competitorsForSaving); + } + + /** + * Creates a new {@link Competitor} object from a {@link CompetitorDescriptor} with a new random {@link UUID}. + * + * @param searchTag + * set as the {@link Competitor#getSearchTag() searchTag} property of all new competitors + */ + private Competitor convertCompetitorDescriptorToCompetitor(CompetitorDescriptor competitorDescriptor, String searchTag) throws URISyntaxException { + Nationality nationality = (competitorDescriptor.getCountryCode() == null + || competitorDescriptor.getCountryCode().getThreeLetterIOCCode() == null + || competitorDescriptor.getCountryCode().getThreeLetterIOCCode().isEmpty()) ? null + : getBaseDomainFactory().getOrCreateNationality(competitorDescriptor.getCountryCode().getThreeLetterIOCCode()); + BoatClass boatClass = getBaseDomainFactory().getOrCreateBoatClass(competitorDescriptor.getBoatClassName()); + DynamicPerson sailor = new PersonImpl(competitorDescriptor.getName(), nationality, null, null); + DynamicTeam team = new TeamImpl(competitorDescriptor.getName(), Collections.singleton(sailor), null); + DynamicBoat boat = new BoatImpl(competitorDescriptor.getSailNumber(), boatClass, competitorDescriptor.getSailNumber()); + Competitor competitor = new CompetitorImpl(UUID.randomUUID(), competitorDescriptor.getName(), + /* color */ null, /* eMail */ null, + /* flag image */ null, team, + boat, competitorDescriptor.getTimeOnTimeFactor(), + competitorDescriptor.getTimeOnDistanceAllowancePerNauticalMile(), searchTag); + return competitor; } @Override @@ -5638,19 +5696,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S Leaderboard leaderboard = getLeaderboardByName(leaderboardName); RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); Fleet fleet = raceColumn.getFleetByName(fleetName); - final RaceHandle raceHandle = getRaceLogTrackingAdapter().startTracking(getService(), leaderboard, raceColumn, fleet); - if (raceHandle != null && trackWind) { - new Thread("Wind tracking starter for race " + leaderboardName + "/" + raceColumnName + "/" + fleetName) { - public void run() { - try { - startTrackingWind(raceHandle, correctWindByDeclination, - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }.start(); - } + getRaceLogTrackingAdapter().startTracking(getService(), leaderboard, raceColumn, fleet, trackWind, correctWindByDeclination); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/CompetitorProviderDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/CompetitorProviderDTO.java new file mode 100644 index 00000000000..537854dd2e0 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/CompetitorProviderDTO.java @@ -0,0 +1,49 @@ +package com.sap.sailing.gwt.ui.shared; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import com.sap.sailing.domain.common.dto.NamedDTO; + +public class CompetitorProviderDTO extends NamedDTO { + private static final long serialVersionUID = -8911619278385485713L; + + private Map> hasCompetitorsForRegattasInEvent; + + public CompetitorProviderDTO() { + } + + public CompetitorProviderDTO(String name, Map> hasCompetitorsForRegattasInEvent) { + super(name); + this.hasCompetitorsForRegattasInEvent = hasCompetitorsForRegattasInEvent; + } + + public Map> getHasCompetitorsForRegattasInEvent() { + return hasCompetitorsForRegattasInEvent; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(hasCompetitorsForRegattasInEvent); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + if (!super.equals(obj)) { + return false; + } + CompetitorProviderDTO other = (CompetitorProviderDTO) obj; + return Objects.equals(hasCompetitorsForRegattasInEvent, other.hasCompetitorsForRegattasInEvent); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/MarkVectorGraphicsFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/MarkVectorGraphicsFactory.java index cfb6aa0acce..35c1a7b4e01 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/MarkVectorGraphicsFactory.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/MarkVectorGraphicsFactory.java @@ -7,20 +7,24 @@ public class MarkVectorGraphicsFactory { public MarkVectorGraphics getMarkVectorGraphics(MarkDTO markDTO) { final MarkType markType = markDTO.type; final MarkVectorGraphics result; - switch (markType) { - case BUOY: + if (markType == null) { result = createBuoyMarkVectorGraphics(markDTO); - break; - case STARTBOAT: - case FINISHBOAT: - result = new BoatMarkVectorGraphics(markDTO.type, markDTO.color, markDTO.shape, markDTO.pattern, markDTO.getIdAsString()); - break; - case LANDMARK: - result = new LandMarkVectorGraphics(markDTO.type, markDTO.color, markDTO.shape, markDTO.pattern); - break; - default: - result = createBuoyMarkVectorGraphics(markDTO); - break; + } else { + switch (markType) { + case BUOY: + result = createBuoyMarkVectorGraphics(markDTO); + break; + case STARTBOAT: + case FINISHBOAT: + result = new BoatMarkVectorGraphics(markDTO.type, markDTO.color, markDTO.shape, markDTO.pattern, markDTO.getIdAsString()); + break; + case LANDMARK: + result = new LandMarkVectorGraphics(markDTO.type, markDTO.color, markDTO.shape, markDTO.pattern); + break; + default: + result = createBuoyMarkVectorGraphics(markDTO); + break; + } } return result; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/InSightAppNotes.html b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/InSightAppNotes.html index a61686eb475..e2327871086 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/InSightAppNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/InSightAppNotes.html @@ -4,6 +4,11 @@

    What's New - SAP Sail InSight (Android)

    +
    November 2016
    +
      +
    • Improved tracking status and accuracy reporting
    • +
    +
    October 2016
    • Improved tracking reliability and accuracy, diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test.json.txt b/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test.json.txt new file mode 100644 index 00000000000..7eba1000800 --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test.json.txt @@ -0,0 +1 @@ +{"Id":"17733a60-305c-4142-98ef-40423449f3be","Name":"IDJM 2015 - 420er","XrrUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/17733a60-305c-4142-98ef-40423449f3be/regatta","Regattas":[{"Id":"cd23aa66-81bc-4ae3-ba36-fa03446762b1","Name":"420","Gender":"Open","ClassName":"420","PdfUrl":"","XrrEntriesUrl":"file:./bin/VSaW_420_Test.xml","XrrPreliminaryUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/17733a60-305c-4142-98ef-40423449f3be/regatta/cd23aa66-81bc-4ae3-ba36-fa03446762b1?xrrType=PreliminaryResults","XrrFinalUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/17733a60-305c-4142-98ef-40423449f3be/regatta/cd23aa66-81bc-4ae3-ba36-fa03446762b1?xrrType=FinalResults","HtmlUrl":"http://manage2sail.com/en-US/Home/ResultsFragment/event/17733a60-305c-4142-98ef-40423449f3be/regatta/cd23aa66-81bc-4ae3-ba36-fa03446762b1"}]} \ No newline at end of file diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test.xml b/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test.xml new file mode 100644 index 00000000000..41f2c54b193 --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test.xml @@ -0,0 +1,830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test_EmptyDivision.json.txt b/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test_EmptyDivision.json.txt new file mode 100644 index 00000000000..49ebe47b41c --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test_EmptyDivision.json.txt @@ -0,0 +1 @@ +{"Id":"17733a60-305c-4142-98ef-40423449f3be","Name":"IDJM 2015 - 420er","XrrUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/17733a60-305c-4142-98ef-40423449f3be/regatta","Regattas":[{"Id":"cd23aa66-81bc-4ae3-ba36-fa03446762b1","Name":"420","Gender":"Open","ClassName":"420","PdfUrl":"","XrrEntriesUrl":"file:./bin/VSaW_420_Test_EmptyDivision.xml","XrrPreliminaryUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/17733a60-305c-4142-98ef-40423449f3be/regatta/cd23aa66-81bc-4ae3-ba36-fa03446762b1?xrrType=PreliminaryResults","XrrFinalUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/17733a60-305c-4142-98ef-40423449f3be/regatta/cd23aa66-81bc-4ae3-ba36-fa03446762b1?xrrType=FinalResults","HtmlUrl":"http://manage2sail.com/en-US/Home/ResultsFragment/event/17733a60-305c-4142-98ef-40423449f3be/regatta/cd23aa66-81bc-4ae3-ba36-fa03446762b1"}]} \ No newline at end of file diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test_EmptyDivision.xml b/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test_EmptyDivision.xml new file mode 100644 index 00000000000..e41a5b1d523 --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport.test/resources/VSaW_420_Test_EmptyDivision.xml @@ -0,0 +1,729 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/resources/eventResults.json.txt b/java/com.sap.sailing.manage2sail.resultimport.test/resources/eventResults.json.txt new file mode 100644 index 00000000000..c37cdbec30d --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport.test/resources/eventResults.json.txt @@ -0,0 +1 @@ +{"Id":"58f73e6c-ec07-4655-972c-e5d730f0048e","Name":"YES - Young Europeans Sailing 2013","IsafId":null,"XrrUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta","Regattas":[{"Id":"14a09b88-3d45-43be-be74-cb9083484099","IsafId":"29ER","ExternalId":null,"Name":"29er","ClassName":"29er","PdfUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadReport/event/58f73e6c-ec07-4655-972c-e5d730f0048e/report/6a424c3b-bce4-4a56-a339-567e25a3bce2","XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/14a09b88-3d45-43be-be74-cb9083484099?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/14a09b88-3d45-43be-be74-cb9083484099?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/14a09b88-3d45-43be-be74-cb9083484099?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/14a09b88-3d45-43be-be74-cb9083484099","Published":"2013-06-10T13:37:57.956+02:00","Final":false},{"Id":"71a5fcc2-8d2f-43c8-84ad-2919f788fbbb","IsafId":null,"ExternalId":null,"Name":"29erXX","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/71a5fcc2-8d2f-43c8-84ad-2919f788fbbb?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/71a5fcc2-8d2f-43c8-84ad-2919f788fbbb?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/71a5fcc2-8d2f-43c8-84ad-2919f788fbbb?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/71a5fcc2-8d2f-43c8-84ad-2919f788fbbb","Published":null,"Final":false},{"Id":"a4d93eb6-aac3-4423-ac1e-b761cc98ac09","IsafId":"420","ExternalId":null,"Name":"420","ClassName":"420","PdfUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadReport/event/58f73e6c-ec07-4655-972c-e5d730f0048e/report/93224d94-0350-4c92-97e2-3b203f491bcc","XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/a4d93eb6-aac3-4423-ac1e-b761cc98ac09?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/a4d93eb6-aac3-4423-ac1e-b761cc98ac09?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/a4d93eb6-aac3-4423-ac1e-b761cc98ac09?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/a4d93eb6-aac3-4423-ac1e-b761cc98ac09","Published":"2013-05-28T11:44:06.759+02:00","Final":false},{"Id":"bfe3260d-1ca1-418d-ac92-f1bd17b14df8","IsafId":null,"ExternalId":null,"Name":"470 (German Junior Championship)","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/bfe3260d-1ca1-418d-ac92-f1bd17b14df8?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/bfe3260d-1ca1-418d-ac92-f1bd17b14df8?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/bfe3260d-1ca1-418d-ac92-f1bd17b14df8?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/bfe3260d-1ca1-418d-ac92-f1bd17b14df8","Published":null,"Final":false},{"Id":"bc5b3773-a88b-450b-bac1-fecb79aa836c","IsafId":null,"ExternalId":null,"Name":"505","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/bc5b3773-a88b-450b-bac1-fecb79aa836c?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/bc5b3773-a88b-450b-bac1-fecb79aa836c?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/bc5b3773-a88b-450b-bac1-fecb79aa836c?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/bc5b3773-a88b-450b-bac1-fecb79aa836c","Published":null,"Final":false},{"Id":"360f04fa-d06f-4bae-a858-ee4aa6ca1aa0","IsafId":null,"ExternalId":null,"Name":"Europe","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/360f04fa-d06f-4bae-a858-ee4aa6ca1aa0?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/360f04fa-d06f-4bae-a858-ee4aa6ca1aa0?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/360f04fa-d06f-4bae-a858-ee4aa6ca1aa0?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/360f04fa-d06f-4bae-a858-ee4aa6ca1aa0","Published":null,"Final":false},{"Id":"81856d12-5491-4344-95b7-fcb5a2d021fb","IsafId":null,"ExternalId":null,"Name":"int. 14-Footer","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/81856d12-5491-4344-95b7-fcb5a2d021fb?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/81856d12-5491-4344-95b7-fcb5a2d021fb?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/81856d12-5491-4344-95b7-fcb5a2d021fb?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/81856d12-5491-4344-95b7-fcb5a2d021fb","Published":null,"Final":false},{"Id":"37c09f65-7a18-4f83-baac-460471f5b165","IsafId":null,"ExternalId":null,"Name":"Laser Std. - M (German Junior Championship)","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/37c09f65-7a18-4f83-baac-460471f5b165?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/37c09f65-7a18-4f83-baac-460471f5b165?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/37c09f65-7a18-4f83-baac-460471f5b165?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/37c09f65-7a18-4f83-baac-460471f5b165","Published":null,"Final":false},{"Id":"50a8e62a-e33d-4d6b-8007-908213b15ba0","IsafId":null,"ExternalId":null,"Name":"Laser Std. - W","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/50a8e62a-e33d-4d6b-8007-908213b15ba0?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/50a8e62a-e33d-4d6b-8007-908213b15ba0?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/50a8e62a-e33d-4d6b-8007-908213b15ba0?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/50a8e62a-e33d-4d6b-8007-908213b15ba0","Published":null,"Final":false},{"Id":"fc538447-3516-47d1-aece-9d52058cae76","IsafId":null,"ExternalId":null,"Name":"Laser Rad. - W (German Junior Championship)","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/fc538447-3516-47d1-aece-9d52058cae76?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/fc538447-3516-47d1-aece-9d52058cae76?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/fc538447-3516-47d1-aece-9d52058cae76?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/fc538447-3516-47d1-aece-9d52058cae76","Published":null,"Final":false},{"Id":"c0cf4ab8-e79d-4e72-8006-973c743fd316","IsafId":null,"ExternalId":null,"Name":"Laser Rad. - M","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/c0cf4ab8-e79d-4e72-8006-973c743fd316?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/c0cf4ab8-e79d-4e72-8006-973c743fd316?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/c0cf4ab8-e79d-4e72-8006-973c743fd316?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/c0cf4ab8-e79d-4e72-8006-973c743fd316","Published":null,"Final":false},{"Id":"42230139-32e8-4b54-aa05-f89da352f351","IsafId":null,"ExternalId":null,"Name":"Laser 4.7","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/42230139-32e8-4b54-aa05-f89da352f351?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/42230139-32e8-4b54-aa05-f89da352f351?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/42230139-32e8-4b54-aa05-f89da352f351?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/42230139-32e8-4b54-aa05-f89da352f351","Published":null,"Final":false},{"Id":"c41d591d-ff2a-4de4-be68-0be86816bc1d","IsafId":null,"ExternalId":null,"Name":"Pirat","ClassName":"Pirate","PdfUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadReport/event/58f73e6c-ec07-4655-972c-e5d730f0048e/report/17003ef7-119d-44d7-bd09-ade3c241a879","XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/c41d591d-ff2a-4de4-be68-0be86816bc1d?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/c41d591d-ff2a-4de4-be68-0be86816bc1d?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/c41d591d-ff2a-4de4-be68-0be86816bc1d?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/c41d591d-ff2a-4de4-be68-0be86816bc1d","Published":"2013-06-06T16:45:16.257+02:00","Final":false},{"Id":"21af6a6c-3128-48d0-afa7-479b627a575d","IsafId":null,"ExternalId":null,"Name":"Coachboat","ClassName":null,"PdfUrl":null,"XrrEntriesUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/21af6a6c-3128-48d0-afa7-479b627a575d?xrrType=Entries","XrrPreliminaryUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/21af6a6c-3128-48d0-afa7-479b627a575d?xrrType=PreliminaryResults","XrrFinalUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/Home/DownloadXrr/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/21af6a6c-3128-48d0-afa7-479b627a575d?xrrType=FinalResults","HtmlUrl":"https://orm.manage2sail.com/shadowtest_info/en-US/resultsFragment/event/58f73e6c-ec07-4655-972c-e5d730f0048e/regatta/21af6a6c-3128-48d0-afa7-479b627a575d","Published":null,"Final":false}]} \ No newline at end of file diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/AbstractEventResultJsonServiceTest.java b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/AbstractEventResultJsonServiceTest.java new file mode 100644 index 00000000000..b2b0d76d5cd --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/AbstractEventResultJsonServiceTest.java @@ -0,0 +1,21 @@ +package com.sap.sailing.manage2sail.resultimport.test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class AbstractEventResultJsonServiceTest { + protected static final String EVENT_RESULTS_JSON = "eventResults.json"; + + protected static final String RESOURCES = "resources/"; + + protected InputStream getInputStream(String filename) throws FileNotFoundException, IOException { + return new FileInputStream(getFile(filename)); + } + + private File getFile(String filename) { + return new File(RESOURCES + filename); + } +} diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java new file mode 100644 index 00000000000..08212e527eb --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java @@ -0,0 +1,102 @@ +package com.sap.sailing.manage2sail.resultimport.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; + +import javax.xml.bind.JAXBException; + +import org.junit.Test; + +import com.sap.sailing.competitorimport.CompetitorProvider; +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sailing.manage2sail.RegattaResultDescriptor; +import com.sap.sailing.manage2sail.resultimport.AbstractManage2SailProvider; +import com.sap.sailing.manage2sail.resultimport.CompetitorDocumentProvider; +import com.sap.sailing.manage2sail.resultimport.CompetitorImporter; +import com.sap.sailing.resultimport.ResultUrlRegistry; +import com.sap.sailing.xrr.resultimport.ParserFactory; +import com.sap.sse.common.Util; + +/** + * The Bundle URL connection implementation is really stupid; it doesn't recognize a content type for files exposed by + * a "bundleresource://" URL. We need to disguise the JSON documents as ".txt" documents for the purpose of this test + * to allow for the bundle resource class loader to find them.

      + * + * Ironically, in an Eclipse-based test environment everything seems fine. The problem occurs when a compiled bundle + * as a JAR file is loaded by an OSGi execution environment, such as during the Maven / Surefire tests. + * + * @author Axel Uhl (d043530) + * + */ +public class CompetitorImportTest extends AbstractEventResultJsonServiceTest { + @Test + public void simpleCompetitorImportTest() throws FileNotFoundException, IOException, JAXBException { + ResultUrlRegistry resultUrlRegistry = mock(ResultUrlRegistry.class); + when(resultUrlRegistry.getResultUrls(AbstractManage2SailProvider.NAME)).thenReturn(Arrays.asList(getClass().getClassLoader().getResource(EVENT_RESULTS_JSON+".txt"))); + final CompetitorImporter competitorImporter = new CompetitorImporter(ParserFactory.INSTANCE, resultUrlRegistry); + assertTrue(competitorImporter.getHasCompetitorsForRegattasInEvent().containsKey("YES - Young Europeans Sailing 2013")); + assertTrue(competitorImporter.getHasCompetitorsForRegattasInEvent().get("YES - Young Europeans Sailing 2013").contains("29er")); + final Iterable competitorDescriptors = competitorImporter.getCompetitorDescriptors("YES - Young Europeans Sailing 2013", null); // get competitors for all regattas in event + assertNotNull(competitorDescriptors); + assertEquals(1270, Util.size(competitorDescriptors)); + final Iterable competitorDescriptors29er = competitorImporter.getCompetitorDescriptors("YES - Young Europeans Sailing 2013", "29er"); // get competitors only for 29er regatta + assertNotNull(competitorDescriptors29er); + assertEquals(134, Util.size(competitorDescriptors29er)); + } + + @Test + public void simpleCompetitorImportTestNoResultsYet() throws FileNotFoundException, IOException, JAXBException { + ResultUrlRegistry resultUrlRegistry = mock(ResultUrlRegistry.class); + when(resultUrlRegistry.getResultUrls(AbstractManage2SailProvider.NAME)).thenReturn(Arrays.asList(getClass().getClassLoader().getResource("VSaW_420_Test.json.txt"))); + final CompetitorProvider competitorImporter = new CompetitorImporter(ParserFactory.INSTANCE, resultUrlRegistry) { + private static final long serialVersionUID = 6450491595924905L; + + @Override + protected CompetitorDocumentProvider getDocumentProvider() { + return new CompetitorDocumentProvider(this) { + @Override + protected URL getDocumentUrlForRegatta(RegattaResultDescriptor regattaResult) { + return getClass().getClassLoader().getResource("VSaW_420_Test.xml"); + } + }; + } + }; + assertTrue(competitorImporter.getHasCompetitorsForRegattasInEvent().containsKey("IDJM 2015 - 420er")); + assertTrue(competitorImporter.getHasCompetitorsForRegattasInEvent().get("IDJM 2015 - 420er").contains("420")); + final Iterable competitorDescriptors = competitorImporter.getCompetitorDescriptors("IDJM 2015 - 420er", "420"); // get competitors for 420 regatta + assertNotNull(competitorDescriptors); + assertEquals(100, Util.size(competitorDescriptors)); + } + + @Test + public void simpleCompetitorImportTestNoResultsYetAndDivisionEmpty() throws FileNotFoundException, IOException, JAXBException { + ResultUrlRegistry resultUrlRegistry = mock(ResultUrlRegistry.class); + when(resultUrlRegistry.getResultUrls(AbstractManage2SailProvider.NAME)).thenReturn(Arrays.asList(getClass().getClassLoader().getResource("VSaW_420_Test_EmptyDivision.json.txt"))); + final CompetitorProvider competitorImporter = new CompetitorImporter(ParserFactory.INSTANCE, resultUrlRegistry) { + private static final long serialVersionUID = -7647297718831584969L; + + @Override + protected CompetitorDocumentProvider getDocumentProvider() { + return new CompetitorDocumentProvider(this) { + @Override + protected URL getDocumentUrlForRegatta(RegattaResultDescriptor regattaResult) { + return getClass().getClassLoader().getResource("VSaW_420_Test_EmptyDivision.xml"); + } + }; + } + }; + assertTrue(competitorImporter.getHasCompetitorsForRegattasInEvent().containsKey("IDJM 2015 - 420er")); + assertTrue(competitorImporter.getHasCompetitorsForRegattasInEvent().get("IDJM 2015 - 420er").contains("420")); + final Iterable competitorDescriptors = competitorImporter.getCompetitorDescriptors("IDJM 2015 - 420er", null); // get competitors for all regattas in event + assertNotNull(competitorDescriptors); + assertEquals(100, Util.size(competitorDescriptors)); + } +} diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/EventResultJsonServiceTest.java b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/EventResultJsonServiceTest.java index 0f022000444..dcad5c4e8d9 100644 --- a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/EventResultJsonServiceTest.java +++ b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/EventResultJsonServiceTest.java @@ -3,37 +3,19 @@ package com.sap.sailing.manage2sail.resultimport.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; import org.junit.Test; import com.sap.sailing.manage2sail.EventResultDescriptor; import com.sap.sailing.manage2sail.Manage2SailEventResultsParserImpl; -public class EventResultJsonServiceTest { - private static final String EVENT_RESULTS_JSON = "eventResults.json"; - - private static final String RESOURCES = "resources/"; - +public class EventResultJsonServiceTest extends AbstractEventResultJsonServiceTest { @Test public void testParsingEventResultsFromJson() throws IOException { Manage2SailEventResultsParserImpl parser = new Manage2SailEventResultsParserImpl(); - EventResultDescriptor eventResult = parser.getEventResult(getInputStream(EVENT_RESULTS_JSON)); assertNotNull(eventResult); - assertEquals(14, eventResult.getRegattaResults().size()); } - - private InputStream getInputStream(String filename) throws FileNotFoundException, IOException { - return new FileInputStream(getFile(filename)); - } - - private File getFile(String filename) { - return new File(RESOURCES + filename); - } } diff --git a/java/com.sap.sailing.manage2sail.resultimport/META-INF/MANIFEST.MF b/java/com.sap.sailing.manage2sail.resultimport/META-INF/MANIFEST.MF index af7d1626b9c..48458425411 100644 --- a/java/com.sap.sailing.manage2sail.resultimport/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.manage2sail.resultimport/META-INF/MANIFEST.MF @@ -14,4 +14,6 @@ Require-Bundle: com.sap.sailing.manage2sail, com.sap.sailing.resultimport, com.sap.sailing.xrr.resultimport, com.sap.sse.common, - com.sap.sailing.xrr.schema + com.sap.sailing.xrr.schema, + com.sap.sailing.domain.shared.android, + com.sap.sailing.competitorimport diff --git a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/AbstractManage2SailProvider.java b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/AbstractManage2SailProvider.java new file mode 100644 index 00000000000..7d1d7fa74e3 --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/AbstractManage2SailProvider.java @@ -0,0 +1,38 @@ +package com.sap.sailing.manage2sail.resultimport; + +import java.net.URL; + +import com.sap.sailing.resultimport.ResultUrlProvider; +import com.sap.sailing.resultimport.ResultUrlRegistry; +import com.sap.sailing.xrr.resultimport.ParserFactory; + +public abstract class AbstractManage2SailProvider implements ResultUrlProvider { + public static final String NAME = "Manage2Sail XRR Result Importer"; + + private final ParserFactory parserFactory; + private final ResultUrlRegistry resultUrlRegistry; + + protected AbstractManage2SailProvider(ParserFactory parserFactory, ResultUrlRegistry resultUrlRegistry) { + super(); + this.parserFactory = parserFactory; + this.resultUrlRegistry = resultUrlRegistry; + } + + protected ParserFactory getParserFactory() { + return parserFactory; + } + + protected ResultUrlRegistry getResultUrlRegistry() { + return resultUrlRegistry; + } + + @Override + public Iterable getUrls() { + return getResultUrlRegistry().getResultUrls(NAME); + } + + @Override + public String getOptionalSampleURL() { + return "http://manage2sail.com/api/public/links/event/d30883d3-2876-4d7e-af49-891af6cbae1b?accesstoken=bDAv8CwsTM94ujZ&mediaType=json"; + } +} diff --git a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/AbstractManage2SailResultDocumentProvider.java b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/AbstractManage2SailResultDocumentProvider.java new file mode 100644 index 00000000000..9fec2c4e571 --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/AbstractManage2SailResultDocumentProvider.java @@ -0,0 +1,63 @@ +package com.sap.sailing.manage2sail.resultimport; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.manage2sail.EventResultDescriptor; +import com.sap.sailing.manage2sail.Manage2SailEventResultsParserImpl; +import com.sap.sailing.manage2sail.RegattaResultDescriptor; +import com.sap.sailing.resultimport.ResultDocumentDescriptor; +import com.sap.sailing.resultimport.ResultUrlProvider; +import com.sap.sailing.resultimport.impl.ResultDocumentDescriptorImpl; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public abstract class AbstractManage2SailResultDocumentProvider { + private final ResultUrlProvider resultUrlProvider; + + public AbstractManage2SailResultDocumentProvider(final ResultUrlProvider resultUrlProvider) { + this.resultUrlProvider = resultUrlProvider; + } + + public Iterable getResultDocumentDescriptors() throws IOException { + List result = new ArrayList<>(); + Manage2SailEventResultsParserImpl parser = new Manage2SailEventResultsParserImpl(); + for (URL url : resultUrlProvider.getUrls()) { + URLConnection eventResultConn = url.openConnection(); + EventResultDescriptor eventResult = parser.getEventResult((InputStream) eventResultConn.getContent()); + addResultsForEvent(result, eventResult); + } + return result; + } + + private void addResultsForEvent(List result, EventResultDescriptor eventResult) + throws IOException { + if (eventResult != null) { + for (RegattaResultDescriptor regattaResult : eventResult.getRegattaResults()) { + // Depending on the regatta type the boat class in the XRR is set or not (in the division tag) + // Olympic boat classes -> IFClassID="2.4M" Title="2.4M open" + // International boat classes -> IFClassID="" Title="Coachboat" + // Open boat classes (ORC) -> IFClassID="" Title="Welcome Race ORC-Club" + // -> therefore we need to take the regatta title as boat class where the IsafID is not available + String boatClass = regattaResult.getIsafId() != null && !regattaResult.getIsafId().isEmpty() ? regattaResult.getIsafId() : regattaResult.getName(); + if (acceptRegatta(regattaResult)) { + final URL resultUrl = getDocumentUrlForRegatta(regattaResult); + if (resultUrl != null) { + URLConnection regattaResultConn = resultUrl.openConnection(); + result.add(new ResultDocumentDescriptorImpl((InputStream) regattaResultConn.getContent(), + resultUrl.toString(), regattaResult.getPublishedAt()==null?null:new MillisecondsTimePoint(regattaResult.getPublishedAt()), + eventResult.getName(), regattaResult.getName(), boatClass, regattaResult + .getCompetitorGenderType())); + } + } + } + } + } + + abstract protected boolean acceptRegatta(RegattaResultDescriptor regattaResult); + + abstract protected URL getDocumentUrlForRegatta(RegattaResultDescriptor regattaResult); +} diff --git a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/Activator.java b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/Activator.java index 1e810ca0f65..fcc37139458 100755 --- a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/Activator.java +++ b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/Activator.java @@ -13,6 +13,7 @@ import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; +import com.sap.sailing.competitorimport.CompetitorProvider; import com.sap.sailing.domain.common.ScoreCorrectionProvider; import com.sap.sailing.resultimport.ResultUrlRegistry; import com.sap.sailing.resultimport.impl.ResultUrlRegistryServiceTrackerCustomizer; @@ -33,6 +34,13 @@ public class Activator implements BundleActivator { ParserFactory.INSTANCE, resultUrlRegistry); return service; } + + @Override + protected CompetitorProvider configureCompetitorProvider(ResultUrlRegistry resultUrlRegistry) { + final CompetitorProvider service = new CompetitorImporter(ParserFactory.INSTANCE, + resultUrlRegistry); + return service; + } }); resultUrlRegistryServiceTracker.open(); diff --git a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/CompetitorDocumentProvider.java b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/CompetitorDocumentProvider.java new file mode 100644 index 00000000000..4968e1b2c7d --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/CompetitorDocumentProvider.java @@ -0,0 +1,29 @@ +package com.sap.sailing.manage2sail.resultimport; + +import java.io.IOException; +import java.net.URL; + +import com.sap.sailing.manage2sail.RegattaResultDescriptor; +import com.sap.sailing.resultimport.ResultDocumentDescriptor; +import com.sap.sailing.resultimport.ResultUrlProvider; + +public class CompetitorDocumentProvider extends AbstractManage2SailResultDocumentProvider { + public CompetitorDocumentProvider(ResultUrlProvider resultUrlProvider) { + super(resultUrlProvider); + } + + @Override + public Iterable getResultDocumentDescriptors() throws IOException { + return super.getResultDocumentDescriptors(); + } + + @Override + protected URL getDocumentUrlForRegatta(RegattaResultDescriptor regattaResult) { + return regattaResult.getXrrEntriesUrl(); + } + + @Override + protected boolean acceptRegatta(RegattaResultDescriptor regattaResult) { + return true; // no further requirements; doesn't need to be final nor have an appropriate time point + } +} diff --git a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/CompetitorImporter.java b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/CompetitorImporter.java new file mode 100644 index 00000000000..c87ce223c15 --- /dev/null +++ b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/CompetitorImporter.java @@ -0,0 +1,186 @@ +package com.sap.sailing.manage2sail.resultimport; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javax.xml.bind.JAXBException; + +import com.sap.sailing.competitorimport.CompetitorProvider; +import com.sap.sailing.domain.base.Nationality; +import com.sap.sailing.domain.base.impl.NationalityImpl; +import com.sap.sailing.domain.common.CompetitorDescriptor; +import com.sap.sailing.domain.common.dto.PersonDTO; +import com.sap.sailing.resultimport.ResultDocumentDescriptor; +import com.sap.sailing.resultimport.ResultUrlRegistry; +import com.sap.sailing.xrr.resultimport.Parser; +import com.sap.sailing.xrr.resultimport.ParserFactory; +import com.sap.sailing.xrr.schema.Boat; +import com.sap.sailing.xrr.schema.Division; +import com.sap.sailing.xrr.schema.Event; +import com.sap.sailing.xrr.schema.Person; +import com.sap.sailing.xrr.schema.Race; +import com.sap.sailing.xrr.schema.RaceResult; +import com.sap.sailing.xrr.schema.RegattaResults; +import com.sap.sailing.xrr.schema.SeriesResult; +import com.sap.sailing.xrr.schema.Team; + +public class CompetitorImporter extends AbstractManage2SailProvider implements CompetitorProvider { + private static final long serialVersionUID = 7389956404604333931L; + private static final Logger logger = Logger.getLogger(CompetitorImporter.class.getName()); + private final CompetitorDocumentProvider documentProvider; + + public CompetitorImporter(ParserFactory parserFactory, ResultUrlRegistry resultUrlRegistry) { + super(parserFactory, resultUrlRegistry); + documentProvider = new CompetitorDocumentProvider(this); + } + + @Override + public Map> getHasCompetitorsForRegattasInEvent() throws IOException { + Map> result = new HashMap<>(); + for (ResultDocumentDescriptor resultDocDescr : getDocumentProvider().getResultDocumentDescriptors()) { + final String eventName = resultDocDescr.getEventName(); + final String regattaName = resultDocDescr.getRegattaName(); + Set set = result.get(eventName); + if (set == null) { + set = new HashSet<>(); + result.put(eventName, set); + } + set.add(regattaName); + } + return result; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public Iterable getCompetitorDescriptors(String eventName, String regattaName) throws JAXBException, IOException { + final List result = new ArrayList<>(); + final Map resultsByTeamID = new HashMap<>(); + final Map teamsWithoutRaceAssignments = new HashMap<>(); // keys are the teamID + for (ResultDocumentDescriptor resultDocDescr : getDocumentProvider().getResultDocumentDescriptors()) { + if (resultDocDescr.getEventName().equals(eventName) && + (regattaName == null || regattaName.equals(resultDocDescr.getRegattaName()))) { + final Parser parser = getParserFactory().createParser(resultDocDescr.getInputStream(), resultDocDescr.getEventName()); + try { + final RegattaResults regattaResults = parser.parse(); + // If teams are found outside of a Division context then no boat class would be assigned to the + // competitor. However, if only one Division exists it can be used as default and the boat class + // can therefore be derived from it. This, however, is not clear until the document has been fully + // consumed. So we keep track of the teams found outside of Divisions, and if exactly one division + // is found we create the CompetitorDescriptors as if they were part of that Division; otherwise + // we need to come up with a default boat class instead. + final Set teamsOutsideOfDivision = new HashSet<>(); + Map divisions = new HashMap<>(); + for (Object o : regattaResults.getPersonOrBoatOrTeam()) { + if (o instanceof Team) { + final Team team = (Team) o; + teamsOutsideOfDivision.add(team); + } else if (o instanceof Event) { + Event event = (Event) o; + for (Object eventO : event.getRaceOrDivisionOrRegattaSeriesResult()) { + if (eventO instanceof Division) { + Division division = (Division) eventO; + divisions.put(division, event); + if (regattaName == null || regattaName.equals(division.getTitle())) { + for (Object divisionO : division.getSeriesResultOrRaceResultOrTRResult()) { + String raceID = null; + Team team = null; + if (divisionO instanceof RaceResult) { + RaceResult raceResult = (RaceResult) divisionO; + raceID = raceResult.getRaceID(); + team = parser.getTeam(raceResult.getTeamID()); + } else if (divisionO instanceof SeriesResult) { + SeriesResult seriesResult = (SeriesResult) divisionO; + team = parser.getTeam(seriesResult.getTeamID()); + } + if (team != null) { + final CompetitorDescriptor competitorDescriptor = createCompetitorDescriptor( + team, parser, event, division, raceID); + resultsByTeamID.put(team.getTeamID(), competitorDescriptor); + result.add(competitorDescriptor); + } + } + } + } + } + } + } + for (final Team teamOutsideOfDivision : teamsOutsideOfDivision) { + if (divisions.size() == 1) { // exactly one Division; use as default for teams outside of division that were not assigned to a division later + if (!resultsByTeamID.containsKey(teamOutsideOfDivision.getTeamID())) { + final CompetitorDescriptor competitorDescriptor = createCompetitorDescriptor( + teamOutsideOfDivision, parser, divisions.values().iterator().next(), divisions.keySet().iterator().next(), /* raceID */ null); + resultsByTeamID.put(teamOutsideOfDivision.getTeamID(), competitorDescriptor); + result.add(competitorDescriptor); + } + } else { + teamsWithoutRaceAssignments.put(teamOutsideOfDivision.getTeamID(), + createCompetitorDescriptor(teamOutsideOfDivision, parser, /* event */ null, /* division */ null, /* raceID */ null)); + } + } + } catch (JAXBException e) { + logger.log(Level.WARNING, "Exception trying to read competitors for event "+resultDocDescr.getEventName()+ + ", regatta "+resultDocDescr.getRegattaName()+" from document "+resultDocDescr.getDocumentName(), e); + } + } + } + for (final Map.Entry tid : teamsWithoutRaceAssignments.entrySet()) { + if (!resultsByTeamID.containsKey(tid.getKey())) { + result.add(tid.getValue()); + } + } + return result; + } + + private CompetitorDescriptor createCompetitorDescriptor(Team team, final Parser parser, Event event, + Division division, String raceID) { + final Boat boat = parser.getBoat(team.getBoatID()); + final String sailNumber = boat.getSailNumber(); + final Race race = parser.getRace(raceID); + final Nationality[] teamNationality = new Nationality[] { + // use that of team; if not defined for team, use first nationality of a team member that has one defined + team.getNOC() == null ? null : new NationalityImpl(team.getNOC().name()) + }; + final String boatClassName = parser.getBoatClassName(division); + List persons = team.getCrew().stream().sorted((c1, c2) -> -c1.getPosition().name().compareTo(c2.getPosition().name())).map((crew)->{ + Person xrrPerson = parser.getPerson(crew.getPersonID()); + String name = xrrPerson.getGivenName()+" "+xrrPerson.getFamilyName(); + final Nationality nationality; + if (xrrPerson.getNOC() == null) { + nationality = null; + } else { + nationality = new NationalityImpl(xrrPerson.getNOC().name()); + if (teamNationality[0] == null) { + teamNationality[0] = nationality; + } + } + PersonDTO person = new PersonDTO( + name, /* dateOfBirth */ null, /* description */ xrrPerson.getGender()==null?null:xrrPerson.getGender().name(), + nationality==null?null:nationality.getCountryCode().getThreeLetterIOCCode()); + return person; + }).collect(Collectors.toList()); + final CompetitorDescriptor competitorDescriptor = new CompetitorDescriptor( + event==null?null:event.getTitle(), + division==null?null:(division.getTitle() + (division.getGender() == null ? "" : division.getGender().name())), + boatClassName, race != null ? race.getRaceName() : null, /* fleetName */ null, sailNumber, + team.getTeamName(), team.getTeamName(), boat.getBoatName(), + teamNationality[0] == null ? null : teamNationality[0].getCountryCode(), + persons, /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null); + return competitorDescriptor; + } + + protected CompetitorDocumentProvider getDocumentProvider() { + return documentProvider; + } +} diff --git a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/Manage2SailResultDocumentProvider.java b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/Manage2SailResultDocumentProvider.java index f3a71db39eb..038f6a05ebf 100644 --- a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/Manage2SailResultDocumentProvider.java +++ b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/Manage2SailResultDocumentProvider.java @@ -1,59 +1,40 @@ package com.sap.sailing.manage2sail.resultimport; import java.io.IOException; -import java.io.InputStream; import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.List; -import com.sap.sailing.manage2sail.EventResultDescriptor; -import com.sap.sailing.manage2sail.Manage2SailEventResultsParserImpl; import com.sap.sailing.manage2sail.RegattaResultDescriptor; import com.sap.sailing.resultimport.ResultDocumentDescriptor; import com.sap.sailing.resultimport.ResultDocumentProvider; import com.sap.sailing.resultimport.ResultUrlProvider; -import com.sap.sailing.resultimport.impl.ResultDocumentDescriptorImpl; -import com.sap.sse.common.impl.MillisecondsTimePoint; -public class Manage2SailResultDocumentProvider implements ResultDocumentProvider { - private final ResultUrlProvider resultUrlProvider; +public class Manage2SailResultDocumentProvider extends AbstractManage2SailResultDocumentProvider implements ResultDocumentProvider { public Manage2SailResultDocumentProvider(final ResultUrlProvider resultUrlProvider) { - this.resultUrlProvider = resultUrlProvider; + super(resultUrlProvider); } @Override public Iterable getResultDocumentDescriptors() throws IOException { - List result = new ArrayList<>(); - Manage2SailEventResultsParserImpl parser = new Manage2SailEventResultsParserImpl(); - for (URL url : resultUrlProvider.getUrls()) { - URLConnection eventResultConn = url.openConnection(); - EventResultDescriptor eventResult = parser.getEventResult((InputStream) eventResultConn.getContent()); - if (eventResult != null) { - for (RegattaResultDescriptor regattaResult : eventResult.getRegattaResults()) { - // Depending on the regatta type the boat class in the XRR is set or not (in the division tag) - // Olympic boat classes -> IFClassID="2.4M" Title="2.4M open" - // International boat classes -> IFClassID="" Title="Coachboat" - // Open boat classes (ORC) -> IFClassID="" Title="Welcome Race ORC-Club" - // -> therefore we need to take the regatta title as boat class where the IsafID is not available - String boatClass = regattaResult.getIsafId() != null && !regattaResult.getIsafId().isEmpty() ? regattaResult.getIsafId() : regattaResult.getName(); - if (regattaResult.getIsFinal() != null && regattaResult.getPublishedAt() != null) { - if (regattaResult.getIsFinal() && regattaResult.getXrrFinalUrl() != null) { - URLConnection regattaResultConn = regattaResult.getXrrFinalUrl().openConnection(); - result.add(new ResultDocumentDescriptorImpl((InputStream) regattaResultConn.getContent(), - regattaResult.getXrrPreliminaryUrl().toString(), new MillisecondsTimePoint(regattaResult.getPublishedAt()), - eventResult.getName(), regattaResult.getName(), boatClass, regattaResult.getCompetitorGenderType())); - } else if (regattaResult.getXrrPreliminaryUrl() != null) { - URLConnection regattaResultConn = regattaResult.getXrrPreliminaryUrl().openConnection(); - result.add(new ResultDocumentDescriptorImpl((InputStream) regattaResultConn.getContent(), - regattaResult.getXrrPreliminaryUrl().toString(), new MillisecondsTimePoint(regattaResult.getPublishedAt()), - eventResult.getName(), regattaResult.getName(), boatClass, regattaResult.getCompetitorGenderType())); - } - } - } - } - } - return result; + return super.getResultDocumentDescriptors(); } + + @Override + protected URL getDocumentUrlForRegatta(RegattaResultDescriptor regattaResult) { + final URL resultUrl; + if (regattaResult.getIsFinal() && regattaResult.getXrrFinalUrl() != null) { + resultUrl = regattaResult.getXrrFinalUrl(); + } else if (regattaResult.getXrrPreliminaryUrl() != null) { + resultUrl = regattaResult.getXrrPreliminaryUrl(); + } else { + resultUrl = null; + } + return resultUrl; + } + + @Override + protected boolean acceptRegatta(RegattaResultDescriptor regattaResult) { + return regattaResult.getIsFinal() != null && regattaResult.getPublishedAt() != null; + } + } diff --git a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/ScoreCorrectionProviderImpl.java b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/ScoreCorrectionProviderImpl.java index 7d779cc658d..49a12e74f0a 100755 --- a/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/ScoreCorrectionProviderImpl.java +++ b/java/com.sap.sailing.manage2sail.resultimport/src/com/sap/sailing/manage2sail/resultimport/ScoreCorrectionProviderImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.manage2sail.resultimport; import java.io.IOException; -import java.net.URL; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -17,7 +16,6 @@ import com.sap.sailing.domain.common.RegattaScoreCorrections; import com.sap.sailing.domain.common.ScoreCorrectionProvider; import com.sap.sailing.resultimport.ResultDocumentDescriptor; import com.sap.sailing.resultimport.ResultDocumentProvider; -import com.sap.sailing.resultimport.ResultUrlProvider; import com.sap.sailing.resultimport.ResultUrlRegistry; import com.sap.sailing.xrr.resultimport.Parser; import com.sap.sailing.xrr.resultimport.ParserFactory; @@ -29,26 +27,20 @@ import com.sap.sailing.xrr.schema.RegattaResults; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; -public class ScoreCorrectionProviderImpl implements ScoreCorrectionProvider, ResultUrlProvider { +public class ScoreCorrectionProviderImpl extends AbstractManage2SailProvider implements ScoreCorrectionProvider { private static final Logger logger = Logger.getLogger(ScoreCorrectionProviderImpl.class.getName()); private static final long serialVersionUID = 222663322974305822L; - public static final String NAME = "Manage2Sail XRR Result Importer"; - - private final ParserFactory parserFactory; - private final ResultUrlRegistry resultUrlRegistry; private final ResultDocumentProvider documentProvider; public ScoreCorrectionProviderImpl(ResultDocumentProvider documentProvider, ParserFactory parserFactory, ResultUrlRegistry resultUrlRegistry) { + super(parserFactory, resultUrlRegistry); this.documentProvider = documentProvider; - this.parserFactory = parserFactory; - this.resultUrlRegistry = resultUrlRegistry; } public ScoreCorrectionProviderImpl(ParserFactory parserFactory, ResultUrlRegistry resultUrlRegistry) { + super(parserFactory, resultUrlRegistry); this.documentProvider = new Manage2SailResultDocumentProvider(this); - this.parserFactory = parserFactory; - this.resultUrlRegistry = resultUrlRegistry; } @Override @@ -87,29 +79,26 @@ public class ScoreCorrectionProviderImpl implements ScoreCorrectionProvider, Res Parser parser = resolveParser(eventName, boatClassName); try { RegattaResults regattaResults = parser.parse(); -// TimePoint timePoint = XRRParserUtil.calculateTimePointForRegattaResults(regattaResults); -// if ((timePoint == null && timePointPublished == null) || (timePoint != null && timePoint.equals(timePointPublished))) { - for (Object o : regattaResults.getPersonOrBoatOrTeam()) { - if (o instanceof Event) { - Event event = (Event) o; - if (event.getTitle().equals(eventName)) { - for (Object eventO : event.getRaceOrDivisionOrRegattaSeriesResult()) { - if (eventO instanceof Division) { - Division division = (Division) eventO; - EventGender divisionGender = division.getGender(); - String divisionBoatClassAndGender = parser.getBoatClassName(division); - if(divisionGender != null) { - divisionBoatClassAndGender += ", " + divisionGender.name(); - } - if (boatClassName.equalsIgnoreCase(divisionBoatClassAndGender) || boatClassName.contains(divisionBoatClassAndGender)) { - return new XRRRegattaResultsAsScoreCorrections(event, division, this, - parser); - } + for (Object o : regattaResults.getPersonOrBoatOrTeam()) { + if (o instanceof Event) { + Event event = (Event) o; + if (event.getTitle().equals(eventName)) { + for (Object eventO : event.getRaceOrDivisionOrRegattaSeriesResult()) { + if (eventO instanceof Division) { + Division division = (Division) eventO; + EventGender divisionGender = division.getGender(); + String divisionBoatClassAndGender = parser.getBoatClassName(division); + if(divisionGender != null) { + divisionBoatClassAndGender += ", " + divisionGender.name(); + } + if (boatClassName.equalsIgnoreCase(divisionBoatClassAndGender) || boatClassName.contains(divisionBoatClassAndGender)) { + return new XRRRegattaResultsAsScoreCorrections(event, division, this, + parser); } } } } -// } + } } } catch (JAXBException e) { logger.info("Parse error during XRR import. Ignoring document " + parser.toString()); @@ -122,24 +111,14 @@ public class ScoreCorrectionProviderImpl implements ScoreCorrectionProvider, Res Parser result = null; for (ResultDocumentDescriptor resultDocDescr : documentProvider.getResultDocumentDescriptors()) { String boatClassAndGenderType = resultDocDescr.getBoatClass(); - if(resultDocDescr.getCompetitorGenderType() != null) { + if (resultDocDescr.getCompetitorGenderType() != null) { boatClassAndGenderType += ", " + resultDocDescr.getCompetitorGenderType().name(); } - if(eventName.equals(resultDocDescr.getEventName()) && boatClassName.equals(boatClassAndGenderType)) { - result = parserFactory.createParser(resultDocDescr.getInputStream(), resultDocDescr.getEventName()); + if (eventName.equals(resultDocDescr.getEventName()) && boatClassName.equals(boatClassAndGenderType)) { + result = getParserFactory().createParser(resultDocDescr.getInputStream(), resultDocDescr.getEventName()); break; } } return result; } - - @Override - public Iterable getUrls() { - return resultUrlRegistry.getResultUrls(NAME); - } - - @Override - public String getOptionalSampleURL() { - return "http://manage2sail.com/api/public/links/event/d30883d3-2876-4d7e-af49-891af6cbae1b?accesstoken=bDAv8CwsTM94ujZ&mediaType=json"; - } } diff --git a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/Manage2SailEventResultsParserImpl.java b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/Manage2SailEventResultsParserImpl.java index 00dc355df69..13739df9aa1 100644 --- a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/Manage2SailEventResultsParserImpl.java +++ b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/Manage2SailEventResultsParserImpl.java @@ -31,7 +31,6 @@ public class Manage2SailEventResultsParserImpl implements Manage2SailEventResult /** * @param is closed before the method returns, also in case of exception - * @throws IOException */ public EventResultDescriptor getEventResult(InputStream is) throws IOException { EventResultDescriptor result = null; diff --git a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RaceResultDescriptor.java b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RaceResultDescriptor.java index a98d3eabf3f..6147ed980a9 100644 --- a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RaceResultDescriptor.java +++ b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RaceResultDescriptor.java @@ -10,71 +10,71 @@ public class RaceResultDescriptor { private String fleetName; private Boolean isTracked; private Date startTime; - - /** Possible states: Scheduled, StartSequence, Racing, Finished, GeneralRecall, Postponed, Abandoned */ - private String status; + + /** Possible states: Scheduled, StartSequence, Racing, Finished, GeneralRecall, Postponed, Abandoned */ + private String status; public Integer getRaceColumnNumber() { - return raceColumnNumber; - } + return raceColumnNumber; + } public void setRaceColumnNumber(Integer raceColumnNumber) { - this.raceColumnNumber = raceColumnNumber; - } - + this.raceColumnNumber = raceColumnNumber; + } + public String getStatus() { - return status; - } - + return status; + } + public void setStatus(String status) { - this.status = status; - } - + this.status = status; + } + public String getSeriesName() { - return seriesName; - } - + return seriesName; + } + public void setSeriesName(String seriesName) { - this.seriesName = seriesName; - } - + this.seriesName = seriesName; + } + public String getFleetName() { - return fleetName; - } - + return fleetName; + } + public void setFleetName(String fleetName) { - this.fleetName = fleetName; - } + this.fleetName = fleetName; + } public String getId() { return id; } - + public void setId(String id) { this.id = id; } - + public String getName() { return name; } - + public void setName(String name) { this.name = name; } - + public Boolean isTracked() { return isTracked; } - + public void setTracked(Boolean isTracked) { this.isTracked = isTracked; } - public Date getStartTime() { - return startTime; - } + public Date getStartTime() { + return startTime; + } - public void setStartTime(Date startTime) { - this.startTime = startTime; - } + public void setStartTime(Date startTime) { + this.startTime = startTime; + } } diff --git a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RegattaResultDescriptor.java b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RegattaResultDescriptor.java index 0d9ded55c56..0031067a739 100644 --- a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RegattaResultDescriptor.java +++ b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RegattaResultDescriptor.java @@ -26,76 +26,99 @@ public class RegattaResultDescriptor { public RegattaResultDescriptor() { raceResults = new ArrayList(); } - + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getIsafId() { return isafId; } + public void setIsafId(String isafId) { this.isafId = isafId; } + public String getExternalId() { return externalId; } + public void setExternalId(String externalId) { this.externalId = externalId; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getClassName() { return className; } + public void setClassName(String className) { this.className = className; } + public URL getPdfUrl() { return pdfUrl; } + public void setPdfUrl(URL pdfUrl) { this.pdfUrl = pdfUrl; } + public URL getXrrPreliminaryUrl() { return xrrPreliminaryUrl; } + public void setXrrPreliminaryUrl(URL xrrPreliminaryUrl) { this.xrrPreliminaryUrl = xrrPreliminaryUrl; } + public URL getXrrFinalUrl() { return xrrFinalUrl; } + public void setXrrFinalUrl(URL xrrFinalUrl) { this.xrrFinalUrl = xrrFinalUrl; } + public URL getHtmlUrl() { return htmlUrl; } + public void setHtmlUrl(URL htmlUrl) { this.htmlUrl = htmlUrl; } + public Date getPublishedAt() { return publishedAt; } + public void setPublishedAt(Date publishedAt) { this.publishedAt = publishedAt; } + public Boolean getIsFinal() { return isFinal; } + public void setIsFinal(Boolean isFinal) { this.isFinal = isFinal; } + public CompetitorGenderType getCompetitorGenderType() { return competitorGenderType; } + public void setCompetitorGenderType(CompetitorGenderType competitorGenderType) { this.competitorGenderType = competitorGenderType; } @@ -108,11 +131,11 @@ public class RegattaResultDescriptor { this.raceResults = raceResults; } - public URL getXrrEntriesUrl() { - return xrrEntriesUrl; - } + public URL getXrrEntriesUrl() { + return xrrEntriesUrl; + } - public void setXrrEntriesUrl(URL xrrEntriesUrl) { - this.xrrEntriesUrl = xrrEntriesUrl; - } + public void setXrrEntriesUrl(URL xrrEntriesUrl) { + this.xrrEntriesUrl = xrrEntriesUrl; + } } diff --git a/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF index fd6cfaa9ae6..be57bdcd808 100644 --- a/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF @@ -28,11 +28,16 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sse.replication, com.sap.sse, com.sap.sse.filestorage, - com.sap.sse.shared.android + com.sap.sse.shared.android, + com.sap.sailing.domain.tractracadapter.persistence, + com.sap.sailing.domain.swisstimingadapter.persistence, + com.sap.sailing.domain.racelogtrackingadapter, + com.sap.sailing.domain.swisstimingreplayadapter Import-Package: com.sap.sailing.domain.common, com.sap.sailing.domain.common.impl, com.sap.sailing.domain.common.racelog, com.sap.sailing.domain.common.racelog.tracking, com.sap.sailing.domain.common.tracking, - com.sap.sailing.domain.common.tracking.impl + com.sap.sailing.domain.common.tracking.impl, + com.sap.sailing.domain.tractracadapter.persistence.impl diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractConnectivityParamsLoadAndStoreTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractConnectivityParamsLoadAndStoreTest.java new file mode 100644 index 00000000000..a36caa478ae --- /dev/null +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractConnectivityParamsLoadAndStoreTest.java @@ -0,0 +1,31 @@ +package com.sap.sailing.mongodb.test; + +import java.net.UnknownHostException; + +import org.junit.Before; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.impl.RacingEventServiceImpl; + +public abstract class AbstractConnectivityParamsLoadAndStoreTest extends AbstractMongoDBTest { + protected MockConnectivityParamsServiceFinderFactory serviceFinderFactory; + protected MongoObjectFactory mongoObjectFactory; + protected DomainObjectFactory domainObjectFactory; + protected RacingEventService racingEventService; + + public AbstractConnectivityParamsLoadAndStoreTest() throws UnknownHostException, MongoException { + super(); + } + + @Before + public void setUp() { + serviceFinderFactory = new MockConnectivityParamsServiceFinderFactory(); + racingEventService = new RacingEventServiceImpl(/* clearPersistentCompetitorStore */ true, serviceFinderFactory, /* restoreTrackedRaces */ false); + serviceFinderFactory.setRacingEventService(racingEventService); + mongoObjectFactory = racingEventService.getMongoObjectFactory(); + domainObjectFactory = racingEventService.getDomainObjectFactory(); + } +} diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractMongoDBTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractMongoDBTest.java index 75f10fdbd1a..ec9a898160d 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractMongoDBTest.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractMongoDBTest.java @@ -18,13 +18,13 @@ public abstract class AbstractMongoDBTest { protected Mongo mongo; protected DB db; private final MongoDBConfiguration dbConfiguration; - private MongoDBService service; + private MongoDBService racingEventService; public AbstractMongoDBTest() throws UnknownHostException, MongoException { dbConfiguration = MongoDBConfiguration.getDefaultTestConfiguration(); - service = getDBConfiguration().getService(); + racingEventService = getDBConfiguration().getService(); mongo = newMongo(); - db = service.getDB(); + db = racingEventService.getDB(); } protected MongoDBConfiguration getDBConfiguration() { @@ -51,6 +51,6 @@ public abstract class AbstractMongoDBTest { } protected MongoDBService getMongoService() { - return service; + return racingEventService; } } diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/MockConnectivityParamsServiceFinderFactory.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/MockConnectivityParamsServiceFinderFactory.java new file mode 100644 index 00000000000..3192cfc3a80 --- /dev/null +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/MockConnectivityParamsServiceFinderFactory.java @@ -0,0 +1,131 @@ +package com.sap.sailing.mongodb.test; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; +import com.sap.sailing.domain.persistence.MongoRegattaLogStoreFactory; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.PlaceHolderDeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.tracking.test.mock.AbstractTypeBasedServiceFinder; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.domain.racelogtracking.impl.RaceLogConnectivityParams; +import com.sap.sailing.domain.swisstimingadapter.DomainFactory; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingAdapterFactoryImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingTrackingConnectivityParameters; +import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayServiceFactory; +import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingReplayConnectivityParameters; +import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingReplayConnectivityParamsHandler; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParametersHandler; +import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory; +import com.sap.sailing.domain.tractracadapter.impl.RaceTrackingConnectivityParametersImpl; +import com.sap.sailing.domain.tractracadapter.persistence.impl.TracTracConnectivityParamsHandler; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.impl.RacingEventServiceImpl; +import com.sap.sse.common.NoCorrespondingServiceRegisteredException; +import com.sap.sse.common.TypeBasedServiceFinder; +import com.sap.sse.common.TypeBasedServiceFinderFactory; + +/** + * A simplified implementation of the {@link TypeBasedServiceFinder} interface that, when the device type + * {@link SmartphoneImeiIdentifier#TYPE} is requested, returns a specific handler that was passed to this object's + * constructor. + * + * @author Fredrik Teschke + * + */ +public class MockConnectivityParamsServiceFinderFactory implements TypeBasedServiceFinderFactory { + Map, TypeBasedServiceFinder> serviceFinders = new HashMap, TypeBasedServiceFinder>(); + private RacingEventService racingEventService; + + public MockConnectivityParamsServiceFinderFactory() { + serviceFinders.put(RaceTrackingConnectivityParametersHandler.class, new AbstractTypeBasedServiceFinder() { + @Override + public RaceTrackingConnectivityParametersHandler findService(String type) + throws NoCorrespondingServiceRegisteredException { + final MongoObjectFactory mongoObjectFactory = racingEventService.getMongoObjectFactory(); + final DomainObjectFactory domainObjectFactory = racingEventService.getDomainObjectFactory(); + final DomainFactory swissTimingDomainFactory = new SwissTimingAdapterFactoryImpl().getOrCreateSwissTimingAdapter(domainObjectFactory.getBaseDomainFactory()).getSwissTimingDomainFactory(); + switch (type) { + case RaceTrackingConnectivityParametersImpl.TYPE: + return new TracTracConnectivityParamsHandler( + MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory), + MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore(mongoObjectFactory, domainObjectFactory), + TracTracAdapterFactory.INSTANCE.getOrCreateTracTracAdapter(domainObjectFactory.getBaseDomainFactory()).getTracTracDomainFactory()); + case SwissTimingTrackingConnectivityParameters.TYPE: + return new com.sap.sailing.domain.swisstimingadapter.persistence.impl.SwissTimingConnectivityParamsHandler( + MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory), + MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore(mongoObjectFactory, domainObjectFactory), + swissTimingDomainFactory); + case SwissTimingReplayConnectivityParameters.TYPE: + return new SwissTimingReplayConnectivityParamsHandler( + MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory), + MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore(mongoObjectFactory, domainObjectFactory), + swissTimingDomainFactory, + SwissTimingReplayServiceFactory.INSTANCE.createSwissTimingReplayService(swissTimingDomainFactory, racingEventService)); + case RaceLogConnectivityParams.TYPE: + return new com.sap.sailing.domain.racelogtracking.impl.RaceLogConnectivityParamsHandler(racingEventService); + default: + return null; + } + } + + @Override + public Set findAllServices() { + final Set result = new HashSet<>(); + for (final String type : new String[] { + RaceTrackingConnectivityParametersImpl.TYPE, + SwissTimingTrackingConnectivityParameters.TYPE, + SwissTimingReplayConnectivityParameters.TYPE, + RaceLogConnectivityParams.TYPE }) { + result.add(findService(type)); + } + return result; + } + + @Override + public void setFallbackService(RaceTrackingConnectivityParametersHandler fallback) { + } + }); + serviceFinders.put(DeviceIdentifierMongoHandler.class, new AbstractTypeBasedServiceFinder() { + @Override + public DeviceIdentifierMongoHandler findService(String type) + throws NoCorrespondingServiceRegisteredException { + switch (type) { + case RaceTrackingConnectivityParametersImpl.TYPE: + return new PlaceHolderDeviceIdentifierMongoHandler(); + default: + return null; + } + } + + @Override + public Set findAllServices() { + return null; + } + + @Override + public void setFallbackService(DeviceIdentifierMongoHandler fallback) { + } + }); + } + + /** + * Must be called right after the constructor invocation when this object has been used to initialize a + * {@link RacingEventServiceImpl} object. + */ + public void setRacingEventService(RacingEventService racingEventService) { + this.racingEventService = racingEventService; + } + + @SuppressWarnings("unchecked") + @Override + public TypeBasedServiceFinder createServiceFinder(Class clazz) { + return (TypeBasedServiceFinder) serviceFinders.get(clazz); + } + +} diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/RaceLogConnectivityParamsLoadAndStoreTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/RaceLogConnectivityParamsLoadAndStoreTest.java new file mode 100644 index 00000000000..1d08976d31a --- /dev/null +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/RaceLogConnectivityParamsLoadAndStoreTest.java @@ -0,0 +1,87 @@ +package com.sap.sailing.mongodb.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.junit.Test; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.CourseAreaImpl; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.racelog.tracking.NotDenotableForRaceLogTrackingException; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; +import com.sap.sailing.domain.racelogtracking.impl.RaceLogConnectivityParams; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sse.common.Duration; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class RaceLogConnectivityParamsLoadAndStoreTest extends AbstractConnectivityParamsLoadAndStoreTest { + public RaceLogConnectivityParamsLoadAndStoreTest() throws UnknownHostException, MongoException { + super(); + } + + @Test + public void testStoreAndLoadRaceLogTrackingParams() throws MalformedURLException, URISyntaxException, NotDenotableForRaceLogTrackingException { + // set up + final long delayToLiveInMillis = 3000; + final boolean trackWind = true; + final boolean correctWindDirectionByMagneticDeclination = true; + final FleetImpl fleet = new FleetImpl("Default"); + final SeriesImpl theSeries = new SeriesImpl("Default", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, + Arrays.asList(fleet), Collections.emptyList(), racingEventService); + final Iterable series = Arrays.asList(theSeries); + final Regatta regatta = racingEventService.createRegatta("My Regatta", "12mR", MillisecondsTimePoint.now(), MillisecondsTimePoint.now().plus(Duration.ONE_DAY), + UUID.randomUUID(), series, /* persistent */ true, new LowPoint(), new CourseAreaImpl("Default", UUID.randomUUID()), + /* buoyZoneRadiusInHullLengths */ 2.0, /* useStartTimeInference */ true, /* controlTrackingFromStartAndFinishTimes */ true, + OneDesignRankingMetric::new); + theSeries.addRaceColumn("R1", racingEventService); + theSeries.addRaceColumn("R2", racingEventService); + theSeries.addRaceColumn("R3", racingEventService); + final RegattaLeaderboard leaderboard = racingEventService.addRegattaLeaderboard(regatta.getRegattaIdentifier(), /* leaderboardDisplayName */ null, new int[] { 5, 9 }); + RaceLogTrackingAdapterFactory.INSTANCE.getAdapter(domainObjectFactory.getBaseDomainFactory()).denoteAllRacesForRaceLogTracking(racingEventService, leaderboard); + final RaceLogConnectivityParams rlParams = new RaceLogConnectivityParams( + racingEventService, regatta, leaderboard.getRaceColumnByName("R2"), fleet, leaderboard, + delayToLiveInMillis, domainObjectFactory.getBaseDomainFactory(), trackWind, + correctWindDirectionByMagneticDeclination); + // store + mongoObjectFactory.addConnectivityParametersForRaceToRestore(rlParams); + // load + final Set connectivityParametersForRacesToRestore = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore.add(params)); + // compare + assertEquals(1, Util.size(connectivityParametersForRacesToRestore)); + final RaceTrackingConnectivityParameters paramsReadFromDB = connectivityParametersForRacesToRestore.iterator().next(); + assertTrue(paramsReadFromDB instanceof RaceLogConnectivityParams); + RaceLogConnectivityParams raceLogParamsReadFromDB = (RaceLogConnectivityParams) paramsReadFromDB; + assertEquals(delayToLiveInMillis, raceLogParamsReadFromDB.getDelayToLiveInMillis()); + assertSame(leaderboard, raceLogParamsReadFromDB.getLeaderboard()); + assertSame(theSeries.getRaceColumnByName("R2"), raceLogParamsReadFromDB.getRaceColumn()); + assertSame(fleet, raceLogParamsReadFromDB.getFleet()); + assertEquals(rlParams.getTrackerID(), raceLogParamsReadFromDB.getTrackerID()); + assertEquals(rlParams.isTrackWind(), raceLogParamsReadFromDB.isTrackWind()); + assertEquals(rlParams.isCorrectWindDirectionByMagneticDeclination(), raceLogParamsReadFromDB.isCorrectWindDirectionByMagneticDeclination()); + // remove again + mongoObjectFactory.removeConnectivityParametersForRaceToRestore(rlParams); + final Set connectivityParametersForRacesToRestore2 = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore2.add(params)); + assertTrue(connectivityParametersForRacesToRestore2.isEmpty()); + } +} diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/SwissTimingConnectivityParamsLoadAndStoreTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/SwissTimingConnectivityParamsLoadAndStoreTest.java new file mode 100644 index 00000000000..006786d2d0e --- /dev/null +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/SwissTimingConnectivityParamsLoadAndStoreTest.java @@ -0,0 +1,132 @@ +package com.sap.sailing.mongodb.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.swisstimingadapter.StartList; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; +import com.sap.sailing.domain.swisstimingadapter.impl.CompetitorWithID; +import com.sap.sailing.domain.swisstimingadapter.impl.CompetitorWithoutID; +import com.sap.sailing.domain.swisstimingadapter.impl.CrewMemberImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.StartListImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingAdapterFactoryImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sse.common.Util; + +public class SwissTimingConnectivityParamsLoadAndStoreTest extends AbstractConnectivityParamsLoadAndStoreTest { + public SwissTimingConnectivityParamsLoadAndStoreTest() throws UnknownHostException, MongoException { + super(); + } + + @Test + public void testStoreAndLoadSwissTimingLiveParamsWithCompetitorsWithIDAndCrew() throws MalformedURLException, URISyntaxException { + final boolean trackWind = true; + final boolean correctWindDirectionByMagneticDeclination = true; + final String hostname = "a.b.com"; + final int port = 1234; + final String raceID = "Race ID 123"; + final String raceName = "The Race"; + final String raceDescription = null; + final BoatClass boatClass = domainObjectFactory.getBaseDomainFactory().getOrCreateBoatClass("49er"); + final CrewMemberImpl c1c1 = new CrewMemberImpl("c1c1", "GER", "Helm"); + final CrewMemberImpl c1c2 = new CrewMemberImpl("c1c2", "POL", "Crew"); + final CrewMemberImpl c2c1 = new CrewMemberImpl("c2c1", "AUS", "Helm"); + final CrewMemberImpl c2c2 = new CrewMemberImpl("c2c2", "NZL", "Crew"); + final CompetitorWithID c1 = new CompetitorWithID("c1", "b1", "GER", "C1", Arrays.asList(c1c1, c1c2)); + final CompetitorWithID c2 = new CompetitorWithID("c2", "b2", "AUS", "C2", Arrays.asList(c2c1, c2c2)); + final StartList startList = new StartListImpl(raceID, Arrays.asList(c1, c2)); + final boolean useInternalMarkPassingAlgorithm = false; + final long delayToLiveInMillis = 30000; + final SwissTimingTrackingConnectivityParameters stParams = new SwissTimingTrackingConnectivityParameters( + hostname, port, raceID, raceName, raceDescription, boatClass, startList, delayToLiveInMillis, + SwissTimingFactory.INSTANCE, new SwissTimingAdapterFactoryImpl().getOrCreateSwissTimingAdapter(domainObjectFactory.getBaseDomainFactory()).getSwissTimingDomainFactory(), + /* raceLogStore */ null, /* regattaLogStore */ null, useInternalMarkPassingAlgorithm, trackWind, correctWindDirectionByMagneticDeclination); + // store + mongoObjectFactory.addConnectivityParametersForRaceToRestore(stParams); + // load + final Set connectivityParametersForRacesToRestore = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore.add(params)); + // compare + assertEquals(1, Util.size(connectivityParametersForRacesToRestore)); + final RaceTrackingConnectivityParameters paramsReadFromDB = connectivityParametersForRacesToRestore.iterator().next(); + assertTrue(paramsReadFromDB instanceof SwissTimingTrackingConnectivityParameters); + SwissTimingTrackingConnectivityParameters stParamsReadFromDB = (SwissTimingTrackingConnectivityParameters) paramsReadFromDB; + assertEquals(delayToLiveInMillis, stParamsReadFromDB.getDelayToLiveInMillis()); + assertEquals(hostname, stParamsReadFromDB.getHostname()); + assertEquals(boatClass, stParamsReadFromDB.getBoatClass()); + assertEquals(port, stParamsReadFromDB.getPort()); + assertEquals(raceDescription, stParamsReadFromDB.getRaceDescription()); + assertEquals(raceID, stParamsReadFromDB.getRaceID()); + assertEquals(raceName, stParamsReadFromDB.getRaceName()); + assertEquals(startList.getRaceID(), stParamsReadFromDB.getStartList().getRaceID()); + assertEquals(startList.getCompetitors(), stParamsReadFromDB.getStartList().getCompetitors()); + assertEquals(stParams.getTrackerID(), stParamsReadFromDB.getTrackerID()); + assertEquals(stParams.isTrackWind(), stParamsReadFromDB.isTrackWind()); + assertEquals(stParams.isCorrectWindDirectionByMagneticDeclination(), stParamsReadFromDB.isCorrectWindDirectionByMagneticDeclination()); + // remove again + mongoObjectFactory.removeConnectivityParametersForRaceToRestore(stParams); + final Set connectivityParametersForRacesToRestore2 = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore2.add(params)); + assertTrue(connectivityParametersForRacesToRestore2.isEmpty()); + } + + @Test + public void testStoreAndLoadSwissTimingLiveParamsWithCompetitorsWithoutID() throws MalformedURLException, URISyntaxException { + final boolean trackWind = false; + final boolean correctWindDirectionByMagneticDeclination = false; + final String hostname = "a.b.com"; + final int port = 1234; + final String raceID = "Race ID 123"; + final String raceName = "The Race"; + final String raceDescription = null; + final BoatClass boatClass = domainObjectFactory.getBaseDomainFactory().getOrCreateBoatClass("49er"); + final CompetitorWithoutID c1 = new CompetitorWithoutID("b1", "GER", "C1"); + final CompetitorWithoutID c2 = new CompetitorWithoutID("b2", "GER", "21"); + final StartList startList = new StartListImpl(raceID, Arrays.asList(c1, c2)); + final boolean useInternalMarkPassingAlgorithm = false; + final long delayToLiveInMillis = 30000; + final SwissTimingTrackingConnectivityParameters stParams = new SwissTimingTrackingConnectivityParameters( + hostname, port, raceID, raceName, raceDescription, boatClass, startList, delayToLiveInMillis, + SwissTimingFactory.INSTANCE, new SwissTimingAdapterFactoryImpl().getOrCreateSwissTimingAdapter(domainObjectFactory.getBaseDomainFactory()).getSwissTimingDomainFactory(), + /* raceLogStore */ null, /* regattaLogStore */ null, useInternalMarkPassingAlgorithm, trackWind, correctWindDirectionByMagneticDeclination); + // store + mongoObjectFactory.addConnectivityParametersForRaceToRestore(stParams); + // load + final Set connectivityParametersForRacesToRestore = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore.add(params)); + // compare + assertEquals(1, Util.size(connectivityParametersForRacesToRestore)); + final RaceTrackingConnectivityParameters paramsReadFromDB = connectivityParametersForRacesToRestore.iterator().next(); + assertTrue(paramsReadFromDB instanceof SwissTimingTrackingConnectivityParameters); + SwissTimingTrackingConnectivityParameters stParamsReadFromDB = (SwissTimingTrackingConnectivityParameters) paramsReadFromDB; + assertEquals(delayToLiveInMillis, stParamsReadFromDB.getDelayToLiveInMillis()); + assertEquals(hostname, stParamsReadFromDB.getHostname()); + assertEquals(boatClass, stParamsReadFromDB.getBoatClass()); + assertEquals(port, stParamsReadFromDB.getPort()); + assertEquals(raceDescription, stParamsReadFromDB.getRaceDescription()); + assertEquals(raceID, stParamsReadFromDB.getRaceID()); + assertEquals(raceName, stParamsReadFromDB.getRaceName()); + assertEquals(startList.getRaceID(), stParamsReadFromDB.getStartList().getRaceID()); + assertEquals(startList.getCompetitors(), stParamsReadFromDB.getStartList().getCompetitors()); + assertEquals(stParams.getTrackerID(), stParamsReadFromDB.getTrackerID()); + assertEquals(stParams.isTrackWind(), stParamsReadFromDB.isTrackWind()); + assertEquals(stParams.isCorrectWindDirectionByMagneticDeclination(), stParamsReadFromDB.isCorrectWindDirectionByMagneticDeclination()); + // remove again + mongoObjectFactory.removeConnectivityParametersForRaceToRestore(stParams); + final Set connectivityParametersForRacesToRestore2 = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore2.add(params)); + assertTrue(connectivityParametersForRacesToRestore2.isEmpty()); + } +} diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/SwissTimingReplayConnectivityParamsLoadAndStoreTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/SwissTimingReplayConnectivityParamsLoadAndStoreTest.java new file mode 100644 index 00000000000..be15ba863ec --- /dev/null +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/SwissTimingReplayConnectivityParamsLoadAndStoreTest.java @@ -0,0 +1,61 @@ +package com.sap.sailing.mongodb.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingAdapterFactoryImpl; +import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingReplayConnectivityParameters; +import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingReplayServiceFactoryImpl; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sse.common.Util; + +public class SwissTimingReplayConnectivityParamsLoadAndStoreTest extends AbstractConnectivityParamsLoadAndStoreTest { + public SwissTimingReplayConnectivityParamsLoadAndStoreTest() throws UnknownHostException, MongoException { + super(); + } + + @Test + public void testStoreAndLoadSwissTimingReplayParams() throws MalformedURLException, URISyntaxException { + final String link = "a.b.com?race=1234&_start=0"; + final String raceID = "Race ID 123"; + final String raceName = "The Race"; + final String boatClassName = "49er"; + final boolean useInternalMarkPassingAlgorithm = false; + final SwissTimingAdapter swissTimingAdapter = new SwissTimingAdapterFactoryImpl().getOrCreateSwissTimingAdapter(domainObjectFactory.getBaseDomainFactory()); + final SwissTimingReplayConnectivityParameters stParams = new SwissTimingReplayConnectivityParameters( + link, raceName, raceID, boatClassName, useInternalMarkPassingAlgorithm, swissTimingAdapter.getSwissTimingDomainFactory(), + new SwissTimingReplayServiceFactoryImpl().createSwissTimingReplayService(swissTimingAdapter.getSwissTimingDomainFactory(), racingEventService), + /* raceLogStore */ null, /* regattaLogStore */ null); + // store + mongoObjectFactory.addConnectivityParametersForRaceToRestore(stParams); + // load + final Set connectivityParametersForRacesToRestore = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore.add(params)); + // compare + assertEquals(1, Util.size(connectivityParametersForRacesToRestore)); + final RaceTrackingConnectivityParameters paramsReadFromDB = connectivityParametersForRacesToRestore.iterator().next(); + assertTrue(paramsReadFromDB instanceof SwissTimingReplayConnectivityParameters); + SwissTimingReplayConnectivityParameters stParamsReadFromDB = (SwissTimingReplayConnectivityParameters) paramsReadFromDB; + assertEquals(link, stParamsReadFromDB.getLink()); + assertEquals(boatClassName, stParamsReadFromDB.getBoatClassName()); + assertEquals(raceID, stParamsReadFromDB.getRaceID()); + assertEquals(raceName, stParamsReadFromDB.getRaceName()); + assertEquals(stParams.getTrackerID(), stParamsReadFromDB.getTrackerID()); + assertEquals(useInternalMarkPassingAlgorithm, stParamsReadFromDB.isUseInternalMarkPassingAlgorithm()); + // remove again + mongoObjectFactory.removeConnectivityParametersForRaceToRestore(stParams); + final Set connectivityParametersForRacesToRestore2 = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore2.add(params)); + assertTrue(connectivityParametersForRacesToRestore2.isEmpty()); + } +} diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java index 3d39b3d70b6..328f135ae8b 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java @@ -396,7 +396,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest @Test public void testLoadStoreSimpleRegattaLeaderboard() { RacingEventService res = new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE); + .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, /* restoreTrackedRaces */ false); final int numberOfQualifyingRaces = 5; final int numberOfFinalRaces = 7; final String regattaBaseName = "Kieler Woche"; @@ -513,7 +513,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest private RacingEventServiceImpl createRacingEventServiceWithOneMockedTrackedRace(final DynamicTrackedRace q2YellowTrackedRace) { return new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE) { + .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, /* restoreTrackedRaces */ false) { @Override public DynamicTrackedRace getExistingTrackedRace(RegattaAndRaceIdentifier raceIdentifier) { return q2YellowTrackedRace; @@ -853,7 +853,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest regatta.addRace(racedef); RacingEventServiceImpl evs = new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE); + .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, /* restoreTrackedRaces */ false); assertNull(evs.getRememberedRegattaForRace(racedef.getId())); evs.raceAdded(regatta, racedef); assertNotNull(evs.getRememberedRegattaForRace(racedef.getId())); diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboardGroups.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboardGroups.java index bfa1136c2a9..136ccccbbff 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboardGroups.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboardGroups.java @@ -318,7 +318,7 @@ public class TestStoringAndRetrievingLeaderboardGroups extends AbstractMongoDBTe // the leaderboard named leaderboardNames[2] occurs in both groups RacingEventService racingEventService = new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(MongoDBService.INSTANCE, DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(MongoDBService.INSTANCE), MediaDBFactory.INSTANCE.getMediaDB(MongoDBService.INSTANCE), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE); // expected to load leaderboard groups + .getMongoObjectFactory(MongoDBService.INSTANCE), MediaDBFactory.INSTANCE.getMediaDB(MongoDBService.INSTANCE), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, /* restoreTrackedRaces */ false); // expected to load leaderboard groups final LeaderboardGroup loadedLeaderboardGroup1 = racingEventService.getLeaderboardGroupByName(groupName1); final LeaderboardGroup loadedLeaderboardGroup2 = racingEventService.getLeaderboardGroupByName(groupName2); diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TracTracConnectivityParamsLoadAndStoreTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TracTracConnectivityParamsLoadAndStoreTest.java new file mode 100644 index 00000000000..c8a664b0def --- /dev/null +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TracTracConnectivityParamsLoadAndStoreTest.java @@ -0,0 +1,84 @@ +package com.sap.sailing.mongodb.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tractracadapter.DomainFactory; +import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants; +import com.sap.sailing.domain.tractracadapter.impl.RaceTrackingConnectivityParametersImpl; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class TracTracConnectivityParamsLoadAndStoreTest extends AbstractConnectivityParamsLoadAndStoreTest { + public TracTracConnectivityParamsLoadAndStoreTest() throws UnknownHostException, MongoException { + super(); + } + + @Test + public void testStoreAndLoadSimpleTracTracParams() throws MalformedURLException, URISyntaxException { + // set up + final boolean trackWind = true; + final boolean correctWindDirectionByMagneticDeclination = true; + final URL paramURL = new URL("http://tractrac.com/some/url"); + final URI storedURI = new URI("live://tractrac.com/storedURI"); + final URI courseDesignUpdateURI = new URI("https://skitrac.dk/reverse/update"); + final TimePoint startOfTracking = MillisecondsTimePoint.now(); + final TimePoint endOfTracking = startOfTracking.plus(1000); + final long delayToLiveInMillis = 3000; + final Duration offsetToStartTimeOfSimulatedRace = Duration.ONE_MINUTE; + final boolean useInternalMarkPassingAlgorithm = false; + final String tracTracUsername = "user"; + final String tracTracPassword = "pass"; + final String raceStatus = (String) TracTracConnectionConstants.REPLAY_STATUS; + final String raceVisibility = (String) TracTracConnectionConstants.REPLAY_VISIBILITY; + final RaceTrackingConnectivityParameters tracTracParams = new RaceTrackingConnectivityParametersImpl( + paramURL, /* live URI */ null, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, + delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, + /* raceLogStore */ null, /* regattaLogStore */ null, DomainFactory.INSTANCE, tracTracUsername, tracTracPassword, + raceStatus, raceVisibility, trackWind, correctWindDirectionByMagneticDeclination); + // store + mongoObjectFactory.addConnectivityParametersForRaceToRestore(tracTracParams); + // load + final Set connectivityParametersForRacesToRestore = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore.add(params)); + // compare + assertEquals(1, Util.size(connectivityParametersForRacesToRestore)); + final RaceTrackingConnectivityParameters paramsReadFromDB = connectivityParametersForRacesToRestore.iterator().next(); + assertTrue(paramsReadFromDB instanceof RaceTrackingConnectivityParametersImpl); + RaceTrackingConnectivityParametersImpl tracTracParamsReadFromDB = (RaceTrackingConnectivityParametersImpl) paramsReadFromDB; + assertEquals(delayToLiveInMillis, tracTracParamsReadFromDB.getDelayToLiveInMillis()); + assertEquals(paramURL, tracTracParamsReadFromDB.getParamURL()); + assertEquals(storedURI, tracTracParamsReadFromDB.getStoredURI()); + assertEquals(courseDesignUpdateURI, tracTracParamsReadFromDB.getCourseDesignUpdateURI()); + assertEquals(startOfTracking, tracTracParamsReadFromDB.getStartOfTracking()); + assertEquals(endOfTracking, tracTracParamsReadFromDB.getEndOfTracking()); + assertEquals(offsetToStartTimeOfSimulatedRace, tracTracParamsReadFromDB.getOffsetToStartTimeOfSimulatedRace()); + assertEquals(useInternalMarkPassingAlgorithm, tracTracParamsReadFromDB.isUseInternalMarkPassingAlgorithm()); + assertEquals(tracTracUsername, tracTracParamsReadFromDB.getTracTracUsername()); + assertEquals(tracTracPassword, tracTracParamsReadFromDB.getTracTracPassword()); + assertEquals(raceStatus, tracTracParamsReadFromDB.getRaceStatus()); + assertEquals(raceVisibility, tracTracParamsReadFromDB.getRaceVisibility()); + assertEquals(tracTracParams.getTrackerID(), tracTracParamsReadFromDB.getTrackerID()); + assertEquals(tracTracParams.isTrackWind(), tracTracParamsReadFromDB.isTrackWind()); + assertEquals(tracTracParams.isCorrectWindDirectionByMagneticDeclination(), tracTracParamsReadFromDB.isCorrectWindDirectionByMagneticDeclination()); + // remove again + mongoObjectFactory.removeConnectivityParametersForRaceToRestore(tracTracParams); + final Set connectivityParametersForRacesToRestore2 = new HashSet<>(); + domainObjectFactory.loadConnectivityParametersForRacesToRestore(params->connectivityParametersForRacesToRestore2.add(params)); + assertTrue(connectivityParametersForRacesToRestore2.isEmpty()); + } +} diff --git a/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_ja.properties.xlf b/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_ja.properties.xlf index dad9d10f358..47fa99e5b1a 100644 --- a/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_ja.properties.xlf +++ b/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_ja.properties.xlf @@ -1,6 +1,6 @@ - +

      H4sIAAAAAAAAAF2PTQrCQAyF93OKXEH32TiiFBSGunAdOrEObTPQaaHHd/6s4Cbv4yW8JMaPNOs3OTmgSRyU2a0jnqgbWCzUVksdaz+uk+AP1Y37RixvGAEylYwzLVRCIWExw3fN1TwubsMoEFU9ndiWpGdMBBmz2QTt5eUsy4I7QR66M4V15ika8TQZMBVVb67b/j74ALSew+fwAAAA diff --git a/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_ru.properties.xlf b/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_ru.properties.xlf index e1b72a65657..6ae3f0fcd32 100644 --- a/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_ru.properties.xlf +++ b/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_ru.properties.xlf @@ -1,6 +1,6 @@ - +
      H4sIAAAAAAAAAF2PTQrCQAyF93OKXEH32TiiFBSGunAdOrEObTPQaaHHd/6s4Cbv4yW8JMaPNOs3OTmgSRyU2a0jnqgbWCzUVksdaz+uk+AP1Y37RixvGAEylYwzLVRCIWExw3fN1TwubsMoEFU9ndiWpGdMBBmz2QTt5eUsy4I7QR66M4V15ika8TQZMBVVb67b/j74ALSew+fwAAAA diff --git a/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_zh_CN.properties.xlf b/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_zh_CN.properties.xlf index 7bb83b9f5a0..67bc6dafac7 100644 --- a/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sailing.polars.datamining/resources/stringmessages/XLF/Polars_StringMessages_zh_CN.properties.xlf @@ -1,6 +1,6 @@ - +
      H4sIAAAAAAAAAF2PTQrCQAyF93OKXEH32TiiFBSGunAdOrEObTPQaaHHd/6s4Cbv4yW8JMaPNOs3OTmgSRyU2a0jnqgbWCzUVksdaz+uk+AP1Y37RixvGAEylYwzLVRCIWExw3fN1TwubsMoEFU9ndiWpGdMBBmz2QTt5eUsy4I7QR66M4V15ika8TQZMBVVb67b/j74ALSew+fwAAAA diff --git a/java/com.sap.sailing.polars/build.properties b/java/com.sap.sailing.polars/build.properties index f11210d3f2c..a4af5b4a1ef 100644 --- a/java/com.sap.sailing.polars/build.properties +++ b/java/com.sap.sailing.polars/build.properties @@ -2,4 +2,5 @@ source.. = src/,\ resources/ output.. = bin/ bin.includes = META-INF/,\ - . + .,\ + WEB-INF/ diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/Activator.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/Activator.java index c235c06fea5..73fc24cda67 100644 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/Activator.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/Activator.java @@ -34,24 +34,33 @@ public class Activator implements BundleActivator { public void start(BundleContext context) throws Exception { logger.info("Registering PolarDataService"); PolarDataServiceImpl service = new PolarDataServiceImpl(); - registrations.add(context.registerService(PolarDataService.class, service, null)); + final ServiceRegistration polarDataServiceRegistration = context.registerService(PolarDataService.class, service, null); + registrations.add(polarDataServiceRegistration); final Dictionary replicableServiceProperties = new Hashtable<>(); replicableServiceProperties.put(Replicable.OSGi_Service_Registry_ID_Property_Name, service.getId().toString()); registrations.add(context.registerService(Replicable.class.getName(), service, replicableServiceProperties)); - final String polarDataSourceURL = System.getProperty(POLAR_DATA_SOURCE_URL_PROPERTY_NAME); if (polarDataSourceURL != null && !polarDataSourceURL.isEmpty()) { - waitForRacingEventServiceToObtainDomainFactory(polarDataSourceURL, service); + waitForRacingEventServiceToObtainDomainFactory(polarDataSourceURL, service, context, polarDataServiceRegistration); } } /** * Spawns a daemon thread that waits for the domain factory to be registered with the {@link PolarDataService}, then - * runs the polar data import from the given URL. The domain factory is required to resolve boat classes during - * de-serialization. + * unregisters the service from the OSGi registry because it will temporarily become unusable, runs the polar data + * import from the given URL and registers the service again, adding the service registration object to the set of + * {@link #registrations}. The domain factory is required to resolve boat classes during de-serialization. + * + * @param polarDataServiceRegistration + * used to remove the service registration temporarily while updating the service by a remote import + * @param registerPolarServiceCallback + * called when the polar data has successfully been obtained; expected to register the polar service as + * {@link Replicable} and as the {@link PolarDataService} with the OSGi registry */ - private void waitForRacingEventServiceToObtainDomainFactory(final String polarDataSourceURL, final ReplicablePolarService polarService) { - final Thread t = new Thread(()->{ + private void waitForRacingEventServiceToObtainDomainFactory(final String polarDataSourceURL, + final ReplicablePolarService polarService, final BundleContext context, + ServiceRegistration polarDataServiceRegistration) { + final Thread t = new Thread(() -> { try { Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); // ensure that classpath:... Shiro ini files are resolved properly logger.info("Waiting for domain factory to be registered with PolarService..."); @@ -59,7 +68,9 @@ public class Activator implements BundleActivator { polarService.runWithDomainFactory(domainFactory -> { PolarDataClient polarDataClient = new PolarDataClient(polarDataSourceURL, polarService); try { + polarDataServiceRegistration.unregister(); polarDataClient.updatePolarDataRegressions(); + registrations.add(context.registerService(PolarDataService.class, polarService, null)); } catch (Exception e) { logger.log(Level.SEVERE, "Exception while trying to import polar data from "+polarDataSourceURL, e); } diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java index dcf03018f82..081c585690b 100755 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java @@ -379,13 +379,6 @@ public class PolarDataServiceImpl implements ReplicablePolarService { consumer.accept(myDomainFactory); } - @Override - public void unregisterDomainFactory(DomainFactory domainFactory) { - if (this.domainFactory == domainFactory) { - this.domainFactory = null; - } - } - public Map getCubicRegressionsPerCourse() { return polarDataMiner.getCubicRegressionPerCourseProcessor().getRegressions(); } diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/jaxrs/client/PolarDataClient.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/jaxrs/client/PolarDataClient.java index 4d1ba927a71..95aba66bf40 100644 --- a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/jaxrs/client/PolarDataClient.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/jaxrs/client/PolarDataClient.java @@ -29,7 +29,7 @@ public class PolarDataClient { private static final String RESOURCE = "polars/api/polar_data"; - private final ReplicablePolarService polarDataServiceImpl; + private final ReplicablePolarService polarDataService; private final String polarDataSourceURL; /** @@ -40,7 +40,7 @@ public class PolarDataClient { * {@link PolarDataServiceImpl} service to work with */ public PolarDataClient(String polarDataSourceURL, ReplicablePolarService polarDataService) { - this.polarDataServiceImpl = polarDataService; + this.polarDataService = polarDataService; this.polarDataSourceURL = polarDataSourceURL; } @@ -52,15 +52,15 @@ public class PolarDataClient { try { logger.log(Level.INFO, "Loading polar regression data from remote server " + polarDataSourceURL); final InputStream inputStream = getContentFromResponse(); - polarDataServiceImpl.clearReplicaState(); - polarDataServiceImpl.initiallyFillFrom(inputStream); + polarDataService.clearReplicaState(); + polarDataService.initiallyFillFrom(inputStream); logger.log(Level.INFO, "Loading polar regression data from remote server " + polarDataSourceURL + " succeeded"); } catch (Exception e) { // Catching ClientProtocolException to indicate problems with HTTP protocol logger.log(Level.WARNING, "Failed to load polar regression data from remote server " + polarDataSourceURL + ", " + e.getMessage()+"; resetting polar data miner"); - polarDataServiceImpl.resetState(); + polarDataService.resetState(); } } diff --git a/java/com.sap.sailing.resultimport/META-INF/MANIFEST.MF b/java/com.sap.sailing.resultimport/META-INF/MANIFEST.MF index 442f3f929e3..45b306128c2 100644 --- a/java/com.sap.sailing.resultimport/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.resultimport/META-INF/MANIFEST.MF @@ -9,9 +9,8 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Export-Package: com.sap.sailing.resultimport, com.sap.sailing.resultimport.impl -Import-Package: com.sap.sailing.domain.base.impl, +Import-Package: com.sap.sailing.competitorimport, com.sap.sailing.domain.common, - com.sap.sailing.domain.common.impl, com.sap.sailing.domain.persistence, com.sap.sailing.server, org.osgi.framework;version="1.5.0", diff --git a/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/impl/ResultUrlRegistryServiceTrackerCustomizer.java b/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/impl/ResultUrlRegistryServiceTrackerCustomizer.java index 48db17c3005..72dd3c92489 100644 --- a/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/impl/ResultUrlRegistryServiceTrackerCustomizer.java +++ b/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/impl/ResultUrlRegistryServiceTrackerCustomizer.java @@ -2,11 +2,29 @@ package com.sap.sailing.resultimport.impl; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; import org.osgi.util.tracker.ServiceTrackerCustomizer; +import com.sap.sailing.competitorimport.CompetitorProvider; import com.sap.sailing.domain.common.ScoreCorrectionProvider; import com.sap.sailing.resultimport.ResultUrlRegistry; +/** + * Helps passing on the reference to the usually central instance of a {@link ResultUrlRegistry} service implementation + * to the services implementing result and competitor import. + *

      + * + * Subclass this abstract service tracker customizer and use it when creating a {@link ServiceTracker} for the + * {@link ResultUrlRegistry} class, then {@link ServiceTracker#open()} that tracker in your result / competitor + * importer's activator. Then, the {@link #configureScoreCorrectionProvider(ResultUrlRegistry)} and + * {@link #configureCompetitorProvider(ResultUrlRegistry)} methods will be invoked when the tracker obtains the + * reference to a/the {@link ResultUrlRegistry} service implementation. If they return a non-{@code null} importer + * implementation then that implementation will be registered in turn as a {@link ScoreCorrectionProvider} or + * {@link CompetitorProvider}, respectively. + * + * @author Axel Uhl (d043530) + * + */ public abstract class ResultUrlRegistryServiceTrackerCustomizer implements ServiceTrackerCustomizer { @@ -21,14 +39,35 @@ public abstract class ResultUrlRegistryServiceTrackerCustomizer implements ServiceReference serviceRefUrlRegistry = bundleContext .getServiceReference(ResultUrlRegistry.class); ResultUrlRegistry resultUrlRegistry = bundleContext.getService(serviceRefUrlRegistry); - - ScoreCorrectionProvider scoreCorrectionProvider = configureScoreCorrectionProvider(resultUrlRegistry); - bundleContext.registerService(ScoreCorrectionProvider.class, scoreCorrectionProvider, /* properties */null); + registerScoreCorrectionProviderService(resultUrlRegistry); + registerCompetitorImportService(resultUrlRegistry); return resultUrlRegistry; } + protected void registerScoreCorrectionProviderService(ResultUrlRegistry resultUrlRegistry) { + ScoreCorrectionProvider scoreCorrectionProvider = configureScoreCorrectionProvider(resultUrlRegistry); + if (scoreCorrectionProvider != null) { + bundleContext.registerService(ScoreCorrectionProvider.class, scoreCorrectionProvider, /* properties */null); + } + } + + private void registerCompetitorImportService(ResultUrlRegistry resultUrlRegistry) { + CompetitorProvider competitorProvider = configureCompetitorProvider(resultUrlRegistry); + if (competitorProvider != null) { + bundleContext.registerService(CompetitorProvider.class, competitorProvider, /* properties */null); + } + } + protected abstract ScoreCorrectionProvider configureScoreCorrectionProvider(ResultUrlRegistry resultUrlRegistry); + /** + * @return {@code null} by default, so a subclass does not have to make mention of a {@link CompetitorProvider} + * implementation + */ + protected CompetitorProvider configureCompetitorProvider(ResultUrlRegistry resultUrlRegistry) { + return null; + } + @Override public void modifiedService(ServiceReference reference, ResultUrlRegistry service) { } diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/impl/TargetTimeInfoDeserializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/impl/TargetTimeInfoDeserializer.java new file mode 100755 index 00000000000..0588510e38d --- /dev/null +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/impl/TargetTimeInfoDeserializer.java @@ -0,0 +1,51 @@ +package com.sap.sailing.server.gateway.deserialization.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.TargetTimeInfo; +import com.sap.sailing.domain.common.TargetTimeInfo.LegTargetTimeInfo; +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.MeterDistance; +import com.sap.sailing.domain.common.impl.TargetTimeInfoImpl; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; +import com.sap.sailing.server.gateway.serialization.impl.TargetTimeInfoSerializer; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsDurationImpl; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class TargetTimeInfoDeserializer implements JsonDeserializer { + final JsonDeserializer windDeserializer; + + public TargetTimeInfoDeserializer(JsonDeserializer windDeserializer) { + super(); + this.windDeserializer = windDeserializer; + } + + @Override + public TargetTimeInfo deserialize(JSONObject object) throws JsonDeserializationException { + final List legInfos = new ArrayList<>(); + final JSONArray legsAsJson = (JSONArray) object.get(TargetTimeInfoSerializer.LEGS); + for (final Object legAsObject : legsAsJson) { + final JSONObject legAsJson = (JSONObject) legAsObject; + final Duration expectedDuration = new MillisecondsDurationImpl(((Number) legAsJson.get(TargetTimeInfoSerializer.LEG_DURATION_MILLIS)).longValue()); + final TimePoint expectedStartTime = new MillisecondsTimePoint(((Number) legAsJson.get(TargetTimeInfoSerializer.LEG_START_TIME_MILLIS)).longValue()); + final Bearing legBearing = new DegreeBearingImpl(((Number) legAsJson.get(TargetTimeInfoSerializer.LEG_BEARING_DEGREES)).doubleValue()); + final Distance legDistance = new MeterDistance(((Number) legAsJson.get(TargetTimeInfoSerializer.LEG_DISTANCE_METERS)).doubleValue()); + final LegType legType = LegType.valueOf((String) legAsJson.get(TargetTimeInfoSerializer.LEG_TYPE)); + final Wind legWind = windDeserializer.deserialize((JSONObject) legAsJson.get(TargetTimeInfoSerializer.LEG_WIND)); + final LegTargetTimeInfo legInfo = new TargetTimeInfoImpl.LegTargetTimeInfoImpl(legDistance, legWind, legBearing, expectedDuration, expectedStartTime, legType); + legInfos.add(legInfo); + } + return new TargetTimeInfoImpl(legInfos); + } +} diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceCellDeserializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceCellDeserializer.java index 767e41dc1f9..891a2270d19 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceCellDeserializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceCellDeserializer.java @@ -5,6 +5,7 @@ import org.json.simple.JSONObject; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.base.racegroup.RaceCell; import com.sap.sailing.domain.base.racegroup.impl.RaceCellImpl; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; import com.sap.sailing.server.gateway.deserialization.impl.Helpers; @@ -12,10 +13,12 @@ import com.sap.sailing.server.gateway.serialization.racegroup.impl.RaceCellJsonS public class RaceCellDeserializer implements JsonDeserializer { - private JsonDeserializer logDeserializer; + private final JsonDeserializer logDeserializer; + private final JsonDeserializer targetTimeDeserializer; - public RaceCellDeserializer(JsonDeserializer logDeserializer) { + public RaceCellDeserializer(JsonDeserializer logDeserializer, JsonDeserializer targetTimeDeserializer) { this.logDeserializer = logDeserializer; + this.targetTimeDeserializer = targetTimeDeserializer; } public RaceCell deserialize(JSONObject object) @@ -25,9 +28,11 @@ public class RaceCellDeserializer implements JsonDeserializer { final double factor = factorJson == null ? 1 : ((Number) factorJson).doubleValue(); final Double explicitFactor = (Double) object.get(RaceCellJsonSerializer.FIELD_EXPLICIT_FACTOR); final Number zeroBasedIndexInFleet = (Number) object.get(RaceCellJsonSerializer.FIELD_ZERO_BASED_INDEX_IN_FLEET); + final JSONObject targetTimeInfoJson = (JSONObject) object.get(RaceCellJsonSerializer.FIELD_TARGET_TIME_INFO); + final TargetTimeInfo targetTime = targetTimeInfoJson == null ? null : targetTimeDeserializer.deserialize(targetTimeInfoJson); JSONObject logJson = Helpers.getNestedObjectSafe(object, RaceCellJsonSerializer.FIELD_RACE_LOG); RaceLog log = logDeserializer.deserialize(logJson); - return new RaceCellImpl(name, log, factor, explicitFactor, zeroBasedIndexInFleet == null ? -1 : zeroBasedIndexInFleet.intValue()); + return new RaceCellImpl(name, log, factor, explicitFactor, zeroBasedIndexInFleet == null ? -1 : zeroBasedIndexInFleet.intValue(), targetTime); } } diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceGroupDeserializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceGroupDeserializer.java index 05f3b44e431..20eb3745964 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceGroupDeserializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceGroupDeserializer.java @@ -18,6 +18,9 @@ import com.sap.sailing.server.gateway.deserialization.impl.BoatClassJsonDeserial import com.sap.sailing.server.gateway.deserialization.impl.ColorDeserializer; import com.sap.sailing.server.gateway.deserialization.impl.FleetDeserializer; import com.sap.sailing.server.gateway.deserialization.impl.Helpers; +import com.sap.sailing.server.gateway.deserialization.impl.PositionJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.TargetTimeInfoDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.WindJsonDeserializer; import com.sap.sailing.server.gateway.deserialization.racelog.impl.RaceLogDeserializer; import com.sap.sailing.server.gateway.deserialization.racelog.impl.RaceLogEventDeserializer; import com.sap.sailing.server.gateway.serialization.racegroup.impl.RaceGroupJsonSerializer; @@ -33,7 +36,8 @@ public class RaceGroupDeserializer implements JsonDeserializer { JsonDeserializer proceduresDeserializer) { return new RaceGroupDeserializer(new BoatClassJsonDeserializer(domainFactory), new SeriesWithRowsDeserializer( new RaceRowDeserializer(new FleetDeserializer(new ColorDeserializer()), new RaceCellDeserializer( - new RaceLogDeserializer(RaceLogEventDeserializer.create(domainFactory))))), + new RaceLogDeserializer(RaceLogEventDeserializer.create(domainFactory)), + new TargetTimeInfoDeserializer(new WindJsonDeserializer(new PositionJsonDeserializer()))))), proceduresDeserializer); } diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/TargetTimeInfoSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/TargetTimeInfoSerializer.java new file mode 100755 index 00000000000..986760ea8d4 --- /dev/null +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/TargetTimeInfoSerializer.java @@ -0,0 +1,51 @@ +package com.sap.sailing.server.gateway.serialization.impl; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import com.sap.sailing.domain.common.TargetTimeInfo; +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.TargetTimeInfo.LegTargetTimeInfo; +import com.sap.sailing.server.gateway.serialization.JsonSerializer; + +public class TargetTimeInfoSerializer implements JsonSerializer { + + public static final String LEGS = "legs"; + public static final String DURATION_MILLIS = "durationMillis"; + public static final String LEG_START_TIME_MILLIS = "legStartMillis"; + public static final String LEG_DURATION_MILLIS = "legDurationMillis"; + public static final String LEG_DISTANCE_METERS = "legDistanceMeters"; + public static final String LEG_BEARING_DEGREES = "legBearingDegrees"; + public static final String LEG_TRUE_WIND_ANGLE_TO_LEG_DEGREES = "legTrueWindAngleToLegDegrees"; + public static final String LEG_WIND = "legWind"; + public static final String LEG_TYPE = "legType"; + + private final JsonSerializer windSerializer; + + public TargetTimeInfoSerializer(JsonSerializer windSerializer) { + super(); + this.windSerializer = windSerializer; + } + + @Override + public JSONObject serialize(TargetTimeInfo object) { + final JSONArray legsAsJson = new JSONArray(); + final JSONObject result = new JSONObject(); + // redundant, but possibly useful to clients other than the deserializer: + result.put(DURATION_MILLIS, object.getExpectedDuration().asMillis()); + result.put(LEGS, legsAsJson); + for (final LegTargetTimeInfo legInfo : object.getLegTargetTimes()) { + final JSONObject legAsJson = new JSONObject(); + legAsJson.put(LEG_START_TIME_MILLIS, legInfo.getExpectedStartTimePoint().asMillis()); + legAsJson.put(LEG_DURATION_MILLIS, legInfo.getExpectedDuration().asMillis()); + legAsJson.put(LEG_DISTANCE_METERS, legInfo.getDistance().getMeters()); + legAsJson.put(LEG_BEARING_DEGREES, legInfo.getLegBearing().getDegrees()); + // redundant, but possibly useful to clients other than the deserializer: + legAsJson.put(LEG_TRUE_WIND_ANGLE_TO_LEG_DEGREES, legInfo.getTrueWindAngleToLeg().getDegrees()); + legAsJson.put(LEG_TYPE, legInfo.getLegType().name()); + legAsJson.put(LEG_WIND, windSerializer.serialize(legInfo.getWind())); + legsAsJson.add(legAsJson); + } + return result; + } +} diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceCellJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceCellJsonSerializer.java index 19240a6b646..6881caa0df7 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceCellJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceCellJsonSerializer.java @@ -4,6 +4,7 @@ import org.json.simple.JSONObject; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.base.racegroup.RaceCell; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.server.gateway.serialization.JsonSerializer; public class RaceCellJsonSerializer implements JsonSerializer { @@ -13,11 +14,14 @@ public class RaceCellJsonSerializer implements JsonSerializer { public static final String FIELD_FACTOR = "factor"; public static final String FIELD_EXPLICIT_FACTOR = "explicitfactor"; public static final String FIELD_ZERO_BASED_INDEX_IN_FLEET = "zerobasedindexinfleet"; + public static final String FIELD_TARGET_TIME_INFO = "targettimeinfo"; - private JsonSerializer logSerializer; + private final JsonSerializer logSerializer; + private final JsonSerializer targetTimeInfoSerializer; - public RaceCellJsonSerializer(JsonSerializer logSerializer) { + public RaceCellJsonSerializer(JsonSerializer logSerializer, JsonSerializer targetTimeInfoSerializer) { this.logSerializer = logSerializer; + this.targetTimeInfoSerializer = targetTimeInfoSerializer; } @Override @@ -28,6 +32,8 @@ public class RaceCellJsonSerializer implements JsonSerializer { result.put(FIELD_EXPLICIT_FACTOR, object.getExplicitFactor()); result.put(FIELD_ZERO_BASED_INDEX_IN_FLEET, object.getZeroBasedIndexInFleet()); result.put(FIELD_RACE_LOG, logSerializer.serialize(object.getRaceLog())); + final TargetTimeInfo targetTime = object.getTargetTime(); + result.put(FIELD_TARGET_TIME_INFO, targetTime==null?null:targetTimeInfoSerializer.serialize(targetTime)); return result; } diff --git a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/racelog/tracking/impl/GPSFixJsonServiceFinder.java b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/racelog/tracking/impl/GPSFixJsonServiceFinder.java deleted file mode 100644 index e755b7bcda9..00000000000 --- a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/racelog/tracking/impl/GPSFixJsonServiceFinder.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.sap.sailing.server.gateway.serialization.racelog.tracking.impl; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import com.sap.sailing.domain.common.tracking.GPSFix; -import com.sap.sailing.domain.common.tracking.GPSFixMoving; -import com.sap.sailing.server.gateway.deserialization.impl.GPSFixJsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.GPSFixMovingJsonDeserializer; -import com.sap.sailing.server.gateway.serialization.impl.GPSFixJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.GPSFixMovingJsonSerializer; -import com.sap.sailing.server.gateway.serialization.racelog.tracking.GPSFixJsonHandler; -import com.sap.sse.common.NoCorrespondingServiceRegisteredException; -import com.sap.sse.common.TypeBasedServiceFinder; - -public class GPSFixJsonServiceFinder implements TypeBasedServiceFinder { - private final GPSFixJsonHandler gpsFixHandler = new GPSFixJsonHandlerImpl(new GPSFixJsonDeserializer(), - new GPSFixJsonSerializer()); - private final GPSFixJsonHandler gpsFixMovingHandler = new GPSFixJsonHandlerImpl( - new GPSFixMovingJsonDeserializer(), new GPSFixMovingJsonSerializer()); - - @Override - public GPSFixJsonHandler findService(String type) throws NoCorrespondingServiceRegisteredException { - if (type.equals(GPSFixJsonDeserializer.TYPE)) { - return gpsFixHandler; - } else if (type.equals(GPSFixMovingJsonDeserializer.TYPE)) { - return gpsFixMovingHandler; - } - throw new NoCorrespondingServiceRegisteredException( - "Only handlers for GPSFix, GPSFixMoving, GPSFixNmeaDTO, GPSFixMovingNmeaDTO are registered", type, - GPSFixJsonHandler.class.getSimpleName()); - } - - @Override - public void setFallbackService(GPSFixJsonHandler fallback) { - - } - - @Override - public Set findAllServices() { - return new HashSet(Arrays.asList(gpsFixHandler, gpsFixMovingHandler)); - } - -} diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/AbstractJaxRsApiTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/AbstractJaxRsApiTest.java index 4af70af38be..e1c15e085d3 100644 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/AbstractJaxRsApiTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/AbstractJaxRsApiTest.java @@ -40,8 +40,7 @@ public abstract class AbstractJaxRsApiTest { public void setUp() { service = MongoDBConfiguration.getDefaultTestConfiguration().getService(); service.getDB().dropDatabase(); - - racingEventService = new RacingEventServiceImpl(true, new MockSmartphoneUuidServiceFinderFactory()); + racingEventService = new RacingEventServiceImpl(/* clearPersistentCompetitorStore */ true, new MockSmartphoneUuidServiceFinderFactory(), /* restoreTrackedRaces */ false); } protected T spyResource(T resource) { diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/GPSFixesResourceTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/GPSFixesResourceTest.java index 40017a17f70..f2694b49954 100644 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/GPSFixesResourceTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/GPSFixesResourceTest.java @@ -45,7 +45,7 @@ public class GPSFixesResourceTest { @Before public void setup() { - service = new RacingEventServiceImpl(true, new MockSmartphoneUuidServiceFinderFactory()); + service = new RacingEventServiceImpl(/* clearPersistentCompetitorStore */ true, new MockSmartphoneUuidServiceFinderFactory(), /* restoreTrackedRaces */ false); service.getMongoObjectFactory().getDatabase().dropDatabase(); } diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java index 602a876e950..71b5efa3781 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java @@ -6,6 +6,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.logging.Logger; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.CourseArea; @@ -25,14 +26,21 @@ import com.sap.sailing.domain.base.racegroup.impl.RaceGroupImpl; import com.sap.sailing.domain.base.racegroup.impl.RaceRowImpl; import com.sap.sailing.domain.base.racegroup.impl.SeriesWithRowsImpl; import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.polars.NotEnoughDataHasBeenAddedException; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsTimePoint; /** * Used to convert {@link Leaderboard}s to {@link RaceGroup}s. */ public class RaceGroupFactory { + private static final Logger logger = Logger.getLogger(RaceGroupFactory.class.getName()); /** * Convert given {@link RegattaLeaderboard} to a {@link RaceGroup}. @@ -98,20 +106,32 @@ public class RaceGroupFactory { } /** - * Delivers the race cell in the order of the {@link RaceColumn}s provided in {@code raceColumns}. + * Delivers the race cell in the order of the {@link RaceColumn}s provided in {@code raceColumns}. The + * {@link RaceCell#getTargetTime() target times} are based on an + * {@link TrackedRace#getEstimatedTimeToComplete(TimePoint) estimation} for the current point in time, when the + * request is being made, using this time point to determine the wind speed and direction for each leg. */ private List getCells(String fleetName, List raceColumns, boolean isFirstRaceColumnVirtual) { boolean skippedFirst = false; List cells = new ArrayList<>(); if (raceColumns != null) { int zeroBasedIndexOfRaceInFleet = 0; + final TimePoint now = MillisecondsTimePoint.now(); for (final RaceColumn raceColumn : raceColumns) { if (isFirstRaceColumnVirtual && !skippedFirst) { skippedFirst = true; } else { - Fleet fleet = raceColumn.getFleetByName(fleetName); + final Fleet fleet = raceColumn.getFleetByName(fleetName); + final TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + TargetTimeInfo targetTime; + try { + targetTime = trackedRace == null ? null : trackedRace.getEstimatedTimeToComplete(now); + } catch (NotEnoughDataHasBeenAddedException | NoWindException e) { + logger.fine(()->"Unable to determine target time for "+trackedRace); + targetTime = null; + } cells.add(new RaceCellImpl(raceColumn.getName(), raceColumn.getRaceLog(fleet), - raceColumn.getFactor(), raceColumn.getExplicitFactor(), zeroBasedIndexOfRaceInFleet++)); + raceColumn.getFactor(), raceColumn.getExplicitFactor(), zeroBasedIndexOfRaceInFleet++, targetTime)); } } } diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupJsonExportServlet.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupJsonExportServlet.java index 91487d6bc84..74048d86294 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupJsonExportServlet.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupJsonExportServlet.java @@ -23,7 +23,10 @@ import com.sap.sailing.server.gateway.serialization.impl.ColorJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.CompetitorJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.CourseAreaJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.FleetJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.PositionJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.RegattaConfigurationJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.TargetTimeInfoSerializer; +import com.sap.sailing.server.gateway.serialization.impl.WindJsonSerializer; import com.sap.sailing.server.gateway.serialization.racegroup.impl.RaceCellJsonSerializer; import com.sap.sailing.server.gateway.serialization.racegroup.impl.RaceGroupJsonSerializer; import com.sap.sailing.server.gateway.serialization.racegroup.impl.RaceRowJsonSerializer; @@ -95,7 +98,8 @@ public class RaceGroupJsonExportServlet extends AbstractJsonHttpServlet { RegattaConfigurationJsonSerializer.create(), new SeriesWithRowsOfRaceGroupSerializer(new SeriesWithRowsJsonSerializer( new RaceRowsOfSeriesWithRowsSerializer(new RaceRowJsonSerializer(new FleetJsonSerializer( - new ColorJsonSerializer()), new RaceCellJsonSerializer(createRaceLogSerializer(clientUuid))))))); + new ColorJsonSerializer()), new RaceCellJsonSerializer(createRaceLogSerializer(clientUuid), + new TargetTimeInfoSerializer(new WindJsonSerializer(new PositionJsonSerializer())))))))); } diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventRaceStatesSerializer.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventRaceStatesSerializer.java index 101f820c6db..7786cd92f83 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventRaceStatesSerializer.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventRaceStatesSerializer.java @@ -13,6 +13,7 @@ import com.sap.sailing.domain.base.Fleet; import com.sap.sailing.domain.base.RaceColumn; import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sailing.server.gateway.serialization.JsonSerializer; +import com.sap.sse.common.Duration; import com.sap.sse.common.Util.Pair; public class EventRaceStatesSerializer implements JsonSerializer>> { @@ -24,11 +25,13 @@ public class EventRaceStatesSerializer implements JsonSerializer> eventAndLeaderboards) { Event event = eventAndLeaderboards.getA(); Iterable leaderboards = eventAndLeaderboards.getB(); - JSONObject result = new JSONObject(); result.put(FIELD_EVENT_NAME, event.getName()); result.put(FIELD_EVENT_ID, event.getId().toString()); JSONArray raceStatesLogEntriesJson = new JSONArray(); result.put(FIELD_RACE_STATES, raceStatesLogEntriesJson); for (CourseArea courseArea : event.getVenue().getCourseAreas()) { - if(filterByCourseArea == null || courseArea.getName().equals(filterByCourseArea)) { + if (filterByCourseArea == null || courseArea.getName().equals(filterByCourseArea)) { for (Leaderboard leaderboard : leaderboards) { RaceStateSerializer raceStateSerializer = new RaceStateSerializer(raceLogResolver); if (filterByLeaderboardName == null || leaderboard.getName().equals(filterByLeaderboardName)) { @@ -68,7 +70,7 @@ public class EventRaceStatesSerializer implements JsonSerializer raceColumnAndFleet = new Pair(raceColumn, fleet); - if(filterDay == null || raceStateSerializer.isRaceStateOfSameDay(raceColumnAndFleet, filterDay)) { + if (filterDay == null || raceStateSerializer.isRaceStateOfSameDay(raceColumnAndFleet, filterDay, clientTimeZoneOffset)) { JSONObject raceStateJson = raceStateSerializer.serialize(raceColumnAndFleet); raceStateJson.put("courseAreaName", courseArea.getName()); raceStateJson.put("leaderboardName", leaderboardName); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventsResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventsResource.java index 82b7a96edba..6d349818588 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventsResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/EventsResource.java @@ -24,7 +24,9 @@ import com.sap.sailing.server.gateway.serialization.impl.CourseAreaJsonSerialize import com.sap.sailing.server.gateway.serialization.impl.EventBaseJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.LeaderboardGroupBaseJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.VenueJsonSerializer; +import com.sap.sse.common.Duration; import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.impl.MillisecondsDurationImpl; @Path("/v1/events") public class EventsResource extends AbstractSailingServerResource { @@ -80,7 +82,8 @@ public class EventsResource extends AbstractSailingServerResource { @Produces("application/json;charset=UTF-8") @Path("{eventId}/racestates") public Response getRaceStates(@PathParam("eventId") String eventId, @QueryParam("filterByLeaderboard") String filterByLeaderboard, - @QueryParam("filterByCourseArea") String filterByCourseArea, @QueryParam("filterByDayOffset") String filterByDayOffset) { + @QueryParam("filterByCourseArea") String filterByCourseArea, @QueryParam("filterByDayOffset") String filterByDayOffset, + @QueryParam("clientTimeZoneOffsetInMinutes") Integer clientTimeZoneOffsetInMinutes) { // TODO bug2589, bug3504: the following will require EVENT:READ permission; it requires cross-server links to be authentication aware... // SecurityUtils.getSubject().checkPermission(Permission.EVENT.getStringPermissionForObjects(Permission.Mode.READ, eventId)); Response response; @@ -94,8 +97,18 @@ public class EventsResource extends AbstractSailingServerResource { if (event == null) { response = getBadEventErrorResponse(eventId); } else { + final Duration clientTimeZoneOffset; + if (filterByDayOffset != null) { + if (clientTimeZoneOffsetInMinutes != null) { + clientTimeZoneOffset = new MillisecondsDurationImpl(1000*60*clientTimeZoneOffsetInMinutes); + } else { + clientTimeZoneOffset = Duration.NULL; + } + } else { + clientTimeZoneOffset = null; + } EventRaceStatesSerializer eventRaceStatesSerializer = new EventRaceStatesSerializer(filterByCourseArea, - filterByLeaderboard, filterByDayOffset, getService()); + filterByLeaderboard, filterByDayOffset, clientTimeZoneOffset, getService()); JSONObject raceStatesJson = eventRaceStatesSerializer.serialize(new Pair>(event, getService().getLeaderboards().values())); String json = raceStatesJson.toJSONString(); response = Response.ok(json).header("Content-Type", MediaType.APPLICATION_JSON + ";charset=UTF-8").build(); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardGroupsResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardGroupsResource.java index 46bd91cf41c..5c1ead02012 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardGroupsResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardGroupsResource.java @@ -3,10 +3,8 @@ package com.sap.sailing.server.gateway.jaxrs.api; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -34,6 +32,7 @@ import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.server.gateway.jaxrs.AbstractSailingServerResource; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Triple; import com.sap.sse.common.impl.MillisecondsTimePoint; @Path("/v1/leaderboardgroups") @@ -93,7 +92,7 @@ public class LeaderboardGroupsResource extends AbstractSailingServerResource { jsonLeaderboard.put("lastScoringUpdate", null); } - final LinkedHashMap> fleetsBySeriesName = new LinkedHashMap<>(); + final List, Iterable>> seriesNameAndFleetsAndRaceColumnsOfSeries = new ArrayList<>(); final Map medalSeriesNames = new HashMap<>(); if (isRegattaLeaderboard) { RegattaLeaderboard regattaLeaderboard = (RegattaLeaderboard) leaderboard; @@ -103,25 +102,28 @@ public class LeaderboardGroupsResource extends AbstractSailingServerResource { for (final Series series : regatta.getSeries()) { List fleets = new ArrayList<>(); Util.addAll(series.getFleets(), fleets); - fleetsBySeriesName.put(series.getName(), fleets); + seriesNameAndFleetsAndRaceColumnsOfSeries.add(new Triple, Iterable>(series.getName(), fleets, series.getRaceColumns())); medalSeriesNames.put(series.getName(), series.isMedal()); } } else { jsonLeaderboard.put("scoringScheme", leaderboard.getScoringScheme().getType()); jsonLeaderboard.put("regattaName", null); - // write a 'default' series to be conform with our common regatta structure - fleetsBySeriesName.put(LeaderboardNameConstants.DEFAULT_SERIES_NAME, Collections.singleton(leaderboard.getFleet(LeaderboardNameConstants.DEFAULT_FLEET_NAME))); + // write a 'default' series to conform with our common regatta structure + seriesNameAndFleetsAndRaceColumnsOfSeries.add(new Triple, Iterable>( + LeaderboardNameConstants.DEFAULT_SERIES_NAME, + Collections.singleton(leaderboard.getFleet(LeaderboardNameConstants.DEFAULT_FLEET_NAME)), + leaderboard.getRaceColumns())); } JSONArray jsonSeriesEntries = new JSONArray(); jsonLeaderboard.put("series", jsonSeriesEntries); - for (final Entry> e : fleetsBySeriesName.entrySet()) { + for (final Triple, Iterable> e : seriesNameAndFleetsAndRaceColumnsOfSeries) { JSONObject jsonSeries = new JSONObject(); jsonSeriesEntries.add(jsonSeries); - jsonSeries.put("name", e.getKey()); - jsonSeries.put("isMedalSeries", medalSeriesNames.get(e.getKey())); + jsonSeries.put("name", e.getA()); + jsonSeries.put("isMedalSeries", medalSeriesNames.get(e.getA())); JSONArray jsonFleetsEntries = new JSONArray(); jsonSeries.put("fleets", jsonFleetsEntries); - for (final Fleet fleet : e.getValue()) { + for (final Fleet fleet : e.getB()) { if (fleet != null) { JSONObject jsonFleet = new JSONObject(); jsonFleet.put("name", fleet.getName()); @@ -130,7 +132,7 @@ public class LeaderboardGroupsResource extends AbstractSailingServerResource { jsonFleetsEntries.add(jsonFleet); JSONArray jsonRacesEntries = new JSONArray(); jsonFleet.put("races", jsonRacesEntries); - for (RaceColumn raceColumn: leaderboard.getRaceColumns()) { + for (RaceColumn raceColumn : e.getC()) { JSONObject jsonRaceColumn = new JSONObject(); jsonRaceColumn.put("name", raceColumn.getName()); jsonRaceColumn.put("isMedalRace" , raceColumn.isMedalRace()); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RaceStateSerializer.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RaceStateSerializer.java index 0c76e3d5a3a..6395c77e30e 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RaceStateSerializer.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RaceStateSerializer.java @@ -20,6 +20,7 @@ import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.domain.racelog.RaceStateOfSameDayHelper; import com.sap.sailing.server.gateway.serialization.JsonSerializer; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util.Pair; @@ -79,10 +80,9 @@ public class RaceStateSerializer implements JsonSerializer raceColumnAndFleet, Calendar dayToCheck) { + public boolean isRaceStateOfSameDay(Pair raceColumnAndFleet, Calendar dayToCheck, Duration clientTimeZoneOffset) { RaceColumn raceColumn = raceColumnAndFleet.getA(); Fleet fleet = raceColumnAndFleet.getB(); - boolean result = false; RaceLog raceLog = raceColumn.getRaceLog(fleet); if (raceLog != null && !raceLog.isEmpty()) { @@ -90,7 +90,7 @@ public class RaceStateSerializer implements JsonSerializerfilterByLeaderboard (filters the race states by the name of the leaderboard)

    filterByCourseArea (filters the race states by the name of the course area)
    filterByDayOffset (filters the race states by a day offset for current day, e.g. 1 means yesterday)
    +
    clientTimeZoneOffsetInMinutes (if filterByDayOffset was provided, this parameter specifies the minutes the client is behind UTC; e.g., -60 for GMT+1 which is CET, Europe/Berlin)
    diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java index 8a729da4acf..48f8238e3e8 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java @@ -55,7 +55,7 @@ public abstract class AbstractServerReplicationTest extends com.sap.sse.replicat @Override public DomainFactory getBaseDomainFactory() { return baseDomainFactory; } @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } }; - }, MediaDBFactory.INSTANCE.getMediaDB(mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, null, null, /* sailingNotificationService */ null); + }, MediaDBFactory.INSTANCE.getMediaDB(mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, null, null, /* sailingNotificationService */ null, /* restoreTrackedRaces */ false); } @Override @@ -74,7 +74,7 @@ public abstract class AbstractServerReplicationTest extends com.sap.sse.replicat @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } }; }, MediaDBFactory.INSTANCE.getMediaDB(mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, - /* serviceFinderFactory */ null, null, /* sailingNotificationService */ null); + /* serviceFinderFactory */ null, null, /* sailingNotificationService */ null, /* restoreTrackedRaces */ false); } } } diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/ConnectionResetAndReconnectTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/ConnectionResetAndReconnectTest.java index 170313fa30c..aee366272ac 100644 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/ConnectionResetAndReconnectTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/ConnectionResetAndReconnectTest.java @@ -26,6 +26,7 @@ import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.replication.Replicable; import com.sap.sse.replication.ReplicationMasterDescriptor; import com.sap.sse.replication.impl.ReplicationMasterDescriptorImpl; @@ -56,12 +57,12 @@ public class ConnectionResetAndReconnectTest extends AbstractServerReplicationTe static class MasterReplicationDescriptorMock extends ReplicationMasterDescriptorImpl { - public MasterReplicationDescriptorMock(String messagingHost, String hostname, String exchangeName, int servletPort, int messagingPort) { - super(messagingHost, exchangeName, messagingPort, UUID.randomUUID().toString(), hostname, servletPort); + public MasterReplicationDescriptorMock(String messagingHost, String hostname, String exchangeName, int servletPort, int messagingPort, Iterable> replicables) { + super(messagingHost, exchangeName, messagingPort, UUID.randomUUID().toString(), hostname, servletPort, replicables); } public static MasterReplicationDescriptorMock from(ReplicationMasterDescriptor obj) { - return new MasterReplicationDescriptorMock(obj.getMessagingHostname(), obj.getHostname(), obj.getExchangeName(), obj.getServletPort(), obj.getMessagingPort()); + return new MasterReplicationDescriptorMock(obj.getMessagingHostname(), obj.getHostname(), obj.getExchangeName(), obj.getServletPort(), obj.getMessagingPort(), obj.getReplicables()); } @Override diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/DelayedLeaderboardCorrectionsReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/DelayedLeaderboardCorrectionsReplicationTest.java index ed07795b3fb..a45375ef323 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/DelayedLeaderboardCorrectionsReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/DelayedLeaderboardCorrectionsReplicationTest.java @@ -164,7 +164,7 @@ public class DelayedLeaderboardCorrectionsReplicationTest extends AbstractServer private RacingEventServiceImpl createRacingEventServiceWithOneMockedTrackedRace(final DynamicTrackedRace q2YellowTrackedRace, DomainFactory domainFactory) { return new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(MongoDBService.INSTANCE, domainFactory), PersistenceFactory.INSTANCE - .getMongoObjectFactory(MongoDBService.INSTANCE), MediaDBFactory.INSTANCE.getMediaDB(MongoDBService.INSTANCE), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE) { + .getMongoObjectFactory(MongoDBService.INSTANCE), MediaDBFactory.INSTANCE.getMediaDB(MongoDBService.INSTANCE), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, /* restoreTrackedRaces */ false) { @Override public DynamicTrackedRace getExistingTrackedRace(RegattaAndRaceIdentifier raceIdentifier) { return q2YellowTrackedRace; diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/InitialLoadReplicationObjectIdentityTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/InitialLoadReplicationObjectIdentityTest.java index 07fb1eba53d..ff35ebd6f41 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/InitialLoadReplicationObjectIdentityTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/InitialLoadReplicationObjectIdentityTest.java @@ -88,9 +88,9 @@ public class InitialLoadReplicationObjectIdentityTest extends AbstractServerRepl public void setUp() throws Exception { persistenceSetUp(/* dropDB */ true); this.master = new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(testSetUp.mongoDBService, DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(testSetUp.mongoDBService), MediaDBFactory.INSTANCE.getMediaDB(testSetUp.mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE); + .getMongoObjectFactory(testSetUp.mongoDBService), MediaDBFactory.INSTANCE.getMediaDB(testSetUp.mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, /* restoreTrackedRaces */ false); this.replica = new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(testSetUp.mongoDBService, DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(testSetUp.mongoDBService), MediaDBFactory.INSTANCE.getMediaDB(testSetUp.mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE); + .getMongoObjectFactory(testSetUp.mongoDBService), MediaDBFactory.INSTANCE.getMediaDB(testSetUp.mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, /* restoreTrackedRaces */ false); } private void performReplicationSetup() throws Exception { diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/ReplicationInstancesManagerLoggingPerformanceTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/ReplicationInstancesManagerLoggingPerformanceTest.java index 9a2e470c9d7..9ebe9bcbdc1 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/ReplicationInstancesManagerLoggingPerformanceTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/ReplicationInstancesManagerLoggingPerformanceTest.java @@ -23,7 +23,7 @@ public class ReplicationInstancesManagerLoggingPerformanceTest { @Before public void setUp() throws UnknownHostException { replicationInstanceManager = new ReplicationInstancesManager(); - replica = new ReplicaDescriptor(InetAddress.getLocalHost(), UUID.randomUUID(), ""); + replica = new ReplicaDescriptor(InetAddress.getLocalHost(), UUID.randomUUID(), "", /* replicableIds */ new String[] { "Humba" }); replicationInstanceManager.registerReplica(replica); operation = new CreateLeaderboardGroup("Test Leaderboard Group", "Description of Test Leaderboard Group", /* displayName */ null, /* displayGroupsInReverseOrder */ false, diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceBoatCompetitorMetadataReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceBoatCompetitorMetadataReplicationTest.java index 7d8f74e6bfd..74f94560354 100644 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceBoatCompetitorMetadataReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceBoatCompetitorMetadataReplicationTest.java @@ -71,13 +71,13 @@ public class TrackRaceBoatCompetitorMetadataReplicationTest extends AbstractServ .createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, /* delayToLiveInMillis */ 0l, /* offsetToStartTimeOfSimulatedRace */null, /*ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, - EmptyRegattaLogStore.INSTANCE, tracTracUsername, tracTracPassword, "", ""); + EmptyRegattaLogStore.INSTANCE, tracTracUsername, tracTracPassword, "", "", /* trackWind */ false, /* correctWindDirectionByMagneticDeclination */ false); } private void startTracking() throws Exception, InterruptedException { startTrackingOnMaster(); waitForTrackRaceReplicationTrigger(); - raceIdentifier = racesHandle.getRaceTracker().getRaceIdentifiers().iterator().next(); + raceIdentifier = racesHandle.getRaceTracker().getRaceIdentifier(); masterTrackedRace = master.getTrackedRace(raceIdentifier); } diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceReplicationTest.java index ab0fcc60042..c165274a8fa 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceReplicationTest.java @@ -89,13 +89,13 @@ public class TrackRaceReplicationTest extends AbstractServerReplicationTest { .createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, /* delayToLiveInMillis */ 0l, /* offsetToStartTimeOfSimulatedRace */null, /*ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, - EmptyRegattaLogStore.INSTANCE, tracTracUsername, tracTracPassword, "", ""); + EmptyRegattaLogStore.INSTANCE, tracTracUsername, tracTracPassword, "", "", /* trackWind */ false, /* correctWindDirectionByMagneticDeclination */ false); } private void startTracking() throws Exception, InterruptedException { startTrackingOnMaster(); waitForTrackRaceReplicationTrigger(); - raceIdentifier = racesHandle.getRaceTracker().getRaceIdentifiers().iterator().next(); + raceIdentifier = racesHandle.getRaceTracker().getRaceIdentifier(); masterTrackedRace = master.getTrackedRace(raceIdentifier); } diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/impl/MasterDataImporterMediaTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/impl/MasterDataImporterMediaTest.java index f531e7c4412..fa75b4e28ed 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/impl/MasterDataImporterMediaTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/impl/MasterDataImporterMediaTest.java @@ -62,7 +62,7 @@ public class MasterDataImporterMediaTest { when(mediaDb.loadAllMediaTracks()).thenReturn(Arrays.asList(existingDbMediaTracks)); racingEventService = spy(new RacingEventServiceImpl(domainObjectFactory, mongoObjectFactory, mediaDb, - windStore, sensorFixStore)); + windStore, sensorFixStore, /* restoreTrackedRaces */ false)); } @Test diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java index 3880fcd120a..b526365cf47 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java @@ -808,7 +808,7 @@ public class MasterDataImportTest { RacingEventService sourceService = new RacingEventServiceImpl( PersistenceFactory.INSTANCE.getDomainObjectFactory(MongoDBService.INSTANCE, sourceDomainFactory), PersistenceFactory.INSTANCE.getMongoObjectFactory(MongoDBService.INSTANCE), - MediaDBFactory.INSTANCE.getDefaultMediaDB(), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE); + MediaDBFactory.INSTANCE.getDefaultMediaDB(), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, /* restoreTrackedRaces */ false); Event event = sourceService.addEvent(TEST_EVENT_NAME, /* eventDescription */null, eventStartDate, eventEndDate, "testVenue", false, eventUUID); UUID courseAreaUUID = UUID.randomUUID(); diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerMock.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerMock.java index 19e3cb03b2e..ba93c62c113 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerMock.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerMock.java @@ -2,11 +2,11 @@ package com.sap.sailing.server.test; import java.io.IOException; import java.net.MalformedURLException; -import java.util.Set; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; import com.sap.sailing.domain.tracking.AbstractRaceTrackerBaseImpl; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.RaceHandle; @@ -16,32 +16,31 @@ public class RaceTrackerMock extends AbstractRaceTrackerBaseImpl { private Long id; private Regatta regatta; - private Set raceDefinitions; + private RaceDefinition raceDefinition; private boolean isTracking; public RaceTrackerMock() { - // TODO check if necesary + super(null); } - - - public RaceTrackerMock(Long id, Regatta regatta, Set raceDefinitions, boolean isTracking) { - super(); + public RaceTrackerMock(Long id, Regatta regatta, RaceDefinition raceDefinition, boolean isTracking) { + super(null); this.id = id; this.regatta = regatta; - this.raceDefinitions = raceDefinitions; + this.raceDefinition = raceDefinition; this.isTracking = isTracking; } - public void setIsTracking(boolean isTracking){ + public void setIsTracking(boolean isTracking) { this.isTracking = isTracking; } - public boolean getIsTracking(){ + public boolean getIsTracking() { return isTracking; } public RaceTrackerMock(Long id) { + super(null); this.id = id; } @@ -56,12 +55,12 @@ public class RaceTrackerMock extends AbstractRaceTrackerBaseImpl { } @Override - public Set getRaces() { - return raceDefinitions; + public RaceDefinition getRace() { + return raceDefinition; } @Override - public RaceHandle getRacesHandle() { + public RaceHandle getRaceHandle() { throw new RuntimeException("No race Handle in RaceTrackerMock"); } @@ -82,8 +81,7 @@ public class RaceTrackerMock extends AbstractRaceTrackerBaseImpl { } @Override - public Set getRaceIdentifiers() { - // TODO Auto-generated method stub - return null; + public RegattaAndRaceIdentifier getRaceIdentifier() { + return new RegattaNameAndRaceName(getRegatta().getName(), getRace().getName()); } } diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerStartStopTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerStartStopTest.java index 25ec563ca13..3153eec6445 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerStartStopTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerStartStopTest.java @@ -11,8 +11,8 @@ import static org.mockito.Mockito.mock; import java.io.IOException; import java.net.MalformedURLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.UUID; @@ -42,6 +42,7 @@ import com.sap.sailing.domain.tracking.TrackedRegatta; import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.testsupport.RacingEventServiceImplMock; +import com.sap.sse.common.Util; public class RaceTrackerStartStopTest { @@ -93,21 +94,12 @@ public class RaceTrackerStartStopTest { /* windStore */ EmptyWindStore.INSTANCE, /* delayToLiveInMillis */ 0l, /* millisecondsOverWhichToAverageWind */ 0l, /* millisecondsOverWhichToAverageSpeed */ 0l, /* raceDefinitionSetToUpdate */ null, /*useMarkPassingCalculator*/ false, mock(RaceLogResolver.class)); - Set raceDefinitionSetRace1 = new HashSet(); - raceDefinitionSetRace1.add(raceDef1); - Set raceDefinitionSetRace2 = new HashSet(); - raceDefinitionSetRace2.add(raceDef1); - raceDefinitionSetRace2.add(raceDef2); - Set raceDefinitionSetRace3 = new HashSet(); - raceDefinitionSetRace3.add(raceDef1); - raceDefinitionSetRace3.add(raceDef2); - raceDefinitionSetRace3.add(raceDef3); Long trackerID1 = new Long(1); Long trackerID2 = new Long(2); Long trackerID3 = new Long(3); - raceTracker1 = new RaceTrackerMock(new Long(1), regatta, raceDefinitionSetRace1, true); - raceTracker2 = new RaceTrackerMock(new Long(2), regatta, raceDefinitionSetRace2, true); - raceTracker3 = new RaceTrackerMock(new Long(3), regatta, raceDefinitionSetRace3, true); + raceTracker1 = new RaceTrackerMock(new Long(1), regatta, raceDef1, true); + raceTracker2 = new RaceTrackerMock(new Long(2), regatta, raceDef2, true); + raceTracker3 = new RaceTrackerMock(new Long(3), regatta, raceDef3, true); raceTrackerSet.add(raceTracker1); raceTrackerSet.add(raceTracker2); raceTrackerSet.add(raceTracker3); @@ -118,7 +110,7 @@ public class RaceTrackerStartStopTest { } /** - * This test method tests, if the {@link RacingEventService#stopTracking(Regatta, RaceDefinition) stopTracking} method works correctly. + * This test method tests if the {@link RacingEventService#stopTracking(Regatta, RaceDefinition) stopTracking} method works correctly. */ @Test public void testStopTrackingRace() throws MalformedURLException, IOException, InterruptedException { @@ -142,21 +134,21 @@ public class RaceTrackerStartStopTest { trackedRegatta.unlockTrackedRacesAfterRead(); } assertTrue(foundTrackedRaceForRaceDef2); - // The raceTracker2 and raceTracker3 should currently not be in track mode. + // The raceTracker2 should no longer be in track mode: assertTrue(raceTracker1.getIsTracking()); assertFalse(raceTracker2.getIsTracking()); - assertFalse(raceTracker3.getIsTracking()); + assertTrue(raceTracker3.getIsTracking()); // The RaceTrackersByID map should not contain the trackers raceTracker2 and raceTracker3 anymore assertTrue(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker1)); assertFalse(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker2)); - assertFalse(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker3)); - // The RaceTrakcersByEvent map should contain a tracker with a set of RaceDefinitions, containing the + assertTrue(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker3)); + // The RaceTrackersByEvent map should contain a tracker with a set of RaceDefinitions, containing the // raceDefinition1 assertEquals(1, racingEventService.getRaceTrackersByRegattaMap().size()); Iterator raceTrackerIter = racingEventService.getRaceTrackersByRegattaMap().get(regatta).iterator(); while (raceTrackerIter.hasNext()) { RaceTracker currentTracker = raceTrackerIter.next(); - assertSame(raceTracker1, currentTracker); + assertTrue(Util.contains(Arrays.asList(raceTracker1, raceTracker3), currentTracker)); } } @@ -185,24 +177,19 @@ public class RaceTrackerStartStopTest { trackedRegatta.unlockTrackedRacesAfterRead(); } assertFalse(foundTrackedRaceForRaceDef2); - // The trackers map should still contain the raceTrackers + // The trackers map should still contain the raceTrackers, except for raceTracker2 which should + // have been removed because the race named RACENAME2 was removed, hopefully together with its tracker... assertTrue(racingEventService.getRaceTrackersByRegattaMap().get(regatta).contains(raceTracker1)); - assertTrue(racingEventService.getRaceTrackersByRegattaMap().get(regatta).contains(raceTracker2)); + assertFalse(racingEventService.getRaceTrackersByRegattaMap().get(regatta).contains(raceTracker2)); assertTrue(racingEventService.getRaceTrackersByRegattaMap().get(regatta).contains(raceTracker3)); - // The raceTrackerMap should still contain the raceTrackers. These raceTracker should not contain the raceDefinition raceDef2 anymore + // The raceTrackerMap should still contain the raceTrackers, except the one for race 2 which was removed. assertTrue(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker1)); - assertTrue(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker2)); + assertFalse(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker2)); assertTrue(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker3)); // The raceTracker should still exist; it shall still contain raceDef1 and raceDef2 because a tracker keeps tracking what it tracks... - assertTrue(raceTracker1.getRaces().contains(raceDef1)); - assertEquals(1, raceTracker1.getRaces().size()); - assertTrue(raceTracker2.getRaces().contains(raceDef1)); - assertTrue(raceTracker2.getRaces().contains(raceDef2)); - assertEquals(2, raceTracker2.getRaces().size()); - assertTrue(raceTracker3.getRaces().contains(raceDef1)); - assertTrue(raceTracker3.getRaces().contains(raceDef2)); - assertTrue(raceTracker3.getRaces().contains(raceDef3)); - assertEquals(3, raceTracker3.getRaces().size()); + assertSame(raceTracker1.getRace(), raceDef1); + assertSame(raceTracker2.getRace(), raceDef2); + assertSame(raceTracker3.getRace(), raceDef3); } /** @@ -222,8 +209,7 @@ public class RaceTrackerStartStopTest { assertFalse(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker2)); assertTrue(racingEventService.getRaceTrackersByIDMap().containsValue(raceTracker3)); // The raceTracker 3 should exist, and it should contain all race definitions still - assertTrue(raceTracker3.getRaces().contains(raceDef3)); - assertEquals(3, raceTracker3.getRaces().size()); + assertSame(raceTracker3.getRace(), raceDef3); } } diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerTest.java index 4855f28335d..ec41b6df4e8 100755 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RaceTrackerTest.java @@ -71,9 +71,9 @@ public class RaceTrackerTest { tracTracAdapterFactory = new TracTracAdapterFactoryImpl(); raceHandle = tracTracAdapterFactory.getOrCreateTracTracAdapter(service.getBaseDomainFactory()).addTracTracRace( service, paramUrl, liveUri, storedUri, courseDesignUpdateUri, EmptyRaceLogStore.INSTANCE, - EmptyRegattaLogStore.INSTANCE, - /* timeoutInMilliseconds */60000, tracTracUsername, tracTracPassword, - TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY); + EmptyRegattaLogStore.INSTANCE, /* timeoutInMilliseconds */60000, tracTracUsername, tracTracPassword, + TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, + /* trackWind */ false, /* correctWindDirectionByMagneticDeclination */ false); logger.info("Calling raceHandle.getRaces()"); RaceDefinition race = raceHandle.getRace(); // wait for RaceDefinition to be completely wired in Regatta logger.info("Obtained race: "+race); @@ -130,9 +130,10 @@ public class RaceTrackerTest { service.removeRegatta(raceHandle.getRegatta()); RaceHandle myRaceHandle = tracTracAdapterFactory.getOrCreateTracTracAdapter(service.getBaseDomainFactory()) .addTracTracRace(service, paramUrl, liveUri, storedUri, courseDesignUpdateUri, - EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, - /* timeoutInMilliseconds */60000, tracTracUsername, tracTracPassword, - TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY); + EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, /* timeoutInMilliseconds */60000, + tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, + TracTracConnectionConstants.ONLINE_VISIBILITY, /* trackWind */ false, + /* correctWindDirectionByMagneticDeclination */ false); TrackedRegatta newTrackedRegatta = myRaceHandle.getTrackedRegatta(); assertNotSame(oldTrackedRegatta, newTrackedRegatta); TrackedRace newTrackedRace = getTrackedRace(newTrackedRegatta); @@ -158,9 +159,10 @@ public class RaceTrackerTest { TrackedRace oldTrackedRace = getTrackedRace(oldTrackedRegatta); RaceHandle myRaceHandle = tracTracAdapterFactory.getOrCreateTracTracAdapter(service.getBaseDomainFactory()) .addTracTracRace(service, paramUrl, liveUri, storedUri, courseDesignUpdateUri, - EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, - /* timeoutInMilliseconds */60000, tracTracUsername, tracTracPassword, - TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY); + EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, /* timeoutInMilliseconds */60000, + tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, + TracTracConnectionConstants.ONLINE_VISIBILITY, /* trackWind */ false, + /* correctWindDirectionByMagneticDeclination */ false); TrackedRegatta newTrackedEvent = myRaceHandle.getTrackedRegatta(); TrackedRace newTrackedRace = getTrackedRace(newTrackedEvent); // expecting a new tracked race to be created when starting over with tracking diff --git a/java/com.sap.sailing.server.testsupport/src/com/sap/sailing/server/testsupport/RacingEventServiceImplMock.java b/java/com.sap.sailing.server.testsupport/src/com/sap/sailing/server/testsupport/RacingEventServiceImplMock.java index 479b0e6c5de..541f75462a7 100644 --- a/java/com.sap.sailing.server.testsupport/src/com/sap/sailing/server/testsupport/RacingEventServiceImplMock.java +++ b/java/com.sap.sailing.server.testsupport/src/com/sap/sailing/server/testsupport/RacingEventServiceImplMock.java @@ -23,7 +23,7 @@ public abstract class RacingEventServiceImplMock extends RacingEventServiceImpl private DataImportLockWithProgress lock; public RacingEventServiceImplMock() { - super(false, null); + super(/* clearPersistentCompetitorStore */ false, null, /* restoreTrackedRaces */ false); } public RacingEventServiceImplMock(DataImportProgressImpl dataImportProgressImpl, TypeBasedServiceFinderFactory serviceFinderFactory) { diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch b/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch index 7d74441786a..41554299eb1 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy OSX).launch @@ -34,5 +34,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy).launch b/java/com.sap.sailing.server/SailingServer (No Proxy).launch index c154e498431..180aeaf4a9a 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy).launch @@ -12,6 +12,8 @@ + + @@ -20,7 +22,7 @@ - + @@ -34,5 +36,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch old mode 100755 new mode 100644 index d3d1ded9d9f..18d8a97794a --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch @@ -33,5 +33,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch old mode 100755 new mode 100644 index a669dfb020a..7c71845cba9 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch old mode 100755 new mode 100644 index 1ed9a4ce270..8330b9e7cef --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch index de031ea0787..307474ae25c 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate dev).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch index 3ae8556b313..0b0495b59f4 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, auto-replicate).launch @@ -29,6 +29,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch old mode 100755 new mode 100644 index 3b621dac2b3..4cc03c38d20 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8890).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch old mode 100755 new mode 100644 index e8e168f3770..01e88913426 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch @@ -27,5 +27,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch old mode 100755 new mode 100644 index 9f94a79baeb..bdd1bf9f626 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch @@ -33,5 +33,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy).launch b/java/com.sap.sailing.server/SailingServer (Proxy).launch old mode 100755 new mode 100644 index 48a51f59964..8bfc8b200e7 --- a/java/com.sap.sailing.server/SailingServer (Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy).launch @@ -22,7 +22,7 @@ - + @@ -36,6 +36,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch b/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch old mode 100755 new mode 100644 index 0cd11ffe63f..1a2c017443c --- a/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch @@ -27,6 +27,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch b/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch old mode 100755 new mode 100644 index eda8c26a0e0..22bf4c2f63c --- a/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch @@ -27,5 +27,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch b/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch old mode 100755 new mode 100644 index 3de21aa811d..22d520b13e5 --- a/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch @@ -26,5 +26,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch b/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch old mode 100755 new mode 100644 index b9a9a82d039..7c854bbbb77 --- a/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch @@ -28,6 +28,6 @@ - + diff --git a/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_ja.properties.xlf b/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_ja.properties.xlf index 2e91063e7d6..14ca95ed69a 100644 --- a/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_ja.properties.xlf +++ b/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_ja.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAKVUTW/bMAy9+1cQ2CEbkAVpdwvgQ5YuWIChDZr2sKPs0A1XWTIk2VkQ5L+PkvNhtzZarBfbEh/f4yNlGZHiQt3jk3BO3GU/KlQuNrwJ+/EBSIGpQzDYXx0GoDNAD+Hl9WEQmZA9l4iun+Tz/tvhy5tUEsUaTaKFWZ8YGlsMGrdzmCJqJq02enuPtpTO/iL1/EBOYuw3WTbshmLtXYWmIty+xPgY6GMwQC+svOiAX0AeMFVC7iy90vZNEMdYFDHczaSw1qfMSZHd4HpVJn8wdbHfg0wb8ChIPSz0byMsZEcsfAWFZ0sgKkFSJBKjTzCewJn+VuQ4hKtJ0L9BmxoqHGnVXcB3vd7Ftw3ajiJ4fNxyEAYvoqOGoVWqDc60MeyEhU6emqyt3A6NPg+NIXdaeSH9ATepzgt05LRZMhJt3aCTl9k5GjgKD1kfJwOSFHpaUZ+NYOVC1zuPbsVg4V1y7KJV93+NIW0p9dbeM4de8c5BtLXePFaPBSeQevLH9WRmWv9VHWPdkpRgnTAOrObS3vdTDOF6Ao5y7JYNNuavxXzdDUFRFEb/pVw4lDtgJN9q7MSWyVloobhmBvBn/FuXwTX3CqliKVDaUUZpCHOvjM5hNV3Cirviw+GCcZTaETxocKVRfBtm7awhFDwki1CRJQc7XRooLRrg0jLiaXNZboP+9DxDglJvR5GPL+tw/Oixx0VEeWGIb+FF/Y74WYl0Fy/rd2SFLF1t5qeQUvumDKN/18D20lAGAAA= diff --git a/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_ru.properties.xlf b/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_ru.properties.xlf index a3619291e1c..5002ff1a35c 100644 --- a/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_ru.properties.xlf +++ b/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_ru.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAKVUTW/bMAy9+1cQ2CEbkAVpdwvgQ5YuWIChDZr2sKPs0A1XWTIk2VkQ5L+PkvNhtzZarBfbEh/f4yNlGZHiQt3jk3BO3GU/KlQuNrwJ+/EBSIGpQzDYXx0GoDNAD+Hl9WEQmZA9l4iun+Tz/tvhy5tUEsUaTaKFWZ8YGlsMGrdzmCJqJq02enuPtpTO/iL1/EBOYuw3WTbshmLtXYWmIty+xPgY6GMwQC+svOiAX0AeMFVC7iy90vZNEMdYFDHczaSw1qfMSZHd4HpVJn8wdbHfg0wb8ChIPSz0byMsZEcsfAWFZ0sgKkFSJBKjTzCewJn+VuQ4hKtJ0L9BmxoqHGnVXcB3vd7Ftw3ajiJ4fNxyEAYvoqOGoVWqDc60MeyEhU6emqyt3A6NPg+NIXdaeSH9ATepzgt05LRZMhJt3aCTl9k5GjgKD1kfJwOSFHpaUZ+NYOVC1zuPbsVg4V1y7KJV93+NIW0p9dbeM4de8c5BtLXePFaPBSeQevLH9WRmWv9VHWPdkpRgnTAOrObS3vdTDOF6Ao5y7JYNNuavxXzdDUFRFEb/pVw4lDtgJN9q7MSWyVloobhmBvBn/FuXwTX3CqliKVDaUUZpCHOvjM5hNV3Cirviw+GCcZTaETxocKVRfBtm7awhFDwki1CRJQc7XRooLRrg0jLiaXNZboP+9DxDglJvR5GPL+tw/Oixx0VEeWGIb+FF/Y74WYl0Fy/rd2SFLF1t5qeQUvumDKN/18D20lAGAAA= diff --git a/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_zh_CN.properties.xlf b/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_zh_CN.properties.xlf index 2eb61120d99..812996cb168 100644 --- a/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sailing.server/resources/stringmessages/XLF/StringMessages_zh_CN.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAKVUTW/bMAy9+1cQ2CEbkAVpdwvgQ5YuWIChDZr2sKPs0A1XWTIk2VkQ5L+PkvNhtzZarBfbEh/f4yNlGZHiQt3jk3BO3GU/KlQuNrwJ+/EBSIGpQzDYXx0GoDNAD+Hl9WEQmZA9l4iun+Tz/tvhy5tUEsUaTaKFWZ8YGlsMGrdzmCJqJq02enuPtpTO/iL1/EBOYuw3WTbshmLtXYWmIty+xPgY6GMwQC+svOiAX0AeMFVC7iy90vZNEMdYFDHczaSw1qfMSZHd4HpVJn8wdbHfg0wb8ChIPSz0byMsZEcsfAWFZ0sgKkFSJBKjTzCewJn+VuQ4hKtJ0L9BmxoqHGnVXcB3vd7Ftw3ajiJ4fNxyEAYvoqOGoVWqDc60MeyEhU6emqyt3A6NPg+NIXdaeSH9ATepzgt05LRZMhJt3aCTl9k5GjgKD1kfJwOSFHpaUZ+NYOVC1zuPbsVg4V1y7KJV93+NIW0p9dbeM4de8c5BtLXePFaPBSeQevLH9WRmWv9VHWPdkpRgnTAOrObS3vdTDOF6Ao5y7JYNNuavxXzdDUFRFEb/pVw4lDtgJN9q7MSWyVloobhmBvBn/FuXwTX3CqliKVDaUUZpCHOvjM5hNV3Cirviw+GCcZTaETxocKVRfBtm7awhFDwki1CRJQc7XRooLRrg0jLiaXNZboP+9DxDglJvR5GPL+tw/Oixx0VEeWGIb+FF/Y74WYl0Fy/rd2SFLF1t5qeQUvumDKN/18D20lAGAAA= diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java index 8e8a5c4965c..6596ebe725d 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java @@ -68,11 +68,13 @@ import com.sap.sailing.domain.regattalike.LeaderboardThatHasRegattaLike; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.RaceListener; import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRegatta; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.TrackerManager; import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tracking.WindTracker; import com.sap.sailing.server.masterdata.DataImportLockWithProgress; import com.sap.sailing.server.simulation.SimulationService; import com.sap.sse.common.TimePoint; @@ -191,6 +193,11 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch */ void startTrackingWind(Regatta regatta, RaceDefinition race, boolean correctByDeclination); + /** + * If a {@link WindTracker} exists for {@code race}, it is stopped and the {@link RaceTrackingConnectivityParameters} + * through which the {@code race} was created are updated to not track wind when restoring that race upon a server + * restart. + */ void stopTrackingWind(Regatta regatta, RaceDefinition race) throws SocketException, IOException; /** @@ -632,4 +639,20 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch * yet, the given callback will be informed asynchronously on registration of the RaceTracker in question. */ void getRaceTrackerByRegattaAndRaceIdentifier(RegattaAndRaceIdentifier raceIdentifier, Consumer callback); + + /** + * When restoring tracked races was requested upon creation of this service and after the corresponding restore records + * were read from the persistent store, this method returns the number of races to be restored. Otherwise, it returns 0. + * + * @see #getNumberOfTrackedRacesRestored() + */ + int getNumberOfTrackedRacesToRestore(); + + /** + * When restoring tracked races was requested upon creation of this service, this method tells the number of races + * whose loading process has already been triggered. Otherwise, it returns 0. + * + * @see #getNumberOfTrackedRacesToRestore() + */ + int getNumberOfTrackedRacesRestored(); } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventServiceMXBean.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventServiceMXBean.java index 4d251c924c7..a7399bf3b8c 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventServiceMXBean.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventServiceMXBean.java @@ -9,4 +9,6 @@ package com.sap.sailing.server; */ public interface RacingEventServiceMXBean { public int getNumberOfLeaderboards(); + public int getNumberOfTrackedRacesToRestore(); + public int getNumberOfTrackedRacesRestored(); } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java index 62ba19f87c4..994d463e531 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java @@ -19,11 +19,14 @@ import org.osgi.framework.ServiceRegistration; import org.osgi.util.tracker.ServiceTracker; import org.osgi.util.tracker.ServiceTrackerCustomizer; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.common.tracking.impl.DoubleVectorFixImpl; import com.sap.sailing.domain.common.tracking.impl.GPSFixImpl; import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; import com.sap.sailing.domain.persistence.racelog.tracking.FixMongoHandler; import com.sap.sailing.domain.persistence.racelog.tracking.impl.DoubleVectorFixMongoHandlerImpl; import com.sap.sailing.domain.persistence.racelog.tracking.impl.GPSFixMongoHandlerImpl; @@ -51,6 +54,8 @@ public class Activator implements BundleActivator { private static final Logger logger = Logger.getLogger(Activator.class.getName()); private static final String CLEAR_PERSISTENT_COMPETITORS_PROPERTY_NAME = "persistentcompetitors.clear"; + + private static final String RESTORE_TRACKED_RACES_PROPERTY_NAME = "restore.tracked.races"; private static ExtenderBundleTracker extenderBundleTracker; @@ -59,6 +64,8 @@ public class Activator implements BundleActivator { private RacingEventServiceImpl racingEventService; private final boolean clearPersistentCompetitors; + + private final boolean restoreTrackedRaces; private Set> registrations = new HashSet<>(); @@ -79,6 +86,8 @@ public class Activator implements BundleActivator { public Activator() { clearPersistentCompetitors = Boolean .valueOf(System.getProperty(CLEAR_PERSISTENT_COMPETITORS_PROPERTY_NAME, "" + false)); + restoreTrackedRaces = Boolean + .valueOf(System.getProperty(RESTORE_TRACKED_RACES_PROPERTY_NAME, "" + false)); logger.log(Level.INFO, "setting " + CLEAR_PERSISTENT_COMPETITORS_PROPERTY_NAME + " to " + clearPersistentCompetitors); // there is exactly one instance of the racingEventService in the whole server @@ -100,7 +109,7 @@ public class Activator implements BundleActivator { // instead. serviceFinderFactory = new CachedOsgiTypeBasedServiceFinderFactory(context); racingEventService = new RacingEventServiceImpl(clearPersistentCompetitors, serviceFinderFactory, - trackedRegattaListener, notificationService); + trackedRegattaListener, notificationService, restoreTrackedRaces); notificationService.setRacingEventService(racingEventService); masterDataImportClassLoaderServiceTracker = new ServiceTracker( @@ -114,11 +123,13 @@ public class Activator implements BundleActivator { // register the racing service in the OSGi registry racingEventService.setBundleContext(context); + context.registerService(MongoObjectFactory.class, racingEventService.getMongoObjectFactory(), /* properties */ null); + context.registerService(DomainObjectFactory.class, racingEventService.getDomainObjectFactory(), /* properties */ null); final Dictionary replicableServiceProperties = new Hashtable<>(); - replicableServiceProperties.put(Replicable.OSGi_Service_Registry_ID_Property_Name, - racingEventService.getId().toString()); + replicableServiceProperties.put(Replicable.OSGi_Service_Registry_ID_Property_Name, racingEventService.getId().toString()); context.registerService(Replicable.class.getName(), racingEventService, replicableServiceProperties); context.registerService(RacingEventService.class.getName(), racingEventService, null); + context.registerService(RaceLogResolver.class, racingEventService, null); context.registerService(ClearStateTestSupport.class.getName(), racingEventService, null); Dictionary properties = new Hashtable(); final GPSFixMongoHandlerImpl gpsFixMongoHandler = new GPSFixMongoHandlerImpl( diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/OSGiBasedTrackedRegattaListener.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/OSGiBasedTrackedRegattaListener.java index dec61364235..988d41beeb1 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/OSGiBasedTrackedRegattaListener.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/OSGiBasedTrackedRegattaListener.java @@ -24,9 +24,9 @@ public class OSGiBasedTrackedRegattaListener implements TrackedRegattaListener { } private void forEachListener(Consumer consumer) { - for(ServiceReference serviceReference : serviceTracker.getServiceReferences()) { + for (ServiceReference serviceReference : serviceTracker.getServiceReferences()) { TrackedRegattaListener listener = serviceTracker.getService(serviceReference); - if(listener != null) { + if (listener != null) { consumer.accept(listener); } } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/PersistentCompetitorStore.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/PersistentCompetitorStore.java index 87020418d70..3f0d6a57e25 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/PersistentCompetitorStore.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/PersistentCompetitorStore.java @@ -107,4 +107,10 @@ public class PersistentCompetitorStore extends TransientCompetitorStoreImpl impl storeTo.storeCompetitor(result); return result; } + + @Override + public void addCompetitors(Iterable competitors) { + storeTo.storeCompetitors(competitors); + super.addCompetitors(competitors); + } } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index 06c93aeca4a..878cba359df 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -9,6 +9,7 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.net.MalformedURLException; import java.net.SocketException; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; @@ -31,6 +32,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import java.util.logging.Level; @@ -93,6 +95,7 @@ import com.sap.sailing.domain.common.DataImportSubProgress; import com.sap.sailing.domain.common.Distance; import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; @@ -437,7 +440,13 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes private final TrackedRegattaListener trackedRegattaListener; - private transient ConcurrentHashMap scoreCorrectionListenersByLeaderboard; + private int numberOfTrackedRacesToRestore; + + private final AtomicInteger numberOfTrackedRacesRestored; + + private transient final ConcurrentHashMap scoreCorrectionListenersByLeaderboard; + + private transient final ConcurrentHashMap connectivityParametersByRace; /** * Providing the constructor parameters for a new {@link RacingEventServiceImpl} instance is a bit tricky @@ -464,20 +473,21 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes * compatibility with prior releases that did not support a persistent competitor collection. */ public RacingEventServiceImpl() { - this(true, null); + this(/* clearPersistentCompetitorStore */ true, /* serviceFinderFactory */ null, /* restoreTrackedRaces */ false); } public RacingEventServiceImpl(WindStore windStore, SensorFixStore sensorFixStore, TypeBasedServiceFinderFactory serviceFinderFactory) { - this(true, windStore, sensorFixStore, serviceFinderFactory, /* sailingNotificationService */ null); + this(/* clearPersistentCompetitorStore */ true, windStore, sensorFixStore, serviceFinderFactory, + /* sailingNotificationService */ null, /* restoreTrackedRaces */ false); } void setBundleContext(BundleContext bundleContext) { this.bundleContext = bundleContext; } - public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory) { - this(clearPersistentCompetitorStore, serviceFinderFactory, null, /* sailingNotificationService */ null); + public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory, boolean restoreTrackedRaces) { + this(clearPersistentCompetitorStore, serviceFinderFactory, null, /* sailingNotificationService */ null, restoreTrackedRaces); } /** @@ -493,7 +503,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes * notification service is available, e.g., in test set-ups */ public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory, - TrackedRegattaListener trackedRegattaListener, SailingNotificationService sailingNotificationService) { + TrackedRegattaListener trackedRegattaListener, SailingNotificationService sailingNotificationService, boolean restoreTrackedRaces) { this((final RaceLogResolver raceLogResolver)-> { return new ConstructorParameters() { private final MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory); @@ -506,11 +516,12 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public DomainFactory getBaseDomainFactory() { return competitorStore.getBaseDomainFactory(); } @Override public CompetitorStore getCompetitorStore() { return competitorStore; } }; - }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), null, null, serviceFinderFactory ,trackedRegattaListener, sailingNotificationService); + }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), null, null, serviceFinderFactory ,trackedRegattaListener, sailingNotificationService, restoreTrackedRaces); } private RacingEventServiceImpl(final boolean clearPersistentCompetitorStore, WindStore windStore, - SensorFixStore sensorFixStore, final TypeBasedServiceFinderFactory serviceFinderFactory, SailingNotificationService sailingNotificationService) { + SensorFixStore sensorFixStore, final TypeBasedServiceFinderFactory serviceFinderFactory, + SailingNotificationService sailingNotificationService, boolean restoreTrackedRaces) { this((final RaceLogResolver raceLogResolver)-> { return new ConstructorParameters() { private final MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory); @@ -523,11 +534,11 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public DomainFactory getBaseDomainFactory() { return competitorStore.getBaseDomainFactory(); } @Override public CompetitorStore getCompetitorStore() { return competitorStore; } }; - }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), windStore, sensorFixStore, serviceFinderFactory, null, sailingNotificationService); + }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), windStore, sensorFixStore, serviceFinderFactory, null, sailingNotificationService, restoreTrackedRaces); } public RacingEventServiceImpl(final DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory, - MediaDB mediaDB, WindStore windStore, SensorFixStore sensorFixStore) { + MediaDB mediaDB, WindStore windStore, SensorFixStore sensorFixStore, boolean restoreTrackedRaces) { this((final RaceLogResolver raceLogResolver)-> { return new ConstructorParameters() { @Override public DomainObjectFactory getDomainObjectFactory() { return domainObjectFactory; } @@ -535,7 +546,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public DomainFactory getBaseDomainFactory() { return domainObjectFactory.getBaseDomainFactory(); } @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } }; - }, mediaDB, windStore, sensorFixStore, null, null, /* sailingNotificationService */ null); + }, mediaDB, windStore, sensorFixStore, null, null, /* sailingNotificationService */ null, restoreTrackedRaces); } /** @@ -549,12 +560,23 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes * @param sailingNotificationService * a notification service to call upon events worth notifying users about, or {@code null} if no * notification service is available, e.g., in test set-ups + * @param restoreTrackedRaces + * if {@code true}, the tracking connectivity parameters for the races last loaded in the server are + * {@link DomainObjectFactory#loadConnectivityParametersForRacesToRestore(Consumer) obtained} from the database, + * and {@link RaceTracker}s are + * {@link #addRace(RegattaIdentifier, RaceTrackingConnectivityParameters, long) created} for those, + * effectively restoring the server state to what it was last according to the database. If {@code false}, + * all restore information is + * {@link MongoObjectFactory#removeAllConnectivityParametersForRacesToRestore() cleared} from the + * database, and the server starts out with an empty list of tracked races. */ public RacingEventServiceImpl(Function constructorParametersProvider, MediaDB mediaDb, final WindStore windStore, final SensorFixStore sensorFixStore, TypeBasedServiceFinderFactory serviceFinderFactory, - TrackedRegattaListener trackedRegattaListener, SailingNotificationService sailingNotificationService) { + TrackedRegattaListener trackedRegattaListener, SailingNotificationService sailingNotificationService, boolean restoreTrackedRaces) { logger.info("Created " + this); + this.numberOfTrackedRacesRestored = new AtomicInteger(); this.scoreCorrectionListenersByLeaderboard = new ConcurrentHashMap<>(); + this.connectivityParametersByRace = new ConcurrentHashMap<>(); this.notificationService = sailingNotificationService; final ConstructorParameters constructorParameters = constructorParametersProvider.apply(this); this.domainObjectFactory = constructorParameters.getDomainObjectFactory(); @@ -590,15 +612,13 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes regattaTrackingCache = new ConcurrentHashMap<>(); regattaTrackingCacheLock = new NamedReentrantReadWriteLock("regattaTrackingCache for " + this, /* fair */false); raceTrackersByRegatta = new ConcurrentHashMap<>(); - raceTrackersByRegattaLock = new NamedReentrantReadWriteLock("raceTrackersByRegatta for " + this, /* fair */ - false); + raceTrackersByRegattaLock = new NamedReentrantReadWriteLock("raceTrackersByRegatta for " + this, /* fair */false); raceTrackersByID = new ConcurrentHashMap<>(); raceTrackersByIDLocks = new ConcurrentHashMap<>(); raceTrackerCallbacks = new ConcurrentHashMap<>(); leaderboardGroupsByName = new ConcurrentHashMap<>(); leaderboardGroupsByID = new ConcurrentHashMap<>(); - leaderboardGroupsByNameLock = new NamedReentrantReadWriteLock("leaderboardGroupsByName for " + this, /* fair */ - false); + leaderboardGroupsByNameLock = new NamedReentrantReadWriteLock("leaderboardGroupsByName for " + this, /* fair */false); leaderboardsByName = new ConcurrentHashMap(); leaderboardsByNameLock = new NamedReentrantReadWriteLock("leaderboardsByName for " + this, /* fair */false); operationExecutionListeners = new ConcurrentHashMap<>(); @@ -615,15 +635,12 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes this.sensorFixStore = sensorFixStore == null ? MongoSensorFixStoreFactory.INSTANCE.getMongoGPSFixStore( mongoObjectFactory, domainObjectFactory, serviceFinderFactory) : sensorFixStore; } catch (Exception e) { - e.printStackTrace(); + logger.log(Level.SEVERE, "Exception trying to obtain MongoDB sensor fix store", e); throw new RuntimeException(e); } - this.configurationMap = new DeviceConfigurationMapImpl(); this.serviceFinderFactory = serviceFinderFactory; - this.trackedRegattaListener = trackedRegattaListener == null ? EmptyTrackedRegattaListener.INSTANCE : trackedRegattaListener; - sailingServerConfiguration = domainObjectFactory.loadServerConfiguration(); final Iterable> loadedEventsWithRequireStoreFlag = loadStoredEvents(); loadStoredRegattas(); @@ -633,7 +650,6 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes loadMediaLibary(); loadStoredDeviceConfigurations(); loadAllRemoteSailingServersAndSchedulePeriodicEventCacheRefresh(); - // Stores all events which run through a data migration // Remark: must be called after loadLinksFromEventsToLeaderboardGroups(), otherwise would loose the Event -> LeaderboardGroup relation for (Pair eventAndRequireStoreFlag : loadedEventsWithRequireStoreFlag) { @@ -641,6 +657,29 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes mongoObjectFactory.storeEvent(eventAndRequireStoreFlag.getA()); } } + if (restoreTrackedRaces) { + restoreTrackedRaces(); + } else { + getMongoObjectFactory().removeAllConnectivityParametersForRacesToRestore(); + } + } + + private void restoreTrackedRaces() { + final ScheduledExecutorService backgroundExecutor = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor(); + try { + numberOfTrackedRacesToRestore = getDomainObjectFactory().loadConnectivityParametersForRacesToRestore(params -> { + backgroundExecutor.execute(()->{ + try { + addRace(/* addToRegatta==null means "default regatta" */ null, params, RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); + numberOfTrackedRacesRestored.incrementAndGet(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception trying to restore race"+params, e); + } + }); + }); + } catch (MalformedURLException | URISyntaxException e) { + logger.log(Level.SEVERE, "Exception trying to obtain connectivity parameters for restoring tracked races", e); + } } @Override @@ -1198,8 +1237,8 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes Set trackers = raceTrackersByRegatta.get(regattaContext); if (trackers != null) { for (RaceTracker tracker : trackers) { - final Set races = tracker.getRaces(); - if (races != null && races.contains(r)) { + final RaceDefinition race = tracker.getRace(); + if (race == r) { return true; } } @@ -1429,6 +1468,15 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes } else { cacheAndReplicateDefaultRegatta(tracker.getRegatta()); } + getMongoObjectFactory().addConnectivityParametersForRaceToRestore(params); + // ensure that as soon as the RaceDefinition becomes available, the connectivity params are linked to it in connectivityParametersByRace + tracker.add((RaceTracker t) -> rememberConnectivityParametersForRace(t)); + if (params.isTrackWind()) { + // start wind tracking if requested, as soon as the RaceDefinition becomes available + tracker.add((RaceTracker t) -> + new Thread(()->startTrackingWind(regattaWithName, t.getRace(), params.isCorrectWindDirectionByMagneticDeclination()), + "Starting wind trackers for race "+t.getRace()).start()); + } } else { logger.warning("Race tracker with ID "+trackerID+" already found; not tracking twice to avoid race duplication"); WindStore existingTrackersWindStore = tracker.getWindStore(); @@ -1440,12 +1488,32 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes if (timeoutInMilliseconds != -1) { scheduleAbortTrackerAfterInitialTimeout(tracker, timeoutInMilliseconds); } - return tracker.getRacesHandle(); + return tracker.getRaceHandle(); } finally { unlockRaceTrackersById(trackerID, raceTrackersByIdLock); } } + /** + * Remembers the link between the {@link RaceDefinition} that the {@code tracker} just produced and its + * {@link RaceTracker#getConnectivityParams() connectivity parameters}. This is important for later removing those + * connectivity parameters from the + * {@link MongoObjectFactory#removeConnectivityParametersForRaceToRestore(RaceTrackingConnectivityParameters) DB} + * when the {@link #removeRace(Regatta, RaceDefinition) race is removed}. + * + * @param tracker + * must have produced a {@link RaceDefinition} which can be guaranteed by waiting for the callback on a + * {@link RaceTracker.RaceCreationListener} + * {@link RaceTracker#add(com.sap.sailing.domain.tracking.RaceTracker.RaceCreationListener) registered} + * on that tracker and not calling this method before that listener has fired. + */ + private void rememberConnectivityParametersForRace(RaceTracker tracker) { + final RaceDefinition race = tracker.getRace(); // guaranteed to be != null by callback + assert race != null; + final RaceTrackingConnectivityParameters connectivityParams = tracker.getConnectivityParams(); + connectivityParametersByRace.put(race, connectivityParams); + } + /** * The regatta and all its contained {@link Regatta#getAllRaces() races} are replicated to all replicas. * @@ -1966,11 +2034,9 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes final Set trackersForRegatta = raceTrackersByRegatta.get(regatta); if (trackersForRegatta != null) { for (RaceTracker raceTracker : trackersForRegatta) { - final Set races = raceTracker.getRaces(); - if (races != null) { - for (RaceDefinition race : races) { - stopTrackingWind(regatta, race); - } + final RaceDefinition race = raceTracker.getRace(); + if (race != null) { + stopTrackingWind(regatta, race); } raceTracker.stop(/* preemptive */false); final Object trackerId = raceTracker.getID(); @@ -2033,7 +2099,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes ScheduledFuture task = getScheduler().schedule(new Runnable() { @Override public void run() { - if (tracker.getRaces() == null || tracker.getRaces().isEmpty()) { + if (tracker.getRace() == null) { try { Regatta regatta = tracker.getRegatta(); logger.log(Level.SEVERE, "RaceDefinition for a race in regatta " + regatta.getName() @@ -2073,8 +2139,8 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes Iterator trackerIter = trackerSet.iterator(); while (trackerIter.hasNext()) { RaceTracker raceTracker = trackerIter.next(); - if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) { - logger.info("Found tracker to stop for races " + raceTracker.getRaces()); + if (raceTracker.getRace() == race) { + logger.info("Found tracker to stop for races " + raceTracker.getRace()); raceTracker.stop(/* preemptive */false); trackerIter.remove(); final Object trackerId = raceTracker.getID(); @@ -2090,6 +2156,14 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes logger.warning("Didn't find any trackers for regatta " + regatta); } stopTrackingWind(regatta, race); + final RaceTrackingConnectivityParameters connectivityParams = connectivityParametersByRace.get(race); + // update the "restore" handle for race in DB such that when restoring, no wind tracker will be requested for race + if (connectivityParams != null) { + connectivityParams.setTrackWind(false); + getMongoObjectFactory().addConnectivityParametersForRaceToRestore(connectivityParams); + } else { + logger.warning("Would have expected to find connectivity params for race "+race+" but didn't"); + } // if the last tracked race was removed, confirm that tracking for the entire regatta has stopped if (trackerSet == null || trackerSet.isEmpty()) { stopTracking(regatta); @@ -2189,6 +2263,10 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes public void removeRace(Regatta regatta, RaceDefinition race) throws MalformedURLException, IOException, InterruptedException { logger.info("Removing the race " + race + "..."); + final RaceTrackingConnectivityParameters connectivityParams = connectivityParametersByRace.remove(race); + if (connectivityParams != null) { + getMongoObjectFactory().removeConnectivityParametersForRaceToRestore(connectivityParams); + } stopAllTrackersForWhichRaceIsLastReachable(regatta, race); stopTrackingWind(regatta, race); TrackedRace trackedRace = getExistingTrackedRace(regatta, race); @@ -2258,40 +2336,27 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes Iterator trackerIter = raceTrackersByRegatta.get(regatta).iterator(); while (trackerIter.hasNext()) { RaceTracker raceTracker = trackerIter.next(); - if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) { - boolean foundReachableRace = false; - for (RaceDefinition raceTrackedByTracker : raceTracker.getRaces()) { - if (raceTrackedByTracker != race && isReachable(regatta, raceTrackedByTracker)) { - foundReachableRace = true; - break; - } + if (raceTracker.getRace() == race) { + // firstly stop the tracker + raceTracker.stop(/* preemptive */true); + // remove it from the raceTrackers by Regatta + trackerIter.remove(); + final Object trackerId = raceTracker.getID(); + final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); + try { + raceTrackersByID.remove(trackerId); + } finally { + unlockRaceTrackersById(trackerId, lock); } - if (!foundReachableRace) { - // firstly stop the tracker - raceTracker.stop(/* preemptive */true); - // remove it from the raceTrackers by Regatta - trackerIter.remove(); - final Object trackerId = raceTracker.getID(); - final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); - try { - raceTrackersByID.remove(trackerId); - } finally { - unlockRaceTrackersById(trackerId, lock); - } - // if the last tracked race was removed, remove the entire regatta - if (raceTrackersByRegatta.get(regatta).isEmpty()) { - stopTracking(regatta); - } + // if the last tracked race was removed, remove the entire regatta + if (raceTrackersByRegatta.get(regatta).isEmpty()) { + stopTracking(regatta); } } } } } - private boolean isReachable(Regatta regatta, RaceDefinition race) { - return Util.contains(regatta.getAllRaces(), race); - } - @Override public void startTrackingWind(Regatta regatta, RaceDefinition race, boolean correctByDeclination) { for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) { @@ -2857,6 +2922,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes } finally { LockUtil.unlockAfterWrite(leaderboardsByNameLock); } + connectivityParametersByRace.clear(); eventsById.clear(); mediaLibrary.clear(); competitorStore.clear(); @@ -3482,7 +3548,9 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes Iterable trackedRaces = trackedRegatta.getTrackedRaces(); for (TrackedRace trackedRace : trackedRaces) { trackedRace.setPolarDataService(service); - service.insertExistingFixes(trackedRace); + if (service != null) { + service.insertExistingFixes(trackedRace); + } } } catch (Throwable e) { logger.log(Level.SEVERE, "Error reconstructing the polars for tracked races", e); @@ -3495,9 +3563,8 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes public void unsetPolarDataService(PolarDataService service) { if (polarDataService == service) { - polarDataService.unregisterDomainFactory(baseDomainFactory); polarDataService = null; - setPolarDataService(null); + setPolarDataServiceOnAllTrackedRaces(null); } } @@ -3562,16 +3629,20 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public void getRaceTrackerByRegattaAndRaceIdentifier(RegattaAndRaceIdentifier raceIdentifier, Consumer callback) { + final Regatta regatta; LockUtil.lockForRead(regattasByNameLock); - Regatta regatta = regattasByName.get(raceIdentifier.getRegattaName()); - LockUtil.unlockAfterRead(regattasByNameLock); + try { + regatta = regattasByName.get(raceIdentifier.getRegattaName()); + } finally { + LockUtil.unlockAfterRead(regattasByNameLock); + } if (regatta != null) { LockUtil.lockForRead(raceTrackersByRegattaLock); try { Set raceTrackersForRegatta = raceTrackersByRegatta.get(regatta); - if(raceTrackersForRegatta != null) { + if (raceTrackersForRegatta != null) { for (RaceTracker raceTracker : raceTrackersForRegatta) { - if (raceTracker.getRaceIdentifiers().contains(raceIdentifier)) { + if (Util.equalsWithNull(raceTracker.getRaceIdentifier(), raceIdentifier)) { callback.accept(raceTracker); return; } @@ -3598,15 +3669,26 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes private void notifyListenersForNewRaceTracker(RaceTracker tracker) { LockUtil.lockForRead(raceTrackersByRegattaLock); try { - tracker.getRaceIdentifiers().forEach((raceIdentifier) -> { + final RaceIdentifier raceIdentifier = tracker.getRaceIdentifier(); + if (raceIdentifier != null) { Set> callbacks = getRaceTrackerCallbacks().remove(raceIdentifier); if (callbacks != null) { callbacks.forEach((callback) -> callback.accept(tracker)); } - }); + }; } finally { LockUtil.unlockAfterRead(raceTrackersByRegattaLock); } } + + @Override + public int getNumberOfTrackedRacesToRestore() { + return numberOfTrackedRacesToRestore; + } + + @Override + public int getNumberOfTrackedRacesRestored() { + return numberOfTrackedRacesRestored.get(); + } } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceMXBeanImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceMXBeanImpl.java index ccfff914913..39baae70ae6 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceMXBeanImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceMXBeanImpl.java @@ -19,4 +19,14 @@ public class RacingEventServiceMXBeanImpl implements RacingEventServiceMXBean { public int getNumberOfLeaderboards() { return getRacingEventService().getLeaderboards().size(); } + + @Override + public int getNumberOfTrackedRacesToRestore() { + return getRacingEventService().getNumberOfTrackedRacesToRestore(); + } + + @Override + public int getNumberOfTrackedRacesRestored() { + return getRacingEventService().getNumberOfTrackedRacesRestored(); + } } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/DummyTrackedRace.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/DummyTrackedRace.java index 4bed05fb47a..2b3736f605a 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/DummyTrackedRace.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/DummyTrackedRace.java @@ -27,6 +27,7 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.tracking.GPSFix; @@ -58,7 +59,6 @@ import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.domain.tracking.WindWithConfidence; import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl; import com.sap.sailing.domain.tracking.impl.EmptyWindStore; -import com.sap.sse.common.Duration; import com.sap.sse.common.IsManagedByCache; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -604,7 +604,7 @@ public class DummyTrackedRace extends TrackedRaceWithWindEssentials { } @Override - public Duration getEstimatedTimeToComplete(TimePoint timepoint) + public TargetTimeInfo getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException { return null; } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/StopTrackingRegatta.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/StopTrackingRegatta.java deleted file mode 100755 index 359ce916824..00000000000 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/StopTrackingRegatta.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.sap.sailing.server.operationaltransformation; - -import java.io.IOException; -import java.net.MalformedURLException; - -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.RacingEventServiceOperation; - -public class StopTrackingRegatta extends AbstractRacingEventServiceOperation { - private static final long serialVersionUID = -651066062098923320L; - private final RegattaIdentifier regattaIdentifier; - - public StopTrackingRegatta(RegattaIdentifier regattaIdentifier) { - super(); - this.regattaIdentifier = regattaIdentifier; - } - - @Override - public Void internalApplyTo(RacingEventService toState) throws MalformedURLException, IOException, InterruptedException { - Regatta regatta = toState.getRegatta(regattaIdentifier); - if (regatta != null) { - toState.stopTracking(regatta); - } - return null; - } - - @Override - public RacingEventServiceOperation transformClientOp(RacingEventServiceOperation serverOp) { - // TODO Auto-generated method stub - return null; - } - - @Override - public RacingEventServiceOperation transformServerOp(RacingEventServiceOperation clientOp) { - // TODO Auto-generated method stub - return null; - } - -} diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PolarDiagramGPS.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PolarDiagramGPS.java index 21e724d6612..6da70b8ec64 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PolarDiagramGPS.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PolarDiagramGPS.java @@ -23,7 +23,7 @@ public class PolarDiagramGPS extends PolarDiagramBase { private static final long serialVersionUID = -9219705955440602679L; private final PolarDataService polarData; - private double avgSpeed; + private double avgSpeedInKnots; public PolarDiagramGPS(BoatClass boatClass, PolarDataService polarData) throws SparseSimulationDataException { this.boatClass = boatClass; @@ -53,7 +53,7 @@ public class PolarDiagramGPS extends PolarDiagramBase { SpeedWithBearing beatPort; SpeedWithBearing beatStar; int avgCount = 0; - avgSpeed = 0; + avgSpeedInKnots = 0; for (int i = 0; i < windSpeeds.size(); i++) { try { beatPort = this.polarData.getAverageSpeedWithBearing(this.boatClass, windSpeeds.get(i), LegType.UPWIND, @@ -71,14 +71,14 @@ public class PolarDiagramGPS extends PolarDiagramBase { beatAngles.add(avgBeatAngle); Speed avgBeatSpeed = new KnotSpeedImpl((beatStar.getKnots() + beatPort.getKnots()) / 2.0); beatSpeed.add(avgBeatSpeed); - avgSpeed += avgBeatSpeed.getKnots(); + avgSpeedInKnots += avgBeatSpeed.getKnots(); avgCount++; } else { beatAngles.add(null); beatSpeed.add(null); } } - avgSpeed /= avgCount; + avgSpeedInKnots /= avgCount; // initialize jibe-angles and -speeds SpeedWithBearing jibePort; @@ -175,7 +175,7 @@ public class PolarDiagramGPS extends PolarDiagramBase { } public double getAvgSpeed() { - return this.avgSpeed; + return this.avgSpeedInKnots; } } diff --git a/java/com.sap.sailing.www/images/JConsoleNumberOfTrackedRacesRestored.png b/java/com.sap.sailing.www/images/JConsoleNumberOfTrackedRacesRestored.png new file mode 100644 index 00000000000..b8dee7f697f Binary files /dev/null and b/java/com.sap.sailing.www/images/JConsoleNumberOfTrackedRacesRestored.png differ diff --git a/java/com.sap.sailing.www/images/JConsoleNumberOfTrackedRacesToRestore.png b/java/com.sap.sailing.www/images/JConsoleNumberOfTrackedRacesToRestore.png new file mode 100644 index 00000000000..0b380e80aff Binary files /dev/null and b/java/com.sap.sailing.www/images/JConsoleNumberOfTrackedRacesToRestore.png differ diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index 13fded00127..07355cda68a 100644 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -23,6 +23,86 @@

    Release Notes - Administration Console

    +

    December 2016

    +
      +
    • Filter boxes now support quoting phrases, such as "Race 2" to make them a single search term. + Previously, when entering Race 2 this would match all lines containing Race as well + as all lines containing the digit 2, such as it occurs in 2015 or 2016. + Enclosing the phrase with double quotes as in "Race 2" will consider the full string in quotes + as a search term. In the unlikely case of searching for a double quote you can do that by prefixing + the double quote with a backslash character, as in \". +
    • +
    +

    December 2016

    +
      +
    • Partial replication is now possible. If the system property replicate.on.start + that controls automatic replication upon start-up provides only a partial comma-separated + list of replicables, the connection to the master server (configured by the other + replication-related system properties such as replicate.master.servlet.host etc.) + will be used to replicate only those replicables whose IDs are specified in + replicate.on.start. All other replicables will operate locally. + Usually, the replicate.on.start system property is controlled + by the environment variable REPLICATE_ON_START that is set in the env.sh + configuration file used during server startup and influenced by the server environments + from http://releases.sapsailing.com/environments + and overridden by the user details provided to the instance through the AWS EC2 machinery.

      + The Administration Console (/gwt/AdminConsole.html entry point) will now + show which replicables a replica replicates, both, on the replica's administration + console's "Replication" panel, as well as on the master's "Replication" panel, there + for each replica.

      + The master will restrict broadcasting its operations to those replicables for which + at least one replica has subscribed. While the initial load is requested individually + for each replica and can therefore be tailored to the replicas requested, the operation + broadcast channel is not specific to any replica. It therefore contains the operations + of all replicables for which at least one replica has subscribed. Replicas receiving + operations for replicables for which they did not subscribe will simply drop those + operations (note, however, that the replica will still have to read the operations from the + channel before dropping; this way, registering a replica may have bandwidth effects for + other replicas). Similarly, "backward replication" from a replica to a master will only + take place for those replicables for which the replica has requested replication from (and + therefore also "to") the master.

      + With this it is now possible to replicate only the Security Service, using ID + com.sap.sse.security.impl.SecurityServiceImpl, running all other replicables + in "master mode." This will let the instance share user, permission, role and session management + details with the instance from which the Security Service is being replicated while + having its own master objects for all other replicables, such as the sailing application + (RacingEventService), mail service or the polar data service. Note that with this feature + the server replicating another instance's Security Service will grant the roles and permissions + to subjects authenticated against that replicated Security Service. For example, if a user + has been granted the admin role by that service then the user has that role also + in all other server instances replicating that Security Service. Therefore, use this feature + with extreme caution. Keep in mind that in the near future we will extend the permissions and + role concept by the possibility to restrict permissions and roles to "scopes" such as an + event or a regatta or an account. See also + https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=3504. +

    • +
    • The server can now optionally restore all races it had tracked the last time. This is + similar to a web browser's capability to restore the tabs the user had open when the browser + was terminated or crashed. To enable this feature, use the system property + restore.tracked.races, using the command line option -Drestore.tracked.races=true. + So far, the default for this system property is false, so by default, when the property + is not provided at all on the command line then the tracked races loaded last won't be restored, + and the server will start out with an empty set of tracked races.

      + Note that the wind tracking properties, as set when initially tracking the race, are also restored + to their last state. If a live race has last been tracked with live wind tracking, with wind directions + being corrected by the local magnetic declination, then upon server restore the same live tracking will + be started again, including live wind tracking with the same declination correction settings.

      + When the "Stop Tracking" feature is used, implicitly tracking wind will be stopped in case live wind + tracking had previously been requested for that race. This change is also considered during the restore + process where such a race will no longer receive live wind tracking upon restoring it.

      + When a tracked race is removed from the server then it will not be restored anymore upon the next + server restart with the restore option enabled. When an entire regatta with a number of tracked + races linked into it is removed then the tracked races are removed as well and will not be + restored upon server re-start.

      + The restore progress can be monitored using the JConsole or any other JMX monitoring tool. In the + com.sap.sailing category you will find the RacingEventService object. Among its + attributes there are now the two new ones: NumberOfTrackedRacesToRestore and + NumberOfTrackedRacesRestored. See the following two screen shots:

      +

      + +

    • +
    +

    November 2016

    • When launching a server, a system property polardata.source.url can @@ -33,6 +113,16 @@ should be at roughly the same version to ensure binary compatibility for the polar sheet data.
    • +
    • In the Tracked Races / Competitors tab there is now a new button "Import Competitor" + that opens a dialog for selecting an import source. The Manage2Sail source is configured through + the same URL as used for the result importer. Select the regatta from which you want to import, + then check for which competitors there are matching / similar existing competitors. Single + selection on the left lets you map to one of the matching existing competitors. Ultimately, + select those competitors (including those you mapped to existing ones) you want to import. + Optionally provide a search tag for them which will be added to the new and mapped-to + competitors for easy retrieval later when selecting competitors to add to a regatta + or race. +

    October 2016

    diff --git a/java/com.sap.sailing.xrr.resultimport/src/com/sap/sailing/xrr/resultimport/Parser.java b/java/com.sap.sailing.xrr.resultimport/src/com/sap/sailing/xrr/resultimport/Parser.java index f76ef31cd91..142cb4a8369 100755 --- a/java/com.sap.sailing.xrr.resultimport/src/com/sap/sailing/xrr/resultimport/Parser.java +++ b/java/com.sap.sailing.xrr.resultimport/src/com/sap/sailing/xrr/resultimport/Parser.java @@ -5,6 +5,7 @@ import javax.xml.bind.JAXBException; import com.sap.sailing.xrr.schema.Boat; import com.sap.sailing.xrr.schema.Division; import com.sap.sailing.xrr.schema.Person; +import com.sap.sailing.xrr.schema.Race; import com.sap.sailing.xrr.schema.RegattaResults; import com.sap.sailing.xrr.schema.Team; @@ -18,4 +19,6 @@ public interface Parser { Team getTeam(String teamID); Person getPerson(String personID); + + Race getRace(String raceID); } diff --git a/java/com.sap.sailing.xrr.resultimport/src/com/sap/sailing/xrr/resultimport/impl/ParserImpl.java b/java/com.sap.sailing.xrr.resultimport/src/com/sap/sailing/xrr/resultimport/impl/ParserImpl.java index e8cf151c43e..813dbfa6168 100755 --- a/java/com.sap.sailing.xrr.resultimport/src/com/sap/sailing/xrr/resultimport/impl/ParserImpl.java +++ b/java/com.sap.sailing.xrr.resultimport/src/com/sap/sailing/xrr/resultimport/impl/ParserImpl.java @@ -12,7 +12,9 @@ import javax.xml.bind.Unmarshaller; import com.sap.sailing.xrr.resultimport.Parser; import com.sap.sailing.xrr.schema.Boat; import com.sap.sailing.xrr.schema.Division; +import com.sap.sailing.xrr.schema.Event; import com.sap.sailing.xrr.schema.Person; +import com.sap.sailing.xrr.schema.Race; import com.sap.sailing.xrr.schema.RegattaResults; import com.sap.sailing.xrr.schema.TRResult; import com.sap.sailing.xrr.schema.Team; @@ -22,6 +24,7 @@ public class ParserImpl implements Parser { private final Map personByID; private final Map teamByID; private final Map boatByID; + private final Map raceByID; private final InputStream inputStream; private final String name; @@ -30,6 +33,7 @@ public class ParserImpl implements Parser { this.personByID = new HashMap<>(); this.teamByID = new HashMap<>(); this.boatByID = new HashMap<>(); + this.raceByID = new HashMap<>(); this.inputStream = inputStream; this.name = name; } @@ -50,6 +54,14 @@ public class ParserImpl implements Parser { } else if (o instanceof Team) { Team team = (Team) o; teamByID.put(team.getTeamID(), team); + } else if (o instanceof Event) { + Event event = (Event) o; + for (Object o2 : event.getRaceOrDivisionOrRegattaSeriesResult()) { + if (o2 instanceof Race) { + Race race = (Race) o2; + raceByID.put(race.getRaceID(), race); + } + } } } return regattaResults; @@ -75,8 +87,14 @@ public class ParserImpl implements Parser { return personByID.get(personID); } + @Override + public Race getRace(String raceID) { + return raceByID.get(raceID); + } + @Override public String toString() { return name==null?"":name; } + } diff --git a/java/com.sap.sse.common.test/src/com/sap/sse/common/test/SplittingStringsTest.java b/java/com.sap.sse.common.test/src/com/sap/sse/common/test/SplittingStringsTest.java new file mode 100755 index 00000000000..bc249c98173 --- /dev/null +++ b/java/com.sap.sse.common.test/src/com/sap/sse/common/test/SplittingStringsTest.java @@ -0,0 +1,39 @@ +package com.sap.sse.common.test; + +import static com.sap.sse.common.Util.isEmpty; +import static com.sap.sse.common.Util.splitAlongWhitespaceRespectingDoubleQuotedPhrases; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; + +import org.junit.Test; + +import com.sap.sse.common.Util; + +/** + * Tests {@link Util#splitAlongWhitespaceRespectingDoubleQuotedPhrases(String)} + * + * @author Axel Uhl (d043530) + * + */ +public class SplittingStringsTest { + @Test + public void testSimpleSplit() { + assertNull(splitAlongWhitespaceRespectingDoubleQuotedPhrases(null)); + assertEquals(Arrays.asList("a", "b", "c"), splitAlongWhitespaceRespectingDoubleQuotedPhrases("a b c")); + assertEquals(Arrays.asList("a", "b c"), splitAlongWhitespaceRespectingDoubleQuotedPhrases("a \"b c\"")); + assertEquals(Arrays.asList("a", "b \" c"), splitAlongWhitespaceRespectingDoubleQuotedPhrases("a \"b \\\" c\"")); + assertEquals(Arrays.asList("a", "bc", "de"), splitAlongWhitespaceRespectingDoubleQuotedPhrases("a \"bc\"de")); + assertEquals(Arrays.asList("a", "bc", "de"), splitAlongWhitespaceRespectingDoubleQuotedPhrases("a\"bc\"de ")); + assertEquals(Arrays.asList("a", "b \" c"), splitAlongWhitespaceRespectingDoubleQuotedPhrases("a \"b \\\" c\"")); + assertEquals(Arrays.asList(" "), splitAlongWhitespaceRespectingDoubleQuotedPhrases("\\ ")); + assertEquals(Arrays.asList("\""), splitAlongWhitespaceRespectingDoubleQuotedPhrases(" \\\"")); + assertEquals(Arrays.asList(" \\"), splitAlongWhitespaceRespectingDoubleQuotedPhrases(" \\ \\\\ ")); + assertTrue(isEmpty(splitAlongWhitespaceRespectingDoubleQuotedPhrases(" \n\t "))); + assertEquals(Arrays.asList("abc"), splitAlongWhitespaceRespectingDoubleQuotedPhrases(" \"abc")); // ignore unterminated quote + assertEquals(Arrays.asList("abc "), splitAlongWhitespaceRespectingDoubleQuotedPhrases(" \"abc ")); // ignore unterminated quote + assertEquals(Arrays.asList("afd", "fsd", "fd\\"), splitAlongWhitespaceRespectingDoubleQuotedPhrases("afd fsd fd\\")); // trailing escape character stands for itself + } +} diff --git a/java/com.sap.sse.common.test/src/com/sap/sse/common/test/StringCompareTest.java b/java/com.sap.sse.common.test/src/com/sap/sse/common/test/StringCompareTest.java new file mode 100755 index 00000000000..fb2ccee73c2 --- /dev/null +++ b/java/com.sap.sse.common.test/src/com/sap/sse/common/test/StringCompareTest.java @@ -0,0 +1,26 @@ +package com.sap.sse.common.test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.sap.sse.common.Util; + +public class StringCompareTest { + @Test + public void testStringCompare() { + assertTrue(Util.equalsWithNull("a", "A", /* ignoreCase */ true)); + assertTrue(Util.equalsWithNull("a", "a", /* ignoreCase */ true)); + assertFalse(Util.equalsWithNull("a", "A", /* ignoreCase */ false)); + assertTrue(Util.equalsWithNull(null, null, /* ignoreCase */ true)); + assertFalse(Util.equalsWithNull("a", null, /* ignoreCase */ true)); + assertTrue(Util.equalsWithNull(null, null, /* ignoreCase */ false)); + assertFalse(Util.equalsWithNull("a", null, /* ignoreCase */ false)); + assertFalse(Util.equalsWithNull("A", null, /* ignoreCase */ true)); + assertTrue(Util.equalsWithNull(null, null, /* ignoreCase */ false)); + assertFalse(Util.equalsWithNull("A", null, /* ignoreCase */ false)); + assertFalse(Util.equalsWithNull(null, "b", /* ignoreCase */ true)); + assertFalse(Util.equalsWithNull(null, "B", /* ignoreCase */ true)); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/CountryCode.java b/java/com.sap.sse.common/src/com/sap/sse/common/CountryCode.java index 6bb75ffeedf..948bcddcfdf 100755 --- a/java/com.sap.sse.common/src/com/sap/sse/common/CountryCode.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/CountryCode.java @@ -1,6 +1,8 @@ package com.sap.sse.common; -public interface CountryCode { +import java.io.Serializable; + +public interface CountryCode extends Serializable { String getTwoLetterISOCode(); diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/TimeRange.java b/java/com.sap.sse.common/src/com/sap/sse/common/TimeRange.java index ba15ebc1c6d..14fcdaa8ca0 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/TimeRange.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/TimeRange.java @@ -67,4 +67,14 @@ public interface TimeRange extends Comparable, Serializable { * Intersection of the two ranges, only possible if {@code other} {@link #intersects()} this range. */ TimeRange intersection(TimeRange other); + + /** + * Returns zero, one or two {@link TimeRange}s such that no {@link TimePoint} that is {@link #includes(TimePoint) + * contained} in {@code other} is contained in any of the {@link TimeRange}s returned and that all {@link TimePoint}s + * that are contained in {@code this} {@link TimeRange} are contained in exactly one of the {@link TimeRange}s + * returned, and that the {@link TimeRange}s returned do not {@link #intersects(TimeRange) intersect}. Furthermore, + * in case two {@link TimeRange}s are returned, the first one {@link #startsBefore(TimeRange) is before} the second + * one. + */ + TimeRange[] subtract(TimeRange other); } \ No newline at end of file diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/TypeBasedServiceFinder.java b/java/com.sap.sse.common/src/com/sap/sse/common/TypeBasedServiceFinder.java index 0edde83f64a..94fd620cd0b 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/TypeBasedServiceFinder.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/TypeBasedServiceFinder.java @@ -5,10 +5,10 @@ import java.util.Set; /** * Manages a set of services implementing {@code ServiceType}. Each of these services provides similar functionality * (e.g. a storage service), but for a different {@code type} of input/output (e.g. storage for competitors or GPS - * fixes). + * fixes).

    * * Methods exist for {@link #findService(String) finding a service for a certain type}, or {@link #findAllServices() - * finding all services} that implement {@code ServiceType}. + * finding all services} that implement {@code ServiceType}.

    * * An implementation of this interface might e.g. be backed by the OSGi service registry. For that case, this * OSGi-agnostic interface allows such a {@code TypeBasedServiceFinder} object to be introduced in contexts that are not @@ -21,6 +21,10 @@ import java.util.Set; */ public interface TypeBasedServiceFinder { public static final String TYPE = "type"; + + public interface Callback { + void withService(ServiceType service); + } /** * Find a service that implements the interface for the specified {@code type} of object (e.g. type of @@ -32,6 +36,17 @@ public interface TypeBasedServiceFinder { * Thrown, if no service is registered for this type. */ ServiceType findService(String type) throws NoCorrespondingServiceRegisteredException; + + /** + * Similar to {@link #findService(String)}; when the service for {@code type} is already available, + * the {@code callback} is immediately {@link Callback#withService(Object) invoked} with the service + * found. If the service is not found, instead of throwing a {@link NoCorrespondingServiceRegisteredException}, + * the {@code callback} is recorded by this finder; when at a later point in time a service with {@code type} + * becomes available, all callbacks recorded this way will be {@link Callback#withService(Object) invoked} + * with the service that became available. The method returns immediately after recording the callback in + * this case. + */ + void applyServiceWhenAvailable(String type, Callback callback); Set findAllServices(); diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/TypeBasedServiceFinderFactory.java b/java/com.sap.sse.common/src/com/sap/sse/common/TypeBasedServiceFinderFactory.java index bb63c63b446..bc26d8bee1a 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/TypeBasedServiceFinderFactory.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/TypeBasedServiceFinderFactory.java @@ -1,6 +1,5 @@ package com.sap.sse.common; - /** * A factory for creating {@link TypeBasedServiceFinder}s, for a specific type of service. * The desired service type is specified by the {@link Class} object passed to the diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java old mode 100644 new mode 100755 index ff8bd30a4cb..49330b92752 --- a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java @@ -339,6 +339,12 @@ public class Util { } return result; } + + public static boolean equalsWithNull(String s1, String s2, boolean ignoreCase) { + final String s1LC = ignoreCase?s1==null?null:s1.toLowerCase():s1; + final String s2LC = ignoreCase?s2==null?null:s2.toLowerCase():s2; + return equalsWithNull(s1LC, s2LC); + } /** * null is permissible for both, o1 and o2, where a null value @@ -444,6 +450,99 @@ public class Util { return join(separator, strings); } + /** + * Splits {@code s} along whitespace (blank, tab, line feed, carriage return, form feed) characters that are not + * within a phrase. Phrases are enclosed by double quotes ({@code "}). To make a double quote or any other + * character part of a {@link String} in the result, a backslash ({@code \}) must precede the double quote as an escape character. With this, + * a {@code \} character or a whitespace character can become part of the split result by escaping it with a {@code \} character. If {@code s}'s last character + * happens to be the (unescaped) escape character it stands for itself. + * + * A double quote {@code "} in the middle of an unquoted phrase marks the beginning of a new quoted phrase. When occurring unescaped in + * a quoted phrase, it marks the end of that quoted phrase, and a new unquoted phrase starts. + *

    + * + * The following example expressions all evaluate to {@code true}: + * + *

    +     * {@link #splitAlongWhitespaceRespectingDoubleQuotedPhrases(String) splitAlongWhitespaceRespectingDoubleQuotedPhrases("a b c")}.equals(Arrays.asList("a", "b", "c"))
    +     * {@link #splitAlongWhitespaceRespectingDoubleQuotedPhrases(String) splitAlongWhitespaceRespectingDoubleQuotedPhrases("a \"b c\"")}.equals(Arrays.asList("a", "b c"))
    +     * {@link #splitAlongWhitespaceRespectingDoubleQuotedPhrases(String) splitAlongWhitespaceRespectingDoubleQuotedPhrases("a \"b \\\" c\"")}.equals(Arrays.asList("a", "b \" c"))
    +     * {@link #splitAlongWhitespaceRespectingDoubleQuotedPhrases(String) splitAlongWhitespaceRespectingDoubleQuotedPhrases("a \"bc\"de")}.equals(Arrays.asList("a", "bc", "de"))
    +     * {@link #splitAlongWhitespaceRespectingDoubleQuotedPhrases(String) splitAlongWhitespaceRespectingDoubleQuotedPhrases("a\"bc\" de")}.equals(Arrays.asList("a", "bc", "de"))
    +     * {@link #splitAlongWhitespaceRespectingDoubleQuotedPhrases(String) splitAlongWhitespaceRespectingDoubleQuotedPhrases("\\ ")}.equals(Arrays.asList(" "))
    +     * {@link #splitAlongWhitespaceRespectingDoubleQuotedPhrases(String) splitAlongWhitespaceRespectingDoubleQuotedPhrases("  \\ \\\\ ")}.equals(Arrays.asList(" \\"))
    +     * {@link #isEmpty(Iterable) isEmpty(splitAlongWhitespaceRespectingDoubleQuotedPhrases(" \n\t  "))
    +     * 
    + * + * @return if {@code s==null}, then {@code null}, else a non-{@code null} but possibly empty sequence of {@link Strings} whose iteration order corresponds with + * the occurrence of the split results, left to right, in {@code s} + */ + public static Iterable splitAlongWhitespaceRespectingDoubleQuotedPhrases(String s) { + final char ESCAPE_CHARACTER = '\\'; + final List result; + if (s == null) { + result = null; + } else { + result = new ArrayList<>(); + boolean escaped = false; + StringBuilder phrase = null; + boolean inQuotedPhrase = false; + for (final char c : s.toCharArray()) { + if (escaped) { + if (phrase == null) { + phrase = new StringBuilder(); + } + phrase.append(c); + escaped = false; + } else if (c == ESCAPE_CHARACTER) { + escaped = true; // don't append but mark for next character to be appended + } else if (c == '"') { + if (inQuotedPhrase) { + result.add(phrase.toString()); + inQuotedPhrase = false; + phrase = null; + } else { + inQuotedPhrase = true; + if (phrase != null) { // starts a quoted phrase in the middle of a running phrase + result.add(phrase.toString()); + } + phrase = new StringBuilder(); + } + } else { + if (inQuotedPhrase) { + phrase.append(c); + } else { + if (new String(new char[] {c}).matches("\\s")) { // whitespace + if (phrase != null) { + // phrase is terminated by this whitespace + result.add(phrase.toString()); + phrase = null; + } // else skip whitespace outside of phrases + } else { + if (phrase == null) { + phrase = new StringBuilder(); + } + phrase.append(c); + } + } + } + } + if (escaped) { + // escape character as last character stands for itself + if (phrase == null) { + phrase = new StringBuilder(); + } + phrase.append(ESCAPE_CHARACTER); + } + if (phrase != null) { + // a phrase is also terminated by the end of the string, also (lenient mode) if + // within an (unterminated) quoted phrase + result.add(phrase.toString()); + } + } + return result; + } + /** * Returns the first non-null object in objects or null * if no such object exists. diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/impl/CountryCodeImpl.java b/java/com.sap.sse.common/src/com/sap/sse/common/impl/CountryCodeImpl.java index 807145e63a2..85ce4b8b654 100755 --- a/java/com.sap.sse.common/src/com/sap/sse/common/impl/CountryCodeImpl.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/impl/CountryCodeImpl.java @@ -3,6 +3,8 @@ package com.sap.sse.common.impl; import com.sap.sse.common.CountryCode; public class CountryCodeImpl implements CountryCode { + private static final long serialVersionUID = 74265853281480956L; + private final String name; private final String threeLetterIOCCode; private final String twoLetterISOCode; diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/impl/SingleTypeBasedServiceFinderImpl.java b/java/com.sap.sse.common/src/com/sap/sse/common/impl/SingleTypeBasedServiceFinderImpl.java index 619800444b2..868890533e5 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/impl/SingleTypeBasedServiceFinderImpl.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/impl/SingleTypeBasedServiceFinderImpl.java @@ -7,8 +7,9 @@ import com.sap.sse.common.NoCorrespondingServiceRegisteredException; import com.sap.sse.common.TypeBasedServiceFinder; /** - * Only can provide the service for a single {@code type}, e.g. on smartphones with no OSGi context. - * Optionally, a {@link #setFallbackService(Object) fallback} can be set. + * Only can provide the service for a single {@code type}, e.g. on smartphones with no OSGi context. Optionally, a + * {@link #setFallbackService(Object) fallback} can be set. + * * @author Fredrik Teschke */ public class SingleTypeBasedServiceFinderImpl implements @@ -30,11 +31,23 @@ TypeBasedServiceFinder { @Override public ServiceType findService(String type) throws NoCorrespondingServiceRegisteredException { - if (this.type.equals(type)) return service; - if (fallback != null) return fallback; + if (this.type.equals(type)) { + return service; + } + if (fallback != null) { + return fallback; + } throw new NoCorrespondingServiceRegisteredException("Only one service registered", type, service.getClass().getSimpleName()); } + @Override + public void applyServiceWhenAvailable(String type, Callback callback) { + final ServiceType service = findService(type); + if (service != null) { + callback.withService(service); + } + } + @Override public void setFallbackService(ServiceType fallback) { this.fallback = fallback; diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/impl/TimeRangeImpl.java b/java/com.sap.sse.common/src/com/sap/sse/common/impl/TimeRangeImpl.java index eb4d020fcc0..2bd26fadc63 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/impl/TimeRangeImpl.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/impl/TimeRangeImpl.java @@ -1,5 +1,8 @@ package com.sap.sse.common.impl; +import java.util.ArrayList; +import java.util.List; + import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.TimeRange; @@ -154,6 +157,26 @@ public class TimeRangeImpl extends Util.Pair implements Ti return result; } + @Override + public TimeRange[] subtract(TimeRange other) { + final List result = new ArrayList<>(); + if (this.startsBefore(other)) { + final TimePoint otherFrom = other.from(); + final TimePoint thisTo = this.to(); + final TimePoint to = otherFrom.after(thisTo) ? thisTo : otherFrom.minus(1); + final TimeRange remainder = new TimeRangeImpl(this.from(), to); + result.add(remainder); + } + if (this.endsAfter(other)) { + final TimePoint otherTo = other.to(); + final TimePoint thisFrom = this.from(); + final TimePoint from = otherTo.after(thisFrom) ? otherTo.plus(1) : thisFrom; + final TimeRange remainder = new TimeRangeImpl(from, this.to()); + result.add(remainder); + } + return result.toArray(new TimeRange[result.size()]); + } + @Override public String toString() { return from() + "-" + to(); diff --git a/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_ja.properties.xlf b/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_ja.properties.xlf index e4d0a8f2e45..d6aa3a6d09d 100644 --- a/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_ja.properties.xlf +++ b/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_ja.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAD2MQQoCMQxF9znFLHWnB8hicCXSjYMHKBpLsE2hTQdhmLsbpuoieT958MeZig+EYyc4erAX7ICpJbQB59+cLH8JjqXfnXDKTRS3bQ1KpWKCi2St+BKzMdJdWQL+Yha4Um1RJw7CT6aCy2Eddstx3cP/d5absG6GZTAHH6Y6WeOvAAAA diff --git a/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_ru.properties.xlf b/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_ru.properties.xlf index 0b36531efac..8560071614a 100644 --- a/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_ru.properties.xlf +++ b/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_ru.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAD2MQQoCMQxF9znFLHWnB8hicCXSjYMHKBpLsE2hTQdhmLsbpuoieT958MeZig+EYyc4erAX7ICpJbQB59+cLH8JjqXfnXDKTRS3bQ1KpWKCi2St+BKzMdJdWQL+Yha4Um1RJw7CT6aCy2Eddstx3cP/d5absG6GZTAHH6Y6WeOvAAAA diff --git a/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_zh_CN.properties.xlf b/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_zh_CN.properties.xlf index 06d2355ce79..5d4af88ca94 100644 --- a/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sse.datamining/resources/stringmessages/XLF/StringMessages_zh_CN.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAD2MQQoCMQxF9znFLHWnB8hicCXSjYMHKBpLsE2hTQdhmLsbpuoieT958MeZig+EYyc4erAX7ICpJbQB59+cLH8JjqXfnXDKTRS3bQ1KpWKCi2St+BKzMdJdWQL+Yha4Um1RJw7CT6aCy2Eddstx3cP/d5absG6GZTAHH6Y6WeOvAAAA diff --git a/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_ja.properties.xlf b/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_ja.properties.xlf index 26c2065be18..14952fa3fe4 100644 --- a/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_ja.properties.xlf +++ b/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_ja.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAJ1SwW7bMAy95yt4bNHBPeTcQ4tiQLGiGNrmAxiZTojSkiHKSb3Dvn2klGTddSdL1OPj4/PT9SNpuHsrKRMMLKSQItyP+Ms+b+sO3vcEmXSWwnFXERBxJGAFhIyxTyNsNk+PMMmsUAydMu84ojQwxd4au5Wu70Mg1ae+DmwXsL4rITwQbAXjB5QEsxL8vu3wqLchU0+xMIoCRy2E/fWF6ActTToZrMCJ0Kr/wfgwhw8qL7ZXpfQDpAFa+UxxtUf1M0q2tgXok7V8A7PAnzNEol5B52HgwDYEJsojq3KKer2SFFBOgvOBg7mdMqj5fvbVFVUDK7S5p4tpHDv4Xt93JsY7qHdohf3Esr9xaNXsWgJG2Nqx+mFILLBFpU2Wv7iXVMhG2ZPPCykW1yuLSzHmEx7GWSsi0+DxuLDyVjwUo7HI4hGxMPhY34g+cZyEKnbPdDAF2wXGNMcaIDz1fV3vax8M2QKlBQuHTnFSZPH8BKumZk+mSTi0f3HxoGsGPzTl1Wc/w+b1GW7gsvhdLRx9JZgnSdibvirlyCL/btgYn88TKme9gV/PqXDtbRf7Mbij1R9TuhdHUgMAAA== diff --git a/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_ru.properties.xlf b/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_ru.properties.xlf index 4aba0a7b5e4..06d3cca4f1e 100644 --- a/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_ru.properties.xlf +++ b/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_ru.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAJ1SwW7bMAy95yt4bNHBPeTcQ4tiQLGiGNrmAxiZTojSkiHKSb3Dvn2klGTddSdL1OPj4/PT9SNpuHsrKRMMLKSQItyP+Ms+b+sO3vcEmXSWwnFXERBxJGAFhIyxTyNsNk+PMMmsUAydMu84ojQwxd4au5Wu70Mg1ae+DmwXsL4rITwQbAXjB5QEsxL8vu3wqLchU0+xMIoCRy2E/fWF6ActTToZrMCJ0Kr/wfgwhw8qL7ZXpfQDpAFa+UxxtUf1M0q2tgXok7V8A7PAnzNEol5B52HgwDYEJsojq3KKer2SFFBOgvOBg7mdMqj5fvbVFVUDK7S5p4tpHDv4Xt93JsY7qHdohf3Esr9xaNXsWgJG2Nqx+mFILLBFpU2Wv7iXVMhG2ZPPCykW1yuLSzHmEx7GWSsi0+DxuLDyVjwUo7HI4hGxMPhY34g+cZyEKnbPdDAF2wXGNMcaIDz1fV3vax8M2QKlBQuHTnFSZPH8BKumZk+mSTi0f3HxoGsGPzTl1Wc/w+b1GW7gsvhdLRx9JZgnSdibvirlyCL/btgYn88TKme9gV/PqXDtbRf7Mbij1R9TuhdHUgMAAA== diff --git a/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_zh_CN.properties.xlf b/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_zh_CN.properties.xlf index 4abe5fd1547..8599337a144 100644 --- a/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sse.filestorage/resources/stringmessages/XLF/FileStorageStringMessages_zh_CN.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAJ1SwW7bMAy95yt4bNHBPeTcQ4tiQLGiGNrmAxiZTojSkiHKSb3Dvn2klGTddSdL1OPj4/PT9SNpuHsrKRMMLKSQItyP+Ms+b+sO3vcEmXSWwnFXERBxJGAFhIyxTyNsNk+PMMmsUAydMu84ojQwxd4au5Wu70Mg1ae+DmwXsL4rITwQbAXjB5QEsxL8vu3wqLchU0+xMIoCRy2E/fWF6ActTToZrMCJ0Kr/wfgwhw8qL7ZXpfQDpAFa+UxxtUf1M0q2tgXok7V8A7PAnzNEol5B52HgwDYEJsojq3KKer2SFFBOgvOBg7mdMqj5fvbVFVUDK7S5p4tpHDv4Xt93JsY7qHdohf3Esr9xaNXsWgJG2Nqx+mFILLBFpU2Wv7iXVMhG2ZPPCykW1yuLSzHmEx7GWSsi0+DxuLDyVjwUo7HI4hGxMPhY34g+cZyEKnbPdDAF2wXGNMcaIDz1fV3vax8M2QKlBQuHTnFSZPH8BKumZk+mSTi0f3HxoGsGPzTl1Wc/w+b1GW7gsvhdLRx9JZgnSdibvirlyCL/btgYn88TKme9gV/PqXDtbRf7Mbij1R9TuhdHUgMAAA== diff --git a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/AdminConsole.gwt.xml b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/AdminConsole.gwt.xml index f7eead5536b..6eff7218c8b 100755 --- a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/AdminConsole.gwt.xml +++ b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/AdminConsole.gwt.xml @@ -1,5 +1,6 @@ + diff --git a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsolePanel.java b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsolePanel.java index 271747316c3..d6cf29f7192 100755 --- a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsolePanel.java +++ b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/AdminConsolePanel.java @@ -30,7 +30,6 @@ import com.sap.sse.security.shared.PermissionsForRoleProvider; import com.sap.sse.security.shared.WildcardPermission; import com.sap.sse.security.ui.client.UserService; import com.sap.sse.security.ui.client.UserStatusEventHandler; -import com.sap.sse.security.ui.loginpanel.LoginPanel; import com.sap.sse.security.ui.loginpanel.LoginPanelCss; import com.sap.sse.security.ui.shared.UserDTO; @@ -153,13 +152,7 @@ public class AdminConsolePanel extends DockLayoutPanel implements HandleTabSelec public AdminConsolePanel(UserService userService, PermissionsForRoleProvider permissionsForRoleProvider, ServerInfoRetriever buildVersionRetriever, String releaseNotesAnchorLabel, - String releaseNotesURL, ErrorReporter errorReporter, LoginPanelCss loginPanelCss) { - this(userService, permissionsForRoleProvider, buildVersionRetriever, releaseNotesAnchorLabel, releaseNotesURL, errorReporter, loginPanelCss, true); - } - - public AdminConsolePanel(UserService userService, PermissionsForRoleProvider permissionsForRoleProvider, - ServerInfoRetriever buildVersionRetriever, String releaseNotesAnchorLabel, - String releaseNotesURL, ErrorReporter errorReporter, LoginPanelCss loginPanelCss, boolean withLogin) { + String releaseNotesURL, ErrorReporter errorReporter, LoginPanelCss loginPanelCss, StringMessages stringMessages) { super(Unit.EM); this.permissionsForRoleProvider = permissionsForRoleProvider; this.permissionsAnyOfWhichIsRequiredToSeeWidget = new HashMap<>(); @@ -212,11 +205,8 @@ public class AdminConsolePanel extends DockLayoutPanel implements HandleTabSelec final DockPanel informationPanel = new DockPanel(); informationPanel.setSize("100%", "95%"); informationPanel.setSpacing(10); - if(withLogin) { - informationPanel.add(new LoginPanel(loginPanelCss, getUserService()), DockPanel.WEST); - } informationPanel.add(errorReporter.getPersistentInformationWidget(), DockPanel.CENTER); - SystemInformationPanel sysinfoPanel = new SystemInformationPanel(buildVersionRetriever, errorReporter); + SystemInformationPanel sysinfoPanel = new SystemInformationPanel(buildVersionRetriever, errorReporter, stringMessages); sysinfoPanel.ensureDebugId("SystemInformation"); final Anchor releaseNotesLink = new Anchor(new SafeHtmlBuilder().appendEscaped(releaseNotesAnchorLabel).toSafeHtml(), releaseNotesURL); sysinfoPanel.add(releaseNotesLink); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ReplicationPanel.java b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/ReplicationPanel.java similarity index 78% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ReplicationPanel.java rename to java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/ReplicationPanel.java index 024720136d9..6bf51cef680 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ReplicationPanel.java +++ b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/ReplicationPanel.java @@ -1,30 +1,36 @@ -package com.sap.sailing.gwt.ui.adminconsole; +package com.sap.sse.gwt.adminconsole; +import java.util.Arrays; +import java.util.HashSet; import java.util.Map; +import java.util.Set; +import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CaptionPanel; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.shared.ReplicaDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationMasterDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; import com.sap.sse.common.Util; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.controls.IntegerBox; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; +import com.sap.sse.gwt.client.replication.RemoteReplicationServiceAsync; +import com.sap.sse.gwt.shared.replication.ReplicaDTO; +import com.sap.sse.gwt.shared.replication.ReplicationMasterDTO; +import com.sap.sse.gwt.shared.replication.ReplicationStateDTO; /** * Allows administrators to manage all aspects of server instance replication such as showing whether the instance @@ -37,7 +43,7 @@ public class ReplicationPanel extends FlowPanel { private final Grid registeredReplicas; private final Grid registeredMasters; - private final SailingServiceAsync sailingService; + private final RemoteReplicationServiceAsync replicationServiceAsync; private final ErrorReporter errorReporter; private final StringMessages stringMessages; @@ -45,10 +51,35 @@ public class ReplicationPanel extends FlowPanel { private final Button stopReplicationButton; private final Button removeAllReplicas; - public ReplicationPanel(SailingServiceAsync sailingService, ErrorReporter errorReporter, StringMessages stringMessages) { - this.sailingService = sailingService; + /** + * For administrators it is important to understand whether an instance is a replica. A + * {@link ErrorReporter#reportPersistentInformation(String) persistent error message} shall be shown + * in this case; however, some replicables may not be considered part of the "domain-based" replication. + * For example, if only a security service for user management is replicated in order to implement + * central user and session management then this shall not lead to the warning being displayed. + * This set contains the stringified IDs of those replicables for which being a replica shall + * lead to a warnings. + */ + private final Set replicableIdsAsStringThatShallLeadToWarningAboutInstanceBeingReplica; + + public ReplicationPanel(RemoteReplicationServiceAsync replicationService, ErrorReporter errorReporter, final StringMessages stringMessages) { + this.replicationServiceAsync = replicationService; this.stringMessages = stringMessages; this.errorReporter = errorReporter; + this.replicableIdsAsStringThatShallLeadToWarningAboutInstanceBeingReplica = new HashSet<>(); + replicationService.getReplicableIdsAsStringThatShallLeadToWarningAboutInstanceBeingReplica(new MarkedAsyncCallback<>(new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + GWT.log(stringMessages.errorFetchingReplicaData(caught.getMessage())); + } + + @Override + public void onSuccess(String[] result) { + for (final String replicableIdAsStringThatShallLeadToWarningAboutInstanceBeingReplica : result) { + replicableIdsAsStringThatShallLeadToWarningAboutInstanceBeingReplica.add(replicableIdAsStringThatShallLeadToWarningAboutInstanceBeingReplica); + } + } + })); Button refreshButton = new Button(stringMessages.refresh()); refreshButton.addClickHandler(new ClickHandler() { @@ -113,7 +144,7 @@ public class ReplicationPanel extends FlowPanel { } protected void stopAllReplicas() { - sailingService.stopAllReplicas(new AsyncCallback() { + replicationServiceAsync.stopAllReplicas(new AsyncCallback() { @Override public void onFailure(Throwable caught) { errorReporter.reportError(caught.getMessage()); @@ -129,7 +160,7 @@ public class ReplicationPanel extends FlowPanel { private void stopReplication() { stopReplicationButton.setEnabled(false); - sailingService.stopReplicatingFromMaster(new AsyncCallback() { + replicationServiceAsync.stopReplicatingFromMaster(new AsyncCallback() { @Override public void onFailure(Throwable caught) { errorReporter.reportError(caught.getMessage()); @@ -158,7 +189,7 @@ public class ReplicationPanel extends FlowPanel { registeredMasters.setWidget(0, 0, new Label(stringMessages.loading())); addButton.setEnabled(false); stopReplicationButton.setEnabled(false); - sailingService.startReplicatingFromMaster(masterNameAndExchangeNameAndMessagingPortNumberAndServletPortNumber.getA().getA(), + replicationServiceAsync.startReplicatingFromMaster(masterNameAndExchangeNameAndMessagingPortNumberAndServletPortNumber.getA().getA(), masterNameAndExchangeNameAndMessagingPortNumberAndServletPortNumber.getA().getB(), masterNameAndExchangeNameAndMessagingPortNumberAndServletPortNumber.getA().getC(), masterNameAndExchangeNameAndMessagingPortNumberAndServletPortNumber.getC(), @@ -190,7 +221,7 @@ public class ReplicationPanel extends FlowPanel { } public void updateReplicaList() { - sailingService.getReplicaInfo(new AsyncCallback() { + replicationServiceAsync.getReplicaInfo(new AsyncCallback() { @Override public void onSuccess(ReplicationStateDTO replicas) { int i=0; @@ -206,7 +237,7 @@ public class ReplicationPanel extends FlowPanel { removeReplicaButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - sailingService.stopSingleReplicaInstance(replica.getIdentifier(), new AsyncCallback() { + replicationServiceAsync.stopSingleReplicaInstance(replica.getIdentifier(), new AsyncCallback() { @Override public void onFailure(Throwable caught) { errorReporter.reportError(caught.getMessage()); @@ -222,6 +253,11 @@ public class ReplicationPanel extends FlowPanel { registeredReplicas.setWidget(i, 2, removeReplicaButton); i++; registeredReplicas.insertRow(i); + registeredReplicas.setWidget(i, 1, new Label(stringMessages.replicables())); + HTML replicablesLabel = new HTML(new SafeHtmlBuilder().appendEscapedLines(Arrays.toString(replica.getReplicableIdsAsStrings()).replaceAll(",", "\n")).toSafeHtml()); + registeredReplicas.setWidget(i, 2, replicablesLabel); + i++; + registeredReplicas.insertRow(i); registeredReplicas.setWidget(i, 1, new Label(stringMessages.averageNumberOfOperationsPerMessage())); registeredReplicas.setWidget(i, 2, new Label(""+replica.getAverageNumberOfOperationsPerMessage())); i++; @@ -268,14 +304,19 @@ public class ReplicationPanel extends FlowPanel { registeredMasters.insertRow(i); registeredMasters.setWidget(i, 0, new Label("Client UUID: " + replicas.getServerIdentifier())); i++; - final ReplicationMasterDTO replicatingFromMaster = replicas.getReplicatingFromMaster(); - if (replicatingFromMaster != null) { - errorReporter.reportPersistentInformation(stringMessages.warningServerIsReplica()); + if (replicatingFromMaster != null) { // TODO bug 2465: replicating only the user service from a "domain controller master" shouldn't lead to a warning here... + for (final String replicableIdAsStringOfReplicableThatWeAreReplicatingCurrently : replicatingFromMaster.getReplicableIdsAsString()) { + if (Util.contains(replicableIdsAsStringThatShallLeadToWarningAboutInstanceBeingReplica, replicableIdAsStringOfReplicableThatWeAreReplicatingCurrently)) { + errorReporter.reportPersistentInformation(stringMessages.warningServerIsReplica()); + break; + } + } registeredMasters.insertRow(i); registeredMasters.setWidget(i, 0, new Label(stringMessages.replicatingFromMaster(replicatingFromMaster.getHostname(), replicatingFromMaster.getMessagingPort(), replicatingFromMaster.getServletPort(), - replicatingFromMaster.getMessagingHostname(), replicatingFromMaster.getExchangeName()))); + replicatingFromMaster.getMessagingHostname(), replicatingFromMaster.getExchangeName(), + Arrays.toString(replicatingFromMaster.getReplicableIdsAsString())))); i++; addButton.setEnabled(false); stopReplicationButton.setEnabled(true); diff --git a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/StringMessages.java b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/StringMessages.java index 2d8abd2fa16..5d6def16543 100755 --- a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/StringMessages.java +++ b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/StringMessages.java @@ -18,4 +18,79 @@ public interface StringMessages extends Messages { String pleaseOnlyUploadContentYouHaveAllUsageRightsFor(); String send(); + + String version(String buildVersion); + + String unknown(); + + String refresh(); + + String explainReplicasRegistered(); + + String explainConnectionsToMaster(); + + String stopAllReplicas(); + + String connectToMaster(); + + String stopConnectionToMaster(); + + String errorStartingReplication(String b, String c, String message); + + String loading(); + + String registeredAt(String string); + + String dropReplicaConnection(); + + String replicables(); + + String averageNumberOfOperationsPerMessage(); + + String numberOfQueueMessagesSent(); + + String averageMessageSize(); + + String totalSize(); + + String totalNumberOfOperations(); + + String explainNoConnectionsFromReplicas(); + + String warningServerIsReplica(); + + String replicatingFromMaster(String hostname, int messagingPort, int servletPort, String messagingHostname, + String exchangeName, String string); + + String explainNoConnectionsToMaster(); + + String errorFetchingReplicaData(String message); + + String connect(); + + String enterMaster(); + + String cancel(); + + String hostname(); + + String explainReplicationHostname(); + + String exchangeHost(); + + String explainExchangeHostName(); + + String exchangeName(); + + String explainReplicationExchangeName(); + + String messagingPortNumber(); + + String explainReplicationExchangePort(); + + String servletPortNumber(); + + String explainReplicationServletPort(); + + String ok(); } diff --git a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/SystemInformationPanel.java b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/SystemInformationPanel.java index a3af04cc0d9..baa58b7f6e9 100644 --- a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/SystemInformationPanel.java +++ b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/SystemInformationPanel.java @@ -13,19 +13,19 @@ public class SystemInformationPanel extends FlowPanel { private final Label buildVersionText; - public SystemInformationPanel(final ServerInfoRetriever buildVersionRetriever, final ErrorReporter errorReporter) { + public SystemInformationPanel(final ServerInfoRetriever buildVersionRetriever, final ErrorReporter errorReporter, final StringMessages stringMessages) { super(); buildVersionText = new Label(""); addFloatingWidget(buildVersionText); buildVersionRetriever.getServerInfo(new AsyncCallback() { @Override public void onSuccess(ServerInfoDTO result) { - buildVersionText.setText("Version: " + result.getBuildVersion()); + buildVersionText.setText(stringMessages.version(result.getBuildVersion())); } @Override public void onFailure(Throwable caught) { - buildVersionText.setText("Version: " + "Unknown"); + buildVersionText.setText(stringMessages.version(stringMessages.unknown())); } }); } @@ -34,5 +34,4 @@ public class SystemInformationPanel extends FlowPanel { w.getElement().getStyle().setFloat(Style.Float.LEFT); add(w); } - } diff --git a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF index 20dbb08af24..132d9d93150 100644 --- a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF @@ -39,6 +39,7 @@ Export-Package: com.google.gwt.user.client.rpc.core.java.util, com.sap.sse.gwt.client.mvp, com.sap.sse.gwt.client.panels, com.sap.sse.gwt.client.player, + com.sap.sse.gwt.client.replication, com.sap.sse.gwt.client.sapheader, com.sap.sse.gwt.client.shared.components, com.sap.sse.gwt.client.shared.perspective, @@ -57,6 +58,8 @@ Export-Package: com.google.gwt.user.client.rpc.core.java.util, com.sap.sse.gwt.resources, com.sap.sse.gwt.server, com.sap.sse.gwt.server.filestorage, + com.sap.sse.gwt.server.replication, com.sap.sse.gwt.settings, com.sap.sse.gwt.shared, - com.sap.sse.gwt.shared.filestorage + com.sap.sse.gwt.shared.filestorage, + com.sap.sse.gwt.shared.replication diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DateTimeUtil.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DateTimeUtil.java new file mode 100644 index 00000000000..22f9a63cf69 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DateTimeUtil.java @@ -0,0 +1,24 @@ +package com.sap.sse.gwt.client; + +import java.util.Date; + +import com.sap.sse.common.Duration; +import com.sap.sse.common.impl.MillisecondsDurationImpl; + +/** + * Utility methods mainly for GWT clients for time/date features + * + * @author Axel Uhl (d043530) + * + */ +public class DateTimeUtil { + /** + * @return a duration that is positive for time zones "west" of GMT/UTC and negative for those "east" of GMT/UTC, + * representing the client's time zone. Conceptually, this duration would have to be "added" to the client's + * local time to obtain GMT/UTC. + */ + @SuppressWarnings("deprecation") // uses the deprecated Date.getTimezoneOffset() method but that's also what GWT does and seems the only way + public static Duration getClientTimezoneOffsetFromUTC() { + return new MillisecondsDurationImpl(new Date().getTimezoneOffset() /* that is in minutes */ * 60 * 1000); + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyDisplay.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyDisplay.java new file mode 100644 index 00000000000..c3f3114f585 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyDisplay.java @@ -0,0 +1,10 @@ +package com.sap.sse.gwt.client.controls.busyindicator; + +/** + * Declare that we can change the busy state when some actions are performed. + * + * @author Alexander Tatarinovich + */ +public interface BusyDisplay { + void setBusy(boolean isBusy); +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicator.java index be4e73d90c5..a876fe59375 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicator.java @@ -7,7 +7,7 @@ import com.google.gwt.user.client.ui.FlowPanel; * A widget to show if something is currently busy. * @author Lennart Hensler (D054527) */ -public abstract class BusyIndicator extends FlowPanel { +public abstract class BusyIndicator extends FlowPanel implements BusyDisplay { protected final static String STYLE_NAME_PREFIX = "busyIndicator-"; protected final static BusyIndicatorResources RESOURCES = GWT.create(BusyIndicatorResources.class); @@ -18,6 +18,7 @@ public abstract class BusyIndicator extends FlowPanel { * Sets the busy state of the BusyIndicator, which displays or hides the busy indicator. * @param busy The new busy state */ + @Override public void setBusy(boolean busy) { this.busy = busy; } @@ -25,5 +26,4 @@ public abstract class BusyIndicator extends FlowPanel { public boolean isBusy() { return busy; } - } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ja.properties.xlf b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ja.properties.xlf index 55c102806f4..1c5c5a7aae1 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ja.properties.xlf +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ja.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAH1SzU4jMQy+5yn8BIgXyKEa2MuuAFEEZ5O4naiZZOR4WiG0746TtLNod+Hi/2/8eb4UPJLdqjFMU9b4sTmTD/b+p3GYHEU7NGfQe7vx3pAPYm/VmEJ8JL5lzmw3CagGkJ1bmMnDaQyRAEVomiWkPUgGl5OgE5CRoKOv4CESFgI3kjvAW14YEskp86FOJ3IScgJMHoTfAPcY0pVxMReyQ7WNudADZ0d+YRowxnaGEKxFqNUz30eaY6Bity0D7qlp7Ic8TUsKDivhlyBjH7LtRuXzqQsnbZ+PMHpT0L+3aU73yKb0WLcIYIE+YFxeor/L8kRFlNxMLJXKUMuQsv4YbcC8dlaAfua/87rqi/G+v/IPjv5CdDaNvDZX1K+M/itY1N63uCOGiK/xAi3/YC8DF3hR5XZMZVS1mjcJJ7J3aswR40L2uVrjqTgOc30G9uZP3AXryiiTtAv7hbFNDZ+zqkbVqwreg7Xyowk+50RJ1i7sVOn3699mbu9yS1GfoD0/0tIy8wFPN7vhNwMAAA== diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ru.properties.xlf b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ru.properties.xlf index d66ddccf844..2ee2e51325e 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ru.properties.xlf +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ru.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAH1SzU4jMQy+5yn8BIgXyKEa2MuuAFEEZ5O4naiZZOR4WiG0746TtLNod+Hi/2/8eb4UPJLdqjFMU9b4sTmTD/b+p3GYHEU7NGfQe7vx3pAPYm/VmEJ8JL5lzmw3CagGkJ1bmMnDaQyRAEVomiWkPUgGl5OgE5CRoKOv4CESFgI3kjvAW14YEskp86FOJ3IScgJMHoTfAPcY0pVxMReyQ7WNudADZ0d+YRowxnaGEKxFqNUz30eaY6Bity0D7qlp7Ic8TUsKDivhlyBjH7LtRuXzqQsnbZ+PMHpT0L+3aU73yKb0WLcIYIE+YFxeor/L8kRFlNxMLJXKUMuQsv4YbcC8dlaAfua/87rqi/G+v/IPjv5CdDaNvDZX1K+M/itY1N63uCOGiK/xAi3/YC8DF3hR5XZMZVS1mjcJJ7J3aswR40L2uVrjqTgOc30G9uZP3AXryiiTtAv7hbFNDZ+zqkbVqwreg7Xyowk+50RJ1i7sVOn3699mbu9yS1GfoD0/0tIy8wFPN7vhNwMAAA== diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_zh_CN.properties.xlf b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_zh_CN.properties.xlf index 2033ee0dce4..4d48de6efa0 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_zh_CN.properties.xlf @@ -1,6 +1,6 @@ - +
    H4sIAAAAAAAAAH1SzU4jMQy+5yn8BIgXyKEa2MuuAFEEZ5O4naiZZOR4WiG0746TtLNod+Hi/2/8eb4UPJLdqjFMU9b4sTmTD/b+p3GYHEU7NGfQe7vx3pAPYm/VmEJ8JL5lzmw3CagGkJ1bmMnDaQyRAEVomiWkPUgGl5OgE5CRoKOv4CESFgI3kjvAW14YEskp86FOJ3IScgJMHoTfAPcY0pVxMReyQ7WNudADZ0d+YRowxnaGEKxFqNUz30eaY6Bity0D7qlp7Ic8TUsKDivhlyBjH7LtRuXzqQsnbZ+PMHpT0L+3aU73yKb0WLcIYIE+YFxeor/L8kRFlNxMLJXKUMuQsv4YbcC8dlaAfua/87rqi/G+v/IPjv5CdDaNvDZX1K+M/itY1N63uCOGiK/xAi3/YC8DF3hR5XZMZVS1mjcJJ7J3aswR40L2uVrjqTgOc30G9uZP3AXryiiTtAv7hbFNDZ+zqkbVqwreg7Xyowk+50RJ1i7sVOn3699mbu9yS1GfoD0/0tIy8wFPN7vhNwMAAA== diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/panels/AbstractFilterablePanel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/panels/AbstractFilterablePanel.java index 48c9da24ca4..f1ee5267bb8 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/panels/AbstractFilterablePanel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/panels/AbstractFilterablePanel.java @@ -1,7 +1,6 @@ package com.sap.sse.gwt.client.panels; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -13,6 +12,7 @@ import com.google.gwt.user.cellview.client.CellTable; import com.google.gwt.user.cellview.client.ColumnSortEvent; import com.google.gwt.user.client.ui.HasVerticalAlignment; import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.view.client.ListDataProvider; import com.sap.sse.common.Util; @@ -177,7 +177,7 @@ public abstract class AbstractFilterablePanel extends HorizontalPanel { public void filter() { filtered.getList().clear(); retainElementsInFilteredThatPassFilter(); - filtered.refresh(); + filtered.flush(); sort(); } @@ -211,7 +211,7 @@ public abstract class AbstractFilterablePanel extends HorizontalPanel { getTextBox().addKeyUpHandler(new KeyUpHandler() { @Override public void onKeyUp(KeyUpEvent event) { - filterer.setKeywords(Arrays.asList(getTextBox().getText().split(" "))); + filterer.setKeywords(Util.splitAlongWhitespaceRespectingDoubleQuotedPhrases(getTextBox().getText())); filter(); } }); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/replication/RemoteReplicationService.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/replication/RemoteReplicationService.java new file mode 100644 index 00000000000..287b497acc2 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/replication/RemoteReplicationService.java @@ -0,0 +1,17 @@ +package com.sap.sse.gwt.client.replication; + +import com.sap.sse.gwt.shared.replication.ReplicationStateDTO; + +public interface RemoteReplicationService { + ReplicationStateDTO getReplicaInfo(); + + void startReplicatingFromMaster(String messagingHost, String masterName, String exchangeName, int servletPort, int messagingPort) throws Exception; + + void stopAllReplicas(); + + void stopReplicatingFromMaster(); + + void stopSingleReplicaInstance(String identifier); + + String[] getReplicableIdsAsStringThatShallLeadToWarningAboutInstanceBeingReplica(); +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/replication/RemoteReplicationServiceAsync.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/replication/RemoteReplicationServiceAsync.java new file mode 100644 index 00000000000..f8a6347d429 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/replication/RemoteReplicationServiceAsync.java @@ -0,0 +1,20 @@ +package com.sap.sse.gwt.client.replication; + +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sse.gwt.shared.replication.ReplicationStateDTO; + +public interface RemoteReplicationServiceAsync { + void getReplicaInfo(AsyncCallback callback); + + void startReplicatingFromMaster(String messagingHost, String masterName, String exchangeName, int servletPort, + int messagingPort, AsyncCallback callback); + + void stopAllReplicas(AsyncCallback asyncCallback); + + void stopReplicatingFromMaster(AsyncCallback asyncCallback); + + void stopSingleReplicaInstance(String identifier, AsyncCallback asyncCallback); + + void getReplicableIdsAsStringThatShallLeadToWarningAboutInstanceBeingReplica(AsyncCallback markedAsyncCallback); + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicaDTO.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/shared/replication/ReplicaDTO.java similarity index 75% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicaDTO.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/shared/replication/ReplicaDTO.java index 21019522bf6..6476d9fdcf2 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicaDTO.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/shared/replication/ReplicaDTO.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.ui.shared; +package com.sap.sse.gwt.shared.replication; import java.util.Date; import java.util.Map; @@ -14,12 +14,14 @@ public class ReplicaDTO implements IsSerializable { private long numberOfMessagesSent; private double averageMessageSizeInBytes; private long totalNumberOfBytesSent; + private String[] replicableIdsAsStrings; ReplicaDTO() {} public ReplicaDTO(String hostname, Date registrationTime, - String identifier, Map operationCountByOperationClassName, double averageNumberOfOperationsPerMessage, - long numberOfMessagesSent, long totalNumberOfBytesSent, double averageMessageSizeInBytes) { + String identifier, String[] replicableIdsAsStrings, Map operationCountByOperationClassName, + double averageNumberOfOperationsPerMessage, long numberOfMessagesSent, long totalNumberOfBytesSent, double averageMessageSizeInBytes) { this.hostname = hostname; this.identifier = identifier; + this.replicableIdsAsStrings = replicableIdsAsStrings; this.registrationTime = registrationTime; this.operationCountByOperationClassName = operationCountByOperationClassName; this.averageNumberOfOperationsPerMessage = averageNumberOfOperationsPerMessage; @@ -51,4 +53,7 @@ public class ReplicaDTO implements IsSerializable { public String getIdentifier() { return identifier; } + public String[] getReplicableIdsAsStrings() { + return replicableIdsAsStrings; + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationMasterDTO.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/shared/replication/ReplicationMasterDTO.java similarity index 65% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationMasterDTO.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/shared/replication/ReplicationMasterDTO.java index f9a4591133d..f10bf04b713 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationMasterDTO.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/shared/replication/ReplicationMasterDTO.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.ui.shared; +package com.sap.sse.gwt.shared.replication; import com.google.gwt.user.client.rpc.IsSerializable; @@ -8,28 +8,45 @@ public class ReplicationMasterDTO implements IsSerializable { private int servletPort; private String messagingHostname; private String exchangeName; - ReplicationMasterDTO() {} - public ReplicationMasterDTO(String hostname, int servletPort, String messagingHostname, int messagingPort, String exchangeName) { + private String[] replicableIdsAsString; + + // TODO bug 2465: capture the set of replicables that are replicated from this master; this may be a subset of the + // replica's / master's Replicables + + ReplicationMasterDTO() { + } + + public ReplicationMasterDTO(String hostname, int servletPort, String messagingHostname, int messagingPort, String exchangeName, String[] replicableIdsAsString) { super(); this.hostname = hostname; this.servletPort = servletPort; this.messagingHostname = messagingHostname; this.messagingPort = messagingPort; this.exchangeName = exchangeName; + this.replicableIdsAsString = replicableIdsAsString; } + public String getHostname() { return hostname; } + public int getMessagingPort() { return messagingPort; } + public int getServletPort() { return servletPort; } + public String getMessagingHostname() { return messagingHostname; } + public String getExchangeName() { return exchangeName; } + + public String[] getReplicableIdsAsString() { + return replicableIdsAsString; + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationStateDTO.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/shared/replication/ReplicationStateDTO.java similarity index 93% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationStateDTO.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/shared/replication/ReplicationStateDTO.java index fc4d3a3fa02..04004a5a175 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationStateDTO.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/shared/replication/ReplicationStateDTO.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.ui.shared; +package com.sap.sse.gwt.shared.replication; import java.util.HashMap; import java.util.Map; diff --git a/java/com.sap.sse.replication.testsupport/src/com/sap/sse/replication/testsupport/AbstractServerReplicationTestSetUp.java b/java/com.sap.sse.replication.testsupport/src/com/sap/sse/replication/testsupport/AbstractServerReplicationTestSetUp.java index df30b57c3dd..95adc3baedc 100644 --- a/java/com.sap.sse.replication.testsupport/src/com/sap/sse/replication/testsupport/AbstractServerReplicationTestSetUp.java +++ b/java/com.sap.sse.replication.testsupport/src/com/sap/sse/replication/testsupport/AbstractServerReplicationTestSetUp.java @@ -17,12 +17,12 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.URL; import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; -import net.jpountz.lz4.LZ4BlockOutputStream; - import org.junit.Rule; import org.junit.rules.Timeout; @@ -41,6 +41,8 @@ import com.sap.sse.replication.impl.ReplicationReceiver; import com.sap.sse.replication.impl.ReplicationServiceImpl; import com.sap.sse.replication.impl.SingletonReplicablesProvider; +import net.jpountz.lz4.LZ4BlockOutputStream; + public abstract class AbstractServerReplicationTestSetUp, ReplicableImpl extends ReplicableInterface> { private static final Logger logger = Logger.getLogger(AbstractServerReplicationTestSetUp.class.getName()); @@ -147,12 +149,12 @@ public abstract class AbstractServerReplicationTestSetUp replicaReplicator = new ReplicationServiceTestImpl(exchangeName, exchangeHost, rim, replicaDescriptor, - this.replica, this.master, masterReplicator, masterDescriptor); + this.replica, this.master, masterReplicator); masterDescriptor = replicaReplicator.getMasterDescriptor(); servletPort = masterDescriptor.getServletPort(); Pair, ReplicationMasterDescriptor> result = new Pair<>(replicaReplicator, masterDescriptor); @@ -236,15 +238,19 @@ public abstract class AbstractServerReplicationTestSetUp> replicablesToReplicate = new ArrayList<>(); + for (final String replicableIdAsString : replicaDescriptor.getReplicableIdsAsStrings()) { + replicablesToReplicate.add(getReplicablesProvider().getReplicable(replicableIdAsString, /* wait */ false)); + } this.masterDescriptor = new ReplicationMasterDescriptorImpl(exchangeHost, exchangeName, /* messagingPort */ 0, - UUID.randomUUID().toString(), "localhost", ss.getLocalPort()); + UUID.randomUUID().toString(), "localhost", ss.getLocalPort(), replicablesToReplicate); } ReplicationMasterDescriptor getMasterDescriptor() { diff --git a/java/com.sap.sse.replication/META-INF/MANIFEST.MF b/java/com.sap.sse.replication/META-INF/MANIFEST.MF index ded07e7ce34..9448e8b758b 100644 --- a/java/com.sap.sse.replication/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.replication/META-INF/MANIFEST.MF @@ -15,6 +15,7 @@ Require-Bundle: com.sap.sse.common, lz4-java;bundle-version="1.3.0", org.apache.commons.lang Bundle-ClassPath: . +Comment: The Web-ContextPath is referred to by ReplicationMasterDescriptorImpl.REPLICATION_SERVLET Web-ContextPath: /replication Import-Package: com.rabbitmq.client;version="2.8.4", javax.servlet;version="2.6.0", diff --git a/java/com.sap.sse.replication/WEB-INF/web.xml b/java/com.sap.sse.replication/WEB-INF/web.xml index 1c632f3388e..0f3b19a152c 100755 --- a/java/com.sap.sse.replication/WEB-INF/web.xml +++ b/java/com.sap.sse.replication/WEB-INF/web.xml @@ -22,6 +22,7 @@ Replication + /replication diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationFactory.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationFactory.java index bbf04436a26..f6e786f11bb 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationFactory.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationFactory.java @@ -5,5 +5,5 @@ import com.sap.sse.replication.impl.ReplicationFactoryImpl; public interface ReplicationFactory { static ReplicationFactory INSTANCE = new ReplicationFactoryImpl(); - ReplicationMasterDescriptor createReplicationMasterDescriptor(String messagingHostname, String hostname, String exchangeName, int servletPort, int jmsPort, String jmsQueueName); + ReplicationMasterDescriptor createReplicationMasterDescriptor(String messagingHostname, String hostname, String exchangeName, int servletPort, int jmsPort, String jmsQueueName, Iterable> replicables); } diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationMasterDescriptor.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationMasterDescriptor.java index 7b038be9ffc..614bde58bc1 100644 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationMasterDescriptor.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationMasterDescriptor.java @@ -14,6 +14,10 @@ import com.sap.sse.replication.impl.ReplicationServlet; /** * Identifies a master server instance from which a replica can obtain an initial load and continuous updates. * + * TODO bug 2465: add the set of {@link Replicable}s that are replicated from the master represented by this descriptor, + * considering that this may be a subset only of the replicables running on this instance or the master server. Example: + * replicating only the SecurityService from some other server but being a master regarding all other Replicables. + * * @author Frank Mittag, Axel Uhl (d043530) * */ @@ -64,4 +68,6 @@ public interface ReplicationMasterDescriptor { */ Channel createChannel() throws IOException; + Iterable> getReplicables(); + } diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationService.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationService.java index 1aa878672e8..f3626ed1372 100644 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationService.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationService.java @@ -31,22 +31,20 @@ public interface ReplicationService { ReplicationMasterDescriptor getReplicatingFromMaster(); /** - * Same as {@link #startToReplicateFrom(ReplicationMasterDescriptor, Iterable)}, where the list of replicables - * is determined by asking this service's {@link ReplicablesProvider} which replicables are currently available. - * Note that this is not a good idea during the OSGi start-up phase, particularly if there is a bundle that - * starts after this bundle because then that {@link Replicable} will not yet be registered and won't become part - * of replication. - */ - void startToReplicateFrom(ReplicationMasterDescriptor master) throws IOException, ClassNotFoundException, InterruptedException; - - /** - * Performs a servlet request to the remote master server's {@link ReplicationServlet}, first registering this - * replica, ensuring the message queue for replicating operations is created, then subscribing to the master's - * replication message queue and asking the servlet for the stream containing the initial load which will then be - * used to {@link Replicable#initiallyFillFrom(java.io.ObjectInputStream) replace} the {@link Replicable replicables}' + * Starts to replicate the {@link Replicables} that the {@code master} descriptor + * {@link ReplicationMasterDescriptor#getReplicables() specifies}. Performs a servlet request to the remote master + * server's {@link ReplicationServlet}, first registering this replica, ensuring the message queue for replicating + * operations is created, then subscribing to the master's replication message queue and asking the servlet for the + * stream containing the initial load for the specific {@link Replicables} which will then be used to + * {@link Replicable#initiallyFillFrom(java.io.ObjectInputStream) replace} those {@link Replicable replicables}' * state. + *

    + * + * Note that calling this method is not a good idea during the OSGi start-up phase, particularly if there is a + * bundle that starts after this bundle because then that {@link Replicable} will not yet be registered and won't + * become part of replication. */ - void startToReplicateFrom(ReplicationMasterDescriptor master, Iterable> replicables) throws IOException, + void startToReplicateFrom(ReplicationMasterDescriptor master) throws IOException, ClassNotFoundException, InterruptedException; /** @@ -62,6 +60,11 @@ public interface ReplicationService { * {@link #registerReplica(ReplicaDescriptor) registered} again. */ void unregisterReplica(ReplicaDescriptor replica) throws IOException; + + /** + * Same as {@link #unregisterReplica(ReplicaDescriptor)}, identifying the replica by its {@link ReplicaDescriptor#getUuid() ID}. + */ + ReplicaDescriptor unregisterReplica(UUID replicaId) throws IOException; /** * For a replica replicating off this master, provides statistics in the form of number of operations sent to that @@ -80,7 +83,7 @@ public interface ReplicationService { /** * Stops all replica currently registered with this server. */ - void stopAllReplica() throws IOException; + void stopAllReplicas() throws IOException; /** * Returns an unique server identifier @@ -94,4 +97,6 @@ public interface ReplicationService { long getNumberOfBytesSent(ReplicaDescriptor replica); double getAverageNumberOfBytesPerMessage(ReplicaDescriptor replica); + + Iterable> getAllReplicables(); } diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Activator.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Activator.java index bc969eb1664..d143937243e 100644 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Activator.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Activator.java @@ -155,9 +155,9 @@ public class Activator implements BundleActivator { Integer.valueOf(System.getProperty(PROPERTY_NAME_REPLICATE_MASTER_QUEUE_PORT).trim()), serverReplicationMasterService.getServerIdentifier().toString(), System.getProperty(PROPERTY_NAME_REPLICATE_MASTER_SERVLET_HOST), - Integer.valueOf(System.getProperty(PROPERTY_NAME_REPLICATE_MASTER_SERVLET_PORT).trim())); + Integer.valueOf(System.getProperty(PROPERTY_NAME_REPLICATE_MASTER_SERVLET_PORT).trim()), replicables); try { - serverReplicationMasterService.startToReplicateFrom(master, replicables); + serverReplicationMasterService.startToReplicateFrom(master); logger.info("Automatic replication has been started."); } catch (ClassNotFoundException | IOException | InterruptedException e) { logger.log(Level.SEVERE, "Error with automatic replication from "+master, e); @@ -173,7 +173,7 @@ public class Activator implements BundleActivator { serverReplicationMasterService.stopToReplicateFromMaster(); } // stop sending stuff to the exchange for other replicas (if this is a master) - serverReplicationMasterService.stopAllReplica(); + serverReplicationMasterService.stopAllReplicas(); } public static BundleContext getDefaultContext() { diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicaDescriptor.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicaDescriptor.java index db2a3d3562b..3051ffb5c8b 100644 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicaDescriptor.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicaDescriptor.java @@ -6,6 +6,7 @@ import java.util.UUID; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.replication.Replicable; /** * Describes a replica by remembering its IP address as well as the replication time and a UUID. Hash code and equality @@ -21,15 +22,18 @@ public class ReplicaDescriptor implements Serializable { private final InetAddress ipAddress; private final TimePoint registrationTime; private final String additionalInformation; + private final String[] replicableIdsAsStrings; /** * Sets the registration time to now. */ - public ReplicaDescriptor(InetAddress ipAddress, UUID serverUuid, String additionalInformation) { + public ReplicaDescriptor(InetAddress ipAddress, UUID serverUuid, String additionalInformation, String[] replicableIdsAsStrings) { + assert replicableIdsAsStrings != null && replicableIdsAsStrings.length > 0; this.uuid = serverUuid; this.registrationTime = MillisecondsTimePoint.now(); this.ipAddress = ipAddress; this.additionalInformation = additionalInformation; + this.replicableIdsAsStrings = replicableIdsAsStrings; } public UUID getUuid() { @@ -48,6 +52,17 @@ public class ReplicaDescriptor implements Serializable { return additionalInformation; } + /** + * The {@link Replicable#getId() IDs} of the replicables that the replica represented by this descriptor + * has requested from the master for replication. The master may send operations for a superset of those + * replicables in case other replicas have requested replication for other replicables. Therefore, the + * replica must filter the operations received for those replicable IDs it has been requesting replication + * for. + */ + public String[] getReplicableIdsAsStrings() { + return replicableIdsAsStrings; + } + @Override public int hashCode() { final int prime = 31; @@ -74,7 +89,7 @@ public class ReplicaDescriptor implements Serializable { } public String toString() { - return ""+uuid+": "+ipAddress+" ("+additionalInformation+")"; + return ""+uuid+": "+ipAddress+" ("+additionalInformation+") for replicables "+String.join(", ", getReplicableIdsAsStrings()); } } diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java index 1c2837b30ea..ae92c9a057e 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java @@ -226,6 +226,10 @@ public interface ReplicableWithObjectInputStream operationWithResultWithIdWrapper); + /** + * @return the descriptor of the master from which this replica is replicating this {@link Replicable}, or + * {@code null} if this {@link Replicable} is currently running as a master. + */ ReplicationMasterDescriptor getMasterDescriptor(); /** diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationFactoryImpl.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationFactoryImpl.java index 50bd889f690..90db95a2d38 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationFactoryImpl.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationFactoryImpl.java @@ -1,12 +1,15 @@ package com.sap.sse.replication.impl; +import com.sap.sse.replication.Replicable; import com.sap.sse.replication.ReplicationFactory; import com.sap.sse.replication.ReplicationMasterDescriptor; public class ReplicationFactoryImpl implements ReplicationFactory { @Override - public ReplicationMasterDescriptor createReplicationMasterDescriptor(String messagingHostname, String hostname, String exchangeName, int servletPort, int messagingPort, String queueName) { - return new ReplicationMasterDescriptorImpl(messagingHostname, exchangeName, messagingPort, queueName, hostname, servletPort); + public ReplicationMasterDescriptor createReplicationMasterDescriptor(String messagingHostname, String hostname, + String exchangeName, int servletPort, int messagingPort, String queueName, + Iterable> replicables) { + return new ReplicationMasterDescriptorImpl(messagingHostname, exchangeName, messagingPort, queueName, hostname, servletPort, replicables); } } diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationInstancesManager.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationInstancesManager.java index f9b2517cc32..423194c3c58 100644 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationInstancesManager.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationInstancesManager.java @@ -1,11 +1,12 @@ package com.sap.sse.replication.impl; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.UUID; import com.sap.sse.common.Util; import com.sap.sse.replication.OperationWithResult; @@ -22,9 +23,10 @@ public class ReplicationInstancesManager { /** * The set of descriptors of all registered slaves. All broadcast operations will send the messages to all - * registered slaves, assuming the slaves will have subscribed for the replication topic. + * registered slaves, assuming the slaves will have subscribed for the replication topic. Keys are the + * {@link ReplicaDescriptor#getUuid() IDs of the corresponding values}. */ - private Set replicaDescriptors; + private Map replicaDescriptors; private Map>, Integer>> replicationCounts; @@ -53,7 +55,7 @@ public class ReplicationInstancesManager { private ReplicationMasterDescriptor replicationMasterDescriptor; public ReplicationInstancesManager() { - replicaDescriptors = new HashSet(); + replicaDescriptors = new HashMap<>(); replicationCounts = new HashMap>,Integer>>(); totalMessageCount = new HashMap<>(); totalNumberOfOperations = new HashMap<>(); @@ -71,21 +73,31 @@ public class ReplicationInstancesManager { } public Iterable getReplicaDescriptors() { - return Collections.unmodifiableCollection(replicaDescriptors); + return Collections.unmodifiableCollection(replicaDescriptors.values()); } public ReplicationMasterDescriptor getReplicationMasterDescriptor() { return replicationMasterDescriptor; } + public Iterable getAllReplicableIdsAtLeastOneReplicaIsReplicating() { + return replicaDescriptors.values().stream().map(replicableDescriptor->Arrays.asList(replicableDescriptor.getReplicableIdsAsStrings())).reduce(new HashSet(), + (result, ids)->{result.addAll(ids); return result;}, (r1, r2)->{r1.addAll(r2); return r1;}); + } + public void registerReplica(ReplicaDescriptor replica) { - replicaDescriptors.add(replica); - replicationCounts.put(replica, new HashMap>, Integer>()); + replicaDescriptors.put(replica.getUuid(), replica); + replicationCounts.put(replica, new HashMap<>()); } public void unregisterReplica(ReplicaDescriptor replica) { - replicaDescriptors.remove(replica); + unregisterReplica(replica.getUuid()); + } + + public ReplicaDescriptor unregisterReplica(UUID replicaUuid) { + final ReplicaDescriptor replica = replicaDescriptors.remove(replicaUuid); replicationCounts.remove(replica); + return replica; } /** diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationMasterDescriptorImpl.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationMasterDescriptorImpl.java index 34dc073b159..10655c9db87 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationMasterDescriptorImpl.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationMasterDescriptorImpl.java @@ -41,6 +41,7 @@ public class ReplicationMasterDescriptorImpl implements ReplicationMasterDescrip private final String messagingHostname; private final int messagingPort; private final String queueName; + private final Iterable> replicables; private QueueingConsumer consumer; @@ -61,9 +62,15 @@ public class ReplicationMasterDescriptorImpl implements ReplicationMasterDescrip * replica with the master * @param servletPort * port for HTTP requests to the master + * @param replicables + * the {@link Replicable} objects to replicate from the master described by this object; the master will + * send operations only for those replicables that at least one replica is registered for; this, however, + * may mean that the master also sends operations for replicables that a particular replica hasn't + * registered for. Replicas shall silently drop operations for such replicables that they haven't + * requested replication for. */ public ReplicationMasterDescriptorImpl(String messagingHostname, String exchangeName, int messagingPort, - String queueName, String masterServletHostname, int servletPort) { + String queueName, String masterServletHostname, int servletPort, Iterable> replicables) { this.masterServletHostname = masterServletHostname; this.messagingHostname = messagingHostname; this.servletPort = servletPort; @@ -71,16 +78,20 @@ public class ReplicationMasterDescriptorImpl implements ReplicationMasterDescrip this.exchangeName = exchangeName; this.queueName = queueName; this.consumer = null; + this.replicables = replicables; } @Override public URL getReplicationRegistrationRequestURL(UUID uuid, String additional) throws MalformedURLException, UnsupportedEncodingException { + final String[] replicableIdsAsString = StreamSupport.stream(replicables.spliterator(), /* parallel */ false).map(r->r.getId()).toArray(i->new String[i]); return new URL("http", getHostname(), servletPort, REPLICATION_SERVLET + "?" + ReplicationServlet.ACTION + "=" + ReplicationServlet.Action.REGISTER.name() + "&" + ReplicationServlet.SERVER_UUID + "=" + java.net.URLEncoder.encode(uuid.toString(), "UTF-8") + "&" + ReplicationServlet.ADDITIONAL_INFORMATION + "=" - + java.net.URLEncoder.encode(ServerInfo.getBuildVersion(), "UTF-8")); + + java.net.URLEncoder.encode(ServerInfo.getBuildVersion(), "UTF-8") + "&" + + ReplicationServlet.REPLICABLES_IDS_AS_STRINGS_COMMA_SEPARATED + "=" + + java.net.URLEncoder.encode(String.join(",", replicableIdsAsString), "UTF-8")); } @Override @@ -90,6 +101,11 @@ public class ReplicationMasterDescriptorImpl implements ReplicationMasterDescrip + uuid.toString()); } + @Override + public Iterable> getReplicables() { + return replicables; + } + @Override public int hashCode() { final int prime = 31; diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationReceiver.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationReceiver.java index a114d3783cf..1702fe792ac 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationReceiver.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationReceiver.java @@ -66,7 +66,11 @@ public class ReplicationReceiver implements Runnable { private static final long CHECK_INTERVAL_MILLIS = 2000; // how long (milliseconds) to pause before checking connection again private static final int CHECK_COUNT = 150; // how long to check, value is CHECK_INTERVAL second steps + /** + * descriptor of the master server from which this replicator receives messages + */ private final ReplicationMasterDescriptor master; + private final ReplicablesProvider replicableProvider; /** @@ -138,12 +142,14 @@ public class ReplicationReceiver implements Runnable { } /** - * Starts fetching messages from the {@link #consumer}. After receiving a single message, assumes it's an - * {@link Iterable} of serialized {@link RacingEventServiceOperation} objects, and applies it to the - * {@link RacingEventService} which is obtained from the service tracker passed to this replicator at construction - * time. + * Starts fetching messages from the {@link #consumer}. After receiving a single message, assumes it's a + * {@link ReplicationServiceImpl#createUncompressingInputStream(InputStream) compressed} stream that first + * {@link DataInputStream#readUTF() encodes a UTF string} representing the {@link Replicable#getId() replicable ID}, + * followed by a sequence of serialized {@code byte[]} objects which each can be {@link Replicable#readOperation(InputStream) de-serialized} + * by the receiving {@link Replicable} identified by the ID received as a prefix. This method then applies these operations to the + * {@link Replicable} identified by the ID, retrieved through the {@link #replicableProvider}. * - * @see ReplicationServiceImpl#executed(RacingEventServiceOperation) + * @see ReplicationServiceExecutionListener#executed(OperationWithResult) */ @Override public void run() { @@ -170,12 +176,13 @@ public class ReplicationReceiver implements Runnable { } } } - byte[] bytesFromMessage = delivery.getBody(); + final byte[] bytesFromMessage = delivery.getBody(); checksPerformed = 0; // Set the replicable's class's class loader as context for deserialization so that all exported classes // of all required bundles/packages can be deserialized at least final InputStream uncompressingInputStream = ReplicationServiceImpl.createUncompressingInputStream(new ByteArrayInputStream(bytesFromMessage)); - String replicableIdAsString = new DataInputStream(uncompressingInputStream).readUTF(); + final String replicableIdAsString = new DataInputStream(uncompressingInputStream).readUTF(); + // TODO bug 2465: decide based on the master descriptor whether we want to process this message; if it's for a Replicable we're not replicating from that master, drop the message Replicable replicable = replicableProvider.getReplicable(replicableIdAsString, /* wait */ false); if (replicable != null) { ObjectInputStream ois = new ObjectInputStream(uncompressingInputStream); // no special stream required; only reading a generic byte[] @@ -183,11 +190,15 @@ public class ReplicationReceiver implements Runnable { try { while (true) { byte[] serializedOperation = (byte[]) ois.readObject(); - readOperationAndApplyOrQueueIt(replicable, serializedOperation); - operationCount++; - operationsInMessage++; - if (operationCount % 10000l == 0) { - logger.info("Received " + operationCount + " operations so far"); + if (Util.contains(master.getReplicables(), replicable)) { + readOperationAndApplyOrQueueIt(replicable, serializedOperation); + operationCount++; + operationsInMessage++; + if (operationCount % 10000l == 0) { + logger.info("Received " + operationCount + " operations so far"); + } + } else { + logger.fine("Dropping operation for non-replicated replicable "+replicable.getId()); } } } catch (EOFException eof) { diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceImpl.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceImpl.java index 8fe224ca762..3b8ba7f1c56 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceImpl.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceImpl.java @@ -13,6 +13,7 @@ import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Timer; @@ -22,9 +23,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; -import net.jpountz.lz4.LZ4BlockInputStream; -import net.jpountz.lz4.LZ4BlockOutputStream; - import org.osgi.util.tracker.ServiceTracker; import com.rabbitmq.client.AMQP.Exchange; @@ -43,6 +41,9 @@ import com.sap.sse.replication.ReplicationMasterDescriptor; import com.sap.sse.replication.ReplicationService; import com.sap.sse.util.HttpUrlConnectionHelper; +import net.jpountz.lz4.LZ4BlockInputStream; +import net.jpountz.lz4.LZ4BlockOutputStream; + /** * Manages a set of observers of {@link Replicable}, receiving notifications for the operations they perform that * require replication. This service provides the connectivity and central management including central keeping of @@ -79,7 +80,8 @@ public class ReplicationServiceImpl implements ReplicationService { /** * null, if this instance is not currently replicating from some master; the master's descriptor - * otherwise + * otherwise; note that partial replication is supported, meaning that only operations for a subset of the + * {@link Replicable}s running on this server will be considered when received from the master. */ private ReplicationMasterDescriptor replicatingFromMaster; @@ -241,10 +243,11 @@ public class ReplicationServiceImpl implements ReplicationService { private class LifeCycleListener implements ReplicableLifeCycleListener { @Override public void replicableAdded(Replicable replicable) { - // add a replication listener to the new replicable only if there are replicas currently registered + // add a replication listener to the new replicable only if there are replicas currently registered... synchronized (replicationInstancesManager) { - if (replicationInstancesManager.hasReplicas()) { - addNewOperationExecutionListener(replicable); + // .. and at least one of them wants to replicate the replicable with that ID + if (Util.contains(replicationInstancesManager.getAllReplicableIdsAtLeastOneReplicaIsReplicating(), replicable.getId().toString())) { + ensureOperationExecutionListener(replicable); } } } @@ -340,12 +343,16 @@ public class ReplicationServiceImpl implements ReplicationService { private Iterable> getReplicables() { return replicablesProvider.getReplicables(); } - + + private Replicable getReplicable(String replicableIdAsString, boolean wait) { + return replicablesProvider.getReplicable(replicableIdAsString, wait); + } + @Override public void registerReplica(ReplicaDescriptor replica) throws IOException { synchronized (replicationInstancesManager) { if (!replicationInstancesManager.hasReplicas()) { - addAsListenerToReplicables(); + addAsListenerToReplicables(replica.getReplicableIdsAsStrings()); synchronized (this) { if (masterChannel == null) { masterChannel = createMasterChannelAndDeclareFanoutExchange(); @@ -357,25 +364,34 @@ public class ReplicationServiceImpl implements ReplicationService { logger.info("Registered replica " + replica); } - private void addAsListenerToReplicables() { - for (Replicable replicable : getReplicables()) { - addNewOperationExecutionListener(replicable); + private void addAsListenerToReplicables(String[] replicableIdsAsStringForReplicablesToReplicate) { + for (final String replicableIdAsStringForReplicableToReplicate : replicableIdsAsStringForReplicablesToReplicate) { + Replicable replicable = getReplicable(replicableIdAsStringForReplicableToReplicate, /* wait */ true); + ensureOperationExecutionListener(replicable); } } - private void addNewOperationExecutionListener(Replicable replicable) { - final ReplicationServiceExecutionListener listener = new ReplicationServiceExecutionListener(this, - replicable); - executionListenersByReplicableIdAsString.put(replicable.getId().toString(), listener); + /** + * If no listener exists yet for {@code replicable}, a new one is created, registered as listener on + * {@code replicable} and remembered in {@link #executionListenersByReplicableIdAsString}. Otherwise, this method is + * a no-op. + */ + private void ensureOperationExecutionListener(Replicable replicable) { + if (!executionListenersByReplicableIdAsString.containsKey(replicable.getId().toString())) { + final ReplicationServiceExecutionListener listener = new ReplicationServiceExecutionListener(this, replicable); + executionListenersByReplicableIdAsString.put(replicable.getId().toString(), listener); + } } @Override - public void unregisterReplica(ReplicaDescriptor replica) throws IOException { - logger.info("Unregistering replica " + replica); + public ReplicaDescriptor unregisterReplica(UUID replicaUuid) throws IOException { + logger.info("Unregistering replica with ID " + replicaUuid); synchronized (replicationInstancesManager) { - replicationInstancesManager.unregisterReplica(replica); - if (!replicationInstancesManager.hasReplicas()) { - removeAsListenerFromReplicables(); + final Iterable oldReplicablesInReplication = replicationInstancesManager.getAllReplicableIdsAtLeastOneReplicaIsReplicating(); + final ReplicaDescriptor unregisteredReplica = replicationInstancesManager.unregisterReplica(replicaUuid); + final Iterable newReplicablesInReplication = replicationInstancesManager.getAllReplicableIdsAtLeastOneReplicaIsReplicating(); + for (final String idAsStringOfReplicableNoReplicaIsInterestedInAnymore : Util.removeAll(newReplicablesInReplication, Util.addAll(oldReplicablesInReplication, new HashSet<>()))) { + removeAsListenerFromReplicable(idAsStringOfReplicableNoReplicaIsInterestedInAnymore); synchronized (this) { if (masterChannel != null) { masterChannel.getConnection().close(); @@ -383,6 +399,23 @@ public class ReplicationServiceImpl implements ReplicationService { } } } + return unregisteredReplica; + } + } + + @Override + public void unregisterReplica(ReplicaDescriptor replica) throws IOException { + logger.info("Unregistering replica " + replica); + unregisterReplica(replica.getUuid()); + } + + private void removeAsListenerFromReplicable(String idAsStringOfReplicableNoReplicaIsInterestedInAnymore) { + ReplicationServiceExecutionListener listener = executionListenersByReplicableIdAsString.remove(idAsStringOfReplicableNoReplicaIsInterestedInAnymore); + if (listener != null) { + logger.info("Unsubscribed replication listener from replicable with ID "+idAsStringOfReplicableNoReplicaIsInterestedInAnymore); + listener.unsubscribe(); + } else { + logger.warning("Couldn't find a replication listener on replicable with ID "+idAsStringOfReplicableNoReplicaIsInterestedInAnymore); } } @@ -540,22 +573,16 @@ public class ReplicationServiceImpl implements ReplicationService { * which implements the initial load sending process. */ @Override - public void startToReplicateFrom(ReplicationMasterDescriptor master) throws IOException, ClassNotFoundException, - InterruptedException { - final Iterable> replicables = getReplicables(); - startToReplicateFrom(master, replicables); - } - - @Override - public void startToReplicateFrom(final ReplicationMasterDescriptor master, final Iterable> replicables) + public void startToReplicateFrom(final ReplicationMasterDescriptor master) throws IOException, ClassNotFoundException, InterruptedException { if (initialLoadChannels.containsKey(master)) { logger.warning("An initial load from "+master+" is already running, replicating the following replicables: "+ initialLoadChannels.get(master).getReplicables()+". Not starting a second time."); } else { + final Iterable> replicables = master.getReplicables(); logger.info("Starting to replicate from " + master); try { - registerReplicaWithMaster(master); + registerReplicaWithMaster(master, replicables); } catch (Exception ex) { logger.log(Level.SEVERE, "ERROR", ex); throw ex; @@ -627,9 +654,14 @@ public class ReplicationServiceImpl implements ReplicationService { } /** + * @param replicables + * the replica is registered for these {@link Replicable}s. The master sends operations only for + * replicables that at least one replica has registered for. This may mean that operations are received + * for replicables for which no replicable was requested. Replicas shall drop such operations silently. + * * @return the UUID that the master generated for this client which is also entered into {@link #replicaUUIDs} */ - private String registerReplicaWithMaster(ReplicationMasterDescriptor master) throws IOException, + private String registerReplicaWithMaster(ReplicationMasterDescriptor master, Iterable> replicables) throws IOException, ClassNotFoundException { URL replicationRegistrationRequestURL = master.getReplicationRegistrationRequestURL(getServerIdentifier(), ServerInfo.getBuildVersion()); @@ -702,6 +734,11 @@ public class ReplicationServiceImpl implements ReplicationService { return replicationInstancesManager.getAverageNumberOfBytesPerMessage(replica); } + @Override + public Iterable> getAllReplicables() { + return getReplicablesProvider().getReplicables(); + } + @Override public void stopToReplicateFromMaster() throws IOException { ReplicationMasterDescriptor descriptor = getReplicatingFromMaster(); @@ -740,7 +777,7 @@ public class ReplicationServiceImpl implements ReplicationService { } @Override - public void stopAllReplica() throws IOException { + public void stopAllReplicas() throws IOException { if (replicationInstancesManager.hasReplicas()) { replicationInstancesManager.removeAll(); removeAsListenerFromReplicables(); diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServlet.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServlet.java index 3277eb00540..b11ff33bbd9 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServlet.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServlet.java @@ -17,6 +17,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import net.jpountz.lz4.LZ4BlockInputStream; import net.jpountz.lz4.LZ4BlockOutputStream; import org.apache.commons.lang.StringEscapeUtils; @@ -77,15 +78,23 @@ public class ReplicationServlet extends AbstractHttpServlet { /** * The client identifies itself in the request. Two servlet operations are supported currently: registering the - * client with the replication service (if not already created, the JMS replication topic will be created by this - * registration); and obtaining an initial load stream that the replica can use to initialize its - * {@link RacingEventService}. The operation performed is selected by passing one of the {@link Action} enumeration - * values for the URL parameter named {@link #ACTION}. + * client with the replication service (if not already created, the message exchange will be created by this + * registration); and triggering sending an initial load stream through RabbitMQ that the replica can use to + * initialize its {@link Replicable} s. The IDs of the replicables of which the initial load is to be sent is + * expected as the {@link #REPLICABLES_IDS_AS_STRINGS_COMMA_SEPARATED} parameter value. The servlet response + * consists of the name of the RabbitMQ queue name that the client can connect to and through which to receive + * the LZ4-compressed initial load stream per replicable, using a {@link RabbitInputStreamProvider} and + * an {@link LZ4BlockInputStream}.. + *

    + * + * The operation performed is selected by passing one of the {@link Action} enumeration values for the URL parameter + * named {@link #ACTION}. */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter(ACTION); logger.info("Received replication request, action is "+action); + String[] replicableIdsAsStrings; switch (Action.valueOf(action)) { case REGISTER: registerClientWithReplicationService(req, resp); @@ -94,7 +103,7 @@ public class ReplicationServlet extends AbstractHttpServlet { deregisterClientWithReplicationService(req, resp); break; case INITIAL_LOAD: - String[] replicableIdsAsStrings = req.getParameter(REPLICABLES_IDS_AS_STRINGS_COMMA_SEPARATED).split(","); + replicableIdsAsStrings = req.getParameter(REPLICABLES_IDS_AS_STRINGS_COMMA_SEPARATED).split(","); Channel channel = getReplicationService().createMasterChannel(); try { RabbitOutputStream ros = new RabbitOutputStream(INITIAL_LOAD_PACKAGE_SIZE, channel, @@ -138,13 +147,13 @@ public class ReplicationServlet extends AbstractHttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - logger.info("Received request to apply and replicate an operation from a replica"); InputStream is = req.getInputStream(); DataInputStream dis = new DataInputStream(is); String replicableIdAsString = dis.readUTF(); try { Replicable replicable = replicablesProvider.getReplicable(replicableIdAsString, /* wait */ false); if (replicable != null) { + logger.info("Received request to apply and replicate an operation from a replica for replicable "+replicable); applyOperationToReplicable(replicable, is); } else { logger.warning("Received operation for replicable "+replicableIdAsString+ @@ -174,17 +183,22 @@ public class ReplicationServlet extends AbstractHttpServlet { } private void deregisterClientWithReplicationService(HttpServletRequest req, HttpServletResponse resp) throws IOException { - ReplicaDescriptor replica = getReplicaDescriptor(req); - getReplicationService().unregisterReplica(replica); - logger.info("Deregistered replication client with this server " + replica.getIpAddress()); - resp.setContentType("text/plain"); - resp.getWriter().print(replica.getUuid()); + final UUID replicaUuid = UUID.fromString(req.getParameter(SERVER_UUID)); + final ReplicaDescriptor replica = getReplicationService().unregisterReplica(replicaUuid); + if (replica != null) { + logger.info("Deregistered replication client with this server " + replica.getIpAddress()); + resp.setContentType("text/plain"); + resp.getWriter().print(replica.getUuid()); + } else { + logger.warning("Couldn't find replica to de-register with ID "+replicaUuid); + } } private void registerClientWithReplicationService(HttpServletRequest req, HttpServletResponse resp) throws IOException { - ReplicaDescriptor replica = getReplicaDescriptor(req); + final ReplicaDescriptor replica = getReplicaDescriptor(req); getReplicationService().registerReplica(replica); + logger.info("Registered new replica " + replica); resp.setContentType("text/plain"); resp.getWriter().print(replica.getUuid()); } @@ -193,7 +207,7 @@ public class ReplicationServlet extends AbstractHttpServlet { InetAddress ipAddress = InetAddress.getByName(req.getRemoteAddr()); UUID uuid = UUID.fromString(req.getParameter(SERVER_UUID)); String additional = req.getParameter(ADDITIONAL_INFORMATION); - logger.info("Registered new replica " + ipAddress + " " + uuid.toString() + " " + additional); - return new ReplicaDescriptor(ipAddress, uuid, additional); + final String[] replicableIdsAsStrings = req.getParameter(REPLICABLES_IDS_AS_STRINGS_COMMA_SEPARATED).split(","); + return new ReplicaDescriptor(ipAddress, uuid, additional, replicableIdsAsStrings); } } diff --git a/java/com.sap.sse.security.ui/Security UI sdm.launch b/java/com.sap.sse.security.ui/Security UI sdm.launch index 42497e6bbf2..d3848dc2ec0 100644 --- a/java/com.sap.sse.security.ui/Security UI sdm.launch +++ b/java/com.sap.sse.security.ui/Security UI sdm.launch @@ -1,54 +1,54 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sse.security.ui/Security UI.launch b/java/com.sap.sse.security.ui/Security UI.launch index 0262bee9a54..92bac81c128 100644 --- a/java/com.sap.sse.security.ui/Security UI.launch +++ b/java/com.sap.sse.security.ui/Security UI.launch @@ -1,53 +1,53 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties index fc38696838d..1a8ac3cafac 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties @@ -1,7 +1,7 @@ name=名前 company=会社 password=パスワード -passwordRepeat=パスワード繰返 +passwordRepeat=パスワード再入力 loggedIn=ユーザ {0} としてログインしました signIn=サインイン signUp=サインアップ @@ -95,3 +95,4 @@ accountConfirmation=アカウント情報 error=未知のエラー refresh=リフレッシュ preferredLanguage=優先言語 +cannotResetInvalidURL=無効なリセット URL... diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties index 7d47a9ab9ab..3d7c32aaa9d 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties @@ -95,3 +95,4 @@ accountConfirmation=Подтверждение учетной записи error=Неизвестная ошибка refresh=Обновить preferredLanguage=Предпочитаемый язык +cannotResetInvalidURL=Недействительный URL-адрес сброса... diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh.properties index 46cdee6ffc5..d57a75372ea 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh.properties @@ -95,3 +95,4 @@ accountConfirmation=账户确认 error=未知错误 refresh=刷新 preferredLanguage=首选语言 +cannotResetInvalidURL=重置 URL 无效... diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh_CN.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh_CN.properties index 46cdee6ffc5..d57a75372ea 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh_CN.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh_CN.properties @@ -95,3 +95,4 @@ accountConfirmation=账户确认 error=未知错误 refresh=刷新 preferredLanguage=首选语言 +cannotResetInvalidURL=重置 URL 无效... diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages.properties.xlf b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages.properties.xlf index bc39ad8e96f..2bb5c2e2ea5 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages.properties.xlf +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages.properties.xlf @@ -1,12 +1,12 @@ - +

    - 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 - +
    @@ -302,6 +302,9 @@ Preferred language + + Invalid reset URL... + \ No newline at end of file diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ja.properties.xlf b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ja.properties.xlf index 268d9c7b15a..6e129ed41ee 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ja.properties.xlf +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ja.properties.xlf @@ -1,12 +1,12 @@ - +
    - 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 - +
    @@ -25,7 +25,7 @@ Repeat password - パスワード繰返 + パスワード再入力 Logged in as user {0}! @@ -65,7 +65,7 @@ Welcome, {0}! - {0} さん、ようこそ + {0} さん、ようこそ Trying to verify user... @@ -77,7 +77,7 @@ Signed in as {0} - {0} としてサインイン + {0} としてサインイン Signed in as: @@ -161,7 +161,7 @@ {0}-Account - {0} アカウント + {0} アカウント Save @@ -349,7 +349,7 @@ Settings for {0} - {0} の設定 + {0} の設定 Please select @@ -399,6 +399,10 @@ Preferred language 優先言語 + + Invalid reset URL... + 無効なリセット URL... +
    \ No newline at end of file diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ru.properties.xlf b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ru.properties.xlf index 3d4a8154018..52245d238ab 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ru.properties.xlf +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ru.properties.xlf @@ -1,12 +1,12 @@ - +
    - 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 - +
    @@ -161,7 +161,7 @@ {0}-Account - {0}-учетная запись + {0}-учетная запись Save @@ -399,6 +399,10 @@ Preferred language Предпочитаемый язык + + Invalid reset URL... + Недействительный URL-адрес сброса... +
    \ No newline at end of file diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_zh_CN.properties.xlf b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_zh_CN.properties.xlf index feba4d7435f..68818b0e333 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_zh_CN.properties.xlf @@ -1,12 +1,12 @@ - +
    - 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 - +
    @@ -399,6 +399,10 @@ Preferred language 首选语言 + + Invalid reset URL... + 重置 URL 无效... +
    \ No newline at end of file diff --git a/java/com.sap.sse/src/com/sap/sse/osgi/CachedOsgiTypeBasedServiceFinder.java b/java/com.sap.sse/src/com/sap/sse/osgi/CachedOsgiTypeBasedServiceFinder.java index 4d4d4c6e95f..3d2b944d5c4 100644 --- a/java/com.sap.sse/src/com/sap/sse/osgi/CachedOsgiTypeBasedServiceFinder.java +++ b/java/com.sap.sse/src/com/sap/sse/osgi/CachedOsgiTypeBasedServiceFinder.java @@ -4,6 +4,8 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; @@ -12,10 +14,17 @@ import org.osgi.util.tracker.ServiceTrackerCustomizer; import com.sap.sse.common.NoCorrespondingServiceRegisteredException; import com.sap.sse.common.TypeBasedServiceFinder; +import com.sap.sse.common.Util; /** - * Caches OSGI services of a certain type, based on their type property. Has to be registered as the - * {@link ServiceTrackerCustomizer} of a {@link ServiceTracker}. + * Caches OSGI services of a certain type, based on their {@link TypeBasedServiceFinder#TYPE} + * {@link BundleContext#registerService(Class, Object, java.util.Dictionary) property}. Has to be registered as the + * {@link ServiceTrackerCustomizer} of a {@link ServiceTracker}. The {@link #findService(String)} method looks + * up the requested type string against the {@link TypeBasedServiceFinder#TYPE} property that OSGi-based + * service implementations must have provided as a property during service registration with the OSGi service + * registry. Don't confuse this "type" string with the classes of the service implementations which all + * have to implement {@code ServiceT} but whose name has nothing to do with the type string used here for + * service lookup. * * @author Fredrik Teschke * @@ -24,34 +33,65 @@ import com.sap.sse.common.TypeBasedServiceFinder; */ public class CachedOsgiTypeBasedServiceFinder implements ServiceTrackerCustomizer, TypeBasedServiceFinder { + private static final Logger logger = Logger.getLogger(CachedOsgiTypeBasedServiceFinder.class.getName()); private final Map services = new HashMap<>(); private final BundleContext context; - private Class serviceType; + private final Class serviceType; private ServiceT fallback; + private final Map>> callbacks; public CachedOsgiTypeBasedServiceFinder(Class serviceType, BundleContext context) { this.serviceType = serviceType; this.context = context; + this.callbacks = new HashMap<>(); } @Override public ServiceT findService(String type) { - ServiceT service = services.get(type); - if (service == null) { - if (fallback != null) { - return fallback; + synchronized (services) { + ServiceT service = services.get(type); + if (service == null) { + if (fallback != null) { + return fallback; + } else { + throw new NoCorrespondingServiceRegisteredException("Could not find service", type, serviceType.getSimpleName()); + } + } + return service; + } + } + + @Override + public void applyServiceWhenAvailable(String type, Callback callback) { + synchronized (services) { + ServiceT service = services.get(type); + if (service != null) { + callback.withService(service); } else { - throw new NoCorrespondingServiceRegisteredException("Could not find service", type, serviceType.getSimpleName()); + Util.addToValueSet(callbacks, type, callback); } } - return service; } @Override public ServiceT addingService(ServiceReference reference) { String type = (String) reference.getProperty(TypeBasedServiceFinder.TYPE); ServiceT service = context.getService(reference); - services.put(type, service); + Set> callbacksToNotify; + synchronized (services) { + services.put(type, service); + callbacksToNotify = callbacks.remove(type); + } + if (callbacksToNotify != null) { + callbacksToNotify.forEach(c -> { + try { + c.withService(service); + } catch (Exception e) { + logger.log(Level.SEVERE, + "Exception trying to inform " + c + " about the availability of service " + service,e); + } + }); + } return service; } @@ -63,7 +103,9 @@ public class CachedOsgiTypeBasedServiceFinder implements ServiceTracke @Override public void removedService(ServiceReference reference, ServiceT service) { String type = (String) reference.getProperty(TypeBasedServiceFinder.TYPE); - services.remove(type); + synchronized (services) { + services.remove(type); + } } @Override @@ -73,6 +115,8 @@ public class CachedOsgiTypeBasedServiceFinder implements ServiceTracke @Override public Set findAllServices() { - return new HashSet(services.values()); + synchronized (services) { + return new HashSet(services.values()); + } } } diff --git a/java/com.sap.sse/src/com/sap/sse/osgi/CachedOsgiTypeBasedServiceFinderFactory.java b/java/com.sap.sse/src/com/sap/sse/osgi/CachedOsgiTypeBasedServiceFinderFactory.java index d2aac7d0a42..686e71df714 100644 --- a/java/com.sap.sse/src/com/sap/sse/osgi/CachedOsgiTypeBasedServiceFinderFactory.java +++ b/java/com.sap.sse/src/com/sap/sse/osgi/CachedOsgiTypeBasedServiceFinderFactory.java @@ -13,13 +13,16 @@ import com.sap.sse.common.TypeBasedServiceFinderFactory; /** * Use the OSGi service registry to find the desired services. + *

    * - * Using this indirection it is possible to integrate new service types into the system without - * the need to change any existing code.

    + * Using this indirection it is possible to integrate new service types into the system without the need to change any + * existing code. + *

    + * + * Note that it is not easily possible to use the {@link CachedOsgiTypeBasedServiceFinderFactory} in test case scenarios + * because there is usually no OSGi context available during test execution. Consider using the + * {@link MockServiceFinderFactory} instead. * - * Note that it is not easily possible to use the {@link CachedOsgiTypeBasedServiceFinderFactory} in test - * case scenarios because there is usually no OSGi context available during test execution. - * Consider using the {@link MockServiceFinderFactory} instead. * @author Fredrik Teschke * */ @@ -35,18 +38,14 @@ public class CachedOsgiTypeBasedServiceFinderFactory implements TypeBasedService @Override public TypeBasedServiceFinder createServiceFinder(Class clazz) { @SuppressWarnings("unchecked") - CachedOsgiTypeBasedServiceFinder finder = (CachedOsgiTypeBasedServiceFinder) serviceFinders - .get(clazz); - + CachedOsgiTypeBasedServiceFinder finder = (CachedOsgiTypeBasedServiceFinder) serviceFinders.get(clazz); if (finder == null) { finder = new CachedOsgiTypeBasedServiceFinder<>(clazz, context); ServiceTracker tracker = new ServiceTracker(context, clazz, finder); - serviceFinders.put(clazz, finder); serviceTrackers.add(tracker); tracker.open(); } - return finder; } diff --git a/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java b/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java index 7988f3f3fbb..c9fd773c4a4 100755 --- a/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java +++ b/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java @@ -421,9 +421,7 @@ public class SmartFutureCache> { logger.finest("resuming cache "+nameForLocks); for (Iterator>>>>> i=triggeredAndNotYetScheduled.entrySet().iterator(); i.hasNext(); ) { Entry>>>> e = i.next(); - if (logger.isLoggable(Level.FINEST)) { - logger.finest("while resuming "+nameForLocks+", triggering update for key "+e.getKey()+" with update interval "+e.getValue()); - } + logger.finest(()->"while resuming "+nameForLocks+", triggering update for key "+e.getKey()+" with update interval "+e.getValue()); triggerUpdate(e.getKey(), e.getValue().getA()); i.remove(); } diff --git a/java/pom.xml b/java/pom.xml index 57fb12c4e9c..c5397e72e3d 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -94,6 +94,7 @@ com.sap.sse.filestorage com.sap.sse.feature com.sap.sse.feature.runtime + com.sap.sailing.competitorimport com.sap.sailing.domain com.sap.sailing.geocoding com.sap.sailing.datamining @@ -190,7 +191,6 @@ com.sap.sailing.simulator.test com.sap.sailing.server.trackfiles.test com.sap.sailing.selenium.test - com.sap.sailing.autoload com.sap.sailing.feature.p2build com.sap.sailing.polars com.sap.sailing.polars.datamining diff --git a/mobile/com.sap.sailing.android.shared/res/layout/custom_toolbar.xml b/mobile/com.sap.sailing.android.shared/res/layout/custom_toolbar.xml new file mode 100644 index 00000000000..970ce4bcc96 --- /dev/null +++ b/mobile/com.sap.sailing.android.shared/res/layout/custom_toolbar.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/mobile/com.sap.sailing.android.shared/res/layout/fragment_container.xml b/mobile/com.sap.sailing.android.shared/res/layout/fragment_container.xml index ff877f51324..a8440701dd2 100644 --- a/mobile/com.sap.sailing.android.shared/res/layout/fragment_container.xml +++ b/mobile/com.sap.sailing.android.shared/res/layout/fragment_container.xml @@ -3,43 +3,9 @@ android:layout_height="match_parent" android:orientation="vertical"> - - - - - - - - - + layout="@layout/custom_toolbar" /> Mehr Informationen auf https://www.sapsailing.com + Gestern + Heute + Morgen \ No newline at end of file diff --git a/mobile/com.sap.sailing.android.shared/res/values/colors.xml b/mobile/com.sap.sailing.android.shared/res/values/colors.xml index b8b85ffe5ec..56728497ca6 100644 --- a/mobile/com.sap.sailing.android.shared/res/values/colors.xml +++ b/mobile/com.sap.sailing.android.shared/res/values/colors.xml @@ -63,4 +63,9 @@ #f3f3f3 #bfbfbf + + #8ab54e + @color/sap_yellow + @color/sap_red + #455B27 diff --git a/mobile/com.sap.sailing.android.shared/res/values/dimens.xml b/mobile/com.sap.sailing.android.shared/res/values/dimens.xml index ec8dcb461d3..dda2497c9ba 100644 --- a/mobile/com.sap.sailing.android.shared/res/values/dimens.xml +++ b/mobile/com.sap.sailing.android.shared/res/values/dimens.xml @@ -53,4 +53,6 @@ 21sp 18sp + 12dp + \ No newline at end of file diff --git a/mobile/com.sap.sailing.android.shared/res/values/strings.xml b/mobile/com.sap.sailing.android.shared/res/values/strings.xml index 2da6ef70149..51361221b34 100644 --- a/mobile/com.sap.sailing.android.shared/res/values/strings.xml +++ b/mobile/com.sap.sailing.android.shared/res/values/strings.xml @@ -81,4 +81,10 @@ More information on https://www.sapsailing.com + Yesterday + Today + Tomorrow + %1$dh %2$02d\' %3$02d\" + yyyy-MM-dd + \ No newline at end of file diff --git a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/data/LeaderboardInfo.java b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/data/LeaderboardInfo.java index 3d67c9a3122..5fcc70da00e 100644 --- a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/data/LeaderboardInfo.java +++ b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/data/LeaderboardInfo.java @@ -3,6 +3,10 @@ package com.sap.sailing.android.shared.data; public class LeaderboardInfo { public String name; + /** + * the leaderboard's display name if one has been explicitly provided, otherwise the same as {@link #name} + */ + public String displayName; public String checkinDigest; public int rowId; public String serverUrl; diff --git a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/ui/customviews/GPSQuality.java b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/ui/customviews/GPSQuality.java new file mode 100644 index 00000000000..a121e993595 --- /dev/null +++ b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/ui/customviews/GPSQuality.java @@ -0,0 +1,26 @@ +package com.sap.sailing.android.shared.ui.customviews; + +public enum GPSQuality { + noSignal(0), poor(2), good(3), great(4); + + private final int mQuality; + + GPSQuality(int quality) { + mQuality = quality; + } + + public int toInt() { + return this.mQuality; + } + + public static GPSQuality getValue(int value) { + GPSQuality result = null; + for (GPSQuality item : GPSQuality.values()) { + if (item.toInt() == value) { + result = item; + break; + } + } + return result; + } +} \ No newline at end of file diff --git a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/ui/customviews/SignalQualityIndicatorView.java b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/ui/customviews/SignalQualityIndicatorView.java index 002e2a4fbb6..469741f7b35 100644 --- a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/ui/customviews/SignalQualityIndicatorView.java +++ b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/ui/customviews/SignalQualityIndicatorView.java @@ -3,7 +3,6 @@ package com.sap.sailing.android.shared.ui.customviews; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.text.TextUtils; @@ -14,17 +13,19 @@ import android.view.accessibility.AccessibilityEvent; import com.sap.sailing.android.shared.R; /** - * Draws one vertical indicator that can is filled green up to a point, depending on the value passed for - * signal-quality; + * Draws one vertical indicator that is colored depending on the signal quality * * @author Lukas Zielinski + * @author Peter Siegmund */ public class SignalQualityIndicatorView extends View { - private int signalQuality; + private GPSQuality mSignalQuality; - private Paint paint; - private Paint paintDark; + private Paint paintNone; + private Paint paintPoor; + private Paint paintGood; + private Paint paintGreat; private float height; private float width; @@ -35,7 +36,10 @@ public class SignalQualityIndicatorView extends View { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SignalQualityIndicatorView, 0, 0); try { - signalQuality = a.getInteger(R.styleable.SignalQualityIndicatorView_signalQuality, 0); + mSignalQuality = GPSQuality.getValue(a.getInteger(R.styleable.SignalQualityIndicatorView_signalQuality, 0)); + if (mSignalQuality == null) { + mSignalQuality = GPSQuality.noSignal; + } } finally { a.recycle(); } @@ -44,52 +48,69 @@ public class SignalQualityIndicatorView extends View { initPaint(); } - public Integer getSignalQuality() { - return signalQuality; + public int getSignalQuality() { + return mSignalQuality.toInt(); } private void setAccessibilityString() { - if (this.signalQuality == 2) { - this.setContentDescription(getContext().getString(R.string.signal_accuracy_indicator_view_description) - + getContext().getString(R.string.poor)); - } else if (this.signalQuality == 3) { - this.setContentDescription(getContext().getString(R.string.signal_accuracy_indicator_view_description) - + getContext().getString(R.string.good)); - } else if (this.signalQuality == 4) { - this.setContentDescription(getContext().getString(R.string.signal_accuracy_indicator_view_description) - + getContext().getString(R.string.great)); - } else { - this.setContentDescription(getContext().getString(R.string.signal_accuracy_indicator_view_description) - + getContext().getString(R.string.no_signal)); + String desc = getContext().getString(R.string.signal_accuracy_indicator_view_description) + " "; + switch (mSignalQuality) { + case poor: + desc += getContext().getString(R.string.poor); + break; + + case good: + desc += getContext().getString(R.string.good); + break; + + case great: + desc += getContext().getString(R.string.great); + break; + + default: + desc += getContext().getString(R.string.no_signal); + } + setContentDescription(desc); } private void initPaint() { - paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setColor(Color.parseColor("#8ab54e")); - paintDark = new Paint(Paint.ANTI_ALIAS_FLAG); - paintDark.setColor(Color.parseColor("#455B27")); + paintNone = new Paint(Paint.ANTI_ALIAS_FLAG); + paintNone.setColor(getResources().getColor(R.color.signal_none)); + paintPoor = new Paint(Paint.ANTI_ALIAS_FLAG); + paintPoor.setColor(getResources().getColor(R.color.signal_poor)); + paintGood = new Paint(Paint.ANTI_ALIAS_FLAG); + paintGood.setColor(getResources().getColor(R.color.signal_good)); + paintGreat = new Paint(Paint.ANTI_ALIAS_FLAG); + paintGreat.setColor(getResources().getColor(R.color.signal_great)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); - float boxHeight = this.height / 4; - // first draw gray background + Paint paint; + switch (mSignalQuality) { + case poor: + paint = paintPoor; + break; + + case good: + paint = paintGood; + break; + + case great: + paint = paintGreat; + break; + + default: + paint = paintNone; + } rect.top = 0; rect.left = 0; rect.right = (int) this.width; rect.bottom = (int) this.height; - canvas.drawRect(rect, paintDark); - - // then draw green box on top - - rect.top = (int) (this.height - (signalQuality * boxHeight)); - rect.left = 0; - rect.right = (int) this.width; - rect.bottom = (int) this.height; canvas.drawRect(rect, paint); } @@ -116,24 +137,17 @@ public class SignalQualityIndicatorView extends View { /** * Must be 0,2,3 or 4, otherwise 1 will be set * - * @param signalQuality + * @param signalQuality the new signal quality */ - public void setSignalQuality(Integer signalQuality) { - Integer previousSingalQuality = this.signalQuality; + public void setSignalQuality(GPSQuality signalQuality) { + GPSQuality previousSignalQuality = mSignalQuality; - if (signalQuality == 0) { - this.signalQuality = 0; - } else if (signalQuality == 2) { - this.signalQuality = 2; - } else if (signalQuality == 3) { - this.signalQuality = 3; - } else if (signalQuality == 4) { - this.signalQuality = 4; - } else { - this.signalQuality = 0; + mSignalQuality = signalQuality; + if (mSignalQuality == null) { + mSignalQuality = GPSQuality.noSignal; } - if (this.signalQuality != previousSingalQuality) { + if (mSignalQuality != previousSignalQuality) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); } @@ -155,13 +169,24 @@ public class SignalQualityIndicatorView extends View { * generate some accessibility info */ public String getAccessibilityText() { - if (this.signalQuality == 2) - return "poor signal quality"; - if (this.signalQuality == 3) - return "good signal quality"; - if (this.signalQuality == 4) - return "great signal quality"; - return "no signal"; + String result; + switch (mSignalQuality) { + case poor: + result = "poor signal quality"; + break; + + case good: + result = "good signal quality"; + break; + + case great: + result = "great signal quality"; + break; + + default: + result = "no signal"; + } + return result; } @Override diff --git a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/TimeUtils.java b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/TimeUtils.java new file mode 100755 index 00000000000..9966aa8f8cf --- /dev/null +++ b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/TimeUtils.java @@ -0,0 +1,230 @@ +package com.sap.sailing.android.shared.util; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; + +import com.sap.sailing.android.shared.R; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.Nullable; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.NumberPicker; +import android.widget.TextView; +import android.widget.TimePicker; + +public class TimeUtils { + + protected TimeUtils() { + // only static methods + } + + /** + * Formats your time to 'kk:mm:ss'. + * + * @param timePoint timestamp to format + */ + public static String formatTime(TimePoint timePoint) { + return formatTime(timePoint, "kk:mm:ss"); + } + + /** + * Formats your time with the help of {@link DateFormat}. + * + * @param timePoint timestamp to format + * @param format format as defined by {@link DateFormat} + * @return timestamp formatted as {@link String} + */ + public static String formatTime(TimePoint timePoint, String format) { + return DateFormat.format(format, timePoint.asDate()).toString(); + } + + public static String formatDuration(TimePoint now, TimePoint timePoint) { + return formatDuration(now, timePoint, true); + } + + public static String formatDuration(TimePoint now, TimePoint timePoint, boolean inclSign) { + String duration; + if (timePoint.after(now)) { + String sign = (inclSign) ? "-" : ""; + duration = sign + TimeUtils.formatDurationUntil(timePoint.minus(now.asMillis()).asMillis(), false); + } else { + duration = TimeUtils.formatDurationSince(now.minus(timePoint.asMillis()).asMillis(), false); + } + return duration; + } + + public static String formatDurationSince(long milliseconds) { + return formatDurationSince(milliseconds, true); + } + + public static String formatDurationSince(long milliseconds, boolean emptyHours) { + int secondsTillStart = (int) Math.floor(milliseconds / 1000f); + return formatDuration(secondsTillStart, emptyHours); + } + + public static String formatDurationUntil(long milliseconds) { + return formatDurationUntil(milliseconds, true); + } + + public static String formatDurationUntil(long milliseconds, boolean emptyHours) { + int secondsTillStart = (int) Math.ceil(milliseconds / 1000f); + return formatDuration(secondsTillStart, emptyHours); + } + + /** + * Formats milliseconds to a string like: 01h 23'45" + */ + public static String formatTimeAgo(Context context, long milliseconds) { + Calendar time = Calendar.getInstance(); + time.setTimeZone(TimeZone.getTimeZone("UTC")); + time.setTimeInMillis(milliseconds); + return context.getString(R.string.time_ago, time.get(Calendar.HOUR_OF_DAY), time.get(Calendar.MINUTE), time.get(Calendar.SECOND)); + } + + public static String calcDuration(Calendar from, Calendar to) { + String retValue; + + long millis = to.getTimeInMillis() - from.getTimeInMillis(); + + long min = millis / (1000 * 60); + long sec = (millis - (min * 60 * 1000)) / 1000; + + retValue = String.valueOf(sec) + "\""; + if (retValue.length() == 2) { + retValue = "0" + retValue; + } + if (min > 0) { + retValue = String.valueOf(min) + "' " + retValue; + } + + return retValue; + } + + public static Calendar floorTime(Calendar calendar) { + if (calendar == null) { + calendar = Calendar.getInstance(); + } + calendar.set(Calendar.MILLISECOND, 0); + + return calendar; + } + + public static int daysBetween(Calendar day1, Calendar day2) { + Calendar dayOne = (Calendar) day1.clone(); + Calendar dayTwo = (Calendar) day2.clone(); + + if (dayOne.get(Calendar.YEAR) == dayTwo.get(Calendar.YEAR)) { + return dayOne.get(Calendar.DAY_OF_YEAR) - dayTwo.get(Calendar.DAY_OF_YEAR); + } else { + if (dayTwo.get(Calendar.YEAR) > dayOne.get(Calendar.YEAR)) { + //swap them + Calendar temp = dayOne; + dayOne = dayTwo; + dayTwo = temp; + } + int extraDays = 0; + + while (dayOne.get(Calendar.YEAR) > dayTwo.get(Calendar.YEAR)) { + dayOne.add(Calendar.YEAR, -1); + // getActualMaximum() important for leap years + extraDays += dayOne.getActualMaximum(Calendar.DAY_OF_YEAR); + } + + return extraDays - dayTwo.get(Calendar.DAY_OF_YEAR) + dayOne.get(Calendar.DAY_OF_YEAR); + } + } + + private static String formatDuration(int secondsTillStart, boolean emptyHours) { + int hours = secondsTillStart / 3600; + int minutes = (secondsTillStart % 3600) / 60; + int seconds = (secondsTillStart % 60); + boolean negative = (hours < 0 || minutes < 0 || seconds < 0); + String timePattern = ((negative) ? "-" : "") + "%s%s:%s"; + String secondsString = seconds < 10 ? "0" + Math.abs(seconds) : "" + Math.abs(seconds); + String minutesString = minutes < 10 ? "0" + Math.abs(minutes) : "" + Math.abs(minutes); + String hoursString = hours < 10 ? "0" + Math.abs(hours) : "" + Math.abs(hours); + hoursString = !emptyHours && hoursString.equals("00") ? "" : hoursString + ":"; + return String.format(timePattern, hoursString, minutesString, secondsString); + } + + public static void initDatePicker(Context context, NumberPicker datePicker, Calendar time, int pastDays, int futureDays) { + initDatePicker(context, datePicker, time, pastDays, futureDays, true); + } + + public static void initDatePicker(Context context, NumberPicker datePicker, Calendar time, int pastDays, int futureDays, boolean useWords) { + java.text.DateFormat dateFormat = java.text.DateFormat.getDateInstance(); + ArrayList dates = new ArrayList<>(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(time.getTime()); + // Past + calendar.add(Calendar.DAY_OF_MONTH, pastDays); + for (int i = pastDays; i < -1; i++) { + dates.add(dateFormat.format(calendar.getTime())); + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + if (useWords) { + dates.add(context.getString(R.string.yesterday)); + dates.add(context.getString(R.string.today)); + dates.add(context.getString(R.string.tomorrow)); + calendar.add(Calendar.DAY_OF_MONTH, 3); + } else { + for (int i = (pastDays == 0 ? pastDays : -1); i < (futureDays < 2 ? futureDays + 1 : 2); i++) { + dates.add(dateFormat.format(calendar.getTime())); + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + } + // Future + for (int i = 2; i <= futureDays; i++) { + dates.add(dateFormat.format(calendar.getTime())); + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + datePicker.setDisplayedValues(dates.toArray(new String[dates.size()])); + datePicker.setMinValue(0); + datePicker.setMaxValue(dates.size() - 1); + datePicker.setWrapSelectorWheel(false); + datePicker.setValue(TimeUtils.daysBetween(calendar, Calendar.getInstance()) + Math.abs(pastDays)); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + TextView date = new TextView(context); + date.setText(dateFormat.format(calendar.getTime())); + int padding = context.getResources().getDimensionPixelSize(R.dimen.default_padding_half); + date.setPadding(padding, padding, padding, padding); + date.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + datePicker.getLayoutParams().width = date.getMeasuredWidth(); + } + } + + public static ArrayList getDates(Context context, TimePoint start, TimePoint end) { + ArrayList dates = new ArrayList<>(); + SimpleDateFormat simpleFormat = new SimpleDateFormat(context.getString(R.string.date_short), Locale.US); + Calendar startDate = Calendar.getInstance(); + Calendar endDate = (Calendar) startDate.clone(); + startDate.setTime(start.asDate()); + endDate.setTime(end.asDate()); + int dayDiff = daysBetween(endDate, startDate); + for (int i = 0; i <= dayDiff; i++) { + dates.add(simpleFormat.format(startDate.getTime())); + startDate.add(Calendar.DAY_OF_MONTH, 1); + } + return dates; + } + + public static TimePoint getTime(TimePicker timePicker) { + return getTime(timePicker, null); + } + + public static TimePoint getTime(TimePicker timePicker, @Nullable NumberPicker secondPicker) { + Calendar time = Calendar.getInstance(); + time.set(Calendar.HOUR_OF_DAY, timePicker.getCurrentHour()); + time.set(Calendar.MINUTE, timePicker.getCurrentMinute()); + time.set(Calendar.SECOND, secondPicker == null ? 0 : secondPicker.getValue()); + time.set(Calendar.MILLISECOND, 0); + return new MillisecondsTimePoint(time.getTime()); + } +} diff --git a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/UniqueDeviceUuid.java b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/UniqueDeviceUuid.java index a3eb892e352..08c3ce11852 100644 --- a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/UniqueDeviceUuid.java +++ b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/UniqueDeviceUuid.java @@ -14,7 +14,7 @@ public class UniqueDeviceUuid { private static String mDeviceId; - @SuppressLint("CommitPrefEdits") + @SuppressLint("ApplySharedPref") public synchronized static String getUniqueId(Context context) { if (mDeviceId == null) { SharedPreferences preferences = context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE); diff --git a/mobile/com.sap.sailing.android.tracking.app/AndroidManifest.xml b/mobile/com.sap.sailing.android.tracking.app/AndroidManifest.xml index 2db55bedcc6..8d92daee5b3 100644 --- a/mobile/com.sap.sailing.android.tracking.app/AndroidManifest.xml +++ b/mobile/com.sap.sailing.android.tracking.app/AndroidManifest.xml @@ -128,15 +128,6 @@ - - - - - - - diff --git a/mobile/com.sap.sailing.android.tracking.app/res/layout-land/fragment_regatta.xml b/mobile/com.sap.sailing.android.tracking.app/res/layout-land/fragment_regatta.xml index 50e25da203f..b2a14d3ab7d 100644 --- a/mobile/com.sap.sailing.android.tracking.app/res/layout-land/fragment_regatta.xml +++ b/mobile/com.sap.sailing.android.tracking.app/res/layout-land/fragment_regatta.xml @@ -301,7 +301,6 @@ android:layout_height="wrap_content" android:layout_marginBottom="5dp" android:padding="0dp" - android:text="@string/days" android:singleLine="true" android:textColor="@color/white"/> @@ -332,7 +331,6 @@ android:layout_height="wrap_content" android:layout_marginBottom="5dp" android:padding="0dp" - android:text="@string/hours" android:singleLine="true" android:textColor="@color/white"/> @@ -363,7 +361,6 @@ android:layout_height="wrap_content" android:layout_marginBottom="5dp" android:padding="0dp" - android:text="@string/minutes" android:singleLine="true" android:textColor="@color/white"/> diff --git a/mobile/com.sap.sailing.android.tracking.app/res/layout/activity_tracking.xml b/mobile/com.sap.sailing.android.tracking.app/res/layout/activity_tracking.xml index d87bf0fba8c..40d564700bd 100644 --- a/mobile/com.sap.sailing.android.tracking.app/res/layout/activity_tracking.xml +++ b/mobile/com.sap.sailing.android.tracking.app/res/layout/activity_tracking.xml @@ -1,48 +1,18 @@ + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/fiori_main_background"> - - - - - - - - - + layout="@layout/custom_toolbar" /> + android:background="@color/separator_color" /> + android:padding="0dp" /> + android:layout_weight="1" />