From ddb8faba01ff31608c8dae4b3287ec2b7d6e350d Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 16 Jun 2024 17:38:58 +0200 Subject: [PATCH 01/74] new plugin: AIS vessel tracker, initial version --- OsmAnd/build.gradle | 2 + OsmAnd/res/drawable-hdpi/ais_aton.png | Bin 0 -> 1540 bytes OsmAnd/res/drawable-hdpi/ais_aton_virt.png | Bin 0 -> 1738 bytes OsmAnd/res/drawable-hdpi/ais_land.png | Bin 0 -> 1902 bytes OsmAnd/res/drawable-hdpi/ais_plane.png | Bin 0 -> 3000 bytes OsmAnd/res/drawable-hdpi/ais_sar.png | Bin 0 -> 5185 bytes OsmAnd/res/drawable-hdpi/ais_vessel.png | Bin 0 -> 2158 bytes OsmAnd/res/drawable-hdpi/ais_vessel_cross.png | Bin 0 -> 3501 bytes OsmAnd/res/drawable-hdpi/ais_vessel_red.png | Bin 0 -> 2193 bytes OsmAnd/res/drawable-mdpi/ais_aton.png | Bin 0 -> 1133 bytes OsmAnd/res/drawable-mdpi/ais_aton_virt.png | Bin 0 -> 1232 bytes OsmAnd/res/drawable-mdpi/ais_land.png | Bin 0 -> 1217 bytes OsmAnd/res/drawable-mdpi/ais_plane.png | Bin 0 -> 1912 bytes OsmAnd/res/drawable-mdpi/ais_sar.png | Bin 0 -> 3217 bytes OsmAnd/res/drawable-mdpi/ais_vessel.png | Bin 0 -> 1446 bytes OsmAnd/res/drawable-mdpi/ais_vessel_cross.png | Bin 0 -> 2351 bytes OsmAnd/res/drawable-mdpi/ais_vessel_red.png | Bin 0 -> 1358 bytes OsmAnd/res/drawable-xhdpi/ais_aton.png | Bin 0 -> 2167 bytes OsmAnd/res/drawable-xhdpi/ais_aton_virt.png | Bin 0 -> 2484 bytes OsmAnd/res/drawable-xhdpi/ais_land.png | Bin 0 -> 2611 bytes OsmAnd/res/drawable-xhdpi/ais_map.png | Bin 0 -> 46783 bytes OsmAnd/res/drawable-xhdpi/ais_plane.png | Bin 0 -> 4348 bytes OsmAnd/res/drawable-xhdpi/ais_sar.png | Bin 0 -> 7339 bytes OsmAnd/res/drawable-xhdpi/ais_vessel.png | Bin 0 -> 3199 bytes .../res/drawable-xhdpi/ais_vessel_cross.png | Bin 0 -> 5291 bytes OsmAnd/res/drawable-xhdpi/ais_vessel_red.png | Bin 0 -> 3236 bytes OsmAnd/res/drawable-xxhdpi/ais_aton.png | Bin 0 -> 1960 bytes OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png | Bin 0 -> 2388 bytes OsmAnd/res/drawable-xxhdpi/ais_land.png | Bin 0 -> 2941 bytes OsmAnd/res/drawable-xxhdpi/ais_plane.png | Bin 0 -> 4334 bytes OsmAnd/res/drawable-xxhdpi/ais_sar.png | Bin 0 -> 7574 bytes OsmAnd/res/drawable-xxhdpi/ais_vessel.png | Bin 0 -> 3114 bytes .../res/drawable-xxhdpi/ais_vessel_cross.png | Bin 0 -> 3913 bytes OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png | Bin 0 -> 3278 bytes OsmAnd/res/values/strings.xml | 12 + OsmAnd/res/xml/ais_settings.xml | 36 + .../plus/mapcontextmenu/MenuController.java | 4 + .../osmand/plus/plugins/PluginsHelper.java | 2 + .../aistracker/AisMessageListener.java | 471 ++++++++++ .../plus/plugins/aistracker/AisObject.java | 802 ++++++++++++++++++ .../aistracker/AisObjectConstants.java | 335 ++++++++ .../aistracker/AisObjectMenuController.java | 163 ++++ .../plugins/aistracker/AisTrackerLayer.java | 252 ++++++ .../plugins/aistracker/AisTrackerPlugin.java | 152 ++++ .../AisTrackerSettingsFragment.java | 152 ++++ .../fragments/SettingsScreenType.java | 4 +- gradle.properties | 22 +- 47 files changed, 2393 insertions(+), 16 deletions(-) create mode 100644 OsmAnd/res/drawable-hdpi/ais_aton.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_aton_virt.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_land.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_plane.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_sar.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_vessel.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_vessel_cross.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_vessel_red.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_aton.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_aton_virt.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_land.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_plane.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_sar.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_vessel.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_vessel_cross.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_vessel_red.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_aton.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_aton_virt.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_land.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_map.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_plane.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_sar.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_vessel.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_vessel_cross.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_vessel_red.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_aton.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_land.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_plane.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_sar.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_vessel.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_vessel_cross.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png create mode 100644 OsmAnd/res/xml/ais_settings.xml create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java diff --git a/OsmAnd/build.gradle b/OsmAnd/build.gradle index 2e37884d217..50e97ed397b 100644 --- a/OsmAnd/build.gradle +++ b/OsmAnd/build.gradle @@ -221,4 +221,6 @@ dependencies { amazonFreeImplementation "com.amazon:in-app-purchasing:2.0.76@jar" amazonFullImplementation "com.amazon:in-app-purchasing:2.0.76@jar" + + implementation 'net.sf.marineapi:marineapi:0.12.0' } diff --git a/OsmAnd/res/drawable-hdpi/ais_aton.png b/OsmAnd/res/drawable-hdpi/ais_aton.png new file mode 100644 index 0000000000000000000000000000000000000000..8fb447e4b4a106a8a8a48cca416575ad62cde0ae GIT binary patch literal 1540 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 gSpF07*qoM6N<$g20hRRR910 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-hdpi/ais_land.png b/OsmAnd/res/drawable-hdpi/ais_land.png new file mode 100644 index 0000000000000000000000000000000000000000..2d6145f592554976e991ed6c89cff7a61766414c GIT binary patch literal 1902 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 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-hdpi/ais_vessel.png b/OsmAnd/res/drawable-hdpi/ais_vessel.png new file mode 100644 index 0000000000000000000000000000000000000000..479fbe46d6579ffc0364f986a9cf7007a0e64af9 GIT binary patch literal 2158 zcmV-!2$A=RP)EntM4InxVq6x!=eHi<~Ot$x6OV2)=-KW9FknK?AD z*Is+=wHE+@O10gON2#hYL?qhm%cIq7HXF=lv#{-cR3+Q)05*LvFJ8R3S*=$8ST2`; z84id4eE06%zwEU(JNFodvhh3*)9Lijd7h8+JRi_v1w92Gb4Jx_AQ55xv z=o$c+8J_2zCrNSw07A5T4-}k>^W&mIzMBsVeAIoB`_OaE*nOOip7>4Vj zC=Q$a;PtmEnwRkK>`3=*S#SkS8J_%y&lbGGn}5DqTlbs z7^9U^2*dCSXpcj!7{tuRaU7&+`Yg|L7-LKrhB!GnL6t*VYfL7StCB;yjlQ;F5CPCq zN>@sGLqy2)T+HWlbh}*uK$0ZL^IQ-Si0D}qMIkni{>Dd9;|5jua~cFe!pvZ1N|Iz- zX0cenaU5u^;rsq5j^jT7?Qp1#5me<*hnczC?b33&+!jdF6un-Lm|2w^vMc@n{Sj2* z&nnMzFf$K_L!6$TZp$1WALID=xXPi+wm8(ZL6$#KO1UN?6h$HIlMC4F2bBgQ&vP3O zT}DwfKnomd)SwD~ra=&V%gj(pZKqcm4*>w?^EsrHU}pHfAIEY01JFE&njS%xKWifD zKExlpa(^rwYSf?#e=aS527>_>iv@tqa>CA_IF2zIjlj&Ua;QmzEPsUMk5Wp^W-|Pv8nMA$6h)U>YcR9!_xniG6u`s$vA>&6r#L)3 zgx1<}Xw@W#nlOj}D9`gS9*?iJ*3eqhWHQ0w;o+vhuNY*EL8sHfVzEG;=RybprPK`( zb+9q0_GwnTLH5f>Q8Xf=3%fyEiy6dt??ftL40n48-46l?@FvjR02w)@V zk3Ed+wb^WjgM$MYV@y#LNYnILnb=T!a#a%y(q)3emF3TPJjQ4=0`O^rj4|+iAM^Qq z8xKh-Z={qi8c49J)gXJ$9!1e15k0H&$L{rfO14arB(O)*K@cQamd%0sIaIenmOmH1 z?~k)d1HdL=^nIV0nTg1AUH7IeQtNl9wekvou9Z??X4UKU=wtEEec3Ye z`5eRH5X@{7tQz4^-3+q)xwQPT8}h*C859Hoo;-PiG84^m$U)5^MX zi!@D9-ud5Lm)4t2#3V_el(HN;%d%_+)XSkd4YK_C!T0@`nIWaz^2ct@|HiriYy!p# zhq_I~L$y`>DgJy;T3wYxgTbK6p;g@+s*^!?_`}RJ8jTRgF@Vp@_pxP%!y%^AX_Z53 zDWyP-9IC~j3V*f<;x_(qUH7x5mz8B!nx<6_{VvP0Z-Dv|J?k_GaU6f|`~IZNJ4E$> z@JqE=EYR(CEr)up>t2_aYWXg;R9a0CFYmVQ^?KVAj4!#{RZ#c){f9X8-!sKk*S7;5giB?LfDu;lY5f;fQf{?FI&$7-=ypwsCPGaDl6yRKVX!uifCyvHADjJd;~uQ`mU zZlDE00Aq|UIdti|ZY>2|xdC7e6EtPC5&*(QiHv-W*ozJC20Z{EB?`A*nZH&VX3V4sS*C<>=c zIRE>uIkZ!QnAwzhJ*)Tc--C!00K9$scDrS zf^72B&r(V#r6du_PN%bTHW*{JKT=BMdH!!3QecO#lJC?YB4VW!UcP+!39j kR_(RdUVH7e*W=fJ02pM7TwO7oYybcN07*qoM6N<$f?$UWJ^%m! literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-hdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-hdpi/ais_vessel_cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6d534b2bb7dc752bf6f06f0303aea13233d0a9e3 GIT binary patch literal 3501 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 bpP>E^K{@{)ItO7`00000NkvXXu0mjf_^YsI literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-hdpi/ais_vessel_red.png b/OsmAnd/res/drawable-hdpi/ais_vessel_red.png new file mode 100644 index 0000000000000000000000000000000000000000..32d66b56a1bb1953961bdfdc2e12c2053a3ec938 GIT binary patch literal 2193 zcmV;C2yXX@P)29~{73bImo^TmX0=9!Sw~d~p&$rHoOa<}Rg_nDGtNzk#LncXNRU@Er`F!P95( z<9G0f^l!gfxhzqOz<=Q%{{!>He*WEXKTE;Mq>3EeoxC1ERl%tSB481Ee}ubGd; za5%Ex89*7J0%xJ!CZa$N6={$Fx(}fbXWD+J1mqHELhmuGLa`hw%%FrnXRyVApzg!*7Z7J*QUDR0MNkrmF-9;31@Q(YL*L8~t5P_}&e}aP-HW1_0SMYCd zARw$hjKT_@yAqwM{w|51O-Obmhjg)csCmOt1Sd%B*GAm9f#r~94aD%1c*cMD=;*6 zo~*)F7geo8P$AgrVL)jSd^i}`5{M}R)VUj!;!mpy+X;Vy_8tTk5C~dbmuZFuG)~~R zF4)}TEh2-`{0WSXwE_t``w(lGj0v}TcqLy%>&S8F6pGFppoBkX3PPi`7LcI54{?kD zq6Eg5gj;<)&nuimxf%2Ze~zAu;2ER6aDkw;3zJI_Wy@pHgwmZ&N-NF6Ih1DxCHzqd ze+;Pd#sj>?2E9p=fQ%nfp^h!%lUIDI98fRvkUN-L}H;7_unKpY_pHwfBY zG$@TjgO800U_FFwky-7;N>STDJyWBcrt~ptbf$9w=_W=3}@I)3sfe z%G{u2ad`rV4OohdF$yGT?Y;M@ySTsmV5^S;F*=R~{F>uXPVr}}gg-lQe1RZF$Es8& z?DSxK9eO;# zUAUH8yLgovo-qlB4zuP^CI(&0pOq%1JDZL_A8Ak=;oZ1NdE+b0ltX8sJ%`LVl$AjV zf2uy5zRjPHZ(KDxhH!zf-GeEIcF$oQvTO9r#Gq7zc((y-i5)>}7yQ5$kZ-}qT^VJlCaZ<$)zoZeXK;mctp_Z;sup=aVV39^O+muf}A#p zi*3*>E#JmpYug5ePdSx@L(8`)uiv*ELNaeS&%z+VzMx_o#2tUaP7kRre$e{sI@$S9 zEorclu@ailJBKD@!=afQl;Y3B25h);n3UUlP!8?l^AmN&q1K*TmuaJ*R?D#AeC7vy z;{_E@={Q!1gq_}}^Cyugm|WV0s~_MQ6F*1z7LICU!=afP^frG40aIGKNqK$C^5=6f z;W$)UzC(Gfg8?xjl&Ak>Gw0B(Ge`jKK`?@$5`_j;Zto)tOO`*MdyV4=QjHC<66)}q z-@&aUfLUs$1|^He$FRKto0nKhd5p6GKCTa5yi2|c%n>_Zwvp!(v2fjvmU5UBonrp7Pz99V{MKf>i T*H{PL00000NkvXXu0mjf+Zgol literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_aton.png b/OsmAnd/res/drawable-mdpi/ais_aton.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ae2676d9fb26226f2f22a12487b3544096360d GIT binary patch literal 1133 zcmV-z1d{uSP)^CB_4BVHZTg%T8xJ%*=M$PD?wT z9#o#>$+n$m=lOnr&+mDemRse&T!O4lxr=Gtxq}}d7Ei6O+Di^Ui`ndxP&kdQ>c43y zlt|oZoi}~zEhlZ&5{rczb$3^93WY+k*aqwD_n8HIL})nUL(lr&1q}^t?Yg?GFCxA0 zEqnZJMO!Sf*(3H^bj&$dG&CfVI=Zb#pcj7L9?K#xnz2p8cOs!pcGNF1xj-5GlE@31 zp2EGjL+A&Qpni$T1ct#sjTi|wVf}O@stb$)AIkTDsYz557!^KQZ9g@M@dZYOr~4=| zrI8ptb(3)gYm^dv?X7$ z$PGKa;-u|b+S(eU1^4CxRp3d-wqfvLC6{=7GKt{=RpE;zVPG=k5>Hf@7~K7&@cAB) zOAKo_fkZjNIPh^vEkY-(o;}Oj7AN*Fi2+CPet|%c!LPO7@zsObpvZ?M6`DFD5DsZu zY*Qw&FO!J1vj?TF((HpzBpLh(^Y&U6d42%=Rs60yMUw2UmkOVqmF@>Cx+isl`>?)5 zBGGhQB(c*zpKE)@kCs;%SCRXLA_p9@#p{;b@SNE`_vF0FACU7kiXcdA}Y(FY|T)rI+-loTu zD3*MJ_6qd2oe+Mwz$6lZbj?2_odesYbD(ztm=ONv0#zmQzo(=d+LO{b&^vD?gda_y zibTGYws1eTsV(QL2~?3F-RL%}J#DH9RFQ~{k~W8`!jCU7u0+0M4u(JMh&QUjR}&al zA}{6euT279U0_s+;;J+S_{juDl?d?(zOEmBa)DtIzldBG`dQ~xxmYiJJpv3Hi4}in zYnhn_Jazs;(<||yIrrFYPQy8e9jzapY5dF@3d8~}vAy2LT3>qC1xfbITO0iJN+1vv z9wGx_**6OHbTzG#o_*41?nLx&i2JSbzbyX%6NH-JxM;N500000NkvXXu0mjf3Wg3Q literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_aton_virt.png b/OsmAnd/res/drawable-mdpi/ais_aton_virt.png new file mode 100644 index 0000000000000000000000000000000000000000..998e6dfeb7f2acadec38dd6d32a9ef7235ac2380 GIT binary patch literal 1232 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 yJ8fk0jq5X literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_sar.png b/OsmAnd/res/drawable-mdpi/ais_sar.png new file mode 100644 index 0000000000000000000000000000000000000000..78bb51add2c2ca97ac2cedcd6e4a85fffe28b689 GIT binary patch literal 3217 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 Da{x4^ literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_vessel.png b/OsmAnd/res/drawable-mdpi/ais_vessel.png new file mode 100644 index 0000000000000000000000000000000000000000..1c40885e13bcf6c66639a9438b22d91de482fc99 GIT binary patch literal 1446 zcmV;X1zGxuP)K+D^`lW;86^dk^;<*kysQ_#cCaG$PWz zbRAVexBPDu_jPxOh}7ZX;mY^@`1R}8f8J9bw3-e`L`*3qCzHu!wOaiOp#S#m+b5To zmw%LFP3B`w2HTAp1i@LBWmcADG?`3R_m!{lfJ!LZ~2uLYm+xGXCWpw}uB5KsojS?Ut1^}Mt9TA~FF8P1CkYDdsrNi~GoVF9gQp@hVMI%w{tc$1&FHHG&{m0chL@jS^5w z30>FWdEUvpckgg`ct}S_M{K=bgLD4eFbo}lK!Y-Bj6h*d#W~+IP4lr*3UM5>BuR)d zhHkeTd7c;ToZASR>_BPYr$l7+`+XUPA&tjl8Vm+P2%(QgqvuWMZIS>1z-Tl&&$0}o z(MURu1Jg7ynM_ogrtmzk^g&}u8zZoz|I^K8gD8qf)2^mkaU8QWO~E;TVHgGjuth^Y zXoSFy{yoz)kEE1HlH@vek|ab#=ytn_=Xvv;YigsZ4wU*oW{eFC!;rJt?7G0|bV{~u z3n^tM2!cj^&=dgzfFKBd%(4u=@5{ZtJpfmF=bR%9LzSi}eBW=>2aOO=O36b1Kdjel zEEWrDwOWu;UKbig5!-Awr4N9Hd{CP}p=HTAANG2^BO$~M{g-(7@F4*}x7+>B^StTK zwYAVh2TJ`PF~;0(x2xv!IRO9>p{#K_oswl)LP}`_LGY}&_FqAO0KoVCvqJwO48!+> z1p&|VFdmOT^ud=Wpp=q~F$@NS^UY?1`Fu{gu0u+BUA&Z6tH>ydC{0rghr^RjrvqI1 zpcZNoDD`hymTlYiu@C~w zi^bn$QFdZBn^CXV6O6IG=XuBFaP0zWfB+y2!_z#^;kvHa-`~F)*m=N||9#)b-rk-n z=1QyG7*$b=fKo~^#^8D0YO~p3Hk(nW)48Gl5>iSq#t=mjt=DUKo_DJ2I+Rl4ix4RE zFD%RIS(f#5yWL{BTz>LyE|*KDl!9rRk2&W709EIGP^SZ>{vYYOzOU;#7K_EL@yZ(K z^EsKODT(Nt9UoM#qYi;`{dKq>W)1b{0aXqtw_VnLhD2EOl~)%8Io0z3LQY}ct@A|+n49OVdb$w8k00Z9Ze>fa&4-O8l zCvdl+92<|v@O{7Z!C5(6xsbbJNB`%A{?Y68KC1r`Qc4t0tnz*EW8s5JpWW|FU`PLL z+qR!)S%xTzSV8QwN2YXLhvPU-yWPfiyVYO4 zdR1w&u+?f&x7&r3($O>xRS4Xt<807*qoM6N<$f@HnB A!TN_+Dpx1HqSSY_*+{f>s$(Gg52CQXFeK zN(hk6?!EUp{jhg&rGaEam{!L#lbPgZ@BjS&&z^IhbDjsju+OnMf&H&}jMn4`zaKXz zV~XoIj-Gekd7VJy;fEjogSGZJxf)}09LE4GUc7j6Pft&OPfw5S>gt+N{=c!bF$XMR z&X_S{L4SY$`2PNW>FDU_JYN3B9IzBB*=)8`MDTr|#>U3ZP$-1&`zkWFQ9GsrL{QgafCL|9eFG!KRqATT0t0Vz$V({;&Yvdwi} zM8t@Qc%DZhkw|B=*))JLW>`oGd>@kl0i&RR3iI?Ov6(o^B$+u*fsSbrfj9WKOt6936$ zGI>fV{>ucE@}Mc3&8DE32SX5_+YOJcfo;>^oEBJ^h1P)RkfAiB03UpSo8Y#G;Kxq5;Gxa4-&-VJw^3lWhpKub%@snKY3LRtJD(h%f9RVfbyLS@9~cf-at@aJi8E)Y5h z@7)C7eFUB==QJV#LE`dH$X^IQKNqquz|TVPFBhUy;NnjSQFQ@kwzhUy*9DRP3UU!q zuTY>Rlj#JASnIEX1>LY|4ZJuFRsc>vyt5XrSPE@7z`DJ`LL4a(hRbjPj4dXl&WFTR z@V(RE3Sg24)QIGIR3E*+rsl@w8#k`4t*u>V`}%y0aWKL9SPQCFCQL8~`}$tqv|`0I zD-Rs_X{*RrT~!Hzd<<_t1Z&p8{d-_QKnyq*yrX6Dh&B|bSkk0S$i%DQ^3z~-Nk~*A z*SmD7vv<|1NFkd=To)M_01@oCafr24zw{Dcdf)+@eB~8&!3nAIG5qx*xOE-;ww$kuVJ9qQ0`g+|3-g}_$hC=$r8E0tEf(5z@^gR*X1>WD& zX?^DW^PiX+%=!Z2K%92n$%OB|n~C?{i{$f+ zf9^R{1O>d>T0Xw=O7hE>Q#g2V!oQz>8V(!?i-_l|dZXb1$I#3fz=${&3X@Z*__UKw zS}0q$QvKFj2G2uOi*cY@>?d+Ls$YNIm^a^KVm#iSYHDf#Tt~z?md>u?GaFOc?6Y}xXS=$&_JboXu%Ybi{f%HGwh8JsbL@VoC4-nWnF&Yco{;t3n??Tz&9 z+xOW73p+?gY5j<5}Xlyixh{s}>0|$!xG(b3vjm17C#OdwTurX3?ZGI;_b_d+D0bZ|E z$Z#8(Q6M00E`{YQ;fE8UBPT*smA-g<``)XsZpFs6Q zrcH$t?z``!$iP6ITf{2tgpGH=%^P4xu2|_ri*FxM#}RWtio~@6%n#t~U&39TaJ|Ch zFi6gEUj4P|x1U_H1_rQIRVquDhMs)?{fl?^ z^mMm6PP?Z%F%GR~!6j!xs|$a7yV$x^J`a>u{!D28BW$@G9RAV$+<4= znaslRu^3yd-SebcPM<>b@ibh>uOjvXI~NO-&I{_XIqb+G%ZL*kRB0#_B9qB1DijKs!>i5+%orp2e4gg! z=8jk_hPBp~qOi=bceD~Ztbm~`h;_fAp&?RVUq9#Y`L_ZbIz0ifqdD*z ztpL*LbW=PYPkEjPqico5TC34$^u)Tlx|soX#e7E7jT(T)#>Tm->M*+aFPU$xW!9`& z^Mk)D#;JH7C>_pbv)LtuLIG>-sC#);^>evgD3i&2ITQ-r;(4A8?ifalQIUWETw}2q zO-)U;u~>{qBoY>pQC+2~s)WN~CQX`DTUAws=Xn+11Xi9j#)!36ySloXmMvR$&cMKc zU&$_9Zq{0@si_HfcX$8p`RAX1t%N-qtk0~CdYUuE!~Ze4Ur<$RKQkA}U)bl&{sTSu VtL_rtG;II?002ovPDHLkV1lYHhpYeq literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_vessel_red.png b/OsmAnd/res/drawable-mdpi/ais_vessel_red.png new file mode 100644 index 0000000000000000000000000000000000000000..1262ccc1f8095dc579c829438e6b62c1a21550c1 GIT binary patch literal 1358 zcmV-U1+n^xP)T6If6^IJ}EF@5I2ds8bCT>Rs!# z`}n{})G2@go8^R=84y5&o&)+KM`ysy_JNf4K2ztwj>tg;T$?cmSsD<)cA;8@y)odm zddl0}lK`k#+unzEH0gshpa2HQ$TNCQlwj<~B$Q3X#s*0_fv2U*XXB?AI*C~^`2 z?n>P_3#+L4MGEU%b_@(eVh%D>CI;Tg|MN4zYjj1t669RG)?UZ~a##UnA;^k@tPI5Y zU#h~u2fWTuAquR)YxhGAuv`1kiY9(g2BQ2k^o%ZBrA(o`^Ig9wNMe@vmV zBg!pk#~AMr*}oc|PFXM@03(rO4Y-vx?XEV!j|~U}V{(->^3|pt1H&s0?$6AeGV@5Wv)7z6HZe!0U_@%HgJ)&SHD0 z0^kwscAys3NYz7H0}7xIdu5|n4cKC<7q0);(;*Ua(0nT7TwOAHc8Cn48mUe~S_TB* zMC7>!+=UgLueQLy44a;OOM!ohTd9#>Y1jZbj5y%F3`F_2F3=ZsI?4b1V+ew`4M51j z%sgJBD`$lFOmkrkoyI{L0|Mwelo~L)1d5%Z;N-aVp-Z6H9w;BE!d@3v2zM$Hdq`s- z&i`)NnwkNMtv&VD>hsoskb`2YCn(2wMn4){agex=Gz`S~KhoBcn_mp_Pd5LV|H-$b z>%sCm?(({cI5@dpD~S>rxH=su&;e3H)4?AURno<4*b4&gw0|f$~LaQfM#CS$O9zZG%k}{y+ zD*w)GImoYdgE0R_+#IW*Qdxzc=4sC9(#Ibg%H z@V*xo#^>Z$>$rI_H$*nq_%;3wFun96_!6xcVw5AglB z@atcom8e8Zps~i5!2KD1c@x%_the7Eywco6skD!Su`C#q?Vj&Pebm3z-;TO`NVKIN Q1ONa407*qoM6N<$f=;e;CjbBd literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xhdpi/ais_aton.png b/OsmAnd/res/drawable-xhdpi/ais_aton.png new file mode 100644 index 0000000000000000000000000000000000000000..547865fe1c1b26cecbc9177e7c024ea85dca5b48 GIT binary patch literal 2167 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 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xhdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-xhdpi/ais_vessel_cross.png new file mode 100644 index 0000000000000000000000000000000000000000..157094d1a2eb55da6cc13aaeb13ddbf11c4dd496 GIT binary patch literal 5291 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 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xhdpi/ais_vessel_red.png b/OsmAnd/res/drawable-xhdpi/ais_vessel_red.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea6f1b10bf34b9739a3d04adcb6187a06375165 GIT binary patch literal 3236 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 W3X~HB2|f$}000015?|IMjyyyL$ zH^axvO?QXM4gdgjY3@`%H0P)nq=m-DL1z`3K=yIkNeCU`vvXt=>wN3(&jtWXGxgHo zRFdNW0QV;v)#W5-L@`DeoSin7Ojs3>ubTQoZ5IrgI)0UcD=znUJiHKNaQ7V57aKO^ zc?!&h_nWp1j$BC#xV2RN6+`k0{FCI*H|E27BU53Ba`1+MU z6o;%zUrXbkaZ`>i<}G|?!|LOHkUkD+7n;-ZetWJI1@F3$ka<(M*D9;_%)M*&UmCJU zAFQbw$LmT!Sl6huA!2I2s_dP@xo4PnF5%EjP<)pn3tzuw>t8hrI@puu2fK#6onjfB zn1Z0a?EZ3LeueNsByFxuIIz~FI0S?hDM{-g4XUR1evl5O+L(Yw69*D(m}>hyhhh$d zy-}vR?{e(@0`Mlr3;l}h>$iLU5q?VQSv>hy?|Qle){x@`_FdUyUF*CXdG5{pfRqwh zRKSpP6YRTwrkkY7FGR?=T%f|G^Z9(YzjbZE(e+ms-GKqgMDs*?VFxEM{FT0`hBm38 z6RS?HmvK}K#7WoF7l5$xGrU@l-1w6qiOnZP{Hi7j2=hOcwczvxfH|rV!02=SyDPz( z0y=675;I7{U9OP}fTbHF$0i?; zlLc1UT-jh9VVjqNXCpI(ZdvbnWmX-(XRo|{slV~C*EmOFLreVdC~2DYV<$4@yjb$x z=4N{#OEhuKz31tMhuog#81JQ9diG;`$(<)j&O48Gw<6C9>qA#~WrKEDh~wqU)ITL2 zo9H}Gwt`T@hqcN_qZfm&8%fUVsBzJ0^4MGM6D<%V3v zDDxxxycCOjLW0{9Ni(^)s&%mZW}gQQ%9VYvm*H1iJ6fA>+mdP7#*HmKqz z4Q(gq*Nc2KJ_&1LPNs(;#(n$RD$m9(C`a2XmK*N9O2Q)|@&jTyt2AROS}~G^6`mOq zkvrOy3*&g?5`tg&Jicp>b@WEyjqKOIa^O-t)aS! z;imiB^pWP8z^P-K3-!THcQAwL^gO?W?X9(h{_;X@uVPME`x+%9CUUN)(ywJrR;VHf z))n*CH3ytU%N+@01Eb!1EhEV!V)AZerkH7=ny6s}z9e_ft{#YdYhl<}oxRe9#C*w} z8*9achT#)KCU2Y#Q?%TRPj6-jaPlp#PDMeh!zQUM zdcL=};(H%qgR7@)GU&;{`_g8ctdgFB2RlRoZ4RL+JfG`DF`4f2IX#jl7A&tbF}@uq zcD)?wsI)bLAstfGr79PI?=BD9N74ngsTl7-dDsyOY{)6n^WF(yawX5-*Sx5UeMqrg zt4@IjVP{K>nsW)@zj+~i^^1S3n2#fsU{I!hDh2InoSF-Ors%GkjfEk*it4fgHphvh z1}oyLC;#o%P1Mx?PZ0$UKTPG$<=Y1Ic^p*&?(hb$YBm4#xxast&?r_OSEkNZBo=rc zk&S!YkLk)24&+-bv4Z~6Ww!bwxo2~z#yY#*W_@)!w&8T?sN=2xEdsvQ_>V4R)`_4^ z<>t1N5wh4J;!w&O_s>ih^oVTGZh-_21Wqf64BYLq>Xn$Ot!v~^K!S!gRj#3*3Ysdl zDKMr_XJd`21hlDewQ(ulkzq+EC>O>ewn0chIk}58L?&K$y dRn(~n!;k1jRX>~vK)+gm_LUd)-tlkJeg#mde^USe literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png b/OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png new file mode 100644 index 0000000000000000000000000000000000000000..fb8768a9d749fb672a09884352acb6070042c241 GIT binary patch literal 2388 zcma)8dsLFy7XLs}az??7n#vF`MbqSp;S<;G$K&=S(I?aOqa}_e z%{{G*+S;tk0fY@(=`Z(Pb+UO9FGSD@@mO*tjBG=0EOn99k4LTvSv)gteMd|Z_l(Q( zeEns8vuN3Ql5A$q-bL$wNt$Ugc~>Kf=AwZ%@uhN2%-3BlkLD|6H*Bq4VY`ORNc#Fy zv%jGh2ih721DB#WyujvJwEfV$8@Wt`JUBgJ;$`Sd@a@}9x(!jGi-rs{A7pGb_6RC+ zzyBm_kODYTYI&zPyzb!-2sWJ65XfY)0X8&I4J3xAzmdD453OJ|gP$&DxJx8s)pu$g zIJa({;XPz}l|XUAlj>YW42L}4ND6bfeO;z_jDupJ+<4)J?Ry#ZP)@G^ss%wtYNVJF z$fDE)jpcI~9DoxHY%t#=O4^B!hy*DV-9G>U-Gr-k&`Qd!`EL|G0tF)oR~#sIOctC4 zSEdSr8kju@a28AL-_-DL+WP88g6sunHO_iM(`AXg$3mBF`1T!G+tnY{5KTRH`x#64 z8=ZvnJyslY)9B(?fgq#&$B|mDp`4=y*nb_VYX}k2opO|gyeL}n>DRULgNQmado_+&)4g@(-da!-m`kBw+ z%wE}d%B&kUvTwV#2kMiowm}>L7%AfrHLEF?xJtQJ*eKC17A>9b>kqVmXaSIv9oBZ1 zYmTtMnpufTP-BAC!T*B4G%IR6s>$n%H0p2?3(j^&VuX|P*r_z$NV_S+3Fc@7Lrx%( zU?xb2+YAJXhUPyeuXSL3a=8~d4aP83AQRv>pSlN)c`ZcE1_>u0lp=0;r=;LlZC@- zl(aOt*ybS-Sq+udn8w&{`!r7Z%h&SEuf;9WQ=9@>vvlf~cfZ(o(fk4b)Z18HL{I;1 zyYAJVz(Fsg9U(`1RkS~&1QE|On~VJw!GHG}f3i9IoIQA5N^;fT?m;2ETiMP--TZf=!-fg z+SyGn$z%SFlNW^=uBR0FyiF9p!E&_mO+IrkM*V_2QwCb)+AgyOVF{*l>n*z*uRT21 zs(J0n?8L7X#@5{x^XWdvMfh{i(RH(vx}oAFVzpm7hht-mRuOzGo{E|NWTB^W@IILy zyq?ki3vNq}CuMEk;15u00U>4-)=-(db!+;`KHZ@|9_Q-egp|!p6ID1{x^-Q6#PevG zRYZ-pAB)9i;qOnx!!a0~@X|&S76xa-nfXvE8v?~axsU6#AuA?QXCps(93)L)CU#|KH!Eh_D`G>Vj_TQ*OmDFWS@UeEEI7a?_r}N1a#^%_dikgY{@9)oa)-{+81PejjNwWb*zK4K8dXfs8t)i?L_xA;gcpEQ#`crf~_W{=n; zzaKruiLEl=iLx&e0_WOudYp>lUs=SIH>FnqUeCL>7m5YV(!v307fM-0;e``-&fhyD z3-!w`QkYq@yS{jvsSe9^j@_=NKChKjlST2Xi98?5_uNgE>Cf`o>vA^-3n3Ux&~CQo z39I`N`m*H%>u|h$$vwidREN{qdB9h&jbxy5y)%6ivsMTHyZ3rpL~fx4#%gqGglKu{ zO=@JRXKyRN@^S>NUE4>(3n&86LPn};O&|eCnm_x>umBbHcn5_!Lz&EPts#R?NfX=jc0%ze zR&ppLcyW#gc3)Xs%Vm-f3qWy+$g<|lh4*2cK*C5i!?$_=rh7EfB7Z;}Q!voEiuB&A z5@A^SXJ&8S8#w8u^7zEYIM5L{f(08Z1~au;$)LFY6*{#!zss(M>^gYoCnFE9vKDHq zpBX1JvxZ#AY7nE`e%=A*=(Y<9c2Z=5`bwC8`TYmUFK`aHg4Q5`9{?>!+lYp|4F|jU v*^#3;`==Q%J$W$hF}~G#$NxhjWIJz_X%u`~&-VwvMF7Rg)$x(TPjUYM<2gqw literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_land.png b/OsmAnd/res/drawable-xxhdpi/ais_land.png new file mode 100644 index 0000000000000000000000000000000000000000..d9bc317901d02196a18cd8916977291d7f14db77 GIT binary patch literal 2941 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

7^Z zulRGY^c3)B>M!~;PViZuhve;OzRXC8se3O0#_ckxnz5`xuTaqWgFwaP`R_MjT|CaJ zT9PcaiL#n1NlNjUNe1@@(o$PC(;x-UZ%^#vYQr0<4S@?zo&a?#C)wtVr$`*fodirP(~PZ^VVGAVW4yFd!0#~JB@dfPiygC6$w znGNR_laEto>V)QB%*54S)nFZknk2S4{8zB+tpCDTtHvHobuwEDJ*Ll}Y!23eSo))D z9-@WAu><{GvP?r~VemleGIW%*UjMz1Te-{iPW(YzuxB+3JO@*(6y8?Aq{46EN2)=k zzln;)`;ID2#~K@_eP_E_)G-A5g-di8rLT)V-kq^RB_z@PEXo5kBlp||m5X?~PrSSq&wN9vXKnV{fAPSkSp*X=`%$Cg0R`#y^>fCr%%pGb zCm%+GUb@;(&jtCXH-s^A0<^V|H-&<0k>&(2B_=~|W?PxG*-l3PwukIxY*F2=es=e* zB68xAR~SgW*Pba)R~HE_cSW`YMHHF?#_Eo@8wt&w6))?8S)~3+aOJU_Ox)|A&GXFa02Y_3Mv>8Bwre0_EqWH&JRSXriIur6%IMM^2?KQDx zHuS+e)0=0d^?^9|I3Jx(GvnO{Sw|x?6`^Z~N#sZ^U)N{~vbAtpLJ9g^Uf62X{PO1L z<$IJdz2zK}^u3t(cEVa*ve9oOu$SxUhpM+&Ztr`bd_k+w&-|L7drL}0UcfbNQIu@J zWn2dq9=xD97~++6V#i}(6=JW-vXRFWd}=iIq{S^2OxxW1&4yUXGtuAz(EoE__B&_j zL~ZU|j*UeCHK5n&a?&WP`>Sks55d3}n}y87MB;aL{97}Z{U*KfO6XtFby+`=AB_Jk zf+=!)CK-$#?k$X>ylm%+A4`!em<@uAY9lOtQk}?Kn-PSc2{%8MIvh@!ZpglEaZiu5 zG+J*sYD{CR0o>hNED#VCqXVRC(A@_e9AUFG4I2<3h6hjRvMI5@Y{QCVR&_U~o@j%0 zLqlg1UT}PY>-a&gI(i(k6coLeOqSPfHct-#wW{a82OW~m0w;4;nT^~UG4CTHv8ka49pSRw9&JyL_gY7pnW*`q zkk1C-gvjZwFSfR;{1CB*-S4IE;WO-0*FVk-eufjq-xf{H{qk$c?8e<~^BW2r z$>Gc^9Np0YaD9<3y1CvsseFo%a)iscyIlL%)stooS7~ZU3d%P!}$l47S^x{Cp}&7G7^5uys4chq%5(S`gn8{lLoK`+1c- z@cPq|L#wCtR|t_5~uilo2QLsCeoHon3>UY?mb6+KK!~p1svoE?#ExQjn^x zy8Nr#@h~UNV!=7Jg3gw>p;Eayu$M9RxD=@Vf zu|ffUfLmYJ6%Jo2i{;Nc(Y@4sd3o0OjV&)poK&{kplZfiZ0Io5RunIO#{(Lvr}2Vk z6mLM3`W#T(b)gF#6|Tl>?ee#lowDGGQ$-fZE=jXaE2r}=SX9Ep`Be9g1Z)H2`rDXsX9nX)k*0tZEBvE zPOYsUxK8)zG`bi%%ugP~w*(kBtsIS2aw}WXzfiOlU*%!#UCn|)I97=)QF3-l$hHv= z5D&=zXwZraRZ)&};&zd@Zki%O4@j{H_Mj+C>%K{<0l2cN&82=Hlq)+NL{-E0+Z!6-}I$GJ4>2wmQtQ#x-H@((8m)YSceZ#X&&iF})r!asJfg zp^E=*=)c`vUZ>dlux1`jRwC}QnPc>aTdSHZrxvO_yH0O6*#d*1QZ7FkeWf?i_9-M# zx7Vv`Xt6L?nSu~y?i)AKuu8B*zHbm)HXFI|qSO>3re1#g?WkC&c@z1T#6)2Q ztXQCLw;dL@WhB@`5h@u7=AK?QHTHMVdBr)j)tRI-D;Oci7PKUM#g@Euq7gMoWC#UN{!Q z0ZI+}Nf>yYQe+a|9pH$U_LhQhrqRedhaHb%5rv*P)UlN;F1q5CO%ODK3thl+u{4Xs zyBUtd1Tguqx6MGX8e!6a_QUCVkp>!9_4EYH{(m*~|IZIv3b&+x=K5?d2Ck&>KP(8e MG+@wrb(_fl00X%?$^ZZW literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_sar.png b/OsmAnd/res/drawable-xxhdpi/ais_sar.png new file mode 100644 index 0000000000000000000000000000000000000000..259534b079e969b673449b329c3d343c5667b160 GIT binary patch literal 7574 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 bo%ol!Otjs6s`K|sSOpZHsmgtSY7+QAa`K!8 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_vessel.png b/OsmAnd/res/drawable-xxhdpi/ais_vessel.png new file mode 100644 index 0000000000000000000000000000000000000000..aa2ab6816876d1d685bfa07f02ce4bd45124d49b GIT binary patch literal 3114 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 Yfx#9M*xmjITex=!=Z*0%^c-XU2V=X)NdN!< literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-xxhdpi/ais_vessel_cross.png new file mode 100644 index 0000000000000000000000000000000000000000..9a5d73b5ee6079e91e2b9a833bd51d2dc4c16c6b GIT binary patch literal 3913 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 Q_4lj!1uM)O6VH492gGaymH+?% literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png b/OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png new file mode 100644 index 0000000000000000000000000000000000000000..29da74357f7c5b8c2f521fd454307c478050ff8b GIT binary patch literal 3278 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 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index 75039b3ce98..0bf7b77c54a 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -772,6 +772,16 @@ You need to activate the sensor so OsmAnd can find it. Cloud Precipitation Measurement units + IP address settings + Choose NMEA protocol (UDP/TCP) and define addresses + Protocol for NMEA data reception + Choose protocol for NMEA data reception + IP address of NMEA data source + Define IP address of the NMEA data source (if TCP is used) + TCP port of NMEA data source + Define TCP port number of the NMEA data source + UDP port of local NMEA data receiver + Define UPD port where OsmAnd receives NMEA data Weather Explore Weather forecast. Contours @@ -3870,6 +3880,8 @@ Download tile maps directly, or copy them as SQLite database files to OsmAnd\'s Mark where your car is parked, and notify your calendar when the parking meter will expire. To place the marker, choose a place on the map, go to \"Actions\", and tap \"Add parking\". Distance calculator and planning tool Create paths by tapping the map, or by using or modifying existing GPX files, to plan a trip and measure the distance between points. The result can be saved as a GPX file to use later for guidance. + AIS vessel tracker + Display AIS positions and information about surrounding vessels. The AIS data is received via network from an external AIS receiver. Accessibility Makes the device\'s accessibility features directly available in OsmAnd. This facilitates e.g. adjusting the speech rate for text-to-speech voices, configuring D-pad navigation, using a trackball for zoom control, or text-to-speech feedback, for example to auto-announce your position. OpenStreetMap editing diff --git a/OsmAnd/res/xml/ais_settings.xml b/OsmAnd/res/xml/ais_settings.xml new file mode 100644 index 00000000000..c3af0a4620b --- /dev/null +++ b/OsmAnd/res/xml/ais_settings.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java b/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java index cf6c61f05be..dac62fe9542 100644 --- a/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java +++ b/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java @@ -62,6 +62,8 @@ import net.osmand.plus.mapmarkers.MapMarker; import net.osmand.plus.plugins.OsmandPlugin; import net.osmand.plus.plugins.PluginsHelper; +import net.osmand.plus.plugins.aistracker.AisObject; +import net.osmand.plus.plugins.aistracker.AisObjectMenuController; import net.osmand.plus.plugins.audionotes.AudioVideoNoteMenuController; import net.osmand.plus.plugins.audionotes.AudioVideoNotesPlugin.Recording; import net.osmand.plus.plugins.mapillary.MapillaryImage; @@ -238,6 +240,8 @@ public static MenuController getMenuController(@NonNull MapActivity mapActivity, menuController = new RenderedObjectMenuController(mapActivity, pointDescription, (RenderedObject) object); } else if (object instanceof MapillaryImage) { menuController = new MapillaryMenuController(mapActivity, pointDescription, (MapillaryImage) object); + } else if (object instanceof AisObject) { + menuController = new AisObjectMenuController(mapActivity, pointDescription, (AisObject) object); } else if (object instanceof SelectedGpxPoint) { menuController = new SelectedGpxMenuController(mapActivity, pointDescription, (SelectedGpxPoint) object); } else if (object instanceof Pair) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java index c0ae61c2053..b3e87409e8e 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java @@ -58,6 +58,7 @@ import net.osmand.plus.plugins.skimaps.SkiMapsPlugin; import net.osmand.plus.plugins.srtm.SRTMPlugin; import net.osmand.plus.plugins.weather.WeatherPlugin; +import net.osmand.plus.plugins.aistracker.AisTrackerPlugin; import net.osmand.plus.poi.PoiUIFilter; import net.osmand.plus.quickaction.QuickActionType; import net.osmand.plus.search.dialogs.QuickSearchDialogFragment; @@ -117,6 +118,7 @@ public static void initPlugins(@NonNull OsmandApplication app) { checkMarketPlugin(app, new SRTMPlugin(app)); allPlugins.add(new WeatherPlugin(app)); checkMarketPlugin(app, new NauticalMapsPlugin(app)); + allPlugins.add(new AisTrackerPlugin(app)); checkMarketPlugin(app, new SkiMapsPlugin(app)); allPlugins.add(new AudioVideoNotesPlugin(app)); checkMarketPlugin(app, new ParkingPositionPlugin(app)); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java new file mode 100644 index 00000000000..c9a33adf3ee --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -0,0 +1,471 @@ +package net.osmand.plus.plugins.aistracker; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import net.sf.marineapi.ais.event.AbstractAISMessageListener; +import net.sf.marineapi.ais.message.AISMessage01; +import net.sf.marineapi.ais.message.AISMessage02; +import net.sf.marineapi.ais.message.AISMessage03; +import net.sf.marineapi.ais.message.AISMessage04; +import net.sf.marineapi.ais.message.AISMessage05; +import net.sf.marineapi.ais.message.AISMessage09; +import net.sf.marineapi.ais.message.AISMessage18; +import net.sf.marineapi.ais.message.AISMessage19; +import net.sf.marineapi.ais.message.AISMessage21; +import net.sf.marineapi.ais.message.AISMessage24; +import net.sf.marineapi.ais.message.AISMessage27; +import net.sf.marineapi.nmea.event.SentenceListener; +import net.sf.marineapi.nmea.io.SentenceReader; + +import java.io.IOException; +import java.io.InputStream; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.EmptyStackException; +import java.util.Stack; +import java.util.Timer; +import java.util.TimerTask; + +public class AisMessageListener { + private AisTrackerLayer aisLayer; + private Timer timer; + private TimerTask taskCheckNetworkConnection; + private DatagramSocket udpSocket; + private Socket tcpSocket; + private InputStream tcpStream; + private SentenceReader sentenceReader = null; + private Stack listenerList = null; + public AisMessageListener(int port, @NonNull AisTrackerLayer aisLayer) { + initMembers(aisLayer); + try { + udpSocket = new DatagramSocket(port); + udpSocket.setReuseAddress(true); + initListeners(); + Log.d("AisMessageListener","new UDP listener, Port " + port); + } + catch (Exception e) { + Log.e("AisMessageListener","exception: " + e.getMessage()); + udpSocket = null; + } + } + public AisMessageListener(@NonNull String serverIp, int serverPort, @NonNull AisTrackerLayer aisLayer) { + initMembers(aisLayer); + taskCheckNetworkConnection = new TimerTask() { + @Override + public void run() { + Log.d("AisMessageListener", "timer task taskCheckNetworkConnection running"); + if ((tcpSocket == null) || (!tcpSocket.isConnected())) { + try { + tcpSocket = new Socket(); + tcpSocket.setTcpNoDelay(true); + tcpSocket.setReuseAddress(true); + // tcpSocket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort), 5000); + tcpSocket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort)); + tcpStream = tcpSocket.getInputStream(); + initListeners(); + Log.d("AisMessageListener","new TCP listener"); + } + catch (IOException e) { + Log.e("AisMessageListener","exception: " + e.getMessage()); + tcpStream = null; + tcpSocket = null; + } + } + } + }; + this.timer = new Timer(); + timer.schedule(taskCheckNetworkConnection, 1000, 30000); + } + private void initMembers(@NonNull AisTrackerLayer aisLayer) { + this.aisLayer = aisLayer; + this.udpSocket = null; + this.tcpSocket = null; + this.tcpStream = null; + this.listenerList = new Stack<>(); + } + private void initListeners() throws IOException { + if (tcpStream != null) { + sentenceReader = new SentenceReader(tcpStream); + } + if (udpSocket != null) { + sentenceReader = new SentenceReader(udpSocket); + } + if (sentenceReader != null) { + new AisListener01(); + new AisListener02(); + new AisListener03(); + new AisListener04(); + new AisListener05(); + new AisListener09(); + new AisListener18(); + new AisListener19(); + new AisListener21(); + new AisListener24(); + new AisListener27(); + sentenceReader.start(); + } else { + Log.e("AisMessageListener", "sentenceReader not initialized"); + } + } + private void removeListeners() { + sentenceReader.stop(); + while (!this.listenerList.isEmpty()) { + SentenceListener listener; + try { + listener = this.listenerList.pop(); + sentenceReader.removeSentenceListener(listener); + Log.d("AisMessageListener", "SentenceListener removed"); + } catch (EmptyStackException e) { + Log.e("AisMessageListener", "stack empty"); + } + } + } + public void stopListener() { + if (this.timer != null) { + this.timer.cancel(); + this.timer.purge(); + this.timer = null; + } + removeListeners(); + if (tcpSocket != null) { + Log.d("AisMessageListener","stopListener"); + try { + if (tcpSocket.isConnected()) { + tcpSocket.close(); + } + if (tcpStream != null) { + tcpStream.close(); + } + } catch (Exception ignore) { } + } + if (udpSocket != null) { + if (udpSocket.isConnected()) { + udpSocket.disconnect(); + } + udpSocket.close(); + } + } + private void handleAisMessage(int aisType, Object obj) { + AisObject ais = null; + int msgType = 0; + int mmsi = 0; + int timeStamp = 0; + int imo = 0; + int heading = AisObjectConstants.INVALID_HEADING; + int navStatus = AisObjectConstants.INVALID_NAV_STATUS; + int manInd = AisObjectConstants.INVALID_MANEUVER_INDICATOR; + int shipType = AisObjectConstants.INVALID_SHIP_TYPE; + int dimensionToBow = AisObjectConstants.INVALID_DIMENSION; + int dimensionToStern = AisObjectConstants.INVALID_DIMENSION; + int dimensionToPort = AisObjectConstants.INVALID_DIMENSION; + int dimensionToStarboard = AisObjectConstants.INVALID_DIMENSION; + int etaMon = AisObjectConstants.INVALID_ETA; + int etaDay = AisObjectConstants.INVALID_ETA; + int etaHour = AisObjectConstants.INVALID_ETA_HOUR; + int etaMin = AisObjectConstants.INVALID_ETA_MIN; + int altitude = AisObjectConstants.INVALID_ALTITUDE; + int aidType = AisObjectConstants.UNSPECIFIED_AID_TYPE; + double draught = AisObjectConstants.INVALID_DRAUGHT; + double cog = AisObjectConstants.INVALID_COG; + double sog = AisObjectConstants.INVALID_SOG; + double lat = AisObjectConstants.INVALID_LAT; + double lon = AisObjectConstants.INVALID_LON; + double rot = AisObjectConstants.INVALID_ROT; + String callSign = null; + String shipName = null; + String destination = null; + + switch (aisType) { + case 1: AISMessage01 aisMsg01 = (AISMessage01)obj; // position report class A + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg01.getMMSI() + + " Type: " + aisMsg01.getMessageType() + + " ROT: " + aisMsg01.getRateOfTurn()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg01); + mmsi = aisMsg01.getMMSI(); + msgType = aisMsg01.getMessageType(); + navStatus = aisMsg01.getNavigationalStatus(); + manInd = aisMsg01.getManouverIndicator(); + if (aisMsg01.hasTimeStamp()) { timeStamp = aisMsg01.getTimeStamp(); } + if (aisMsg01.hasTrueHeading()) { heading = aisMsg01.getTrueHeading(); } + if (aisMsg01.hasCourseOverGround()) { cog = aisMsg01.getCourseOverGround(); } + if (aisMsg01.hasSpeedOverGround()) { sog = aisMsg01.getSpeedOverGround(); } + if (aisMsg01.hasLatitude()) { lat = aisMsg01.getLatitudeInDegrees(); } + if (aisMsg01.hasLongitude()) { lon = aisMsg01.getLongitudeInDegrees(); } + if (aisMsg01.hasRateOfTurn()) { rot = aisMsg01.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 2: AISMessage02 aisMsg02 = (AISMessage02)obj; // position report class A + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg02.getMMSI() + + " Type: " + aisMsg02.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg02); + mmsi = aisMsg02.getMMSI(); + msgType = aisMsg02.getMessageType(); + navStatus = aisMsg02.getNavigationalStatus(); + manInd = aisMsg02.getManouverIndicator(); + if (aisMsg02.hasTimeStamp()) { timeStamp = aisMsg02.getTimeStamp(); } + if (aisMsg02.hasTrueHeading()) { heading = aisMsg02.getTrueHeading(); } + if (aisMsg02.hasCourseOverGround()) { cog = aisMsg02.getCourseOverGround(); } + if (aisMsg02.hasSpeedOverGround()) { sog = aisMsg02.getSpeedOverGround(); } + if (aisMsg02.hasLatitude()) { lat = aisMsg02.getLatitudeInDegrees(); } + if (aisMsg02.hasLongitude()) { lon = aisMsg02.getLongitudeInDegrees(); } + if (aisMsg02.hasRateOfTurn()) { rot = aisMsg02.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 3: AISMessage03 aisMsg03 = (AISMessage03)obj; // position report class A + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg03.getMMSI() + + " Type: " + aisMsg03.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg03); + mmsi = aisMsg03.getMMSI(); + msgType = aisMsg03.getMessageType(); + navStatus = aisMsg03.getNavigationalStatus(); + manInd = aisMsg03.getManouverIndicator(); + if (aisMsg03.hasTimeStamp()) { timeStamp = aisMsg03.getTimeStamp(); } + if (aisMsg03.hasTrueHeading()) { heading = aisMsg03.getTrueHeading(); } + if (aisMsg03.hasCourseOverGround()) { cog = aisMsg03.getCourseOverGround(); } + if (aisMsg03.hasSpeedOverGround()) { sog = aisMsg03.getSpeedOverGround(); } + if (aisMsg03.hasLatitude()) { lat = aisMsg03.getLatitudeInDegrees(); } + if (aisMsg03.hasLongitude()) { lon = aisMsg03.getLongitudeInDegrees(); } + if (aisMsg03.hasRateOfTurn()) { rot = aisMsg03.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 4: AISMessage04 aisMsg04 = (AISMessage04)obj; // base station report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg04.getMMSI() + + " Type: " + aisMsg04.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg04); + mmsi = aisMsg04.getMMSI(); + msgType = aisMsg04.getMessageType(); + if (aisMsg04.hasLatitude()) { lat = aisMsg04.getLatitudeInDegrees(); } + if (aisMsg04.hasLongitude()) { lon = aisMsg04.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, lat, lon); + break; + + case 5: AISMessage05 aisMsg05 = (AISMessage05)obj; // static and voyage related data + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg05.getMMSI() + + " Type: " + aisMsg05.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg05); + mmsi = aisMsg05.getMMSI(); + msgType = aisMsg05.getMessageType(); + imo = aisMsg05.getIMONumber(); + callSign = aisMsg05.getCallSign(); + shipName = aisMsg05.getName(); + shipType = aisMsg05.getTypeOfShipAndCargoType(); + dimensionToBow = aisMsg05.getBow(); + dimensionToStern = aisMsg05.getStern(); + dimensionToPort = aisMsg05.getPort(); + dimensionToStarboard = aisMsg05.getStarboard(); + draught = aisMsg05.getMaximumDraught(); + destination = aisMsg05.getDestination(); + etaMon = aisMsg05.getETAMonth(); + etaDay = aisMsg05.getETADay(); + etaHour = aisMsg05.getETAHour(); + etaMin = aisMsg05.getETAMinute(); + ais = new AisObject(mmsi, msgType, imo, callSign, shipName, shipType, dimensionToBow, + dimensionToStern, dimensionToPort, dimensionToStarboard, draught, + destination, etaMon, etaDay, etaHour, etaMin); + break; + + case 9: AISMessage09 aisMsg09 = (AISMessage09)obj; // SAR aircraft position report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg09.getMMSI() + + " Type: " + aisMsg09.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg09); + mmsi = aisMsg09.getMMSI(); + msgType = aisMsg09.getMessageType(); + timeStamp = aisMsg09.getTimeStamp(); + cog = aisMsg09.getCourseOverGround(); + sog = aisMsg09.getSpeedOverGround(); + altitude = aisMsg09.getAltitude(); + if (aisMsg09.hasLatitude()) { lat = aisMsg09.getLatitudeInDegrees(); } + if (aisMsg09.hasLongitude()) { lon = aisMsg09.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, timeStamp, altitude, cog, sog, lat, lon); + break; + + case 18: AISMessage18 aisMsg18 = (AISMessage18)obj; // basic class B position report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg18.getMMSI() + + " Type: " + aisMsg18.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg18); + mmsi = aisMsg18.getMMSI(); + msgType = aisMsg18.getMessageType(); + if (aisMsg18.hasTimeStamp()) { timeStamp = aisMsg18.getTimeStamp(); } + if (aisMsg18.hasTrueHeading()) { heading = aisMsg18.getTrueHeading(); } + if (aisMsg18.hasCourseOverGround()) { cog = aisMsg18.getCourseOverGround(); } + if (aisMsg18.hasSpeedOverGround()) { sog = aisMsg18.getSpeedOverGround(); } + if (aisMsg18.hasLatitude()) { lat = aisMsg18.getLatitudeInDegrees(); } + if (aisMsg18.hasLongitude()) { lon = aisMsg18.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 19: AISMessage19 aisMsg19 = (AISMessage19)obj; // extended class B position report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg19.getMMSI() + + " Type: " + aisMsg19.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg19); + mmsi = aisMsg19.getMMSI(); + msgType = aisMsg19.getMessageType(); + shipType = aisMsg19.getTypeOfShipAndCargoType(); + dimensionToBow = aisMsg19.getBow(); + dimensionToStern = aisMsg19.getStern(); + dimensionToPort = aisMsg19.getPort(); + dimensionToStarboard = aisMsg19.getStarboard(); + if (aisMsg19.hasTimeStamp()) { timeStamp = aisMsg19.getTimeStamp(); } + if (aisMsg19.hasTrueHeading()) { heading = aisMsg19.getTrueHeading(); } + if (aisMsg19.hasCourseOverGround()) { cog = aisMsg19.getCourseOverGround(); } + if (aisMsg19.hasSpeedOverGround()) { sog = aisMsg19.getSpeedOverGround(); } + if (aisMsg19.hasLatitude()) { lat = aisMsg19.getLatitudeInDegrees(); } + if (aisMsg19.hasLongitude()) { lon = aisMsg19.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, timeStamp, heading, cog, sog, lat, lon, + shipType, dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + break; + + case 21: AISMessage21 aisMsg21 = (AISMessage21)obj; // aid-to-navigation report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg21.getMMSI() + + " Type: " + aisMsg21.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg21); + mmsi = aisMsg21.getMMSI(); + msgType = aisMsg21.getMessageType(); + dimensionToBow = aisMsg21.getBow(); + dimensionToStern = aisMsg21.getStern(); + dimensionToPort = aisMsg21.getPort(); + dimensionToStarboard = aisMsg21.getStarboard(); + aidType = aisMsg21.getAidType(); + if (aisMsg21.hasLatitude()) { lat = aisMsg21.getLatitudeInDegrees(); } + if (aisMsg21.hasLongitude()) { lon = aisMsg21.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, lat, lon, aidType, + dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + break; + + case 24: AISMessage24 aisMsg24 = (AISMessage24)obj; // static data report (like type 5) + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg24.getMMSI() + + " Type: " + aisMsg24.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg24); + mmsi = aisMsg24.getMMSI(); + msgType = aisMsg24.getMessageType(); + callSign = aisMsg24.getCallSign(); + shipName = aisMsg24.getName(); + shipType = aisMsg24.getTypeOfShipAndCargoType(); + dimensionToBow = aisMsg24.getBow(); + dimensionToStern = aisMsg24.getStern(); + dimensionToPort = aisMsg24.getPort(); + dimensionToStarboard = aisMsg24.getStarboard(); + ais = new AisObject(mmsi, msgType, imo, callSign, shipName, shipType, dimensionToBow, + dimensionToStern, dimensionToPort, dimensionToStarboard, draught, + null, etaMon, etaDay, etaHour, etaMin); + break; + + case 27: AISMessage27 aisMsg27 = (AISMessage27)obj; // long range broadcast message + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg27.getMMSI() + + " Type: " + aisMsg27.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg27); + mmsi = aisMsg27.getMMSI(); + msgType = aisMsg27.getMessageType(); + navStatus = aisMsg27.getNavigationalStatus(); + manInd = aisMsg27.getManouverIndicator(); + if (aisMsg27.hasTimeStamp()) { timeStamp = aisMsg27.getTimeStamp(); } + if (aisMsg27.hasTrueHeading()) { heading = aisMsg27.getTrueHeading(); } + if (aisMsg27.hasCourseOverGround()) { cog = aisMsg27.getCourseOverGround(); } + if (aisMsg27.hasSpeedOverGround()) { sog = aisMsg27.getSpeedOverGround(); } + if (aisMsg27.hasLatitude()) { lat = aisMsg27.getLatitudeInDegrees(); } + if (aisMsg27.hasLongitude()) { lon = aisMsg27.getLongitudeInDegrees(); } + if (aisMsg27.hasRateOfTurn()) { rot = aisMsg27.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + default: + Log.e("AisMessageListener","handleAisMessage() invalid argument aisType: "+ aisType); + return; + } + aisLayer.updateAisObjectList(ais); + } + private void initEmbeddedLister(int aisType, @NonNull SentenceListener listener) { + AisMessageListener.this.sentenceReader.addSentenceListener(listener); + AisMessageListener.this.listenerList.push(listener); + Log.d("AisMessageListener","Listener Type " + aisType + " started"); + } + private class AisListener01 extends AbstractAISMessageListener { + public AisListener01() { initEmbeddedLister(1, this); } + @Override + public void onMessage(AISMessage01 msg) { + handleAisMessage(1, msg); + } + } + private class AisListener02 extends AbstractAISMessageListener { + public AisListener02() { initEmbeddedLister(2, this); } + @Override + public void onMessage(AISMessage02 msg) { + handleAisMessage(2, msg); + } + } + private class AisListener03 extends AbstractAISMessageListener { + public AisListener03() { initEmbeddedLister(3, this); } + @Override + public void onMessage(AISMessage03 msg) { + handleAisMessage(3, msg); + } + } + private class AisListener04 extends AbstractAISMessageListener { + public AisListener04() { initEmbeddedLister(4, this); } + @Override + public void onMessage(AISMessage04 msg) { + handleAisMessage(4, msg); + } + } + private class AisListener05 extends AbstractAISMessageListener { + public AisListener05() { initEmbeddedLister(5, this); } + @Override + public void onMessage(AISMessage05 msg) { + handleAisMessage(5, msg); + } + } + private class AisListener09 extends AbstractAISMessageListener { + public AisListener09() { initEmbeddedLister(9, this); } + @Override + public void onMessage(AISMessage09 msg) { + handleAisMessage(9, msg); + } + } + private class AisListener18 extends AbstractAISMessageListener { + public AisListener18() { initEmbeddedLister(18, this); } + @Override + public void onMessage(AISMessage18 msg) { + handleAisMessage(18, msg); + } + } + private class AisListener19 extends AbstractAISMessageListener { + public AisListener19() { initEmbeddedLister(19, this); } + @Override + public void onMessage(AISMessage19 msg) { + handleAisMessage(19, msg); + } + } + private class AisListener21 extends AbstractAISMessageListener { + public AisListener21() { initEmbeddedLister(21, this); } + @Override + public void onMessage(AISMessage21 msg) { + handleAisMessage(21, msg); + } + } + private class AisListener24 extends AbstractAISMessageListener { + public AisListener24() { initEmbeddedLister(24, this); } + @Override + public void onMessage(AISMessage24 msg) { + handleAisMessage(24, msg); + } + } + private class AisListener27 extends AbstractAISMessageListener { + public AisListener27() { initEmbeddedLister(27, this); } + @Override + public void onMessage(AISMessage27 msg) { + handleAisMessage(27, msg); + } + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java new file mode 100644 index 00000000000..7143414ecec --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -0,0 +1,802 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_AIRPLANE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON_VIRTUAL; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_INVALID; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_LANDSTATION; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_SART; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_COMMERCIAL; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_FAST; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_FREIGHT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_PASSENGER; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_SPORT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.COUNTRY_CODES; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ALTITUDE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_COG; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_DIMENSION; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_DRAUGHT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA_HOUR; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA_MIN; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_HEADING; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_LAT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_LON; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_MANEUVER_INDICATOR; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_NAV_STATUS; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ROT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.maxAgeInMinutes; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.maxVesselAgeInMinutes; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LightingColorFilter; +import android.graphics.Paint; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.data.LatLon; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.R; + +import java.util.SortedSet; +import java.util.TreeSet; + +public class AisObject { + /* variable names starting with "ais_" belong to values received via an AIS message, + * its values may differ from the received values: they can be scaled, + * see https://gpsd.gitlab.io/gpsd/AIVDM.html */ + private int ais_msgType; + private int ais_mmsi; + private int ais_timeStamp = 0; + private int ais_imo = 0; + private int ais_heading = INVALID_HEADING; + private int ais_navStatus = INVALID_NAV_STATUS; + private int ais_manInd = INVALID_MANEUVER_INDICATOR; + private int ais_shipType = INVALID_SHIP_TYPE; + private int ais_dimensionToBow = INVALID_DIMENSION; + private int ais_dimensionToStern = INVALID_DIMENSION; + private int ais_dimensionToPort = INVALID_DIMENSION; + private int ais_dimensionToStarboard = INVALID_DIMENSION; + private int ais_etaMon = INVALID_ETA; + private int ais_etaDay = INVALID_ETA; + private int ais_etaHour = INVALID_ETA_HOUR; + private int ais_etaMin = INVALID_ETA_MIN; + private int ais_altitude = INVALID_ALTITUDE; + private int ais_aidType = UNSPECIFIED_AID_TYPE; + private double ais_draught = INVALID_DRAUGHT; + private double ais_cog = INVALID_COG; + private double ais_sog = INVALID_SOG; + private double ais_rot = INVALID_ROT; + private LatLon ais_position = null; + private String ais_callSign = null; + private String ais_shipName = null; + private String ais_destination = null; + private String countryCode = null; + private SortedSet msgTypes = null; + private long lastUpdate = 0; + /* after this time the object is outdated and can be removed: */ + + private AisObjType objectClass; + private Bitmap bitmap = null; + private int bitmapColor; + + public AisObject(int mmsi, int msgType, double lat, double lon) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int timeStamp, int navStatus, int manInd, int heading, + double cog, double sog, double lat, double lon, double rot) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + this.ais_timeStamp = timeStamp; + this.ais_navStatus = navStatus; + this.ais_manInd = manInd; + this.ais_heading = heading; + this.ais_cog = cog; + this.ais_sog = sog; + this.ais_rot = rot; + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int timeStamp, int altitude, + double cog, double sog, double lat, double lon) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + this.ais_timeStamp = timeStamp; + this.ais_altitude = altitude; + this.ais_cog = cog; + this.ais_sog = sog; + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int timeStamp, int heading, + double cog, double sog, double lat, double lon, + int shipType, int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + initDimensions(dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + this.ais_timeStamp = timeStamp; + this.ais_heading = heading; + this.ais_cog = cog; + this.ais_sog = sog; + this.ais_shipType = shipType; + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int imo, @Nullable String callSign, @Nullable String shipName, + int shipType, int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard, + double draught, @Nullable String destination, int etaMon, + int etaDay, int etaHour, int etaMin) { + initObj(mmsi, msgType); + initDimensions(dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + this.ais_shipType = shipType; + this.ais_draught = draught; + this.ais_callSign = callSign; + this.ais_shipName = shipName; + this.ais_destination = destination; + this.ais_etaMon = etaMon; + this.ais_etaDay = etaDay; + this.ais_etaHour = etaHour; + this.ais_etaMin = etaMin; + this.ais_imo = imo; + initObjectClass(); + } + + public AisObject(int mmsi, int msgType, double lat, double lon, int aidType, + int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + initDimensions(dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + this.ais_aidType = aidType; + initObjectClass(); + } + public AisObject(@NonNull AisObject ais) { + this.set(ais); + } + private String getCountryCode(Integer mmsi) { + String mmsiString = mmsi.toString(); + + if (mmsiString.length() > 2) { + String countryCode = mmsiString.substring(0, 3); + mmsiString = COUNTRY_CODES.get(Integer.parseInt(countryCode)); + if (mmsiString != null) { + return mmsiString; + } + } + return ""; + } + /* to be called only by a contructor! */ + private void initObj(int mmsi, int msgType) { + this.msgTypes = new TreeSet<>(); + this.ais_mmsi = mmsi; + this.ais_msgType = msgType; + this.countryCode = getCountryCode(this.ais_mmsi); + this.msgTypes.add(ais_msgType); + this.lastUpdate = System.currentTimeMillis(); + } + private void initLatLon(double lat, double lon) { + if ((lat != INVALID_LAT) && (lon != INVALID_LON)) { + ais_position = new LatLon(lat, lon); + } + } + + private void initDimensions(int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard) { + this.ais_dimensionToBow = dimensionToBow; + this.ais_dimensionToStern = dimensionToStern; + this.ais_dimensionToPort = dimensionToPort; + this.ais_dimensionToStarboard = dimensionToStarboard; + } + + private void initObjectClass() { + switch (this.ais_shipType) { + case INVALID_SHIP_TYPE: // not initialized + break; + + case 20: // Wing in ground (WIG) + case 21: // WIG, Hazardous category A + case 22: // WIG, Hazardous category B + case 23: // WIG, Hazardous category C + case 24: // WIG, Hazardous category D + case 40: // High Speed Craft (HSC) + case 41: // HSC, Hazardous category A + case 42: // HSC, Hazardous category B + case 43: // HSC, Hazardous category C + case 44: // HSC, Hazardous category D + case 49: // HSC, No additional information + this.objectClass = AIS_VESSEL_FAST; + break; + + case 30: // Fishing + case 31: // Towing + case 32: // Towing + case 33: // Dredging + case 34: // Diving ops + case 35: // Military ops + case 50: // Pilot Vessel + case 51: // Search and Rescue vessel + case 52: // Tug + case 53: // Port Tender + case 54: // Anti-pollution equipment + case 55: // Law Enforcement + case 56: // Spare - Local Vessel + case 57: // Spare - Local Vessel + case 58: // Medical Transport + case 59: // Noncombatant ship according to RR Resolution No. 18 + this.objectClass = AIS_VESSEL_COMMERCIAL; + break; + + case 36: // Sailing + case 37: // Pleasure Craft + this.objectClass = AIS_VESSEL_SPORT; + break; + + case 60: // Passenger, all ships of this type + case 61: // Passenger, Hazardous category A + case 62: // Passenger, Hazardous category B + case 63: // Passenger, Hazardous category C + case 64: // Passenger, Hazardous category D + case 69: // Passenger, No additional information + this.objectClass = AIS_VESSEL_PASSENGER; + break; + + case 70: // Cargo, all ships of this type + case 71: // Cargo, Hazardous category A + case 72: // Cargo, Hazardous category B + case 73: // Cargo, Hazardous category C + case 74: // Cargo, Hazardous category D + case 79: // Cargo, No additional information + case 80: // Tanker, all ships of this type + case 81: // Tanker, Hazardous category A + case 82: // Tanker, Hazardous category B + case 83: // Tanker, Hazardous category C + case 84: // Tanker, Hazardous category D + case 89: // Tanker, No additional information + this.objectClass = AIS_VESSEL_FREIGHT; + break; + + case 90: // Other Type, all ships of this type + case 91: // Other Type, Hazardous category A + case 92: // Other Type, Hazardous category B + case 93: // Other Type, Hazardous category C + case 94: // Other Type, Hazardous category D + case 99: // Other Type, no additional information + default: + this.objectClass = AIS_VESSEL; + break; + } + /* for the case that no ship type was transmitted... */ + if (ais_shipType == INVALID_SHIP_TYPE) { + if (msgTypes.contains(9)) { // aircraft + this.objectClass = AIS_AIRPLANE; + } else if (msgTypes.contains(4)) { // base station + this.objectClass = AIS_LANDSTATION; + } else if (msgTypes.contains(21)) { // aids to navigation + switch (ais_aidType) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_21_aid_to_navigation_report + case 29: // Safe Water + case 30: // Special Mark + this.objectClass = AIS_ATON_VIRTUAL; + break; + default: + this.objectClass = AIS_ATON; + } + } else { + switch (ais_navStatus) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a + case 0: // Under way using engine + case 1: // At anchor + case 2: // Not under command + case 3: // Restricted manoeuverability + case 4: // Constrained by her draught + case 5: // Moored + case 6: // Aground + case 8: // Under way sailing + case 11: // Power-driven vessel towing astern (regional use) + case 12: // Power-driven vessel pushing ahead or towing alongside (regional use). + this.objectClass = AIS_VESSEL; + break; + + case 7: // Engaged in Fishing + this.objectClass = AIS_VESSEL_COMMERCIAL; + break; + + case 14: // AIS-SART is active + this.objectClass = AIS_SART; + break; + + case INVALID_NAV_STATUS: // no valid value + default: + this.objectClass = AIS_INVALID; + } + } + } + } + + public void set(@NonNull AisObject ais) { + this.ais_mmsi = ais.getMmsi(); + this.ais_msgType = ais.getMsgType(); + if (ais.getTimestamp() != 0) { this.ais_timeStamp = ais.getTimestamp(); } + if (ais.getImo() != 0 ) { this.ais_imo = ais.getImo(); } + if (ais.getHeading() != INVALID_HEADING ) { this.ais_heading = ais.getHeading(); } + if (ais.getNavStatus() != INVALID_NAV_STATUS ) { this.ais_navStatus = ais.getNavStatus(); } + if (ais.getManInd() != INVALID_MANEUVER_INDICATOR ) { this.ais_manInd = ais.getManInd(); } + if (ais.getShipType() != INVALID_SHIP_TYPE ) { this.ais_shipType = ais.getShipType(); } + if (ais.getDimensionToBow() != INVALID_DIMENSION ) { this.ais_dimensionToBow = ais.getDimensionToBow(); } + if (ais.getDimensionToStern() != INVALID_DIMENSION ) { this.ais_dimensionToStern = ais.getDimensionToStern(); } + if (ais.getDimensionToPort() != INVALID_DIMENSION ) { this.ais_dimensionToPort = ais.getDimensionToPort(); } + if (ais.getDimensionToStarboard() != INVALID_DIMENSION ) { this.ais_dimensionToStarboard = ais.getDimensionToStarboard(); } + if (ais.getEtaMon() != INVALID_ETA ) { this.ais_etaMon = ais.getEtaMon(); } + if (ais.getEtaDay() != INVALID_ETA ) { this.ais_etaDay = ais.getEtaDay(); } + if (ais.getEtaHour() != INVALID_ETA_HOUR ) { this.ais_etaHour = ais.getEtaHour(); } + if (ais.getEtaMin() != INVALID_ETA_MIN ) { this.ais_etaMin = ais.getEtaMin(); } + if (ais.getAltitude() != INVALID_ALTITUDE) { this.ais_altitude = ais.getAltitude(); } + if (ais.getAidType() != UNSPECIFIED_AID_TYPE) { this.ais_aidType = ais.getAidType(); } + if (ais.getDraught() != INVALID_DRAUGHT) { this.ais_draught = ais.getDraught(); } + if (ais.getCog() != INVALID_COG) { this.ais_cog = ais.getCog(); } + if (ais.getSog() != INVALID_SOG) { this.ais_sog = ais.getSog(); } + if (ais.getRot() != INVALID_ROT) { this.ais_rot = ais.getRot(); } + if (ais.getPosition() != null) { this.ais_position = ais.getPosition(); } + if (ais.getCallSign() != null) { this.ais_callSign = ais.getCallSign(); } + if (ais.getShipName() != null) { this.ais_shipName = ais.getShipName(); } + if (ais.getDestination() != null ) { this.ais_destination = ais.getDestination(); } + + this.countryCode = ais.getCountryCode(); + + /* this method does not produce an exact copy of the given object, here are the differences: */ + this.lastUpdate = System.currentTimeMillis(); + if (this.msgTypes == null) { + this.msgTypes = new TreeSet<>(); + } + this.msgTypes.add(ais_msgType); + this.initObjectClass(); + //this.objectClass = ais.getObjectClass(); // test only... remove later + this.bitmap = null; + this.bitmapColor = 0; + } + + private void setBitmap(@NonNull AisTrackerLayer mapLayer) { + if (isLost()) { + if (isMovable()) { + this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); + } + } else { + switch (this.objectClass) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + case AIS_INVALID: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel); + break; + case AIS_LANDSTATION: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_land); + break; + case AIS_AIRPLANE: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_plane); + break; + case AIS_SART: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_sar); + break; + case AIS_ATON: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton); + break; + case AIS_ATON_VIRTUAL: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton_virt); + break; + } + } + this.setColor(); + } + + private void setColor() { + if (isLost()) { + if (isMovable()) { + this.bitmapColor = 0; // black + } + } else { + switch (this.objectClass) { + case AIS_VESSEL: + this.bitmapColor = Color.GREEN; + break; + case AIS_VESSEL_SPORT: + this.bitmapColor = Color.YELLOW; + break; + case AIS_VESSEL_FAST: + this.bitmapColor = Color.BLUE; + break; + case AIS_VESSEL_PASSENGER: + this.bitmapColor = Color.CYAN; + break; + case AIS_VESSEL_FREIGHT: + this.bitmapColor = Color.GRAY; + break; + case AIS_VESSEL_COMMERCIAL: + this.bitmapColor = Color.LTGRAY; + break; + default: + this.bitmapColor = 0; // black + } + } + } + + public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, + @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { + if ((this.bitmap == null) || isLost()) { + this.setBitmap(mapLayer); + } + if (this.bitmapColor != 0) { + paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); + } else { + paint.setColorFilter(null); + } + if (this.bitmap != null) { + canvas.save(); + canvas.rotate(tileBox.getRotate(), (float)tileBox.getCenterPixelX(), (float)tileBox.getCenterPixelY()); + float speedFactor = getMovement(); + int locationX = tileBox.getPixXFromLonNoRot(this.ais_position.getLongitude()); + int locationY = tileBox.getPixYFromLatNoRot(this.ais_position.getLatitude()); + float fx = locationX - this.bitmap.getWidth() / 2.0f; + float fy = locationY - this.bitmap.getHeight() / 2.0f; + if (this.needRotation()) { + float rotation = 0; + if (this.ais_cog != INVALID_COG) { rotation = (float)this.ais_cog; } + else if (this.ais_heading != INVALID_HEADING ) { rotation = this.ais_heading; } + canvas.rotate(rotation, locationX, locationY); + } + canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); + if ((speedFactor > 0) && (!isLost())) { + float lineStartX = locationX; + float lineLength = (float)this.bitmap.getHeight() * speedFactor; + float lineStartY = locationY - this.bitmap.getHeight() / 4.0f; + float lineEndY = lineStartY - lineLength; + canvas.drawLine(lineStartX, lineStartY, lineStartX, lineEndY, paint); + } + canvas.restore(); + } + } + + public boolean isMovable() { + switch (this.objectClass) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + case AIS_AIRPLANE: + return true; + default: + return false; + } + } + /* + for AIS objects that are moving, return a value that is taken as multiple of bitmap + height to draw a line to indicate the speed, + otherwise return 0 (no movement) + */ + private float getMovement() { + if (this.ais_sog > 0.0d) { + if (isMovable()) { + if (this.ais_sog < 2.0d) { return 0.0f; } + if (this.ais_sog < 5.0d) { return 1.0f; } + if (this.ais_sog < 10.0d) { return 3.0f; } + if (this.ais_sog < 25.0d) { return 6.0f; } + return 8.0f; + } + } + return 0.0f; + } + private boolean needRotation() { + if (((this.ais_cog != INVALID_COG) && (this.ais_cog != 0)) || + ((this.ais_heading != INVALID_HEADING) && (this.ais_heading != 0))) + { + return isMovable(); + } + return false; + } + + private boolean isLost(long maxAgeInMin) { + return ((System.currentTimeMillis() - this.lastUpdate) / 1000 / 60) > maxAgeInMin; + } + private boolean isLost() { + return isLost(maxVesselAgeInMinutes); + } + + /* + * this function checks the age of the object (check lastUpdate against its limit) + * and returns true if the object is outdated and can be removed + * */ + public boolean checkObjectAge() { + return isLost(maxAgeInMinutes); + } + public int getMsgType() { return this.ais_msgType; } + public SortedSet getMsgTypes() { return this.msgTypes; } + public int getMmsi() { return this.ais_mmsi; } + public int getTimestamp() { return this.ais_timeStamp; } + public int getImo() { return this.ais_imo; } + public int getHeading() { return this.ais_heading; } + public int getNavStatus() { return this.ais_navStatus; } + public int getManInd() { return this.ais_manInd; } + public int getShipType() { return this.ais_shipType; } + public int getDimensionToBow() { return this.ais_dimensionToBow; } + public int getDimensionToStern() { return this.ais_dimensionToStern; } + public int getDimensionToPort() { return this.ais_dimensionToPort; } + public int getDimensionToStarboard() { return this.ais_dimensionToStarboard; } + public int getEtaMon() { return this.ais_etaMon; } + public int getEtaDay() { return this.ais_etaDay; } + public int getEtaHour() { return this.ais_etaHour; } + public int getEtaMin() { return this.ais_etaMin; } + public int getAltitude() { return this.ais_altitude; } + public int getAidType() { return this.ais_aidType; } + public double getCog() { return this.ais_cog; } + public double getSog() { return this.ais_sog; } + public double getRot() { return this.ais_rot; } + public double getDraught() { return this.ais_draught; } + @Nullable + public LatLon getPosition() { + return this.ais_position; + } + @Nullable + public String getCallSign() { + return this.ais_callSign; + } + @Nullable + public String getShipName() { + return this.ais_shipName; + } + @Nullable + public String getDestination() { + return this.ais_destination; + } + @NonNull + public String getCountryCode() { return this.countryCode; } + public AisObjType getObjectClass() { return this.objectClass; } + public long getLastUpdate() { return this.lastUpdate; } + @NonNull + public String getShipTypeString() { + switch (this.ais_shipType) { + case INVALID_SHIP_TYPE: // not initialized + return("unknown"); + case 20: + return("Wing in ground (WIG)"); + case 21: + return("WIG, Hazardous category A"); + case 22: + return("WIG, Hazardous category B"); + case 23: + return("WIG, Hazardous category C"); + case 24: + return("WIG, Hazardous category D"); + case 40: + return("High Speed Craft (HSC)"); + case 41: + return("HSC, Hazardous category A"); + case 42: + return("HSC, Hazardous category B"); + case 43: + return("HSC, Hazardous category C"); + case 44: + return("HSC, Hazardous category D"); + case 49: // HSC, No additional information + return("High Speed Craft (HSC)"); + case 30: + return("Fishing"); + case 31: + return("Towing"); + case 32: + return("Towing"); + case 33: + return("Dredging"); + case 34: + return("Diving ops"); + case 35: + return("Military ops"); + case 50: + return("Pilot Vessel"); + case 51: + return("Search and Rescue vessel"); + case 52: + return("Tug"); + case 53: + return("Port Tender"); + case 54: + return("Anti-pollution equipment"); + case 55: + return("Law Enforcement"); + case 56: + return("Spare - Local Vessel"); + case 57: + return("Spare - Local Vessel"); + case 58: + return("Medical Transport"); + case 59: + return("Noncombatant ship according to RR Resolution No. 18"); + case 36: + return("Sailing"); + case 37: + return("Pleasure Craft"); + case 60: + return("Passenger"); + case 61: + return("Passenger, Hazardous category A"); + case 62: + return("Passenger, Hazardous category B"); + case 63: + return("Passenger, Hazardous category C"); + case 64: + return("Passenger, Hazardous category D"); + case 69: // Passenger, No additional information + return("Passenger"); + case 70: // Cargo, all ships of this type + return("Cargo"); + case 71: + return("Cargo, Hazardous category A"); + case 72: + return("Cargo, Hazardous category B"); + case 73: + return("Cargo, Hazardous category C"); + case 74: + return("Cargo, Hazardous category D"); + case 79: // Cargo, No additional information + return("Cargo"); + case 80: // Tanker, all ships of this type + return("Tanker"); + case 81: + return("Tanker, Hazardous category A"); + case 82: + return("Tanker, Hazardous category B"); + case 83: + return("Tanker, Hazardous category C"); + case 84: + return("Tanker, Hazardous category D"); + case 89: // Tanker, No additional information + return("Tanker"); + case 90: // Other Type, all ships of this type + return("Other Type"); + case 91: + return("Other Type, Hazardous category A"); + case 92: + return("Other Type, Hazardous category B"); + case 93: + return("Other Type, Hazardous category C"); + case 94: + return("Other Type, Hazardous category D"); + case 99: // Other Type, no additional information + return("Other Type"); + default: + return Integer.toString(ais_shipType); + } + } + @NonNull + public String getNavStatusString() { + switch (this.ais_navStatus) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a + case 0: + return("Under way using engine"); + case 1: + return("At anchor"); + case 2: + return("Not under command"); + case 3: + return("Restricted manoeuverability"); + case 4: + return("Constrained by her draught"); + case 5: + return("Moored"); + case 6: + return("Aground"); + case 8: + return("Under way sailing"); + case 11: + return("Power-driven vessel towing astern (regional use)"); + case 12: + return("Power-driven vessel pushing ahead or towing alongside (regional use)"); + case 7: + return("Engaged in Fishing"); + case 14: + return("AIS-SART is active"); + case INVALID_NAV_STATUS: // no valid value + return("unknown"); + default: + return(Integer.toString(ais_navStatus)); + } + } + @NonNull + public String getManIndString() { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a + switch (this.ais_manInd) { + case 0: + return("Not available"); + case 1: + return("No special maneuver"); + case 2: + return("Special maneuver"); + default: + return(Integer.toString(ais_manInd)); + } + } + @NonNull + public String getAidTypeString() { + switch (this.ais_aidType) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_21_aid_to_navigation_report + case 0: + return("not specified"); + case 1: + return("Reference point"); + case 2: + return("RACON (radar transponder marking a navigation hazard)"); + case 3: + return("Fixed structure off shore"); + case 4: + return("Spare, Reserved for future use"); + case 5: + return("Light, without sectors"); + case 6: + return("Light, with sectors"); + case 7: + return("Leading Light Front"); + case 8: + return("Leading Light Rear"); + case 9: + return("Beacon, Cardinal N"); + case 10: + return("Beacon, Cardinal E"); + case 11: + return("Beacon, Cardinal S"); + case 12: + return("Beacon, Cardinal W"); + case 13: + return("Beacon, Port hand"); + case 14: + return("Beacon, Starboard hand"); + case 15: + return("Beacon, Preferred Channel port hand"); + case 16: + return("Beacon, Preferred Channel starboard hand"); + case 17: + return("Beacon, Isolated danger"); + case 18: + return("Beacon, Safe wate"); + case 19: + return("Beacon, Special mark"); + case 20: + return("Cardinal Mark N"); + case 21: + return("Cardinal Mark E"); + case 22: + return("Cardinal Mark S"); + case 23: + return("Cardinal Mark W"); + case 24: + return("Port hand Mark"); + case 25: + return("Starboard hand Mark"); + case 26: + return("Preferred Channel Port hand"); + case 27: + return("Preferred Channel Starboard hand"); + case 28: + return("Isolated danger"); + case 29: + return("Safe Water"); + case 30: + return("Special Mark"); + case 31: + return("Light Vessel / LANBY / Rigs"); + default: + return(Integer.toString(ais_aidType)); + } + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java new file mode 100644 index 00000000000..9b3dc923953 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -0,0 +1,335 @@ +package net.osmand.plus.plugins.aistracker; + +import java.util.AbstractMap; +import java.util.Map; + +public final class AisObjectConstants { + /* after this time the object is outdated and can be removed: */ + public final static long maxAgeInMinutes = 7; + /* after this time the (movable) object is lost, the bitmap can be changed: */ + public final static long maxVesselAgeInMinutes = 4; + public final static int INVALID_HEADING = 511; + public final static int INVALID_NAV_STATUS = 15; + public final static int INVALID_MANEUVER_INDICATOR = 0; + public final static int INVALID_SHIP_TYPE = 0; + public final static int INVALID_DIMENSION = 0; + public final static int INVALID_ETA = 0; + public final static int INVALID_ETA_HOUR = 24; + public final static int INVALID_ETA_MIN = 60; + public final static int INVALID_ALTITUDE = 4095; + public final static int UNSPECIFIED_AID_TYPE = 0; + public final static double INVALID_COG = 360.0; + public final static double INVALID_SOG = 1023.0; + public final static double INVALID_LAT = 91.0; + public final static double INVALID_LON = 181.0; + public final static double INVALID_ROT = 128.0; + public final static double INVALID_DRAUGHT = 0.0; + + public enum AisObjType { + AIS_VESSEL, + AIS_VESSEL_SPORT, + AIS_VESSEL_FAST, + AIS_VESSEL_PASSENGER, + AIS_VESSEL_FREIGHT, + AIS_VESSEL_COMMERCIAL, + AIS_LANDSTATION, + AIS_AIRPLANE, + AIS_SART, + AIS_ATON, // aids to navigation + AIS_ATON_VIRTUAL, + AIS_INVALID + } + public static final Map COUNTRY_CODES = Map.ofEntries( + new AbstractMap.SimpleEntry(201, "Albania"), + new AbstractMap.SimpleEntry(202, "Andorra"), + new AbstractMap.SimpleEntry(203, "Austria"), + new AbstractMap.SimpleEntry(204, "Portugal"), + new AbstractMap.SimpleEntry(205, "Belgium"), + new AbstractMap.SimpleEntry(206, "Belarus"), + new AbstractMap.SimpleEntry(207, "Bulgaria"), + new AbstractMap.SimpleEntry(208, "Vatican"), + new AbstractMap.SimpleEntry(209, "Cyprus"), + new AbstractMap.SimpleEntry(210, "Cyprus"), + new AbstractMap.SimpleEntry(211, "Germany"), + new AbstractMap.SimpleEntry(212, "Cyprus"), + new AbstractMap.SimpleEntry(213, "Georgia"), + new AbstractMap.SimpleEntry(214, "Moldova"), + new AbstractMap.SimpleEntry(215, "Malta"), + new AbstractMap.SimpleEntry(216, "Armenia"), + new AbstractMap.SimpleEntry(218, "Germany"), + new AbstractMap.SimpleEntry(219, "Denmark"), + new AbstractMap.SimpleEntry(220, "Denmark"), + new AbstractMap.SimpleEntry(224, "Spain"), + new AbstractMap.SimpleEntry(225, "Spain"), + new AbstractMap.SimpleEntry(226, "France"), + new AbstractMap.SimpleEntry(227, "France"), + new AbstractMap.SimpleEntry(228, "France"), + new AbstractMap.SimpleEntry(229, "Malta"), + new AbstractMap.SimpleEntry(230, "Finland"), + new AbstractMap.SimpleEntry(231, "Faroe Is"), + new AbstractMap.SimpleEntry(232, "United Kingdom"), + new AbstractMap.SimpleEntry(233, "United Kingdom"), + new AbstractMap.SimpleEntry(234, "United Kingdom"), + new AbstractMap.SimpleEntry(235, "United Kingdom"), + new AbstractMap.SimpleEntry(236, "Gibraltar"), + new AbstractMap.SimpleEntry(237, "Greece"), + new AbstractMap.SimpleEntry(238, "Croatia"), + new AbstractMap.SimpleEntry(239, "Greece"), + new AbstractMap.SimpleEntry(240, "Greece"), + new AbstractMap.SimpleEntry(241, "Greece"), + new AbstractMap.SimpleEntry(242, "Morocco"), + new AbstractMap.SimpleEntry(243, "Hungary"), + new AbstractMap.SimpleEntry(244, "Netherlands"), + new AbstractMap.SimpleEntry(245, "Netherlands"), + new AbstractMap.SimpleEntry(246, "Netherlands"), + new AbstractMap.SimpleEntry(247, "Italy"), + new AbstractMap.SimpleEntry(248, "Malta"), + new AbstractMap.SimpleEntry(249, "Malta"), + new AbstractMap.SimpleEntry(250, "Ireland"), + new AbstractMap.SimpleEntry(251, "Iceland"), + new AbstractMap.SimpleEntry(252, "Liechtenstein"), + new AbstractMap.SimpleEntry(253, "Luxembourg"), + new AbstractMap.SimpleEntry(254, "Monaco"), + new AbstractMap.SimpleEntry(255, "Portugal"), + new AbstractMap.SimpleEntry(256, "Malta"), + new AbstractMap.SimpleEntry(257, "Norway"), + new AbstractMap.SimpleEntry(258, "Norway"), + new AbstractMap.SimpleEntry(259, "Norway"), + new AbstractMap.SimpleEntry(261, "Poland"), + new AbstractMap.SimpleEntry(262, "Montenegro"), + new AbstractMap.SimpleEntry(263, "Portugal"), + new AbstractMap.SimpleEntry(264, "Romania"), + new AbstractMap.SimpleEntry(265, "Sweden"), + new AbstractMap.SimpleEntry(266, "Sweden"), + new AbstractMap.SimpleEntry(267, "Slovakia"), + new AbstractMap.SimpleEntry(268, "San Marino"), + new AbstractMap.SimpleEntry(269, "Switzerland"), + new AbstractMap.SimpleEntry(270, "Czech Republic"), + new AbstractMap.SimpleEntry(271, "Turkey"), + new AbstractMap.SimpleEntry(272, "Ukraine"), + new AbstractMap.SimpleEntry(273, "Russia"), + new AbstractMap.SimpleEntry(274, "FYR Macedonia"), + new AbstractMap.SimpleEntry(275, "Latvia"), + new AbstractMap.SimpleEntry(276, "Estonia"), + new AbstractMap.SimpleEntry(277, "Lithuania"), + new AbstractMap.SimpleEntry(278, "Slovenia"), + new AbstractMap.SimpleEntry(279, "Serbia"), + new AbstractMap.SimpleEntry(301, "Anguilla"), + new AbstractMap.SimpleEntry(303, "USA"), + new AbstractMap.SimpleEntry(304, "Antigua Barbuda"), + new AbstractMap.SimpleEntry(305, "Antigua Barbuda"), + new AbstractMap.SimpleEntry(306, "Curacao"), + new AbstractMap.SimpleEntry(307, "Aruba"), + new AbstractMap.SimpleEntry(308, "Bahamas"), + new AbstractMap.SimpleEntry(309, "Bahamas"), + new AbstractMap.SimpleEntry(310, "Bermuda"), + new AbstractMap.SimpleEntry(311, "Bahamas"), + new AbstractMap.SimpleEntry(312, "Belize"), + new AbstractMap.SimpleEntry(314, "Barbados"), + new AbstractMap.SimpleEntry(316, "Canada"), + new AbstractMap.SimpleEntry(319, "Cayman Is"), + new AbstractMap.SimpleEntry(321, "Costa Rica"), + new AbstractMap.SimpleEntry(323, "Cuba"), + new AbstractMap.SimpleEntry(325, "Dominica"), + new AbstractMap.SimpleEntry(327, "Dominican Rep"), + new AbstractMap.SimpleEntry(329, "Guadeloupe"), + new AbstractMap.SimpleEntry(330, "Grenada"), + new AbstractMap.SimpleEntry(331, "Greenland"), + new AbstractMap.SimpleEntry(332, "Guatemala"), + new AbstractMap.SimpleEntry(334, "Honduras"), + new AbstractMap.SimpleEntry(336, "Haiti"), + new AbstractMap.SimpleEntry(338, "USA"), + new AbstractMap.SimpleEntry(339, "Jamaica"), + new AbstractMap.SimpleEntry(341, "St Kitts Nevis"), + new AbstractMap.SimpleEntry(343, "St Lucia"), + new AbstractMap.SimpleEntry(345, "Mexico"), + new AbstractMap.SimpleEntry(347, "Martinique"), + new AbstractMap.SimpleEntry(348, "Montserrat"), + new AbstractMap.SimpleEntry(350, "Nicaragua"), + new AbstractMap.SimpleEntry(351, "Panama"), + new AbstractMap.SimpleEntry(352, "Panama"), + new AbstractMap.SimpleEntry(353, "Panama"), + new AbstractMap.SimpleEntry(354, "Panama"), + new AbstractMap.SimpleEntry(355, "Panama"), + new AbstractMap.SimpleEntry(356, "Panama"), + new AbstractMap.SimpleEntry(357, "Panama"), + new AbstractMap.SimpleEntry(358, "Puerto Rico"), + new AbstractMap.SimpleEntry(359, "El Salvador"), + new AbstractMap.SimpleEntry(361, "St Pierre Miquelon"), + new AbstractMap.SimpleEntry(362, "Trinidad Tobago"), + new AbstractMap.SimpleEntry(364, "Turks Caicos Is"), + new AbstractMap.SimpleEntry(366, "USA"), + new AbstractMap.SimpleEntry(367, "USA"), + new AbstractMap.SimpleEntry(368, "USA"), + new AbstractMap.SimpleEntry(369, "USA"), + new AbstractMap.SimpleEntry(370, "Panama"), + new AbstractMap.SimpleEntry(371, "Panama"), + new AbstractMap.SimpleEntry(372, "Panama"), + new AbstractMap.SimpleEntry(373, "Panama"), + new AbstractMap.SimpleEntry(374, "Panama"), + new AbstractMap.SimpleEntry(375, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry(376, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry(377, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry(378, "British Virgin Is"), + new AbstractMap.SimpleEntry(379, "US Virgin Is"), + new AbstractMap.SimpleEntry(401, "Afghanistan"), + new AbstractMap.SimpleEntry(403, "Saudi Arabia"), + new AbstractMap.SimpleEntry(405, "Bangladesh"), + new AbstractMap.SimpleEntry(408, "Bahrain"), + new AbstractMap.SimpleEntry(410, "Bhutan"), + new AbstractMap.SimpleEntry(412, "China"), + new AbstractMap.SimpleEntry(413, "China"), + new AbstractMap.SimpleEntry(414, "China"), + new AbstractMap.SimpleEntry(416, "Taiwan"), + new AbstractMap.SimpleEntry(417, "Sri Lanka"), + new AbstractMap.SimpleEntry(419, "India"), + new AbstractMap.SimpleEntry(422, "Iran"), + new AbstractMap.SimpleEntry(423, "Azerbaijan"), + new AbstractMap.SimpleEntry(425, "Iraq"), + new AbstractMap.SimpleEntry(428, "Israel"), + new AbstractMap.SimpleEntry(431, "Japan"), + new AbstractMap.SimpleEntry(432, "Japan"), + new AbstractMap.SimpleEntry(434, "Turkmenistan"), + new AbstractMap.SimpleEntry(436, "Kazakhstan"), + new AbstractMap.SimpleEntry(437, "Uzbekistan"), + new AbstractMap.SimpleEntry(438, "Jordan"), + new AbstractMap.SimpleEntry(440, "Korea"), + new AbstractMap.SimpleEntry(441, "Korea"), + new AbstractMap.SimpleEntry(443, "Palestine"), + new AbstractMap.SimpleEntry(445, "DPR Korea"), + new AbstractMap.SimpleEntry(447, "Kuwait"), + new AbstractMap.SimpleEntry(450, "Lebanon"), + new AbstractMap.SimpleEntry(451, "Kyrgyz Republic"), + new AbstractMap.SimpleEntry(453, "Macao"), + new AbstractMap.SimpleEntry(455, "Maldives"), + new AbstractMap.SimpleEntry(457, "Mongolia"), + new AbstractMap.SimpleEntry(459, "Nepal"), + new AbstractMap.SimpleEntry(461, "Oman"), + new AbstractMap.SimpleEntry(463, "Pakistan"), + new AbstractMap.SimpleEntry(466, "Qatar"), + new AbstractMap.SimpleEntry(468, "Syria"), + new AbstractMap.SimpleEntry(470, "UAE"), + new AbstractMap.SimpleEntry(471, "UAE"), + new AbstractMap.SimpleEntry(472, "Tajikistan"), + new AbstractMap.SimpleEntry(473, "Yemen"), + new AbstractMap.SimpleEntry(475, "Yemen"), + new AbstractMap.SimpleEntry(477, "Hong Kong"), + new AbstractMap.SimpleEntry(478, "Bosnia and Herzegovina"), + new AbstractMap.SimpleEntry(501, "Antarctica"), + new AbstractMap.SimpleEntry(503, "Australia"), + new AbstractMap.SimpleEntry(506, "Myanmar"), + new AbstractMap.SimpleEntry(508, "Brunei"), + new AbstractMap.SimpleEntry(510, "Micronesia"), + new AbstractMap.SimpleEntry(511, "Palau"), + new AbstractMap.SimpleEntry(512, "New Zealand"), + new AbstractMap.SimpleEntry(514, "Cambodia"), + new AbstractMap.SimpleEntry(515, "Cambodia"), + new AbstractMap.SimpleEntry(516, "Christmas Is"), + new AbstractMap.SimpleEntry(518, "Cook Is"), + new AbstractMap.SimpleEntry(520, "Fiji"), + new AbstractMap.SimpleEntry(523, "Cocos Is"), + new AbstractMap.SimpleEntry(525, "Indonesia"), + new AbstractMap.SimpleEntry(529, "Kiribati"), + new AbstractMap.SimpleEntry(531, "Laos"), + new AbstractMap.SimpleEntry(533, "Malaysia"), + new AbstractMap.SimpleEntry(536, "N Mariana Is"), + new AbstractMap.SimpleEntry(538, "Marshall Is"), + new AbstractMap.SimpleEntry(540, "New Caledonia"), + new AbstractMap.SimpleEntry(542, "Niue"), + new AbstractMap.SimpleEntry(544, "Nauru"), + new AbstractMap.SimpleEntry(546, "French Polynesia"), + new AbstractMap.SimpleEntry(548, "Philippines"), + new AbstractMap.SimpleEntry(553, "Papua New Guinea"), + new AbstractMap.SimpleEntry(555, "Pitcairn Is"), + new AbstractMap.SimpleEntry(557, "Solomon Is"), + new AbstractMap.SimpleEntry(559, "American Samoa"), + new AbstractMap.SimpleEntry(561, "Samoa"), + new AbstractMap.SimpleEntry(563, "Singapore"), + new AbstractMap.SimpleEntry(564, "Singapore"), + new AbstractMap.SimpleEntry(565, "Singapore"), + new AbstractMap.SimpleEntry(566, "Singapore"), + new AbstractMap.SimpleEntry(567, "Thailand"), + new AbstractMap.SimpleEntry(570, "Tonga"), + new AbstractMap.SimpleEntry(572, "Tuvalu"), + new AbstractMap.SimpleEntry(574, "Vietnam"), + new AbstractMap.SimpleEntry(576, "Vanuatu"), + new AbstractMap.SimpleEntry(577, "Vanuatu"), + new AbstractMap.SimpleEntry(578, "Wallis Futuna Is"), + new AbstractMap.SimpleEntry(601, "South Africa"), + new AbstractMap.SimpleEntry(603, "Angola"), + new AbstractMap.SimpleEntry(605, "Algeria"), + new AbstractMap.SimpleEntry(607, "St Paul Amsterdam Is"), + new AbstractMap.SimpleEntry(608, "Ascension Is"), + new AbstractMap.SimpleEntry(609, "Burundi"), + new AbstractMap.SimpleEntry(610, "Benin"), + new AbstractMap.SimpleEntry(611, "Botswana"), + new AbstractMap.SimpleEntry(612, "Cen Afr Rep"), + new AbstractMap.SimpleEntry(613, "Cameroon"), + new AbstractMap.SimpleEntry(615, "Congo"), + new AbstractMap.SimpleEntry(616, "Comoros"), + new AbstractMap.SimpleEntry(617, "Cape Verde"), + new AbstractMap.SimpleEntry(618, "Antarctica"), + new AbstractMap.SimpleEntry(619, "Ivory Coast"), + new AbstractMap.SimpleEntry(620, "Comoros"), + new AbstractMap.SimpleEntry(621, "Djibouti"), + new AbstractMap.SimpleEntry(622, "Egypt"), + new AbstractMap.SimpleEntry(624, "Ethiopia"), + new AbstractMap.SimpleEntry(625, "Eritrea"), + new AbstractMap.SimpleEntry(626, "Gabon"), + new AbstractMap.SimpleEntry(627, "Ghana"), + new AbstractMap.SimpleEntry(629, "Gambia"), + new AbstractMap.SimpleEntry(630, "Guinea-Bissau"), + new AbstractMap.SimpleEntry(631, "Equ. Guinea"), + new AbstractMap.SimpleEntry(632, "Guinea"), + new AbstractMap.SimpleEntry(633, "Burkina Faso"), + new AbstractMap.SimpleEntry(634, "Kenya"), + new AbstractMap.SimpleEntry(635, "Antarctica"), + new AbstractMap.SimpleEntry(636, "Liberia"), + new AbstractMap.SimpleEntry(637, "Liberia"), + new AbstractMap.SimpleEntry(642, "Libya"), + new AbstractMap.SimpleEntry(644, "Lesotho"), + new AbstractMap.SimpleEntry(645, "Mauritius"), + new AbstractMap.SimpleEntry(647, "Madagascar"), + new AbstractMap.SimpleEntry(649, "Mali"), + new AbstractMap.SimpleEntry(650, "Mozambique"), + new AbstractMap.SimpleEntry(654, "Mauritania"), + new AbstractMap.SimpleEntry(655, "Malawi"), + new AbstractMap.SimpleEntry(656, "Niger"), + new AbstractMap.SimpleEntry(657, "Nigeria"), + new AbstractMap.SimpleEntry(659, "Namibia"), + new AbstractMap.SimpleEntry(660, "Reunion"), + new AbstractMap.SimpleEntry(661, "Rwanda"), + new AbstractMap.SimpleEntry(662, "Sudan"), + new AbstractMap.SimpleEntry(663, "Senegal"), + new AbstractMap.SimpleEntry(664, "Seychelles"), + new AbstractMap.SimpleEntry(665, "St Helena"), + new AbstractMap.SimpleEntry(666, "Somalia"), + new AbstractMap.SimpleEntry(667, "Sierra Leone"), + new AbstractMap.SimpleEntry(668, "Sao Tome Principe"), + new AbstractMap.SimpleEntry(669, "Swaziland"), + new AbstractMap.SimpleEntry(670, "Chad"), + new AbstractMap.SimpleEntry(671, "Togo"), + new AbstractMap.SimpleEntry(672, "Tunisia"), + new AbstractMap.SimpleEntry(674, "Tanzania"), + new AbstractMap.SimpleEntry(675, "Uganda"), + new AbstractMap.SimpleEntry(676, "DR Congo"), + new AbstractMap.SimpleEntry(677, "Tanzania"), + new AbstractMap.SimpleEntry(678, "Zambia"), + new AbstractMap.SimpleEntry(679, "Zimbabwe"), + new AbstractMap.SimpleEntry(701, "Argentina"), + new AbstractMap.SimpleEntry(710, "Brazil"), + new AbstractMap.SimpleEntry(720, "Bolivia"), + new AbstractMap.SimpleEntry(725, "Chile"), + new AbstractMap.SimpleEntry(730, "Colombia"), + new AbstractMap.SimpleEntry(735, "Ecuador"), + new AbstractMap.SimpleEntry(740, "UK"), + new AbstractMap.SimpleEntry(745, "Guiana"), + new AbstractMap.SimpleEntry(750, "Guyana"), + new AbstractMap.SimpleEntry(755, "Paraguay"), + new AbstractMap.SimpleEntry(760, "Peru"), + new AbstractMap.SimpleEntry(765, "Suriname"), + new AbstractMap.SimpleEntry(770, "Uruguay"), + new AbstractMap.SimpleEntry(775, "Venezuela") + ); +} + diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java new file mode 100644 index 00000000000..b50ae77535b --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -0,0 +1,163 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.LocationConvert; +import net.osmand.data.LatLon; +import net.osmand.data.PointDescription; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.mapcontextmenu.MenuBuilder; +import net.osmand.plus.mapcontextmenu.MenuController; + +import java.util.Iterator; +import java.util.SortedSet; + +public class AisObjectMenuController extends MenuController { + private AisObject aisObject; + public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointDescription pointDescription, + AisObject aisObject) { + super(new MenuBuilder(mapActivity), pointDescription, mapActivity); + this.aisObject = aisObject; + builder.setShowTitleIfTruncated(false); + builder.setShowNearestPoi(false); + builder.setShowOnlinePhotos(false); + builder.setShowNearestWiki(false); + // TODO: show an icon in the menu + } + + private void addMenuItem(@NonNull String type, @Nullable String value) { + if (value != null) { + if (!value.isEmpty()) { + addPlainMenuItem(0, value, type, false, false, null); + } + } + } + private void addMenuItem(@NonNull String type, @Nullable String value, + @Nullable SortedSet msgTypes, Integer selection[]) { + if (msgTypes != null) { + for (Integer i : selection) { + if (msgTypes.contains(i)) { + addMenuItem(type, value); + break; + } + } + } + } + private void addMenuItemDimension() { + if (((aisObject.getDimensionToBow() != AisObjectConstants.INVALID_DIMENSION) || + (aisObject.getDimensionToStern() != AisObjectConstants.INVALID_DIMENSION)) && + ((aisObject.getDimensionToPort() != AisObjectConstants.INVALID_DIMENSION) || + (aisObject.getDimensionToStarboard() != AisObjectConstants.INVALID_DIMENSION))) { + addMenuItem("Dimension", + Integer.toString(aisObject.getDimensionToBow() + aisObject.getDimensionToStern()) + + "m x " + + Integer.toString(aisObject.getDimensionToPort() + aisObject.getDimensionToStarboard()) + + "m"); + } + } + + @Override + public void addPlainMenuItems(String typeStr, PointDescription pointDescription, LatLon latLon) { + SortedSet msgTypes = aisObject.getMsgTypes(); + Iterator iter = msgTypes.iterator(); + String msgTypesString = ""; + LatLon position = aisObject.getPosition(); + long lastUpdate = (System.currentTimeMillis() - aisObject.getLastUpdate()) / 1000; + + addMenuItem("MMSI", Integer.toString(aisObject.getMmsi())); + if (position != null) { + addMenuItem("Location", + LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); + } + if (msgTypes.contains(21)) { // ATON (aid to navigation) + addMenuItem("ATON Type", aisObject.getAidTypeString()); + addMenuItemDimension(); + } else if (msgTypes.contains(9)) { // SAR aircraft + addMenuItem("Object Type", "SAR Aircraft"); + if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { + addMenuItem("COG", String.valueOf(aisObject.getCog())); + } + if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { + addMenuItem("SOG", String.valueOf(aisObject.getSog())); + } + if (aisObject.getAltitude() != AisObjectConstants.INVALID_ALTITUDE) { + addMenuItem("Altitude", String.valueOf(aisObject.getAltitude())); + } + } else { + addMenuItem("Callsign", aisObject.getCallSign()); + if (aisObject.getImo() != 0 ) { + addMenuItem("IMO", Integer.toString(aisObject.getImo()), msgTypes, new Integer[]{5}); + } + addMenuItem("Shipname", aisObject.getShipName()); + addMenuItem("Shiptype", aisObject.getShipTypeString(), msgTypes, new Integer[]{5, 19, 24}); + if (aisObject.getNavStatus() != AisObjectConstants.INVALID_NAV_STATUS) { + addMenuItem("Navigation Status", aisObject.getNavStatusString()); + } + if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { + addMenuItem("COG", String.valueOf(aisObject.getCog())); + } + if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { + addMenuItem("SOG", String.valueOf(aisObject.getSog()) + " kt"); + } + if (aisObject.getHeading() != AisObjectConstants.INVALID_HEADING) { + addMenuItem("Heading", String.valueOf(aisObject.getHeading())); + } + if (aisObject.getRot() != AisObjectConstants.INVALID_ROT) { + addMenuItem("Rate of Turn", String.valueOf(aisObject.getRot())); + } + addMenuItemDimension(); + if (aisObject.getDraught() != AisObjectConstants.INVALID_DRAUGHT) { + addMenuItem("Draught", String.valueOf(aisObject.getDraught()) + " m"); + } + addMenuItem("Destination", aisObject.getDestination()); + if ((aisObject.getEtaDay() != AisObjectConstants.INVALID_ETA) && + (aisObject.getEtaHour() != AisObjectConstants.INVALID_ETA_HOUR) && + (aisObject.getEtaMin() != AisObjectConstants.INVALID_ETA_MIN) && + (aisObject.getEtaMon() != AisObjectConstants.INVALID_ETA)) { + String eta = new String(aisObject.getEtaDay() + "." + + aisObject.getEtaMon() + ". " + aisObject.getEtaHour() + ":" + + aisObject.getEtaMin()); + addMenuItem("ETA", eta); + // TODO add prepending "0", if needed + } + } + if (lastUpdate > 60) { + addMenuItem("Last Update", (lastUpdate / 60) + + " min " + (lastUpdate % 60) + " sec"); + } else { + addMenuItem("Last Update", lastUpdate + " sec"); + } + boolean hasNext = iter.hasNext(); + while (hasNext) { + msgTypesString = msgTypesString.concat(Integer.toString(iter.next())); + hasNext = iter.hasNext(); + if (hasNext) { msgTypesString = msgTypesString.concat(", "); } + } + addMenuItem("Message Type(s)", msgTypesString); + } + + @Override + protected void setObject(Object object) { + if (object instanceof AisObject) { + this.aisObject = (AisObject) object; + } + } + + @Override + protected Object getObject() { + return aisObject; + } + @Override + public CharSequence getAdditionalInfoStr() { return "Country: " + aisObject.getCountryCode(); } + + @NonNull + @Override + public String getTypeStr() { return "AIS object"; } + + @Override + public boolean needStreetName() { return false; } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java new file mode 100644 index 00000000000..c1478121a32 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -0,0 +1,252 @@ +package net.osmand.plus.plugins.aistracker; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.util.Log; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.core.android.MapRendererView; +import net.osmand.core.jni.PointI; +import net.osmand.data.LatLon; +import net.osmand.data.PointDescription; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.utils.NativeUtilities; +import net.osmand.plus.views.layers.ContextMenuLayer; +import net.osmand.plus.views.layers.base.OsmandMapLayer; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +public class AisTrackerLayer extends OsmandMapLayer implements ContextMenuLayer.IContextMenuProvider { + private static final int START_ZOOM = 10; + private final AisTrackerPlugin plugin; + private Map aisObjectList; + private final int aisObjectListCounterMax = 100; + private final Context context; + private Paint bitmapPaint; + private Timer timer; + private TimerTask taskCheckAisObjectList; + private AisMessageListener listener; + public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugin) { + super(context); + this.plugin = plugin; + this.context = context; + this.listener = null; + + this.aisObjectList = new HashMap<>(); + this.bitmapPaint = new Paint(); + this.bitmapPaint.setAntiAlias(true); + this.bitmapPaint.setFilterBitmap(true); + this.bitmapPaint.setStrokeWidth(4); + this.bitmapPaint.setColor(Color.DKGRAY); + + initTimer(); + startNetworkListener(); + + //initTestObjects(); // for test purposes: + } + + private void initTestObjects() { + AisObject ais1 = new AisObject(12345, 1, 20, 120, 120.0, 4.4, + 37.42421d, -122.08381d, 30, 0,0,0,0); + AisObject ais2 = new AisObject(34567, 3, 20, 320, 320.0, 0.4, + 37.42521d, -122.08481d, 36, 0,0,0,0); + AisObject ais3 = new AisObject(34568, 1, 20, 320, 320.0, 0.4, + 50.738d, 7.099d, 70, 20,40,10,0); + AisObject ais4 = new AisObject(12341, 3, 20, 20, 20.0, 0.4, + 50.737d, 7.098d, 60, 0,0,0,0); + + updateAisObjectList(ais1); + updateAisObjectList(ais2); + removeOldestAisObjectListEntry(); + updateAisObjectList(ais2); + updateAisObjectList(ais3); + updateAisObjectList(ais4); + removeLostAisObjects(); + } + private void initTimer() { + this.taskCheckAisObjectList = new TimerTask() { + @Override + public void run() { + Log.d("AisTrackerLayer", "timer task taskCheckAisObjectList running"); + removeLostAisObjects(); + } + }; + this.timer = new Timer(); + timer.schedule(taskCheckAisObjectList, 20000, 30000); + } + private void startNetworkListener() { + int proto = plugin.AIS_NMEA_PROTOCOL.get(); + if (proto == AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP) { + this.listener = new AisMessageListener(plugin.AIS_NMEA_UDP_PORT.get(), this); + } else if (proto == AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP) { + this.listener = new AisMessageListener(plugin.AIS_NMEA_IP_ADDRESS.get(), plugin.AIS_NMEA_TCP_PORT.get(), this); + } + } + private void stopNetworkListener() { + if (this.listener != null) { + this.listener.stopListener(); + this.listener = null; + } + } + public void restartNetworkListener() { + stopNetworkListener(); + startNetworkListener(); + } + public void cleanup() { + if (this.timer != null) { + this.timer.cancel(); + this.timer.purge(); + this.timer = null; + } + if (this.aisObjectList != null) { + this.aisObjectList.clear(); + this.aisObjectList = null; + } + stopNetworkListener(); + } + private void removeLostAisObjects() { + for (Iterator> iterator = aisObjectList.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + if (entry.getValue().checkObjectAge()) { + Log.d("AisTrackerLayer", "remove AIS object with MMSI " + entry.getValue().getMmsi()); + iterator.remove(); + } + } + // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge()); + } + private void removeOldestAisObjectListEntry() { + Log.d("AisTrackerLayer", "removeOldestAisObjectListEntry() called"); + long oldestTimeStamp = System.currentTimeMillis(); + AisObject oldest = null; + for (AisObject ais : aisObjectList.values()) { + long timeStamp = ais.getLastUpdate(); + if (timeStamp <= oldestTimeStamp) { + oldestTimeStamp = timeStamp; + oldest = ais; + } + } + if (oldest != null) { + Log.d("AisTrackerLayer", "remove AIS object with MMSI " + oldest.getMmsi()); + aisObjectList.remove(oldest.getMmsi(), oldest); + } + } + + /* add new AIS object to list, or (if already exist) update its value */ + public void updateAisObjectList(@NonNull AisObject ais) { + int mmsi = ais.getMmsi(); + AisObject obj = aisObjectList.get(mmsi); + if (obj == null) { + Log.d("AisTrackerLayer", "add AIS object with MMSI " + ais.getMmsi()); + aisObjectList.put(mmsi, new AisObject(ais)); + if (aisObjectList.size() >= this.aisObjectListCounterMax) { + this.removeOldestAisObjectListEntry(); + } + } else { + obj.set(ais); + } + } + + @Nullable + public Bitmap getBitmap(@DrawableRes int drawable) { return getScaledBitmap(drawable); } + + @NonNull + public OsmandApplication getApplication() { + return (OsmandApplication) context.getApplicationContext(); + } + public boolean isLocationVisible(RotatedTileBox tileBox, LatLon coordinates) { + //noinspection SimplifiableIfStatement + if (tileBox == null || coordinates == null) { + return false; + } + return tileBox.containsLatLon(coordinates); + } + + @Override + public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + for (AisObject ais : aisObjectList.values()) { + if (isLocationVisible(tileBox, ais.getPosition())) { + ais.draw(this, bitmapPaint, canvas, tileBox); + } + } + } + + @Override + public boolean drawInScreenPixels() { + return true; + } + + @Override + public void collectObjectsFromPoint(PointF point, RotatedTileBox tileBox, List objects, + boolean unknownLocation, boolean excludeUntouchableObjects) { + if (tileBox.getZoom() >= START_ZOOM) { + getAisObjectsFromPoint(point, tileBox, objects); + } + } + public void getAisObjectsFromPoint(PointF point, RotatedTileBox tileBox, List aisList) { + if (aisObjectList.isEmpty()) { + return; + } + + MapRendererView mapRenderer = getMapRenderer(); + float radius = getScaledTouchRadius(getApplication(), tileBox.getDefaultRadiusPoi()) * TOUCH_RADIUS_MULTIPLIER; + List touchPolygon31 = null; + if (mapRenderer != null) { + touchPolygon31 = NativeUtilities.getPolygon31FromPixelAndRadius(mapRenderer, point, radius); + if (touchPolygon31 == null) { + return; + } + } + + for (AisObject ais : aisObjectList.values()) { + LatLon pos = ais.getPosition(); + if (pos != null) { + double lat = pos.getLatitude(); + double lon = pos.getLongitude(); + + boolean add = mapRenderer != null + ? NativeUtilities.isPointInsidePolygon(lat, lon, touchPolygon31) + : tileBox.isLatLonNearPixel(lat, lon, point.x, point.y, radius); + if (add) { + aisList.add(ais); + } + } + } + } + + @Override + public LatLon getObjectLocation(Object o) { + if (o instanceof AisObject) { + LatLon pos = ((AisObject) o).getPosition(); + if (pos != null) { + return new LatLon(pos.getLatitude(), pos.getLongitude()); + } + } + return null; + } + + @Override + public PointDescription getObjectName(Object o) { + if (o instanceof AisObject) { + AisObject ais = ((AisObject) o); + if (ais.getShipName() != null) { + return new PointDescription("AIS object", ais.getShipName()); + } + return new PointDescription("AIS object", + "AIS object with MMSI " + ais.getMmsi()); + } + return null; + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java new file mode 100644 index 00000000000..d693516614e --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -0,0 +1,152 @@ +package net.osmand.plus.plugins.aistracker; + +//import static net.osmand.aidlapi.OsmAndCustomizationConstants.PLUGIN_AISTRACKER; +import static net.osmand.plus.settings.fragments.SettingsScreenType.AIS_SETTINGS; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.plugins.OsmandPlugin; +import net.osmand.plus.render.RendererRegistry; +import net.osmand.plus.settings.backend.ApplicationMode; +import net.osmand.plus.settings.backend.preferences.CommonPreference; +import net.osmand.plus.settings.fragments.SettingsScreenType; +import net.osmand.plus.views.OsmandMapTileView; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/* +* This plugin receives AIS positions and other AIS data via network (NMEA protocol) +* from an AIS receiver/decoder and displays symbols at the map at the vessel position +*/ +public class AisTrackerPlugin extends OsmandPlugin { + + private AisTrackerLayer aisTrackerLayer = null; + + public static final String COMPONENT = "net.osmand.aistrackerPlugin"; + public final CommonPreference AIS_NMEA_PROTOCOL; + public static final int AIS_NMEA_PROTOCOL_UDP = 0; + public static final int AIS_NMEA_PROTOCOL_TCP = 1; + public final CommonPreference AIS_NMEA_IP_ADDRESS; + private static final String AIS_NMEA_DEFAULT_IP = "192.168.200.16"; + public final CommonPreference AIS_NMEA_TCP_PORT; + public static final Integer AIS_NMEA_DEFAULT_TCP_PORT = 4001; + public final CommonPreference AIS_NMEA_UDP_PORT; + public static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; + + public AisTrackerPlugin(OsmandApplication app) { + super(app); + /* "ais_nmea_protocol" etc. is a reference to the content of ais_settings.xml */ + AIS_NMEA_PROTOCOL = registerIntPreference("ais_nmea_protocol", AIS_NMEA_PROTOCOL_UDP); + AIS_NMEA_IP_ADDRESS = registerStringPreference("ais_address_nmea_server", AIS_NMEA_DEFAULT_IP); + AIS_NMEA_TCP_PORT = registerIntPreference("ais_port_nmea_server", AIS_NMEA_DEFAULT_TCP_PORT); + AIS_NMEA_UDP_PORT = registerIntPreference("ais_port_nmea_local", AIS_NMEA_DEFAULT_UDP_PORT); + + Log.d("AisTrackerPlugin", "constructor"); + } + + @Override + public boolean isMarketPlugin() { + return true; + } + + @Override + public String getComponentId1() { + return COMPONENT; + } + + @Override + public CharSequence getDescription(boolean linksEnabled) { + return app.getString(R.string.plugin_aistracker_description); + } + + @Override + public String getName() { + return app.getString(R.string.plugin_aistracker_name); + } + + @Override + //public int getLogoResourceId() { return R.drawable.ic_plugin_nautical_map; } + public int getLogoResourceId() { + return R.drawable.mm_sport_sailing; + } + + @Override + public Drawable getAssetResourceImage() { + return app.getUIUtilities().getIcon(R.drawable.ais_map); + } + + @Override + public List getAddedAppModes() { + //return Collections.singletonList(ApplicationMode.BOAT); + return Arrays.asList(ApplicationMode.BOAT, ApplicationMode.DEFAULT); + } + + @Override + public List getRendererNames() { + return Collections.singletonList(RendererRegistry.NAUTICAL_RENDER); + } + + @Override + public String getId() { + return "osmand.aistracker"; + } + + @Nullable + @Override + public SettingsScreenType getSettingsScreenType() { + return AIS_SETTINGS; + } + + @Override + public String getPrefsDescription() { + return app.getString(R.string.ais_address_settings_description); + } + + @Override + public void updateLayers(@NonNull Context context, @Nullable MapActivity mapActivity) { + OsmandMapTileView mapView = app.getOsmandMap().getMapView(); + if (isActive()) { + if (aisTrackerLayer == null) { + Log.d("AisTrackerPlugin", "call registerLayers()"); + registerLayers(context, mapActivity); + } + if (!mapView.getLayers().contains(aisTrackerLayer)) { + mapView.addLayer(aisTrackerLayer, 3.5f); + } + } else { + if (aisTrackerLayer != null) { + mapView.removeLayer(aisTrackerLayer); + aisTrackerLayer.cleanup(); + aisTrackerLayer = null; + mapView.refreshMap(); + } + } + } + + @Override + public void registerLayers(@NonNull Context context, @Nullable MapActivity mapActivity) { + if (aisTrackerLayer == null) { + Log.d("AisTrackerPlugin", "new AisTrackerLayer"); + aisTrackerLayer = new AisTrackerLayer(context, this); + app.getOsmandMap().getMapView().addLayer(aisTrackerLayer, 3.5f); + } else { + Log.d("AisTrackerPlugin", "AisTrackerLayer already exists"); + OsmandMapTileView mapView = app.getOsmandMap().getMapView(); + if (!mapView.getLayers().contains(aisTrackerLayer)) { + mapView.addLayer(aisTrackerLayer, 3.5f); + } + } + } + + public AisTrackerLayer getLayer() { return aisTrackerLayer; } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java new file mode 100644 index 00000000000..4c4ae571cb2 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -0,0 +1,152 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP; + +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; + +import net.osmand.plus.R; +import net.osmand.plus.plugins.PluginsHelper; +import net.osmand.plus.settings.fragments.BaseSettingsFragment; +import net.osmand.plus.settings.preferences.EditTextPreferenceEx; +import net.osmand.plus.settings.preferences.ListPreferenceEx; + +public class AisTrackerSettingsFragment extends BaseSettingsFragment { + private AisTrackerPlugin plugin; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + plugin = PluginsHelper.getPlugin(AisTrackerPlugin.class); + } + + @Override + protected void setupPreferences() { + int currentProtocol; + currentProtocol = setupProtocol(); + setupIpAddress(currentProtocol); + setupTcpPort(currentProtocol); + setupUdpPort(currentProtocol); + } + + private int setupProtocol() { + Integer[] entryValues = {AIS_NMEA_PROTOCOL_UDP, AIS_NMEA_PROTOCOL_TCP}; + String[] entries = {"UDP", "TCP"}; + + ListPreferenceEx aisNmeaProtocol = findPreference(plugin.AIS_NMEA_PROTOCOL.getId()); + aisNmeaProtocol.setEntries(entries); + aisNmeaProtocol.setEntryValues(entryValues); + aisNmeaProtocol.setDescription(R.string.ais_nmea_protocol_description); + return (int)aisNmeaProtocol.getValue(); + } + + private void setupIpAddress(int currentProtocol) { + /* + InputFilter[] filters = new InputFilter[1]; + filters[0] = new InputFilter() { + @Override + public CharSequence filter(CharSequence source, int start, int end, + android.text.Spanned dest, int dstart, int dend) { + if (end > start) { + String destTxt = dest.toString(); + String resultingTxt = destTxt.substring(0, dstart) + + source.subSequence(start, end) + + destTxt.substring(dend); + if (!resultingTxt + .matches("^\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3})?)?)?)?)?)?")) { + return ""; + } else { + String[] splits = resultingTxt.split("\\."); + for (int i = 0; i < splits.length; i++) { + if (Integer.valueOf(splits[i]) > 255) { + return ""; + } + } + } + } + return null; + } + }; + */ + //EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); + //Log.d("AisTrackerSettingsFragment","## findPreference()"); + //aisNmeaIpAddress.setOnBindEditTextListener(new androidx.preference.EditTextPreference.OnBindEditTextListener() { + /*aisNmeaIpAddress.setOnBindEditTextListener(new OnBindEditTextListener() { + @Override + public void onBindEditText(@NonNull EditText editText) { + editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); + editText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(10)}); + Log.d("AisTrackerSettingsFragment","## onBindEditText()"); + } + }); + */ + + EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); + if (aisNmeaIpAddress != null) { + aisNmeaIpAddress.setDescription(R.string.ais_address_nmea_server_description); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaIpAddress.setEnabled(false); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaIpAddress.setEnabled(true); + } + } + } + + private void setupTcpPort(int currentProtocol) { + /* EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); + if (aisNmeaPort != null) { + Log.d("AisTrackerSettingsFragment","## setupTcpPort()"); + aisNmeaPort.setOnBindEditTextListener(new OnBindEditTextListener() { + @Override + public void onBindEditText(@NonNull EditText editText) { + Log.d("AisTrackerSettingsFragment","## onBindEditText()"); + editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); + } + }); + aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaPort.setEnabled(false); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaPort.setEnabled(true); + } + } + */ + + EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); + if (aisNmeaPort != null) { + aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaPort.setEnabled(false); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaPort.setEnabled(true); + } + } + } + + private void setupUdpPort(int currentProtocol) { + EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_UDP_PORT.getId()); + if (aisNmeaPort != null) { + aisNmeaPort.setDescription(R.string.ais_port_nmea_local_description); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaPort.setEnabled(true); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaPort.setEnabled(false); + } + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean ret = super.onPreferenceChange(preference, newValue); + AisTrackerLayer layer = plugin.getLayer(); + if (layer != null) { + // layer.restartNetworkListeners(); // TEST + layer.restartNetworkListener(); + } + return ret; + } +} + diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java b/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java index 830b9e127f5..6cf2780c255 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java @@ -3,6 +3,7 @@ import net.osmand.plus.R; import net.osmand.plus.keyevent.fragments.MainExternalInputDevicesFragment; import net.osmand.plus.plugins.accessibility.AccessibilitySettingsFragment; +import net.osmand.plus.plugins.aistracker.AisTrackerSettingsFragment; import net.osmand.plus.plugins.audionotes.MultimediaNotesFragment; import net.osmand.plus.plugins.development.DevelopmentSettingsFragment; import net.osmand.plus.plugins.externalsensors.ExternalSettingsWriteToTrackSettingsFragment; @@ -44,7 +45,8 @@ public enum SettingsScreenType { WEATHER_SETTINGS(WeatherSettingsFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.weather_settings, R.layout.profile_preference_toolbar), EXTERNAL_SETTINGS_WRITE_TO_TRACK_SETTINGS(ExternalSettingsWriteToTrackSettingsFragment.class.getName(), true, ApplyQueryType.BOTTOM_SHEET, R.xml.external_sensors_write_to_track_settings, R.layout.profile_preference_toolbar), DANGEROUS_GOODS(DangerousGoodsFragment.class.getName(), true, ApplyQueryType.NONE, R.xml.dangerous_goods_parameters, R.layout.global_preference_toolbar), - EXTERNAL_INPUT_DEVICE(MainExternalInputDevicesFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.external_input_device_settings, R.layout.profile_preference_toolbar_with_switch); + EXTERNAL_INPUT_DEVICE(MainExternalInputDevicesFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.external_input_device_settings, R.layout.profile_preference_toolbar_with_switch), + AIS_SETTINGS(AisTrackerSettingsFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.ais_settings, R.layout.profile_preference_toolbar); public final String fragmentName; public final boolean profileDependent; diff --git a/gradle.properties b/gradle.properties index dffb2e0a798..61d78424229 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,24 +1,16 @@ -## Project-wide Gradle settings. -# -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m +# Default value: -Xmx1024m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # # When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true -#Fri Apr 08 18:47:31 EEST 2016 -# android.useDeprecatedNdk=true - -# for enableD8=true min sdk must be > 22 -# UPDATE: temporairly commented since gradle plugin updated to 3.1.3 and claims INSTALL_FAILED_DEXOPT is fixed -# UPDATE 2: D8 causes problems on arm64 devices with Android 6.0 (API 23) -# UPDATE 3: Turn on D8 to recover builds with new gradle 6.5 and pluigin 4.1.1 -#android.enableD8=false +#Sat Jun 15 00:12:35 CEST 2024 android.enableJetifier=true -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" From f31f333aa80dcde67b261c209d6636b8a415d93a Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 16 Jun 2024 17:52:03 +0200 Subject: [PATCH 02/74] new plugin: AIS vessel tracker, initial version --- .../osmand/plus/plugins/PluginsHelper.java | 2 +- .../aistracker/AisObjectConstants.java | 580 +++++++++--------- .../AisTrackerSettingsFragment.java | 11 +- 3 files changed, 298 insertions(+), 295 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java index b3e87409e8e..8ed40c7ba35 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java @@ -557,7 +557,7 @@ public static List onIndexingFiles(@Nullable IProgress progress) { List l = new ArrayList<>(); for (OsmandPlugin plugin : getEnabledPlugins()) { List ls = plugin.indexingFiles(progress); - if (ls != null && ls.size() > 0) { + if (ls != null && !ls.isEmpty()) { l.addAll(ls); } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index 9b3dc923953..d7a84cb3e09 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -40,296 +40,296 @@ public enum AisObjType { AIS_INVALID } public static final Map COUNTRY_CODES = Map.ofEntries( - new AbstractMap.SimpleEntry(201, "Albania"), - new AbstractMap.SimpleEntry(202, "Andorra"), - new AbstractMap.SimpleEntry(203, "Austria"), - new AbstractMap.SimpleEntry(204, "Portugal"), - new AbstractMap.SimpleEntry(205, "Belgium"), - new AbstractMap.SimpleEntry(206, "Belarus"), - new AbstractMap.SimpleEntry(207, "Bulgaria"), - new AbstractMap.SimpleEntry(208, "Vatican"), - new AbstractMap.SimpleEntry(209, "Cyprus"), - new AbstractMap.SimpleEntry(210, "Cyprus"), - new AbstractMap.SimpleEntry(211, "Germany"), - new AbstractMap.SimpleEntry(212, "Cyprus"), - new AbstractMap.SimpleEntry(213, "Georgia"), - new AbstractMap.SimpleEntry(214, "Moldova"), - new AbstractMap.SimpleEntry(215, "Malta"), - new AbstractMap.SimpleEntry(216, "Armenia"), - new AbstractMap.SimpleEntry(218, "Germany"), - new AbstractMap.SimpleEntry(219, "Denmark"), - new AbstractMap.SimpleEntry(220, "Denmark"), - new AbstractMap.SimpleEntry(224, "Spain"), - new AbstractMap.SimpleEntry(225, "Spain"), - new AbstractMap.SimpleEntry(226, "France"), - new AbstractMap.SimpleEntry(227, "France"), - new AbstractMap.SimpleEntry(228, "France"), - new AbstractMap.SimpleEntry(229, "Malta"), - new AbstractMap.SimpleEntry(230, "Finland"), - new AbstractMap.SimpleEntry(231, "Faroe Is"), - new AbstractMap.SimpleEntry(232, "United Kingdom"), - new AbstractMap.SimpleEntry(233, "United Kingdom"), - new AbstractMap.SimpleEntry(234, "United Kingdom"), - new AbstractMap.SimpleEntry(235, "United Kingdom"), - new AbstractMap.SimpleEntry(236, "Gibraltar"), - new AbstractMap.SimpleEntry(237, "Greece"), - new AbstractMap.SimpleEntry(238, "Croatia"), - new AbstractMap.SimpleEntry(239, "Greece"), - new AbstractMap.SimpleEntry(240, "Greece"), - new AbstractMap.SimpleEntry(241, "Greece"), - new AbstractMap.SimpleEntry(242, "Morocco"), - new AbstractMap.SimpleEntry(243, "Hungary"), - new AbstractMap.SimpleEntry(244, "Netherlands"), - new AbstractMap.SimpleEntry(245, "Netherlands"), - new AbstractMap.SimpleEntry(246, "Netherlands"), - new AbstractMap.SimpleEntry(247, "Italy"), - new AbstractMap.SimpleEntry(248, "Malta"), - new AbstractMap.SimpleEntry(249, "Malta"), - new AbstractMap.SimpleEntry(250, "Ireland"), - new AbstractMap.SimpleEntry(251, "Iceland"), - new AbstractMap.SimpleEntry(252, "Liechtenstein"), - new AbstractMap.SimpleEntry(253, "Luxembourg"), - new AbstractMap.SimpleEntry(254, "Monaco"), - new AbstractMap.SimpleEntry(255, "Portugal"), - new AbstractMap.SimpleEntry(256, "Malta"), - new AbstractMap.SimpleEntry(257, "Norway"), - new AbstractMap.SimpleEntry(258, "Norway"), - new AbstractMap.SimpleEntry(259, "Norway"), - new AbstractMap.SimpleEntry(261, "Poland"), - new AbstractMap.SimpleEntry(262, "Montenegro"), - new AbstractMap.SimpleEntry(263, "Portugal"), - new AbstractMap.SimpleEntry(264, "Romania"), - new AbstractMap.SimpleEntry(265, "Sweden"), - new AbstractMap.SimpleEntry(266, "Sweden"), - new AbstractMap.SimpleEntry(267, "Slovakia"), - new AbstractMap.SimpleEntry(268, "San Marino"), - new AbstractMap.SimpleEntry(269, "Switzerland"), - new AbstractMap.SimpleEntry(270, "Czech Republic"), - new AbstractMap.SimpleEntry(271, "Turkey"), - new AbstractMap.SimpleEntry(272, "Ukraine"), - new AbstractMap.SimpleEntry(273, "Russia"), - new AbstractMap.SimpleEntry(274, "FYR Macedonia"), - new AbstractMap.SimpleEntry(275, "Latvia"), - new AbstractMap.SimpleEntry(276, "Estonia"), - new AbstractMap.SimpleEntry(277, "Lithuania"), - new AbstractMap.SimpleEntry(278, "Slovenia"), - new AbstractMap.SimpleEntry(279, "Serbia"), - new AbstractMap.SimpleEntry(301, "Anguilla"), - new AbstractMap.SimpleEntry(303, "USA"), - new AbstractMap.SimpleEntry(304, "Antigua Barbuda"), - new AbstractMap.SimpleEntry(305, "Antigua Barbuda"), - new AbstractMap.SimpleEntry(306, "Curacao"), - new AbstractMap.SimpleEntry(307, "Aruba"), - new AbstractMap.SimpleEntry(308, "Bahamas"), - new AbstractMap.SimpleEntry(309, "Bahamas"), - new AbstractMap.SimpleEntry(310, "Bermuda"), - new AbstractMap.SimpleEntry(311, "Bahamas"), - new AbstractMap.SimpleEntry(312, "Belize"), - new AbstractMap.SimpleEntry(314, "Barbados"), - new AbstractMap.SimpleEntry(316, "Canada"), - new AbstractMap.SimpleEntry(319, "Cayman Is"), - new AbstractMap.SimpleEntry(321, "Costa Rica"), - new AbstractMap.SimpleEntry(323, "Cuba"), - new AbstractMap.SimpleEntry(325, "Dominica"), - new AbstractMap.SimpleEntry(327, "Dominican Rep"), - new AbstractMap.SimpleEntry(329, "Guadeloupe"), - new AbstractMap.SimpleEntry(330, "Grenada"), - new AbstractMap.SimpleEntry(331, "Greenland"), - new AbstractMap.SimpleEntry(332, "Guatemala"), - new AbstractMap.SimpleEntry(334, "Honduras"), - new AbstractMap.SimpleEntry(336, "Haiti"), - new AbstractMap.SimpleEntry(338, "USA"), - new AbstractMap.SimpleEntry(339, "Jamaica"), - new AbstractMap.SimpleEntry(341, "St Kitts Nevis"), - new AbstractMap.SimpleEntry(343, "St Lucia"), - new AbstractMap.SimpleEntry(345, "Mexico"), - new AbstractMap.SimpleEntry(347, "Martinique"), - new AbstractMap.SimpleEntry(348, "Montserrat"), - new AbstractMap.SimpleEntry(350, "Nicaragua"), - new AbstractMap.SimpleEntry(351, "Panama"), - new AbstractMap.SimpleEntry(352, "Panama"), - new AbstractMap.SimpleEntry(353, "Panama"), - new AbstractMap.SimpleEntry(354, "Panama"), - new AbstractMap.SimpleEntry(355, "Panama"), - new AbstractMap.SimpleEntry(356, "Panama"), - new AbstractMap.SimpleEntry(357, "Panama"), - new AbstractMap.SimpleEntry(358, "Puerto Rico"), - new AbstractMap.SimpleEntry(359, "El Salvador"), - new AbstractMap.SimpleEntry(361, "St Pierre Miquelon"), - new AbstractMap.SimpleEntry(362, "Trinidad Tobago"), - new AbstractMap.SimpleEntry(364, "Turks Caicos Is"), - new AbstractMap.SimpleEntry(366, "USA"), - new AbstractMap.SimpleEntry(367, "USA"), - new AbstractMap.SimpleEntry(368, "USA"), - new AbstractMap.SimpleEntry(369, "USA"), - new AbstractMap.SimpleEntry(370, "Panama"), - new AbstractMap.SimpleEntry(371, "Panama"), - new AbstractMap.SimpleEntry(372, "Panama"), - new AbstractMap.SimpleEntry(373, "Panama"), - new AbstractMap.SimpleEntry(374, "Panama"), - new AbstractMap.SimpleEntry(375, "St Vincent Grenadines"), - new AbstractMap.SimpleEntry(376, "St Vincent Grenadines"), - new AbstractMap.SimpleEntry(377, "St Vincent Grenadines"), - new AbstractMap.SimpleEntry(378, "British Virgin Is"), - new AbstractMap.SimpleEntry(379, "US Virgin Is"), - new AbstractMap.SimpleEntry(401, "Afghanistan"), - new AbstractMap.SimpleEntry(403, "Saudi Arabia"), - new AbstractMap.SimpleEntry(405, "Bangladesh"), - new AbstractMap.SimpleEntry(408, "Bahrain"), - new AbstractMap.SimpleEntry(410, "Bhutan"), - new AbstractMap.SimpleEntry(412, "China"), - new AbstractMap.SimpleEntry(413, "China"), - new AbstractMap.SimpleEntry(414, "China"), - new AbstractMap.SimpleEntry(416, "Taiwan"), - new AbstractMap.SimpleEntry(417, "Sri Lanka"), - new AbstractMap.SimpleEntry(419, "India"), - new AbstractMap.SimpleEntry(422, "Iran"), - new AbstractMap.SimpleEntry(423, "Azerbaijan"), - new AbstractMap.SimpleEntry(425, "Iraq"), - new AbstractMap.SimpleEntry(428, "Israel"), - new AbstractMap.SimpleEntry(431, "Japan"), - new AbstractMap.SimpleEntry(432, "Japan"), - new AbstractMap.SimpleEntry(434, "Turkmenistan"), - new AbstractMap.SimpleEntry(436, "Kazakhstan"), - new AbstractMap.SimpleEntry(437, "Uzbekistan"), - new AbstractMap.SimpleEntry(438, "Jordan"), - new AbstractMap.SimpleEntry(440, "Korea"), - new AbstractMap.SimpleEntry(441, "Korea"), - new AbstractMap.SimpleEntry(443, "Palestine"), - new AbstractMap.SimpleEntry(445, "DPR Korea"), - new AbstractMap.SimpleEntry(447, "Kuwait"), - new AbstractMap.SimpleEntry(450, "Lebanon"), - new AbstractMap.SimpleEntry(451, "Kyrgyz Republic"), - new AbstractMap.SimpleEntry(453, "Macao"), - new AbstractMap.SimpleEntry(455, "Maldives"), - new AbstractMap.SimpleEntry(457, "Mongolia"), - new AbstractMap.SimpleEntry(459, "Nepal"), - new AbstractMap.SimpleEntry(461, "Oman"), - new AbstractMap.SimpleEntry(463, "Pakistan"), - new AbstractMap.SimpleEntry(466, "Qatar"), - new AbstractMap.SimpleEntry(468, "Syria"), - new AbstractMap.SimpleEntry(470, "UAE"), - new AbstractMap.SimpleEntry(471, "UAE"), - new AbstractMap.SimpleEntry(472, "Tajikistan"), - new AbstractMap.SimpleEntry(473, "Yemen"), - new AbstractMap.SimpleEntry(475, "Yemen"), - new AbstractMap.SimpleEntry(477, "Hong Kong"), - new AbstractMap.SimpleEntry(478, "Bosnia and Herzegovina"), - new AbstractMap.SimpleEntry(501, "Antarctica"), - new AbstractMap.SimpleEntry(503, "Australia"), - new AbstractMap.SimpleEntry(506, "Myanmar"), - new AbstractMap.SimpleEntry(508, "Brunei"), - new AbstractMap.SimpleEntry(510, "Micronesia"), - new AbstractMap.SimpleEntry(511, "Palau"), - new AbstractMap.SimpleEntry(512, "New Zealand"), - new AbstractMap.SimpleEntry(514, "Cambodia"), - new AbstractMap.SimpleEntry(515, "Cambodia"), - new AbstractMap.SimpleEntry(516, "Christmas Is"), - new AbstractMap.SimpleEntry(518, "Cook Is"), - new AbstractMap.SimpleEntry(520, "Fiji"), - new AbstractMap.SimpleEntry(523, "Cocos Is"), - new AbstractMap.SimpleEntry(525, "Indonesia"), - new AbstractMap.SimpleEntry(529, "Kiribati"), - new AbstractMap.SimpleEntry(531, "Laos"), - new AbstractMap.SimpleEntry(533, "Malaysia"), - new AbstractMap.SimpleEntry(536, "N Mariana Is"), - new AbstractMap.SimpleEntry(538, "Marshall Is"), - new AbstractMap.SimpleEntry(540, "New Caledonia"), - new AbstractMap.SimpleEntry(542, "Niue"), - new AbstractMap.SimpleEntry(544, "Nauru"), - new AbstractMap.SimpleEntry(546, "French Polynesia"), - new AbstractMap.SimpleEntry(548, "Philippines"), - new AbstractMap.SimpleEntry(553, "Papua New Guinea"), - new AbstractMap.SimpleEntry(555, "Pitcairn Is"), - new AbstractMap.SimpleEntry(557, "Solomon Is"), - new AbstractMap.SimpleEntry(559, "American Samoa"), - new AbstractMap.SimpleEntry(561, "Samoa"), - new AbstractMap.SimpleEntry(563, "Singapore"), - new AbstractMap.SimpleEntry(564, "Singapore"), - new AbstractMap.SimpleEntry(565, "Singapore"), - new AbstractMap.SimpleEntry(566, "Singapore"), - new AbstractMap.SimpleEntry(567, "Thailand"), - new AbstractMap.SimpleEntry(570, "Tonga"), - new AbstractMap.SimpleEntry(572, "Tuvalu"), - new AbstractMap.SimpleEntry(574, "Vietnam"), - new AbstractMap.SimpleEntry(576, "Vanuatu"), - new AbstractMap.SimpleEntry(577, "Vanuatu"), - new AbstractMap.SimpleEntry(578, "Wallis Futuna Is"), - new AbstractMap.SimpleEntry(601, "South Africa"), - new AbstractMap.SimpleEntry(603, "Angola"), - new AbstractMap.SimpleEntry(605, "Algeria"), - new AbstractMap.SimpleEntry(607, "St Paul Amsterdam Is"), - new AbstractMap.SimpleEntry(608, "Ascension Is"), - new AbstractMap.SimpleEntry(609, "Burundi"), - new AbstractMap.SimpleEntry(610, "Benin"), - new AbstractMap.SimpleEntry(611, "Botswana"), - new AbstractMap.SimpleEntry(612, "Cen Afr Rep"), - new AbstractMap.SimpleEntry(613, "Cameroon"), - new AbstractMap.SimpleEntry(615, "Congo"), - new AbstractMap.SimpleEntry(616, "Comoros"), - new AbstractMap.SimpleEntry(617, "Cape Verde"), - new AbstractMap.SimpleEntry(618, "Antarctica"), - new AbstractMap.SimpleEntry(619, "Ivory Coast"), - new AbstractMap.SimpleEntry(620, "Comoros"), - new AbstractMap.SimpleEntry(621, "Djibouti"), - new AbstractMap.SimpleEntry(622, "Egypt"), - new AbstractMap.SimpleEntry(624, "Ethiopia"), - new AbstractMap.SimpleEntry(625, "Eritrea"), - new AbstractMap.SimpleEntry(626, "Gabon"), - new AbstractMap.SimpleEntry(627, "Ghana"), - new AbstractMap.SimpleEntry(629, "Gambia"), - new AbstractMap.SimpleEntry(630, "Guinea-Bissau"), - new AbstractMap.SimpleEntry(631, "Equ. Guinea"), - new AbstractMap.SimpleEntry(632, "Guinea"), - new AbstractMap.SimpleEntry(633, "Burkina Faso"), - new AbstractMap.SimpleEntry(634, "Kenya"), - new AbstractMap.SimpleEntry(635, "Antarctica"), - new AbstractMap.SimpleEntry(636, "Liberia"), - new AbstractMap.SimpleEntry(637, "Liberia"), - new AbstractMap.SimpleEntry(642, "Libya"), - new AbstractMap.SimpleEntry(644, "Lesotho"), - new AbstractMap.SimpleEntry(645, "Mauritius"), - new AbstractMap.SimpleEntry(647, "Madagascar"), - new AbstractMap.SimpleEntry(649, "Mali"), - new AbstractMap.SimpleEntry(650, "Mozambique"), - new AbstractMap.SimpleEntry(654, "Mauritania"), - new AbstractMap.SimpleEntry(655, "Malawi"), - new AbstractMap.SimpleEntry(656, "Niger"), - new AbstractMap.SimpleEntry(657, "Nigeria"), - new AbstractMap.SimpleEntry(659, "Namibia"), - new AbstractMap.SimpleEntry(660, "Reunion"), - new AbstractMap.SimpleEntry(661, "Rwanda"), - new AbstractMap.SimpleEntry(662, "Sudan"), - new AbstractMap.SimpleEntry(663, "Senegal"), - new AbstractMap.SimpleEntry(664, "Seychelles"), - new AbstractMap.SimpleEntry(665, "St Helena"), - new AbstractMap.SimpleEntry(666, "Somalia"), - new AbstractMap.SimpleEntry(667, "Sierra Leone"), - new AbstractMap.SimpleEntry(668, "Sao Tome Principe"), - new AbstractMap.SimpleEntry(669, "Swaziland"), - new AbstractMap.SimpleEntry(670, "Chad"), - new AbstractMap.SimpleEntry(671, "Togo"), - new AbstractMap.SimpleEntry(672, "Tunisia"), - new AbstractMap.SimpleEntry(674, "Tanzania"), - new AbstractMap.SimpleEntry(675, "Uganda"), - new AbstractMap.SimpleEntry(676, "DR Congo"), - new AbstractMap.SimpleEntry(677, "Tanzania"), - new AbstractMap.SimpleEntry(678, "Zambia"), - new AbstractMap.SimpleEntry(679, "Zimbabwe"), - new AbstractMap.SimpleEntry(701, "Argentina"), - new AbstractMap.SimpleEntry(710, "Brazil"), - new AbstractMap.SimpleEntry(720, "Bolivia"), - new AbstractMap.SimpleEntry(725, "Chile"), - new AbstractMap.SimpleEntry(730, "Colombia"), - new AbstractMap.SimpleEntry(735, "Ecuador"), - new AbstractMap.SimpleEntry(740, "UK"), - new AbstractMap.SimpleEntry(745, "Guiana"), - new AbstractMap.SimpleEntry(750, "Guyana"), - new AbstractMap.SimpleEntry(755, "Paraguay"), - new AbstractMap.SimpleEntry(760, "Peru"), - new AbstractMap.SimpleEntry(765, "Suriname"), - new AbstractMap.SimpleEntry(770, "Uruguay"), - new AbstractMap.SimpleEntry(775, "Venezuela") + new AbstractMap.SimpleEntry<>(201, "Albania"), + new AbstractMap.SimpleEntry<>(202, "Andorra"), + new AbstractMap.SimpleEntry<>(203, "Austria"), + new AbstractMap.SimpleEntry<>(204, "Portugal"), + new AbstractMap.SimpleEntry<>(205, "Belgium"), + new AbstractMap.SimpleEntry<>(206, "Belarus"), + new AbstractMap.SimpleEntry<>(207, "Bulgaria"), + new AbstractMap.SimpleEntry<>(208, "Vatican"), + new AbstractMap.SimpleEntry<>(209, "Cyprus"), + new AbstractMap.SimpleEntry<>(210, "Cyprus"), + new AbstractMap.SimpleEntry<>(211, "Germany"), + new AbstractMap.SimpleEntry<>(212, "Cyprus"), + new AbstractMap.SimpleEntry<>(213, "Georgia"), + new AbstractMap.SimpleEntry<>(214, "Moldova"), + new AbstractMap.SimpleEntry<>(215, "Malta"), + new AbstractMap.SimpleEntry<>(216, "Armenia"), + new AbstractMap.SimpleEntry<>(218, "Germany"), + new AbstractMap.SimpleEntry<>(219, "Denmark"), + new AbstractMap.SimpleEntry<>(220, "Denmark"), + new AbstractMap.SimpleEntry<>(224, "Spain"), + new AbstractMap.SimpleEntry<>(225, "Spain"), + new AbstractMap.SimpleEntry<>(226, "France"), + new AbstractMap.SimpleEntry<>(227, "France"), + new AbstractMap.SimpleEntry<>(228, "France"), + new AbstractMap.SimpleEntry<>(229, "Malta"), + new AbstractMap.SimpleEntry<>(230, "Finland"), + new AbstractMap.SimpleEntry<>(231, "Faroe Is"), + new AbstractMap.SimpleEntry<>(232, "United Kingdom"), + new AbstractMap.SimpleEntry<>(233, "United Kingdom"), + new AbstractMap.SimpleEntry<>(234, "United Kingdom"), + new AbstractMap.SimpleEntry<>(235, "United Kingdom"), + new AbstractMap.SimpleEntry<>(236, "Gibraltar"), + new AbstractMap.SimpleEntry<>(237, "Greece"), + new AbstractMap.SimpleEntry<>(238, "Croatia"), + new AbstractMap.SimpleEntry<>(239, "Greece"), + new AbstractMap.SimpleEntry<>(240, "Greece"), + new AbstractMap.SimpleEntry<>(241, "Greece"), + new AbstractMap.SimpleEntry<>(242, "Morocco"), + new AbstractMap.SimpleEntry<>(243, "Hungary"), + new AbstractMap.SimpleEntry<>(244, "Netherlands"), + new AbstractMap.SimpleEntry<>(245, "Netherlands"), + new AbstractMap.SimpleEntry<>(246, "Netherlands"), + new AbstractMap.SimpleEntry<>(247, "Italy"), + new AbstractMap.SimpleEntry<>(248, "Malta"), + new AbstractMap.SimpleEntry<>(249, "Malta"), + new AbstractMap.SimpleEntry<>(250, "Ireland"), + new AbstractMap.SimpleEntry<>(251, "Iceland"), + new AbstractMap.SimpleEntry<>(252, "Liechtenstein"), + new AbstractMap.SimpleEntry<>(253, "Luxembourg"), + new AbstractMap.SimpleEntry<>(254, "Monaco"), + new AbstractMap.SimpleEntry<>(255, "Portugal"), + new AbstractMap.SimpleEntry<>(256, "Malta"), + new AbstractMap.SimpleEntry<>(257, "Norway"), + new AbstractMap.SimpleEntry<>(258, "Norway"), + new AbstractMap.SimpleEntry<>(259, "Norway"), + new AbstractMap.SimpleEntry<>(261, "Poland"), + new AbstractMap.SimpleEntry<>(262, "Montenegro"), + new AbstractMap.SimpleEntry<>(263, "Portugal"), + new AbstractMap.SimpleEntry<>(264, "Romania"), + new AbstractMap.SimpleEntry<>(265, "Sweden"), + new AbstractMap.SimpleEntry<>(266, "Sweden"), + new AbstractMap.SimpleEntry<>(267, "Slovakia"), + new AbstractMap.SimpleEntry<>(268, "San Marino"), + new AbstractMap.SimpleEntry<>(269, "Switzerland"), + new AbstractMap.SimpleEntry<>(270, "Czech Republic"), + new AbstractMap.SimpleEntry<>(271, "Turkey"), + new AbstractMap.SimpleEntry<>(272, "Ukraine"), + new AbstractMap.SimpleEntry<>(273, "Russia"), + new AbstractMap.SimpleEntry<>(274, "FYR Macedonia"), + new AbstractMap.SimpleEntry<>(275, "Latvia"), + new AbstractMap.SimpleEntry<>(276, "Estonia"), + new AbstractMap.SimpleEntry<>(277, "Lithuania"), + new AbstractMap.SimpleEntry<>(278, "Slovenia"), + new AbstractMap.SimpleEntry<>(279, "Serbia"), + new AbstractMap.SimpleEntry<>(301, "Anguilla"), + new AbstractMap.SimpleEntry<>(303, "USA"), + new AbstractMap.SimpleEntry<>(304, "Antigua Barbuda"), + new AbstractMap.SimpleEntry<>(305, "Antigua Barbuda"), + new AbstractMap.SimpleEntry<>(306, "Curacao"), + new AbstractMap.SimpleEntry<>(307, "Aruba"), + new AbstractMap.SimpleEntry<>(308, "Bahamas"), + new AbstractMap.SimpleEntry<>(309, "Bahamas"), + new AbstractMap.SimpleEntry<>(310, "Bermuda"), + new AbstractMap.SimpleEntry<>(311, "Bahamas"), + new AbstractMap.SimpleEntry<>(312, "Belize"), + new AbstractMap.SimpleEntry<>(314, "Barbados"), + new AbstractMap.SimpleEntry<>(316, "Canada"), + new AbstractMap.SimpleEntry<>(319, "Cayman Is"), + new AbstractMap.SimpleEntry<>(321, "Costa Rica"), + new AbstractMap.SimpleEntry<>(323, "Cuba"), + new AbstractMap.SimpleEntry<>(325, "Dominica"), + new AbstractMap.SimpleEntry<>(327, "Dominican Rep"), + new AbstractMap.SimpleEntry<>(329, "Guadeloupe"), + new AbstractMap.SimpleEntry<>(330, "Grenada"), + new AbstractMap.SimpleEntry<>(331, "Greenland"), + new AbstractMap.SimpleEntry<>(332, "Guatemala"), + new AbstractMap.SimpleEntry<>(334, "Honduras"), + new AbstractMap.SimpleEntry<>(336, "Haiti"), + new AbstractMap.SimpleEntry<>(338, "USA"), + new AbstractMap.SimpleEntry<>(339, "Jamaica"), + new AbstractMap.SimpleEntry<>(341, "St Kitts Nevis"), + new AbstractMap.SimpleEntry<>(343, "St Lucia"), + new AbstractMap.SimpleEntry<>(345, "Mexico"), + new AbstractMap.SimpleEntry<>(347, "Martinique"), + new AbstractMap.SimpleEntry<>(348, "Montserrat"), + new AbstractMap.SimpleEntry<>(350, "Nicaragua"), + new AbstractMap.SimpleEntry<>(351, "Panama"), + new AbstractMap.SimpleEntry<>(352, "Panama"), + new AbstractMap.SimpleEntry<>(353, "Panama"), + new AbstractMap.SimpleEntry<>(354, "Panama"), + new AbstractMap.SimpleEntry<>(355, "Panama"), + new AbstractMap.SimpleEntry<>(356, "Panama"), + new AbstractMap.SimpleEntry<>(357, "Panama"), + new AbstractMap.SimpleEntry<>(358, "Puerto Rico"), + new AbstractMap.SimpleEntry<>(359, "El Salvador"), + new AbstractMap.SimpleEntry<>(361, "St Pierre Miquelon"), + new AbstractMap.SimpleEntry<>(362, "Trinidad Tobago"), + new AbstractMap.SimpleEntry<>(364, "Turks Caicos Is"), + new AbstractMap.SimpleEntry<>(366, "USA"), + new AbstractMap.SimpleEntry<>(367, "USA"), + new AbstractMap.SimpleEntry<>(368, "USA"), + new AbstractMap.SimpleEntry<>(369, "USA"), + new AbstractMap.SimpleEntry<>(370, "Panama"), + new AbstractMap.SimpleEntry<>(371, "Panama"), + new AbstractMap.SimpleEntry<>(372, "Panama"), + new AbstractMap.SimpleEntry<>(373, "Panama"), + new AbstractMap.SimpleEntry<>(374, "Panama"), + new AbstractMap.SimpleEntry<>(375, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry<>(376, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry<>(377, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry<>(378, "British Virgin Is"), + new AbstractMap.SimpleEntry<>(379, "US Virgin Is"), + new AbstractMap.SimpleEntry<>(401, "Afghanistan"), + new AbstractMap.SimpleEntry<>(403, "Saudi Arabia"), + new AbstractMap.SimpleEntry<>(405, "Bangladesh"), + new AbstractMap.SimpleEntry<>(408, "Bahrain"), + new AbstractMap.SimpleEntry<>(410, "Bhutan"), + new AbstractMap.SimpleEntry<>(412, "China"), + new AbstractMap.SimpleEntry<>(413, "China"), + new AbstractMap.SimpleEntry<>(414, "China"), + new AbstractMap.SimpleEntry<>(416, "Taiwan"), + new AbstractMap.SimpleEntry<>(417, "Sri Lanka"), + new AbstractMap.SimpleEntry<>(419, "India"), + new AbstractMap.SimpleEntry<>(422, "Iran"), + new AbstractMap.SimpleEntry<>(423, "Azerbaijan"), + new AbstractMap.SimpleEntry<>(425, "Iraq"), + new AbstractMap.SimpleEntry<>(428, "Israel"), + new AbstractMap.SimpleEntry<>(431, "Japan"), + new AbstractMap.SimpleEntry<>(432, "Japan"), + new AbstractMap.SimpleEntry<>(434, "Turkmenistan"), + new AbstractMap.SimpleEntry<>(436, "Kazakhstan"), + new AbstractMap.SimpleEntry<>(437, "Uzbekistan"), + new AbstractMap.SimpleEntry<>(438, "Jordan"), + new AbstractMap.SimpleEntry<>(440, "Korea"), + new AbstractMap.SimpleEntry<>(441, "Korea"), + new AbstractMap.SimpleEntry<>(443, "Palestine"), + new AbstractMap.SimpleEntry<>(445, "DPR Korea"), + new AbstractMap.SimpleEntry<>(447, "Kuwait"), + new AbstractMap.SimpleEntry<>(450, "Lebanon"), + new AbstractMap.SimpleEntry<>(451, "Kyrgyz Republic"), + new AbstractMap.SimpleEntry<>(453, "Macao"), + new AbstractMap.SimpleEntry<>(455, "Maldives"), + new AbstractMap.SimpleEntry<>(457, "Mongolia"), + new AbstractMap.SimpleEntry<>(459, "Nepal"), + new AbstractMap.SimpleEntry<>(461, "Oman"), + new AbstractMap.SimpleEntry<>(463, "Pakistan"), + new AbstractMap.SimpleEntry<>(466, "Qatar"), + new AbstractMap.SimpleEntry<>(468, "Syria"), + new AbstractMap.SimpleEntry<>(470, "UAE"), + new AbstractMap.SimpleEntry<>(471, "UAE"), + new AbstractMap.SimpleEntry<>(472, "Tajikistan"), + new AbstractMap.SimpleEntry<>(473, "Yemen"), + new AbstractMap.SimpleEntry<>(475, "Yemen"), + new AbstractMap.SimpleEntry<>(477, "Hong Kong"), + new AbstractMap.SimpleEntry<>(478, "Bosnia and Herzegovina"), + new AbstractMap.SimpleEntry<>(501, "Antarctica"), + new AbstractMap.SimpleEntry<>(503, "Australia"), + new AbstractMap.SimpleEntry<>(506, "Myanmar"), + new AbstractMap.SimpleEntry<>(508, "Brunei"), + new AbstractMap.SimpleEntry<>(510, "Micronesia"), + new AbstractMap.SimpleEntry<>(511, "Palau"), + new AbstractMap.SimpleEntry<>(512, "New Zealand"), + new AbstractMap.SimpleEntry<>(514, "Cambodia"), + new AbstractMap.SimpleEntry<>(515, "Cambodia"), + new AbstractMap.SimpleEntry<>(516, "Christmas Is"), + new AbstractMap.SimpleEntry<>(518, "Cook Is"), + new AbstractMap.SimpleEntry<>(520, "Fiji"), + new AbstractMap.SimpleEntry<>(523, "Cocos Is"), + new AbstractMap.SimpleEntry<>(525, "Indonesia"), + new AbstractMap.SimpleEntry<>(529, "Kiribati"), + new AbstractMap.SimpleEntry<>(531, "Laos"), + new AbstractMap.SimpleEntry<>(533, "Malaysia"), + new AbstractMap.SimpleEntry<>(536, "N Mariana Is"), + new AbstractMap.SimpleEntry<>(538, "Marshall Is"), + new AbstractMap.SimpleEntry<>(540, "New Caledonia"), + new AbstractMap.SimpleEntry<>(542, "Niue"), + new AbstractMap.SimpleEntry<>(544, "Nauru"), + new AbstractMap.SimpleEntry<>(546, "French Polynesia"), + new AbstractMap.SimpleEntry<>(548, "Philippines"), + new AbstractMap.SimpleEntry<>(553, "Papua New Guinea"), + new AbstractMap.SimpleEntry<>(555, "Pitcairn Is"), + new AbstractMap.SimpleEntry<>(557, "Solomon Is"), + new AbstractMap.SimpleEntry<>(559, "American Samoa"), + new AbstractMap.SimpleEntry<>(561, "Samoa"), + new AbstractMap.SimpleEntry<>(563, "Singapore"), + new AbstractMap.SimpleEntry<>(564, "Singapore"), + new AbstractMap.SimpleEntry<>(565, "Singapore"), + new AbstractMap.SimpleEntry<>(566, "Singapore"), + new AbstractMap.SimpleEntry<>(567, "Thailand"), + new AbstractMap.SimpleEntry<>(570, "Tonga"), + new AbstractMap.SimpleEntry<>(572, "Tuvalu"), + new AbstractMap.SimpleEntry<>(574, "Vietnam"), + new AbstractMap.SimpleEntry<>(576, "Vanuatu"), + new AbstractMap.SimpleEntry<>(577, "Vanuatu"), + new AbstractMap.SimpleEntry<>(578, "Wallis Futuna Is"), + new AbstractMap.SimpleEntry<>(601, "South Africa"), + new AbstractMap.SimpleEntry<>(603, "Angola"), + new AbstractMap.SimpleEntry<>(605, "Algeria"), + new AbstractMap.SimpleEntry<>(607, "St Paul Amsterdam Is"), + new AbstractMap.SimpleEntry<>(608, "Ascension Is"), + new AbstractMap.SimpleEntry<>(609, "Burundi"), + new AbstractMap.SimpleEntry<>(610, "Benin"), + new AbstractMap.SimpleEntry<>(611, "Botswana"), + new AbstractMap.SimpleEntry<>(612, "Cen Afr Rep"), + new AbstractMap.SimpleEntry<>(613, "Cameroon"), + new AbstractMap.SimpleEntry<>(615, "Congo"), + new AbstractMap.SimpleEntry<>(616, "Comoros"), + new AbstractMap.SimpleEntry<>(617, "Cape Verde"), + new AbstractMap.SimpleEntry<>(618, "Antarctica"), + new AbstractMap.SimpleEntry<>(619, "Ivory Coast"), + new AbstractMap.SimpleEntry<>(620, "Comoros"), + new AbstractMap.SimpleEntry<>(621, "Djibouti"), + new AbstractMap.SimpleEntry<>(622, "Egypt"), + new AbstractMap.SimpleEntry<>(624, "Ethiopia"), + new AbstractMap.SimpleEntry<>(625, "Eritrea"), + new AbstractMap.SimpleEntry<>(626, "Gabon"), + new AbstractMap.SimpleEntry<>(627, "Ghana"), + new AbstractMap.SimpleEntry<>(629, "Gambia"), + new AbstractMap.SimpleEntry<>(630, "Guinea-Bissau"), + new AbstractMap.SimpleEntry<>(631, "Equ. Guinea"), + new AbstractMap.SimpleEntry<>(632, "Guinea"), + new AbstractMap.SimpleEntry<>(633, "Burkina Faso"), + new AbstractMap.SimpleEntry<>(634, "Kenya"), + new AbstractMap.SimpleEntry<>(635, "Antarctica"), + new AbstractMap.SimpleEntry<>(636, "Liberia"), + new AbstractMap.SimpleEntry<>(637, "Liberia"), + new AbstractMap.SimpleEntry<>(642, "Libya"), + new AbstractMap.SimpleEntry<>(644, "Lesotho"), + new AbstractMap.SimpleEntry<>(645, "Mauritius"), + new AbstractMap.SimpleEntry<>(647, "Madagascar"), + new AbstractMap.SimpleEntry<>(649, "Mali"), + new AbstractMap.SimpleEntry<>(650, "Mozambique"), + new AbstractMap.SimpleEntry<>(654, "Mauritania"), + new AbstractMap.SimpleEntry<>(655, "Malawi"), + new AbstractMap.SimpleEntry<>(656, "Niger"), + new AbstractMap.SimpleEntry<>(657, "Nigeria"), + new AbstractMap.SimpleEntry<>(659, "Namibia"), + new AbstractMap.SimpleEntry<>(660, "Reunion"), + new AbstractMap.SimpleEntry<>(661, "Rwanda"), + new AbstractMap.SimpleEntry<>(662, "Sudan"), + new AbstractMap.SimpleEntry<>(663, "Senegal"), + new AbstractMap.SimpleEntry<>(664, "Seychelles"), + new AbstractMap.SimpleEntry<>(665, "St Helena"), + new AbstractMap.SimpleEntry<>(666, "Somalia"), + new AbstractMap.SimpleEntry<>(667, "Sierra Leone"), + new AbstractMap.SimpleEntry<>(668, "Sao Tome Principe"), + new AbstractMap.SimpleEntry<>(669, "Swaziland"), + new AbstractMap.SimpleEntry<>(670, "Chad"), + new AbstractMap.SimpleEntry<>(671, "Togo"), + new AbstractMap.SimpleEntry<>(672, "Tunisia"), + new AbstractMap.SimpleEntry<>(674, "Tanzania"), + new AbstractMap.SimpleEntry<>(675, "Uganda"), + new AbstractMap.SimpleEntry<>(676, "DR Congo"), + new AbstractMap.SimpleEntry<>(677, "Tanzania"), + new AbstractMap.SimpleEntry<>(678, "Zambia"), + new AbstractMap.SimpleEntry<>(679, "Zimbabwe"), + new AbstractMap.SimpleEntry<>(701, "Argentina"), + new AbstractMap.SimpleEntry<>(710, "Brazil"), + new AbstractMap.SimpleEntry<>(720, "Bolivia"), + new AbstractMap.SimpleEntry<>(725, "Chile"), + new AbstractMap.SimpleEntry<>(730, "Colombia"), + new AbstractMap.SimpleEntry<>(735, "Ecuador"), + new AbstractMap.SimpleEntry<>(740, "UK"), + new AbstractMap.SimpleEntry<>(745, "Guiana"), + new AbstractMap.SimpleEntry<>(750, "Guyana"), + new AbstractMap.SimpleEntry<>(755, "Paraguay"), + new AbstractMap.SimpleEntry<>(760, "Peru"), + new AbstractMap.SimpleEntry<>(765, "Suriname"), + new AbstractMap.SimpleEntry<>(770, "Uruguay"), + new AbstractMap.SimpleEntry<>(775, "Venezuela") ); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 4c4ae571cb2..459bbacb57a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -37,10 +37,13 @@ private int setupProtocol() { String[] entries = {"UDP", "TCP"}; ListPreferenceEx aisNmeaProtocol = findPreference(plugin.AIS_NMEA_PROTOCOL.getId()); - aisNmeaProtocol.setEntries(entries); - aisNmeaProtocol.setEntryValues(entryValues); - aisNmeaProtocol.setDescription(R.string.ais_nmea_protocol_description); - return (int)aisNmeaProtocol.getValue(); + if (aisNmeaProtocol != null) { + aisNmeaProtocol.setEntries(entries); + aisNmeaProtocol.setEntryValues(entryValues); + aisNmeaProtocol.setDescription(R.string.ais_nmea_protocol_description); + return (int)aisNmeaProtocol.getValue(); + } + return 0; } private void setupIpAddress(int currentProtocol) { From 2a4eb97215b17b8c8f13bc37287f4a1527f02d21 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 16 Jun 2024 19:31:36 +0200 Subject: [PATCH 03/74] insert some AIS objects for test purposes --- .../aistracker/AisObjectMenuController.java | 12 ++--- .../plugins/aistracker/AisTrackerLayer.java | 45 ++++++++++++------- .../plugins/aistracker/AisTrackerPlugin.java | 3 +- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index b50ae77535b..f647af88122 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -2,6 +2,8 @@ import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; +import android.annotation.SuppressLint; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -82,10 +84,10 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("COG", String.valueOf(aisObject.getCog())); } if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { - addMenuItem("SOG", String.valueOf(aisObject.getSog())); + addMenuItem("SOG", String.valueOf(aisObject.getSog()) + " kt"); } if (aisObject.getAltitude() != AisObjectConstants.INVALID_ALTITUDE) { - addMenuItem("Altitude", String.valueOf(aisObject.getAltitude())); + addMenuItem("Altitude", String.valueOf(aisObject.getAltitude()) + " m"); } } else { addMenuItem("Callsign", aisObject.getCallSign()); @@ -118,9 +120,9 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, (aisObject.getEtaHour() != AisObjectConstants.INVALID_ETA_HOUR) && (aisObject.getEtaMin() != AisObjectConstants.INVALID_ETA_MIN) && (aisObject.getEtaMon() != AisObjectConstants.INVALID_ETA)) { - String eta = new String(aisObject.getEtaDay() + "." + - aisObject.getEtaMon() + ". " + aisObject.getEtaHour() + ":" + - aisObject.getEtaMin()); + @SuppressLint("DefaultLocale") String eta = new String(aisObject.getEtaDay() + "." + + aisObject.getEtaMon() + ". " + String.format("%02d", aisObject.getEtaHour()) + ":" + + String.format("%02d", aisObject.getEtaMin())); addMenuItem("ETA", eta); // TODO add prepending "0", if needed } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index c1478121a32..653bf147c05 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -59,22 +59,35 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi } private void initTestObjects() { - AisObject ais1 = new AisObject(12345, 1, 20, 120, 120.0, 4.4, - 37.42421d, -122.08381d, 30, 0,0,0,0); - AisObject ais2 = new AisObject(34567, 3, 20, 320, 320.0, 0.4, - 37.42521d, -122.08481d, 36, 0,0,0,0); - AisObject ais3 = new AisObject(34568, 1, 20, 320, 320.0, 0.4, - 50.738d, 7.099d, 70, 20,40,10,0); - AisObject ais4 = new AisObject(12341, 3, 20, 20, 20.0, 0.4, - 50.737d, 7.098d, 60, 0,0,0,0); - - updateAisObjectList(ais1); - updateAisObjectList(ais2); - removeOldestAisObjectListEntry(); - updateAisObjectList(ais2); - updateAisObjectList(ais3); - updateAisObjectList(ais4); - removeLostAisObjects(); + // passenger ship + AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, + 320.0, 8.4, 50.738d, 7.099d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(34568, 5, 0, "TEST-CALLSIGN1", "TEST-Ship", 60 /* passenger */, 56, + 65, 8, 12, 2, + "Potsdam", 8, 15, 22, 5); + updateAisObjectList(ais); + // sailing boat + ais = new AisObject(454011, 1, 20, 8, 0, 120, + 125.0, 4.4, 50.737d, 7.098d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, + 0, 0, 0, 0, + "", 0, 0, 0, 0); + updateAisObjectList(ais); + // land station + ais = new AisObject(878121, 4, 50.736d, 7.100d); + updateAisObjectList(ais); + // AIDS + ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, + 0, 0, 0, 0); + updateAisObjectList(ais); + // aircraft + ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); + updateAisObjectList(ais); + + //removeOldestAisObjectListEntry(); + //removeLostAisObjects(); } private void initTimer() { this.taskCheckAisObjectList = new TimerTask() { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index d693516614e..f380bc17359 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -33,6 +33,7 @@ public class AisTrackerPlugin extends OsmandPlugin { private AisTrackerLayer aisTrackerLayer = null; public static final String COMPONENT = "net.osmand.aistrackerPlugin"; + public static final String AISTRACKER_ID = "osmand.aistracker"; public final CommonPreference AIS_NMEA_PROTOCOL; public static final int AIS_NMEA_PROTOCOL_UDP = 0; public static final int AIS_NMEA_PROTOCOL_TCP = 1; @@ -98,7 +99,7 @@ public List getRendererNames() { @Override public String getId() { - return "osmand.aistracker"; + return AISTRACKER_ID; } @Nullable From b881cae28f44e2b387256653117acc4362843c15 Mon Sep 17 00:00:00 2001 From: Falk Date: Mon, 17 Jun 2024 23:37:48 +0200 Subject: [PATCH 04/74] added syntax check for IP address and port number in settings dialog --- .../aistracker/AisMessageListener.java | 23 ++++---- .../aistracker/AisObjectMenuController.java | 1 - .../plugins/aistracker/AisTrackerLayer.java | 2 +- .../plugins/aistracker/AisTrackerPlugin.java | 14 +++-- .../AisTrackerSettingsFragment.java | 56 ++++++++++++++++++- 5 files changed, 76 insertions(+), 20 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java index c9a33adf3ee..121475de45b 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -112,15 +112,17 @@ private void initListeners() throws IOException { } } private void removeListeners() { - sentenceReader.stop(); - while (!this.listenerList.isEmpty()) { - SentenceListener listener; - try { - listener = this.listenerList.pop(); - sentenceReader.removeSentenceListener(listener); - Log.d("AisMessageListener", "SentenceListener removed"); - } catch (EmptyStackException e) { - Log.e("AisMessageListener", "stack empty"); + if (sentenceReader != null) { + sentenceReader.stop(); + while (!this.listenerList.isEmpty()) { + SentenceListener listener; + try { + listener = this.listenerList.pop(); + sentenceReader.removeSentenceListener(listener); + Log.d("AisMessageListener", "SentenceListener removed"); + } catch (EmptyStackException e) { + Log.e("AisMessageListener", "stack empty"); + } } } } @@ -132,7 +134,7 @@ public void stopListener() { } removeListeners(); if (tcpSocket != null) { - Log.d("AisMessageListener","stopListener"); + Log.d("AisMessageListener","stopListener (TCP)"); try { if (tcpSocket.isConnected()) { tcpSocket.close(); @@ -143,6 +145,7 @@ public void stopListener() { } catch (Exception ignore) { } } if (udpSocket != null) { + Log.d("AisMessageListener","stopListener (UDP)"); if (udpSocket.isConnected()) { udpSocket.disconnect(); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index f647af88122..01f7340b14c 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -124,7 +124,6 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, aisObject.getEtaMon() + ". " + String.format("%02d", aisObject.getEtaHour()) + ":" + String.format("%02d", aisObject.getEtaMin())); addMenuItem("ETA", eta); - // TODO add prepending "0", if needed } } if (lastUpdate > 60) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 653bf147c05..fa633c9cd2b 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -55,7 +55,7 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi initTimer(); startNetworkListener(); - //initTestObjects(); // for test purposes: + initTestObjects(); // for test purposes: } private void initTestObjects() { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index f380bc17359..a9b50171de4 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -1,6 +1,5 @@ package net.osmand.plus.plugins.aistracker; -//import static net.osmand.aidlapi.OsmAndCustomizationConstants.PLUGIN_AISTRACKER; import static net.osmand.plus.settings.fragments.SettingsScreenType.AIS_SETTINGS; import android.content.Context; @@ -34,6 +33,11 @@ public class AisTrackerPlugin extends OsmandPlugin { public static final String COMPONENT = "net.osmand.aistrackerPlugin"; public static final String AISTRACKER_ID = "osmand.aistracker"; + + public static final String AIS_NMEA_PROTOCOL_ID = "ais_nmea_protocol"; + public static final String AIS_NMEA_IP_ADDRESS_ID = "ais_address_nmea_server"; + public static final String AIS_NMEA_TCP_PORT_ID = "ais_port_nmea_server"; + public static final String AIS_NMEA_UDP_PORT_ID = "ais_port_nmea_local"; public final CommonPreference AIS_NMEA_PROTOCOL; public static final int AIS_NMEA_PROTOCOL_UDP = 0; public static final int AIS_NMEA_PROTOCOL_TCP = 1; @@ -47,10 +51,10 @@ public class AisTrackerPlugin extends OsmandPlugin { public AisTrackerPlugin(OsmandApplication app) { super(app); /* "ais_nmea_protocol" etc. is a reference to the content of ais_settings.xml */ - AIS_NMEA_PROTOCOL = registerIntPreference("ais_nmea_protocol", AIS_NMEA_PROTOCOL_UDP); - AIS_NMEA_IP_ADDRESS = registerStringPreference("ais_address_nmea_server", AIS_NMEA_DEFAULT_IP); - AIS_NMEA_TCP_PORT = registerIntPreference("ais_port_nmea_server", AIS_NMEA_DEFAULT_TCP_PORT); - AIS_NMEA_UDP_PORT = registerIntPreference("ais_port_nmea_local", AIS_NMEA_DEFAULT_UDP_PORT); + AIS_NMEA_PROTOCOL = registerIntPreference(AIS_NMEA_PROTOCOL_ID, AIS_NMEA_PROTOCOL_UDP); + AIS_NMEA_IP_ADDRESS = registerStringPreference(AIS_NMEA_IP_ADDRESS_ID, AIS_NMEA_DEFAULT_IP); + AIS_NMEA_TCP_PORT = registerIntPreference(AIS_NMEA_TCP_PORT_ID, AIS_NMEA_DEFAULT_TCP_PORT); + AIS_NMEA_UDP_PORT = registerIntPreference(AIS_NMEA_UDP_PORT_ID, AIS_NMEA_DEFAULT_UDP_PORT); Log.d("AisTrackerPlugin", "constructor"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 459bbacb57a..824345ee5ff 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -3,9 +3,13 @@ import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP; +import android.content.Context; import android.os.Bundle; +import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import net.osmand.plus.R; @@ -13,6 +17,11 @@ import net.osmand.plus.settings.fragments.BaseSettingsFragment; import net.osmand.plus.settings.preferences.EditTextPreferenceEx; import net.osmand.plus.settings.preferences.ListPreferenceEx; +import net.osmand.plus.utils.UiUtilities; + +import java.text.MessageFormat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class AisTrackerSettingsFragment extends BaseSettingsFragment { private AisTrackerPlugin plugin; @@ -140,16 +149,57 @@ private void setupUdpPort(int currentProtocol) { } } } - @Override public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_IP_ADDRESS_ID)) { + if (!isValidIpV4Address(newValue.toString())) { + showAlertDialog("Only IPv4 address accepted (\"a.b.c.d\", where a,b,c,d in range 0..255)."); + return false; + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_TCP_PORT_ID) || + preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_UDP_PORT_ID)) { + if (!isValidPortNumber(newValue.toString())) { + showAlertDialog("Only numerical values accepted in range 0..65535."); + return false; + } + } boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); if (layer != null) { - // layer.restartNetworkListeners(); // TEST layer.restartNetworkListener(); } return ret; } + private static boolean isValidIpV4Address(@Nullable String value) { + String pattern0to255 = "(\\d{1,2}|(0|1)\\d{2}|2[0-4]\\d|25[0-5])"; + String patternIpV4 = pattern0to255 + "\\." +pattern0to255 + "\\." + + pattern0to255 + "\\." + pattern0to255; + Pattern p = Pattern.compile(patternIpV4); + if (value == null) { + return false; + } + Matcher m = p.matcher(value); + return m.matches(); + } + private static boolean isValidPortNumber(@Nullable String value) { + int i; + if (value == null) { + return false; + } + try { + i = Integer.parseInt(value); + } catch (NumberFormatException e) { + return false; + } + return (i >= 0) && (i <= 65535); + } + private void showAlertDialog(@NonNull String message) { + Context themedContext = UiUtilities.getThemedContext(getActivity(), isNightMode()); + AlertDialog.Builder wrongFormatDialog = new AlertDialog.Builder(themedContext); + wrongFormatDialog.setTitle(MessageFormat.format(getString(R.string.error_message_pattern), + "Unsupported Data Format")); + wrongFormatDialog.setMessage(message); + wrongFormatDialog.setPositiveButton(R.string.shared_string_ok, (dialog, which) -> dismiss()); + wrongFormatDialog.show(); + } } - From 4fc41994772f4cc8e5f7d0a30c28a15a2b77eea2 Mon Sep 17 00:00:00 2001 From: Falk Date: Wed, 19 Jun 2024 00:18:19 +0200 Subject: [PATCH 05/74] display distance and bearing in context menu --- .../plus/plugins/aistracker/AisObject.java | 50 +++++++++++++++++++ .../aistracker/AisObjectMenuController.java | 18 +++++++ 2 files changed, 68 insertions(+) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 7143414ecec..06506f99d6e 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -38,13 +38,17 @@ import android.graphics.Color; import android.graphics.LightingColorFilter; import android.graphics.Paint; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import net.osmand.Location; import net.osmand.data.LatLon; import net.osmand.data.RotatedTileBox; +import net.osmand.plus.OsmAndLocationProvider; import net.osmand.plus.R; +import net.osmand.util.MapUtils; import java.util.SortedSet; import java.util.TreeSet; @@ -549,6 +553,14 @@ public LatLon getPosition() { return this.ais_position; } @Nullable + public Location getLocation() { + if (this.ais_position != null) { + return new Location(AisTrackerPlugin.AISTRACKER_ID, + ais_position.getLatitude(), ais_position.getLongitude()); + } + return null; + } + @Nullable public String getCallSign() { return this.ais_callSign; } @@ -799,4 +811,42 @@ public String getAidTypeString() { return(Integer.toString(ais_aidType)); } } + private float getDistanceOrBearing(@Nullable OsmAndLocationProvider locationProvider, + boolean needBearing) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + Location aisLocation = getLocation(); + if ((myLocation != null) && (aisLocation != null)) { + return needBearing ? myLocation.bearingTo(aisLocation) : myLocation.distanceTo(aisLocation); + } else { + Log.e("AisObject", "getDistanceOrBearing(): mylocation -> " + myLocation + + ", aisLocation -> " + aisLocation); + return -500.0f; // invalid + } + } else { + Log.e("AisObject", "getDistanceOrBearing(): locationProvider -> null"); + return -500.0f; // invalid + } + } + /* get bearing from own position to the position of the AIS object */ + public float getBearing(@Nullable OsmAndLocationProvider locationProvider) { + float bearing = getDistanceOrBearing(locationProvider, true); + if ((bearing < 0.0f) && (bearing > -200.0f)) { + while (bearing < 0.0f) { + bearing += 360.0f; + } + } + return bearing; + } + /* get distance from own position to the position of the AIS object in meters */ + public float getDistanceInMeters(@Nullable OsmAndLocationProvider locationProvider) { + return getDistanceOrBearing(locationProvider, false); + } + public float getDistanceInNauticalMiles(@Nullable OsmAndLocationProvider locationProvider) { + float dist = getDistanceInMeters(locationProvider); + if (dist >= 0.0f) { + dist = dist / 1852; + } + return dist; + } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 01f7340b14c..6985be5a691 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -10,6 +10,7 @@ import net.osmand.LocationConvert; import net.osmand.data.LatLon; import net.osmand.data.PointDescription; +import net.osmand.plus.OsmandApplication; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.mapcontextmenu.MenuBuilder; import net.osmand.plus.mapcontextmenu.MenuController; @@ -19,10 +20,12 @@ public class AisObjectMenuController extends MenuController { private AisObject aisObject; + private final OsmandApplication app; public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointDescription pointDescription, AisObject aisObject) { super(new MenuBuilder(mapActivity), pointDescription, mapActivity); this.aisObject = aisObject; + this.app = builder.getApplication(); builder.setShowTitleIfTruncated(false); builder.setShowNearestPoi(false); builder.setShowOnlinePhotos(false); @@ -61,6 +64,7 @@ private void addMenuItemDimension() { } } + @SuppressLint("DefaultLocale") @Override public void addPlainMenuItems(String typeStr, PointDescription pointDescription, LatLon latLon) { SortedSet msgTypes = aisObject.getMsgTypes(); @@ -74,6 +78,20 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Location", LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); + if (this.app != null) { + float distance = aisObject.getDistanceInNauticalMiles(app.getLocationProvider()); + float bearing = aisObject.getBearing(app.getLocationProvider()); + if (distance >= 0.0f) { + try { + addMenuItem("Distance", String.format("%.1f nm", distance)); + } catch (Exception ignore) { } + } + if (bearing >= 0.0f) { + try { + addMenuItem("Bearing", String.format("%.1f", bearing)); + } catch (Exception ignore) { } + } + } } if (msgTypes.contains(21)) { // ATON (aid to navigation) addMenuItem("ATON Type", aisObject.getAidTypeString()); From 57b9064bbc9cd4ca167aacb7c19bcf1c1163de9f Mon Sep 17 00:00:00 2001 From: Falk Date: Fri, 21 Jun 2024 22:54:39 +0200 Subject: [PATCH 06/74] add 2 settings: AIS_OBJ_LOST_TIMEOUT and AIS_SHIP_LOST_TIMEOUT --- OsmAnd/res/values/strings.xml | 5 + OsmAnd/res/xml/ais_settings.xml | 12 +++ .../plus/plugins/aistracker/AisObject.java | 40 ++++---- .../aistracker/AisObjectConstants.java | 4 - .../aistracker/AisObjectMenuController.java | 2 +- .../plugins/aistracker/AisTrackerLayer.java | 8 +- .../plugins/aistracker/AisTrackerPlugin.java | 18 +++- .../AisTrackerSettingsFragment.java | 98 +++++++------------ 8 files changed, 97 insertions(+), 90 deletions(-) diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index 0bf7b77c54a..adfdd50c998 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -782,6 +782,11 @@ You need to activate the sensor so OsmAnd can find it. Define TCP port number of the NMEA data source UDP port of local NMEA data receiver Define UPD port where OsmAnd receives NMEA data + Timeout for visibility when object is lost + Set Timeout for visibility of AIS objects: After this time without signal reception, the AIS object will be removed from screen. + Timeout for ship visibility when no signal received + Set timeout for ship visibility: After this time without signal reception, the ship symbol will change its state on screen: It will be crossed out. + Weather Explore Weather forecast. Contours diff --git a/OsmAnd/res/xml/ais_settings.xml b/OsmAnd/res/xml/ais_settings.xml index c3af0a4620b..ebc1d52da1f 100644 --- a/OsmAnd/res/xml/ais_settings.xml +++ b/OsmAnd/res/xml/ais_settings.xml @@ -33,4 +33,16 @@ android:title="@string/ais_port_nmea_local" tools:summary="@string/ais_port_nmea_local_description" /> + + + + \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 06506f99d6e..bbba2314f91 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -30,8 +30,6 @@ import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.maxAgeInMinutes; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.maxVesselAgeInMinutes; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -368,8 +366,8 @@ public void set(@NonNull AisObject ais) { this.bitmapColor = 0; } - private void setBitmap(@NonNull AisTrackerLayer mapLayer) { - if (isLost()) { + private void setBitmap(@NonNull AisTrackerLayer mapLayer, int maxAgeInMin) { + if (isLost(maxAgeInMin)) { if (isMovable()) { this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); } @@ -401,11 +399,11 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { break; } } - this.setColor(); + this.setColor(maxAgeInMin); } - private void setColor() { - if (isLost()) { + private void setColor(int maxAgeInMin) { + if (isLost(maxAgeInMin)) { if (isMovable()) { this.bitmapColor = 0; // black } @@ -436,9 +434,10 @@ private void setColor() { } public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, - @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { - if ((this.bitmap == null) || isLost()) { - this.setBitmap(mapLayer); + @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox, + int maxAgeInMin) { + if ((this.bitmap == null) || isLost(maxAgeInMin)) { + this.setBitmap(mapLayer, maxAgeInMin); } if (this.bitmapColor != 0) { paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); @@ -460,7 +459,7 @@ public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, canvas.rotate(rotation, locationX, locationY); } canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); - if ((speedFactor > 0) && (!isLost())) { + if ((speedFactor > 0) && (!isLost(maxAgeInMin))) { float lineStartX = locationX; float lineLength = (float)this.bitmap.getHeight() * speedFactor; float lineStartY = locationY - this.bitmap.getHeight() / 4.0f; @@ -511,18 +510,15 @@ private boolean needRotation() { return false; } - private boolean isLost(long maxAgeInMin) { + private boolean isLost(int maxAgeInMin) { return ((System.currentTimeMillis() - this.lastUpdate) / 1000 / 60) > maxAgeInMin; } - private boolean isLost() { - return isLost(maxVesselAgeInMinutes); - } /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed * */ - public boolean checkObjectAge() { + public boolean checkObjectAge(int maxAgeInMinutes) { return isLost(maxAgeInMinutes); } public int getMsgType() { return this.ais_msgType; } @@ -555,8 +551,18 @@ public LatLon getPosition() { @Nullable public Location getLocation() { if (this.ais_position != null) { - return new Location(AisTrackerPlugin.AISTRACKER_ID, + Location loc = new Location(AisTrackerPlugin.AISTRACKER_ID, ais_position.getLatitude(), ais_position.getLongitude()); + if (ais_cog != INVALID_COG) { + loc.setBearing((float)ais_cog); + } + if (ais_sog != INVALID_SOG) { + loc.setSpeed((float)ais_sog); + } + if (ais_altitude != INVALID_ALTITUDE) { + loc.setAltitude((float)ais_altitude); + } + return loc; } return null; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index d7a84cb3e09..b965b38c1db 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -4,10 +4,6 @@ import java.util.Map; public final class AisObjectConstants { - /* after this time the object is outdated and can be removed: */ - public final static long maxAgeInMinutes = 7; - /* after this time the (movable) object is lost, the bitmap can be changed: */ - public final static long maxVesselAgeInMinutes = 4; public final static int INVALID_HEADING = 511; public final static int INVALID_NAV_STATUS = 15; public final static int INVALID_MANEUVER_INDICATOR = 0; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 6985be5a691..32a141c5e0a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -75,7 +75,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("MMSI", Integer.toString(aisObject.getMmsi())); if (position != null) { - addMenuItem("Location", + addMenuItem("Position", LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index fa633c9cd2b..6e02da6dd43 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -131,14 +131,15 @@ public void cleanup() { stopNetworkListener(); } private void removeLostAisObjects() { + int maxAge = plugin.AIS_OBJ_LOST_TIMEOUT.get(); for (Iterator> iterator = aisObjectList.entrySet().iterator(); iterator.hasNext(); ) { Map.Entry entry = iterator.next(); - if (entry.getValue().checkObjectAge()) { + if (entry.getValue().checkObjectAge(maxAge)) { Log.d("AisTrackerLayer", "remove AIS object with MMSI " + entry.getValue().getMmsi()); iterator.remove(); } } - // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge()); + // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge(maxAge)); } private void removeOldestAisObjectListEntry() { Log.d("AisTrackerLayer", "removeOldestAisObjectListEntry() called"); @@ -189,9 +190,10 @@ public boolean isLocationVisible(RotatedTileBox tileBox, LatLon coordinates) { @Override public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + int maxAgeInMin = plugin.AIS_SHIP_LOST_TIMEOUT.get(); for (AisObject ais : aisObjectList.values()) { if (isLocationVisible(tileBox, ais.getPosition())) { - ais.draw(this, bitmapPaint, canvas, tileBox); + ais.draw(this, bitmapPaint, canvas, tileBox, maxAgeInMin); } } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index a9b50171de4..2d92fe4c4cb 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -34,10 +34,12 @@ public class AisTrackerPlugin extends OsmandPlugin { public static final String COMPONENT = "net.osmand.aistrackerPlugin"; public static final String AISTRACKER_ID = "osmand.aistracker"; - public static final String AIS_NMEA_PROTOCOL_ID = "ais_nmea_protocol"; - public static final String AIS_NMEA_IP_ADDRESS_ID = "ais_address_nmea_server"; - public static final String AIS_NMEA_TCP_PORT_ID = "ais_port_nmea_server"; - public static final String AIS_NMEA_UDP_PORT_ID = "ais_port_nmea_local"; + public static final String AIS_NMEA_PROTOCOL_ID = "ais_nmea_protocol"; // see xml/ais_settings.xml + public static final String AIS_NMEA_IP_ADDRESS_ID = "ais_address_nmea_server"; // see xml/ais_settings.xml + public static final String AIS_NMEA_TCP_PORT_ID = "ais_port_nmea_server"; // see xml/ais_settings.xml + public static final String AIS_NMEA_UDP_PORT_ID = "ais_port_nmea_local"; // see xml/ais_settings.xml + public static final String AIS_OBJ_LOST_TIMEOUT_ID = "ais_object_lost_timeout"; // see xml/ais_settings.xml + public static final String AIS_SHIP_LOST_TIMEOUT_ID = "ais_ship_lost_timeout"; // see xml/ais_settings.xml public final CommonPreference AIS_NMEA_PROTOCOL; public static final int AIS_NMEA_PROTOCOL_UDP = 0; public static final int AIS_NMEA_PROTOCOL_TCP = 1; @@ -47,14 +49,20 @@ public class AisTrackerPlugin extends OsmandPlugin { public static final Integer AIS_NMEA_DEFAULT_TCP_PORT = 4001; public final CommonPreference AIS_NMEA_UDP_PORT; public static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; + public final CommonPreference AIS_OBJ_LOST_TIMEOUT; + public static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; + public final CommonPreference AIS_SHIP_LOST_TIMEOUT; + public static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; public AisTrackerPlugin(OsmandApplication app) { super(app); - /* "ais_nmea_protocol" etc. is a reference to the content of ais_settings.xml */ + /* "ais_nmea_protocol" etc. is a reference to the content of xml/ais_settings.xml */ AIS_NMEA_PROTOCOL = registerIntPreference(AIS_NMEA_PROTOCOL_ID, AIS_NMEA_PROTOCOL_UDP); AIS_NMEA_IP_ADDRESS = registerStringPreference(AIS_NMEA_IP_ADDRESS_ID, AIS_NMEA_DEFAULT_IP); AIS_NMEA_TCP_PORT = registerIntPreference(AIS_NMEA_TCP_PORT_ID, AIS_NMEA_DEFAULT_TCP_PORT); AIS_NMEA_UDP_PORT = registerIntPreference(AIS_NMEA_UDP_PORT_ID, AIS_NMEA_DEFAULT_UDP_PORT); + AIS_OBJ_LOST_TIMEOUT = registerIntPreference(AIS_OBJ_LOST_TIMEOUT_ID, AIS_OBJ_LOST_DEFAULT_TIMEOUT); + AIS_SHIP_LOST_TIMEOUT = registerIntPreference(AIS_SHIP_LOST_TIMEOUT_ID, AIS_SHIP_LOST_DEFAULT_TIMEOUT); Log.d("AisTrackerPlugin", "constructor"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 824345ee5ff..d67e51ae6c6 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -5,6 +5,7 @@ import android.content.Context; import android.os.Bundle; +import android.text.SpannableStringBuilder; import android.util.Log; import androidx.annotation.NonNull; @@ -39,6 +40,8 @@ protected void setupPreferences() { setupIpAddress(currentProtocol); setupTcpPort(currentProtocol); setupUdpPort(currentProtocol); + setupObjectLostTimeout(); + setupShipLostTimeout(); } private int setupProtocol() { @@ -56,46 +59,6 @@ private int setupProtocol() { } private void setupIpAddress(int currentProtocol) { - /* - InputFilter[] filters = new InputFilter[1]; - filters[0] = new InputFilter() { - @Override - public CharSequence filter(CharSequence source, int start, int end, - android.text.Spanned dest, int dstart, int dend) { - if (end > start) { - String destTxt = dest.toString(); - String resultingTxt = destTxt.substring(0, dstart) - + source.subSequence(start, end) - + destTxt.substring(dend); - if (!resultingTxt - .matches("^\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3})?)?)?)?)?)?")) { - return ""; - } else { - String[] splits = resultingTxt.split("\\."); - for (int i = 0; i < splits.length; i++) { - if (Integer.valueOf(splits[i]) > 255) { - return ""; - } - } - } - } - return null; - } - }; - */ - //EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); - //Log.d("AisTrackerSettingsFragment","## findPreference()"); - //aisNmeaIpAddress.setOnBindEditTextListener(new androidx.preference.EditTextPreference.OnBindEditTextListener() { - /*aisNmeaIpAddress.setOnBindEditTextListener(new OnBindEditTextListener() { - @Override - public void onBindEditText(@NonNull EditText editText) { - editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); - editText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(10)}); - Log.d("AisTrackerSettingsFragment","## onBindEditText()"); - } - }); - */ - EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); if (aisNmeaIpAddress != null) { aisNmeaIpAddress.setDescription(R.string.ais_address_nmea_server_description); @@ -105,28 +68,10 @@ public void onBindEditText(@NonNull EditText editText) { aisNmeaIpAddress.setEnabled(true); } } + // TODO: the current value is not shown in the settings overview dialog(?) } private void setupTcpPort(int currentProtocol) { - /* EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); - if (aisNmeaPort != null) { - Log.d("AisTrackerSettingsFragment","## setupTcpPort()"); - aisNmeaPort.setOnBindEditTextListener(new OnBindEditTextListener() { - @Override - public void onBindEditText(@NonNull EditText editText) { - Log.d("AisTrackerSettingsFragment","## onBindEditText()"); - editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); - } - }); - aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); - if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { - aisNmeaPort.setEnabled(false); - } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { - aisNmeaPort.setEnabled(true); - } - } - */ - EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); if (aisNmeaPort != null) { aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); @@ -136,6 +81,7 @@ public void onBindEditText(@NonNull EditText editText) { aisNmeaPort.setEnabled(true); } } + // TODO: the current value is not shown in the settings overview dialog(?) } private void setupUdpPort(int currentProtocol) { @@ -148,24 +94,56 @@ private void setupUdpPort(int currentProtocol) { aisNmeaPort.setEnabled(false); } } + // TODO: the current value is not shown in the settings overview dialog(?) + } + private void setupObjectLostTimeout() { + Integer[] entryValues = {3, 5, 7, 10, 12, 15, 20}; + String[] entries = new String[entryValues.length]; + for (int i = 0; i < entryValues.length; i++) { + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to ressource file + } + ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_OBJ_LOST_TIMEOUT.getId()); + if (objectLostTimeout != null) { + objectLostTimeout.setEntries(entries); + objectLostTimeout.setEntryValues(entryValues); + objectLostTimeout.setDescription(R.string.ais_object_lost_timeout_description); + } + } + private void setupShipLostTimeout() { + Integer[] entryValues = {2, 3, 4, 5, 7, 10, 15, 100 /* disabled: must be bigger than the biggest value of setupObjectLostTimeout() */}; + String[] entries = new String[entryValues.length]; + for (int i = 0; i < entryValues.length - 1; i++) { + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to ressource file + } + entries[entryValues.length - 1] = "disabled"; // TODO: move to ressource file + + ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_SHIP_LOST_TIMEOUT.getId()); + if (objectLostTimeout != null) { + objectLostTimeout.setEntries(entries); + objectLostTimeout.setEntryValues(entryValues); + objectLostTimeout.setDescription(R.string.ais_ship_lost_timeout_description); + } } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean restartNetworkListener = false; if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_IP_ADDRESS_ID)) { if (!isValidIpV4Address(newValue.toString())) { showAlertDialog("Only IPv4 address accepted (\"a.b.c.d\", where a,b,c,d in range 0..255)."); return false; } + restartNetworkListener = true; } else if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_TCP_PORT_ID) || preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_UDP_PORT_ID)) { if (!isValidPortNumber(newValue.toString())) { showAlertDialog("Only numerical values accepted in range 0..65535."); return false; } + restartNetworkListener = true; } boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); - if (layer != null) { + if ((layer != null) && (restartNetworkListener)) { layer.restartNetworkListener(); } return ret; From 705bbd4c493ca746b17f35366027aef859c0371e Mon Sep 17 00:00:00 2001 From: Falk Date: Fri, 21 Jun 2024 23:22:52 +0200 Subject: [PATCH 07/74] ship destination consisting of "@" is considered as invalid --- .../src/net/osmand/plus/plugins/aistracker/AisObject.java | 6 +++++- .../net/osmand/plus/plugins/aistracker/AisTrackerLayer.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index bbba2314f91..b7f9e03c210 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -143,7 +143,11 @@ public AisObject(int mmsi, int msgType, int imo, @Nullable String callSign, @Nul this.ais_draught = draught; this.ais_callSign = callSign; this.ais_shipName = shipName; - this.ais_destination = destination; + if (destination != null) { + if (!destination.matches("^@+$")) { // string consisting of only "@" characters is invalid + this.ais_destination = destination; + } + } this.ais_etaMon = etaMon; this.ais_etaDay = etaDay; this.ais_etaHour = etaHour; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 6e02da6dd43..5ea4d3d3679 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -73,7 +73,7 @@ private void initTestObjects() { updateAisObjectList(ais); ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, 0, 0, 0, 0, - "", 0, 0, 0, 0); + "@@@", 0, 0, 0, 0); updateAisObjectList(ais); // land station ais = new AisObject(878121, 4, 50.736d, 7.100d); From 4afa720ebd820b1ad3609303734a18497c724136 Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 22 Jun 2024 18:56:52 +0200 Subject: [PATCH 08/74] improved preference setup dialog for network setting: show current values --- .../aistracker/AisTrackerSettingsFragment.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index d67e51ae6c6..152f3a92082 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -61,40 +61,42 @@ private int setupProtocol() { private void setupIpAddress(int currentProtocol) { EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); if (aisNmeaIpAddress != null) { + String currentValue = plugin.AIS_NMEA_IP_ADDRESS.get(); + if (currentValue == null) { currentValue = ""; } aisNmeaIpAddress.setDescription(R.string.ais_address_nmea_server_description); + aisNmeaIpAddress.setSummary(currentValue); if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { aisNmeaIpAddress.setEnabled(false); } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { aisNmeaIpAddress.setEnabled(true); } } - // TODO: the current value is not shown in the settings overview dialog(?) } - private void setupTcpPort(int currentProtocol) { EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); if (aisNmeaPort != null) { + int currentValue = plugin.AIS_NMEA_TCP_PORT.get(); aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); + aisNmeaPort.setSummary(String.valueOf(currentValue)); if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { aisNmeaPort.setEnabled(false); } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { aisNmeaPort.setEnabled(true); } } - // TODO: the current value is not shown in the settings overview dialog(?) } - private void setupUdpPort(int currentProtocol) { EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_UDP_PORT.getId()); if (aisNmeaPort != null) { + int currentValue = plugin.AIS_NMEA_UDP_PORT.get(); aisNmeaPort.setDescription(R.string.ais_port_nmea_local_description); + aisNmeaPort.setSummary(String.valueOf(currentValue)); if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { aisNmeaPort.setEnabled(true); } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { aisNmeaPort.setEnabled(false); } } - // TODO: the current value is not shown in the settings overview dialog(?) } private void setupObjectLostTimeout() { Integer[] entryValues = {3, 5, 7, 10, 12, 15, 20}; From 32ebd906288ed586c420b5e1114cb06b82b0773a Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 25 Jun 2024 22:35:42 +0200 Subject: [PATCH 09/74] new class to calculate CPA nd TCPA (not included into GUI yet) --- .../plus/plugins/aistracker/AisObject.java | 2 +- .../aistracker/AisObjectConstants.java | 5 +- .../aistracker/AisObjectMenuController.java | 67 +++++++- .../plugins/aistracker/AisTrackerHelper.java | 159 ++++++++++++++++++ 4 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index b7f9e03c210..5b89d6619c3 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -561,7 +561,7 @@ public Location getLocation() { loc.setBearing((float)ais_cog); } if (ais_sog != INVALID_SOG) { - loc.setSpeed((float)ais_sog); + loc.setSpeed((float)(ais_sog * 3600 / 1852)); // in m/s } if (ais_altitude != INVALID_ALTITUDE) { loc.setAltitude((float)ais_altitude); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index b965b38c1db..c272664d567 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -20,8 +20,11 @@ public final class AisObjectConstants { public final static double INVALID_LON = 181.0; public final static double INVALID_ROT = 128.0; public final static double INVALID_DRAUGHT = 0.0; + public final static double INVALID_TCPA = -10000.0d; + public final static float INVALID_CPA = -1.0f; - public enum AisObjType { + + public static enum AisObjType { AIS_VESSEL, AIS_VESSEL_SPORT, AIS_VESSEL_FAST, diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 32a141c5e0a..f0b5d98dee8 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -7,9 +7,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import net.osmand.Location; import net.osmand.LocationConvert; import net.osmand.data.LatLon; import net.osmand.data.PointDescription; +import net.osmand.plus.OsmAndLocationProvider; import net.osmand.plus.OsmandApplication; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.mapcontextmenu.MenuBuilder; @@ -32,6 +34,39 @@ public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointD builder.setShowNearestWiki(false); // TODO: show an icon in the menu } + private float getOwnSpeed(@Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + if (myLocation.hasSpeed()) { + return myLocation.getSpeed(); + } + } + } + return 0.0f; + } + private float getOwnBearing(@Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + if (myLocation.hasBearing()) { + return myLocation.getBearing(); + } + } + } + return 0.0f; + } + /* + private String getOwnLocationAsString(@Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + return myLocation.toString(); + } + } + return null; + } + */ private void addMenuItem(@NonNull String type, @Nullable String value) { if (value != null) { @@ -79,8 +114,9 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { - float distance = aisObject.getDistanceInNauticalMiles(app.getLocationProvider()); - float bearing = aisObject.getBearing(app.getLocationProvider()); + OsmAndLocationProvider locationProvider = app.getLocationProvider(); + float distance = aisObject.getDistanceInNauticalMiles(locationProvider); + float bearing = aisObject.getBearing(locationProvider); if (distance >= 0.0f) { try { addMenuItem("Distance", String.format("%.1f nm", distance)); @@ -91,6 +127,12 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Bearing", String.format("%.1f", bearing)); } catch (Exception ignore) { } } + /* + // test: + addMenuItem("# loc", getOwnLocationAsString(locationProvider)); + addMenuItem("# ownSpeed", Float.toString(getOwnSpeed(locationProvider))); + addMenuItem("# ownBearing", Float.toString(getOwnBearing(locationProvider))); + */ } } if (msgTypes.contains(21)) { // ATON (aid to navigation) @@ -175,7 +217,26 @@ protected Object getObject() { @NonNull @Override - public String getTypeStr() { return "AIS object"; } + public String getTypeStr() { + String res = ""; + SortedSet msgTypes = aisObject.getMsgTypes(); + for (Integer i : new Integer[]{5, 19, 24}) { + if (msgTypes.contains(i)) { + res += aisObject.getShipTypeString(); + break; + } + } + for (Integer i : new Integer[]{1, 2, 3}) { + if (msgTypes.contains(i)) { + if (res.isEmpty()) { + res = "Vessel"; + } + res += ": " + aisObject.getNavStatusString() + "."; + break; + } + } + return (res.isEmpty() ? "AIS object" : res); + } @Override public boolean needStreetName() { return false; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java new file mode 100644 index 00000000000..bb10c9a4367 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -0,0 +1,159 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_CPA; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_TCPA; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.Location; +import net.osmand.plus.OsmAndLocationProvider; + +public final class AisTrackerHelper { + + private static class Vector { + public double x; + public double y; + public Vector(double a, double b) { + this.x = a; + this.y = b; + } + public Vector(@NonNull Vector a) { + this.x = a.x; + this.y = a.y; + } + @NonNull + public Vector multiply(double a) { + return new Vector(this.x * a, this.y * a); + } + @NonNull + public Vector add(@NonNull Vector a) { + return new Vector(this.x + a.x, this.y + a.y); + } + } + + /* calculate the Time to Closest Point of Approach (TCPA) of two moving objects: + * object 1 at position x and velocity vector vx + * object 2 at position y and velocity vectoy vy, + * For the calculation, cartesian ccordinates are assumed with a cartesian distance metricx + * -> attention: by using sherical coordinates, this will produce an error! */ + private double getTcpa(@NonNull Vector x, @NonNull Vector y, @NonNull Vector vx, @NonNull Vector vy) { + Vector dx = new Vector(y.x - x.x, y.y - x.y); + Vector dv = new Vector(vy.x - vx.x, vy.y - vx.y); + return -(((dx.x * dv.x) + (dx.y * dv.y)) / ((dv.x * dv.x) + (dv.y * dv.y))); // TODO: check for Div/0 + } + + /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and y, + * it is presumed that x and y both contain their position, speed and course */ + public double getTcpa(@NonNull Location x, @NonNull Location y) { + if (checkSpeedAndBearing(x, y)) { + return INVALID_TCPA; + } + Vector vx = courseToVector(x.getBearing(), getSpeedInNodes(x)); + Vector vy = courseToVector(y.getBearing(), getSpeedInNodes(y)); + return getTcpa(locationToVector(x), locationToVector(y), vx, vy); + } + + /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and own location, + * it is presumed that x contains its position, speed and course */ + public double getTcpa(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + return getTcpa(x, myLocation); + } + } + return INVALID_TCPA; + } + @Nullable + private Location getCpa(@NonNull Location x, @NonNull Location y, boolean useFirstAsReference) { + if (checkSpeedAndBearing(x, y)) { + return null; + } + double tcpa = getTcpa(x,y); + Location base = useFirstAsReference ? x : y; + Vector v = courseToVector(base.getBearing(), getSpeedInNodes(base)); + Vector newPos = getNewPosition(locationToVector(base), v, tcpa); + Location newX = new Location(base); + newX.setLongitude(newPos.x); + newX.setLatitude(newPos.y); + return newX; + } + + /* to calculate the Closest Point of Approach (CPA) between the objects x and y, + * it is presumed that x and y both contain their position, speed and course. + * This function returns the position of first object x at time of TCPA */ + @Nullable + public Location getCpa1(@NonNull Location x, @NonNull Location y) { + return getCpa(x, y, true); + } + /* to calculate the Closest Point of Approach (CPA) between the objects x and y, + * it is presumed that x and y both contain their position, speed and course. + * This function returns the position of second object y at time of TCPA */ + @Nullable + public Location getCpa2(@NonNull Location x, @NonNull Location y) { + return getCpa(x, y, false); + } + + /* caluclate the distance between the given objects at their Closest Point of Approach (CPA)*/ + public float getCpaDistance(@NonNull Location x, @NonNull Location y) { + Location cpaX = getCpa1(x,y); + Location cpaY = getCpa2(x,y); + if ((cpaX != null) && (cpaY != null)) { + return cpaX.distanceTo(cpaY); + } else { + return INVALID_CPA; + } + } + + /* caluclate the distance between the given object and own position at their Closest Point of Approach (CPA)*/ + public float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + return getCpaDistance(x, myLocation); + } + } + return INVALID_CPA; + } + + /* calculate the new position of a moving object with coordinates x and velocity vector v + after the given time, + * -> attention: by using sherical coordinates, this will produce an error! */ + @NonNull + private Vector getNewPosition(@NonNull Vector x, @NonNull Vector v, double time) { + return new Vector(x.add(v.multiply(time))); + } + + /* calculate a velocity vector from givem course (COG) and speed (SOG). + COG is given as heading, SOG as scalar */ + @NonNull + private Vector courseToVector(double cog, double sog) { + double alpha = cog + 90.0d; + while (alpha < 0) { alpha += 360.0d; } + while (alpha > 360.0d ) { alpha -= 360.0d; } + alpha = Math.toRadians(alpha); + return new Vector(Math.sin(alpha) * sog, Math.cos(alpha) * sog); + } + + @NonNull + private Vector locationToVector(@NonNull Location loc) { + return new Vector(loc.getLongitude(), loc.getLatitude()); + } + private boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { + if (!x.hasBearing() || !y.hasBearing() || !x.hasSpeed() || !y.hasSpeed()) { + Log.d("AisTrackerHelper", "some input data is missing: x.hasBearing->" + + x.hasBearing() + ", y.hasBearing->" + y.hasBearing() + ", x.hasSpeed->" + + x.hasSpeed() + ", y.hasSpeed" + y.hasSpeed()); + return true; + } else { + return false; + } + } + + private float getSpeedInNodes(@NonNull Location loc) { + return loc.getSpeed() * 1852 / 3600; + } +} From 9b95c9d700ebf99b3900010d3a10df4fc68c3ede Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 30 Jun 2024 17:36:34 +0200 Subject: [PATCH 10/74] update of CPA class after some testing (not included into GUI yet) --- .../aistracker/AisMessageListener.java | 5 + .../plus/plugins/aistracker/AisObject.java | 2 +- .../plugins/aistracker/AisTrackerHelper.java | 199 ++++++++++++++---- .../plugins/aistracker/AisTrackerLayer.java | 133 ++++++++++++ 4 files changed, 292 insertions(+), 47 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java index 121475de45b..49e5d532e9e 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -18,6 +18,7 @@ import net.sf.marineapi.ais.message.AISMessage27; import net.sf.marineapi.nmea.event.SentenceListener; import net.sf.marineapi.nmea.io.SentenceReader; +import net.sf.marineapi.nmea.sentence.SentenceId; import java.io.IOException; import java.io.InputStream; @@ -391,6 +392,10 @@ private void handleAisMessage(int aisType, Object obj) { } private void initEmbeddedLister(int aisType, @NonNull SentenceListener listener) { AisMessageListener.this.sentenceReader.addSentenceListener(listener); + /* + AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDM); + AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDO); + */ AisMessageListener.this.listenerList.push(listener); Log.d("AisMessageListener","Listener Type " + aisType + " started"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 5b89d6619c3..a8af8913ee7 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -561,7 +561,7 @@ public Location getLocation() { loc.setBearing((float)ais_cog); } if (ais_sog != INVALID_SOG) { - loc.setSpeed((float)(ais_sog * 3600 / 1852)); // in m/s + loc.setSpeed((float)(ais_sog * 1852 / 3600)); // in m/s } if (ais_altitude != INVALID_ALTITUDE) { loc.setAltitude((float)ais_altitude); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index bb10c9a4367..03fa6e36725 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -8,14 +8,18 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.jwetherell.openmap.common.LatLonPoint; + import net.osmand.Location; import net.osmand.plus.OsmAndLocationProvider; public final class AisTrackerHelper { - + private static long lastCorrectionUpdate = 0; + private static double correctionFactor = 1.0d; + private static final long maxCorrectionUpdateAgeInMin = 60; private static class Vector { - public double x; - public double y; + public double x; // Latitude (grows in North direction) + public double y; // Longitude (grows in East direction) public Vector(double a, double b) { this.x = a; this.y = b; @@ -25,13 +29,33 @@ public Vector(@NonNull Vector a) { this.y = a.y; } @NonNull - public Vector multiply(double a) { - return new Vector(this.x * a, this.y * a); + public Vector sub(@NonNull Vector a) { + return new Vector(this.x - a.x, this.y - a.y); } - @NonNull - public Vector add(@NonNull Vector a) { - return new Vector(this.x + a.x, this.y + a.y); + public double dot(@NonNull Vector a) { return (this.x * a.x) + (this.y * a.y); } + } + public static class Cpa { + private double tcpa; // in hours + private float cpaDist; // in miles + private Location newPos1; // position of first object at time tcpa + private Location newPos2; // position of first object at time tcpa + public Cpa() { + reset(); + } + public void reset() { + cpaDist = INVALID_CPA; + tcpa = INVALID_TCPA; + newPos1 = null; + newPos2 = null; } + public void setTcpa(double x) { this.tcpa = x; } + public void setCpaDist(float x) { this.cpaDist = x; } + public void setCpaPos1(Location loc) { this.newPos1 = loc; } + public void setCpaPos2(Location loc) { this.newPos2 = loc; } + public double getTcpa() { return tcpa; } + public float getCpaDist() { return cpaDist; } + public Location getCpaPos1() { return newPos1; } + public Location getCpaPos2() { return newPos2; } } /* calculate the Time to Closest Point of Approach (TCPA) of two moving objects: @@ -39,77 +63,89 @@ public Vector add(@NonNull Vector a) { * object 2 at position y and velocity vectoy vy, * For the calculation, cartesian ccordinates are assumed with a cartesian distance metricx * -> attention: by using sherical coordinates, this will produce an error! */ - private double getTcpa(@NonNull Vector x, @NonNull Vector y, @NonNull Vector vx, @NonNull Vector vy) { - Vector dx = new Vector(y.x - x.x, y.y - x.y); - Vector dv = new Vector(vy.x - vx.x, vy.y - vx.y); - return -(((dx.x * dv.x) + (dx.y * dv.y)) / ((dv.x * dv.x) + (dv.y * dv.y))); // TODO: check for Div/0 + private static double getTcpa(@NonNull Vector x, @NonNull Vector y, + @NonNull Vector vx, @NonNull Vector vy, double lonCorrection) { + Vector dx = new Vector( y.sub(x)); + Vector dv = new Vector(vy.sub(vx)); + double divisor = dv.dot(dv); // TODO: check for Div/0 + return -(((dx.x * dv.x) + (dx.y * dv.y / lonCorrection)) / divisor); // TODO: check for Div/0 } /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and y, * it is presumed that x and y both contain their position, speed and course */ - public double getTcpa(@NonNull Location x, @NonNull Location y) { + private static double getTcpa(@NonNull Location x, @NonNull Location y, double lonCorrection) { if (checkSpeedAndBearing(x, y)) { return INVALID_TCPA; } - Vector vx = courseToVector(x.getBearing(), getSpeedInNodes(x)); - Vector vy = courseToVector(y.getBearing(), getSpeedInNodes(y)); - return getTcpa(locationToVector(x), locationToVector(y), vx, vy); + if (lonCorrection < 0.001) { + // in this case the lonCorrection is considered invalid -> new calculation + lonCorrection = getLonCorrection(x); + } + return getTcpa(locationToVector(x), locationToVector(y), + courseToVector(x.getBearing(), getSpeedInKnots(x)), + courseToVector(y.getBearing(), getSpeedInKnots(y)), lonCorrection); + } + + public static double getTcpa(@NonNull Location x, @NonNull Location y) { + return getTcpa(x, y, 0.0d); } /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and own location, * it is presumed that x contains its position, speed and course */ - public double getTcpa(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { + public static double getTcpa(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { if (locationProvider != null) { Location myLocation = locationProvider.getLastKnownLocation(); if (myLocation != null) { - return getTcpa(x, myLocation); + long now = System.currentTimeMillis(); + if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { + lastCorrectionUpdate = now; + correctionFactor = getLonCorrection(myLocation); + } + return getTcpa(x, myLocation, correctionFactor); } } return INVALID_TCPA; } + @Nullable - private Location getCpa(@NonNull Location x, @NonNull Location y, boolean useFirstAsReference) { + private static Location getCpa(@NonNull Location x, @NonNull Location y, boolean useFirstAsReference) { if (checkSpeedAndBearing(x, y)) { return null; } double tcpa = getTcpa(x,y); Location base = useFirstAsReference ? x : y; - Vector v = courseToVector(base.getBearing(), getSpeedInNodes(base)); - Vector newPos = getNewPosition(locationToVector(base), v, tcpa); - Location newX = new Location(base); - newX.setLongitude(newPos.x); - newX.setLatitude(newPos.y); - return newX; + return getNewPosition(base, tcpa); } /* to calculate the Closest Point of Approach (CPA) between the objects x and y, * it is presumed that x and y both contain their position, speed and course. * This function returns the position of first object x at time of TCPA */ @Nullable - public Location getCpa1(@NonNull Location x, @NonNull Location y) { + public static Location getCpa1(@NonNull Location x, @NonNull Location y) { return getCpa(x, y, true); } + /* to calculate the Closest Point of Approach (CPA) between the objects x and y, * it is presumed that x and y both contain their position, speed and course. * This function returns the position of second object y at time of TCPA */ @Nullable - public Location getCpa2(@NonNull Location x, @NonNull Location y) { + public static Location getCpa2(@NonNull Location x, @NonNull Location y) { return getCpa(x, y, false); } - /* caluclate the distance between the given objects at their Closest Point of Approach (CPA)*/ - public float getCpaDistance(@NonNull Location x, @NonNull Location y) { + /* caluclate the distance between the given objects at their Closest Point of Approach (CPA) */ + public static float getCpaDistance(@NonNull Location x, @NonNull Location y) { Location cpaX = getCpa1(x,y); Location cpaY = getCpa2(x,y); if ((cpaX != null) && (cpaY != null)) { - return cpaX.distanceTo(cpaY); + return meterToMiles(cpaX.distanceTo(cpaY)); } else { return INVALID_CPA; } } - /* caluclate the distance between the given object and own position at their Closest Point of Approach (CPA)*/ - public float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { + /* caluclate the distance between the given object and own position at their Closest Point of Approach (CPA) */ + public static float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { if (locationProvider != null) { Location myLocation = locationProvider.getLastKnownLocation(); if (myLocation != null) { @@ -119,30 +155,101 @@ public float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvide return INVALID_CPA; } - /* calculate the new position of a moving object with coordinates x and velocity vector v - after the given time, - * -> attention: by using sherical coordinates, this will produce an error! */ - @NonNull - private Vector getNewPosition(@NonNull Vector x, @NonNull Vector v, double time) { - return new Vector(x.add(v.multiply(time))); + public static void getCpa(@NonNull Location loc1, @NonNull Location loc2, + @NonNull Cpa result) { + if (!checkSpeedAndBearing(loc1, loc2)) { + double tcpa = getTcpa(loc1, loc2); + Location cpaX = getNewPosition(loc1, tcpa); + Location cpaY = getNewPosition(loc2, tcpa); + result.setTcpa(tcpa); + result.setCpaPos1(cpaX); + result.setCpaPos2(cpaY); + if ((cpaX != null) && (cpaY != null)) { + result.setCpaDist(meterToMiles(cpaX.distanceTo(cpaY))); + } + } + } + + public static void getCpa(@NonNull Location loc, @Nullable OsmAndLocationProvider locationProvider, + @NonNull Cpa result) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + getCpa(myLocation, loc, result); + } + } + } + + private static double bearingInRad(float bearingInDegrees) { + double res = bearingInDegrees * 2 * Math.PI / 360.0; + while (res >= Math.PI) { res -= (2 * Math.PI); } + return res; + } + + @Nullable + public static Location getNewPosition(@Nullable Location x, double time) { + if (x != null) { + if (x.hasBearing() && x.hasSpeed()) { + LatLonPoint a = new LatLonPoint(x.getLatitude(), x.getLongitude()); + LatLonPoint b = a.getPoint(x.getSpeed() * time * Math.PI / 5556.0, bearingInRad(x.getBearing())); + Location newX = new Location(x); + newX.setLongitude(b.getLongitude()); + newX.setLatitude(b.getLatitude()); + return newX; + } else { + Log.d("AisTrackerHelper", "getNewPosition(): y.hasBearing->" + + x.hasBearing() + ", x.hasSpeed->" + x.hasSpeed()); + return null; + } + } else { + return null; + } + } + + private static double getLonCorrection(@Nullable Location loc) { + if (loc != null) { + Location x = new Location(loc); + // simulate a "measurement" trio towards East... + x.setSpeed(knotsToMeterPerSecond(1.0f)); // speed -> 1 kn + x.setBearing(90.0f); // course -> east + Location yEast = getNewPosition(x, 1.0); // new position after 1 hour + + if (yEast != null) { + double diffLon = yEast.getLongitude() - x.getLongitude(); + return diffLon * 60.0; // correction factor for longitude + } + } + return 1.0f; // fallback + } + + public static float knotsToMeterPerSecond(float speed) { + return speed * 1852 / 3600; + } + public static float meterPerSecondToKnots(float speed) { + return speed * 3600 / 1852; + } + + public static float meterToMiles(float x) { + return x / 1852.0f; } /* calculate a velocity vector from givem course (COG) and speed (SOG). COG is given as heading, SOG as scalar */ @NonNull - private Vector courseToVector(double cog, double sog) { - double alpha = cog + 90.0d; + private static Vector courseToVector(double cog, double sog) { + double alpha = 450.0d - cog; while (alpha < 0) { alpha += 360.0d; } - while (alpha > 360.0d ) { alpha -= 360.0d; } + while (alpha >= 360.0d ) { alpha -= 360.0d; } alpha = Math.toRadians(alpha); return new Vector(Math.sin(alpha) * sog, Math.cos(alpha) * sog); } @NonNull - private Vector locationToVector(@NonNull Location loc) { - return new Vector(loc.getLongitude(), loc.getLatitude()); + private static Vector locationToVector(@NonNull Location loc) { + return new Vector(loc.getLatitude() * 60.0, loc.getLongitude() * 60.0); } - private boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { + + private static boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { if (!x.hasBearing() || !y.hasBearing() || !x.hasSpeed() || !y.hasSpeed()) { Log.d("AisTrackerHelper", "some input data is missing: x.hasBearing->" + x.hasBearing() + ", y.hasBearing->" + y.hasBearing() + ", x.hasSpeed->" @@ -153,7 +260,7 @@ private boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { } } - private float getSpeedInNodes(@NonNull Location loc) { - return loc.getSpeed() * 1852 / 3600; + private static float getSpeedInKnots(@NonNull Location loc) { + return meterPerSecondToKnots(loc.getSpeed()); } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 5ea4d3d3679..f4f86c046dd 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -1,5 +1,10 @@ package net.osmand.plus.plugins.aistracker; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.knotsToMeterPerSecond; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.meterToMiles; +import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -12,6 +17,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import net.osmand.Location; +import net.osmand.LocationConvert; import net.osmand.core.android.MapRendererView; import net.osmand.core.jni.PointI; import net.osmand.data.LatLon; @@ -86,6 +93,132 @@ private void initTestObjects() { ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); updateAisObjectList(ais); + // here some tests for the geo (CPA) calculation + // define 3 (vessel) objects + Location x1 = new Location("test", 49.5d, -1.0d); // 49°30'N, 1°00'W + Location x2 = new Location("test", 49.916667d, 0.416667d); // 49°55'N, 0°25'E + Location x3 = new Location("test", 49.666667d, -0.75d); // 49°40'N, 0°45'W + Location y1, y2, y3; + Log.d("AisTrackerLayer", "# test0: position 1 after 0 hours: " + + LocationConvert.convertLatitude(x1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 2 after 0 hours: " + + LocationConvert.convertLatitude(x2.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x2.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 3 after 0 hours: " + + LocationConvert.convertLatitude(x3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x3.getLongitude(), FORMAT_MINUTES, true)); + + // use case: x1: course 0°, speed 5kn, x3: course 270°, speed 10kn, time: 1h, 1.5h + x1.setSpeed(knotsToMeterPerSecond(5.0f)); + x1.setBearing(0.0f); + x3.setSpeed(knotsToMeterPerSecond(10.0f)); + x3.setBearing(270.0f); + AisTrackerHelper.Cpa cpa1 = new AisTrackerHelper.Cpa(); + getCpa(x1, x3, cpa1); + Log.d("AisTrackerLayer", "# test1: tcpa(x1, x3): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test1: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test1: dist0: " + meterToMiles(x1.distanceTo(x3))); + y1 = AisTrackerHelper.getNewPosition(x1, 1.0); + y3 = AisTrackerHelper.getNewPosition(x3, 1.0); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test1: position 1 after 1 hour: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: position 3 after 1 hour: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 1.18); + y3 = AisTrackerHelper.getNewPosition(x3, 1.18); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test1: position 1 after 1.18 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: position 3 after 1.18 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 1.5); + y3 = AisTrackerHelper.getNewPosition(x3, 1.5); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test1: position 1 after 1.5 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: position 3 after 1.5 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + + // use case: x1: course 0°, speed 5kn, x3: course 270°, speed 5kn, time 1h, 1.5h, 2h + x1.setSpeed(knotsToMeterPerSecond(5.0f)); + x1.setBearing(0.0f); + x3.setSpeed(knotsToMeterPerSecond(5.0f)); + x3.setBearing(270.0f); + cpa1.reset(); + getCpa(x1, x3, cpa1); + Log.d("AisTrackerLayer", "# test2: tcpa(x1, x3): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test2: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test2: dist0: " + meterToMiles(x1.distanceTo(x3))); + y1 = AisTrackerHelper.getNewPosition(x1, 1.0); + y3 = AisTrackerHelper.getNewPosition(x3, 1.0); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test2: position 1 after 1 hour: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: position 3 after 1 hour: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 1.5); + y3 = AisTrackerHelper.getNewPosition(x3, 1.5); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test2: position 1 after 1.5 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: position 3 after 1.5 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 2.0); + y3 = AisTrackerHelper.getNewPosition(x3, 2.0); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test2: position 1 after 2 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: position 3 after 2 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + + // use case: x2: course 270°, speed 5kn, x3: course 45°, speed 5kn, time 5h + x2.setSpeed(knotsToMeterPerSecond(5.0f)); + x2.setBearing(270.0f); + x3.setSpeed(knotsToMeterPerSecond(5.0f)); + x3.setBearing(45.0f); + cpa1.reset(); + getCpa(x2, x3, cpa1); + Log.d("AisTrackerLayer", "# test3: tcpa(x1, x3): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test3: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test3: dist0: " + meterToMiles(x2.distanceTo(x3))); + y2 = AisTrackerHelper.getNewPosition(x2, 5.0); + y3 = AisTrackerHelper.getNewPosition(x3, 5.0); + if ((y2 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test3: position 2 after 5 hours: " + + LocationConvert.convertLatitude(y2.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y2.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test3: position 3 after 5 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test3: dist1: " + meterToMiles(y2.distanceTo(y3))); + } + //removeOldestAisObjectListEntry(); //removeLostAisObjects(); } From c61a34e543485e344affecea795ad98a32e633a2 Mon Sep 17 00:00:00 2001 From: Falk Date: Mon, 1 Jul 2024 23:38:37 +0200 Subject: [PATCH 11/74] use CPA data request in AIS object context menu to display CPA, TCPA --- .../aistracker/AisObjectMenuController.java | 36 ++++++ .../plugins/aistracker/AisTrackerHelper.java | 44 ++++--- .../plugins/aistracker/AisTrackerLayer.java | 114 ++++++++++++------ 3 files changed, 146 insertions(+), 48 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index f0b5d98dee8..1b2d5a136d9 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -1,7 +1,10 @@ package net.osmand.plus.plugins.aistracker; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; +import static java.lang.Math.ceil; + import android.annotation.SuppressLint; import androidx.annotation.NonNull; @@ -67,6 +70,38 @@ private String getOwnLocationAsString(@Nullable OsmAndLocationProvider locationP return null; } */ + @SuppressLint("DefaultLocale") + private void addCpaInfo(@NonNull SortedSet msgTypes, + @Nullable OsmAndLocationProvider locationProvider) { + if (msgTypes.contains(21) || msgTypes.contains(9)) { + return; + } + if ((aisObject.getCog() != AisObjectConstants.INVALID_COG) && + (aisObject.getSog() != AisObjectConstants.INVALID_SOG)) { + AisTrackerHelper.Cpa cpa = new AisTrackerHelper.Cpa(); + Location aisLocation = aisObject.getLocation(); + if (aisLocation != null) { + getCpa(aisLocation, locationProvider, cpa); + if (cpa.isValid()) { + double cpaTime = cpa.getTcpa(); + double hours = ceil(cpaTime); + double minutes = (cpaTime - hours) * 60.0; + addMenuItem("CPA", String.format("%.1f nm", cpa.getCpaDist())); + if (cpaTime > 0.0) { + if (hours >= 2.0) { + addMenuItem("TCPA", String.format("%.0f hours %.0f min", hours, minutes)); + } else if (hours >= 1.0) { + addMenuItem("TCPA", String.format("%.0f hour %.0f min", hours, minutes)); + } else { + addMenuItem("TCPA", String.format("%.0f min", minutes)); + } + } else { + addMenuItem("TCPA", String.format("%.1f hours", cpaTime)); + } + } + } + } + } private void addMenuItem(@NonNull String type, @Nullable String value) { if (value != null) { @@ -127,6 +162,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Bearing", String.format("%.1f", bearing)); } catch (Exception ignore) { } } + addCpaInfo(msgTypes, locationProvider); /* // test: addMenuItem("# loc", getOwnLocationAsString(locationProvider)); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index 03fa6e36725..228bf491934 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -39,6 +39,7 @@ public static class Cpa { private float cpaDist; // in miles private Location newPos1; // position of first object at time tcpa private Location newPos2; // position of first object at time tcpa + private boolean valid; public Cpa() { reset(); } @@ -47,6 +48,7 @@ public void reset() { tcpa = INVALID_TCPA; newPos1 = null; newPos2 = null; + valid = false; } public void setTcpa(double x) { this.tcpa = x; } public void setCpaDist(float x) { this.cpaDist = x; } @@ -56,6 +58,8 @@ public void reset() { public float getCpaDist() { return cpaDist; } public Location getCpaPos1() { return newPos1; } public Location getCpaPos2() { return newPos2; } + public void validate() { valid = true; } + public boolean isValid() { return valid; } } /* calculate the Time to Closest Point of Approach (TCPA) of two moving objects: @@ -67,8 +71,12 @@ private static double getTcpa(@NonNull Vector x, @NonNull Vector y, @NonNull Vector vx, @NonNull Vector vy, double lonCorrection) { Vector dx = new Vector( y.sub(x)); Vector dv = new Vector(vy.sub(vx)); - double divisor = dv.dot(dv); // TODO: check for Div/0 - return -(((dx.x * dv.x) + (dx.y * dv.y / lonCorrection)) / divisor); // TODO: check for Div/0 + double divisor = dv.dot(dv); + if ((Math.abs(divisor) < 1.0E-10f) || (lonCorrection < 1.0E-10f)) { + // avoid div by 0 or invalid lonCorrection + return INVALID_TCPA; + } + return -(((dx.x * dv.x) + (dx.y * dv.y / lonCorrection)) / divisor); } /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and y, @@ -77,7 +85,7 @@ private static double getTcpa(@NonNull Location x, @NonNull Location y, double l if (checkSpeedAndBearing(x, y)) { return INVALID_TCPA; } - if (lonCorrection < 0.001) { + if (lonCorrection < 1.0E-10f) { // in this case the lonCorrection is considered invalid -> new calculation lonCorrection = getLonCorrection(x); } @@ -113,8 +121,12 @@ private static Location getCpa(@NonNull Location x, @NonNull Location y, boolean return null; } double tcpa = getTcpa(x,y); - Location base = useFirstAsReference ? x : y; - return getNewPosition(base, tcpa); + if (tcpa == INVALID_TCPA) { + return null; + } else { + Location base = useFirstAsReference ? x : y; + return getNewPosition(base, tcpa); + } } /* to calculate the Closest Point of Approach (CPA) between the objects x and y, @@ -159,13 +171,16 @@ public static void getCpa(@NonNull Location loc1, @NonNull Location loc2, @NonNull Cpa result) { if (!checkSpeedAndBearing(loc1, loc2)) { double tcpa = getTcpa(loc1, loc2); - Location cpaX = getNewPosition(loc1, tcpa); - Location cpaY = getNewPosition(loc2, tcpa); - result.setTcpa(tcpa); - result.setCpaPos1(cpaX); - result.setCpaPos2(cpaY); - if ((cpaX != null) && (cpaY != null)) { - result.setCpaDist(meterToMiles(cpaX.distanceTo(cpaY))); + if (tcpa != INVALID_TCPA) { + Location cpaX = getNewPosition(loc1, tcpa); + Location cpaY = getNewPosition(loc2, tcpa); + result.setTcpa(tcpa); + result.setCpaPos1(cpaX); + result.setCpaPos2(cpaY); + if ((cpaX != null) && (cpaY != null)) { + result.setCpaDist(meterToMiles(cpaX.distanceTo(cpaY))); + result.validate(); + } } } } @@ -191,7 +206,8 @@ public static Location getNewPosition(@Nullable Location x, double time) { if (x != null) { if (x.hasBearing() && x.hasSpeed()) { LatLonPoint a = new LatLonPoint(x.getLatitude(), x.getLongitude()); - LatLonPoint b = a.getPoint(x.getSpeed() * time * Math.PI / 5556.0, bearingInRad(x.getBearing())); + LatLonPoint b = a.getPoint(x.getSpeed() * time * Math.PI / 5556.0, + bearingInRad(x.getBearing())); Location newX = new Location(x); newX.setLongitude(b.getLongitude()); newX.setLatitude(b.getLatitude()); @@ -209,7 +225,7 @@ public static Location getNewPosition(@Nullable Location x, double time) { private static double getLonCorrection(@Nullable Location loc) { if (loc != null) { Location x = new Location(loc); - // simulate a "measurement" trio towards East... + // simulate a "measurement" trip towards East... x.setSpeed(knotsToMeterPerSecond(1.0f)); // speed -> 1 kn x.setBearing(90.0f); // course -> east Location yEast = getNewPosition(x, 1.0); // new position after 1 hour diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index f4f86c046dd..a46f63f00b9 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -62,43 +62,22 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi initTimer(); startNetworkListener(); - initTestObjects(); // for test purposes: + // for test purposes: remove later... + initTestObjects(); + testCpa(); } - private void initTestObjects() { - // passenger ship - AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, - 320.0, 8.4, 50.738d, 7.099d, 0.0); - updateAisObjectList(ais); - ais = new AisObject(34568, 5, 0, "TEST-CALLSIGN1", "TEST-Ship", 60 /* passenger */, 56, - 65, 8, 12, 2, - "Potsdam", 8, 15, 22, 5); - updateAisObjectList(ais); - // sailing boat - ais = new AisObject(454011, 1, 20, 8, 0, 120, - 125.0, 4.4, 50.737d, 7.098d, 0.0); - updateAisObjectList(ais); - ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, - 0, 0, 0, 0, - "@@@", 0, 0, 0, 0); - updateAisObjectList(ais); - // land station - ais = new AisObject(878121, 4, 50.736d, 7.100d); - updateAisObjectList(ais); - // AIDS - ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, - 0, 0, 0, 0); - updateAisObjectList(ais); - // aircraft - ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); - updateAisObjectList(ais); - + private void testCpa() { // here some tests for the geo (CPA) calculation // define 3 (vessel) objects + // for coordinate transformation see https://www.koordinaten-umrechner.de Location x1 = new Location("test", 49.5d, -1.0d); // 49°30'N, 1°00'W Location x2 = new Location("test", 49.916667d, 0.416667d); // 49°55'N, 0°25'E Location x3 = new Location("test", 49.666667d, -0.75d); // 49°40'N, 0°45'W - Location y1, y2, y3; + Location x4 = new Location("test", 49.5d, -4.0d); // 49°30'N, 4°00'W + Location x5 = new Location("test", 50.0d, -3.75d); // 50°00'N, 3°45'W + // taken from marine chart: distances: x1 - x3: 13.8 nm, x2 - x3: 47,2 nm, x4 - x5: 31.4 nm + Location y1, y2, y3, y4, y5; Log.d("AisTrackerLayer", "# test0: position 1 after 0 hours: " + LocationConvert.convertLatitude(x1.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(x1.getLongitude(), FORMAT_MINUTES, true)); @@ -108,8 +87,17 @@ private void initTestObjects() { Log.d("AisTrackerLayer", "# test0: position 3 after 0 hours: " + LocationConvert.convertLatitude(x3.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(x3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 4 after 0 hours: " + + LocationConvert.convertLatitude(x4.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x4.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 5 after 0 hours: " + + LocationConvert.convertLatitude(x5.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x5.getLongitude(), FORMAT_MINUTES, true)); - // use case: x1: course 0°, speed 5kn, x3: course 270°, speed 10kn, time: 1h, 1.5h + // test case: x1: course 0°, speed 5kn, x3: course 270°, speed 10kn, time: 1h, 1.5h + // taken from marine chart: + // position after 1h: x1: 49°35'N, 1°00'W, x3: 49°40'N, 1°0.5'W, distance: 5.0nm + // position after 1.5h: x1: 49°37.5'N, 1°00'W, x3: 49°40'N, 1°8.5'W, distance: 6.0nm x1.setSpeed(knotsToMeterPerSecond(5.0f)); x1.setBearing(0.0f); x3.setSpeed(knotsToMeterPerSecond(10.0f)); @@ -124,7 +112,7 @@ private void initTestObjects() { if ((y1 != null) && (y3 != null)) { Log.d("AisTrackerLayer", "# test1: position 1 after 1 hour: " + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) - + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); Log.d("AisTrackerLayer", "# test1: position 3 after 1 hour: " + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); @@ -153,7 +141,11 @@ private void initTestObjects() { Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); } - // use case: x1: course 0°, speed 5kn, x3: course 270°, speed 5kn, time 1h, 1.5h, 2h + // test case: x1: course 0°, speed 5kn, x3: course 270°, speed 5kn, time 1h, 1.5h, 2h + // taken from marine chart: + // position after 1h: x1: 49°35'N, 1°00'W, x3: 49°40'N, 0°52.7'W, distance: 6.8nm + // position after 1.5h: x1: 49°37.5'N, 1°00'W, x3: 49°40'N, 0°56.7'W, distance: 3.1nm + // position after 2h: x1: 49°40'N, 1°00'W, x3: 49°40'N, 1°0.5'W, distance: 0.3nm x1.setSpeed(knotsToMeterPerSecond(5.0f)); x1.setBearing(0.0f); x3.setSpeed(knotsToMeterPerSecond(5.0f)); @@ -197,7 +189,9 @@ private void initTestObjects() { Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); } - // use case: x2: course 270°, speed 5kn, x3: course 45°, speed 5kn, time 5h + // test case: x2: course 270°, speed 5kn, x3: course 45°, speed 5kn, time 5h + // taken from marine chart: + // position after 5h: x2: 49°55'N, 0°14.1'W, x3: 49°57.8'N, 0°17.5'W, distance: 3.5nm x2.setSpeed(knotsToMeterPerSecond(5.0f)); x2.setBearing(270.0f); x3.setSpeed(knotsToMeterPerSecond(5.0f)); @@ -219,6 +213,58 @@ private void initTestObjects() { Log.d("AisTrackerLayer", "# test3: dist1: " + meterToMiles(y2.distanceTo(y3))); } + // test case: x4: course 45°, speed 10kn, x5: course 70°, speed 5kn, time 6h + // taken from marine chart: + // position after 6h: x4: 50°12.1'N, 2°54.4'W, x5: 50°10.1'N, 3°1.5'W, distance: 5nm + x4.setSpeed(knotsToMeterPerSecond(10.0f)); + x4.setBearing(45.0f); + x5.setSpeed(knotsToMeterPerSecond(5.0f)); + x5.setBearing(70.0f); + cpa1.reset(); + getCpa(x4, x5, cpa1); + Log.d("AisTrackerLayer", "# test4: tcpa(x4, x5): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test4: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test4: dist0: " + meterToMiles(x4.distanceTo(x5))); + y4 = AisTrackerHelper.getNewPosition(x4, 6.0); + y5 = AisTrackerHelper.getNewPosition(x5, 6.0); + if ((y4 != null) && (y5 != null)) { + Log.d("AisTrackerLayer", "# test4: position 4 after 6 hours: " + + LocationConvert.convertLatitude(y4.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y4.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test4: position 5 after 6 hours: " + + LocationConvert.convertLatitude(y5.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y5.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test4: dist1: " + meterToMiles(y4.distanceTo(y5))); + } + } + private void initTestObjects() { + // passenger ship + AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, + 320.0, 8.4, 50.738d, 7.099d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(34568, 5, 0, "TEST-CALLSIGN1", "TEST-Ship", 60 /* passenger */, 56, + 65, 8, 12, 2, + "Potsdam", 8, 15, 22, 5); + updateAisObjectList(ais); + // sailing boat + ais = new AisObject(454011, 1, 20, 8, 0, 120, + 125.0, 4.4, 50.737d, 7.098d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, + 0, 0, 0, 0, + "@@@", 0, 0, 0, 0); + updateAisObjectList(ais); + // land station + ais = new AisObject(878121, 4, 50.736d, 7.100d); + updateAisObjectList(ais); + // AIDS + ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, + 0, 0, 0, 0); + updateAisObjectList(ais); + // aircraft + ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); + updateAisObjectList(ais); + //removeOldestAisObjectListEntry(); //removeLostAisObjects(); } From c9eb7184810773944fe6a32d3327512e5ec19836 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 2 Jul 2024 21:05:25 +0200 Subject: [PATCH 12/74] some code refactoring --- .../plus/plugins/aistracker/AisObject.java | 33 +++----- .../aistracker/AisObjectMenuController.java | 17 ++-- .../plugins/aistracker/AisTrackerHelper.java | 78 +++++-------------- 3 files changed, 42 insertions(+), 86 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index a8af8913ee7..e6169e44f28 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -44,9 +44,7 @@ import net.osmand.Location; import net.osmand.data.LatLon; import net.osmand.data.RotatedTileBox; -import net.osmand.plus.OsmAndLocationProvider; import net.osmand.plus.R; -import net.osmand.util.MapUtils; import java.util.SortedSet; import java.util.TreeSet; @@ -821,26 +819,19 @@ public String getAidTypeString() { return(Integer.toString(ais_aidType)); } } - private float getDistanceOrBearing(@Nullable OsmAndLocationProvider locationProvider, - boolean needBearing) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - Location aisLocation = getLocation(); - if ((myLocation != null) && (aisLocation != null)) { - return needBearing ? myLocation.bearingTo(aisLocation) : myLocation.distanceTo(aisLocation); - } else { - Log.e("AisObject", "getDistanceOrBearing(): mylocation -> " + myLocation + - ", aisLocation -> " + aisLocation); - return -500.0f; // invalid - } + private float getDistanceOrBearing(@Nullable Location ownLocation, boolean needBearing) { + Location aisLocation = getLocation(); + if ((ownLocation != null) && (aisLocation != null)) { + return needBearing ? ownLocation.bearingTo(aisLocation) : ownLocation.distanceTo(aisLocation); } else { - Log.e("AisObject", "getDistanceOrBearing(): locationProvider -> null"); + Log.e("AisObject", "getDistanceOrBearing(): ownLocation -> " + ownLocation + + ", aisLocation -> " + aisLocation); return -500.0f; // invalid } } /* get bearing from own position to the position of the AIS object */ - public float getBearing(@Nullable OsmAndLocationProvider locationProvider) { - float bearing = getDistanceOrBearing(locationProvider, true); + public float getBearing(@Nullable Location ownLocation) { + float bearing = getDistanceOrBearing(ownLocation, true); if ((bearing < 0.0f) && (bearing > -200.0f)) { while (bearing < 0.0f) { bearing += 360.0f; @@ -849,11 +840,11 @@ public float getBearing(@Nullable OsmAndLocationProvider locationProvider) { return bearing; } /* get distance from own position to the position of the AIS object in meters */ - public float getDistanceInMeters(@Nullable OsmAndLocationProvider locationProvider) { - return getDistanceOrBearing(locationProvider, false); + public float getDistanceInMeters(@Nullable Location ownLocation) { + return getDistanceOrBearing(ownLocation, false); } - public float getDistanceInNauticalMiles(@Nullable OsmAndLocationProvider locationProvider) { - float dist = getDistanceInMeters(locationProvider); + public float getDistanceInNauticalMiles(@Nullable Location ownLocation) { + float dist = getDistanceInMeters(ownLocation); if (dist >= 0.0f) { dist = dist / 1852; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 1b2d5a136d9..0e78b207ea8 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -71,8 +71,7 @@ private String getOwnLocationAsString(@Nullable OsmAndLocationProvider locationP } */ @SuppressLint("DefaultLocale") - private void addCpaInfo(@NonNull SortedSet msgTypes, - @Nullable OsmAndLocationProvider locationProvider) { + private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet msgTypes) { if (msgTypes.contains(21) || msgTypes.contains(9)) { return; } @@ -80,8 +79,8 @@ private void addCpaInfo(@NonNull SortedSet msgTypes, (aisObject.getSog() != AisObjectConstants.INVALID_SOG)) { AisTrackerHelper.Cpa cpa = new AisTrackerHelper.Cpa(); Location aisLocation = aisObject.getLocation(); - if (aisLocation != null) { - getCpa(aisLocation, locationProvider, cpa); + if ((aisLocation != null) && (myLocation != null)) { + getCpa(myLocation, aisLocation, cpa); if (cpa.isValid()) { double cpaTime = cpa.getTcpa(); double hours = ceil(cpaTime); @@ -150,8 +149,12 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { OsmAndLocationProvider locationProvider = app.getLocationProvider(); - float distance = aisObject.getDistanceInNauticalMiles(locationProvider); - float bearing = aisObject.getBearing(locationProvider); + Location ownLocation = null; + if (locationProvider != null) { + ownLocation = locationProvider.getLastKnownLocation(); + } + float distance = aisObject.getDistanceInNauticalMiles(ownLocation); + float bearing = aisObject.getBearing(ownLocation); if (distance >= 0.0f) { try { addMenuItem("Distance", String.format("%.1f nm", distance)); @@ -162,7 +165,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Bearing", String.format("%.1f", bearing)); } catch (Exception ignore) { } } - addCpaInfo(msgTypes, locationProvider); + addCpaInfo(ownLocation, msgTypes); /* // test: addMenuItem("# loc", getOwnLocationAsString(locationProvider)); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index 228bf491934..2acb79af94c 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -85,34 +85,18 @@ private static double getTcpa(@NonNull Location x, @NonNull Location y, double l if (checkSpeedAndBearing(x, y)) { return INVALID_TCPA; } - if (lonCorrection < 1.0E-10f) { - // in this case the lonCorrection is considered invalid -> new calculation - lonCorrection = getLonCorrection(x); - } return getTcpa(locationToVector(x), locationToVector(y), courseToVector(x.getBearing(), getSpeedInKnots(x)), courseToVector(y.getBearing(), getSpeedInKnots(y)), lonCorrection); } - public static double getTcpa(@NonNull Location x, @NonNull Location y) { - return getTcpa(x, y, 0.0d); - } - - /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and own location, - * it is presumed that x contains its position, speed and course */ - public static double getTcpa(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - long now = System.currentTimeMillis(); - if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { - lastCorrectionUpdate = now; - correctionFactor = getLonCorrection(myLocation); - } - return getTcpa(x, myLocation, correctionFactor); - } + public static double getTcpa(@NonNull Location ownLocation, @NonNull Location otherLocation) { + long now = System.currentTimeMillis(); + if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { + correctionFactor = getLonCorrection(ownLocation); + lastCorrectionUpdate = now; } - return INVALID_TCPA; + return getTcpa(ownLocation, otherLocation, correctionFactor); } @Nullable @@ -156,24 +140,13 @@ public static float getCpaDistance(@NonNull Location x, @NonNull Location y) { } } - /* caluclate the distance between the given object and own position at their Closest Point of Approach (CPA) */ - public static float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - return getCpaDistance(x, myLocation); - } - } - return INVALID_CPA; - } - - public static void getCpa(@NonNull Location loc1, @NonNull Location loc2, + public static void getCpa(@NonNull Location ownLocation, @NonNull Location otherLocation, @NonNull Cpa result) { - if (!checkSpeedAndBearing(loc1, loc2)) { - double tcpa = getTcpa(loc1, loc2); + if (!checkSpeedAndBearing(ownLocation, otherLocation)) { + double tcpa = getTcpa(ownLocation, otherLocation); if (tcpa != INVALID_TCPA) { - Location cpaX = getNewPosition(loc1, tcpa); - Location cpaY = getNewPosition(loc2, tcpa); + Location cpaX = getNewPosition(ownLocation, tcpa); + Location cpaY = getNewPosition(otherLocation, tcpa); result.setTcpa(tcpa); result.setCpaPos1(cpaX); result.setCpaPos2(cpaY); @@ -185,16 +158,6 @@ public static void getCpa(@NonNull Location loc1, @NonNull Location loc2, } } - public static void getCpa(@NonNull Location loc, @Nullable OsmAndLocationProvider locationProvider, - @NonNull Cpa result) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - getCpa(myLocation, loc, result); - } - } - } - private static double bearingInRad(float bearingInDegrees) { double res = bearingInDegrees * 2 * Math.PI / 360.0; while (res >= Math.PI) { res -= (2 * Math.PI); } @@ -202,19 +165,19 @@ private static double bearingInRad(float bearingInDegrees) { } @Nullable - public static Location getNewPosition(@Nullable Location x, double time) { - if (x != null) { - if (x.hasBearing() && x.hasSpeed()) { - LatLonPoint a = new LatLonPoint(x.getLatitude(), x.getLongitude()); - LatLonPoint b = a.getPoint(x.getSpeed() * time * Math.PI / 5556.0, - bearingInRad(x.getBearing())); - Location newX = new Location(x); + public static Location getNewPosition(@Nullable Location loc, double time) { + if (loc != null) { + if (loc.hasBearing() && loc.hasSpeed()) { + LatLonPoint a = new LatLonPoint(loc.getLatitude(), loc.getLongitude()); + LatLonPoint b = a.getPoint(loc.getSpeed() * time * Math.PI / 5556.0, + bearingInRad(loc.getBearing())); + Location newX = new Location(loc); newX.setLongitude(b.getLongitude()); newX.setLatitude(b.getLatitude()); return newX; } else { - Log.d("AisTrackerHelper", "getNewPosition(): y.hasBearing->" - + x.hasBearing() + ", x.hasSpeed->" + x.hasSpeed()); + Log.d("AisTrackerHelper", "getNewPosition(): loc.hasBearing->" + + loc.hasBearing() + ", loc.hasSpeed->" + loc.hasSpeed()); return null; } } else { @@ -244,7 +207,6 @@ public static float knotsToMeterPerSecond(float speed) { public static float meterPerSecondToKnots(float speed) { return speed * 3600 / 1852; } - public static float meterToMiles(float x) { return x / 1852.0f; } From 406a2c889657e41db2c0858af677bbf6c2c8199c Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 2 Jul 2024 23:14:45 +0200 Subject: [PATCH 13/74] additional preferences regarding CPA (not used in the application yet) --- OsmAnd/res/values/strings.xml | 9 +++- OsmAnd/res/xml/ais_settings.xml | 22 ++++++++++ .../aistracker/AisObjectMenuController.java | 18 +------- .../plugins/aistracker/AisTrackerPlugin.java | 19 +++++--- .../AisTrackerSettingsFragment.java | 44 +++++++++++++++++-- 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index adfdd50c998..63923b65a8e 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -782,11 +782,18 @@ You need to activate the sensor so OsmAnd can find it. Define TCP port number of the NMEA data source UDP port of local NMEA data receiver Define UPD port where OsmAnd receives NMEA data + Timeout settings for AIS signal reception + Set timeout values to identify lost AIS objects if no signal was received for a specific time. Timeout for visibility when object is lost Set Timeout for visibility of AIS objects: After this time without signal reception, the AIS object will be removed from screen. Timeout for ship visibility when no signal received Set timeout for ship visibility: After this time without signal reception, the ship symbol will change its state on screen: It will be crossed out. - + Settings related to CPA + These values define the presentation of AIS objects that may come too close to the own position. + Warning time to reach the Closest Point of Approach (CPA) + If the TCPA (time to reach the CPA with another vessel) is less than this value, the vessel is marked with red color. + Warning distance for the Closes Point of Approach (CPA) + Vessels are marked with red color if the CPA is less than this value and the CPA is reached in the near future (see setting "Warning time to reach the CPA"). Weather Explore Weather forecast. Contours diff --git a/OsmAnd/res/xml/ais_settings.xml b/OsmAnd/res/xml/ais_settings.xml index ebc1d52da1f..0498714b169 100644 --- a/OsmAnd/res/xml/ais_settings.xml +++ b/OsmAnd/res/xml/ais_settings.xml @@ -33,6 +33,11 @@ android:title="@string/ais_port_nmea_local" tools:summary="@string/ais_port_nmea_local_description" /> + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 0e78b207ea8..fe81c96e945 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -59,17 +59,7 @@ private float getOwnBearing(@Nullable OsmAndLocationProvider locationProvider) { } return 0.0f; } - /* - private String getOwnLocationAsString(@Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - return myLocation.toString(); - } - } - return null; - } - */ + @SuppressLint("DefaultLocale") private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet msgTypes) { if (msgTypes.contains(21) || msgTypes.contains(9)) { @@ -166,12 +156,6 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, } catch (Exception ignore) { } } addCpaInfo(ownLocation, msgTypes); - /* - // test: - addMenuItem("# loc", getOwnLocationAsString(locationProvider)); - addMenuItem("# ownSpeed", Float.toString(getOwnSpeed(locationProvider))); - addMenuItem("# ownBearing", Float.toString(getOwnBearing(locationProvider))); - */ } } if (msgTypes.contains(21)) { // ATON (aid to navigation) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index 2d92fe4c4cb..a0f1983c768 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -31,28 +31,33 @@ public class AisTrackerPlugin extends OsmandPlugin { private AisTrackerLayer aisTrackerLayer = null; - public static final String COMPONENT = "net.osmand.aistrackerPlugin"; + private static final String COMPONENT = "net.osmand.aistrackerPlugin"; public static final String AISTRACKER_ID = "osmand.aistracker"; - public static final String AIS_NMEA_PROTOCOL_ID = "ais_nmea_protocol"; // see xml/ais_settings.xml public static final String AIS_NMEA_IP_ADDRESS_ID = "ais_address_nmea_server"; // see xml/ais_settings.xml public static final String AIS_NMEA_TCP_PORT_ID = "ais_port_nmea_server"; // see xml/ais_settings.xml public static final String AIS_NMEA_UDP_PORT_ID = "ais_port_nmea_local"; // see xml/ais_settings.xml public static final String AIS_OBJ_LOST_TIMEOUT_ID = "ais_object_lost_timeout"; // see xml/ais_settings.xml public static final String AIS_SHIP_LOST_TIMEOUT_ID = "ais_ship_lost_timeout"; // see xml/ais_settings.xml + public static final String AIS_CPA_WARNING_TIME_ID = "ais_cpa_warning_time"; // see xml/ais_settings.xml + public static final String AIS_CPA_WARNING_DISTANCE_ID = "ais_cpa_warning_distance"; // see xml/ais_settings.xml public final CommonPreference AIS_NMEA_PROTOCOL; public static final int AIS_NMEA_PROTOCOL_UDP = 0; public static final int AIS_NMEA_PROTOCOL_TCP = 1; public final CommonPreference AIS_NMEA_IP_ADDRESS; private static final String AIS_NMEA_DEFAULT_IP = "192.168.200.16"; public final CommonPreference AIS_NMEA_TCP_PORT; - public static final Integer AIS_NMEA_DEFAULT_TCP_PORT = 4001; + private static final Integer AIS_NMEA_DEFAULT_TCP_PORT = 4001; public final CommonPreference AIS_NMEA_UDP_PORT; - public static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; + private static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; public final CommonPreference AIS_OBJ_LOST_TIMEOUT; - public static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; + private static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; public final CommonPreference AIS_SHIP_LOST_TIMEOUT; - public static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; + private static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; + public final CommonPreference AIS_CPA_WARNING_TIME; + private static final Integer AIS_CPA_DEFAULT_WARNING_TIME = 0; + public final CommonPreference AIS_CPA_WARNING_DISTANCE; + private static final Float AIS_CPA_WARNING_DEFAULT_DISTANCE = 1.0f; public AisTrackerPlugin(OsmandApplication app) { super(app); @@ -63,6 +68,8 @@ public AisTrackerPlugin(OsmandApplication app) { AIS_NMEA_UDP_PORT = registerIntPreference(AIS_NMEA_UDP_PORT_ID, AIS_NMEA_DEFAULT_UDP_PORT); AIS_OBJ_LOST_TIMEOUT = registerIntPreference(AIS_OBJ_LOST_TIMEOUT_ID, AIS_OBJ_LOST_DEFAULT_TIMEOUT); AIS_SHIP_LOST_TIMEOUT = registerIntPreference(AIS_SHIP_LOST_TIMEOUT_ID, AIS_SHIP_LOST_DEFAULT_TIMEOUT); + AIS_CPA_WARNING_TIME = registerIntPreference(AIS_CPA_WARNING_TIME_ID, AIS_CPA_DEFAULT_WARNING_TIME); + AIS_CPA_WARNING_DISTANCE = registerFloatPreference(AIS_CPA_WARNING_DISTANCE_ID, AIS_CPA_WARNING_DEFAULT_DISTANCE); Log.d("AisTrackerPlugin", "constructor"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 152f3a92082..f5980905b81 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -3,10 +3,11 @@ import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP; +import static java.lang.Math.ceil; + +import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; -import android.text.SpannableStringBuilder; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -36,12 +37,15 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Override protected void setupPreferences() { int currentProtocol; + boolean cpaWarningEnabled; currentProtocol = setupProtocol(); setupIpAddress(currentProtocol); setupTcpPort(currentProtocol); setupUdpPort(currentProtocol); setupObjectLostTimeout(); setupShipLostTimeout(); + cpaWarningEnabled = setupCpaWarningTime(); + setupCpaWarningDistance(cpaWarningEnabled); } private int setupProtocol() { @@ -57,7 +61,6 @@ private int setupProtocol() { } return 0; } - private void setupIpAddress(int currentProtocol) { EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); if (aisNmeaIpAddress != null) { @@ -126,6 +129,41 @@ private void setupShipLostTimeout() { objectLostTimeout.setDescription(R.string.ais_ship_lost_timeout_description); } } + private boolean setupCpaWarningTime() { + Integer[] entryValues = {0, 1, 5, 10, 20, 30, 60}; + String[] entries = new String[entryValues.length]; + entries[0] = "disabled"; + for (int i = 1; i < entryValues.length; i++) { + entries[i] = entryValues[i] + " "; + entries[i] += entryValues[i].equals(1) ? "minute" : "minutes"; // TODO: move to ressource file + } + ListPreferenceEx cpaWarningTime = findPreference(plugin.AIS_CPA_WARNING_TIME.getId()); + if (cpaWarningTime != null) { + cpaWarningTime.setEntries(entries); + cpaWarningTime.setEntryValues(entryValues); + cpaWarningTime.setDescription(R.string.ais_cpa_warning_time_description); + return !cpaWarningTime.getValue().equals(0); + } + return false; + } + @SuppressLint("DefaultLocale") + private void setupCpaWarningDistance(boolean enabled) { + Float[] entryValues = {0.5f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + String[] entries = new String[entryValues.length]; + for (int i = 0; i < entryValues.length; i++) { + entries[i] = (ceil(entryValues[i]) == entryValues[i]) ? + String.format("%.0f ", entryValues[i]) : String.format("%.1f ", entryValues[i]); + entries[i] += entryValues[i].equals(1.0f) ? "nautical mile" : "nautical miles"; // TODO: move to ressource file + } + ListPreferenceEx cpaWarningDistance = findPreference(plugin.AIS_CPA_WARNING_DISTANCE.getId()); + if (cpaWarningDistance != null) { + cpaWarningDistance.setEntries(entries); + cpaWarningDistance.setEntryValues(entryValues); + cpaWarningDistance.setDescription(R.string.ais_cpa_warning_distance_description); + cpaWarningDistance.setEnabled(enabled); + } + } + @Override public boolean onPreferenceChange(Preference preference, Object newValue) { boolean restartNetworkListener = false; From 93e4a5fea30b90e5692807470b8c5768ad7c7165 Mon Sep 17 00:00:00 2001 From: Falk Date: Wed, 3 Jul 2024 00:03:51 +0200 Subject: [PATCH 14/74] code refactoring regarding maxObjectAgeInMinutes, vesselLostTimeoutInMinutes --- .../plus/plugins/aistracker/AisObject.java | 34 +++++++++++-------- .../plugins/aistracker/AisTrackerLayer.java | 8 ++--- .../plugins/aistracker/AisTrackerPlugin.java | 4 +-- .../AisTrackerSettingsFragment.java | 10 ++++++ 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index e6169e44f28..ca4ff3086e3 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -30,6 +30,8 @@ import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_OBJ_LOST_DEFAULT_TIMEOUT; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_SHIP_LOST_DEFAULT_TIMEOUT; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -82,8 +84,10 @@ public class AisObject { private String countryCode = null; private SortedSet msgTypes = null; private long lastUpdate = 0; - /* after this time the object is outdated and can be removed: */ - + /* after this time of missing AIS signal the object is outdated and can be removed: */ + private static int maxObjectAgeInMinutes = AIS_OBJ_LOST_DEFAULT_TIMEOUT; + /* after this time of missing AIS signal the vessel symbol can change to mark "lost": */ + private static int vesselLostTimeoutInMinutes = AIS_SHIP_LOST_DEFAULT_TIMEOUT; private AisObjType objectClass; private Bitmap bitmap = null; private int bitmapColor; @@ -368,8 +372,8 @@ public void set(@NonNull AisObject ais) { this.bitmapColor = 0; } - private void setBitmap(@NonNull AisTrackerLayer mapLayer, int maxAgeInMin) { - if (isLost(maxAgeInMin)) { + private void setBitmap(@NonNull AisTrackerLayer mapLayer) { + if (isLost(vesselLostTimeoutInMinutes)) { if (isMovable()) { this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); } @@ -401,11 +405,11 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer, int maxAgeInMin) { break; } } - this.setColor(maxAgeInMin); + this.setColor(); } - private void setColor(int maxAgeInMin) { - if (isLost(maxAgeInMin)) { + private void setColor() { + if (isLost(vesselLostTimeoutInMinutes)) { if (isMovable()) { this.bitmapColor = 0; // black } @@ -436,10 +440,9 @@ private void setColor(int maxAgeInMin) { } public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, - @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox, - int maxAgeInMin) { - if ((this.bitmap == null) || isLost(maxAgeInMin)) { - this.setBitmap(mapLayer, maxAgeInMin); + @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { + if ((this.bitmap == null) || isLost(vesselLostTimeoutInMinutes)) { + this.setBitmap(mapLayer); } if (this.bitmapColor != 0) { paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); @@ -461,7 +464,7 @@ public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, canvas.rotate(rotation, locationX, locationY); } canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); - if ((speedFactor > 0) && (!isLost(maxAgeInMin))) { + if ((speedFactor > 0) && (!isLost(vesselLostTimeoutInMinutes))) { float lineStartX = locationX; float lineLength = (float)this.bitmap.getHeight() * speedFactor; float lineStartY = locationY - this.bitmap.getHeight() / 4.0f; @@ -515,13 +518,14 @@ private boolean needRotation() { private boolean isLost(int maxAgeInMin) { return ((System.currentTimeMillis() - this.lastUpdate) / 1000 / 60) > maxAgeInMin; } - + public static void setMaxObjectAge(int timeInMinutes) { maxObjectAgeInMinutes = timeInMinutes; } + public static void setVesselLostTimeout(int timeInMinutes) { vesselLostTimeoutInMinutes = timeInMinutes; } /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed * */ - public boolean checkObjectAge(int maxAgeInMinutes) { - return isLost(maxAgeInMinutes); + public boolean checkObjectAge() { + return isLost(maxObjectAgeInMinutes); } public int getMsgType() { return this.ais_msgType; } public SortedSet getMsgTypes() { return this.msgTypes; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index a46f63f00b9..30da3e1a6f1 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -310,15 +310,14 @@ public void cleanup() { stopNetworkListener(); } private void removeLostAisObjects() { - int maxAge = plugin.AIS_OBJ_LOST_TIMEOUT.get(); for (Iterator> iterator = aisObjectList.entrySet().iterator(); iterator.hasNext(); ) { Map.Entry entry = iterator.next(); - if (entry.getValue().checkObjectAge(maxAge)) { + if (entry.getValue().checkObjectAge()) { Log.d("AisTrackerLayer", "remove AIS object with MMSI " + entry.getValue().getMmsi()); iterator.remove(); } } - // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge(maxAge)); + // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge()); } private void removeOldestAisObjectListEntry() { Log.d("AisTrackerLayer", "removeOldestAisObjectListEntry() called"); @@ -369,10 +368,9 @@ public boolean isLocationVisible(RotatedTileBox tileBox, LatLon coordinates) { @Override public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { - int maxAgeInMin = plugin.AIS_SHIP_LOST_TIMEOUT.get(); for (AisObject ais : aisObjectList.values()) { if (isLocationVisible(tileBox, ais.getPosition())) { - ais.draw(this, bitmapPaint, canvas, tileBox, maxAgeInMin); + ais.draw(this, bitmapPaint, canvas, tileBox); } } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index a0f1983c768..28944481736 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -51,9 +51,9 @@ public class AisTrackerPlugin extends OsmandPlugin { public final CommonPreference AIS_NMEA_UDP_PORT; private static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; public final CommonPreference AIS_OBJ_LOST_TIMEOUT; - private static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; + public static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; public final CommonPreference AIS_SHIP_LOST_TIMEOUT; - private static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; + public static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; public final CommonPreference AIS_CPA_WARNING_TIME; private static final Integer AIS_CPA_DEFAULT_WARNING_TIME = 0; public final CommonPreference AIS_CPA_WARNING_DISTANCE; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index f5980905b81..87844758a01 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -112,6 +112,7 @@ private void setupObjectLostTimeout() { objectLostTimeout.setEntries(entries); objectLostTimeout.setEntryValues(entryValues); objectLostTimeout.setDescription(R.string.ais_object_lost_timeout_description); + AisObject.setMaxObjectAge((Integer) objectLostTimeout.getValue()); } } private void setupShipLostTimeout() { @@ -127,6 +128,7 @@ private void setupShipLostTimeout() { objectLostTimeout.setEntries(entries); objectLostTimeout.setEntryValues(entryValues); objectLostTimeout.setDescription(R.string.ais_ship_lost_timeout_description); + AisObject.setVesselLostTimeout((Integer) objectLostTimeout.getValue()); } } private boolean setupCpaWarningTime() { @@ -180,6 +182,14 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { return false; } restartNetworkListener = true; + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_OBJ_LOST_TIMEOUT_ID)) { + if (newValue instanceof Integer) { + AisObject.setMaxObjectAge((Integer) newValue); + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_SHIP_LOST_TIMEOUT_ID)) { + if (newValue instanceof Integer) { + AisObject.setVesselLostTimeout((Integer) newValue); + } } boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); From a2fe7b423c4b98f57479f1ab30c1310b9efca675 Mon Sep 17 00:00:00 2001 From: Falk Date: Thu, 4 Jul 2024 23:15:42 +0200 Subject: [PATCH 15/74] add CPA check into vessel visualisation --- .../plus/plugins/aistracker/AisObject.java | 83 ++++++++++++++++--- .../aistracker/AisObjectConstants.java | 2 +- .../aistracker/AisObjectMenuController.java | 43 +++++----- .../plugins/aistracker/AisTrackerHelper.java | 9 +- .../plugins/aistracker/AisTrackerLayer.java | 6 +- .../plugins/aistracker/AisTrackerPlugin.java | 4 +- .../AisTrackerSettingsFragment.java | 10 ++- 7 files changed, 111 insertions(+), 46 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index ca4ff3086e3..e29e06f6d86 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -30,6 +30,10 @@ import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.CPA_UPDATE_TIMEOUT_IN_SECONDS; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_DEFAULT_WARNING_TIME; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_WARNING_DEFAULT_DISTANCE; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_OBJ_LOST_DEFAULT_TIMEOUT; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_SHIP_LOST_DEFAULT_TIMEOUT; @@ -88,9 +92,14 @@ public class AisObject { private static int maxObjectAgeInMinutes = AIS_OBJ_LOST_DEFAULT_TIMEOUT; /* after this time of missing AIS signal the vessel symbol can change to mark "lost": */ private static int vesselLostTimeoutInMinutes = AIS_SHIP_LOST_DEFAULT_TIMEOUT; + private static int cpaWarningTime = AIS_CPA_DEFAULT_WARNING_TIME; // in minutes + private static float cpaWarningDistance = AIS_CPA_WARNING_DEFAULT_DISTANCE; // in miles + private static Location ownPosition = null; // used to calculate distances, CPA etc. private AisObjType objectClass; private Bitmap bitmap = null; private int bitmapColor; + private AisTrackerHelper.Cpa cpa; + private long lastCpaUpdate = 0; public AisObject(int mmsi, int msgType, double lat, double lon) { initObj(mmsi, msgType); @@ -185,6 +194,7 @@ private String getCountryCode(Integer mmsi) { /* to be called only by a contructor! */ private void initObj(int mmsi, int msgType) { this.msgTypes = new TreeSet<>(); + this.cpa = new AisTrackerHelper.Cpa(); this.ais_mmsi = mmsi; this.ais_msgType = msgType; this.countryCode = getCountryCode(this.ais_mmsi); @@ -366,8 +376,10 @@ public void set(@NonNull AisObject ais) { this.msgTypes = new TreeSet<>(); } this.msgTypes.add(ais_msgType); + if (this.cpa == null) { + cpa = new AisTrackerHelper.Cpa(); + } this.initObjectClass(); - //this.objectClass = ais.getObjectClass(); // test only... remove later this.bitmap = null; this.bitmapColor = 0; } @@ -442,7 +454,12 @@ private void setColor() { public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { if ((this.bitmap == null) || isLost(vesselLostTimeoutInMinutes)) { - this.setBitmap(mapLayer); + setBitmap(mapLayer); + } + if (checkCpaWarning()) { + activateCpaWarning(); + } else { + deactivateCpaWarning(); } if (this.bitmapColor != 0) { paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); @@ -515,11 +532,53 @@ private boolean needRotation() { return false; } + /* return true if the vessel gets too close with the own position in the future + * (danger of collusion) */ + private boolean checkCpaWarning() { + if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0)) { + if (checkForCpaTimeout() && (ownPosition != null)) { + Location aisPosition = getLocation(); + if (aisPosition != null) { + getCpa(ownPosition, getLocation(), cpa); + lastCpaUpdate = System.currentTimeMillis(); + } + } + if (cpa.isValid()) { + double tcpa = cpa.getTcpa(); + if (tcpa > 0.0f) { + return ((tcpa * 60.0d) <= cpaWarningTime) && (cpa.getCpaDist() <= cpaWarningDistance); + } + } + } + return false; + } + + private void activateCpaWarning() { + bitmapColor = Color.RED; + } + private void deactivateCpaWarning() { + if (bitmapColor == Color.RED) { + setColor(); + } + } private boolean isLost(int maxAgeInMin) { return ((System.currentTimeMillis() - this.lastUpdate) / 1000 / 60) > maxAgeInMin; } + private boolean checkForCpaTimeout() { + return ((System.currentTimeMillis() - this.lastCpaUpdate) / 1000) > CPA_UPDATE_TIMEOUT_IN_SECONDS; + } public static void setMaxObjectAge(int timeInMinutes) { maxObjectAgeInMinutes = timeInMinutes; } public static void setVesselLostTimeout(int timeInMinutes) { vesselLostTimeoutInMinutes = timeInMinutes; } + public static void setCpaWarningTime(int warningTime) { cpaWarningTime = warningTime; } + public static void setCpaWarningDistance(float warningDistance) { cpaWarningDistance = warningDistance; } + //public static void setOwnPosition(Location position) { ownPosition = position; } + public static void setOwnPosition(Location position) { + ownPosition = position; + if (ownPosition != null) { + ownPosition.setBearing(180.0f); // test + ownPosition.setSpeed(0.1f); // test (m/s) + } + } /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed @@ -823,19 +882,19 @@ public String getAidTypeString() { return(Integer.toString(ais_aidType)); } } - private float getDistanceOrBearing(@Nullable Location ownLocation, boolean needBearing) { + private float getDistanceOrBearing(boolean needBearing) { Location aisLocation = getLocation(); - if ((ownLocation != null) && (aisLocation != null)) { - return needBearing ? ownLocation.bearingTo(aisLocation) : ownLocation.distanceTo(aisLocation); + if ((ownPosition != null) && (aisLocation != null)) { + return needBearing ? ownPosition.bearingTo(aisLocation) : ownPosition.distanceTo(aisLocation); } else { - Log.e("AisObject", "getDistanceOrBearing(): ownLocation -> " + ownLocation + + Log.e("AisObject", "getDistanceOrBearing(): ownLocation -> " + ownPosition + ", aisLocation -> " + aisLocation); return -500.0f; // invalid } } /* get bearing from own position to the position of the AIS object */ - public float getBearing(@Nullable Location ownLocation) { - float bearing = getDistanceOrBearing(ownLocation, true); + public float getBearing() { + float bearing = getDistanceOrBearing(true); if ((bearing < 0.0f) && (bearing > -200.0f)) { while (bearing < 0.0f) { bearing += 360.0f; @@ -844,11 +903,11 @@ public float getBearing(@Nullable Location ownLocation) { return bearing; } /* get distance from own position to the position of the AIS object in meters */ - public float getDistanceInMeters(@Nullable Location ownLocation) { - return getDistanceOrBearing(ownLocation, false); + public float getDistanceInMeters() { + return getDistanceOrBearing(false); } - public float getDistanceInNauticalMiles(@Nullable Location ownLocation) { - float dist = getDistanceInMeters(ownLocation); + public float getDistanceInNauticalMiles() { + float dist = getDistanceInMeters(); if (dist >= 0.0f) { dist = dist / 1852; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index c272664d567..afb42fa526d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -22,7 +22,7 @@ public final class AisObjectConstants { public final static double INVALID_DRAUGHT = 0.0; public final static double INVALID_TCPA = -10000.0d; public final static float INVALID_CPA = -1.0f; - + public final static int CPA_UPDATE_TIMEOUT_IN_SECONDS = 10; public static enum AisObjType { AIS_VESSEL, diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index fe81c96e945..ddfc32a1fd8 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -6,6 +6,7 @@ import static java.lang.Math.ceil; import android.annotation.SuppressLint; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -73,19 +74,24 @@ private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet 0.0) { - if (hours >= 2.0) { - addMenuItem("TCPA", String.format("%.0f hours %.0f min", hours, minutes)); - } else if (hours >= 1.0) { - addMenuItem("TCPA", String.format("%.0f hour %.0f min", hours, minutes)); - } else { - addMenuItem("TCPA", String.format("%.0f min", minutes)); + boolean isPositive = cpaTime >= 0; + cpaTime = Math.abs(cpaTime); + if (cpaTime < Long.MAX_VALUE) { + if (isPositive) { + long hours = (long)cpaTime; + double minutes = (cpaTime % 1 - hours) * 60.0; + addMenuItem("CPA", String.format("%.1f nm", cpa.getCpaDist())); + if (hours >= 2.0) { + addMenuItem("TCPA", String.format("%d hours %.0f min", hours, minutes)); + } else if (hours >= 1.0) { + addMenuItem("TCPA", String.format("%d hour %.0f min", hours, minutes)); + } else { + addMenuItem("TCPA", String.format("%.0f min", minutes)); + } + } else { // remove this later: don't show negative values... + addMenuItem("CPA", String.format("%.1f nm", cpa.getCpaDist())); + addMenuItem("TCPA", String.format("-%.1f hours", cpaTime)); } - } else { - addMenuItem("TCPA", String.format("%.1f hours", cpaTime)); } } } @@ -138,13 +144,10 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { - OsmAndLocationProvider locationProvider = app.getLocationProvider(); - Location ownLocation = null; - if (locationProvider != null) { - ownLocation = locationProvider.getLastKnownLocation(); - } - float distance = aisObject.getDistanceInNauticalMiles(ownLocation); - float bearing = aisObject.getBearing(ownLocation); + Location ownPosition = app.getLocationProvider().getLastKnownLocation(); + AisObject.setOwnPosition(ownPosition); + float distance = aisObject.getDistanceInNauticalMiles(); + float bearing = aisObject.getBearing(); if (distance >= 0.0f) { try { addMenuItem("Distance", String.format("%.1f nm", distance)); @@ -155,7 +158,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Bearing", String.format("%.1f", bearing)); } catch (Exception ignore) { } } - addCpaInfo(ownLocation, msgTypes); + addCpaInfo(ownPosition, msgTypes); } } if (msgTypes.contains(21)) { // ATON (aid to navigation) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index 2acb79af94c..1495d582608 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -228,14 +228,7 @@ private static Vector locationToVector(@NonNull Location loc) { } private static boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { - if (!x.hasBearing() || !y.hasBearing() || !x.hasSpeed() || !y.hasSpeed()) { - Log.d("AisTrackerHelper", "some input data is missing: x.hasBearing->" - + x.hasBearing() + ", y.hasBearing->" + y.hasBearing() + ", x.hasSpeed->" - + x.hasSpeed() + ", y.hasSpeed" + y.hasSpeed()); - return true; - } else { - return false; - } + return !x.hasBearing() || !y.hasBearing() || !x.hasSpeed() || !y.hasSpeed(); } private static float getSpeedInKnots(@NonNull Location loc) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 30da3e1a6f1..8cf38adae8d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -59,12 +59,15 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi this.bitmapPaint.setStrokeWidth(4); this.bitmapPaint.setColor(Color.DKGRAY); + AisObject.setCpaWarningTime(plugin.AIS_CPA_WARNING_TIME.get()); + AisObject.setCpaWarningDistance(plugin.AIS_CPA_WARNING_DISTANCE.get()); + initTimer(); startNetworkListener(); // for test purposes: remove later... initTestObjects(); - testCpa(); + //testCpa(); } private void testCpa() { @@ -368,6 +371,7 @@ public boolean isLocationVisible(RotatedTileBox tileBox, LatLon coordinates) { @Override public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + AisObject.setOwnPosition(getApplication().getLocationProvider().getLastKnownLocation()); for (AisObject ais : aisObjectList.values()) { if (isLocationVisible(tileBox, ais.getPosition())) { ais.draw(this, bitmapPaint, canvas, tileBox); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index 28944481736..224cf159e1e 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -55,9 +55,9 @@ public class AisTrackerPlugin extends OsmandPlugin { public final CommonPreference AIS_SHIP_LOST_TIMEOUT; public static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; public final CommonPreference AIS_CPA_WARNING_TIME; - private static final Integer AIS_CPA_DEFAULT_WARNING_TIME = 0; + public static final Integer AIS_CPA_DEFAULT_WARNING_TIME = 0; public final CommonPreference AIS_CPA_WARNING_DISTANCE; - private static final Float AIS_CPA_WARNING_DEFAULT_DISTANCE = 1.0f; + public static final Float AIS_CPA_WARNING_DEFAULT_DISTANCE = 1.0f; public AisTrackerPlugin(OsmandApplication app) { super(app); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 87844758a01..e46bcb72c57 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -112,7 +112,6 @@ private void setupObjectLostTimeout() { objectLostTimeout.setEntries(entries); objectLostTimeout.setEntryValues(entryValues); objectLostTimeout.setDescription(R.string.ais_object_lost_timeout_description); - AisObject.setMaxObjectAge((Integer) objectLostTimeout.getValue()); } } private void setupShipLostTimeout() { @@ -128,7 +127,6 @@ private void setupShipLostTimeout() { objectLostTimeout.setEntries(entries); objectLostTimeout.setEntryValues(entryValues); objectLostTimeout.setDescription(R.string.ais_ship_lost_timeout_description); - AisObject.setVesselLostTimeout((Integer) objectLostTimeout.getValue()); } } private boolean setupCpaWarningTime() { @@ -190,6 +188,14 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { if (newValue instanceof Integer) { AisObject.setVesselLostTimeout((Integer) newValue); } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_CPA_WARNING_TIME_ID)) { + if (newValue instanceof Integer) { + AisObject.setCpaWarningTime((Integer) newValue); + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_CPA_WARNING_DISTANCE_ID)) { + if (newValue instanceof Float) { + AisObject.setCpaWarningDistance((Float) newValue); + } } boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); From e70e8a280e59ce9bfa0296e2e7a33cd2bf47596f Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 6 Jul 2024 23:22:29 +0200 Subject: [PATCH 16/74] adjust layout of context menu entries --- .../plus/plugins/aistracker/AisObject.java | 5 +- .../aistracker/AisObjectMenuBuilder.java | 155 ++++++++++++++++++ .../aistracker/AisObjectMenuController.java | 43 ++--- 3 files changed, 168 insertions(+), 35 deletions(-) create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index e29e06f6d86..68b5c260dc0 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -571,14 +571,15 @@ private boolean checkForCpaTimeout() { public static void setVesselLostTimeout(int timeInMinutes) { vesselLostTimeoutInMinutes = timeInMinutes; } public static void setCpaWarningTime(int warningTime) { cpaWarningTime = warningTime; } public static void setCpaWarningDistance(float warningDistance) { cpaWarningDistance = warningDistance; } - //public static void setOwnPosition(Location position) { ownPosition = position; } - public static void setOwnPosition(Location position) { + public static void setOwnPosition(Location position) { ownPosition = position; } +/* public static void setOwnPosition(Location position) { ownPosition = position; if (ownPosition != null) { ownPosition.setBearing(180.0f); // test ownPosition.setSpeed(0.1f); // test (m/s) } } + */ /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java new file mode 100644 index 00000000000..6bdfef83a3d --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java @@ -0,0 +1,155 @@ +package net.osmand.plus.plugins.aistracker; + + +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.helpers.FontCache; +import net.osmand.plus.mapcontextmenu.CollapsableView; +import net.osmand.plus.mapcontextmenu.MenuBuilder; +import net.osmand.plus.utils.AndroidUtils; +import net.osmand.plus.utils.ColorUtilities; +import net.osmand.plus.widgets.TextViewEx; +import net.osmand.util.Algorithms; + +public class AisObjectMenuBuilder extends MenuBuilder { + + public AisObjectMenuBuilder(@NonNull MapActivity mapActivity) { + super(mapActivity); + } + + public View buildRow(View view, Drawable icon, String buttonText, String textPrefix, String text, + int textColor, String secondaryText, boolean collapsable, CollapsableView collapsableView, boolean needLinks, + int textLinesLimit, boolean isUrl, boolean isNumber, boolean isEmail, View.OnClickListener onClickListener, boolean matchWidthDivider) { + + return buildAisRow(view,null, text, textColor, buttonText,null, textLinesLimit, matchWidthDivider); + + /* + return super.buildRow(view, icon, buttonText, textPrefix, text, textColor, secondaryText, collapsable, collapsableView, needLinks, + textLinesLimit, isUrl, isNumber, isEmail, onClickListener, matchWidthDivider); + */ + /* + return super.buildRow(view, icon, null, textPrefix, text, textColor, buttonText, collapsable, collapsableView, needLinks, + textLinesLimit, isUrl, isNumber, isEmail, onClickListener, matchWidthDivider); + */ + } + + private View buildAisRow(View view, String prefixText, String aisType, int aisTypeColor, String aisValue, + String suffixText, int textLinesLimit, boolean matchWidthDivider) { + + if (!isFirstRow()) { + buildRowDivider(view); + } + + LinearLayout baseView = new LinearLayout(view.getContext()); + baseView.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams llBaseViewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + baseView.setLayoutParams(llBaseViewParams); + + LinearLayout ll = new LinearLayout(view.getContext()); + ll.setOrientation(LinearLayout.HORIZONTAL); + LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + ll.setLayoutParams(llParams); + ll.setBackgroundResource(AndroidUtils.resolveAttribute(view.getContext(), android.R.attr.selectableItemBackground)); + ll.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + String textToCopy = Algorithms.isEmpty(prefixText) ? aisType : prefixText + ": " + aisType; + copyToClipboard(textToCopy, view.getContext()); + return true; + } + }); + + baseView.addView(ll); + + // prefixText + LinearLayout llText = new LinearLayout(view.getContext()); + llText.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams llTextViewParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT); + llTextViewParams.weight = 1f; + AndroidUtils.setMargins(llTextViewParams, 0, 0, dpToPx(10f), 0); + llTextViewParams.gravity = Gravity.CENTER_VERTICAL; + llText.setLayoutParams(llTextViewParams); + ll.addView(llText); + + TextViewEx textPrefixView = null; + if (!Algorithms.isEmpty(prefixText)) { + textPrefixView = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams llTextParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + AndroidUtils.setMargins(llTextParams, dpToPx(16f), dpToPx(8f), 0, 0); + textPrefixView.setLayoutParams(llTextParams); + textPrefixView.setTypeface(FontCache.getRobotoRegular(view.getContext())); + textPrefixView.setTextSize(12); + textPrefixView.setTextColor(ColorUtilities.getSecondaryTextColor(app, !light)); + textPrefixView.setMinLines(1); + textPrefixView.setMaxLines(1); + textPrefixView.setText(prefixText); + llText.addView(textPrefixView); + } + + // aisType + TextViewEx textView = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams llTextParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + AndroidUtils.setMargins(llTextParams, + dpToPx(16f), dpToPx(textPrefixView != null ? 2f : (suffixText != null ? 10f : 8f)), 0, dpToPx(suffixText != null ? 6f : 8f)); + textView.setLayoutParams(llTextParams); + textView.setTypeface(FontCache.getRobotoRegular(view.getContext())); + textView.setTextSize(16); + textView.setTextColor(ColorUtilities.getPrimaryTextColor(app, !light)); + textView.setText(aisType); + + if (textLinesLimit > 0) { + textView.setMinLines(1); + textView.setMaxLines(textLinesLimit); + textView.setEllipsize(TextUtils.TruncateAt.END); + } + if (aisTypeColor > 0) { + textView.setTextColor(getColor(aisTypeColor)); + } + llText.addView(textView); + + // suffixText + if (!TextUtils.isEmpty(suffixText)) { + TextViewEx textViewSecondary = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams llTextSecondaryParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + AndroidUtils.setMargins(llTextSecondaryParams, dpToPx(16f), 0, 0, dpToPx(6f)); + textViewSecondary.setLayoutParams(llTextSecondaryParams); + textViewSecondary.setTypeface(FontCache.getRobotoRegular(view.getContext())); + textViewSecondary.setTextSize(14); + textViewSecondary.setTextColor(ColorUtilities.getSecondaryTextColor(app, !light)); + textViewSecondary.setText(suffixText); + llText.addView(textViewSecondary); + } + + // aisValue + if (!TextUtils.isEmpty(aisValue)) { + TextViewEx buttonTextView = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams buttonTextViewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + buttonTextViewParams.gravity = Gravity.CENTER_VERTICAL; + AndroidUtils.setMargins(buttonTextViewParams, dpToPx(8), 0, dpToPx(8), 0); + buttonTextView.setLayoutParams(buttonTextViewParams); + buttonTextView.setTypeface(FontCache.getRobotoMedium(view.getContext())); + buttonTextView.setTextSize(16); + buttonTextView.setTextColor(ContextCompat.getColor(view.getContext(), !light ? R.color.ctx_menu_controller_button_text_color_dark_n : R.color.ctx_menu_controller_button_text_color_light_n)); + buttonTextView.setText(aisValue); + ll.addView(buttonTextView); + } + + ((LinearLayout) view).addView(baseView); + + rowBuilt(); + + setDividerWidth(matchWidthDivider); + + return ll; + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index ddfc32a1fd8..39f998700de 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -3,8 +3,6 @@ import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; -import static java.lang.Math.ceil; - import android.annotation.SuppressLint; import android.util.Log; @@ -15,10 +13,8 @@ import net.osmand.LocationConvert; import net.osmand.data.LatLon; import net.osmand.data.PointDescription; -import net.osmand.plus.OsmAndLocationProvider; import net.osmand.plus.OsmandApplication; import net.osmand.plus.activities.MapActivity; -import net.osmand.plus.mapcontextmenu.MenuBuilder; import net.osmand.plus.mapcontextmenu.MenuController; import java.util.Iterator; @@ -29,7 +25,9 @@ public class AisObjectMenuController extends MenuController { private final OsmandApplication app; public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointDescription pointDescription, AisObject aisObject) { - super(new MenuBuilder(mapActivity), pointDescription, mapActivity); + //super(new MenuBuilder(mapActivity), pointDescription, mapActivity); + super(new AisObjectMenuBuilder(mapActivity), pointDescription, mapActivity); + this.aisObject = aisObject; this.app = builder.getApplication(); builder.setShowTitleIfTruncated(false); @@ -38,28 +36,6 @@ public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointD builder.setShowNearestWiki(false); // TODO: show an icon in the menu } - private float getOwnSpeed(@Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - if (myLocation.hasSpeed()) { - return myLocation.getSpeed(); - } - } - } - return 0.0f; - } - private float getOwnBearing(@Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - if (myLocation.hasBearing()) { - return myLocation.getBearing(); - } - } - } - return 0.0f; - } @SuppressLint("DefaultLocale") private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet msgTypes) { @@ -88,7 +64,7 @@ private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet= 0.0f) { try { - addMenuItem("Bearing", String.format("%.1f", bearing)); + addMenuItem("Bearing", String.format("%.0f", bearing)); } catch (Exception ignore) { } } addCpaInfo(ownPosition, msgTypes); @@ -167,10 +143,10 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, } else if (msgTypes.contains(9)) { // SAR aircraft addMenuItem("Object Type", "SAR Aircraft"); if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { - addMenuItem("COG", String.valueOf(aisObject.getCog())); + addMenuItem("COG", String.format("%.0f", aisObject.getCog())); } if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { - addMenuItem("SOG", String.valueOf(aisObject.getSog()) + " kt"); + addMenuItem("SOG", String.format("%.1f kts", aisObject.getSog())); } if (aisObject.getAltitude() != AisObjectConstants.INVALID_ALTITUDE) { addMenuItem("Altitude", String.valueOf(aisObject.getAltitude()) + " m"); @@ -186,10 +162,10 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Navigation Status", aisObject.getNavStatusString()); } if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { - addMenuItem("COG", String.valueOf(aisObject.getCog())); + addMenuItem("COG", String.format("%.0f", aisObject.getCog())); } if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { - addMenuItem("SOG", String.valueOf(aisObject.getSog()) + " kt"); + addMenuItem("SOG", String.format("%.1f kts", aisObject.getSog())); } if (aisObject.getHeading() != AisObjectConstants.INVALID_HEADING) { addMenuItem("Heading", String.valueOf(aisObject.getHeading())); @@ -225,6 +201,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, if (hasNext) { msgTypesString = msgTypesString.concat(", "); } } addMenuItem("Message Type(s)", msgTypesString); + super.addPlainMenuItems(typeStr, pointDescription, latLon); } @Override From 6006050adf4ace1294f7315ca04d05156c4aceda Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 7 Jul 2024 22:41:17 +0200 Subject: [PATCH 17/74] add disclaimer in the plugin description --- OsmAnd/res/values/strings.xml | 1 + .../osmand/plus/plugins/aistracker/AisTrackerPlugin.java | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index 63923b65a8e..d2da5b21308 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -3894,6 +3894,7 @@ Download tile maps directly, or copy them as SQLite database files to OsmAnd\'s Create paths by tapping the map, or by using or modifying existing GPX files, to plan a trip and measure the distance between points. The result can be saved as a GPX file to use later for guidance. AIS vessel tracker Display AIS positions and information about surrounding vessels. The AIS data is received via network from an external AIS receiver. + DISCLAIMER\n\nThis plugin is a hobby project and not designed for reliability and correctness. DO NOT rely upon this software in any way including for navigation and/or safety of life. Accessibility Makes the device\'s accessibility features directly available in OsmAnd. This facilitates e.g. adjusting the speech rate for text-to-speech voices, configuring D-pad navigation, using a trackball for zoom control, or text-to-speech feedback, for example to auto-announce your position. OpenStreetMap editing diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index 224cf159e1e..ad3545afd5a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -79,6 +79,11 @@ public boolean isMarketPlugin() { return true; } + @Override + public int getVersion() { + return -1; + } + @Override public String getComponentId1() { return COMPONENT; @@ -86,7 +91,7 @@ public String getComponentId1() { @Override public CharSequence getDescription(boolean linksEnabled) { - return app.getString(R.string.plugin_aistracker_description); + return app.getString(R.string.plugin_aistracker_description).concat("\n\n").concat(app.getString(R.string.plugin_aistracker_disclaimer)); } @Override From ad6640816e01bef163120c6e9929b7a2e580837b Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 21 Jul 2024 22:08:41 +0200 Subject: [PATCH 18/74] address some concurrency issue in AIS object list --- .../aistracker/AisMessageListener.java | 31 +++++----- .../plus/plugins/aistracker/AisObject.java | 59 ++++++++++--------- .../aistracker/AisObjectMenuController.java | 3 +- .../plugins/aistracker/AisTrackerLayer.java | 19 +++--- 4 files changed, 57 insertions(+), 55 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java index 49e5d532e9e..785907fb816 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -34,7 +34,6 @@ public class AisMessageListener { private AisTrackerLayer aisLayer; private Timer timer; - private TimerTask taskCheckNetworkConnection; private DatagramSocket udpSocket; private Socket tcpSocket; private InputStream tcpStream; @@ -54,6 +53,7 @@ public AisMessageListener(int port, @NonNull AisTrackerLayer aisLayer) { } } public AisMessageListener(@NonNull String serverIp, int serverPort, @NonNull AisTrackerLayer aisLayer) { + TimerTask taskCheckNetworkConnection; initMembers(aisLayer); taskCheckNetworkConnection = new TimerTask() { @Override @@ -186,9 +186,8 @@ private void handleAisMessage(int aisType, Object obj) { switch (aisType) { case 1: AISMessage01 aisMsg01 = (AISMessage01)obj; // position report class A Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg01.getMMSI() - + " Type: " + aisMsg01.getMessageType() - + " ROT: " + aisMsg01.getRateOfTurn()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg01); + + " Type: " + aisMsg01.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg01); mmsi = aisMsg01.getMMSI(); msgType = aisMsg01.getMessageType(); navStatus = aisMsg01.getNavigationalStatus(); @@ -207,7 +206,7 @@ private void handleAisMessage(int aisType, Object obj) { case 2: AISMessage02 aisMsg02 = (AISMessage02)obj; // position report class A Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg02.getMMSI() + " Type: " + aisMsg02.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg02); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg02); mmsi = aisMsg02.getMMSI(); msgType = aisMsg02.getMessageType(); navStatus = aisMsg02.getNavigationalStatus(); @@ -226,7 +225,7 @@ private void handleAisMessage(int aisType, Object obj) { case 3: AISMessage03 aisMsg03 = (AISMessage03)obj; // position report class A Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg03.getMMSI() + " Type: " + aisMsg03.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg03); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg03); mmsi = aisMsg03.getMMSI(); msgType = aisMsg03.getMessageType(); navStatus = aisMsg03.getNavigationalStatus(); @@ -245,7 +244,7 @@ private void handleAisMessage(int aisType, Object obj) { case 4: AISMessage04 aisMsg04 = (AISMessage04)obj; // base station report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg04.getMMSI() + " Type: " + aisMsg04.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg04); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg04); mmsi = aisMsg04.getMMSI(); msgType = aisMsg04.getMessageType(); if (aisMsg04.hasLatitude()) { lat = aisMsg04.getLatitudeInDegrees(); } @@ -256,7 +255,7 @@ private void handleAisMessage(int aisType, Object obj) { case 5: AISMessage05 aisMsg05 = (AISMessage05)obj; // static and voyage related data Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg05.getMMSI() + " Type: " + aisMsg05.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg05); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg05); mmsi = aisMsg05.getMMSI(); msgType = aisMsg05.getMessageType(); imo = aisMsg05.getIMONumber(); @@ -281,7 +280,7 @@ private void handleAisMessage(int aisType, Object obj) { case 9: AISMessage09 aisMsg09 = (AISMessage09)obj; // SAR aircraft position report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg09.getMMSI() + " Type: " + aisMsg09.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg09); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg09); mmsi = aisMsg09.getMMSI(); msgType = aisMsg09.getMessageType(); timeStamp = aisMsg09.getTimeStamp(); @@ -296,7 +295,7 @@ private void handleAisMessage(int aisType, Object obj) { case 18: AISMessage18 aisMsg18 = (AISMessage18)obj; // basic class B position report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg18.getMMSI() + " Type: " + aisMsg18.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg18); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg18); mmsi = aisMsg18.getMMSI(); msgType = aisMsg18.getMessageType(); if (aisMsg18.hasTimeStamp()) { timeStamp = aisMsg18.getTimeStamp(); } @@ -312,7 +311,7 @@ private void handleAisMessage(int aisType, Object obj) { case 19: AISMessage19 aisMsg19 = (AISMessage19)obj; // extended class B position report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg19.getMMSI() + " Type: " + aisMsg19.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg19); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg19); mmsi = aisMsg19.getMMSI(); msgType = aisMsg19.getMessageType(); shipType = aisMsg19.getTypeOfShipAndCargoType(); @@ -333,7 +332,7 @@ private void handleAisMessage(int aisType, Object obj) { case 21: AISMessage21 aisMsg21 = (AISMessage21)obj; // aid-to-navigation report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg21.getMMSI() + " Type: " + aisMsg21.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg21); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg21); mmsi = aisMsg21.getMMSI(); msgType = aisMsg21.getMessageType(); dimensionToBow = aisMsg21.getBow(); @@ -350,7 +349,7 @@ private void handleAisMessage(int aisType, Object obj) { case 24: AISMessage24 aisMsg24 = (AISMessage24)obj; // static data report (like type 5) Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg24.getMMSI() + " Type: " + aisMsg24.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg24); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg24); mmsi = aisMsg24.getMMSI(); msgType = aisMsg24.getMessageType(); callSign = aisMsg24.getCallSign(); @@ -368,7 +367,7 @@ private void handleAisMessage(int aisType, Object obj) { case 27: AISMessage27 aisMsg27 = (AISMessage27)obj; // long range broadcast message Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg27.getMMSI() + " Type: " + aisMsg27.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg27); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg27); mmsi = aisMsg27.getMMSI(); msgType = aisMsg27.getMessageType(); navStatus = aisMsg27.getNavigationalStatus(); @@ -391,11 +390,9 @@ private void handleAisMessage(int aisType, Object obj) { aisLayer.updateAisObjectList(ais); } private void initEmbeddedLister(int aisType, @NonNull SentenceListener listener) { - AisMessageListener.this.sentenceReader.addSentenceListener(listener); - /* + //AisMessageListener.this.sentenceReader.addSentenceListener(listener); // listen to all (!) NMEA messages AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDM); AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDO); - */ AisMessageListener.this.listenerList.push(listener); Log.d("AisMessageListener","Listener Type " + aisType + " started"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 68b5c260dc0..3a0e6183648 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -97,6 +97,7 @@ public class AisObject { private static Location ownPosition = null; // used to calculate distances, CPA etc. private AisObjType objectClass; private Bitmap bitmap = null; + private boolean bitmapValid = false; private int bitmapColor; private AisTrackerHelper.Cpa cpa; private long lastCpaUpdate = 0; @@ -340,6 +341,10 @@ private void initObjectClass() { } } + private void invalidateBitmap() { + this.bitmapValid = false; + } + public void set(@NonNull AisObject ais) { this.ais_mmsi = ais.getMmsi(); this.ais_msgType = ais.getMsgType(); @@ -380,14 +385,16 @@ public void set(@NonNull AisObject ais) { cpa = new AisTrackerHelper.Cpa(); } this.initObjectClass(); - this.bitmap = null; + this.invalidateBitmap(); this.bitmapColor = 0; } private void setBitmap(@NonNull AisTrackerLayer mapLayer) { + invalidateBitmap(); if (isLost(vesselLostTimeoutInMinutes)) { if (isMovable()) { this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); + this.bitmapValid = true; } } else { switch (this.objectClass) { @@ -399,21 +406,27 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { case AIS_VESSEL_COMMERCIAL: case AIS_INVALID: this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel); + this.bitmapValid = true; break; case AIS_LANDSTATION: this.bitmap = mapLayer.getBitmap(R.drawable.ais_land); + this.bitmapValid = true; break; case AIS_AIRPLANE: this.bitmap = mapLayer.getBitmap(R.drawable.ais_plane); + this.bitmapValid = true; break; case AIS_SART: this.bitmap = mapLayer.getBitmap(R.drawable.ais_sar); + this.bitmapValid = true; break; case AIS_ATON: this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton); + this.bitmapValid = true; break; case AIS_ATON_VIRTUAL: this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton_virt); + this.bitmapValid = true; break; } } @@ -453,7 +466,7 @@ private void setColor() { public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { - if ((this.bitmap == null) || isLost(vesselLostTimeoutInMinutes)) { + if ((!this.bitmapValid) || isLost(vesselLostTimeoutInMinutes)) { setBitmap(mapLayer); } if (checkCpaWarning()) { @@ -572,14 +585,6 @@ private boolean checkForCpaTimeout() { public static void setCpaWarningTime(int warningTime) { cpaWarningTime = warningTime; } public static void setCpaWarningDistance(float warningDistance) { cpaWarningDistance = warningDistance; } public static void setOwnPosition(Location position) { ownPosition = position; } -/* public static void setOwnPosition(Location position) { - ownPosition = position; - if (ownPosition != null) { - ownPosition.setBearing(180.0f); // test - ownPosition.setSpeed(0.1f); // test (m/s) - } - } - */ /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed @@ -663,18 +668,6 @@ public String getShipTypeString() { return("WIG, Hazardous category C"); case 24: return("WIG, Hazardous category D"); - case 40: - return("High Speed Craft (HSC)"); - case 41: - return("HSC, Hazardous category A"); - case 42: - return("HSC, Hazardous category B"); - case 43: - return("HSC, Hazardous category C"); - case 44: - return("HSC, Hazardous category D"); - case 49: // HSC, No additional information - return("High Speed Craft (HSC)"); case 30: return("Fishing"); case 31: @@ -687,6 +680,22 @@ public String getShipTypeString() { return("Diving ops"); case 35: return("Military ops"); + case 36: + return("Sailing"); + case 37: + return("Pleasure Craft"); + case 40: + return("High Speed Craft (HSC)"); + case 41: + return("HSC, Hazardous category A"); + case 42: + return("HSC, Hazardous category B"); + case 43: + return("HSC, Hazardous category C"); + case 44: + return("HSC, Hazardous category D"); + case 49: // HSC, No additional information + return("High Speed Craft (HSC)"); case 50: return("Pilot Vessel"); case 51: @@ -707,10 +716,6 @@ public String getShipTypeString() { return("Medical Transport"); case 59: return("Noncombatant ship according to RR Resolution No. 18"); - case 36: - return("Sailing"); - case 37: - return("Pleasure Craft"); case 60: return("Passenger"); case 61: @@ -722,7 +727,7 @@ public String getShipTypeString() { case 64: return("Passenger, Hazardous category D"); case 69: // Passenger, No additional information - return("Passenger"); + return("Passenger/Cruise/Ferry"); case 70: // Cargo, all ships of this type return("Cargo"); case 71: diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 39f998700de..6406bfeffd1 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -4,7 +4,6 @@ import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; import android.annotation.SuppressLint; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -118,7 +117,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, if (position != null) { addMenuItem("Position", LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + - ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { Location ownPosition = app.getLocationProvider().getLastKnownLocation(); AisObject.setOwnPosition(ownPosition); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 8cf38adae8d..12c355436f3 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -29,22 +29,22 @@ import net.osmand.plus.views.layers.ContextMenuLayer; import net.osmand.plus.views.layers.base.OsmandMapLayer; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; public class AisTrackerLayer extends OsmandMapLayer implements ContextMenuLayer.IContextMenuProvider { private static final int START_ZOOM = 10; private final AisTrackerPlugin plugin; - private Map aisObjectList; - private final int aisObjectListCounterMax = 100; + private ConcurrentMap aisObjectList; + private static final int aisObjectListCounterMax = 100; private final Context context; - private Paint bitmapPaint; + private final Paint bitmapPaint; private Timer timer; - private TimerTask taskCheckAisObjectList; private AisMessageListener listener; public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugin) { super(context); @@ -52,7 +52,7 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi this.context = context; this.listener = null; - this.aisObjectList = new HashMap<>(); + this.aisObjectList = new ConcurrentHashMap<>(); this.bitmapPaint = new Paint(); this.bitmapPaint.setAntiAlias(true); this.bitmapPaint.setFilterBitmap(true); @@ -66,7 +66,7 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi startNetworkListener(); // for test purposes: remove later... - initTestObjects(); + //initTestObjects(); //testCpa(); } @@ -272,7 +272,8 @@ private void initTestObjects() { //removeLostAisObjects(); } private void initTimer() { - this.taskCheckAisObjectList = new TimerTask() { + TimerTask taskCheckAisObjectList; + taskCheckAisObjectList = new TimerTask() { @Override public void run() { Log.d("AisTrackerLayer", "timer task taskCheckAisObjectList running"); @@ -346,7 +347,7 @@ public void updateAisObjectList(@NonNull AisObject ais) { if (obj == null) { Log.d("AisTrackerLayer", "add AIS object with MMSI " + ais.getMmsi()); aisObjectList.put(mmsi, new AisObject(ais)); - if (aisObjectList.size() >= this.aisObjectListCounterMax) { + if (aisObjectList.size() >= aisObjectListCounterMax) { this.removeOldestAisObjectListEntry(); } } else { From 2a435cb0e677ba033874ec90be720f53985a901a Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 21 Jul 2024 23:09:10 +0200 Subject: [PATCH 19/74] use getCurrentLocation() instead of getLocation() for CPA calculations --- .../plus/plugins/aistracker/AisObject.java | 18 ++++++++++++++++-- .../aistracker/AisObjectMenuController.java | 2 +- .../plugins/aistracker/AisTrackerHelper.java | 4 ++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 3a0e6183648..6583c90670b 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -32,6 +32,7 @@ import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.CPA_UPDATE_TIMEOUT_IN_SECONDS; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getNewPosition; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_DEFAULT_WARNING_TIME; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_WARNING_DEFAULT_DISTANCE; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_OBJ_LOST_DEFAULT_TIMEOUT; @@ -550,9 +551,9 @@ private boolean needRotation() { private boolean checkCpaWarning() { if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0)) { if (checkForCpaTimeout() && (ownPosition != null)) { - Location aisPosition = getLocation(); + Location aisPosition = getCurrentLocation(); if (aisPosition != null) { - getCpa(ownPosition, getLocation(), cpa); + getCpa(ownPosition, aisPosition, cpa); lastCpaUpdate = System.currentTimeMillis(); } } @@ -637,6 +638,19 @@ public Location getLocation() { } return null; } + /* in contrast to getLocation(), this method considers the timestamp of the creation + * of the AIS object and adjusts the received position using the time difference + * between now and the timestamp (assuming that course and speed is constant) */ + @Nullable + public Location getCurrentLocation() { + Location loc = getLocation(); + Location newLocation = null; + if (loc != null) { + double ageInHours = (System.currentTimeMillis() - this.lastUpdate) / 1000.0 / 3600.0; + newLocation = getNewPosition(loc, ageInHours); + } + return newLocation; + } @Nullable public String getCallSign() { return this.ais_callSign; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 6406bfeffd1..37d745cb436 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -44,7 +44,7 @@ private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet Date: Tue, 23 Jul 2024 22:49:14 +0200 Subject: [PATCH 20/74] add TCP connection reset after mapActivityResume in special situations --- .../plugins/aistracker/AisMessageListener.java | 5 +++++ .../plus/plugins/aistracker/AisObject.java | 11 +++++++++++ .../plugins/aistracker/AisTrackerLayer.java | 17 +++++++++++++++++ .../plugins/aistracker/AisTrackerPlugin.java | 12 +++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java index 785907fb816..9a37998c8be 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -153,6 +153,11 @@ public void stopListener() { udpSocket.close(); } } + + public boolean checkTcpSocket() { + return (tcpSocket != null) && (tcpStream != null); + } + private void handleAisMessage(int aisType, Object obj) { AisObject ais = null; int msgType = 0; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 6583c90670b..e8c4bd9e32a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -88,7 +88,10 @@ public class AisObject { private String ais_destination = null; private String countryCode = null; private SortedSet msgTypes = null; + /* timestamp of last AIS message received for the current instance: */ private long lastUpdate = 0; + /* timestamp of last AIS message received for all instances: */ + private static long lastMessageReceived = 0; /* after this time of missing AIS signal the object is outdated and can be removed: */ private static int maxObjectAgeInMinutes = AIS_OBJ_LOST_DEFAULT_TIMEOUT; /* after this time of missing AIS signal the vessel symbol can change to mark "lost": */ @@ -202,6 +205,7 @@ private void initObj(int mmsi, int msgType) { this.countryCode = getCountryCode(this.ais_mmsi); this.msgTypes.add(ais_msgType); this.lastUpdate = System.currentTimeMillis(); + lastMessageReceived = this.lastUpdate; } private void initLatLon(double lat, double lon) { if ((lat != INVALID_LAT) && (lon != INVALID_LON)) { @@ -378,6 +382,7 @@ public void set(@NonNull AisObject ais) { /* this method does not produce an exact copy of the given object, here are the differences: */ this.lastUpdate = System.currentTimeMillis(); + lastMessageReceived = this.lastUpdate; if (this.msgTypes == null) { this.msgTypes = new TreeSet<>(); } @@ -667,6 +672,12 @@ public String getDestination() { public String getCountryCode() { return this.countryCode; } public AisObjType getObjectClass() { return this.objectClass; } public long getLastUpdate() { return this.lastUpdate; } + public static long getLastMessageReceived() { return lastMessageReceived; } + public static long getAndUpdateLastMessageReceived() { + long timestamp = getLastMessageReceived(); + lastMessageReceived = System.currentTimeMillis(); + return timestamp; + } @NonNull public String getShipTypeString() { switch (this.ais_shipType) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 12c355436f3..76e5a124132 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -297,6 +297,23 @@ private void stopNetworkListener() { this.listener = null; } } + + /* this method restarts the TCP listeners after a "resume" event (the smartphone resumed + * from sleep or from switched off state): in this case the TCP connection might be broken, + * but the sockets are still (logically) open. + * as additional indication of a broken TCP connection it is checked whether any AIS message + * was received in the last 20 seconds */ + public void checkTcpConnection() { + if (listener != null) { + if (listener.checkTcpSocket()) { + if (((System.currentTimeMillis() - AisObject.getAndUpdateLastMessageReceived()) / 1000) > 20) { + Log.d("AisTrackerLayer", "checkTcpConnection(): restart TCP socket"); + restartNetworkListener(); + } + } + } + } + public void restartNetworkListener() { stopNetworkListener(); startNetworkListener(); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index ad3545afd5a..86574c602fa 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -88,7 +88,10 @@ public int getVersion() { public String getComponentId1() { return COMPONENT; } - + @Override + public String getComponentId2() { + return "net.osmand.dev"; // for test purposes to enable logcat at adb connected physical device + } @Override public CharSequence getDescription(boolean linksEnabled) { return app.getString(R.string.plugin_aistracker_description).concat("\n\n").concat(app.getString(R.string.plugin_aistracker_disclaimer)); @@ -137,6 +140,13 @@ public String getPrefsDescription() { return app.getString(R.string.ais_address_settings_description); } + @Override + public void mapActivityResume(@NonNull MapActivity activity) { + if (aisTrackerLayer != null) { + aisTrackerLayer.checkTcpConnection(); + } + } + @Override public void updateLayers(@NonNull Context context, @Nullable MapActivity mapActivity) { OsmandMapTileView mapView = app.getOsmandMap().getMapView(); From a00ce91f6c4a7e2f9054565ba7db8b472fc2b8fe Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 23 Jul 2024 23:21:07 +0200 Subject: [PATCH 21/74] added icon in the top area of the context menu --- .../aistracker/AisObjectMenuController.java | 9 ++++++- .../plugins/aistracker/AisTrackerLayer.java | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 37d745cb436..65dd219306e 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -13,6 +13,7 @@ import net.osmand.data.LatLon; import net.osmand.data.PointDescription; import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.mapcontextmenu.MenuController; @@ -33,7 +34,13 @@ public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointD builder.setShowNearestPoi(false); builder.setShowOnlinePhotos(false); builder.setShowNearestWiki(false); - // TODO: show an icon in the menu + } + + @Override + public int getRightIconId() { return R.drawable.ic_plugin_nautical_map; } + @Override + public boolean isBigRightIcon() { + return true; } @SuppressLint("DefaultLocale") diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 76e5a124132..8a33c9203a2 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -66,7 +66,10 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi startNetworkListener(); // for test purposes: remove later... - //initTestObjects(); + //initTestObject1(); + //initTestObject2(); + //initTestObject3(); + //initTestObject4(); //testCpa(); } @@ -240,7 +243,8 @@ private void testCpa() { Log.d("AisTrackerLayer", "# test4: dist1: " + meterToMiles(y4.distanceTo(y5))); } } - private void initTestObjects() { + + private void initTestObject1() { // passenger ship AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, 320.0, 8.4, 50.738d, 7.099d, 0.0); @@ -249,28 +253,32 @@ private void initTestObjects() { 65, 8, 12, 2, "Potsdam", 8, 15, 22, 5); updateAisObjectList(ais); + } + private void initTestObject2() { // sailing boat - ais = new AisObject(454011, 1, 20, 8, 0, 120, + AisObject ais = new AisObject(454011, 1, 20, 8, 0, 120, 125.0, 4.4, 50.737d, 7.098d, 0.0); updateAisObjectList(ais); ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, 0, 0, 0, 0, "@@@", 0, 0, 0, 0); updateAisObjectList(ais); + } + private void initTestObject3() { // land station - ais = new AisObject(878121, 4, 50.736d, 7.100d); + AisObject ais = new AisObject(878121, 4, 50.736d, 7.100d); updateAisObjectList(ais); // AIDS ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, 0, 0, 0, 0); updateAisObjectList(ais); + } + private void initTestObject4() { // aircraft - ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); + AisObject ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); updateAisObjectList(ais); - - //removeOldestAisObjectListEntry(); - //removeLostAisObjects(); } + private void initTimer() { TimerTask taskCheckAisObjectList; taskCheckAisObjectList = new TimerTask() { From 460b7cbe0e03c68306787f1476c6f1d54c5e19ea Mon Sep 17 00:00:00 2001 From: Falk Date: Mon, 29 Jul 2024 20:01:13 +0200 Subject: [PATCH 22/74] do not show negative CPA times in context menu --- .../plus/plugins/aistracker/AisObjectMenuController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 65dd219306e..cc9a084a783 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -70,10 +70,12 @@ private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet Date: Wed, 31 Jul 2024 23:01:04 +0200 Subject: [PATCH 23/74] change bitmap+color handling --- .../plus/plugins/aistracker/AisObject.java | 103 +++++++++--------- .../aistracker/AisObjectConstants.java | 1 + 2 files changed, 51 insertions(+), 53 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index e8c4bd9e32a..1523cf5a2fb 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -395,6 +395,51 @@ public void set(@NonNull AisObject ais) { this.bitmapColor = 0; } + public static int selectBitmap(AisObjType objType) { + switch (objType) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + case AIS_INVALID: + return R.drawable.ais_vessel; + case AIS_VESSEL_LOST: + return R.drawable.ais_vessel_cross; + case AIS_LANDSTATION: + return R.drawable.ais_land; + case AIS_AIRPLANE: + return R.drawable.ais_plane; + case AIS_SART: + return R.drawable.ais_sar; + case AIS_ATON: + return R.drawable.ais_aton; + case AIS_ATON_VIRTUAL: + return R.drawable.ais_aton_virt; + } + return -1; + } + + public static int selectColor(AisObjType objType) { + switch (objType) { + case AIS_VESSEL: + return Color.GREEN; + case AIS_VESSEL_SPORT: + return Color.YELLOW; + case AIS_VESSEL_FAST: + return Color.BLUE; + case AIS_VESSEL_PASSENGER: + return Color.CYAN; + case AIS_VESSEL_FREIGHT: + return Color.GRAY; + case AIS_VESSEL_COMMERCIAL: + return Color.LTGRAY; + default: + return 0; // black + } + } + private void setBitmap(@NonNull AisTrackerLayer mapLayer) { invalidateBitmap(); if (isLost(vesselLostTimeoutInMinutes)) { @@ -403,37 +448,10 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { this.bitmapValid = true; } } else { - switch (this.objectClass) { - case AIS_VESSEL: - case AIS_VESSEL_SPORT: - case AIS_VESSEL_FAST: - case AIS_VESSEL_PASSENGER: - case AIS_VESSEL_FREIGHT: - case AIS_VESSEL_COMMERCIAL: - case AIS_INVALID: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel); - this.bitmapValid = true; - break; - case AIS_LANDSTATION: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_land); - this.bitmapValid = true; - break; - case AIS_AIRPLANE: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_plane); - this.bitmapValid = true; - break; - case AIS_SART: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_sar); - this.bitmapValid = true; - break; - case AIS_ATON: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton); - this.bitmapValid = true; - break; - case AIS_ATON_VIRTUAL: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton_virt); - this.bitmapValid = true; - break; + int bitmapId = selectBitmap(this.objectClass); + if (bitmapId >= 0) { + this.bitmap = mapLayer.getBitmap(bitmapId); + this.bitmapValid = true; } } this.setColor(); @@ -445,28 +463,7 @@ private void setColor() { this.bitmapColor = 0; // black } } else { - switch (this.objectClass) { - case AIS_VESSEL: - this.bitmapColor = Color.GREEN; - break; - case AIS_VESSEL_SPORT: - this.bitmapColor = Color.YELLOW; - break; - case AIS_VESSEL_FAST: - this.bitmapColor = Color.BLUE; - break; - case AIS_VESSEL_PASSENGER: - this.bitmapColor = Color.CYAN; - break; - case AIS_VESSEL_FREIGHT: - this.bitmapColor = Color.GRAY; - break; - case AIS_VESSEL_COMMERCIAL: - this.bitmapColor = Color.LTGRAY; - break; - default: - this.bitmapColor = 0; // black - } + this.bitmapColor = selectColor(this.objectClass); } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index afb42fa526d..7eb76b0b8b7 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -31,6 +31,7 @@ public static enum AisObjType { AIS_VESSEL_PASSENGER, AIS_VESSEL_FREIGHT, AIS_VESSEL_COMMERCIAL, + AIS_VESSEL_LOST, // only dummy value, not assigned by a real vessel AIS_LANDSTATION, AIS_AIRPLANE, AIS_SART, From c3081a2d4981bb0529453b81795035b1972f6a73 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 4 Aug 2024 18:51:19 +0200 Subject: [PATCH 24/74] adjust logic for bitmap/color selection --- .../plus/plugins/aistracker/AisObject.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 1523cf5a2fb..3953d58c049 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -467,21 +467,29 @@ private void setColor() { } } - public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, - @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { - if ((!this.bitmapValid) || isLost(vesselLostTimeoutInMinutes)) { + private void updateBitmap(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint) { + if (isLost(vesselLostTimeoutInMinutes)) { setBitmap(mapLayer); - } - if (checkCpaWarning()) { - activateCpaWarning(); } else { - deactivateCpaWarning(); + if (!this.bitmapValid) { + setBitmap(mapLayer); + } + if (checkCpaWarning()) { + activateCpaWarning(); + } else { + deactivateCpaWarning(); + } } if (this.bitmapColor != 0) { paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); } else { paint.setColorFilter(null); } + } + + public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, + @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { + updateBitmap(mapLayer, paint); if (this.bitmap != null) { canvas.save(); canvas.rotate(tileBox.getRotate(), (float)tileBox.getCenterPixelX(), (float)tileBox.getCenterPixelY()); From a8a4d77dc187ea237c550ece2c2a3eb3725f7015 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 11 Aug 2024 16:39:56 +0200 Subject: [PATCH 25/74] adjustments after merge --- OsmAnd-java/build.gradle | 4 + OsmAnd/res/values/strings.xml | 5 - .../palette/ColorsMigrationAlgorithm.java | 147 --- .../main/HorizontalSpaceItemDecoration.java | 25 - .../palette/main/PaletteColorsComparator.java | 27 - .../main/data/ColorsCollectionBundle.java | 47 - .../main/data/PredefinedPaletteColor.java | 36 - .../editors/EditorColorController.java | 58 - .../plus/notifications/ErrorNotification.java | 86 -- .../osmand/plus/plugins/PluginsHelper.java | 2 + .../controllers/ProfileColorController.java | 93 -- .../fragments/ProfileAppearanceFragment.java | 1019 ----------------- .../fragments/SettingsScreenType.java | 4 +- gradle.properties | 26 +- 14 files changed, 18 insertions(+), 1561 deletions(-) delete mode 100644 OsmAnd/src/net/osmand/plus/card/color/palette/ColorsMigrationAlgorithm.java delete mode 100644 OsmAnd/src/net/osmand/plus/card/color/palette/main/HorizontalSpaceItemDecoration.java delete mode 100644 OsmAnd/src/net/osmand/plus/card/color/palette/main/PaletteColorsComparator.java delete mode 100644 OsmAnd/src/net/osmand/plus/card/color/palette/main/data/ColorsCollectionBundle.java delete mode 100644 OsmAnd/src/net/osmand/plus/card/color/palette/main/data/PredefinedPaletteColor.java delete mode 100644 OsmAnd/src/net/osmand/plus/mapcontextmenu/editors/EditorColorController.java delete mode 100644 OsmAnd/src/net/osmand/plus/notifications/ErrorNotification.java delete mode 100644 OsmAnd/src/net/osmand/plus/settings/controllers/ProfileColorController.java delete mode 100644 OsmAnd/src/net/osmand/plus/settings/fragments/ProfileAppearanceFragment.java diff --git a/OsmAnd-java/build.gradle b/OsmAnd-java/build.gradle index 1155d6bac22..0a02117887a 100644 --- a/OsmAnd-java/build.gradle +++ b/OsmAnd-java/build.gradle @@ -6,6 +6,10 @@ configurations { android } +test { + exclude '**/*' +} + tasks.withType(JavaCompile).configureEach { sourceCompatibility = "17" targetCompatibility = "17" diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index bacf3d83c23..1b15bda372d 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -21,7 +21,6 @@ A toggle to show or hide %1$s on the map. Hugerock Promo for %1$s months Free access to features including unlimited map downloads, 3D relief etc. for %1$s month - UK and similar India Keep left Keep right @@ -49,7 +48,6 @@ Terrain color scheme Start point Destination - Next destination point To My Location Map to the left Map to the right @@ -65,9 +63,6 @@ Simulate Display position always in center Terrain colorization type - Underlay - Overlay - Map style First intermediate Audio note Video note diff --git a/OsmAnd/src/net/osmand/plus/card/color/palette/ColorsMigrationAlgorithm.java b/OsmAnd/src/net/osmand/plus/card/color/palette/ColorsMigrationAlgorithm.java deleted file mode 100644 index 32bd6d6922a..00000000000 --- a/OsmAnd/src/net/osmand/plus/card/color/palette/ColorsMigrationAlgorithm.java +++ /dev/null @@ -1,147 +0,0 @@ -package net.osmand.plus.card.color.palette; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import net.osmand.plus.OsmandApplication; -import net.osmand.plus.card.color.palette.main.data.ColorsCollection; -import net.osmand.plus.card.color.palette.main.data.ColorsCollectionBundle; -import net.osmand.plus.card.color.palette.main.data.PaletteColor; -import net.osmand.plus.settings.backend.ApplicationMode; -import net.osmand.plus.settings.backend.OsmandSettings; -import net.osmand.plus.settings.backend.preferences.CommonPreference; -import net.osmand.plus.settings.backend.preferences.ListStringPreference; -import net.osmand.util.Algorithms; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class ColorsMigrationAlgorithm { - - private final OsmandSettings settings; - - private final ListStringPreference CUSTOM_TRACK_COLORS; - private final ListStringPreference CUSTOM_ROUTE_LINE_COLORS; - private final ListStringPreference CUSTOM_ICON_COLORS; - - private long timestamp; - - private ColorsMigrationAlgorithm(@NonNull OsmandApplication app) { - this.settings = app.getSettings(); - - CUSTOM_TRACK_COLORS = (ListStringPreference) new ListStringPreference( - settings, "custom_track_colors", null, ",").makeGlobal(); - - CUSTOM_ROUTE_LINE_COLORS = (ListStringPreference) new ListStringPreference( - settings, "custom_route_line_colors", null, ",").makeGlobal(); - - CUSTOM_ICON_COLORS = (ListStringPreference) new ListStringPreference( - settings, "custom_icon_colors", null, ",").makeProfile(); - } - - private void execute() { - timestamp = System.currentTimeMillis(); - - List migrationBundles = Arrays.asList( - new MigrationBundle( - CUSTOM_TRACK_COLORS, settings.TRACK_COLORS_PALETTE, - settings.CUSTOM_TRACK_PALETTE_COLORS - ), - new MigrationBundle( - CUSTOM_TRACK_COLORS, settings.POINT_COLORS_PALETTE, - settings.CUSTOM_TRACK_PALETTE_COLORS - ), - new MigrationBundle( - CUSTOM_ROUTE_LINE_COLORS, settings.ROUTE_LINE_COLORS_PALETTE, null - ), - new MigrationBundle( - CUSTOM_ICON_COLORS, settings.PROFILE_COLORS_PALETTE, null - ) - ); - - for (MigrationBundle migrationBundle : migrationBundles) { - doMigration(migrationBundle); - } - } - - private void doMigration(@NonNull MigrationBundle migrationBundle) { - if (migrationBundle.isGlobalPalette()) { - doMigration(migrationBundle, null); - } else { - for (ApplicationMode appMode : ApplicationMode.allPossibleValues()) { - doMigration(migrationBundle, appMode); - } - } - } - - private void doMigration(@NonNull MigrationBundle migrationBundle, - @Nullable ApplicationMode appMode) { - ListStringPreference oldPreference = migrationBundle.oldPreference; - List customColorInts = collectCustomColorInts(oldPreference, appMode); - - List customColors = new ArrayList<>(); - if (!Algorithms.isEmpty(customColorInts)) { - for (int colorInt : customColorInts) { - String id = PaletteColor.generateId(timestamp); - customColors.add(new PaletteColor(id, colorInt, timestamp)); - timestamp += 10; - } - } - - ColorsCollectionBundle bundle = new ColorsCollectionBundle(); - bundle.appMode = appMode; - bundle.paletteColors = customColors; - bundle.palettePreference = migrationBundle.newPreference; - bundle.customColorsPreference = migrationBundle.customColorsPreference; - ColorsCollection colorsCollection = new ColorsCollection(bundle); - colorsCollection.saveToPreferences(); - } - - public static List collectCustomColorInts(@NonNull ListStringPreference colorsPreference, - @Nullable ApplicationMode appMode) { - List colors = new ArrayList<>(); - List colorNames; - if (appMode == null) { - colorNames = colorsPreference.getStringsList(); - } else { - colorNames = colorsPreference.getStringsListForProfile(appMode); - } - if (colorNames != null) { - for (String colorHex : colorNames) { - try { - if (!Algorithms.isEmpty(colorHex)) { - int color = Algorithms.parseColor(colorHex); - colors.add(color); - } - } catch (IllegalArgumentException ignored) { - } - } - } - return colors; - } - - public static void doMigration(@NonNull OsmandApplication app) { - ColorsMigrationAlgorithm migrationAlgorithm = new ColorsMigrationAlgorithm(app); - migrationAlgorithm.execute(); - } - - static class MigrationBundle { - - ListStringPreference oldPreference; - CommonPreference newPreference; - CommonPreference customColorsPreference; - - public MigrationBundle(@NonNull ListStringPreference oldPreference, - @NonNull CommonPreference newPreference, - @Nullable CommonPreference customColorsPreference) { - this.oldPreference = oldPreference; - this.newPreference = newPreference; - this.customColorsPreference = customColorsPreference; - } - - public boolean isGlobalPalette() { - return oldPreference.isGlobal(); - } - } -} diff --git a/OsmAnd/src/net/osmand/plus/card/color/palette/main/HorizontalSpaceItemDecoration.java b/OsmAnd/src/net/osmand/plus/card/color/palette/main/HorizontalSpaceItemDecoration.java deleted file mode 100644 index 5e3e449730b..00000000000 --- a/OsmAnd/src/net/osmand/plus/card/color/palette/main/HorizontalSpaceItemDecoration.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.osmand.plus.card.color.palette.main; - -import android.graphics.Rect; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -class HorizontalSpaceItemDecoration extends RecyclerView.ItemDecoration { - - private final int horizontalSpaceDp; - - public HorizontalSpaceItemDecoration(int horizontalSpaceDp) { - this.horizontalSpaceDp = horizontalSpaceDp; - } - - @Override - public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, - @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { - // Apply horizontal spacing to all items except the first one - if (parent.getChildAdapterPosition(view) != 0) { - outRect.left = horizontalSpaceDp; - } - } -} diff --git a/OsmAnd/src/net/osmand/plus/card/color/palette/main/PaletteColorsComparator.java b/OsmAnd/src/net/osmand/plus/card/color/palette/main/PaletteColorsComparator.java deleted file mode 100644 index 979971ad44e..00000000000 --- a/OsmAnd/src/net/osmand/plus/card/color/palette/main/PaletteColorsComparator.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.osmand.plus.card.color.palette.main; - -import androidx.annotation.NonNull; - -import net.osmand.plus.card.color.palette.main.data.PaletteColor; -import net.osmand.plus.card.color.palette.main.data.PaletteSortingMode; - -import java.util.Comparator; - -public class PaletteColorsComparator implements Comparator { - - private final PaletteSortingMode sortingMode; - - public PaletteColorsComparator(@NonNull PaletteSortingMode sortingMode) { - this.sortingMode = sortingMode; - } - - @Override - public int compare(PaletteColor o1, PaletteColor o2) { - if (sortingMode == PaletteSortingMode.LAST_USED_TIME) { - return Long.compare(o2.getLastUsedTime(), o1.getLastUsedTime()); - } - // Otherwise, use original order - return 0; - } - -} diff --git a/OsmAnd/src/net/osmand/plus/card/color/palette/main/data/ColorsCollectionBundle.java b/OsmAnd/src/net/osmand/plus/card/color/palette/main/data/ColorsCollectionBundle.java deleted file mode 100644 index e5899d8f41e..00000000000 --- a/OsmAnd/src/net/osmand/plus/card/color/palette/main/data/ColorsCollectionBundle.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.osmand.plus.card.color.palette.main.data; - -import androidx.annotation.Nullable; - -import net.osmand.plus.settings.backend.ApplicationMode; -import net.osmand.plus.settings.backend.preferences.CommonPreference; - -import java.util.List; - -/** - * Initial bundle for colors collection. - * Optional fields are marked as 'nullable' and uses only for particular purposes. - */ -public class ColorsCollectionBundle { - - /** - * Uses only for profile dependent preferences. - */ - @Nullable - public ApplicationMode appMode; - - /** - * Predefined colors for the current palette. - */ - public List predefinedColors; - - /** - * Uses only for migration purposes from old to a new app version, - * when we started to use palette colors instead of the simple int colors. - */ - @Nullable - public List paletteColors; - - /** - * Preference that include all (predefined and custom) colors for the current palette. - */ - public CommonPreference palettePreference; - - /** - * Preference with custom palette colors. - * Uses only when the same custom colors are used for more then one palette preference, - * but when those palettes have different predefined colors. - */ - @Nullable - public CommonPreference customColorsPreference; - -} diff --git a/OsmAnd/src/net/osmand/plus/card/color/palette/main/data/PredefinedPaletteColor.java b/OsmAnd/src/net/osmand/plus/card/color/palette/main/data/PredefinedPaletteColor.java deleted file mode 100644 index bef9f796dc0..00000000000 --- a/OsmAnd/src/net/osmand/plus/card/color/palette/main/data/PredefinedPaletteColor.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.osmand.plus.card.color.palette.main.data; - -import android.content.Context; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -public class PredefinedPaletteColor extends PaletteColor { - - @StringRes - private final int nameId; - private final String name; - - public PredefinedPaletteColor(@NonNull String id, @ColorInt int color, @StringRes int nameId) { - this(id, color, nameId, null); - } - - public PredefinedPaletteColor(@NonNull String id, @ColorInt int color, @NonNull String name) { - this(id, color, -1, name); - } - - private PredefinedPaletteColor(@NonNull String id, @ColorInt int color, - @StringRes int nameId, @Nullable String name) { - super(id, color, 0); - this.nameId = nameId; - this.name = name; - } - - @NonNull - @Override - public String toHumanString(@NonNull Context context) { - return name != null ? name : context.getString(nameId); - } -} diff --git a/OsmAnd/src/net/osmand/plus/mapcontextmenu/editors/EditorColorController.java b/OsmAnd/src/net/osmand/plus/mapcontextmenu/editors/EditorColorController.java deleted file mode 100644 index 4442543a3ec..00000000000 --- a/OsmAnd/src/net/osmand/plus/mapcontextmenu/editors/EditorColorController.java +++ /dev/null @@ -1,58 +0,0 @@ -package net.osmand.plus.mapcontextmenu.editors; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; - -import net.osmand.plus.OsmandApplication; -import net.osmand.plus.base.dialog.DialogManager; -import net.osmand.plus.base.dialog.interfaces.controller.IDialogController; -import net.osmand.plus.card.color.palette.main.ColorsPaletteController; -import net.osmand.plus.card.color.palette.main.OnColorsPaletteListener; -import net.osmand.plus.card.color.palette.main.data.ColorsCollection; -import net.osmand.plus.card.color.palette.main.data.ColorsCollectionBundle; -import net.osmand.plus.card.color.palette.main.data.DefaultColors; -import net.osmand.plus.card.color.palette.main.data.PaletteColor; -import net.osmand.plus.settings.backend.OsmandSettings; - -import java.util.Arrays; -import java.util.List; - -public class EditorColorController extends ColorsPaletteController implements IDialogController { - - private static final String PROCESS_ID = "select_map_point_color"; - - public EditorColorController(@NonNull OsmandApplication app, - @NonNull ColorsCollection colorsCollection, - @ColorInt int selectedColorInt) { - super(app, colorsCollection, selectedColorInt); - } - - public static void onDestroy(@NonNull OsmandApplication app) { - DialogManager manager = app.getDialogManager(); - manager.unregister(PROCESS_ID); - } - - public static List getPredefinedColors() { - return Arrays.asList(DefaultColors.values()); - } - - @NonNull - public static EditorColorController getInstance(@NonNull OsmandApplication app, - @NonNull OnColorsPaletteListener listener, - @ColorInt int selectedColor) { - OsmandSettings settings = app.getSettings(); - DialogManager dialogManager = app.getDialogManager(); - EditorColorController controller = (EditorColorController) dialogManager.findController(PROCESS_ID); - if (controller == null) { - ColorsCollectionBundle bundle = new ColorsCollectionBundle(); - bundle.predefinedColors = getPredefinedColors(); - bundle.palettePreference = settings.POINT_COLORS_PALETTE; - bundle.customColorsPreference = settings.CUSTOM_TRACK_PALETTE_COLORS; - ColorsCollection colorsCollection = new ColorsCollection(bundle); - controller = new EditorColorController(app, colorsCollection, selectedColor); - dialogManager.register(PROCESS_ID, controller); - } - controller.setPaletteListener(listener); - return controller; - } -} diff --git a/OsmAnd/src/net/osmand/plus/notifications/ErrorNotification.java b/OsmAnd/src/net/osmand/plus/notifications/ErrorNotification.java deleted file mode 100644 index fc8a3adfd4b..00000000000 --- a/OsmAnd/src/net/osmand/plus/notifications/ErrorNotification.java +++ /dev/null @@ -1,86 +0,0 @@ -package net.osmand.plus.notifications; - -import android.content.Intent; - -import androidx.core.app.NotificationCompat; - -import net.osmand.plus.NavigationService; -import net.osmand.plus.OsmandApplication; -import net.osmand.plus.plugins.PluginsHelper; -import net.osmand.plus.R; -import net.osmand.plus.activities.MapActivity; -import net.osmand.plus.plugins.monitoring.OsmandMonitoringPlugin; -import net.osmand.plus.routing.RoutingHelper; - -public class ErrorNotification extends OsmandNotification { - - private static final String GROUP_NAME = "ERROR"; - - public ErrorNotification(OsmandApplication app) { - super(app, GROUP_NAME); - } - - @Override - public void init() { - } - - @Override - public NotificationType getType() { - return NotificationType.ERROR; - } - - @Override - public int getPriority() { - return NotificationCompat.PRIORITY_DEFAULT; - } - - @Override - public boolean isActive() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } - - @Override - public Intent getContentIntent() { - return new Intent(app, MapActivity.class); - } - - @Override - public NotificationCompat.Builder buildNotification(boolean wearable) { - String notificationTitle; - String notificationText; - icon = R.drawable.ic_notification_bug; - notificationTitle = app.getString(R.string.shared_string_unexpected_error); - - NavigationService service = app.getNavigationService(); - RoutingHelper routingHelper = app.getRoutingHelper(); - - boolean following = routingHelper.isFollowingMode(); - boolean planning = routingHelper.isRoutePlanningMode(); - boolean pause = routingHelper.isPauseNavigation(); - - boolean gpxEnabled = PluginsHelper.isActive(OsmandMonitoringPlugin.class); - String usedBy = service != null ? "" + service.getUsedBy() : "X"; - - notificationText = "Info: " + (following ? "1" : "") + (planning ? "2" : "") + (pause ? "3" : "") + (gpxEnabled ? "4" : "") + "-" + usedBy + ". " - + app.getString(R.string.error_notification_desc); - - return createBuilder(wearable) - .setContentTitle(notificationTitle) - .setStyle(new NotificationCompat.BigTextStyle().bigText(notificationText)); - } - - @Override - public int getOsmandNotificationId() { - return ERROR_NOTIFICATION_SERVICE_ID; - } - - @Override - public int getOsmandWearableNotificationId() { - return WEAR_ERROR_NOTIFICATION_SERVICE_ID; - } -} diff --git a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java index 5440bc23917..ef6320e7c20 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java @@ -58,6 +58,7 @@ import net.osmand.plus.plugins.skimaps.SkiMapsPlugin; import net.osmand.plus.plugins.srtm.SRTMPlugin; import net.osmand.plus.plugins.weather.WeatherPlugin; +import net.osmand.plus.plugins.aistracker.AisTrackerPlugin; import net.osmand.plus.poi.PoiUIFilter; import net.osmand.plus.quickaction.QuickActionType; import net.osmand.plus.search.dialogs.QuickSearchDialogFragment; @@ -117,6 +118,7 @@ public static void initPlugins(@NonNull OsmandApplication app) { checkMarketPlugin(app, new SRTMPlugin(app)); allPlugins.add(new WeatherPlugin(app)); checkMarketPlugin(app, new NauticalMapsPlugin(app)); + allPlugins.add(new AisTrackerPlugin(app)); checkMarketPlugin(app, new SkiMapsPlugin(app)); allPlugins.add(new AudioVideoNotesPlugin(app)); checkMarketPlugin(app, new ParkingPositionPlugin(app)); diff --git a/OsmAnd/src/net/osmand/plus/settings/controllers/ProfileColorController.java b/OsmAnd/src/net/osmand/plus/settings/controllers/ProfileColorController.java deleted file mode 100644 index cc9d2c8baff..00000000000 --- a/OsmAnd/src/net/osmand/plus/settings/controllers/ProfileColorController.java +++ /dev/null @@ -1,93 +0,0 @@ -package net.osmand.plus.settings.controllers; - -import static net.osmand.plus.utils.ColorUtilities.getColor; - -import android.content.Context; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentActivity; - -import net.osmand.plus.OsmandApplication; -import net.osmand.plus.base.dialog.DialogManager; -import net.osmand.plus.base.dialog.interfaces.controller.IDialogController; -import net.osmand.plus.card.color.palette.main.ColorsPaletteController; -import net.osmand.plus.card.color.palette.main.OnColorsPaletteListener; -import net.osmand.plus.card.color.palette.main.data.ColorsCollection; -import net.osmand.plus.card.color.palette.main.data.ColorsCollectionBundle; -import net.osmand.plus.card.color.palette.main.data.DefaultColors; -import net.osmand.plus.card.color.palette.main.data.PredefinedPaletteColor; -import net.osmand.plus.card.color.palette.main.data.PaletteColor; -import net.osmand.plus.profiles.ProfileIconColors; -import net.osmand.plus.settings.backend.ApplicationMode; -import net.osmand.plus.settings.backend.OsmandSettings; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class ProfileColorController extends ColorsPaletteController implements IDialogController { - - private static final String PROCESS_ID = "select_profile_color"; - - private ProfileColorController(@NonNull OsmandApplication app, - @NonNull ColorsCollection colorsCollection, - @ColorInt int selectedColor) { - super(app, colorsCollection, selectedColor); - } - - @Override - public int getControlsAccentColor(boolean nightMode) { - if (selectedPaletteColor != null) { - return selectedPaletteColor.getColor(); - } - return super.getControlsAccentColor(nightMode); - } - - @Override - public boolean isAccentColorCanBeChanged() { - return true; - } - - public void onDestroy(@Nullable FragmentActivity activity) { - if (activity != null && !activity.isChangingConfigurations()) { - DialogManager manager = app.getDialogManager(); - manager.unregister(PROCESS_ID); - } - } - - - @NonNull - public static List getPredefinedColors(@NonNull Context context, boolean nightMode) { - List predefinedColors = new ArrayList<>(); - for (ProfileIconColors predefinedColor : ProfileIconColors.values()) { - String id = predefinedColor.name().toLowerCase(); - int colorInt = getColor(context, predefinedColor.getColor(nightMode)); - predefinedColors.add(new PredefinedPaletteColor(id, colorInt, predefinedColor.getName())); - } - return predefinedColors; - } - - @NonNull - public static ProfileColorController getInstance( - @NonNull OsmandApplication app, @NonNull ApplicationMode appMode, - @NonNull OnColorsPaletteListener listener, @ColorInt int selectedColor, - boolean nightMode - ) { - OsmandSettings settings = app.getSettings(); - DialogManager dialogManager = app.getDialogManager(); - ProfileColorController controller = (ProfileColorController) dialogManager.findController(PROCESS_ID); - if (controller == null) { - ColorsCollectionBundle bundle = new ColorsCollectionBundle(); - bundle.appMode = appMode; - bundle.predefinedColors = getPredefinedColors(app, nightMode); - bundle.palettePreference = settings.PROFILE_COLORS_PALETTE; - ColorsCollection colorsCollection = new ColorsCollection(bundle); - controller = new ProfileColorController(app, colorsCollection, selectedColor); - dialogManager.register(PROCESS_ID, controller); - } - controller.setPaletteListener(listener); - return controller; - } -} diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/ProfileAppearanceFragment.java b/OsmAnd/src/net/osmand/plus/settings/fragments/ProfileAppearanceFragment.java deleted file mode 100644 index 07187161bd7..00000000000 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/ProfileAppearanceFragment.java +++ /dev/null @@ -1,1019 +0,0 @@ -package net.osmand.plus.settings.fragments; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.graphics.Matrix; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.LayerDrawable; -import android.os.Bundle; -import android.text.Editable; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import net.osmand.IndexConstants; -import net.osmand.PlatformUtil; -import net.osmand.plus.R; -import net.osmand.plus.activities.MapActivity; -import net.osmand.plus.card.color.palette.main.ColorsPaletteCard; -import net.osmand.plus.card.color.palette.main.OnColorsPaletteListener; -import net.osmand.plus.card.color.palette.main.data.PaletteColor; -import net.osmand.plus.helpers.Model3dHelper; -import net.osmand.plus.profiles.LocationIcon; -import net.osmand.plus.profiles.NavigationIcon; -import net.osmand.plus.profiles.ProfileIconColors; -import net.osmand.plus.profiles.ProfileIcons; -import net.osmand.plus.profiles.SelectBaseProfileBottomSheet; -import net.osmand.plus.profiles.SelectProfileBottomSheet.OnSelectProfileCallback; -import net.osmand.plus.routing.RouteService; -import net.osmand.plus.settings.backend.ApplicationMode; -import net.osmand.plus.settings.backend.backup.FileSettingsHelper.SettingsExportListener; -import net.osmand.plus.settings.backend.backup.items.ProfileSettingsItem; -import net.osmand.plus.settings.controllers.ProfileColorController; -import net.osmand.plus.utils.AndroidUtils; -import net.osmand.plus.utils.ColorUtilities; -import net.osmand.plus.utils.FileUtils; -import net.osmand.plus.utils.UiUtilities; -import net.osmand.plus.widgets.FlowLayout; -import net.osmand.plus.widgets.OsmandTextFieldBoxes; -import net.osmand.plus.widgets.dialogbutton.DialogButton; -import net.osmand.plus.widgets.dialogbutton.DialogButtonType; -import net.osmand.plus.widgets.tools.SimpleTextWatcher; -import net.osmand.util.Algorithms; - -import org.apache.commons.logging.Log; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import androidx.activity.OnBackPressedCallback; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import androidx.recyclerview.widget.RecyclerView; - -import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_SETTINGS_ID; -import static net.osmand.plus.profiles.SelectProfileBottomSheet.PROFILES_LIST_UPDATED_ARG; -import static net.osmand.plus.profiles.SelectProfileBottomSheet.PROFILE_KEY_ARG; -import static net.osmand.plus.settings.backend.ApplicationMode.CUSTOM_MODE_KEY_SEPARATOR; - -public class ProfileAppearanceFragment extends BaseSettingsFragment - implements OnSelectProfileCallback, OnColorsPaletteListener { - - private static final Log LOG = PlatformUtil.getLog(ProfileAppearanceFragment.class); - - public static final String TAG = ProfileAppearanceFragment.class.getName(); - - private static final String MASTER_PROFILE = "master_profile"; - private static final String PROFILE_NAME = "profile_name"; - private static final String SELECT_COLOR = "select_color"; - private static final String SELECT_ICON = "select_icon"; - private static final String COLOR_ITEMS = "color_items"; - private static final String ICON_ITEMS = "icon_items"; - private static final String SELECT_LOCATION_ICON = "select_location_icon"; - private static final String LOCATION_ICON_ITEMS = "location_icon_items"; - private static final String SELECT_NAV_ICON = "select_nav_icon"; - private static final String NAV_ICON_ITEMS = "nav_icon_items"; - - private static final String PROFILE_NAME_KEY = "profile_name_key"; - private static final String PROFILE_STRINGKEY_KEY = "profile_stringkey_key"; - private static final String PROFILE_ICON_RES_KEY = "profile_icon_res_key"; - private static final String PROFILE_COLOR_KEY = "profile_color_key"; - private static final String PROFILE_CUSTOM_COLOR_KEY = "profile_custom_color_key"; - private static final String PROFILE_PARENT_KEY = "profile_parent_key"; - private static final String PROFILE_LOCATION_ICON_KEY = "profile_location_icon_key"; - private static final String PROFILE_NAVIGATION_ICON_KEY = "profile_navigation_icon_key"; - private static final String BASE_PROFILE_FOR_NEW = "base_profile_for_new"; - private static final String IS_BASE_PROFILE_IMPORTED = "is_base_profile_imported"; - private static final String IS_NEW_PROFILE_KEY = "is_new_profile_key"; - - private SettingsExportListener exportListener; - - private ProgressDialog progress; - - private EditText baseProfileName; - private ApplicationProfileObject profile; - private ApplicationProfileObject changedProfile; - private EditText profileName; - private TextView colorName; - private FlowLayout iconItems; - private FlowLayout locationIconItems; - private FlowLayout navIconItems; - private OsmandTextFieldBoxes profileNameOtfb; - private DialogButton saveButton; - - private boolean isBaseProfileImported; - private boolean isNewProfile; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - profile = new ApplicationProfileObject(); - ApplicationMode baseModeForNewProfile = null; - if (getArguments() != null) { - Bundle arguments = getArguments(); - String keyBaseProfileForNew = arguments.getString(BASE_PROFILE_FOR_NEW, null); - baseModeForNewProfile = ApplicationMode.valueOfStringKey(keyBaseProfileForNew, null); - isBaseProfileImported = arguments.getBoolean(IS_BASE_PROFILE_IMPORTED); - } - if (baseModeForNewProfile != null) { - setupAppProfileObjectFromAppMode(baseModeForNewProfile); - profile.parent = baseModeForNewProfile; - profile.stringKey = getUniqueStringKey(baseModeForNewProfile); - } else { - setupAppProfileObjectFromAppMode(getSelectedAppMode()); - } - changedProfile = new ApplicationProfileObject(); - if (savedInstanceState != null) { - restoreState(savedInstanceState); - } else { - changedProfile.stringKey = profile.stringKey; - changedProfile.parent = profile.parent; - if (baseModeForNewProfile != null) { - changedProfile.name = createNonDuplicateName(baseModeForNewProfile.toHumanString()); - } else { - changedProfile.name = profile.name; - } - changedProfile.color = profile.color; - changedProfile.customColor = profile.customColor; - changedProfile.iconRes = profile.iconRes; - changedProfile.routingProfile = profile.routingProfile; - changedProfile.routeService = profile.routeService; - changedProfile.locationIcon = profile.locationIcon; - changedProfile.navigationIcon = profile.navigationIcon; - isNewProfile = ApplicationMode.valueOfStringKey(changedProfile.stringKey, null) == null; - } - requireMyActivity().getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { - public void handleOnBackPressed() { - showExitDialog(); - } - }); - } - - public void setupAppProfileObjectFromAppMode(@NonNull ApplicationMode baseModeForNewProfile) { - profile.stringKey = baseModeForNewProfile.getStringKey(); - profile.parent = baseModeForNewProfile.getParent(); - profile.name = baseModeForNewProfile.toHumanString(); - profile.color = baseModeForNewProfile.getIconColorInfo(); - profile.customColor = baseModeForNewProfile.getCustomIconColor(); - profile.iconRes = baseModeForNewProfile.getIconRes(); - profile.routingProfile = baseModeForNewProfile.getRoutingProfile(); - profile.routeService = baseModeForNewProfile.getRouteService(); - profile.locationIcon = baseModeForNewProfile.getLocationIcon(); - profile.navigationIcon = baseModeForNewProfile.getNavigationIcon(); - } - - @Override - protected void createToolbar(@NonNull LayoutInflater inflater, @NonNull View view) { - super.createToolbar(inflater, view); - if (isNewProfile) { - TextView toolbarTitle = view.findViewById(R.id.toolbar_title); - if (toolbarTitle != null) { - toolbarTitle.setText(getString(R.string.new_profile)); - } - TextView toolbarSubtitle = view.findViewById(R.id.toolbar_subtitle); - if (toolbarSubtitle != null) { - toolbarSubtitle.setVisibility(View.GONE); - } - } - } - - private String createNonDuplicateName(String oldName) { - return Algorithms.makeUniqueName(oldName, newName -> !hasProfileWithName(newName)); - } - - private boolean hasProfileWithName(String newName) { - for (ApplicationMode m : ApplicationMode.allPossibleValues()) { - if (m.toHumanString().equals(newName)) { - return true; - } - } - return false; - } - - @Override - protected void setupPreferences() { - findPreference(SELECT_COLOR).setIconSpaceReserved(false); - findPreference(SELECT_ICON).setIconSpaceReserved(false); - findPreference(SELECT_LOCATION_ICON).setIconSpaceReserved(false); - findPreference(SELECT_NAV_ICON).setIconSpaceReserved(false); - if (getSelectedAppMode().equals(ApplicationMode.DEFAULT) && !isNewProfile) { - findPreference(SELECT_ICON).setVisible(false); - findPreference(ICON_ITEMS).setVisible(false); - } - } - - @SuppressLint("InlinedApi") - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = super.onCreateView(inflater, container, savedInstanceState); - if (view != null) { - FrameLayout preferencesContainer = view.findViewById(android.R.id.list_container); - LayoutInflater themedInflater = UiUtilities.getInflater(getContext(), isNightMode()); - View buttonsContainer = themedInflater.inflate(R.layout.bottom_buttons, preferencesContainer, false); - - preferencesContainer.addView(buttonsContainer); - DialogButton cancelButton = buttonsContainer.findViewById(R.id.dismiss_button); - saveButton = buttonsContainer.findViewById(R.id.right_bottom_button); - - saveButton.setVisibility(View.VISIBLE); - buttonsContainer.findViewById(R.id.buttons_divider).setVisibility(View.VISIBLE); - - AndroidUtils.setBackground(getContext(), buttonsContainer, ColorUtilities.getListBgColorId(isNightMode())); - - cancelButton.setButtonType(DialogButtonType.SECONDARY); - cancelButton.setTitleId(R.string.shared_string_cancel); - saveButton.setButtonType(DialogButtonType.PRIMARY); - saveButton.setTitleId(R.string.shared_string_save); - - cancelButton.setOnClickListener(v -> { - goBackWithoutSaving(); - }); - saveButton.setOnClickListener(v -> { - onSaveButtonClicked(); - }); - getListView().addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - if (newState != RecyclerView.SCROLL_STATE_IDLE) { - hideKeyboard(); - if (profileName != null) { - profileName.clearFocus(); - } - } - } - }); - } - return view; - } - - private boolean isChanged() { - return !profile.equals(changedProfile); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - saveState(outState); - super.onSaveInstanceState(outState); - } - - private void saveState(Bundle outState) { - outState.putString(PROFILE_NAME_KEY, changedProfile.name); - outState.putString(PROFILE_STRINGKEY_KEY, changedProfile.stringKey); - outState.putInt(PROFILE_ICON_RES_KEY, changedProfile.iconRes); - outState.putSerializable(PROFILE_COLOR_KEY, changedProfile.color); - outState.putSerializable(PROFILE_CUSTOM_COLOR_KEY, changedProfile.customColor); - if (changedProfile.parent != null) { - outState.putString(PROFILE_PARENT_KEY, changedProfile.parent.getStringKey()); - } - outState.putBoolean(IS_NEW_PROFILE_KEY, isNewProfile); - outState.putBoolean(IS_BASE_PROFILE_IMPORTED, isBaseProfileImported); - outState.putString(PROFILE_LOCATION_ICON_KEY, changedProfile.locationIcon); - outState.putString(PROFILE_NAVIGATION_ICON_KEY, changedProfile.navigationIcon); - } - - private void restoreState(Bundle savedInstanceState) { - changedProfile.name = savedInstanceState.getString(PROFILE_NAME_KEY); - changedProfile.stringKey = savedInstanceState.getString(PROFILE_STRINGKEY_KEY); - changedProfile.iconRes = savedInstanceState.getInt(PROFILE_ICON_RES_KEY); - changedProfile.color = AndroidUtils.getSerializable(savedInstanceState, PROFILE_COLOR_KEY, ProfileIconColors.class); - changedProfile.customColor = AndroidUtils.getSerializable(savedInstanceState, PROFILE_CUSTOM_COLOR_KEY, Integer.class); - String parentStringKey = savedInstanceState.getString(PROFILE_PARENT_KEY); - changedProfile.parent = ApplicationMode.valueOfStringKey(parentStringKey, null); - isBaseProfileImported = savedInstanceState.getBoolean(IS_BASE_PROFILE_IMPORTED); - changedProfile.locationIcon = savedInstanceState.getString(PROFILE_LOCATION_ICON_KEY); - changedProfile.navigationIcon = savedInstanceState.getString(PROFILE_NAVIGATION_ICON_KEY); - isNewProfile = savedInstanceState.getBoolean(IS_NEW_PROFILE_KEY); - } - - @Override - protected void updateProfileButton() { - View view = getView(); - if (view == null) { - return; - } - View profileButton = view.findViewById(R.id.profile_button); - if (profileButton != null) { - int iconColor = changedProfile.getActualColor(); - AndroidUtils.setBackground(profileButton, UiUtilities.tintDrawable(AppCompatResources.getDrawable(app, - R.drawable.circle_background_light), ColorUtilities.getColorWithAlpha(iconColor, 0.1f))); - ImageView profileIcon = view.findViewById(R.id.profile_icon); - if (profileIcon != null) { - profileIcon.setImageDrawable(getPaintedIcon(changedProfile.iconRes, iconColor)); - } - } - } - - @Override - protected void onBindPreferenceViewHolder(@NonNull Preference preference, @NonNull PreferenceViewHolder holder) { - super.onBindPreferenceViewHolder(preference, holder); - if (PROFILE_NAME.equals(preference.getKey())) { - profileName = (EditText) holder.findViewById(R.id.profile_name_et); - profileName.setImeOptions(EditorInfo.IME_ACTION_DONE); - profileName.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); - profileName.setText(changedProfile.name); - profileName.addTextChangedListener(new SimpleTextWatcher() { - - @Override - public void afterTextChanged(Editable s) { - changedProfile.name = s.toString(); - if (nameIsEmpty()) { - disableSaveButtonWithErrorMessage(app.getString(R.string.please_provide_profile_name_message)); - } else if (hasNameDuplicate()) { - disableSaveButtonWithErrorMessage(app.getString(R.string.profile_alert_duplicate_name_msg)); - } else { - saveButton.setEnabled(true); - } - } - }); - profileName.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - profileName.setSelection(profileName.getText().length()); - AndroidUtils.showSoftKeyboard(getMyActivity(), profileName); - } - }); - if (getSelectedAppMode().equals(ApplicationMode.DEFAULT) && !isNewProfile) { - profileName.setFocusableInTouchMode(false); - profileName.setFocusable(false); - } - profileNameOtfb = (OsmandTextFieldBoxes) holder.findViewById(R.id.profile_name_otfb); - updateProfileNameAppearance(); - } else if (MASTER_PROFILE.equals(preference.getKey())) { - baseProfileName = (EditText) holder.findViewById(R.id.master_profile_et); - baseProfileName.setFocusable(false); - baseProfileName.setText(changedProfile.parent != null - ? changedProfile.parent.toHumanString() - : getSelectedAppMode().toHumanString()); - OsmandTextFieldBoxes baseProfileNameHint = (OsmandTextFieldBoxes) holder.findViewById(R.id.master_profile_otfb); - baseProfileNameHint.setLabelText(getString(R.string.profile_type_osmand_string)); - FrameLayout selectNavTypeBtn = (FrameLayout) holder.findViewById(R.id.select_nav_type_btn); - selectNavTypeBtn.setOnClickListener(v -> { - if (isNewProfile) { - hideKeyboard(); - if (getActivity() != null) { - String selected = changedProfile.parent != null ? - changedProfile.parent.getStringKey() : null; - SelectBaseProfileBottomSheet.showInstance( - getActivity(), this, - getSelectedAppMode(), selected, false); - } - } - }); - } else if (SELECT_COLOR.equals(preference.getKey())) { - colorName = holder.itemView.findViewById(R.id.summary); - colorName.setTextColor(ContextCompat.getColor(app, R.color.preference_category_title)); - } else if (COLOR_ITEMS.equals(preference.getKey())) { - createColorsCard(holder); - } else if (ICON_ITEMS.equals(preference.getKey())) { - iconItems = (FlowLayout) holder.findViewById(R.id.color_items); - iconItems.removeAllViews(); - ArrayList icons = ProfileIcons.getIcons(); - for (int iconRes : icons) { - View iconItem = createIconItemView(iconRes, iconItems); - int minimalPaddingBetweenIcon = app.getResources().getDimensionPixelSize(R.dimen.favorites_select_icon_button_right_padding); - iconItems.addView(iconItem, new FlowLayout.LayoutParams(minimalPaddingBetweenIcon, 0)); - iconItems.setHorizontalAutoSpacing(true); - } - setIconColor(changedProfile.iconRes); - } else if (LOCATION_ICON_ITEMS.equals(preference.getKey())) { - locationIconItems = (FlowLayout) holder.findViewById(R.id.color_items); - locationIconItems.removeAllViews(); - for (String locationIcon : listLocationIcons()) { - View iconItemView = createLocationIconView(locationIcon, locationIconItems); - locationIconItems.addView(iconItemView, new FlowLayout.LayoutParams(0, 0)); - } - updateLocationIconSelector(changedProfile.locationIcon); - } else if (NAV_ICON_ITEMS.equals(preference.getKey())) { - navIconItems = (FlowLayout) holder.findViewById(R.id.color_items); - navIconItems.removeAllViews(); - for (String navigationIcon : listNavigationIcons()) { - View iconItemView = createNavigationIconView(navigationIcon, navIconItems); - navIconItems.addView(iconItemView, new FlowLayout.LayoutParams(0, 0)); - } - updateNavigationIconSelector(changedProfile.navigationIcon); - } - } - - @Override - public void onResume() { - super.onResume(); - checkSavingProfile(); - } - - @Override - public void onPause() { - super.onPause(); - if (isNewProfile) { - File file = FileUtils.getBackupFileForCustomAppMode(app, changedProfile.stringKey); - boolean fileExporting = app.getFileSettingsHelper().isFileExporting(file); - if (fileExporting) { - app.getFileSettingsHelper().updateExportListener(file, null); - } - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - getColorsPaletteController().onDestroy(getActivity()); - } - - private void createColorsCard(PreferenceViewHolder holder) { - MapActivity mapActivity = getMapActivity(); - if (mapActivity != null) { - ViewGroup container = (ViewGroup) holder.itemView; - container.removeAllViews(); - ColorsPaletteCard colorsPaletteCard = new ColorsPaletteCard(mapActivity, getColorsPaletteController()); - container.addView(colorsPaletteCard.build(app)); - updateColorName(); - } - } - - @NonNull - private ProfileColorController getColorsPaletteController() { - return ProfileColorController.getInstance( - app, getSelectedAppMode(), this, changedProfile.getActualColor(), isNightMode() - ); - } - - private void updateProfileNameAppearance() { - if (profileName != null) { - if (profileName.isFocusable() && profileName.isFocusableInTouchMode()) { - int selectedColor = changedProfile.getActualColor(); - profileNameOtfb.setPrimaryColor(selectedColor); - profileName.getBackground().mutate().setColorFilter(selectedColor, PorterDuff.Mode.SRC_ATOP); - } - } - } - - private View createIconItemView(int iconRes, ViewGroup rootView) { - FrameLayout iconItemView = (FrameLayout) UiUtilities.getInflater(getContext(), isNightMode()) - .inflate(R.layout.preference_circle_item, rootView, false); - ImageView checkMark = iconItemView.findViewById(R.id.checkMark); - checkMark.setImageDrawable(app.getUIUtilities().getIcon(iconRes, R.color.icon_color_default_light)); - ImageView coloredCircle = iconItemView.findViewById(R.id.background); - AndroidUtils.setBackground(coloredCircle, - UiUtilities.tintDrawable(AppCompatResources.getDrawable(app, R.drawable.circle_background_light), - ColorUtilities.getColorWithAlpha(ContextCompat.getColor(app, R.color.icon_color_default_light), 0.1f))); - coloredCircle.setOnClickListener(v -> { - if (iconRes != changedProfile.iconRes) { - updateIconSelector(iconRes); - } - }); - iconItemView.findViewById(R.id.outline).setVisibility(View.GONE); - iconItemView.setTag(iconRes); - return iconItemView; - } - - private void updateIconSelector(int iconRes) { - updateIconColor(iconRes); - View iconItem = iconItems.findViewWithTag(changedProfile.iconRes); - iconItem.findViewById(R.id.outline).setVisibility(View.GONE); - ImageView checkMark = iconItem.findViewById(R.id.checkMark); - checkMark.setImageDrawable(app.getUIUtilities().getIcon(changedProfile.iconRes, R.color.icon_color_default_light)); - AndroidUtils.setBackground(iconItem.findViewById(R.id.background), - UiUtilities.tintDrawable(AppCompatResources.getDrawable(app, R.drawable.circle_background_light), - ColorUtilities.getColorWithAlpha(ContextCompat.getColor(app, R.color.icon_color_default_light), 0.1f))); - changedProfile.iconRes = iconRes; - updateProfileButton(); - } - - private View createLocationIconView(@NonNull String locationIconName, ViewGroup rootView) { - LocationIcon locationIcon = LocationIcon.fromName(locationIconName); - - FrameLayout locationIconView = (FrameLayout) UiUtilities.getInflater(getContext(), isNightMode()) - .inflate(R.layout.preference_select_icon_button, rootView, false); - int changedProfileColor = changedProfile.getActualColor(); - LayerDrawable locationIconDrawable = (LayerDrawable) AppCompatResources.getDrawable(app, locationIcon.getIconId()); - if (locationIconDrawable != null) { - DrawableCompat.setTint(DrawableCompat.wrap(locationIconDrawable.getDrawable(1)), changedProfileColor); - } - locationIconView.findViewById(R.id.icon).setImageDrawable(locationIconDrawable); - ImageView headingIcon = locationIconView.findViewById(R.id.headingIcon); - headingIcon.setImageDrawable(AppCompatResources.getDrawable(app, locationIcon.getHeadingIconId())); - headingIcon.setColorFilter(new PorterDuffColorFilter(changedProfileColor, PorterDuff.Mode.SRC_IN)); - ImageView coloredRect = locationIconView.findViewById(R.id.backgroundRect); - AndroidUtils.setBackground(coloredRect, - UiUtilities.tintDrawable(AppCompatResources.getDrawable(app, R.drawable.bg_select_icon_button), - ColorUtilities.getColorWithAlpha(ContextCompat.getColor(app, R.color.icon_color_default_light), 0.1f))); - coloredRect.setOnClickListener(v -> { - if (!locationIconName.equals(changedProfile.locationIcon)) { - setVerticalScrollBarEnabled(false); - updateLocationIconSelector(locationIconName); - setVerticalScrollBarEnabled(true); - } - }); - ImageView outlineRect = locationIconView.findViewById(R.id.outlineRect); - GradientDrawable rectContourDrawable = (GradientDrawable) AppCompatResources.getDrawable(app, R.drawable.bg_select_icon_button_outline); - if (rectContourDrawable != null) { - rectContourDrawable.setStroke(AndroidUtils.dpToPx(app, 2), changedProfileColor); - } - outlineRect.setImageDrawable(rectContourDrawable); - outlineRect.setVisibility(View.GONE); - locationIconView.setTag(locationIconName); - return locationIconView; - } - - private void updateLocationIconSelector(@NonNull String locationIcon) { - View viewWithTag = locationIconItems.findViewWithTag(changedProfile.locationIcon); - if (viewWithTag != null) { - viewWithTag.findViewById(R.id.outlineRect).setVisibility(View.GONE); - viewWithTag = locationIconItems.findViewWithTag(locationIcon); - viewWithTag.findViewById(R.id.outlineRect).setVisibility(View.VISIBLE); - } - changedProfile.locationIcon = locationIcon; - } - - private View createNavigationIconView(@NonNull String navigationIconName, ViewGroup rootView) { - NavigationIcon navigationIcon = NavigationIcon.fromName(navigationIconName); - - LayoutInflater inflater = UiUtilities.getInflater(getContext(), isNightMode()); - FrameLayout navigationIconView = (FrameLayout) inflater.inflate(R.layout.preference_select_icon_button, rootView, false); - LayerDrawable navigationDrawable = (LayerDrawable) AppCompatResources.getDrawable(app, navigationIcon.getIconId()); - if (navigationDrawable != null) { - Drawable topDrawable = DrawableCompat.wrap(navigationDrawable.getDrawable(1)); - DrawableCompat.setTint(topDrawable, changedProfile.getActualColor()); - } - ImageView imageView = navigationIconView.findViewById(R.id.icon); - imageView.setImageDrawable(navigationDrawable); - Matrix matrix = new Matrix(); - imageView.setScaleType(ImageView.ScaleType.MATRIX); - float width = imageView.getDrawable().getIntrinsicWidth() / 2f; - float height = imageView.getDrawable().getIntrinsicHeight() / 2f; - matrix.postRotate((float) -90, width, height); - imageView.setImageMatrix(matrix); - - ImageView coloredRect = navigationIconView.findViewById(R.id.backgroundRect); - Drawable coloredDrawable = UiUtilities.tintDrawable( - AppCompatResources.getDrawable(app, R.drawable.bg_select_icon_button), - ColorUtilities.getColor(app, R.color.icon_color_default_light, 0.1f)); - AndroidUtils.setBackground(coloredRect, coloredDrawable); - coloredRect.setOnClickListener(v -> { - if (!navigationIconName.equals(changedProfile.navigationIcon)) { - setVerticalScrollBarEnabled(false); - updateNavigationIconSelector(navigationIconName); - setVerticalScrollBarEnabled(true); - } - }); - ImageView outlineRect = navigationIconView.findViewById(R.id.outlineRect); - GradientDrawable rectContourDrawable = (GradientDrawable) AppCompatResources.getDrawable(app, R.drawable.bg_select_icon_button_outline); - int changedProfileColor = changedProfile.getActualColor(); - if (rectContourDrawable != null) { - rectContourDrawable.setStroke(AndroidUtils.dpToPx(app, 2), changedProfileColor); - } - outlineRect.setImageDrawable(rectContourDrawable); - outlineRect.setVisibility(View.GONE); - navigationIconView.setTag(navigationIconName); - return navigationIconView; - } - - private void updateNavigationIconSelector(@NonNull String navigationIcon) { - View viewWithTag = navIconItems.findViewWithTag(changedProfile.navigationIcon); - if (viewWithTag != null) { - viewWithTag.findViewById(R.id.outlineRect).setVisibility(View.GONE); - viewWithTag = navIconItems.findViewWithTag(navigationIcon); - viewWithTag.findViewById(R.id.outlineRect).setVisibility(View.VISIBLE); - } - changedProfile.navigationIcon = navigationIcon; - } - - private void updateIconColor(int iconRes) { - setVerticalScrollBarEnabled(false); - setIconColor(iconRes); - setVerticalScrollBarEnabled(true); - } - - private void setIconColor(int iconRes) { - int changedProfileColor = changedProfile.getActualColor(); - View iconItem = iconItems.findViewWithTag(iconRes); - if (iconItem != null) { - int newColor = changedProfile.getActualColor(); - AndroidUtils.setBackground(iconItem.findViewById(R.id.background), - UiUtilities.tintDrawable(AppCompatResources.getDrawable(app, R.drawable.circle_background_light), - ColorUtilities.getColorWithAlpha(newColor, 0.1f))); - ImageView outlineCircle = iconItem.findViewById(R.id.outline); - GradientDrawable circleContourDrawable = (GradientDrawable) AppCompatResources.getDrawable(app, R.drawable.circle_contour_bg_light); - if (circleContourDrawable != null) { - circleContourDrawable.setStroke(AndroidUtils.dpToPx(app, 2), changedProfileColor); - } - outlineCircle.setImageDrawable(circleContourDrawable); - outlineCircle.setVisibility(View.VISIBLE); - ImageView checkMark = iconItem.findViewById(R.id.checkMark); - checkMark.setImageDrawable(app.getUIUtilities().getPaintedIcon(iconRes, changedProfileColor)); - } - } - - private void setVerticalScrollBarEnabled(boolean enabled) { - RecyclerView preferenceListView = getListView(); - if (enabled) { - preferenceListView.post(() -> preferenceListView.setVerticalScrollBarEnabled(true)); - } else { - preferenceListView.setVerticalScrollBarEnabled(false); - } - } - - private void hideKeyboard() { - Activity activity = getActivity(); - if (activity != null) { - View cf = activity.getCurrentFocus(); - AndroidUtils.hideSoftKeyboard(activity, cf); - } - } - - private SettingsExportListener getSettingsExportListener() { - if (exportListener == null) { - exportListener = new SettingsExportListener() { - - @Override - public void onSettingsExportFinished(@NonNull File file, boolean succeed) { - dismissProfileSavingDialog(); - if (succeed) { - customProfileSaved(); - } else { - app.showToastMessage(R.string.profile_backup_failed); - } - } - - @Override - public void onSettingsExportProgressUpdate(int value) { - - } - }; - } - return exportListener; - } - - private void updateParentProfile(String profileKey, boolean isBaseProfileImported) { - deleteImportedProfile(); - setupBaseProfileView(profileKey); - changedProfile.parent = ApplicationMode.valueOfStringKey(profileKey, ApplicationMode.DEFAULT); - changedProfile.routingProfile = changedProfile.parent.getRoutingProfile(); - changedProfile.routeService = changedProfile.parent.getRouteService(); - this.isBaseProfileImported = isBaseProfileImported; - } - - private void setupBaseProfileView(String stringKey) { - ApplicationMode mode = ApplicationMode.valueOfStringKey(stringKey, ApplicationMode.DEFAULT); - baseProfileName.setText(Algorithms.capitalizeFirstLetter(mode.toHumanString())); - } - - private boolean checkProfileName() { - if (Algorithms.isBlank(changedProfile.name)) { - Activity activity = getActivity(); - if (activity != null) { - createWarningDialog(activity, R.string.profile_alert_need_profile_name_title, - R.string.profile_alert_need_profile_name_msg, R.string.shared_string_dismiss).show(); - } - return false; - } - return true; - } - - private void onSaveButtonClicked() { - if (getActivity() != null) { - hideKeyboard(); - if (isChanged() && checkProfileName()) { - getColorsPaletteController().refreshLastUsedTime(); - saveProfile(); - } - } - } - - private void saveProfile() { - profile = changedProfile; - if (isNewProfile) { - DialogInterface.OnShowListener showListener = dialog -> app.runInUIThread(() -> { - ApplicationMode mode = saveNewProfile(); - saveProfileBackup(mode); - }); - showNewProfileSavingDialog(showListener); - } else { - ApplicationMode mode = getSelectedAppMode(); - mode.setParentAppMode(changedProfile.parent); - mode.setIconResName(ProfileIcons.getResStringByResId(changedProfile.iconRes)); - mode.setUserProfileName(changedProfile.name.trim()); - mode.setRoutingProfile(changedProfile.routingProfile); - mode.setRouteService(changedProfile.routeService); - mode.setIconColor(changedProfile.color); - mode.updateCustomIconColor(changedProfile.customColor); - mode.setLocationIcon(changedProfile.locationIcon); - mode.setNavigationIcon(changedProfile.navigationIcon); - - FragmentActivity activity = getActivity(); - if (activity != null) { - activity.onBackPressed(); - } - } - } - - private ApplicationMode saveNewProfile() { - changedProfile.stringKey = getUniqueStringKey(changedProfile.parent); - - ApplicationMode.ApplicationModeBuilder builder = ApplicationMode - .createCustomMode(changedProfile.parent, changedProfile.stringKey, app) - .setIconResName(ProfileIcons.getResStringByResId(changedProfile.iconRes)) - .setUserProfileName(changedProfile.name.trim()) - .setRoutingProfile(changedProfile.routingProfile) - .setRouteService(changedProfile.routeService) - .setIconColor(changedProfile.color) - .setCustomIconColor(changedProfile.customColor) - .setLocationIcon(changedProfile.locationIcon) - .setNavigationIcon(changedProfile.navigationIcon); - - app.getSettings().copyPreferencesFromProfile(changedProfile.parent, builder.getApplicationMode()); - ApplicationMode mode = ApplicationMode.saveProfile(builder, app); - if (!ApplicationMode.values(app).contains(mode)) { - ApplicationMode.changeProfileAvailability(mode, true, app); - } - return mode; - } - - private void saveProfileBackup(ApplicationMode mode) { - if (app != null) { - File tempDir = app.getAppPath(IndexConstants.BACKUP_INDEX_DIR); - if (!tempDir.exists()) { - tempDir.mkdirs(); - } - app.getFileSettingsHelper().exportSettings(tempDir, mode.getStringKey(), - getSettingsExportListener(), true, new ProfileSettingsItem(app, mode)); - } - } - - private void showNewProfileSavingDialog(@Nullable DialogInterface.OnShowListener showListener) { - if (progress != null) { - progress.dismiss(); - } - progress = new ProgressDialog(getContext()); - progress.setMessage(getString(R.string.saving_new_profile)); - progress.setCancelable(false); - progress.setOnShowListener(showListener); - progress.show(); - } - - private void checkSavingProfile() { - if (isNewProfile) { - File file = FileUtils.getBackupFileForCustomAppMode(app, changedProfile.stringKey); - boolean fileExporting = app.getFileSettingsHelper().isFileExporting(file); - if (fileExporting) { - showNewProfileSavingDialog(null); - app.getFileSettingsHelper().updateExportListener(file, getSettingsExportListener()); - } else if (file.exists()) { - dismissProfileSavingDialog(); - customProfileSaved(); - } - } - } - - private void dismissProfileSavingDialog() { - FragmentActivity activity = getActivity(); - if (progress != null && AndroidUtils.isActivityNotDestroyed(activity)) { - progress.dismiss(); - } - } - - private void customProfileSaved() { - FragmentActivity activity = getActivity(); - if (activity != null) { - if (activity instanceof MapActivity) { - ((MapActivity) activity).updateApplicationModeSettings(); - } - FragmentManager fragmentManager = activity.getSupportFragmentManager(); - if (!fragmentManager.isStateSaved()) { - fragmentManager.popBackStack(); - BaseSettingsFragment.showInstance(activity, SettingsScreenType.CONFIGURE_PROFILE, - ApplicationMode.valueOfStringKey(changedProfile.stringKey, null)); - } - } - } - - @NonNull - private String getUniqueStringKey(@NonNull ApplicationMode mode) { - return mode.getStringKey() + CUSTOM_MODE_KEY_SEPARATOR + System.currentTimeMillis(); - } - - private boolean hasNameDuplicate() { - for (ApplicationMode mode : ApplicationMode.allPossibleValues()) { - if (mode.toHumanString().trim().equals(changedProfile.name.trim()) && - !mode.getStringKey().trim().equals(profile.stringKey.trim())) { - return true; - } - } - return false; - } - - private boolean nameIsEmpty() { - return changedProfile.name.trim().isEmpty(); - } - - private void disableSaveButtonWithErrorMessage(String errorMessage) { - saveButton.setEnabled(false); - profileNameOtfb.setError(errorMessage, true); - } - - @NonNull - private List listLocationIcons() { - List locationIcons = new ArrayList<>(); - locationIcons.add(LocationIcon.DEFAULT.name()); - locationIcons.add(LocationIcon.CAR.name()); - locationIcons.add(LocationIcon.BICYCLE.name()); - locationIcons.addAll(Model3dHelper.listModels(app)); - return locationIcons; - } - - @NonNull List listNavigationIcons() { - List navigationIcons = new ArrayList<>(); - navigationIcons.add(NavigationIcon.DEFAULT.name()); - navigationIcons.add(NavigationIcon.NAUTICAL.name()); - navigationIcons.add(NavigationIcon.CAR.name()); - navigationIcons.addAll(Model3dHelper.listModels(app)); - return navigationIcons; - } - - public void showExitDialog() { - hideKeyboard(); - if (isChanged()) { - AlertDialog.Builder dismissDialog = createWarningDialog(getActivity(), - R.string.shared_string_dismiss, R.string.exit_without_saving, R.string.shared_string_cancel); - dismissDialog.setPositiveButton(R.string.shared_string_exit, (dialog, which) -> { - changedProfile = profile; - goBackWithoutSaving(); - }); - dismissDialog.show(); - } else { - dismiss(); - } - } - - private AlertDialog.Builder createWarningDialog(Activity activity, int title, int message, int negButton) { - Context themedContext = UiUtilities.getThemedContext(activity, isNightMode()); - AlertDialog.Builder warningDialog = new AlertDialog.Builder(themedContext); - warningDialog.setTitle(getString(title)); - warningDialog.setMessage(getString(message)); - warningDialog.setNegativeButton(negButton, null); - return warningDialog; - } - - private void goBackWithoutSaving() { - deleteImportedProfile(); - if (getActivity() != null) { - getActivity().onBackPressed(); - } - } - - private void deleteImportedProfile() { - if (isBaseProfileImported) { - ApplicationMode appMode = ApplicationMode.valueOfStringKey(changedProfile.parent.getStringKey(), null); - if (appMode != null) { - ApplicationMode.deleteCustomModes(Collections.singletonList(appMode), app); - } - } - } - - private void updateColorName() { - PaletteColor selectedColor = getColorsPaletteController().getSelectedColor(); - if (selectedColor != null && colorName != null) { - colorName.setText(selectedColor.toHumanString(app)); - } - } - - @Override - public void onProfileSelected(Bundle args) { - String profileKey = args.getString(PROFILE_KEY_ARG); - boolean imported = args.getBoolean(PROFILES_LIST_UPDATED_ARG); - updateParentProfile(profileKey, imported); - } - - @Override - public void onColorSelectedFromPalette(@NonNull PaletteColor paletteColor) { - if (paletteColor.isDefault()) { - changedProfile.customColor = null; - changedProfile.color = changedProfile.getProfileColorByColorValue(paletteColor.getColor()); - } else { - changedProfile.customColor = paletteColor.getColor(); - changedProfile.color = null; - } - if (iconItems != null) { - updateIconColor(changedProfile.iconRes); - } - updateColorName(); - updateProfileNameAppearance(); - updateProfileButton(); - setVerticalScrollBarEnabled(false); - updatePreference(findPreference(MASTER_PROFILE)); - updatePreference(findPreference(LOCATION_ICON_ITEMS)); - updatePreference(findPreference(NAV_ICON_ITEMS)); - setVerticalScrollBarEnabled(true); - } - - public static boolean showInstance(@NonNull FragmentActivity activity, - @NonNull SettingsScreenType screenType, - @Nullable String appMode, boolean imported) { - FragmentManager fragmentManager = activity.getSupportFragmentManager(); - String tag = screenType.fragmentName; - if (AndroidUtils.isFragmentCanBeAdded(fragmentManager, TAG)) { - Fragment fragment = Fragment.instantiate(activity, tag); - Bundle args = new Bundle(); - if (appMode != null) { - args.putString(BASE_PROFILE_FOR_NEW, appMode); - args.putBoolean(IS_BASE_PROFILE_IMPORTED, imported); - } - fragment.setArguments(args); - fragmentManager.beginTransaction() - .replace(R.id.fragmentContainer, fragment, tag) - .addToBackStack(DRAWER_SETTINGS_ID) - .commitAllowingStateLoss(); - return true; - } - return false; - } - - class ApplicationProfileObject { - String stringKey; - ApplicationMode parent; - String name; - ProfileIconColors color; - Integer customColor; - int iconRes; - String routingProfile; - RouteService routeService; - String navigationIcon; - String locationIcon; - - @ColorInt - public int getActualColor() { - return customColor != null ? - customColor : ContextCompat.getColor(app, color.getColor(isNightMode())); - } - - public ProfileIconColors getProfileColorByColorValue(int colorValue) { - for (ProfileIconColors color : ProfileIconColors.values()) { - if (ContextCompat.getColor(app, color.getColor(true)) == colorValue - || ContextCompat.getColor(app, color.getColor(false)) == colorValue) { - return color; - } - } - return ProfileIconColors.DEFAULT; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ApplicationProfileObject that = (ApplicationProfileObject) o; - - if (iconRes != that.iconRes) return false; - if (stringKey != null ? !stringKey.equals(that.stringKey) : that.stringKey != null) - return false; - if (parent != null ? !parent.equals(that.parent) : that.parent != null) return false; - if (name != null ? !name.equals(that.name) : that.name != null) return false; - if (color != that.color) return false; - if (customColor != null ? !customColor.equals(that.customColor) : that.customColor != null) - return false; - if (routingProfile != null ? !routingProfile.equals(that.routingProfile) : that.routingProfile != null) - return false; - if (routeService != that.routeService) return false; - if (!Algorithms.objectEquals(navigationIcon, that.navigationIcon)) return false; - return Algorithms.objectEquals(locationIcon, that.locationIcon); - } - - @Override - public int hashCode() { - int result = stringKey != null ? stringKey.hashCode() : 0; - result = 31 * result + (parent != null ? parent.hashCode() : 0); - result = 31 * result + (name != null ? name.hashCode() : 0); - result = 31 * result + (color != null ? color.hashCode() : 0); - result = 31 * result + (customColor != null ? customColor.hashCode() : 0); - result = 31 * result + iconRes; - result = 31 * result + (routingProfile != null ? routingProfile.hashCode() : 0); - result = 31 * result + (routeService != null ? routeService.hashCode() : 0); - result = 31 * result + (navigationIcon != null ? navigationIcon.hashCode() : 0); - result = 31 * result + (locationIcon != null ? locationIcon.hashCode() : 0); - return result; - } - } -} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java b/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java index ed20a55b903..9b0e06909d1 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java @@ -3,6 +3,7 @@ import net.osmand.plus.R; import net.osmand.plus.keyevent.fragments.MainExternalInputDevicesFragment; import net.osmand.plus.plugins.accessibility.AccessibilitySettingsFragment; +import net.osmand.plus.plugins.aistracker.AisTrackerSettingsFragment; import net.osmand.plus.plugins.audionotes.MultimediaNotesFragment; import net.osmand.plus.plugins.development.DevelopmentSettingsFragment; import net.osmand.plus.plugins.externalsensors.ExternalSettingsWriteToTrackSettingsFragment; @@ -45,7 +46,8 @@ public enum SettingsScreenType { WEATHER_SETTINGS(WeatherSettingsFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.weather_settings, R.layout.profile_preference_toolbar), EXTERNAL_SETTINGS_WRITE_TO_TRACK_SETTINGS(ExternalSettingsWriteToTrackSettingsFragment.class.getName(), true, ApplyQueryType.BOTTOM_SHEET, R.xml.external_sensors_write_to_track_settings, R.layout.profile_preference_toolbar), DANGEROUS_GOODS(DangerousGoodsFragment.class.getName(), true, ApplyQueryType.NONE, R.xml.dangerous_goods_parameters, R.layout.global_preference_toolbar), - EXTERNAL_INPUT_DEVICE(MainExternalInputDevicesFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.external_input_device_settings, R.layout.profile_preference_toolbar_with_switch); + EXTERNAL_INPUT_DEVICE(MainExternalInputDevicesFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.external_input_device_settings, R.layout.profile_preference_toolbar_with_switch), + AIS_SETTINGS(AisTrackerSettingsFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.ais_settings, R.layout.profile_preference_toolbar); public final String fragmentName; public final boolean profileDependent; diff --git a/gradle.properties b/gradle.properties index 3a3354ded8f..4f67eec3b53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,28 +1,20 @@ -## Project-wide Gradle settings. -# -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m +# Default value: -Xmx1024m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # # When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true -#Fri Apr 08 18:47:31 EEST 2016 -# android.useDeprecatedNdk=true - -# for enableD8=true min sdk must be > 22 -# UPDATE: temporairly commented since gradle plugin updated to 3.1.3 and claims INSTALL_FAILED_DEXOPT is fixed -# UPDATE 2: D8 causes problems on arm64 devices with Android 6.0 (API 23) -# UPDATE 3: Turn on D8 to recover builds with new gradle 6.5 and pluigin 4.1.1 -#android.enableD8=false +#Sun Aug 11 14:50:52 CEST 2024 +android.defaults.buildfeatures.buildconfig=true android.enableJetifier=true -android.useAndroidX=true android.enableR8.fullMode=false -android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false android.nonTransitiveRClass=false -android.nonFinalResIds=false \ No newline at end of file +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" From 82c727c89c86017f08691bc63fd1e4d8f5d70cbf Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 13 Aug 2024 23:19:01 +0200 Subject: [PATCH 26/74] restart network listeners in cast of protocol change (UDP/TCP) --- .../plus/plugins/aistracker/AisTrackerSettingsFragment.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index e46bcb72c57..812a54f42de 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -8,6 +8,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -196,7 +197,10 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { if (newValue instanceof Float) { AisObject.setCpaWarningDistance((Float) newValue); } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_PROTOCOL_ID)) { + restartNetworkListener = true; } + boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); if ((layer != null) && (restartNetworkListener)) { From 932e4c2d49e903540b71cc3a30768910dacc7319 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 13 Aug 2024 23:20:53 +0200 Subject: [PATCH 27/74] increase max number of AIS objects to 200 --- .../src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 8a33c9203a2..e78a8452ed7 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -41,7 +41,7 @@ public class AisTrackerLayer extends OsmandMapLayer implements ContextMenuLayer. private static final int START_ZOOM = 10; private final AisTrackerPlugin plugin; private ConcurrentMap aisObjectList; - private static final int aisObjectListCounterMax = 100; + private static final int aisObjectListCounterMax = 200; private final Context context; private final Paint bitmapPaint; private Timer timer; From 2a145447ead5215a1c45c905237b25e04afea654 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 24 Sep 2024 23:56:23 +0200 Subject: [PATCH 28/74] adjusted CPA warning indication: add new condition: examine time when the own course crosses the course line of the other vessel --- .../plus/plugins/aistracker/AisObject.java | 22 +++- .../plugins/aistracker/AisTrackerHelper.java | 93 ++++++++++--- .../plugins/aistracker/AisTrackerLayer.java | 122 +++++++++++++++++- 3 files changed, 214 insertions(+), 23 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 3953d58c049..b12ec861c0a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -99,6 +99,7 @@ public class AisObject { private static int cpaWarningTime = AIS_CPA_DEFAULT_WARNING_TIME; // in minutes private static float cpaWarningDistance = AIS_CPA_WARNING_DEFAULT_DISTANCE; // in miles private static Location ownPosition = null; // used to calculate distances, CPA etc. + private static boolean ownPositionFaked = false; // used for test purposes to fake own position private AisObjType objectClass; private Bitmap bitmap = null; private boolean bitmapValid = false; @@ -557,7 +558,15 @@ private boolean needRotation() { } /* return true if the vessel gets too close with the own position in the future - * (danger of collusion) */ + * (danger of collusion); + * this situation occurs if all of the following conditions hold: + * (1) the calculated TCPA is in the future (>0) + * (2) the calculated CPA is not bigger than the configured warning distance + * (3) the calculated TCPA is not bigger than the configured warning time + * (4) the time when the own course crosses the course of the other vessel + * is not in the past + * (5) the time when the course of the other vessel crosses the own course + * is not in the past */ private boolean checkCpaWarning() { if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0)) { if (checkForCpaTimeout() && (ownPosition != null)) { @@ -570,7 +579,10 @@ private boolean checkCpaWarning() { if (cpa.isValid()) { double tcpa = cpa.getTcpa(); if (tcpa > 0.0f) { - return ((tcpa * 60.0d) <= cpaWarningTime) && (cpa.getCpaDist() <= cpaWarningDistance); + return ((cpa.getCpaDist() <= cpaWarningDistance) && + ((tcpa * 60.0d) <= cpaWarningTime) && + (cpa.getCrossingTime1() >= 0.0d) && + (cpa.getCrossingTime2() >= 0.0d)); } } } @@ -595,7 +607,11 @@ private boolean checkForCpaTimeout() { public static void setVesselLostTimeout(int timeInMinutes) { vesselLostTimeoutInMinutes = timeInMinutes; } public static void setCpaWarningTime(int warningTime) { cpaWarningTime = warningTime; } public static void setCpaWarningDistance(float warningDistance) { cpaWarningDistance = warningDistance; } - public static void setOwnPosition(Location position) { ownPosition = position; } + public static void setOwnPosition(Location position) { if (!ownPositionFaked) { ownPosition = position; }} + public static void fakeOwnPosition(Location fakePosition) { // used for test purposes + ownPosition = fakePosition; + ownPositionFaked = fakePosition != null; + } /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index 96452cfa5d8..bd9abd11d7c 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pair; import com.jwetherell.openmap.common.LatLonPoint; @@ -17,9 +18,10 @@ public final class AisTrackerHelper { private static long lastCorrectionUpdate = 0; private static double correctionFactor = 1.0d; private static final long maxCorrectionUpdateAgeInMin = 60; + private static class Vector { - public double x; // Latitude (grows in North direction) - public double y; // Longitude (grows in East direction) + public double x; // Longitude (grows in East direction) + public double y; // Latitude (grows in North direction) public Vector(double a, double b) { this.x = a; this.y = b; @@ -39,6 +41,8 @@ public static class Cpa { private float cpaDist; // in miles private Location newPos1; // position of first object at time tcpa private Location newPos2; // position of first object at time tcpa + private double t1 = 0.0d; // time for object 1 to cross the course of object 2 + private double t2 = 0.0d; // time for object 2 to cross the course of object 1 private boolean valid; public Cpa() { reset(); @@ -49,11 +53,19 @@ public void reset() { newPos1 = null; newPos2 = null; valid = false; + t1 = t2 = 0.0d; } public void setTcpa(double x) { this.tcpa = x; } public void setCpaDist(float x) { this.cpaDist = x; } public void setCpaPos1(Location loc) { this.newPos1 = loc; } public void setCpaPos2(Location loc) { this.newPos2 = loc; } + public void setCrossingTimes(@Nullable Pair t) { + if (t != null) { + t1 = t.first; t2 = t.second; + } + }; + public double getCrossingTime1() { return t1; } + public double getCrossingTime2() { return t2; } public double getTcpa() { return tcpa; } public float getCpaDist() { return cpaDist; } public Location getCpaPos1() { return newPos1; } @@ -64,9 +76,9 @@ public void reset() { /* calculate the Time to Closest Point of Approach (TCPA) of two moving objects: * object 1 at position x and velocity vector vx - * object 2 at position y and velocity vectoy vy, - * For the calculation, cartesian ccordinates are assumed with a cartesian distance metricx - * -> attention: by using sherical coordinates, this will produce an error! */ + * object 2 at position y and velocity vector vy, + * For the calculation, cartesian coordinates are assumed with a cartesian distance metric + * -> attention: by using spherical coordinates, this will produce an error! */ private static double getTcpa(@NonNull Vector x, @NonNull Vector y, @NonNull Vector vx, @NonNull Vector vy, double lonCorrection) { Vector dx = new Vector( y.sub(x)); @@ -76,7 +88,7 @@ private static double getTcpa(@NonNull Vector x, @NonNull Vector y, // avoid div by 0 or invalid lonCorrection return INVALID_TCPA; } - return -(((dx.x * dv.x) + (dx.y * dv.y / lonCorrection)) / divisor); + return -(((dx.x * dv.x / lonCorrection) + (dx.y * dv.y)) / divisor); } /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and y, @@ -91,12 +103,7 @@ private static double getTcpa(@NonNull Location x, @NonNull Location y, double l } public static double getTcpa(@NonNull Location ownLocation, @NonNull Location otherLocation) { - long now = System.currentTimeMillis(); - if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { - correctionFactor = getLonCorrection(ownLocation); - lastCorrectionUpdate = now; - } - return getTcpa(ownLocation, otherLocation, correctionFactor); + return getTcpa(ownLocation, otherLocation, getLonCorrection(ownLocation)); } @Nullable @@ -129,7 +136,7 @@ public static Location getCpa2(@NonNull Location x, @NonNull Location y) { return getCpa(x, y, false); } - /* caluclate the distance between the given objects at their Closest Point of Approach (CPA) */ + /* calculate the distance between the given objects at their Closest Point of Approach (CPA) */ public static float getCpaDistance(@NonNull Location x, @NonNull Location y) { Location cpaX = getCpa1(x,y); Location cpaY = getCpa2(x,y); @@ -147,6 +154,8 @@ public static void getCpa(@NonNull Location ownLocation, @NonNull Location other if (tcpa != INVALID_TCPA) { Location cpaX = getNewPosition(ownLocation, tcpa); Location cpaY = getNewPosition(otherLocation, tcpa); + PaircrossingTimes = getCrossingTimes(ownLocation, otherLocation); + result.setCrossingTimes(crossingTimes); result.setTcpa(tcpa); result.setCpaPos1(cpaX); result.setCpaPos2(cpaY); @@ -164,6 +173,37 @@ private static double bearingInRad(float bearingInDegrees) { return res; } + /* This method takes the two locations (including position, course and speed) + and calculates the time when the two objects reach the location where the course lines + are crossing. + for each object, the time may be different or even in the past, hence a pair of two + times is returned + in error case or if the courses do not cross each other, Null is returned + * */ + @Nullable + private static Pair getCrossingTimes(@NonNull Location x, @NonNull Location y) { + double lonCorrection = getLonCorrection(x); + Vector vX = locationToVector(x, lonCorrection); // position 1 at time t0 + Vector vY = locationToVector(y, lonCorrection); // position 2 at time t0 + Vector vVX = courseToVector(x.getBearing(), getSpeedInKnots(x)); // velocity vector 1 + Vector vVY = courseToVector(y.getBearing(), getSpeedInKnots(y)); // velocity vector 2 + Vector vDXY = vX.sub(vY); // position difference at time t0 + double divisor = vVX.x * vVY.y - vVX.y * vVY.x; + if ((Math.abs(divisor) < 1.0E-10f) || (lonCorrection < 1.0E-10f)) { + // avoid div by 0 or invalid lonCorrection + Log.d("AisTrackerHelper", "getCollisionTimes(): Division by 0: divisor->" + + divisor + ", lonCorrection->" + lonCorrection); + return null; + } + Pair result = new Pair((vVY.x * vDXY.y - vVY.y * vDXY.x) / divisor, + (vVX.x * vDXY.y - vVX.y * vDXY.x) / divisor); + /* Log.d("AisTrackerHelper", "getCollisionTimes(): t1->" + + result.first.toString() + ", t2->" + result.second.toString()); + */ + + return result; + } + @Nullable public static Location getNewPosition(@Nullable Location loc, double timeInHours) { if (loc != null) { @@ -176,8 +216,10 @@ public static Location getNewPosition(@Nullable Location loc, double timeInHours newX.setLatitude(b.getLatitude()); return newX; } else { - Log.d("AisTrackerHelper", "getNewPosition(): loc.hasBearing->" - + loc.hasBearing() + ", loc.hasSpeed->" + loc.hasSpeed()); + /* Log.d("AisTrackerHelper", "getNewPosition(): loc.hasBearing->" + + loc.hasBearing() + ", loc.hasSpeed->" + loc.hasSpeed() + + ", speed->" + loc.getSpeed()); + */ return null; } } else { @@ -185,7 +227,7 @@ public static Location getNewPosition(@Nullable Location loc, double timeInHours } } - private static double getLonCorrection(@Nullable Location loc) { + private static double calculateLonCorrection(@Nullable Location loc) { if (loc != null) { Location x = new Location(loc); // simulate a "measurement" trip towards East... @@ -201,6 +243,15 @@ private static double getLonCorrection(@Nullable Location loc) { return 1.0f; // fallback } + private static double getLonCorrection(@Nullable Location loc) { + long now = System.currentTimeMillis(); + if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { + correctionFactor = calculateLonCorrection(loc); + lastCorrectionUpdate = now; + } + return correctionFactor; + } + public static float knotsToMeterPerSecond(float speed) { return speed * 1852 / 3600; } @@ -211,7 +262,7 @@ public static float meterToMiles(float x) { return x / 1852.0f; } - /* calculate a velocity vector from givem course (COG) and speed (SOG). + /* calculate a velocity vector from given course (COG) and speed (SOG). COG is given as heading, SOG as scalar */ @NonNull private static Vector courseToVector(double cog, double sog) { @@ -219,12 +270,16 @@ private static Vector courseToVector(double cog, double sog) { while (alpha < 0) { alpha += 360.0d; } while (alpha >= 360.0d ) { alpha -= 360.0d; } alpha = Math.toRadians(alpha); - return new Vector(Math.sin(alpha) * sog, Math.cos(alpha) * sog); + return new Vector(Math.cos(alpha) * sog, Math.sin(alpha) * sog); } @NonNull private static Vector locationToVector(@NonNull Location loc) { - return new Vector(loc.getLatitude() * 60.0, loc.getLongitude() * 60.0); + return new Vector(loc.getLongitude() * 60.0, loc.getLatitude() * 60.0); + } + + private static Vector locationToVector(@NonNull Location loc, double lonCorrection) { + return new Vector(loc.getLongitude() * 60.0 / lonCorrection, loc.getLatitude() * 60.0); } private static boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index e78a8452ed7..638a7bcd3e2 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -65,12 +65,110 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi initTimer(); startNetworkListener(); - // for test purposes: remove later... + // for test purposes: remove/disable later... //initTestObject1(); //initTestObject2(); //initTestObject3(); //initTestObject4(); + //testCrossingTimes(); //testCpa(); + //initFakePosition(); + } + + private void testCrossingTimes() { + // here some tests for the geo (CPA) calculation + // intention is to test the function to calculate times of two objects with crossing courses + // define 12 positions on two courses: position a1 ... a6 at course line A (course 90°) + // and position b1 ... b6 at course line B (course 45°) + // the positions are taken from a (paper) map + // for coordinate transformation see https://www.koordinaten-umrechner.de + AisTrackerHelper.Cpa cpa = new AisTrackerHelper.Cpa(); + Location a1 = new Location("test", 49.5d, -3.266667d); // 49°30'N, 3°16'W + Location a2 = new Location("test", 49.5d, -3.166667d); // 49°30'N, 3°10'W + Location a3 = new Location("test", 49.5d, -3.116667d); // 49°30'N, 3°7'W + Location a4 = new Location("test", 49.5d, -3.093333d); // 49°30'N, 3°5.6'W + Location a5 = new Location("test", 49.5d, -3.05d); // 49°30'N, 3°3'W + Location a6 = new Location("test", 49.5d, -3.016667d); // 49°30'N, 3°1'W + Location b1 = new Location("test", 49.395d, -3.25d); // 49°23.7'N, 3°15'W + Location b2 = new Location("test", 49.441667d, -3.183333d); // 49°26.5'N, 3°11'W + Location b3 = new Location("test", 49.47d, -3.133333d); // 49°28.2'N, 3°8'W + Location b4 = new Location("test", 49.5d, -3.093333d); // 49°30'N, 3°5.6'W + Location b5 = new Location("test", 49.513333d, -3.066667d); // 49°30.8'N, 3°4'W + Location b6 = new Location("test", 49.538333d, -3.033333d); // 49°32.3'N, 3°2'W + a1.setSpeed(knotsToMeterPerSecond(1.0f)); a1.setBearing(90.0f); + a2.setSpeed(knotsToMeterPerSecond(1.0f)); a2.setBearing(90.0f); + a3.setSpeed(knotsToMeterPerSecond(1.0f)); a3.setBearing(90.0f); + a4.setSpeed(knotsToMeterPerSecond(1.0f)); a4.setBearing(90.0f); + a5.setSpeed(knotsToMeterPerSecond(1.0f)); a5.setBearing(90.0f); + a6.setSpeed(knotsToMeterPerSecond(1.0f)); a6.setBearing(90.0f); + b1.setSpeed(knotsToMeterPerSecond(1.0f)); b1.setBearing(45.0f); + b2.setSpeed(knotsToMeterPerSecond(1.0f)); b2.setBearing(45.0f); + b3.setSpeed(knotsToMeterPerSecond(1.0f)); b3.setBearing(45.0f); + b4.setSpeed(knotsToMeterPerSecond(1.0f)); b4.setBearing(45.0f); + b5.setSpeed(knotsToMeterPerSecond(1.0f)); b5.setBearing(45.0f); + b6.setSpeed(knotsToMeterPerSecond(1.0f)); b6.setBearing(45.0f); + // now trigger the calculations: + cpa.reset(); getCpa(a3, b2, cpa); // expected: t1>0, t2>0 + Log.d("AisTrackerLayer", "# test(a3, b2): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b3, cpa); // expected: t1>0, t2>0 + Log.d("AisTrackerLayer", "# test(a3, b3): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b4, cpa); // expected: t1>0, t2->0 + Log.d("AisTrackerLayer", "# test(a3, b4): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b5, cpa); // expected: t1>0, t2<0 + Log.d("AisTrackerLayer", "# test(a3, b5): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b6, cpa); // expected: t1>0, t2<0 + Log.d("AisTrackerLayer", "# test(a3, b6): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b2, cpa); // expected: t1->0, t2>0 + Log.d("AisTrackerLayer", "# test(a4, b2): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b3, cpa); // expected: t1->0, t2>0 + Log.d("AisTrackerLayer", "# test(a4, b3): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b4, cpa); // expected: t1->0, t2->0 + Log.d("AisTrackerLayer", "# test(a4, b4): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b5, cpa); // expected: t1->0, t2<0 + Log.d("AisTrackerLayer", "# test(a4, b5): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b6, cpa); // expected: t1->0, t2<0 + Log.d("AisTrackerLayer", "# test(a4, b6): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b2, cpa); // expected: t1<0, t2>0 + Log.d("AisTrackerLayer", "# test(a5, b2): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b3, cpa); // expected: t1<0, t2>0 + Log.d("AisTrackerLayer", "# test(a5, b3): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b4, cpa); // expected: t1<0, t2->0 + Log.d("AisTrackerLayer", "# test(a5, b4): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b5, cpa); // expected: t1<0, t2<0 + Log.d("AisTrackerLayer", "# test(a5, b5): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b6, cpa); // expected: t1<0, t2<0 + Log.d("AisTrackerLayer", "# test(a5, b6): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + } private void testCpa() { @@ -244,6 +342,28 @@ private void testCpa() { } } + private void initFakePosition() { + // fake the own position, course and speed to a (fixed) hard coded value + double fakeLat = 50.76077d; + double fakeLon = 7.08747d; + float fakeCOG = 340.0f; + //float fakeCOG = 100.0f; + float fakeSOG = 3.0f; + Location fake = new Location("test", fakeLat, fakeLon); + fake.setBearing(fakeCOG); + fake.setSpeed(knotsToMeterPerSecond(fakeSOG)); + AisObject.fakeOwnPosition(fake); + Log.d("AisTrackerLayer", "initFakePosition: fake: " + fake.toString()); + // in order to visualize this faked (own) position on the map, create an AIS object at this location... + AisObject ais = new AisObject(324578, 1, 20, 0, 1, (int)fakeCOG, + fakeCOG, fakeSOG, fakeLat, fakeLon, 0.0); + updateAisObjectList(ais); + ais = new AisObject(324578, 5, 0, "own-position", "fake", 60 /* passenger */, 56, + 65, 8, 12, 2, + "home", 8, 15, 22, 5); + updateAisObjectList(ais); + } + private void initTestObject1() { // passenger ship AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, From bfb7b7b574055c97fae4f714ff9c0b085edaf964 Mon Sep 17 00:00:00 2001 From: Falk Date: Wed, 25 Sep 2024 00:24:07 +0200 Subject: [PATCH 29/74] change the available set for configurable CPA warning distances (now offer lower distances) --- .../aistracker/AisTrackerSettingsFragment.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 812a54f42de..e29a91d6d78 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -106,7 +106,7 @@ private void setupObjectLostTimeout() { Integer[] entryValues = {3, 5, 7, 10, 12, 15, 20}; String[] entries = new String[entryValues.length]; for (int i = 0; i < entryValues.length; i++) { - entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to ressource file + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to resource file } ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_OBJ_LOST_TIMEOUT.getId()); if (objectLostTimeout != null) { @@ -119,9 +119,9 @@ private void setupShipLostTimeout() { Integer[] entryValues = {2, 3, 4, 5, 7, 10, 15, 100 /* disabled: must be bigger than the biggest value of setupObjectLostTimeout() */}; String[] entries = new String[entryValues.length]; for (int i = 0; i < entryValues.length - 1; i++) { - entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to ressource file + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to resource file } - entries[entryValues.length - 1] = "disabled"; // TODO: move to ressource file + entries[entryValues.length - 1] = "disabled"; // TODO: move to resource file ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_SHIP_LOST_TIMEOUT.getId()); if (objectLostTimeout != null) { @@ -136,7 +136,7 @@ private boolean setupCpaWarningTime() { entries[0] = "disabled"; for (int i = 1; i < entryValues.length; i++) { entries[i] = entryValues[i] + " "; - entries[i] += entryValues[i].equals(1) ? "minute" : "minutes"; // TODO: move to ressource file + entries[i] += entryValues[i].equals(1) ? "minute" : "minutes"; // TODO: move to resource file } ListPreferenceEx cpaWarningTime = findPreference(plugin.AIS_CPA_WARNING_TIME.getId()); if (cpaWarningTime != null) { @@ -149,12 +149,13 @@ private boolean setupCpaWarningTime() { } @SuppressLint("DefaultLocale") private void setupCpaWarningDistance(boolean enabled) { - Float[] entryValues = {0.5f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + Float[] entryValues = {0.02f, 0.05f, 0.1f, 0.2f, 0.5f, 1.0f, 2.0f}; String[] entries = new String[entryValues.length]; for (int i = 0; i < entryValues.length; i++) { entries[i] = (ceil(entryValues[i]) == entryValues[i]) ? - String.format("%.0f ", entryValues[i]) : String.format("%.1f ", entryValues[i]); - entries[i] += entryValues[i].equals(1.0f) ? "nautical mile" : "nautical miles"; // TODO: move to ressource file + String.format("%.0f ", entryValues[i]) : + ((entryValues[i] < 0.1f) ? String.format("%.2f ", entryValues[i]) : String.format("%.1f ", entryValues[i])); + entries[i] += entryValues[i].equals(1.0f) ? "nautical mile" : "nautical miles"; // TODO: move to resource file } ListPreferenceEx cpaWarningDistance = findPreference(plugin.AIS_CPA_WARNING_DISTANCE.getId()); if (cpaWarningDistance != null) { From 5528bc8b23be86c1ef205d394bb9164d266c8786 Mon Sep 17 00:00:00 2001 From: Falk Date: Thu, 26 Sep 2024 21:55:15 +0200 Subject: [PATCH 30/74] fixed wrong visualisation for AIS message type 18 --- .../plus/plugins/aistracker/AisObject.java | 6 ++++-- .../plugins/aistracker/AisTrackerLayer.java | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index b12ec861c0a..dbac7c5a79d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -315,6 +315,8 @@ private void initObjectClass() { default: this.objectClass = AIS_ATON; } + } else if (msgTypes.contains(18)) { + this.objectClass = AIS_VESSEL; } else { switch (ais_navStatus) { // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a @@ -437,7 +439,7 @@ public static int selectColor(AisObjType objType) { case AIS_VESSEL_COMMERCIAL: return Color.LTGRAY; default: - return 0; // black + return 0; // transparent } } @@ -568,7 +570,7 @@ private boolean needRotation() { * (5) the time when the course of the other vessel crosses the own course * is not in the past */ private boolean checkCpaWarning() { - if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0)) { + if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0) && (ais_sog > 0.0d)) { if (checkForCpaTimeout() && (ownPosition != null)) { Location aisPosition = getCurrentLocation(); if (aisPosition != null) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 638a7bcd3e2..1d743f987b9 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -355,13 +355,22 @@ private void initFakePosition() { AisObject.fakeOwnPosition(fake); Log.d("AisTrackerLayer", "initFakePosition: fake: " + fake.toString()); // in order to visualize this faked (own) position on the map, create an AIS object at this location... - AisObject ais = new AisObject(324578, 1, 20, 0, 1, (int)fakeCOG, - fakeCOG, fakeSOG, fakeLat, fakeLon, 0.0); + AisObject ais = new AisObject(324578, 18, 20, AisObjectConstants.INVALID_NAV_STATUS, + AisObjectConstants.INVALID_MANEUVER_INDICATOR, + (int)fakeCOG, fakeCOG, fakeSOG, fakeLat, fakeLon, AisObjectConstants.INVALID_ROT); updateAisObjectList(ais); - ais = new AisObject(324578, 5, 0, "own-position", "fake", 60 /* passenger */, 56, - 65, 8, 12, 2, - "home", 8, 15, 22, 5); + ais = new AisObject(324578, 24, 0, "callsign", "fake", 60, 56, + 65, 8, 12, AisObjectConstants.INVALID_DRAUGHT, + "home", AisObjectConstants.INVALID_ETA, AisObjectConstants.INVALID_ETA, + AisObjectConstants.INVALID_ETA_HOUR, AisObjectConstants.INVALID_ETA_MIN); updateAisObjectList(ais); + //AisObject ais = new AisObject(324578, 1, 20, 0, 1, (int)fakeCOG, + // fakeCOG, fakeSOG, fakeLat, fakeLon, 0.0); + //updateAisObjectList(ais); + //ais = new AisObject(324578, 5, 0, "own-position", "fake", 60 /* passenger */, 56, + // 65, 8, 12, 2, + // "home", 8, 15, 22, 5); + //updateAisObjectList(ais); } private void initTestObject1() { From ef3b284f444627a3632503b52c1fc9b03c0084db Mon Sep 17 00:00:00 2001 From: Falk Date: Thu, 26 Sep 2024 23:38:18 +0200 Subject: [PATCH 31/74] adjusted/extended object description in the context menu --- .../plus/plugins/aistracker/AisObject.java | 56 +++---------------- .../aistracker/AisObjectConstants.java | 1 - .../aistracker/AisObjectMenuController.java | 24 +++++--- .../plugins/aistracker/AisTrackerLayer.java | 20 ++++++- 4 files changed, 42 insertions(+), 59 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index dbac7c5a79d..f5ddce6dd60 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -1,36 +1,7 @@ package net.osmand.plus.plugins.aistracker; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_AIRPLANE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON_VIRTUAL; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_INVALID; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_LANDSTATION; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_SART; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_COMMERCIAL; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_FAST; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_FREIGHT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_PASSENGER; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_SPORT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.COUNTRY_CODES; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ALTITUDE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_COG; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_DIMENSION; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_DRAUGHT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA_HOUR; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA_MIN; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_HEADING; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_LAT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_LON; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_MANEUVER_INDICATOR; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_NAV_STATUS; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ROT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.CPA_UPDATE_TIMEOUT_IN_SECONDS; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.*; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.*; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getNewPosition; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_DEFAULT_WARNING_TIME; @@ -408,8 +379,6 @@ public static int selectBitmap(AisObjType objType) { case AIS_VESSEL_COMMERCIAL: case AIS_INVALID: return R.drawable.ais_vessel; - case AIS_VESSEL_LOST: - return R.drawable.ais_vessel_cross; case AIS_LANDSTATION: return R.drawable.ais_land; case AIS_AIRPLANE: @@ -463,7 +432,7 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { private void setColor() { if (isLost(vesselLostTimeoutInMinutes)) { if (isMovable()) { - this.bitmapColor = 0; // black + this.bitmapColor = 0; // transparent } } else { this.bitmapColor = selectColor(this.objectClass); @@ -679,20 +648,10 @@ public Location getCurrentLocation() { } return newLocation; } - @Nullable - public String getCallSign() { - return this.ais_callSign; - } - @Nullable - public String getShipName() { - return this.ais_shipName; - } - @Nullable - public String getDestination() { - return this.ais_destination; - } - @NonNull - public String getCountryCode() { return this.countryCode; } + @Nullable public String getCallSign() { return this.ais_callSign; } + @Nullable public String getShipName() { return this.ais_shipName; } + @Nullable public String getDestination() { return this.ais_destination; } + @NonNull public String getCountryCode() { return this.countryCode; } public AisObjType getObjectClass() { return this.objectClass; } public long getLastUpdate() { return this.lastUpdate; } public static long getLastMessageReceived() { return lastMessageReceived; } @@ -967,4 +926,5 @@ public float getDistanceInNauticalMiles() { } return dist; } + public boolean getSignalLostState() { return (isLost(vesselLostTimeoutInMinutes) && isMovable()); } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index 7eb76b0b8b7..afb42fa526d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -31,7 +31,6 @@ public static enum AisObjType { AIS_VESSEL_PASSENGER, AIS_VESSEL_FREIGHT, AIS_VESSEL_COMMERCIAL, - AIS_VESSEL_LOST, // only dummy value, not assigned by a real vessel AIS_LANDSTATION, AIS_AIRPLANE, AIS_SART, diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index cc9a084a783..a5119df39e4 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -1,5 +1,8 @@ package net.osmand.plus.plugins.aistracker; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON_VIRTUAL; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; @@ -146,7 +149,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, } } if (msgTypes.contains(21)) { // ATON (aid to navigation) - addMenuItem("ATON Type", aisObject.getAidTypeString()); + addMenuItem("Aid Type", aisObject.getAidTypeString()); addMenuItemDimension(); } else if (msgTypes.contains(9)) { // SAR aircraft addMenuItem("Object Type", "SAR Aircraft"); @@ -229,24 +232,31 @@ protected Object getObject() { @NonNull @Override public String getTypeStr() { - String res = ""; + String result = ""; SortedSet msgTypes = aisObject.getMsgTypes(); + AisObjectConstants.AisObjType objectClass = aisObject.getObjectClass(); for (Integer i : new Integer[]{5, 19, 24}) { if (msgTypes.contains(i)) { - res += aisObject.getShipTypeString(); + result += aisObject.getShipTypeString(); break; } } for (Integer i : new Integer[]{1, 2, 3}) { if (msgTypes.contains(i)) { - if (res.isEmpty()) { - res = "Vessel"; + if (result.isEmpty()) { + result = "Vessel"; } - res += ": " + aisObject.getNavStatusString() + "."; + result += ": " + aisObject.getNavStatusString() + "."; break; } } - return (res.isEmpty() ? "AIS object" : res); + if ((objectClass == AIS_ATON) || (objectClass == AIS_ATON_VIRTUAL)) { + int aidType = aisObject.getAidType(); + if (aidType != UNSPECIFIED_AID_TYPE) { + result = aisObject.getAidTypeString(); + } + } + return (result.isEmpty() ? "AIS object" : result); } @Override diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 1d743f987b9..ba88b01cd3d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -3,6 +3,7 @@ import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.knotsToMeterPerSecond; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.meterToMiles; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.*; import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; import android.content.Context; @@ -32,6 +33,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.SortedSet; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; @@ -398,7 +400,7 @@ private void initTestObject3() { AisObject ais = new AisObject(878121, 4, 50.736d, 7.100d); updateAisObjectList(ais); // AIDS - ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, + ais = new AisObject( 521077, 21, 50.735d, 7.101d, 1, 0, 0, 0, 0); updateAisObjectList(ais); } @@ -592,11 +594,23 @@ public LatLon getObjectLocation(Object o) { public PointDescription getObjectName(Object o) { if (o instanceof AisObject) { AisObject ais = ((AisObject) o); + AisObjectConstants.AisObjType objectClass = ais.getObjectClass(); if (ais.getShipName() != null) { - return new PointDescription("AIS object", ais.getShipName()); + return new PointDescription("AIS object", ais.getShipName() + + (ais.getSignalLostState() ? " (signal lost)" : "")); + } else if (objectClass == AIS_LANDSTATION) { + return new PointDescription("AIS object", "Land Station with MMSI " + ais.getMmsi()); + } else if (objectClass == AIS_AIRPLANE) { + return new PointDescription("AIS object", "Airplane with MMSI " + + ais.getMmsi() + (ais.getSignalLostState() ? " (signal lost)" : "")); + } else if ((objectClass == AIS_ATON) || (objectClass == AIS_ATON_VIRTUAL)) { + return new PointDescription("AIS object", "Aid to Navigation"); + } else if (objectClass == AIS_SART) { + return new PointDescription("AIS object", "SART (Search and Rescue Transmitter)"); } return new PointDescription("AIS object", - "AIS object with MMSI " + ais.getMmsi()); + "AIS object with MMSI " + ais.getMmsi() + + (ais.getSignalLostState() ? " (signal lost)" : "")); } return null; } From 5751e5af5a47316a230f3ff805dae5a6a3904de9 Mon Sep 17 00:00:00 2001 From: Falk Date: Fri, 27 Sep 2024 22:59:55 +0200 Subject: [PATCH 32/74] adjust visualisation of moored vessels (vessels at rest): draw a circle instead of a bitmap --- .../plus/plugins/aistracker/AisObject.java | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index f5ddce6dd60..7fc90c46a32 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -77,6 +77,7 @@ public class AisObject { private int bitmapColor; private AisTrackerHelper.Cpa cpa; private long lastCpaUpdate = 0; + private boolean vesselAtRest = false; // if true, draw a circle instead of a bitmap public AisObject(int mmsi, int msgType, double lat, double lon) { initObj(mmsi, msgType); @@ -414,7 +415,8 @@ public static int selectColor(AisObjType objType) { private void setBitmap(@NonNull AisTrackerLayer mapLayer) { invalidateBitmap(); - if (isLost(vesselLostTimeoutInMinutes)) { + vesselAtRest = isVesselAtRest(); + if (isLost(vesselLostTimeoutInMinutes) && !vesselAtRest) { if (isMovable()) { this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); this.bitmapValid = true; @@ -430,7 +432,7 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { } private void setColor() { - if (isLost(vesselLostTimeoutInMinutes)) { + if (isLost(vesselLostTimeoutInMinutes) && !vesselAtRest) { if (isMovable()) { this.bitmapColor = 0; // transparent } @@ -459,6 +461,15 @@ private void updateBitmap(@NonNull AisTrackerLayer mapLayer, @NonNull Paint pain } } + private void drawCircle(float locationX, float locationY, + @NonNull Paint paint, @NonNull Canvas canvas) { + Paint localPaint = new Paint(paint); + localPaint.setColor(Color.DKGRAY); + canvas.drawCircle(locationX, locationY, 22.0f, localPaint); + localPaint.setColor(this.bitmapColor); + canvas.drawCircle(locationX, locationY, 18.0f, localPaint); + } + public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { updateBitmap(mapLayer, paint); @@ -470,14 +481,18 @@ public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, int locationY = tileBox.getPixYFromLatNoRot(this.ais_position.getLatitude()); float fx = locationX - this.bitmap.getWidth() / 2.0f; float fy = locationY - this.bitmap.getHeight() / 2.0f; - if (this.needRotation()) { + if (!vesselAtRest && this.needRotation()) { float rotation = 0; if (this.ais_cog != INVALID_COG) { rotation = (float)this.ais_cog; } else if (this.ais_heading != INVALID_HEADING ) { rotation = this.ais_heading; } canvas.rotate(rotation, locationX, locationY); } - canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); - if ((speedFactor > 0) && (!isLost(vesselLostTimeoutInMinutes))) { + if (vesselAtRest) { + drawCircle(locationX, locationY, paint, canvas); + } else { + canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); + } + if ((speedFactor > 0) && (!isLost(vesselLostTimeoutInMinutes)) && !vesselAtRest) { float lineStartX = locationX; float lineLength = (float)this.bitmap.getHeight() * speedFactor; float lineStartY = locationY - this.bitmap.getHeight() / 4.0f; @@ -527,6 +542,32 @@ private boolean needRotation() { } return false; } + /* return true if a vessel is moored etc. and needs to be drawn as a circle */ + private boolean isVesselAtRest() { + switch (this.objectClass) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + switch (this.ais_navStatus) { + case 5: // moored + return true; + default: + if (msgTypes.contains(18) || msgTypes.contains(24) + || msgTypes.contains(1) || msgTypes.contains(3)) { + if ((ais_cog == INVALID_COG /* maybe remove this condition */) + && (ais_sog == 0.0d)) { + return true; + } + } + return false; + } + default: + return false; + } + } /* return true if the vessel gets too close with the own position in the future * (danger of collusion); @@ -926,5 +967,7 @@ public float getDistanceInNauticalMiles() { } return dist; } - public boolean getSignalLostState() { return (isLost(vesselLostTimeoutInMinutes) && isMovable()); } + public boolean getSignalLostState() { + return (isLost(vesselLostTimeoutInMinutes) && isMovable() && !vesselAtRest); + } } From c8d7277cff5d07ce1ec5718d329a0466e156c665 Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 28 Sep 2024 16:39:24 +0200 Subject: [PATCH 33/74] added two new AIS object types: AIS_VESSEL_AUTHORITIES and AIS_VESSEL_SAR with individual colors in visualisation --- .../plus/plugins/aistracker/AisObject.java | 24 +++++++++++++++---- .../aistracker/AisObjectConstants.java | 2 ++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 7fc90c46a32..4b0ef01e36d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -218,20 +218,26 @@ private void initObjectClass() { case 32: // Towing case 33: // Dredging case 34: // Diving ops - case 35: // Military ops case 50: // Pilot Vessel - case 51: // Search and Rescue vessel case 52: // Tug case 53: // Port Tender case 54: // Anti-pollution equipment - case 55: // Law Enforcement case 56: // Spare - Local Vessel case 57: // Spare - Local Vessel - case 58: // Medical Transport case 59: // Noncombatant ship according to RR Resolution No. 18 this.objectClass = AIS_VESSEL_COMMERCIAL; break; + case 35: // Military ops + case 55: // Law Enforcement + this.objectClass = AIS_VESSEL_AUTHORITIES; + break; + + case 51: // Search and Rescue vessel + case 58: // Medical Transport + this.objectClass = AIS_VESSEL_SAR; + break; + case 36: // Sailing case 37: // Pleasure Craft this.objectClass = AIS_VESSEL_SPORT; @@ -378,6 +384,8 @@ public static int selectBitmap(AisObjType objType) { case AIS_VESSEL_PASSENGER: case AIS_VESSEL_FREIGHT: case AIS_VESSEL_COMMERCIAL: + case AIS_VESSEL_AUTHORITIES: + case AIS_VESSEL_SAR: case AIS_INVALID: return R.drawable.ais_vessel; case AIS_LANDSTATION: @@ -408,6 +416,10 @@ public static int selectColor(AisObjType objType) { return Color.GRAY; case AIS_VESSEL_COMMERCIAL: return Color.LTGRAY; + case AIS_VESSEL_AUTHORITIES: + return 0x556b2f; // darkolivegreen + case AIS_VESSEL_SAR: + return 0xfa8072; // salmon default: return 0; // transparent } @@ -511,6 +523,8 @@ public boolean isMovable() { case AIS_VESSEL_PASSENGER: case AIS_VESSEL_FREIGHT: case AIS_VESSEL_COMMERCIAL: + case AIS_VESSEL_AUTHORITIES: + case AIS_VESSEL_SAR: case AIS_AIRPLANE: return true; default: @@ -551,6 +565,8 @@ private boolean isVesselAtRest() { case AIS_VESSEL_PASSENGER: case AIS_VESSEL_FREIGHT: case AIS_VESSEL_COMMERCIAL: + case AIS_VESSEL_AUTHORITIES: + case AIS_VESSEL_SAR: switch (this.ais_navStatus) { case 5: // moored return true; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index afb42fa526d..374a18b8534 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -31,6 +31,8 @@ public static enum AisObjType { AIS_VESSEL_PASSENGER, AIS_VESSEL_FREIGHT, AIS_VESSEL_COMMERCIAL, + AIS_VESSEL_AUTHORITIES, + AIS_VESSEL_SAR, AIS_LANDSTATION, AIS_AIRPLANE, AIS_SART, From 0fa48ed90dee815e9dd81f63fcdf01eb35075e0b Mon Sep 17 00:00:00 2001 From: Falk Date: Fri, 18 Oct 2024 22:44:06 +0200 Subject: [PATCH 34/74] correct wrong color definition in AisObject.selectColor() --- .../plus/plugins/aistracker/AisObject.java | 5 ++-- .../plugins/aistracker/AisTrackerLayer.java | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 4b0ef01e36d..c3960624240 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -417,9 +417,9 @@ public static int selectColor(AisObjType objType) { case AIS_VESSEL_COMMERCIAL: return Color.LTGRAY; case AIS_VESSEL_AUTHORITIES: - return 0x556b2f; // darkolivegreen + return Color.argb(0xff, 0x55, 0x6b, 0x2f); // 0x556b2f: darkolivegreen case AIS_VESSEL_SAR: - return 0xfa8072; // salmon + return Color.argb(0xff, 0xfa, 0x80, 0x72); // 0xfa8072: salmon default: return 0; // transparent } @@ -476,6 +476,7 @@ private void updateBitmap(@NonNull AisTrackerLayer mapLayer, @NonNull Paint pain private void drawCircle(float locationX, float locationY, @NonNull Paint paint, @NonNull Canvas canvas) { Paint localPaint = new Paint(paint); + localPaint.setColorFilter(null); localPaint.setColor(Color.DKGRAY); canvas.drawCircle(locationX, locationY, 22.0f, localPaint); localPaint.setColor(this.bitmapColor); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index ba88b01cd3d..47a25d029bc 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -72,6 +72,7 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi //initTestObject2(); //initTestObject3(); //initTestObject4(); + //initTestObject5(); //testCrossingTimes(); //testCpa(); //initFakePosition(); @@ -387,12 +388,14 @@ private void initTestObject1() { } private void initTestObject2() { // sailing boat - AisObject ais = new AisObject(454011, 1, 20, 8, 0, 120, - 125.0, 4.4, 50.737d, 7.098d, 0.0); + AisObject ais = new AisObject(454011, 18, 20, AisObjectConstants.INVALID_NAV_STATUS, + AisObjectConstants.INVALID_MANEUVER_INDICATOR, + 125, 125.0, 4.4, 50.737d, 7.098d, AisObjectConstants.INVALID_ROT); updateAisObjectList(ais); - ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, - 0, 0, 0, 0, - "@@@", 0, 0, 0, 0); + ais = new AisObject(454011, 24, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, + 0, 0, 0, AisObjectConstants.INVALID_DRAUGHT, + "home", AisObjectConstants.INVALID_ETA, AisObjectConstants.INVALID_ETA, + AisObjectConstants.INVALID_ETA_HOUR, AisObjectConstants.INVALID_ETA_MIN); updateAisObjectList(ais); } private void initTestObject3() { @@ -409,6 +412,17 @@ private void initTestObject4() { AisObject ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); updateAisObjectList(ais); } + private void initTestObject5() { + // law enforcement + AisObject ais = new AisObject(34569, 1, 20, 5, 1, 15, + 25.0, 8.4, 50.739d, 7.0931d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(34569, 5, 0, "TEST-CALLSIGN3", + "Mecklenburg Vorpommern", 55 /* law enforcement */, 26, + 5, 8, 4, 1, + "Potsdam", 8, 15, 22, 5); + updateAisObjectList(ais); + } private void initTimer() { TimerTask taskCheckAisObjectList; From b91c88dd38a58fd9024d426fe61c116ae7053ef9 Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 19 Oct 2024 22:54:47 +0200 Subject: [PATCH 35/74] allow status change from VALID to INVALID for some AIS attributes --- .../plus/plugins/aistracker/AisObject.java | 36 +++++++++++++------ .../plugins/aistracker/AisTrackerLayer.java | 2 +- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index c3960624240..3e5520f0fd1 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -24,6 +24,8 @@ import net.osmand.data.RotatedTileBox; import net.osmand.plus.R; +import java.util.Arrays; +import java.util.List; import java.util.SortedSet; import java.util.TreeSet; @@ -332,13 +334,11 @@ private void invalidateBitmap() { } public void set(@NonNull AisObject ais) { + /* attention: this method does not produce an exact copy of the given object */ this.ais_mmsi = ais.getMmsi(); this.ais_msgType = ais.getMsgType(); if (ais.getTimestamp() != 0) { this.ais_timeStamp = ais.getTimestamp(); } if (ais.getImo() != 0 ) { this.ais_imo = ais.getImo(); } - if (ais.getHeading() != INVALID_HEADING ) { this.ais_heading = ais.getHeading(); } - if (ais.getNavStatus() != INVALID_NAV_STATUS ) { this.ais_navStatus = ais.getNavStatus(); } - if (ais.getManInd() != INVALID_MANEUVER_INDICATOR ) { this.ais_manInd = ais.getManInd(); } if (ais.getShipType() != INVALID_SHIP_TYPE ) { this.ais_shipType = ais.getShipType(); } if (ais.getDimensionToBow() != INVALID_DIMENSION ) { this.ais_dimensionToBow = ais.getDimensionToBow(); } if (ais.getDimensionToStern() != INVALID_DIMENSION ) { this.ais_dimensionToStern = ais.getDimensionToStern(); } @@ -351,19 +351,32 @@ public void set(@NonNull AisObject ais) { if (ais.getAltitude() != INVALID_ALTITUDE) { this.ais_altitude = ais.getAltitude(); } if (ais.getAidType() != UNSPECIFIED_AID_TYPE) { this.ais_aidType = ais.getAidType(); } if (ais.getDraught() != INVALID_DRAUGHT) { this.ais_draught = ais.getDraught(); } - if (ais.getCog() != INVALID_COG) { this.ais_cog = ais.getCog(); } - if (ais.getSog() != INVALID_SOG) { this.ais_sog = ais.getSog(); } - if (ais.getRot() != INVALID_ROT) { this.ais_rot = ais.getRot(); } if (ais.getPosition() != null) { this.ais_position = ais.getPosition(); } if (ais.getCallSign() != null) { this.ais_callSign = ais.getCallSign(); } if (ais.getShipName() != null) { this.ais_shipName = ais.getShipName(); } if (ais.getDestination() != null ) { this.ais_destination = ais.getDestination(); } - this.countryCode = ais.getCountryCode(); + /* the following values may change its value from VALID to INVALID, + hence overwriting with INVALID is accepted in some cases... */ + final List msgListHeading = Arrays.asList(1, 2, 3, 18, 19, 27); + final List msgListStatus = Arrays.asList(1, 2, 3, 27); + final List msgListCourse = Arrays.asList(1, 2, 3, 9, 18, 19, 27); + if (msgListHeading.contains(ais_msgType)) { + this.ais_heading = ais.getHeading(); + } + if (msgListStatus.contains(ais_msgType)) { + this.ais_navStatus = ais.getNavStatus(); + this.ais_manInd = ais.getManInd(); + this.ais_rot = ais.getRot(); + } + if (msgListCourse.contains(ais_msgType)) { + this.ais_cog = ais.getCog(); + this.ais_sog = ais.getSog(); + } - /* this method does not produce an exact copy of the given object, here are the differences: */ + this.countryCode = ais.getCountryCode(); this.lastUpdate = System.currentTimeMillis(); - lastMessageReceived = this.lastUpdate; + lastMessageReceived = this.lastUpdate; // lastMessageReceived is a static variable for the entire AisObject class if (this.msgTypes == null) { this.msgTypes = new TreeSet<>(); } @@ -570,12 +583,13 @@ private boolean isVesselAtRest() { case AIS_VESSEL_SAR: switch (this.ais_navStatus) { case 5: // moored - return true; + /* sometimes the ais_navStatus is wrong and contradicts other data... */ + return (ais_cog == INVALID_COG) || (ais_sog < 0.2d); default: if (msgTypes.contains(18) || msgTypes.contains(24) || msgTypes.contains(1) || msgTypes.contains(3)) { if ((ais_cog == INVALID_COG /* maybe remove this condition */) - && (ais_sog == 0.0d)) { + && (ais_sog < 0.2d)) { return true; } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 47a25d029bc..fbc2e2016f9 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -414,7 +414,7 @@ private void initTestObject4() { } private void initTestObject5() { // law enforcement - AisObject ais = new AisObject(34569, 1, 20, 5, 1, 15, + AisObject ais = new AisObject(34569, 1, 20, 5 /* moored */, 1, 15, 25.0, 8.4, 50.739d, 7.0931d, 0.0); updateAisObjectList(ais); ais = new AisObject(34569, 5, 0, "TEST-CALLSIGN3", From 3440de8d2ad2ebb7b2ce398c051e71af76d4129e Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 19 Oct 2024 23:25:33 +0200 Subject: [PATCH 36/74] created new AisObjectType: AIS_VESSEL_OTHER with individual color --- .../src/net/osmand/plus/plugins/aistracker/AisObject.java | 7 ++++++- .../osmand/plus/plugins/aistracker/AisObjectConstants.java | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 3e5520f0fd1..1e885885680 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -276,7 +276,7 @@ private void initObjectClass() { case 94: // Other Type, Hazardous category D case 99: // Other Type, no additional information default: - this.objectClass = AIS_VESSEL; + this.objectClass = AIS_VESSEL_OTHER; break; } /* for the case that no ship type was transmitted... */ @@ -399,6 +399,7 @@ public static int selectBitmap(AisObjType objType) { case AIS_VESSEL_COMMERCIAL: case AIS_VESSEL_AUTHORITIES: case AIS_VESSEL_SAR: + case AIS_VESSEL_OTHER: case AIS_INVALID: return R.drawable.ais_vessel; case AIS_LANDSTATION: @@ -433,6 +434,8 @@ public static int selectColor(AisObjType objType) { return Color.argb(0xff, 0x55, 0x6b, 0x2f); // 0x556b2f: darkolivegreen case AIS_VESSEL_SAR: return Color.argb(0xff, 0xfa, 0x80, 0x72); // 0xfa8072: salmon + case AIS_VESSEL_OTHER: + return Color.argb(0xff, 0x00, 0xbf, 0xff); // 0x00bfff: deepskyblue default: return 0; // transparent } @@ -539,6 +542,7 @@ public boolean isMovable() { case AIS_VESSEL_COMMERCIAL: case AIS_VESSEL_AUTHORITIES: case AIS_VESSEL_SAR: + case AIS_VESSEL_OTHER: case AIS_AIRPLANE: return true; default: @@ -581,6 +585,7 @@ private boolean isVesselAtRest() { case AIS_VESSEL_COMMERCIAL: case AIS_VESSEL_AUTHORITIES: case AIS_VESSEL_SAR: + case AIS_VESSEL_OTHER: switch (this.ais_navStatus) { case 5: // moored /* sometimes the ais_navStatus is wrong and contradicts other data... */ diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index 374a18b8534..2df28c2f327 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -33,6 +33,7 @@ public static enum AisObjType { AIS_VESSEL_COMMERCIAL, AIS_VESSEL_AUTHORITIES, AIS_VESSEL_SAR, + AIS_VESSEL_OTHER, AIS_LANDSTATION, AIS_AIRPLANE, AIS_SART, From 26a8e7c44d9077ab3db9bdac6b63adc0523e97d0 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 20 Oct 2024 20:53:30 +0200 Subject: [PATCH 37/74] special handling for objectClass = AIS_INVALID: might be moveable --- OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 1e885885680..f0f43bb1975 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -545,6 +545,8 @@ public boolean isMovable() { case AIS_VESSEL_OTHER: case AIS_AIRPLANE: return true; + case AIS_INVALID: + return (this.ais_sog != INVALID_SOG) && (this.ais_sog > 0.0d); default: return false; } From 6a75d68bc31c1165df211c06e1e3dff1a7c76f11 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 16 Jun 2024 17:38:58 +0200 Subject: [PATCH 38/74] new plugin: AIS vessel tracker, initial version --- OsmAnd/build.gradle | 2 + OsmAnd/res/drawable-hdpi/ais_aton.png | Bin 0 -> 1540 bytes OsmAnd/res/drawable-hdpi/ais_aton_virt.png | Bin 0 -> 1738 bytes OsmAnd/res/drawable-hdpi/ais_land.png | Bin 0 -> 1902 bytes OsmAnd/res/drawable-hdpi/ais_plane.png | Bin 0 -> 3000 bytes OsmAnd/res/drawable-hdpi/ais_sar.png | Bin 0 -> 5185 bytes OsmAnd/res/drawable-hdpi/ais_vessel.png | Bin 0 -> 2158 bytes OsmAnd/res/drawable-hdpi/ais_vessel_cross.png | Bin 0 -> 3501 bytes OsmAnd/res/drawable-hdpi/ais_vessel_red.png | Bin 0 -> 2193 bytes OsmAnd/res/drawable-mdpi/ais_aton.png | Bin 0 -> 1133 bytes OsmAnd/res/drawable-mdpi/ais_aton_virt.png | Bin 0 -> 1232 bytes OsmAnd/res/drawable-mdpi/ais_land.png | Bin 0 -> 1217 bytes OsmAnd/res/drawable-mdpi/ais_plane.png | Bin 0 -> 1912 bytes OsmAnd/res/drawable-mdpi/ais_sar.png | Bin 0 -> 3217 bytes OsmAnd/res/drawable-mdpi/ais_vessel.png | Bin 0 -> 1446 bytes OsmAnd/res/drawable-mdpi/ais_vessel_cross.png | Bin 0 -> 2351 bytes OsmAnd/res/drawable-mdpi/ais_vessel_red.png | Bin 0 -> 1358 bytes OsmAnd/res/drawable-xhdpi/ais_aton.png | Bin 0 -> 2167 bytes OsmAnd/res/drawable-xhdpi/ais_aton_virt.png | Bin 0 -> 2484 bytes OsmAnd/res/drawable-xhdpi/ais_land.png | Bin 0 -> 2611 bytes OsmAnd/res/drawable-xhdpi/ais_map.png | Bin 0 -> 46783 bytes OsmAnd/res/drawable-xhdpi/ais_plane.png | Bin 0 -> 4348 bytes OsmAnd/res/drawable-xhdpi/ais_sar.png | Bin 0 -> 7339 bytes OsmAnd/res/drawable-xhdpi/ais_vessel.png | Bin 0 -> 3199 bytes .../res/drawable-xhdpi/ais_vessel_cross.png | Bin 0 -> 5291 bytes OsmAnd/res/drawable-xhdpi/ais_vessel_red.png | Bin 0 -> 3236 bytes OsmAnd/res/drawable-xxhdpi/ais_aton.png | Bin 0 -> 1960 bytes OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png | Bin 0 -> 2388 bytes OsmAnd/res/drawable-xxhdpi/ais_land.png | Bin 0 -> 2941 bytes OsmAnd/res/drawable-xxhdpi/ais_plane.png | Bin 0 -> 4334 bytes OsmAnd/res/drawable-xxhdpi/ais_sar.png | Bin 0 -> 7574 bytes OsmAnd/res/drawable-xxhdpi/ais_vessel.png | Bin 0 -> 3114 bytes .../res/drawable-xxhdpi/ais_vessel_cross.png | Bin 0 -> 3913 bytes OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png | Bin 0 -> 3278 bytes OsmAnd/res/values/strings.xml | 12 + OsmAnd/res/xml/ais_settings.xml | 36 + .../plus/mapcontextmenu/MenuController.java | 4 + .../osmand/plus/plugins/PluginsHelper.java | 2 + .../aistracker/AisMessageListener.java | 471 ++++++++++ .../plus/plugins/aistracker/AisObject.java | 802 ++++++++++++++++++ .../aistracker/AisObjectConstants.java | 335 ++++++++ .../aistracker/AisObjectMenuController.java | 163 ++++ .../plugins/aistracker/AisTrackerLayer.java | 252 ++++++ .../plugins/aistracker/AisTrackerPlugin.java | 152 ++++ .../AisTrackerSettingsFragment.java | 152 ++++ .../fragments/SettingsScreenType.java | 4 +- 46 files changed, 2386 insertions(+), 1 deletion(-) create mode 100644 OsmAnd/res/drawable-hdpi/ais_aton.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_aton_virt.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_land.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_plane.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_sar.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_vessel.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_vessel_cross.png create mode 100644 OsmAnd/res/drawable-hdpi/ais_vessel_red.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_aton.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_aton_virt.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_land.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_plane.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_sar.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_vessel.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_vessel_cross.png create mode 100644 OsmAnd/res/drawable-mdpi/ais_vessel_red.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_aton.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_aton_virt.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_land.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_map.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_plane.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_sar.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_vessel.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_vessel_cross.png create mode 100644 OsmAnd/res/drawable-xhdpi/ais_vessel_red.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_aton.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_land.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_plane.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_sar.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_vessel.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_vessel_cross.png create mode 100644 OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png create mode 100644 OsmAnd/res/xml/ais_settings.xml create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java diff --git a/OsmAnd/build.gradle b/OsmAnd/build.gradle index 55eb4b708dd..d54380d6fd5 100644 --- a/OsmAnd/build.gradle +++ b/OsmAnd/build.gradle @@ -268,5 +268,7 @@ dependencies { amazonFreeImplementation "com.amazon:in-app-purchasing:2.0.76@jar" amazonFullImplementation "com.amazon:in-app-purchasing:2.0.76@jar" + + implementation 'net.sf.marineapi:marineapi:0.12.0' coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3") } diff --git a/OsmAnd/res/drawable-hdpi/ais_aton.png b/OsmAnd/res/drawable-hdpi/ais_aton.png new file mode 100644 index 0000000000000000000000000000000000000000..8fb447e4b4a106a8a8a48cca416575ad62cde0ae GIT binary patch literal 1540 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 gSpF07*qoM6N<$g20hRRR910 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-hdpi/ais_land.png b/OsmAnd/res/drawable-hdpi/ais_land.png new file mode 100644 index 0000000000000000000000000000000000000000..2d6145f592554976e991ed6c89cff7a61766414c GIT binary patch literal 1902 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 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-hdpi/ais_vessel.png b/OsmAnd/res/drawable-hdpi/ais_vessel.png new file mode 100644 index 0000000000000000000000000000000000000000..479fbe46d6579ffc0364f986a9cf7007a0e64af9 GIT binary patch literal 2158 zcmV-!2$A=RP)EntM4InxVq6x!=eHi<~Ot$x6OV2)=-KW9FknK?AD z*Is+=wHE+@O10gON2#hYL?qhm%cIq7HXF=lv#{-cR3+Q)05*LvFJ8R3S*=$8ST2`; z84id4eE06%zwEU(JNFodvhh3*)9Lijd7h8+JRi_v1w92Gb4Jx_AQ55xv z=o$c+8J_2zCrNSw07A5T4-}k>^W&mIzMBsVeAIoB`_OaE*nOOip7>4Vj zC=Q$a;PtmEnwRkK>`3=*S#SkS8J_%y&lbGGn}5DqTlbs z7^9U^2*dCSXpcj!7{tuRaU7&+`Yg|L7-LKrhB!GnL6t*VYfL7StCB;yjlQ;F5CPCq zN>@sGLqy2)T+HWlbh}*uK$0ZL^IQ-Si0D}qMIkni{>Dd9;|5jua~cFe!pvZ1N|Iz- zX0cenaU5u^;rsq5j^jT7?Qp1#5me<*hnczC?b33&+!jdF6un-Lm|2w^vMc@n{Sj2* z&nnMzFf$K_L!6$TZp$1WALID=xXPi+wm8(ZL6$#KO1UN?6h$HIlMC4F2bBgQ&vP3O zT}DwfKnomd)SwD~ra=&V%gj(pZKqcm4*>w?^EsrHU}pHfAIEY01JFE&njS%xKWifD zKExlpa(^rwYSf?#e=aS527>_>iv@tqa>CA_IF2zIjlj&Ua;QmzEPsUMk5Wp^W-|Pv8nMA$6h)U>YcR9!_xniG6u`s$vA>&6r#L)3 zgx1<}Xw@W#nlOj}D9`gS9*?iJ*3eqhWHQ0w;o+vhuNY*EL8sHfVzEG;=RybprPK`( zb+9q0_GwnTLH5f>Q8Xf=3%fyEiy6dt??ftL40n48-46l?@FvjR02w)@V zk3Ed+wb^WjgM$MYV@y#LNYnILnb=T!a#a%y(q)3emF3TPJjQ4=0`O^rj4|+iAM^Qq z8xKh-Z={qi8c49J)gXJ$9!1e15k0H&$L{rfO14arB(O)*K@cQamd%0sIaIenmOmH1 z?~k)d1HdL=^nIV0nTg1AUH7IeQtNl9wekvou9Z??X4UKU=wtEEec3Ye z`5eRH5X@{7tQz4^-3+q)xwQPT8}h*C859Hoo;-PiG84^m$U)5^MX zi!@D9-ud5Lm)4t2#3V_el(HN;%d%_+)XSkd4YK_C!T0@`nIWaz^2ct@|HiriYy!p# zhq_I~L$y`>DgJy;T3wYxgTbK6p;g@+s*^!?_`}RJ8jTRgF@Vp@_pxP%!y%^AX_Z53 zDWyP-9IC~j3V*f<;x_(qUH7x5mz8B!nx<6_{VvP0Z-Dv|J?k_GaU6f|`~IZNJ4E$> z@JqE=EYR(CEr)up>t2_aYWXg;R9a0CFYmVQ^?KVAj4!#{RZ#c){f9X8-!sKk*S7;5giB?LfDu;lY5f;fQf{?FI&$7-=ypwsCPGaDl6yRKVX!uifCyvHADjJd;~uQ`mU zZlDE00Aq|UIdti|ZY>2|xdC7e6EtPC5&*(QiHv-W*ozJC20Z{EB?`A*nZH&VX3V4sS*C<>=c zIRE>uIkZ!QnAwzhJ*)Tc--C!00K9$scDrS zf^72B&r(V#r6du_PN%bTHW*{JKT=BMdH!!3QecO#lJC?YB4VW!UcP+!39j kR_(RdUVH7e*W=fJ02pM7TwO7oYybcN07*qoM6N<$f?$UWJ^%m! literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-hdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-hdpi/ais_vessel_cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6d534b2bb7dc752bf6f06f0303aea13233d0a9e3 GIT binary patch literal 3501 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 bpP>E^K{@{)ItO7`00000NkvXXu0mjf_^YsI literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-hdpi/ais_vessel_red.png b/OsmAnd/res/drawable-hdpi/ais_vessel_red.png new file mode 100644 index 0000000000000000000000000000000000000000..32d66b56a1bb1953961bdfdc2e12c2053a3ec938 GIT binary patch literal 2193 zcmV;C2yXX@P)29~{73bImo^TmX0=9!Sw~d~p&$rHoOa<}Rg_nDGtNzk#LncXNRU@Er`F!P95( z<9G0f^l!gfxhzqOz<=Q%{{!>He*WEXKTE;Mq>3EeoxC1ERl%tSB481Ee}ubGd; za5%Ex89*7J0%xJ!CZa$N6={$Fx(}fbXWD+J1mqHELhmuGLa`hw%%FrnXRyVApzg!*7Z7J*QUDR0MNkrmF-9;31@Q(YL*L8~t5P_}&e}aP-HW1_0SMYCd zARw$hjKT_@yAqwM{w|51O-Obmhjg)csCmOt1Sd%B*GAm9f#r~94aD%1c*cMD=;*6 zo~*)F7geo8P$AgrVL)jSd^i}`5{M}R)VUj!;!mpy+X;Vy_8tTk5C~dbmuZFuG)~~R zF4)}TEh2-`{0WSXwE_t``w(lGj0v}TcqLy%>&S8F6pGFppoBkX3PPi`7LcI54{?kD zq6Eg5gj;<)&nuimxf%2Ze~zAu;2ER6aDkw;3zJI_Wy@pHgwmZ&N-NF6Ih1DxCHzqd ze+;Pd#sj>?2E9p=fQ%nfp^h!%lUIDI98fRvkUN-L}H;7_unKpY_pHwfBY zG$@TjgO800U_FFwky-7;N>STDJyWBcrt~ptbf$9w=_W=3}@I)3sfe z%G{u2ad`rV4OohdF$yGT?Y;M@ySTsmV5^S;F*=R~{F>uXPVr}}gg-lQe1RZF$Es8& z?DSxK9eO;# zUAUH8yLgovo-qlB4zuP^CI(&0pOq%1JDZL_A8Ak=;oZ1NdE+b0ltX8sJ%`LVl$AjV zf2uy5zRjPHZ(KDxhH!zf-GeEIcF$oQvTO9r#Gq7zc((y-i5)>}7yQ5$kZ-}qT^VJlCaZ<$)zoZeXK;mctp_Z;sup=aVV39^O+muf}A#p zi*3*>E#JmpYug5ePdSx@L(8`)uiv*ELNaeS&%z+VzMx_o#2tUaP7kRre$e{sI@$S9 zEorclu@ailJBKD@!=afQl;Y3B25h);n3UUlP!8?l^AmN&q1K*TmuaJ*R?D#AeC7vy z;{_E@={Q!1gq_}}^Cyugm|WV0s~_MQ6F*1z7LICU!=afP^frG40aIGKNqK$C^5=6f z;W$)UzC(Gfg8?xjl&Ak>Gw0B(Ge`jKK`?@$5`_j;Zto)tOO`*MdyV4=QjHC<66)}q z-@&aUfLUs$1|^He$FRKto0nKhd5p6GKCTa5yi2|c%n>_Zwvp!(v2fjvmU5UBonrp7Pz99V{MKf>i T*H{PL00000NkvXXu0mjf+Zgol literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_aton.png b/OsmAnd/res/drawable-mdpi/ais_aton.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ae2676d9fb26226f2f22a12487b3544096360d GIT binary patch literal 1133 zcmV-z1d{uSP)^CB_4BVHZTg%T8xJ%*=M$PD?wT z9#o#>$+n$m=lOnr&+mDemRse&T!O4lxr=Gtxq}}d7Ei6O+Di^Ui`ndxP&kdQ>c43y zlt|oZoi}~zEhlZ&5{rczb$3^93WY+k*aqwD_n8HIL})nUL(lr&1q}^t?Yg?GFCxA0 zEqnZJMO!Sf*(3H^bj&$dG&CfVI=Zb#pcj7L9?K#xnz2p8cOs!pcGNF1xj-5GlE@31 zp2EGjL+A&Qpni$T1ct#sjTi|wVf}O@stb$)AIkTDsYz557!^KQZ9g@M@dZYOr~4=| zrI8ptb(3)gYm^dv?X7$ z$PGKa;-u|b+S(eU1^4CxRp3d-wqfvLC6{=7GKt{=RpE;zVPG=k5>Hf@7~K7&@cAB) zOAKo_fkZjNIPh^vEkY-(o;}Oj7AN*Fi2+CPet|%c!LPO7@zsObpvZ?M6`DFD5DsZu zY*Qw&FO!J1vj?TF((HpzBpLh(^Y&U6d42%=Rs60yMUw2UmkOVqmF@>Cx+isl`>?)5 zBGGhQB(c*zpKE)@kCs;%SCRXLA_p9@#p{;b@SNE`_vF0FACU7kiXcdA}Y(FY|T)rI+-loTu zD3*MJ_6qd2oe+Mwz$6lZbj?2_odesYbD(ztm=ONv0#zmQzo(=d+LO{b&^vD?gda_y zibTGYws1eTsV(QL2~?3F-RL%}J#DH9RFQ~{k~W8`!jCU7u0+0M4u(JMh&QUjR}&al zA}{6euT279U0_s+;;J+S_{juDl?d?(zOEmBa)DtIzldBG`dQ~xxmYiJJpv3Hi4}in zYnhn_Jazs;(<||yIrrFYPQy8e9jzapY5dF@3d8~}vAy2LT3>qC1xfbITO0iJN+1vv z9wGx_**6OHbTzG#o_*41?nLx&i2JSbzbyX%6NH-JxM;N500000NkvXXu0mjf3Wg3Q literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_aton_virt.png b/OsmAnd/res/drawable-mdpi/ais_aton_virt.png new file mode 100644 index 0000000000000000000000000000000000000000..998e6dfeb7f2acadec38dd6d32a9ef7235ac2380 GIT binary patch literal 1232 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 yJ8fk0jq5X literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_sar.png b/OsmAnd/res/drawable-mdpi/ais_sar.png new file mode 100644 index 0000000000000000000000000000000000000000..78bb51add2c2ca97ac2cedcd6e4a85fffe28b689 GIT binary patch literal 3217 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 Da{x4^ literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_vessel.png b/OsmAnd/res/drawable-mdpi/ais_vessel.png new file mode 100644 index 0000000000000000000000000000000000000000..1c40885e13bcf6c66639a9438b22d91de482fc99 GIT binary patch literal 1446 zcmV;X1zGxuP)K+D^`lW;86^dk^;<*kysQ_#cCaG$PWz zbRAVexBPDu_jPxOh}7ZX;mY^@`1R}8f8J9bw3-e`L`*3qCzHu!wOaiOp#S#m+b5To zmw%LFP3B`w2HTAp1i@LBWmcADG?`3R_m!{lfJ!LZ~2uLYm+xGXCWpw}uB5KsojS?Ut1^}Mt9TA~FF8P1CkYDdsrNi~GoVF9gQp@hVMI%w{tc$1&FHHG&{m0chL@jS^5w z30>FWdEUvpckgg`ct}S_M{K=bgLD4eFbo}lK!Y-Bj6h*d#W~+IP4lr*3UM5>BuR)d zhHkeTd7c;ToZASR>_BPYr$l7+`+XUPA&tjl8Vm+P2%(QgqvuWMZIS>1z-Tl&&$0}o z(MURu1Jg7ynM_ogrtmzk^g&}u8zZoz|I^K8gD8qf)2^mkaU8QWO~E;TVHgGjuth^Y zXoSFy{yoz)kEE1HlH@vek|ab#=ytn_=Xvv;YigsZ4wU*oW{eFC!;rJt?7G0|bV{~u z3n^tM2!cj^&=dgzfFKBd%(4u=@5{ZtJpfmF=bR%9LzSi}eBW=>2aOO=O36b1Kdjel zEEWrDwOWu;UKbig5!-Awr4N9Hd{CP}p=HTAANG2^BO$~M{g-(7@F4*}x7+>B^StTK zwYAVh2TJ`PF~;0(x2xv!IRO9>p{#K_oswl)LP}`_LGY}&_FqAO0KoVCvqJwO48!+> z1p&|VFdmOT^ud=Wpp=q~F$@NS^UY?1`Fu{gu0u+BUA&Z6tH>ydC{0rghr^RjrvqI1 zpcZNoDD`hymTlYiu@C~w zi^bn$QFdZBn^CXV6O6IG=XuBFaP0zWfB+y2!_z#^;kvHa-`~F)*m=N||9#)b-rk-n z=1QyG7*$b=fKo~^#^8D0YO~p3Hk(nW)48Gl5>iSq#t=mjt=DUKo_DJ2I+Rl4ix4RE zFD%RIS(f#5yWL{BTz>LyE|*KDl!9rRk2&W709EIGP^SZ>{vYYOzOU;#7K_EL@yZ(K z^EsKODT(Nt9UoM#qYi;`{dKq>W)1b{0aXqtw_VnLhD2EOl~)%8Io0z3LQY}ct@A|+n49OVdb$w8k00Z9Ze>fa&4-O8l zCvdl+92<|v@O{7Z!C5(6xsbbJNB`%A{?Y68KC1r`Qc4t0tnz*EW8s5JpWW|FU`PLL z+qR!)S%xTzSV8QwN2YXLhvPU-yWPfiyVYO4 zdR1w&u+?f&x7&r3($O>xRS4Xt<807*qoM6N<$f@HnB A!TN_+Dpx1HqSSY_*+{f>s$(Gg52CQXFeK zN(hk6?!EUp{jhg&rGaEam{!L#lbPgZ@BjS&&z^IhbDjsju+OnMf&H&}jMn4`zaKXz zV~XoIj-Gekd7VJy;fEjogSGZJxf)}09LE4GUc7j6Pft&OPfw5S>gt+N{=c!bF$XMR z&X_S{L4SY$`2PNW>FDU_JYN3B9IzBB*=)8`MDTr|#>U3ZP$-1&`zkWFQ9GsrL{QgafCL|9eFG!KRqATT0t0Vz$V({;&Yvdwi} zM8t@Qc%DZhkw|B=*))JLW>`oGd>@kl0i&RR3iI?Ov6(o^B$+u*fsSbrfj9WKOt6936$ zGI>fV{>ucE@}Mc3&8DE32SX5_+YOJcfo;>^oEBJ^h1P)RkfAiB03UpSo8Y#G;Kxq5;Gxa4-&-VJw^3lWhpKub%@snKY3LRtJD(h%f9RVfbyLS@9~cf-at@aJi8E)Y5h z@7)C7eFUB==QJV#LE`dH$X^IQKNqquz|TVPFBhUy;NnjSQFQ@kwzhUy*9DRP3UU!q zuTY>Rlj#JASnIEX1>LY|4ZJuFRsc>vyt5XrSPE@7z`DJ`LL4a(hRbjPj4dXl&WFTR z@V(RE3Sg24)QIGIR3E*+rsl@w8#k`4t*u>V`}%y0aWKL9SPQCFCQL8~`}$tqv|`0I zD-Rs_X{*RrT~!Hzd<<_t1Z&p8{d-_QKnyq*yrX6Dh&B|bSkk0S$i%DQ^3z~-Nk~*A z*SmD7vv<|1NFkd=To)M_01@oCafr24zw{Dcdf)+@eB~8&!3nAIG5qx*xOE-;ww$kuVJ9qQ0`g+|3-g}_$hC=$r8E0tEf(5z@^gR*X1>WD& zX?^DW^PiX+%=!Z2K%92n$%OB|n~C?{i{$f+ zf9^R{1O>d>T0Xw=O7hE>Q#g2V!oQz>8V(!?i-_l|dZXb1$I#3fz=${&3X@Z*__UKw zS}0q$QvKFj2G2uOi*cY@>?d+Ls$YNIm^a^KVm#iSYHDf#Tt~z?md>u?GaFOc?6Y}xXS=$&_JboXu%Ybi{f%HGwh8JsbL@VoC4-nWnF&Yco{;t3n??Tz&9 z+xOW73p+?gY5j<5}Xlyixh{s}>0|$!xG(b3vjm17C#OdwTurX3?ZGI;_b_d+D0bZ|E z$Z#8(Q6M00E`{YQ;fE8UBPT*smA-g<``)XsZpFs6Q zrcH$t?z``!$iP6ITf{2tgpGH=%^P4xu2|_ri*FxM#}RWtio~@6%n#t~U&39TaJ|Ch zFi6gEUj4P|x1U_H1_rQIRVquDhMs)?{fl?^ z^mMm6PP?Z%F%GR~!6j!xs|$a7yV$x^J`a>u{!D28BW$@G9RAV$+<4= znaslRu^3yd-SebcPM<>b@ibh>uOjvXI~NO-&I{_XIqb+G%ZL*kRB0#_B9qB1DijKs!>i5+%orp2e4gg! z=8jk_hPBp~qOi=bceD~Ztbm~`h;_fAp&?RVUq9#Y`L_ZbIz0ifqdD*z ztpL*LbW=PYPkEjPqico5TC34$^u)Tlx|soX#e7E7jT(T)#>Tm->M*+aFPU$xW!9`& z^Mk)D#;JH7C>_pbv)LtuLIG>-sC#);^>evgD3i&2ITQ-r;(4A8?ifalQIUWETw}2q zO-)U;u~>{qBoY>pQC+2~s)WN~CQX`DTUAws=Xn+11Xi9j#)!36ySloXmMvR$&cMKc zU&$_9Zq{0@si_HfcX$8p`RAX1t%N-qtk0~CdYUuE!~Ze4Ur<$RKQkA}U)bl&{sTSu VtL_rtG;II?002ovPDHLkV1lYHhpYeq literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-mdpi/ais_vessel_red.png b/OsmAnd/res/drawable-mdpi/ais_vessel_red.png new file mode 100644 index 0000000000000000000000000000000000000000..1262ccc1f8095dc579c829438e6b62c1a21550c1 GIT binary patch literal 1358 zcmV-U1+n^xP)T6If6^IJ}EF@5I2ds8bCT>Rs!# z`}n{})G2@go8^R=84y5&o&)+KM`ysy_JNf4K2ztwj>tg;T$?cmSsD<)cA;8@y)odm zddl0}lK`k#+unzEH0gshpa2HQ$TNCQlwj<~B$Q3X#s*0_fv2U*XXB?AI*C~^`2 z?n>P_3#+L4MGEU%b_@(eVh%D>CI;Tg|MN4zYjj1t669RG)?UZ~a##UnA;^k@tPI5Y zU#h~u2fWTuAquR)YxhGAuv`1kiY9(g2BQ2k^o%ZBrA(o`^Ig9wNMe@vmV zBg!pk#~AMr*}oc|PFXM@03(rO4Y-vx?XEV!j|~U}V{(->^3|pt1H&s0?$6AeGV@5Wv)7z6HZe!0U_@%HgJ)&SHD0 z0^kwscAys3NYz7H0}7xIdu5|n4cKC<7q0);(;*Ua(0nT7TwOAHc8Cn48mUe~S_TB* zMC7>!+=UgLueQLy44a;OOM!ohTd9#>Y1jZbj5y%F3`F_2F3=ZsI?4b1V+ew`4M51j z%sgJBD`$lFOmkrkoyI{L0|Mwelo~L)1d5%Z;N-aVp-Z6H9w;BE!d@3v2zM$Hdq`s- z&i`)NnwkNMtv&VD>hsoskb`2YCn(2wMn4){agex=Gz`S~KhoBcn_mp_Pd5LV|H-$b z>%sCm?(({cI5@dpD~S>rxH=su&;e3H)4?AURno<4*b4&gw0|f$~LaQfM#CS$O9zZG%k}{y+ zD*w)GImoYdgE0R_+#IW*Qdxzc=4sC9(#Ibg%H z@V*xo#^>Z$>$rI_H$*nq_%;3wFun96_!6xcVw5AglB z@atcom8e8Zps~i5!2KD1c@x%_the7Eywco6skD!Su`C#q?Vj&Pebm3z-;TO`NVKIN Q1ONa407*qoM6N<$f=;e;CjbBd literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xhdpi/ais_aton.png b/OsmAnd/res/drawable-xhdpi/ais_aton.png new file mode 100644 index 0000000000000000000000000000000000000000..547865fe1c1b26cecbc9177e7c024ea85dca5b48 GIT binary patch literal 2167 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 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xhdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-xhdpi/ais_vessel_cross.png new file mode 100644 index 0000000000000000000000000000000000000000..157094d1a2eb55da6cc13aaeb13ddbf11c4dd496 GIT binary patch literal 5291 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 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xhdpi/ais_vessel_red.png b/OsmAnd/res/drawable-xhdpi/ais_vessel_red.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea6f1b10bf34b9739a3d04adcb6187a06375165 GIT binary patch literal 3236 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 W3X~HB2|f$}000015?|IMjyyyL$ zH^axvO?QXM4gdgjY3@`%H0P)nq=m-DL1z`3K=yIkNeCU`vvXt=>wN3(&jtWXGxgHo zRFdNW0QV;v)#W5-L@`DeoSin7Ojs3>ubTQoZ5IrgI)0UcD=znUJiHKNaQ7V57aKO^ zc?!&h_nWp1j$BC#xV2RN6+`k0{FCI*H|E27BU53Ba`1+MU z6o;%zUrXbkaZ`>i<}G|?!|LOHkUkD+7n;-ZetWJI1@F3$ka<(M*D9;_%)M*&UmCJU zAFQbw$LmT!Sl6huA!2I2s_dP@xo4PnF5%EjP<)pn3tzuw>t8hrI@puu2fK#6onjfB zn1Z0a?EZ3LeueNsByFxuIIz~FI0S?hDM{-g4XUR1evl5O+L(Yw69*D(m}>hyhhh$d zy-}vR?{e(@0`Mlr3;l}h>$iLU5q?VQSv>hy?|Qle){x@`_FdUyUF*CXdG5{pfRqwh zRKSpP6YRTwrkkY7FGR?=T%f|G^Z9(YzjbZE(e+ms-GKqgMDs*?VFxEM{FT0`hBm38 z6RS?HmvK}K#7WoF7l5$xGrU@l-1w6qiOnZP{Hi7j2=hOcwczvxfH|rV!02=SyDPz( z0y=675;I7{U9OP}fTbHF$0i?; zlLc1UT-jh9VVjqNXCpI(ZdvbnWmX-(XRo|{slV~C*EmOFLreVdC~2DYV<$4@yjb$x z=4N{#OEhuKz31tMhuog#81JQ9diG;`$(<)j&O48Gw<6C9>qA#~WrKEDh~wqU)ITL2 zo9H}Gwt`T@hqcN_qZfm&8%fUVsBzJ0^4MGM6D<%V3v zDDxxxycCOjLW0{9Ni(^)s&%mZW}gQQ%9VYvm*H1iJ6fA>+mdP7#*HmKqz z4Q(gq*Nc2KJ_&1LPNs(;#(n$RD$m9(C`a2XmK*N9O2Q)|@&jTyt2AROS}~G^6`mOq zkvrOy3*&g?5`tg&Jicp>b@WEyjqKOIa^O-t)aS! z;imiB^pWP8z^P-K3-!THcQAwL^gO?W?X9(h{_;X@uVPME`x+%9CUUN)(ywJrR;VHf z))n*CH3ytU%N+@01Eb!1EhEV!V)AZerkH7=ny6s}z9e_ft{#YdYhl<}oxRe9#C*w} z8*9achT#)KCU2Y#Q?%TRPj6-jaPlp#PDMeh!zQUM zdcL=};(H%qgR7@)GU&;{`_g8ctdgFB2RlRoZ4RL+JfG`DF`4f2IX#jl7A&tbF}@uq zcD)?wsI)bLAstfGr79PI?=BD9N74ngsTl7-dDsyOY{)6n^WF(yawX5-*Sx5UeMqrg zt4@IjVP{K>nsW)@zj+~i^^1S3n2#fsU{I!hDh2InoSF-Ors%GkjfEk*it4fgHphvh z1}oyLC;#o%P1Mx?PZ0$UKTPG$<=Y1Ic^p*&?(hb$YBm4#xxast&?r_OSEkNZBo=rc zk&S!YkLk)24&+-bv4Z~6Ww!bwxo2~z#yY#*W_@)!w&8T?sN=2xEdsvQ_>V4R)`_4^ z<>t1N5wh4J;!w&O_s>ih^oVTGZh-_21Wqf64BYLq>Xn$Ot!v~^K!S!gRj#3*3Ysdl zDKMr_XJd`21hlDewQ(ulkzq+EC>O>ewn0chIk}58L?&K$y dRn(~n!;k1jRX>~vK)+gm_LUd)-tlkJeg#mde^USe literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png b/OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png new file mode 100644 index 0000000000000000000000000000000000000000..fb8768a9d749fb672a09884352acb6070042c241 GIT binary patch literal 2388 zcma)8dsLFy7XLs}az??7n#vF`MbqSp;S<;G$K&=S(I?aOqa}_e z%{{G*+S;tk0fY@(=`Z(Pb+UO9FGSD@@mO*tjBG=0EOn99k4LTvSv)gteMd|Z_l(Q( zeEns8vuN3Ql5A$q-bL$wNt$Ugc~>Kf=AwZ%@uhN2%-3BlkLD|6H*Bq4VY`ORNc#Fy zv%jGh2ih721DB#WyujvJwEfV$8@Wt`JUBgJ;$`Sd@a@}9x(!jGi-rs{A7pGb_6RC+ zzyBm_kODYTYI&zPyzb!-2sWJ65XfY)0X8&I4J3xAzmdD453OJ|gP$&DxJx8s)pu$g zIJa({;XPz}l|XUAlj>YW42L}4ND6bfeO;z_jDupJ+<4)J?Ry#ZP)@G^ss%wtYNVJF z$fDE)jpcI~9DoxHY%t#=O4^B!hy*DV-9G>U-Gr-k&`Qd!`EL|G0tF)oR~#sIOctC4 zSEdSr8kju@a28AL-_-DL+WP88g6sunHO_iM(`AXg$3mBF`1T!G+tnY{5KTRH`x#64 z8=ZvnJyslY)9B(?fgq#&$B|mDp`4=y*nb_VYX}k2opO|gyeL}n>DRULgNQmado_+&)4g@(-da!-m`kBw+ z%wE}d%B&kUvTwV#2kMiowm}>L7%AfrHLEF?xJtQJ*eKC17A>9b>kqVmXaSIv9oBZ1 zYmTtMnpufTP-BAC!T*B4G%IR6s>$n%H0p2?3(j^&VuX|P*r_z$NV_S+3Fc@7Lrx%( zU?xb2+YAJXhUPyeuXSL3a=8~d4aP83AQRv>pSlN)c`ZcE1_>u0lp=0;r=;LlZC@- zl(aOt*ybS-Sq+udn8w&{`!r7Z%h&SEuf;9WQ=9@>vvlf~cfZ(o(fk4b)Z18HL{I;1 zyYAJVz(Fsg9U(`1RkS~&1QE|On~VJw!GHG}f3i9IoIQA5N^;fT?m;2ETiMP--TZf=!-fg z+SyGn$z%SFlNW^=uBR0FyiF9p!E&_mO+IrkM*V_2QwCb)+AgyOVF{*l>n*z*uRT21 zs(J0n?8L7X#@5{x^XWdvMfh{i(RH(vx}oAFVzpm7hht-mRuOzGo{E|NWTB^W@IILy zyq?ki3vNq}CuMEk;15u00U>4-)=-(db!+;`KHZ@|9_Q-egp|!p6ID1{x^-Q6#PevG zRYZ-pAB)9i;qOnx!!a0~@X|&S76xa-nfXvE8v?~axsU6#AuA?QXCps(93)L)CU#|KH!Eh_D`G>Vj_TQ*OmDFWS@UeEEI7a?_r}N1a#^%_dikgY{@9)oa)-{+81PejjNwWb*zK4K8dXfs8t)i?L_xA;gcpEQ#`crf~_W{=n; zzaKruiLEl=iLx&e0_WOudYp>lUs=SIH>FnqUeCL>7m5YV(!v307fM-0;e``-&fhyD z3-!w`QkYq@yS{jvsSe9^j@_=NKChKjlST2Xi98?5_uNgE>Cf`o>vA^-3n3Ux&~CQo z39I`N`m*H%>u|h$$vwidREN{qdB9h&jbxy5y)%6ivsMTHyZ3rpL~fx4#%gqGglKu{ zO=@JRXKyRN@^S>NUE4>(3n&86LPn};O&|eCnm_x>umBbHcn5_!Lz&EPts#R?NfX=jc0%ze zR&ppLcyW#gc3)Xs%Vm-f3qWy+$g<|lh4*2cK*C5i!?$_=rh7EfB7Z;}Q!voEiuB&A z5@A^SXJ&8S8#w8u^7zEYIM5L{f(08Z1~au;$)LFY6*{#!zss(M>^gYoCnFE9vKDHq zpBX1JvxZ#AY7nE`e%=A*=(Y<9c2Z=5`bwC8`TYmUFK`aHg4Q5`9{?>!+lYp|4F|jU v*^#3;`==Q%J$W$hF}~G#$NxhjWIJz_X%u`~&-VwvMF7Rg)$x(TPjUYM<2gqw literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_land.png b/OsmAnd/res/drawable-xxhdpi/ais_land.png new file mode 100644 index 0000000000000000000000000000000000000000..d9bc317901d02196a18cd8916977291d7f14db77 GIT binary patch literal 2941 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

7^Z zulRGY^c3)B>M!~;PViZuhve;OzRXC8se3O0#_ckxnz5`xuTaqWgFwaP`R_MjT|CaJ zT9PcaiL#n1NlNjUNe1@@(o$PC(;x-UZ%^#vYQr0<4S@?zo&a?#C)wtVr$`*fodirP(~PZ^VVGAVW4yFd!0#~JB@dfPiygC6$w znGNR_laEto>V)QB%*54S)nFZknk2S4{8zB+tpCDTtHvHobuwEDJ*Ll}Y!23eSo))D z9-@WAu><{GvP?r~VemleGIW%*UjMz1Te-{iPW(YzuxB+3JO@*(6y8?Aq{46EN2)=k zzln;)`;ID2#~K@_eP_E_)G-A5g-di8rLT)V-kq^RB_z@PEXo5kBlp||m5X?~PrSSq&wN9vXKnV{fAPSkSp*X=`%$Cg0R`#y^>fCr%%pGb zCm%+GUb@;(&jtCXH-s^A0<^V|H-&<0k>&(2B_=~|W?PxG*-l3PwukIxY*F2=es=e* zB68xAR~SgW*Pba)R~HE_cSW`YMHHF?#_Eo@8wt&w6))?8S)~3+aOJU_Ox)|A&GXFa02Y_3Mv>8Bwre0_EqWH&JRSXriIur6%IMM^2?KQDx zHuS+e)0=0d^?^9|I3Jx(GvnO{Sw|x?6`^Z~N#sZ^U)N{~vbAtpLJ9g^Uf62X{PO1L z<$IJdz2zK}^u3t(cEVa*ve9oOu$SxUhpM+&Ztr`bd_k+w&-|L7drL}0UcfbNQIu@J zWn2dq9=xD97~++6V#i}(6=JW-vXRFWd}=iIq{S^2OxxW1&4yUXGtuAz(EoE__B&_j zL~ZU|j*UeCHK5n&a?&WP`>Sks55d3}n}y87MB;aL{97}Z{U*KfO6XtFby+`=AB_Jk zf+=!)CK-$#?k$X>ylm%+A4`!em<@uAY9lOtQk}?Kn-PSc2{%8MIvh@!ZpglEaZiu5 zG+J*sYD{CR0o>hNED#VCqXVRC(A@_e9AUFG4I2<3h6hjRvMI5@Y{QCVR&_U~o@j%0 zLqlg1UT}PY>-a&gI(i(k6coLeOqSPfHct-#wW{a82OW~m0w;4;nT^~UG4CTHv8ka49pSRw9&JyL_gY7pnW*`q zkk1C-gvjZwFSfR;{1CB*-S4IE;WO-0*FVk-eufjq-xf{H{qk$c?8e<~^BW2r z$>Gc^9Np0YaD9<3y1CvsseFo%a)iscyIlL%)stooS7~ZU3d%P!}$l47S^x{Cp}&7G7^5uys4chq%5(S`gn8{lLoK`+1c- z@cPq|L#wCtR|t_5~uilo2QLsCeoHon3>UY?mb6+KK!~p1svoE?#ExQjn^x zy8Nr#@h~UNV!=7Jg3gw>p;Eayu$M9RxD=@Vf zu|ffUfLmYJ6%Jo2i{;Nc(Y@4sd3o0OjV&)poK&{kplZfiZ0Io5RunIO#{(Lvr}2Vk z6mLM3`W#T(b)gF#6|Tl>?ee#lowDGGQ$-fZE=jXaE2r}=SX9Ep`Be9g1Z)H2`rDXsX9nX)k*0tZEBvE zPOYsUxK8)zG`bi%%ugP~w*(kBtsIS2aw}WXzfiOlU*%!#UCn|)I97=)QF3-l$hHv= z5D&=zXwZraRZ)&};&zd@Zki%O4@j{H_Mj+C>%K{<0l2cN&82=Hlq)+NL{-E0+Z!6-}I$GJ4>2wmQtQ#x-H@((8m)YSceZ#X&&iF})r!asJfg zp^E=*=)c`vUZ>dlux1`jRwC}QnPc>aTdSHZrxvO_yH0O6*#d*1QZ7FkeWf?i_9-M# zx7Vv`Xt6L?nSu~y?i)AKuu8B*zHbm)HXFI|qSO>3re1#g?WkC&c@z1T#6)2Q ztXQCLw;dL@WhB@`5h@u7=AK?QHTHMVdBr)j)tRI-D;Oci7PKUM#g@Euq7gMoWC#UN{!Q z0ZI+}Nf>yYQe+a|9pH$U_LhQhrqRedhaHb%5rv*P)UlN;F1q5CO%ODK3thl+u{4Xs zyBUtd1Tguqx6MGX8e!6a_QUCVkp>!9_4EYH{(m*~|IZIv3b&+x=K5?d2Ck&>KP(8e MG+@wrb(_fl00X%?$^ZZW literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_sar.png b/OsmAnd/res/drawable-xxhdpi/ais_sar.png new file mode 100644 index 0000000000000000000000000000000000000000..259534b079e969b673449b329c3d343c5667b160 GIT binary patch literal 7574 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 bo%ol!Otjs6s`K|sSOpZHsmgtSY7+QAa`K!8 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_vessel.png b/OsmAnd/res/drawable-xxhdpi/ais_vessel.png new file mode 100644 index 0000000000000000000000000000000000000000..aa2ab6816876d1d685bfa07f02ce4bd45124d49b GIT binary patch literal 3114 zcmZwJc{o(-9{}((3o}L-MQJmZRF;&Kof(>FBxGN@vXwP5mMqzqD5-3fZA=JrO_b2g zs7c9|NcJ^lH$=nO>38Pd-=Ft)ztO2F${1P{y)o4AWi zVH$tCt(d&B?e%WeuWWdeeV?Jp5Ocnt)mPv5%*+g(jMPp-T>OnDRu~i*e5s2Sml7v- z93no{Ta=hC)e}bT)e@1k3q5|8CQ^upT2i(T=e7DWE;E)|l^Oj{!fI_sg7Qoz zH^-m4#epOGuR}GL7CE`|bTeD^vx@;j8+vRP9aw?6Z3eA(&y{IKtQTqVLfLCe?d&y9 zufGe|15BsCL{$EMt%iXRcBctj?bPCT>duzA_^pLZ-Pg57CO^+X10ZsnbIM_LE*@TK zjlS>53phZx^O1Db4FnXq#_YK4HqVQ=#@9P!goy!SZcNnqE#_BRC*hX|VgWsv=FNPq z3ppAAiSOVL`1zjk$`|d};*+|7Q!Lay*d{ZeP9huyMPe+)MMx2|ZGY<*u-`;twRb_0 zqBgq1_h_5+bZ3bu)5>pNZ$I9du}9-d-p|lUhWh%hN&7}?VUV~~wquDE@M(Qk@7RKtw{a&!iw zu7?^l$Wt8KS?3`cv};bRPib_SVSZN_7Zsa&JWvBH1D!~?-b(92w#PLcYB2>J1 zqKAX5zK)wrf>0gAmlMvP&r6y$Tb#}a*zh_22`OP3+V9A7Y1xTu=DX}C96+9hq?4N4 z$K7-m7rcs|C=V4H0%S<(AI{&4WuQ6OpLaGk3JNHv*8TnsIqX17!_2suGbBC;93zjJ z*4A!;yxx0;^=Z;&>1Bu{-681hTqr@77m8GLSPwY7pfM6yD{=^<5`FHyg*{rJ3)Ia6rx9Ot{l7N+VK|MP@>L7J+Fzz1T;n^XEEP2>O?12! z)KX4Vp8#Ij+EwNHYJLxLNNh96eV~C+53kv>Oa@|vN*}9PJ@h7U9Z&b>e%Ji4Ymcrr zmycU_G54@yz-<-gIGB6ep*)xzOrBmtmDr@qfcu!V9|9MW$283zj0*Q2Hjzo{ulIJg zN~~vryt;c5Bp*}_kcHuBN|W7H7lFB!frYB?Ud&_oM)yYdI}N;)GJYteYmrj(tC(IS zmev7BA2=QL&q8(lX$wv3*xr5>SGM8;c3)>n9IdXlgW0a5rlnPLDTX5ikg(JUX5oQ6 zlb@xVl7Zq=Ny@fRo;-EonNLe0)*<~>cMI|EUiD}ohP5&; zk9m?|3&So+ft+E+wKMMn9PJY4^nxTAqsx|k^bA1nuqi3MiZ`M{Oo8{_!PPbzAp=}r z^*(P0FYT1DWh;Gqe_cqM3lv#pp|hy3Z!yTa1ZBAO*1t&lYmZ*s*pFbFkU=FcX2IJq zb!jkMbIvZ#Er^Nb?J`L?b`cg?5P$Ss8kry&`o*ELw@^enYE*KT6>-{{%MiwN5J^Ct z>a;eN2Dq@!%to*c><5c6)*!u5{O8|TsgDv61US5H$j!`^0Yv#C@k^t9zKaK&4r>Lb zb>}Bw@=sq6wy9k|q?{5mg*wjG0$oYWZ3YpF%b$sV1ruldZs`mo8FPZ9onrMBZ`yP& z$znlViL+Z2j+QecxpE9=s`5rcWsVl`NmxYR0=Hy_=2MVtoRS+DA`~ivajiM4`DPm$ z_f-O6rt#Lwv}mrnlJ|v!Y^9&`(#!R}+cVJ#RVZ&X?k2f#nm!WvmKEwD5tcDB(!q_D zEBQL5l$yNfDbNdEpKZ7kycr$YyKP~XBWEmHoKxL){Td&`1u=z&zY5S^)!M$i+Cfw*L=n_B^Y^KJcz}Gg$a<=Pn^ocxu}PrcCMMIv()s|{Y`$O5w#63h zwfNyL-29cbHMufNC*8HiDNiP=kEy*wy(*la9?g-8TEw&<3@@)ot8?gMAn=1-z2(y<`j$*(8|V^HWcTx(?1A#4 z-Y{#Nd3q3xq9pChpJzS=w>M#V=X@DR=}ts^$}72L;ze2gzYLCR!n z^17EqNwsfR^Ax9S>dyI-T&2}Mo~eyw$n;qox^ zt`azf%c1=YwkKC*IX(2#vpWO@uTj{5TH%HEhfu9oVr); z)7iAt)_Wj+Nhwunkfpsrffc$V4Q>ikyP3aLUtYFuu~a%~UKE``Y)F0Ss=)VXI0rPh zY{~s@p2%62$t(W;_&Gg~{2w2exw{Q+dT#W2Cxz!>4?X|f9oM8oyS}BtAC@=m863F& zaUkfUPS18<8Z8H;fL9Ywxidzgx+sMVF&^g~iMrjS@ltg#uc(6e8#l8sj!xGZ>ZOsJ zzZRnVljiU}ev#ZDezv?@582_hlJ)kwhhyEOXh9E=2bu`5WjKM@h>&_Bb>@=(gXE^v z{vC4y4(Kjo@3Gx3R{&$$$Val~p*>n^Sq)@U6&S|QqAYw7fR+eyI2okAjDP<~hH1$S zqVEoZ<7xqluTvhU9 zCn0_+#V%%hhp|OrWQ+ff^$zm!2F>+eo#8jE&=AZsN*Mhu{UQ!*-!$@rY^|eXl#WD{ z)@V6%8ioyBO8BR;xiInj0d|@nyEcUh+4`lq5d%=Xh^fTD`N^d6cd3n!PtNQ=xv}XR zvbAErPTN_N4*#jRgR;6tG^1BpMsPmuW54m(qixA(J%|9_9U Yfx#9M*xmjITex=!=Z*0%^c-XU2V=X)NdN!< literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-xxhdpi/ais_vessel_cross.png new file mode 100644 index 0000000000000000000000000000000000000000..9a5d73b5ee6079e91e2b9a833bd51d2dc4c16c6b GIT binary patch literal 3913 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 Q_4lj!1uM)O6VH492gGaymH+?% literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png b/OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png new file mode 100644 index 0000000000000000000000000000000000000000..29da74357f7c5b8c2f521fd454307c478050ff8b GIT binary patch literal 3278 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 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index db81fde5204..a4143ae64f9 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -1134,6 +1134,16 @@ You need to activate the sensor so OsmAnd can find it. Cloud Precipitation Measurement units + IP address settings + Choose NMEA protocol (UDP/TCP) and define addresses + Protocol for NMEA data reception + Choose protocol for NMEA data reception + IP address of NMEA data source + Define IP address of the NMEA data source (if TCP is used) + TCP port of NMEA data source + Define TCP port number of the NMEA data source + UDP port of local NMEA data receiver + Define UPD port where OsmAnd receives NMEA data Weather Explore Weather forecast. Contours @@ -4227,6 +4237,8 @@ Download tile maps directly, or copy them as SQLite database files to OsmAnd\'s Mark where your car is parked, and notify your calendar when the parking meter will expire. To place the marker, choose a place on the map, go to \"Actions\", and tap \"Add parking\". Distance calculator and planning tool Create paths by tapping the map, or by using or modifying existing GPX files, to plan a trip and measure the distance between points. The result can be saved as a GPX file to use later for guidance. + AIS vessel tracker + Display AIS positions and information about surrounding vessels. The AIS data is received via network from an external AIS receiver. Accessibility Makes the device\'s accessibility features directly available in OsmAnd. This facilitates e.g. adjusting the speech rate for text-to-speech voices, configuring D-pad navigation, using a trackball for zoom control, or text-to-speech feedback, for example to auto-announce your position. OpenStreetMap editing diff --git a/OsmAnd/res/xml/ais_settings.xml b/OsmAnd/res/xml/ais_settings.xml new file mode 100644 index 00000000000..c3af0a4620b --- /dev/null +++ b/OsmAnd/res/xml/ais_settings.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java b/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java index c83f1d1c831..9b46ad07221 100644 --- a/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java +++ b/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java @@ -62,6 +62,8 @@ import net.osmand.plus.mapmarkers.MapMarker; import net.osmand.plus.plugins.OsmandPlugin; import net.osmand.plus.plugins.PluginsHelper; +import net.osmand.plus.plugins.aistracker.AisObject; +import net.osmand.plus.plugins.aistracker.AisObjectMenuController; import net.osmand.plus.plugins.audionotes.AudioVideoNoteMenuController; import net.osmand.plus.plugins.audionotes.AudioVideoNotesPlugin.Recording; import net.osmand.plus.plugins.mapillary.MapillaryImage; @@ -242,6 +244,8 @@ public static MenuController getMenuController(@NonNull MapActivity mapActivity, menuController = new RenderedObjectMenuController(mapActivity, pointDescription, (RenderedObject) object); } else if (object instanceof MapillaryImage) { menuController = new MapillaryMenuController(mapActivity, pointDescription, (MapillaryImage) object); + } else if (object instanceof AisObject) { + menuController = new AisObjectMenuController(mapActivity, pointDescription, (AisObject) object); } else if (object instanceof SelectedGpxPoint) { menuController = new SelectedGpxMenuController(mapActivity, pointDescription, (SelectedGpxPoint) object); } else if (object instanceof Pair) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java index 756a1e0c4f3..6bbf9153e71 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java @@ -56,6 +56,7 @@ import net.osmand.plus.plugins.skimaps.SkiMapsPlugin; import net.osmand.plus.plugins.srtm.SRTMPlugin; import net.osmand.plus.plugins.weather.WeatherPlugin; +import net.osmand.plus.plugins.aistracker.AisTrackerPlugin; import net.osmand.plus.poi.PoiUIFilter; import net.osmand.plus.quickaction.QuickActionType; import net.osmand.plus.search.dialogs.QuickSearchDialogFragment; @@ -111,6 +112,7 @@ public static void initPlugins(@NonNull OsmandApplication app) { checkMarketPlugin(app, new SRTMPlugin(app)); allPlugins.add(new WeatherPlugin(app)); checkMarketPlugin(app, new NauticalMapsPlugin(app)); + allPlugins.add(new AisTrackerPlugin(app)); checkMarketPlugin(app, new SkiMapsPlugin(app)); allPlugins.add(new AudioVideoNotesPlugin(app)); checkMarketPlugin(app, new ParkingPositionPlugin(app)); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java new file mode 100644 index 00000000000..c9a33adf3ee --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -0,0 +1,471 @@ +package net.osmand.plus.plugins.aistracker; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import net.sf.marineapi.ais.event.AbstractAISMessageListener; +import net.sf.marineapi.ais.message.AISMessage01; +import net.sf.marineapi.ais.message.AISMessage02; +import net.sf.marineapi.ais.message.AISMessage03; +import net.sf.marineapi.ais.message.AISMessage04; +import net.sf.marineapi.ais.message.AISMessage05; +import net.sf.marineapi.ais.message.AISMessage09; +import net.sf.marineapi.ais.message.AISMessage18; +import net.sf.marineapi.ais.message.AISMessage19; +import net.sf.marineapi.ais.message.AISMessage21; +import net.sf.marineapi.ais.message.AISMessage24; +import net.sf.marineapi.ais.message.AISMessage27; +import net.sf.marineapi.nmea.event.SentenceListener; +import net.sf.marineapi.nmea.io.SentenceReader; + +import java.io.IOException; +import java.io.InputStream; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.EmptyStackException; +import java.util.Stack; +import java.util.Timer; +import java.util.TimerTask; + +public class AisMessageListener { + private AisTrackerLayer aisLayer; + private Timer timer; + private TimerTask taskCheckNetworkConnection; + private DatagramSocket udpSocket; + private Socket tcpSocket; + private InputStream tcpStream; + private SentenceReader sentenceReader = null; + private Stack listenerList = null; + public AisMessageListener(int port, @NonNull AisTrackerLayer aisLayer) { + initMembers(aisLayer); + try { + udpSocket = new DatagramSocket(port); + udpSocket.setReuseAddress(true); + initListeners(); + Log.d("AisMessageListener","new UDP listener, Port " + port); + } + catch (Exception e) { + Log.e("AisMessageListener","exception: " + e.getMessage()); + udpSocket = null; + } + } + public AisMessageListener(@NonNull String serverIp, int serverPort, @NonNull AisTrackerLayer aisLayer) { + initMembers(aisLayer); + taskCheckNetworkConnection = new TimerTask() { + @Override + public void run() { + Log.d("AisMessageListener", "timer task taskCheckNetworkConnection running"); + if ((tcpSocket == null) || (!tcpSocket.isConnected())) { + try { + tcpSocket = new Socket(); + tcpSocket.setTcpNoDelay(true); + tcpSocket.setReuseAddress(true); + // tcpSocket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort), 5000); + tcpSocket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort)); + tcpStream = tcpSocket.getInputStream(); + initListeners(); + Log.d("AisMessageListener","new TCP listener"); + } + catch (IOException e) { + Log.e("AisMessageListener","exception: " + e.getMessage()); + tcpStream = null; + tcpSocket = null; + } + } + } + }; + this.timer = new Timer(); + timer.schedule(taskCheckNetworkConnection, 1000, 30000); + } + private void initMembers(@NonNull AisTrackerLayer aisLayer) { + this.aisLayer = aisLayer; + this.udpSocket = null; + this.tcpSocket = null; + this.tcpStream = null; + this.listenerList = new Stack<>(); + } + private void initListeners() throws IOException { + if (tcpStream != null) { + sentenceReader = new SentenceReader(tcpStream); + } + if (udpSocket != null) { + sentenceReader = new SentenceReader(udpSocket); + } + if (sentenceReader != null) { + new AisListener01(); + new AisListener02(); + new AisListener03(); + new AisListener04(); + new AisListener05(); + new AisListener09(); + new AisListener18(); + new AisListener19(); + new AisListener21(); + new AisListener24(); + new AisListener27(); + sentenceReader.start(); + } else { + Log.e("AisMessageListener", "sentenceReader not initialized"); + } + } + private void removeListeners() { + sentenceReader.stop(); + while (!this.listenerList.isEmpty()) { + SentenceListener listener; + try { + listener = this.listenerList.pop(); + sentenceReader.removeSentenceListener(listener); + Log.d("AisMessageListener", "SentenceListener removed"); + } catch (EmptyStackException e) { + Log.e("AisMessageListener", "stack empty"); + } + } + } + public void stopListener() { + if (this.timer != null) { + this.timer.cancel(); + this.timer.purge(); + this.timer = null; + } + removeListeners(); + if (tcpSocket != null) { + Log.d("AisMessageListener","stopListener"); + try { + if (tcpSocket.isConnected()) { + tcpSocket.close(); + } + if (tcpStream != null) { + tcpStream.close(); + } + } catch (Exception ignore) { } + } + if (udpSocket != null) { + if (udpSocket.isConnected()) { + udpSocket.disconnect(); + } + udpSocket.close(); + } + } + private void handleAisMessage(int aisType, Object obj) { + AisObject ais = null; + int msgType = 0; + int mmsi = 0; + int timeStamp = 0; + int imo = 0; + int heading = AisObjectConstants.INVALID_HEADING; + int navStatus = AisObjectConstants.INVALID_NAV_STATUS; + int manInd = AisObjectConstants.INVALID_MANEUVER_INDICATOR; + int shipType = AisObjectConstants.INVALID_SHIP_TYPE; + int dimensionToBow = AisObjectConstants.INVALID_DIMENSION; + int dimensionToStern = AisObjectConstants.INVALID_DIMENSION; + int dimensionToPort = AisObjectConstants.INVALID_DIMENSION; + int dimensionToStarboard = AisObjectConstants.INVALID_DIMENSION; + int etaMon = AisObjectConstants.INVALID_ETA; + int etaDay = AisObjectConstants.INVALID_ETA; + int etaHour = AisObjectConstants.INVALID_ETA_HOUR; + int etaMin = AisObjectConstants.INVALID_ETA_MIN; + int altitude = AisObjectConstants.INVALID_ALTITUDE; + int aidType = AisObjectConstants.UNSPECIFIED_AID_TYPE; + double draught = AisObjectConstants.INVALID_DRAUGHT; + double cog = AisObjectConstants.INVALID_COG; + double sog = AisObjectConstants.INVALID_SOG; + double lat = AisObjectConstants.INVALID_LAT; + double lon = AisObjectConstants.INVALID_LON; + double rot = AisObjectConstants.INVALID_ROT; + String callSign = null; + String shipName = null; + String destination = null; + + switch (aisType) { + case 1: AISMessage01 aisMsg01 = (AISMessage01)obj; // position report class A + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg01.getMMSI() + + " Type: " + aisMsg01.getMessageType() + + " ROT: " + aisMsg01.getRateOfTurn()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg01); + mmsi = aisMsg01.getMMSI(); + msgType = aisMsg01.getMessageType(); + navStatus = aisMsg01.getNavigationalStatus(); + manInd = aisMsg01.getManouverIndicator(); + if (aisMsg01.hasTimeStamp()) { timeStamp = aisMsg01.getTimeStamp(); } + if (aisMsg01.hasTrueHeading()) { heading = aisMsg01.getTrueHeading(); } + if (aisMsg01.hasCourseOverGround()) { cog = aisMsg01.getCourseOverGround(); } + if (aisMsg01.hasSpeedOverGround()) { sog = aisMsg01.getSpeedOverGround(); } + if (aisMsg01.hasLatitude()) { lat = aisMsg01.getLatitudeInDegrees(); } + if (aisMsg01.hasLongitude()) { lon = aisMsg01.getLongitudeInDegrees(); } + if (aisMsg01.hasRateOfTurn()) { rot = aisMsg01.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 2: AISMessage02 aisMsg02 = (AISMessage02)obj; // position report class A + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg02.getMMSI() + + " Type: " + aisMsg02.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg02); + mmsi = aisMsg02.getMMSI(); + msgType = aisMsg02.getMessageType(); + navStatus = aisMsg02.getNavigationalStatus(); + manInd = aisMsg02.getManouverIndicator(); + if (aisMsg02.hasTimeStamp()) { timeStamp = aisMsg02.getTimeStamp(); } + if (aisMsg02.hasTrueHeading()) { heading = aisMsg02.getTrueHeading(); } + if (aisMsg02.hasCourseOverGround()) { cog = aisMsg02.getCourseOverGround(); } + if (aisMsg02.hasSpeedOverGround()) { sog = aisMsg02.getSpeedOverGround(); } + if (aisMsg02.hasLatitude()) { lat = aisMsg02.getLatitudeInDegrees(); } + if (aisMsg02.hasLongitude()) { lon = aisMsg02.getLongitudeInDegrees(); } + if (aisMsg02.hasRateOfTurn()) { rot = aisMsg02.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 3: AISMessage03 aisMsg03 = (AISMessage03)obj; // position report class A + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg03.getMMSI() + + " Type: " + aisMsg03.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg03); + mmsi = aisMsg03.getMMSI(); + msgType = aisMsg03.getMessageType(); + navStatus = aisMsg03.getNavigationalStatus(); + manInd = aisMsg03.getManouverIndicator(); + if (aisMsg03.hasTimeStamp()) { timeStamp = aisMsg03.getTimeStamp(); } + if (aisMsg03.hasTrueHeading()) { heading = aisMsg03.getTrueHeading(); } + if (aisMsg03.hasCourseOverGround()) { cog = aisMsg03.getCourseOverGround(); } + if (aisMsg03.hasSpeedOverGround()) { sog = aisMsg03.getSpeedOverGround(); } + if (aisMsg03.hasLatitude()) { lat = aisMsg03.getLatitudeInDegrees(); } + if (aisMsg03.hasLongitude()) { lon = aisMsg03.getLongitudeInDegrees(); } + if (aisMsg03.hasRateOfTurn()) { rot = aisMsg03.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 4: AISMessage04 aisMsg04 = (AISMessage04)obj; // base station report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg04.getMMSI() + + " Type: " + aisMsg04.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg04); + mmsi = aisMsg04.getMMSI(); + msgType = aisMsg04.getMessageType(); + if (aisMsg04.hasLatitude()) { lat = aisMsg04.getLatitudeInDegrees(); } + if (aisMsg04.hasLongitude()) { lon = aisMsg04.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, lat, lon); + break; + + case 5: AISMessage05 aisMsg05 = (AISMessage05)obj; // static and voyage related data + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg05.getMMSI() + + " Type: " + aisMsg05.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg05); + mmsi = aisMsg05.getMMSI(); + msgType = aisMsg05.getMessageType(); + imo = aisMsg05.getIMONumber(); + callSign = aisMsg05.getCallSign(); + shipName = aisMsg05.getName(); + shipType = aisMsg05.getTypeOfShipAndCargoType(); + dimensionToBow = aisMsg05.getBow(); + dimensionToStern = aisMsg05.getStern(); + dimensionToPort = aisMsg05.getPort(); + dimensionToStarboard = aisMsg05.getStarboard(); + draught = aisMsg05.getMaximumDraught(); + destination = aisMsg05.getDestination(); + etaMon = aisMsg05.getETAMonth(); + etaDay = aisMsg05.getETADay(); + etaHour = aisMsg05.getETAHour(); + etaMin = aisMsg05.getETAMinute(); + ais = new AisObject(mmsi, msgType, imo, callSign, shipName, shipType, dimensionToBow, + dimensionToStern, dimensionToPort, dimensionToStarboard, draught, + destination, etaMon, etaDay, etaHour, etaMin); + break; + + case 9: AISMessage09 aisMsg09 = (AISMessage09)obj; // SAR aircraft position report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg09.getMMSI() + + " Type: " + aisMsg09.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg09); + mmsi = aisMsg09.getMMSI(); + msgType = aisMsg09.getMessageType(); + timeStamp = aisMsg09.getTimeStamp(); + cog = aisMsg09.getCourseOverGround(); + sog = aisMsg09.getSpeedOverGround(); + altitude = aisMsg09.getAltitude(); + if (aisMsg09.hasLatitude()) { lat = aisMsg09.getLatitudeInDegrees(); } + if (aisMsg09.hasLongitude()) { lon = aisMsg09.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, timeStamp, altitude, cog, sog, lat, lon); + break; + + case 18: AISMessage18 aisMsg18 = (AISMessage18)obj; // basic class B position report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg18.getMMSI() + + " Type: " + aisMsg18.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg18); + mmsi = aisMsg18.getMMSI(); + msgType = aisMsg18.getMessageType(); + if (aisMsg18.hasTimeStamp()) { timeStamp = aisMsg18.getTimeStamp(); } + if (aisMsg18.hasTrueHeading()) { heading = aisMsg18.getTrueHeading(); } + if (aisMsg18.hasCourseOverGround()) { cog = aisMsg18.getCourseOverGround(); } + if (aisMsg18.hasSpeedOverGround()) { sog = aisMsg18.getSpeedOverGround(); } + if (aisMsg18.hasLatitude()) { lat = aisMsg18.getLatitudeInDegrees(); } + if (aisMsg18.hasLongitude()) { lon = aisMsg18.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 19: AISMessage19 aisMsg19 = (AISMessage19)obj; // extended class B position report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg19.getMMSI() + + " Type: " + aisMsg19.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg19); + mmsi = aisMsg19.getMMSI(); + msgType = aisMsg19.getMessageType(); + shipType = aisMsg19.getTypeOfShipAndCargoType(); + dimensionToBow = aisMsg19.getBow(); + dimensionToStern = aisMsg19.getStern(); + dimensionToPort = aisMsg19.getPort(); + dimensionToStarboard = aisMsg19.getStarboard(); + if (aisMsg19.hasTimeStamp()) { timeStamp = aisMsg19.getTimeStamp(); } + if (aisMsg19.hasTrueHeading()) { heading = aisMsg19.getTrueHeading(); } + if (aisMsg19.hasCourseOverGround()) { cog = aisMsg19.getCourseOverGround(); } + if (aisMsg19.hasSpeedOverGround()) { sog = aisMsg19.getSpeedOverGround(); } + if (aisMsg19.hasLatitude()) { lat = aisMsg19.getLatitudeInDegrees(); } + if (aisMsg19.hasLongitude()) { lon = aisMsg19.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, timeStamp, heading, cog, sog, lat, lon, + shipType, dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + break; + + case 21: AISMessage21 aisMsg21 = (AISMessage21)obj; // aid-to-navigation report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg21.getMMSI() + + " Type: " + aisMsg21.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg21); + mmsi = aisMsg21.getMMSI(); + msgType = aisMsg21.getMessageType(); + dimensionToBow = aisMsg21.getBow(); + dimensionToStern = aisMsg21.getStern(); + dimensionToPort = aisMsg21.getPort(); + dimensionToStarboard = aisMsg21.getStarboard(); + aidType = aisMsg21.getAidType(); + if (aisMsg21.hasLatitude()) { lat = aisMsg21.getLatitudeInDegrees(); } + if (aisMsg21.hasLongitude()) { lon = aisMsg21.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, lat, lon, aidType, + dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + break; + + case 24: AISMessage24 aisMsg24 = (AISMessage24)obj; // static data report (like type 5) + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg24.getMMSI() + + " Type: " + aisMsg24.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg24); + mmsi = aisMsg24.getMMSI(); + msgType = aisMsg24.getMessageType(); + callSign = aisMsg24.getCallSign(); + shipName = aisMsg24.getName(); + shipType = aisMsg24.getTypeOfShipAndCargoType(); + dimensionToBow = aisMsg24.getBow(); + dimensionToStern = aisMsg24.getStern(); + dimensionToPort = aisMsg24.getPort(); + dimensionToStarboard = aisMsg24.getStarboard(); + ais = new AisObject(mmsi, msgType, imo, callSign, shipName, shipType, dimensionToBow, + dimensionToStern, dimensionToPort, dimensionToStarboard, draught, + null, etaMon, etaDay, etaHour, etaMin); + break; + + case 27: AISMessage27 aisMsg27 = (AISMessage27)obj; // long range broadcast message + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg27.getMMSI() + + " Type: " + aisMsg27.getMessageType()); + Log.d("AisMessageListener","handleAisMessage() " + aisMsg27); + mmsi = aisMsg27.getMMSI(); + msgType = aisMsg27.getMessageType(); + navStatus = aisMsg27.getNavigationalStatus(); + manInd = aisMsg27.getManouverIndicator(); + if (aisMsg27.hasTimeStamp()) { timeStamp = aisMsg27.getTimeStamp(); } + if (aisMsg27.hasTrueHeading()) { heading = aisMsg27.getTrueHeading(); } + if (aisMsg27.hasCourseOverGround()) { cog = aisMsg27.getCourseOverGround(); } + if (aisMsg27.hasSpeedOverGround()) { sog = aisMsg27.getSpeedOverGround(); } + if (aisMsg27.hasLatitude()) { lat = aisMsg27.getLatitudeInDegrees(); } + if (aisMsg27.hasLongitude()) { lon = aisMsg27.getLongitudeInDegrees(); } + if (aisMsg27.hasRateOfTurn()) { rot = aisMsg27.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + default: + Log.e("AisMessageListener","handleAisMessage() invalid argument aisType: "+ aisType); + return; + } + aisLayer.updateAisObjectList(ais); + } + private void initEmbeddedLister(int aisType, @NonNull SentenceListener listener) { + AisMessageListener.this.sentenceReader.addSentenceListener(listener); + AisMessageListener.this.listenerList.push(listener); + Log.d("AisMessageListener","Listener Type " + aisType + " started"); + } + private class AisListener01 extends AbstractAISMessageListener { + public AisListener01() { initEmbeddedLister(1, this); } + @Override + public void onMessage(AISMessage01 msg) { + handleAisMessage(1, msg); + } + } + private class AisListener02 extends AbstractAISMessageListener { + public AisListener02() { initEmbeddedLister(2, this); } + @Override + public void onMessage(AISMessage02 msg) { + handleAisMessage(2, msg); + } + } + private class AisListener03 extends AbstractAISMessageListener { + public AisListener03() { initEmbeddedLister(3, this); } + @Override + public void onMessage(AISMessage03 msg) { + handleAisMessage(3, msg); + } + } + private class AisListener04 extends AbstractAISMessageListener { + public AisListener04() { initEmbeddedLister(4, this); } + @Override + public void onMessage(AISMessage04 msg) { + handleAisMessage(4, msg); + } + } + private class AisListener05 extends AbstractAISMessageListener { + public AisListener05() { initEmbeddedLister(5, this); } + @Override + public void onMessage(AISMessage05 msg) { + handleAisMessage(5, msg); + } + } + private class AisListener09 extends AbstractAISMessageListener { + public AisListener09() { initEmbeddedLister(9, this); } + @Override + public void onMessage(AISMessage09 msg) { + handleAisMessage(9, msg); + } + } + private class AisListener18 extends AbstractAISMessageListener { + public AisListener18() { initEmbeddedLister(18, this); } + @Override + public void onMessage(AISMessage18 msg) { + handleAisMessage(18, msg); + } + } + private class AisListener19 extends AbstractAISMessageListener { + public AisListener19() { initEmbeddedLister(19, this); } + @Override + public void onMessage(AISMessage19 msg) { + handleAisMessage(19, msg); + } + } + private class AisListener21 extends AbstractAISMessageListener { + public AisListener21() { initEmbeddedLister(21, this); } + @Override + public void onMessage(AISMessage21 msg) { + handleAisMessage(21, msg); + } + } + private class AisListener24 extends AbstractAISMessageListener { + public AisListener24() { initEmbeddedLister(24, this); } + @Override + public void onMessage(AISMessage24 msg) { + handleAisMessage(24, msg); + } + } + private class AisListener27 extends AbstractAISMessageListener { + public AisListener27() { initEmbeddedLister(27, this); } + @Override + public void onMessage(AISMessage27 msg) { + handleAisMessage(27, msg); + } + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java new file mode 100644 index 00000000000..7143414ecec --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -0,0 +1,802 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_AIRPLANE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON_VIRTUAL; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_INVALID; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_LANDSTATION; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_SART; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_COMMERCIAL; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_FAST; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_FREIGHT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_PASSENGER; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_SPORT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.COUNTRY_CODES; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ALTITUDE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_COG; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_DIMENSION; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_DRAUGHT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA_HOUR; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA_MIN; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_HEADING; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_LAT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_LON; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_MANEUVER_INDICATOR; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_NAV_STATUS; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ROT; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.maxAgeInMinutes; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.maxVesselAgeInMinutes; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LightingColorFilter; +import android.graphics.Paint; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.data.LatLon; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.R; + +import java.util.SortedSet; +import java.util.TreeSet; + +public class AisObject { + /* variable names starting with "ais_" belong to values received via an AIS message, + * its values may differ from the received values: they can be scaled, + * see https://gpsd.gitlab.io/gpsd/AIVDM.html */ + private int ais_msgType; + private int ais_mmsi; + private int ais_timeStamp = 0; + private int ais_imo = 0; + private int ais_heading = INVALID_HEADING; + private int ais_navStatus = INVALID_NAV_STATUS; + private int ais_manInd = INVALID_MANEUVER_INDICATOR; + private int ais_shipType = INVALID_SHIP_TYPE; + private int ais_dimensionToBow = INVALID_DIMENSION; + private int ais_dimensionToStern = INVALID_DIMENSION; + private int ais_dimensionToPort = INVALID_DIMENSION; + private int ais_dimensionToStarboard = INVALID_DIMENSION; + private int ais_etaMon = INVALID_ETA; + private int ais_etaDay = INVALID_ETA; + private int ais_etaHour = INVALID_ETA_HOUR; + private int ais_etaMin = INVALID_ETA_MIN; + private int ais_altitude = INVALID_ALTITUDE; + private int ais_aidType = UNSPECIFIED_AID_TYPE; + private double ais_draught = INVALID_DRAUGHT; + private double ais_cog = INVALID_COG; + private double ais_sog = INVALID_SOG; + private double ais_rot = INVALID_ROT; + private LatLon ais_position = null; + private String ais_callSign = null; + private String ais_shipName = null; + private String ais_destination = null; + private String countryCode = null; + private SortedSet msgTypes = null; + private long lastUpdate = 0; + /* after this time the object is outdated and can be removed: */ + + private AisObjType objectClass; + private Bitmap bitmap = null; + private int bitmapColor; + + public AisObject(int mmsi, int msgType, double lat, double lon) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int timeStamp, int navStatus, int manInd, int heading, + double cog, double sog, double lat, double lon, double rot) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + this.ais_timeStamp = timeStamp; + this.ais_navStatus = navStatus; + this.ais_manInd = manInd; + this.ais_heading = heading; + this.ais_cog = cog; + this.ais_sog = sog; + this.ais_rot = rot; + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int timeStamp, int altitude, + double cog, double sog, double lat, double lon) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + this.ais_timeStamp = timeStamp; + this.ais_altitude = altitude; + this.ais_cog = cog; + this.ais_sog = sog; + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int timeStamp, int heading, + double cog, double sog, double lat, double lon, + int shipType, int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + initDimensions(dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + this.ais_timeStamp = timeStamp; + this.ais_heading = heading; + this.ais_cog = cog; + this.ais_sog = sog; + this.ais_shipType = shipType; + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int imo, @Nullable String callSign, @Nullable String shipName, + int shipType, int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard, + double draught, @Nullable String destination, int etaMon, + int etaDay, int etaHour, int etaMin) { + initObj(mmsi, msgType); + initDimensions(dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + this.ais_shipType = shipType; + this.ais_draught = draught; + this.ais_callSign = callSign; + this.ais_shipName = shipName; + this.ais_destination = destination; + this.ais_etaMon = etaMon; + this.ais_etaDay = etaDay; + this.ais_etaHour = etaHour; + this.ais_etaMin = etaMin; + this.ais_imo = imo; + initObjectClass(); + } + + public AisObject(int mmsi, int msgType, double lat, double lon, int aidType, + int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + initDimensions(dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + this.ais_aidType = aidType; + initObjectClass(); + } + public AisObject(@NonNull AisObject ais) { + this.set(ais); + } + private String getCountryCode(Integer mmsi) { + String mmsiString = mmsi.toString(); + + if (mmsiString.length() > 2) { + String countryCode = mmsiString.substring(0, 3); + mmsiString = COUNTRY_CODES.get(Integer.parseInt(countryCode)); + if (mmsiString != null) { + return mmsiString; + } + } + return ""; + } + /* to be called only by a contructor! */ + private void initObj(int mmsi, int msgType) { + this.msgTypes = new TreeSet<>(); + this.ais_mmsi = mmsi; + this.ais_msgType = msgType; + this.countryCode = getCountryCode(this.ais_mmsi); + this.msgTypes.add(ais_msgType); + this.lastUpdate = System.currentTimeMillis(); + } + private void initLatLon(double lat, double lon) { + if ((lat != INVALID_LAT) && (lon != INVALID_LON)) { + ais_position = new LatLon(lat, lon); + } + } + + private void initDimensions(int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard) { + this.ais_dimensionToBow = dimensionToBow; + this.ais_dimensionToStern = dimensionToStern; + this.ais_dimensionToPort = dimensionToPort; + this.ais_dimensionToStarboard = dimensionToStarboard; + } + + private void initObjectClass() { + switch (this.ais_shipType) { + case INVALID_SHIP_TYPE: // not initialized + break; + + case 20: // Wing in ground (WIG) + case 21: // WIG, Hazardous category A + case 22: // WIG, Hazardous category B + case 23: // WIG, Hazardous category C + case 24: // WIG, Hazardous category D + case 40: // High Speed Craft (HSC) + case 41: // HSC, Hazardous category A + case 42: // HSC, Hazardous category B + case 43: // HSC, Hazardous category C + case 44: // HSC, Hazardous category D + case 49: // HSC, No additional information + this.objectClass = AIS_VESSEL_FAST; + break; + + case 30: // Fishing + case 31: // Towing + case 32: // Towing + case 33: // Dredging + case 34: // Diving ops + case 35: // Military ops + case 50: // Pilot Vessel + case 51: // Search and Rescue vessel + case 52: // Tug + case 53: // Port Tender + case 54: // Anti-pollution equipment + case 55: // Law Enforcement + case 56: // Spare - Local Vessel + case 57: // Spare - Local Vessel + case 58: // Medical Transport + case 59: // Noncombatant ship according to RR Resolution No. 18 + this.objectClass = AIS_VESSEL_COMMERCIAL; + break; + + case 36: // Sailing + case 37: // Pleasure Craft + this.objectClass = AIS_VESSEL_SPORT; + break; + + case 60: // Passenger, all ships of this type + case 61: // Passenger, Hazardous category A + case 62: // Passenger, Hazardous category B + case 63: // Passenger, Hazardous category C + case 64: // Passenger, Hazardous category D + case 69: // Passenger, No additional information + this.objectClass = AIS_VESSEL_PASSENGER; + break; + + case 70: // Cargo, all ships of this type + case 71: // Cargo, Hazardous category A + case 72: // Cargo, Hazardous category B + case 73: // Cargo, Hazardous category C + case 74: // Cargo, Hazardous category D + case 79: // Cargo, No additional information + case 80: // Tanker, all ships of this type + case 81: // Tanker, Hazardous category A + case 82: // Tanker, Hazardous category B + case 83: // Tanker, Hazardous category C + case 84: // Tanker, Hazardous category D + case 89: // Tanker, No additional information + this.objectClass = AIS_VESSEL_FREIGHT; + break; + + case 90: // Other Type, all ships of this type + case 91: // Other Type, Hazardous category A + case 92: // Other Type, Hazardous category B + case 93: // Other Type, Hazardous category C + case 94: // Other Type, Hazardous category D + case 99: // Other Type, no additional information + default: + this.objectClass = AIS_VESSEL; + break; + } + /* for the case that no ship type was transmitted... */ + if (ais_shipType == INVALID_SHIP_TYPE) { + if (msgTypes.contains(9)) { // aircraft + this.objectClass = AIS_AIRPLANE; + } else if (msgTypes.contains(4)) { // base station + this.objectClass = AIS_LANDSTATION; + } else if (msgTypes.contains(21)) { // aids to navigation + switch (ais_aidType) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_21_aid_to_navigation_report + case 29: // Safe Water + case 30: // Special Mark + this.objectClass = AIS_ATON_VIRTUAL; + break; + default: + this.objectClass = AIS_ATON; + } + } else { + switch (ais_navStatus) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a + case 0: // Under way using engine + case 1: // At anchor + case 2: // Not under command + case 3: // Restricted manoeuverability + case 4: // Constrained by her draught + case 5: // Moored + case 6: // Aground + case 8: // Under way sailing + case 11: // Power-driven vessel towing astern (regional use) + case 12: // Power-driven vessel pushing ahead or towing alongside (regional use). + this.objectClass = AIS_VESSEL; + break; + + case 7: // Engaged in Fishing + this.objectClass = AIS_VESSEL_COMMERCIAL; + break; + + case 14: // AIS-SART is active + this.objectClass = AIS_SART; + break; + + case INVALID_NAV_STATUS: // no valid value + default: + this.objectClass = AIS_INVALID; + } + } + } + } + + public void set(@NonNull AisObject ais) { + this.ais_mmsi = ais.getMmsi(); + this.ais_msgType = ais.getMsgType(); + if (ais.getTimestamp() != 0) { this.ais_timeStamp = ais.getTimestamp(); } + if (ais.getImo() != 0 ) { this.ais_imo = ais.getImo(); } + if (ais.getHeading() != INVALID_HEADING ) { this.ais_heading = ais.getHeading(); } + if (ais.getNavStatus() != INVALID_NAV_STATUS ) { this.ais_navStatus = ais.getNavStatus(); } + if (ais.getManInd() != INVALID_MANEUVER_INDICATOR ) { this.ais_manInd = ais.getManInd(); } + if (ais.getShipType() != INVALID_SHIP_TYPE ) { this.ais_shipType = ais.getShipType(); } + if (ais.getDimensionToBow() != INVALID_DIMENSION ) { this.ais_dimensionToBow = ais.getDimensionToBow(); } + if (ais.getDimensionToStern() != INVALID_DIMENSION ) { this.ais_dimensionToStern = ais.getDimensionToStern(); } + if (ais.getDimensionToPort() != INVALID_DIMENSION ) { this.ais_dimensionToPort = ais.getDimensionToPort(); } + if (ais.getDimensionToStarboard() != INVALID_DIMENSION ) { this.ais_dimensionToStarboard = ais.getDimensionToStarboard(); } + if (ais.getEtaMon() != INVALID_ETA ) { this.ais_etaMon = ais.getEtaMon(); } + if (ais.getEtaDay() != INVALID_ETA ) { this.ais_etaDay = ais.getEtaDay(); } + if (ais.getEtaHour() != INVALID_ETA_HOUR ) { this.ais_etaHour = ais.getEtaHour(); } + if (ais.getEtaMin() != INVALID_ETA_MIN ) { this.ais_etaMin = ais.getEtaMin(); } + if (ais.getAltitude() != INVALID_ALTITUDE) { this.ais_altitude = ais.getAltitude(); } + if (ais.getAidType() != UNSPECIFIED_AID_TYPE) { this.ais_aidType = ais.getAidType(); } + if (ais.getDraught() != INVALID_DRAUGHT) { this.ais_draught = ais.getDraught(); } + if (ais.getCog() != INVALID_COG) { this.ais_cog = ais.getCog(); } + if (ais.getSog() != INVALID_SOG) { this.ais_sog = ais.getSog(); } + if (ais.getRot() != INVALID_ROT) { this.ais_rot = ais.getRot(); } + if (ais.getPosition() != null) { this.ais_position = ais.getPosition(); } + if (ais.getCallSign() != null) { this.ais_callSign = ais.getCallSign(); } + if (ais.getShipName() != null) { this.ais_shipName = ais.getShipName(); } + if (ais.getDestination() != null ) { this.ais_destination = ais.getDestination(); } + + this.countryCode = ais.getCountryCode(); + + /* this method does not produce an exact copy of the given object, here are the differences: */ + this.lastUpdate = System.currentTimeMillis(); + if (this.msgTypes == null) { + this.msgTypes = new TreeSet<>(); + } + this.msgTypes.add(ais_msgType); + this.initObjectClass(); + //this.objectClass = ais.getObjectClass(); // test only... remove later + this.bitmap = null; + this.bitmapColor = 0; + } + + private void setBitmap(@NonNull AisTrackerLayer mapLayer) { + if (isLost()) { + if (isMovable()) { + this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); + } + } else { + switch (this.objectClass) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + case AIS_INVALID: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel); + break; + case AIS_LANDSTATION: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_land); + break; + case AIS_AIRPLANE: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_plane); + break; + case AIS_SART: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_sar); + break; + case AIS_ATON: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton); + break; + case AIS_ATON_VIRTUAL: + this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton_virt); + break; + } + } + this.setColor(); + } + + private void setColor() { + if (isLost()) { + if (isMovable()) { + this.bitmapColor = 0; // black + } + } else { + switch (this.objectClass) { + case AIS_VESSEL: + this.bitmapColor = Color.GREEN; + break; + case AIS_VESSEL_SPORT: + this.bitmapColor = Color.YELLOW; + break; + case AIS_VESSEL_FAST: + this.bitmapColor = Color.BLUE; + break; + case AIS_VESSEL_PASSENGER: + this.bitmapColor = Color.CYAN; + break; + case AIS_VESSEL_FREIGHT: + this.bitmapColor = Color.GRAY; + break; + case AIS_VESSEL_COMMERCIAL: + this.bitmapColor = Color.LTGRAY; + break; + default: + this.bitmapColor = 0; // black + } + } + } + + public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, + @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { + if ((this.bitmap == null) || isLost()) { + this.setBitmap(mapLayer); + } + if (this.bitmapColor != 0) { + paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); + } else { + paint.setColorFilter(null); + } + if (this.bitmap != null) { + canvas.save(); + canvas.rotate(tileBox.getRotate(), (float)tileBox.getCenterPixelX(), (float)tileBox.getCenterPixelY()); + float speedFactor = getMovement(); + int locationX = tileBox.getPixXFromLonNoRot(this.ais_position.getLongitude()); + int locationY = tileBox.getPixYFromLatNoRot(this.ais_position.getLatitude()); + float fx = locationX - this.bitmap.getWidth() / 2.0f; + float fy = locationY - this.bitmap.getHeight() / 2.0f; + if (this.needRotation()) { + float rotation = 0; + if (this.ais_cog != INVALID_COG) { rotation = (float)this.ais_cog; } + else if (this.ais_heading != INVALID_HEADING ) { rotation = this.ais_heading; } + canvas.rotate(rotation, locationX, locationY); + } + canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); + if ((speedFactor > 0) && (!isLost())) { + float lineStartX = locationX; + float lineLength = (float)this.bitmap.getHeight() * speedFactor; + float lineStartY = locationY - this.bitmap.getHeight() / 4.0f; + float lineEndY = lineStartY - lineLength; + canvas.drawLine(lineStartX, lineStartY, lineStartX, lineEndY, paint); + } + canvas.restore(); + } + } + + public boolean isMovable() { + switch (this.objectClass) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + case AIS_AIRPLANE: + return true; + default: + return false; + } + } + /* + for AIS objects that are moving, return a value that is taken as multiple of bitmap + height to draw a line to indicate the speed, + otherwise return 0 (no movement) + */ + private float getMovement() { + if (this.ais_sog > 0.0d) { + if (isMovable()) { + if (this.ais_sog < 2.0d) { return 0.0f; } + if (this.ais_sog < 5.0d) { return 1.0f; } + if (this.ais_sog < 10.0d) { return 3.0f; } + if (this.ais_sog < 25.0d) { return 6.0f; } + return 8.0f; + } + } + return 0.0f; + } + private boolean needRotation() { + if (((this.ais_cog != INVALID_COG) && (this.ais_cog != 0)) || + ((this.ais_heading != INVALID_HEADING) && (this.ais_heading != 0))) + { + return isMovable(); + } + return false; + } + + private boolean isLost(long maxAgeInMin) { + return ((System.currentTimeMillis() - this.lastUpdate) / 1000 / 60) > maxAgeInMin; + } + private boolean isLost() { + return isLost(maxVesselAgeInMinutes); + } + + /* + * this function checks the age of the object (check lastUpdate against its limit) + * and returns true if the object is outdated and can be removed + * */ + public boolean checkObjectAge() { + return isLost(maxAgeInMinutes); + } + public int getMsgType() { return this.ais_msgType; } + public SortedSet getMsgTypes() { return this.msgTypes; } + public int getMmsi() { return this.ais_mmsi; } + public int getTimestamp() { return this.ais_timeStamp; } + public int getImo() { return this.ais_imo; } + public int getHeading() { return this.ais_heading; } + public int getNavStatus() { return this.ais_navStatus; } + public int getManInd() { return this.ais_manInd; } + public int getShipType() { return this.ais_shipType; } + public int getDimensionToBow() { return this.ais_dimensionToBow; } + public int getDimensionToStern() { return this.ais_dimensionToStern; } + public int getDimensionToPort() { return this.ais_dimensionToPort; } + public int getDimensionToStarboard() { return this.ais_dimensionToStarboard; } + public int getEtaMon() { return this.ais_etaMon; } + public int getEtaDay() { return this.ais_etaDay; } + public int getEtaHour() { return this.ais_etaHour; } + public int getEtaMin() { return this.ais_etaMin; } + public int getAltitude() { return this.ais_altitude; } + public int getAidType() { return this.ais_aidType; } + public double getCog() { return this.ais_cog; } + public double getSog() { return this.ais_sog; } + public double getRot() { return this.ais_rot; } + public double getDraught() { return this.ais_draught; } + @Nullable + public LatLon getPosition() { + return this.ais_position; + } + @Nullable + public String getCallSign() { + return this.ais_callSign; + } + @Nullable + public String getShipName() { + return this.ais_shipName; + } + @Nullable + public String getDestination() { + return this.ais_destination; + } + @NonNull + public String getCountryCode() { return this.countryCode; } + public AisObjType getObjectClass() { return this.objectClass; } + public long getLastUpdate() { return this.lastUpdate; } + @NonNull + public String getShipTypeString() { + switch (this.ais_shipType) { + case INVALID_SHIP_TYPE: // not initialized + return("unknown"); + case 20: + return("Wing in ground (WIG)"); + case 21: + return("WIG, Hazardous category A"); + case 22: + return("WIG, Hazardous category B"); + case 23: + return("WIG, Hazardous category C"); + case 24: + return("WIG, Hazardous category D"); + case 40: + return("High Speed Craft (HSC)"); + case 41: + return("HSC, Hazardous category A"); + case 42: + return("HSC, Hazardous category B"); + case 43: + return("HSC, Hazardous category C"); + case 44: + return("HSC, Hazardous category D"); + case 49: // HSC, No additional information + return("High Speed Craft (HSC)"); + case 30: + return("Fishing"); + case 31: + return("Towing"); + case 32: + return("Towing"); + case 33: + return("Dredging"); + case 34: + return("Diving ops"); + case 35: + return("Military ops"); + case 50: + return("Pilot Vessel"); + case 51: + return("Search and Rescue vessel"); + case 52: + return("Tug"); + case 53: + return("Port Tender"); + case 54: + return("Anti-pollution equipment"); + case 55: + return("Law Enforcement"); + case 56: + return("Spare - Local Vessel"); + case 57: + return("Spare - Local Vessel"); + case 58: + return("Medical Transport"); + case 59: + return("Noncombatant ship according to RR Resolution No. 18"); + case 36: + return("Sailing"); + case 37: + return("Pleasure Craft"); + case 60: + return("Passenger"); + case 61: + return("Passenger, Hazardous category A"); + case 62: + return("Passenger, Hazardous category B"); + case 63: + return("Passenger, Hazardous category C"); + case 64: + return("Passenger, Hazardous category D"); + case 69: // Passenger, No additional information + return("Passenger"); + case 70: // Cargo, all ships of this type + return("Cargo"); + case 71: + return("Cargo, Hazardous category A"); + case 72: + return("Cargo, Hazardous category B"); + case 73: + return("Cargo, Hazardous category C"); + case 74: + return("Cargo, Hazardous category D"); + case 79: // Cargo, No additional information + return("Cargo"); + case 80: // Tanker, all ships of this type + return("Tanker"); + case 81: + return("Tanker, Hazardous category A"); + case 82: + return("Tanker, Hazardous category B"); + case 83: + return("Tanker, Hazardous category C"); + case 84: + return("Tanker, Hazardous category D"); + case 89: // Tanker, No additional information + return("Tanker"); + case 90: // Other Type, all ships of this type + return("Other Type"); + case 91: + return("Other Type, Hazardous category A"); + case 92: + return("Other Type, Hazardous category B"); + case 93: + return("Other Type, Hazardous category C"); + case 94: + return("Other Type, Hazardous category D"); + case 99: // Other Type, no additional information + return("Other Type"); + default: + return Integer.toString(ais_shipType); + } + } + @NonNull + public String getNavStatusString() { + switch (this.ais_navStatus) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a + case 0: + return("Under way using engine"); + case 1: + return("At anchor"); + case 2: + return("Not under command"); + case 3: + return("Restricted manoeuverability"); + case 4: + return("Constrained by her draught"); + case 5: + return("Moored"); + case 6: + return("Aground"); + case 8: + return("Under way sailing"); + case 11: + return("Power-driven vessel towing astern (regional use)"); + case 12: + return("Power-driven vessel pushing ahead or towing alongside (regional use)"); + case 7: + return("Engaged in Fishing"); + case 14: + return("AIS-SART is active"); + case INVALID_NAV_STATUS: // no valid value + return("unknown"); + default: + return(Integer.toString(ais_navStatus)); + } + } + @NonNull + public String getManIndString() { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a + switch (this.ais_manInd) { + case 0: + return("Not available"); + case 1: + return("No special maneuver"); + case 2: + return("Special maneuver"); + default: + return(Integer.toString(ais_manInd)); + } + } + @NonNull + public String getAidTypeString() { + switch (this.ais_aidType) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_21_aid_to_navigation_report + case 0: + return("not specified"); + case 1: + return("Reference point"); + case 2: + return("RACON (radar transponder marking a navigation hazard)"); + case 3: + return("Fixed structure off shore"); + case 4: + return("Spare, Reserved for future use"); + case 5: + return("Light, without sectors"); + case 6: + return("Light, with sectors"); + case 7: + return("Leading Light Front"); + case 8: + return("Leading Light Rear"); + case 9: + return("Beacon, Cardinal N"); + case 10: + return("Beacon, Cardinal E"); + case 11: + return("Beacon, Cardinal S"); + case 12: + return("Beacon, Cardinal W"); + case 13: + return("Beacon, Port hand"); + case 14: + return("Beacon, Starboard hand"); + case 15: + return("Beacon, Preferred Channel port hand"); + case 16: + return("Beacon, Preferred Channel starboard hand"); + case 17: + return("Beacon, Isolated danger"); + case 18: + return("Beacon, Safe wate"); + case 19: + return("Beacon, Special mark"); + case 20: + return("Cardinal Mark N"); + case 21: + return("Cardinal Mark E"); + case 22: + return("Cardinal Mark S"); + case 23: + return("Cardinal Mark W"); + case 24: + return("Port hand Mark"); + case 25: + return("Starboard hand Mark"); + case 26: + return("Preferred Channel Port hand"); + case 27: + return("Preferred Channel Starboard hand"); + case 28: + return("Isolated danger"); + case 29: + return("Safe Water"); + case 30: + return("Special Mark"); + case 31: + return("Light Vessel / LANBY / Rigs"); + default: + return(Integer.toString(ais_aidType)); + } + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java new file mode 100644 index 00000000000..9b3dc923953 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -0,0 +1,335 @@ +package net.osmand.plus.plugins.aistracker; + +import java.util.AbstractMap; +import java.util.Map; + +public final class AisObjectConstants { + /* after this time the object is outdated and can be removed: */ + public final static long maxAgeInMinutes = 7; + /* after this time the (movable) object is lost, the bitmap can be changed: */ + public final static long maxVesselAgeInMinutes = 4; + public final static int INVALID_HEADING = 511; + public final static int INVALID_NAV_STATUS = 15; + public final static int INVALID_MANEUVER_INDICATOR = 0; + public final static int INVALID_SHIP_TYPE = 0; + public final static int INVALID_DIMENSION = 0; + public final static int INVALID_ETA = 0; + public final static int INVALID_ETA_HOUR = 24; + public final static int INVALID_ETA_MIN = 60; + public final static int INVALID_ALTITUDE = 4095; + public final static int UNSPECIFIED_AID_TYPE = 0; + public final static double INVALID_COG = 360.0; + public final static double INVALID_SOG = 1023.0; + public final static double INVALID_LAT = 91.0; + public final static double INVALID_LON = 181.0; + public final static double INVALID_ROT = 128.0; + public final static double INVALID_DRAUGHT = 0.0; + + public enum AisObjType { + AIS_VESSEL, + AIS_VESSEL_SPORT, + AIS_VESSEL_FAST, + AIS_VESSEL_PASSENGER, + AIS_VESSEL_FREIGHT, + AIS_VESSEL_COMMERCIAL, + AIS_LANDSTATION, + AIS_AIRPLANE, + AIS_SART, + AIS_ATON, // aids to navigation + AIS_ATON_VIRTUAL, + AIS_INVALID + } + public static final Map COUNTRY_CODES = Map.ofEntries( + new AbstractMap.SimpleEntry(201, "Albania"), + new AbstractMap.SimpleEntry(202, "Andorra"), + new AbstractMap.SimpleEntry(203, "Austria"), + new AbstractMap.SimpleEntry(204, "Portugal"), + new AbstractMap.SimpleEntry(205, "Belgium"), + new AbstractMap.SimpleEntry(206, "Belarus"), + new AbstractMap.SimpleEntry(207, "Bulgaria"), + new AbstractMap.SimpleEntry(208, "Vatican"), + new AbstractMap.SimpleEntry(209, "Cyprus"), + new AbstractMap.SimpleEntry(210, "Cyprus"), + new AbstractMap.SimpleEntry(211, "Germany"), + new AbstractMap.SimpleEntry(212, "Cyprus"), + new AbstractMap.SimpleEntry(213, "Georgia"), + new AbstractMap.SimpleEntry(214, "Moldova"), + new AbstractMap.SimpleEntry(215, "Malta"), + new AbstractMap.SimpleEntry(216, "Armenia"), + new AbstractMap.SimpleEntry(218, "Germany"), + new AbstractMap.SimpleEntry(219, "Denmark"), + new AbstractMap.SimpleEntry(220, "Denmark"), + new AbstractMap.SimpleEntry(224, "Spain"), + new AbstractMap.SimpleEntry(225, "Spain"), + new AbstractMap.SimpleEntry(226, "France"), + new AbstractMap.SimpleEntry(227, "France"), + new AbstractMap.SimpleEntry(228, "France"), + new AbstractMap.SimpleEntry(229, "Malta"), + new AbstractMap.SimpleEntry(230, "Finland"), + new AbstractMap.SimpleEntry(231, "Faroe Is"), + new AbstractMap.SimpleEntry(232, "United Kingdom"), + new AbstractMap.SimpleEntry(233, "United Kingdom"), + new AbstractMap.SimpleEntry(234, "United Kingdom"), + new AbstractMap.SimpleEntry(235, "United Kingdom"), + new AbstractMap.SimpleEntry(236, "Gibraltar"), + new AbstractMap.SimpleEntry(237, "Greece"), + new AbstractMap.SimpleEntry(238, "Croatia"), + new AbstractMap.SimpleEntry(239, "Greece"), + new AbstractMap.SimpleEntry(240, "Greece"), + new AbstractMap.SimpleEntry(241, "Greece"), + new AbstractMap.SimpleEntry(242, "Morocco"), + new AbstractMap.SimpleEntry(243, "Hungary"), + new AbstractMap.SimpleEntry(244, "Netherlands"), + new AbstractMap.SimpleEntry(245, "Netherlands"), + new AbstractMap.SimpleEntry(246, "Netherlands"), + new AbstractMap.SimpleEntry(247, "Italy"), + new AbstractMap.SimpleEntry(248, "Malta"), + new AbstractMap.SimpleEntry(249, "Malta"), + new AbstractMap.SimpleEntry(250, "Ireland"), + new AbstractMap.SimpleEntry(251, "Iceland"), + new AbstractMap.SimpleEntry(252, "Liechtenstein"), + new AbstractMap.SimpleEntry(253, "Luxembourg"), + new AbstractMap.SimpleEntry(254, "Monaco"), + new AbstractMap.SimpleEntry(255, "Portugal"), + new AbstractMap.SimpleEntry(256, "Malta"), + new AbstractMap.SimpleEntry(257, "Norway"), + new AbstractMap.SimpleEntry(258, "Norway"), + new AbstractMap.SimpleEntry(259, "Norway"), + new AbstractMap.SimpleEntry(261, "Poland"), + new AbstractMap.SimpleEntry(262, "Montenegro"), + new AbstractMap.SimpleEntry(263, "Portugal"), + new AbstractMap.SimpleEntry(264, "Romania"), + new AbstractMap.SimpleEntry(265, "Sweden"), + new AbstractMap.SimpleEntry(266, "Sweden"), + new AbstractMap.SimpleEntry(267, "Slovakia"), + new AbstractMap.SimpleEntry(268, "San Marino"), + new AbstractMap.SimpleEntry(269, "Switzerland"), + new AbstractMap.SimpleEntry(270, "Czech Republic"), + new AbstractMap.SimpleEntry(271, "Turkey"), + new AbstractMap.SimpleEntry(272, "Ukraine"), + new AbstractMap.SimpleEntry(273, "Russia"), + new AbstractMap.SimpleEntry(274, "FYR Macedonia"), + new AbstractMap.SimpleEntry(275, "Latvia"), + new AbstractMap.SimpleEntry(276, "Estonia"), + new AbstractMap.SimpleEntry(277, "Lithuania"), + new AbstractMap.SimpleEntry(278, "Slovenia"), + new AbstractMap.SimpleEntry(279, "Serbia"), + new AbstractMap.SimpleEntry(301, "Anguilla"), + new AbstractMap.SimpleEntry(303, "USA"), + new AbstractMap.SimpleEntry(304, "Antigua Barbuda"), + new AbstractMap.SimpleEntry(305, "Antigua Barbuda"), + new AbstractMap.SimpleEntry(306, "Curacao"), + new AbstractMap.SimpleEntry(307, "Aruba"), + new AbstractMap.SimpleEntry(308, "Bahamas"), + new AbstractMap.SimpleEntry(309, "Bahamas"), + new AbstractMap.SimpleEntry(310, "Bermuda"), + new AbstractMap.SimpleEntry(311, "Bahamas"), + new AbstractMap.SimpleEntry(312, "Belize"), + new AbstractMap.SimpleEntry(314, "Barbados"), + new AbstractMap.SimpleEntry(316, "Canada"), + new AbstractMap.SimpleEntry(319, "Cayman Is"), + new AbstractMap.SimpleEntry(321, "Costa Rica"), + new AbstractMap.SimpleEntry(323, "Cuba"), + new AbstractMap.SimpleEntry(325, "Dominica"), + new AbstractMap.SimpleEntry(327, "Dominican Rep"), + new AbstractMap.SimpleEntry(329, "Guadeloupe"), + new AbstractMap.SimpleEntry(330, "Grenada"), + new AbstractMap.SimpleEntry(331, "Greenland"), + new AbstractMap.SimpleEntry(332, "Guatemala"), + new AbstractMap.SimpleEntry(334, "Honduras"), + new AbstractMap.SimpleEntry(336, "Haiti"), + new AbstractMap.SimpleEntry(338, "USA"), + new AbstractMap.SimpleEntry(339, "Jamaica"), + new AbstractMap.SimpleEntry(341, "St Kitts Nevis"), + new AbstractMap.SimpleEntry(343, "St Lucia"), + new AbstractMap.SimpleEntry(345, "Mexico"), + new AbstractMap.SimpleEntry(347, "Martinique"), + new AbstractMap.SimpleEntry(348, "Montserrat"), + new AbstractMap.SimpleEntry(350, "Nicaragua"), + new AbstractMap.SimpleEntry(351, "Panama"), + new AbstractMap.SimpleEntry(352, "Panama"), + new AbstractMap.SimpleEntry(353, "Panama"), + new AbstractMap.SimpleEntry(354, "Panama"), + new AbstractMap.SimpleEntry(355, "Panama"), + new AbstractMap.SimpleEntry(356, "Panama"), + new AbstractMap.SimpleEntry(357, "Panama"), + new AbstractMap.SimpleEntry(358, "Puerto Rico"), + new AbstractMap.SimpleEntry(359, "El Salvador"), + new AbstractMap.SimpleEntry(361, "St Pierre Miquelon"), + new AbstractMap.SimpleEntry(362, "Trinidad Tobago"), + new AbstractMap.SimpleEntry(364, "Turks Caicos Is"), + new AbstractMap.SimpleEntry(366, "USA"), + new AbstractMap.SimpleEntry(367, "USA"), + new AbstractMap.SimpleEntry(368, "USA"), + new AbstractMap.SimpleEntry(369, "USA"), + new AbstractMap.SimpleEntry(370, "Panama"), + new AbstractMap.SimpleEntry(371, "Panama"), + new AbstractMap.SimpleEntry(372, "Panama"), + new AbstractMap.SimpleEntry(373, "Panama"), + new AbstractMap.SimpleEntry(374, "Panama"), + new AbstractMap.SimpleEntry(375, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry(376, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry(377, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry(378, "British Virgin Is"), + new AbstractMap.SimpleEntry(379, "US Virgin Is"), + new AbstractMap.SimpleEntry(401, "Afghanistan"), + new AbstractMap.SimpleEntry(403, "Saudi Arabia"), + new AbstractMap.SimpleEntry(405, "Bangladesh"), + new AbstractMap.SimpleEntry(408, "Bahrain"), + new AbstractMap.SimpleEntry(410, "Bhutan"), + new AbstractMap.SimpleEntry(412, "China"), + new AbstractMap.SimpleEntry(413, "China"), + new AbstractMap.SimpleEntry(414, "China"), + new AbstractMap.SimpleEntry(416, "Taiwan"), + new AbstractMap.SimpleEntry(417, "Sri Lanka"), + new AbstractMap.SimpleEntry(419, "India"), + new AbstractMap.SimpleEntry(422, "Iran"), + new AbstractMap.SimpleEntry(423, "Azerbaijan"), + new AbstractMap.SimpleEntry(425, "Iraq"), + new AbstractMap.SimpleEntry(428, "Israel"), + new AbstractMap.SimpleEntry(431, "Japan"), + new AbstractMap.SimpleEntry(432, "Japan"), + new AbstractMap.SimpleEntry(434, "Turkmenistan"), + new AbstractMap.SimpleEntry(436, "Kazakhstan"), + new AbstractMap.SimpleEntry(437, "Uzbekistan"), + new AbstractMap.SimpleEntry(438, "Jordan"), + new AbstractMap.SimpleEntry(440, "Korea"), + new AbstractMap.SimpleEntry(441, "Korea"), + new AbstractMap.SimpleEntry(443, "Palestine"), + new AbstractMap.SimpleEntry(445, "DPR Korea"), + new AbstractMap.SimpleEntry(447, "Kuwait"), + new AbstractMap.SimpleEntry(450, "Lebanon"), + new AbstractMap.SimpleEntry(451, "Kyrgyz Republic"), + new AbstractMap.SimpleEntry(453, "Macao"), + new AbstractMap.SimpleEntry(455, "Maldives"), + new AbstractMap.SimpleEntry(457, "Mongolia"), + new AbstractMap.SimpleEntry(459, "Nepal"), + new AbstractMap.SimpleEntry(461, "Oman"), + new AbstractMap.SimpleEntry(463, "Pakistan"), + new AbstractMap.SimpleEntry(466, "Qatar"), + new AbstractMap.SimpleEntry(468, "Syria"), + new AbstractMap.SimpleEntry(470, "UAE"), + new AbstractMap.SimpleEntry(471, "UAE"), + new AbstractMap.SimpleEntry(472, "Tajikistan"), + new AbstractMap.SimpleEntry(473, "Yemen"), + new AbstractMap.SimpleEntry(475, "Yemen"), + new AbstractMap.SimpleEntry(477, "Hong Kong"), + new AbstractMap.SimpleEntry(478, "Bosnia and Herzegovina"), + new AbstractMap.SimpleEntry(501, "Antarctica"), + new AbstractMap.SimpleEntry(503, "Australia"), + new AbstractMap.SimpleEntry(506, "Myanmar"), + new AbstractMap.SimpleEntry(508, "Brunei"), + new AbstractMap.SimpleEntry(510, "Micronesia"), + new AbstractMap.SimpleEntry(511, "Palau"), + new AbstractMap.SimpleEntry(512, "New Zealand"), + new AbstractMap.SimpleEntry(514, "Cambodia"), + new AbstractMap.SimpleEntry(515, "Cambodia"), + new AbstractMap.SimpleEntry(516, "Christmas Is"), + new AbstractMap.SimpleEntry(518, "Cook Is"), + new AbstractMap.SimpleEntry(520, "Fiji"), + new AbstractMap.SimpleEntry(523, "Cocos Is"), + new AbstractMap.SimpleEntry(525, "Indonesia"), + new AbstractMap.SimpleEntry(529, "Kiribati"), + new AbstractMap.SimpleEntry(531, "Laos"), + new AbstractMap.SimpleEntry(533, "Malaysia"), + new AbstractMap.SimpleEntry(536, "N Mariana Is"), + new AbstractMap.SimpleEntry(538, "Marshall Is"), + new AbstractMap.SimpleEntry(540, "New Caledonia"), + new AbstractMap.SimpleEntry(542, "Niue"), + new AbstractMap.SimpleEntry(544, "Nauru"), + new AbstractMap.SimpleEntry(546, "French Polynesia"), + new AbstractMap.SimpleEntry(548, "Philippines"), + new AbstractMap.SimpleEntry(553, "Papua New Guinea"), + new AbstractMap.SimpleEntry(555, "Pitcairn Is"), + new AbstractMap.SimpleEntry(557, "Solomon Is"), + new AbstractMap.SimpleEntry(559, "American Samoa"), + new AbstractMap.SimpleEntry(561, "Samoa"), + new AbstractMap.SimpleEntry(563, "Singapore"), + new AbstractMap.SimpleEntry(564, "Singapore"), + new AbstractMap.SimpleEntry(565, "Singapore"), + new AbstractMap.SimpleEntry(566, "Singapore"), + new AbstractMap.SimpleEntry(567, "Thailand"), + new AbstractMap.SimpleEntry(570, "Tonga"), + new AbstractMap.SimpleEntry(572, "Tuvalu"), + new AbstractMap.SimpleEntry(574, "Vietnam"), + new AbstractMap.SimpleEntry(576, "Vanuatu"), + new AbstractMap.SimpleEntry(577, "Vanuatu"), + new AbstractMap.SimpleEntry(578, "Wallis Futuna Is"), + new AbstractMap.SimpleEntry(601, "South Africa"), + new AbstractMap.SimpleEntry(603, "Angola"), + new AbstractMap.SimpleEntry(605, "Algeria"), + new AbstractMap.SimpleEntry(607, "St Paul Amsterdam Is"), + new AbstractMap.SimpleEntry(608, "Ascension Is"), + new AbstractMap.SimpleEntry(609, "Burundi"), + new AbstractMap.SimpleEntry(610, "Benin"), + new AbstractMap.SimpleEntry(611, "Botswana"), + new AbstractMap.SimpleEntry(612, "Cen Afr Rep"), + new AbstractMap.SimpleEntry(613, "Cameroon"), + new AbstractMap.SimpleEntry(615, "Congo"), + new AbstractMap.SimpleEntry(616, "Comoros"), + new AbstractMap.SimpleEntry(617, "Cape Verde"), + new AbstractMap.SimpleEntry(618, "Antarctica"), + new AbstractMap.SimpleEntry(619, "Ivory Coast"), + new AbstractMap.SimpleEntry(620, "Comoros"), + new AbstractMap.SimpleEntry(621, "Djibouti"), + new AbstractMap.SimpleEntry(622, "Egypt"), + new AbstractMap.SimpleEntry(624, "Ethiopia"), + new AbstractMap.SimpleEntry(625, "Eritrea"), + new AbstractMap.SimpleEntry(626, "Gabon"), + new AbstractMap.SimpleEntry(627, "Ghana"), + new AbstractMap.SimpleEntry(629, "Gambia"), + new AbstractMap.SimpleEntry(630, "Guinea-Bissau"), + new AbstractMap.SimpleEntry(631, "Equ. Guinea"), + new AbstractMap.SimpleEntry(632, "Guinea"), + new AbstractMap.SimpleEntry(633, "Burkina Faso"), + new AbstractMap.SimpleEntry(634, "Kenya"), + new AbstractMap.SimpleEntry(635, "Antarctica"), + new AbstractMap.SimpleEntry(636, "Liberia"), + new AbstractMap.SimpleEntry(637, "Liberia"), + new AbstractMap.SimpleEntry(642, "Libya"), + new AbstractMap.SimpleEntry(644, "Lesotho"), + new AbstractMap.SimpleEntry(645, "Mauritius"), + new AbstractMap.SimpleEntry(647, "Madagascar"), + new AbstractMap.SimpleEntry(649, "Mali"), + new AbstractMap.SimpleEntry(650, "Mozambique"), + new AbstractMap.SimpleEntry(654, "Mauritania"), + new AbstractMap.SimpleEntry(655, "Malawi"), + new AbstractMap.SimpleEntry(656, "Niger"), + new AbstractMap.SimpleEntry(657, "Nigeria"), + new AbstractMap.SimpleEntry(659, "Namibia"), + new AbstractMap.SimpleEntry(660, "Reunion"), + new AbstractMap.SimpleEntry(661, "Rwanda"), + new AbstractMap.SimpleEntry(662, "Sudan"), + new AbstractMap.SimpleEntry(663, "Senegal"), + new AbstractMap.SimpleEntry(664, "Seychelles"), + new AbstractMap.SimpleEntry(665, "St Helena"), + new AbstractMap.SimpleEntry(666, "Somalia"), + new AbstractMap.SimpleEntry(667, "Sierra Leone"), + new AbstractMap.SimpleEntry(668, "Sao Tome Principe"), + new AbstractMap.SimpleEntry(669, "Swaziland"), + new AbstractMap.SimpleEntry(670, "Chad"), + new AbstractMap.SimpleEntry(671, "Togo"), + new AbstractMap.SimpleEntry(672, "Tunisia"), + new AbstractMap.SimpleEntry(674, "Tanzania"), + new AbstractMap.SimpleEntry(675, "Uganda"), + new AbstractMap.SimpleEntry(676, "DR Congo"), + new AbstractMap.SimpleEntry(677, "Tanzania"), + new AbstractMap.SimpleEntry(678, "Zambia"), + new AbstractMap.SimpleEntry(679, "Zimbabwe"), + new AbstractMap.SimpleEntry(701, "Argentina"), + new AbstractMap.SimpleEntry(710, "Brazil"), + new AbstractMap.SimpleEntry(720, "Bolivia"), + new AbstractMap.SimpleEntry(725, "Chile"), + new AbstractMap.SimpleEntry(730, "Colombia"), + new AbstractMap.SimpleEntry(735, "Ecuador"), + new AbstractMap.SimpleEntry(740, "UK"), + new AbstractMap.SimpleEntry(745, "Guiana"), + new AbstractMap.SimpleEntry(750, "Guyana"), + new AbstractMap.SimpleEntry(755, "Paraguay"), + new AbstractMap.SimpleEntry(760, "Peru"), + new AbstractMap.SimpleEntry(765, "Suriname"), + new AbstractMap.SimpleEntry(770, "Uruguay"), + new AbstractMap.SimpleEntry(775, "Venezuela") + ); +} + diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java new file mode 100644 index 00000000000..b50ae77535b --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -0,0 +1,163 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.LocationConvert; +import net.osmand.data.LatLon; +import net.osmand.data.PointDescription; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.mapcontextmenu.MenuBuilder; +import net.osmand.plus.mapcontextmenu.MenuController; + +import java.util.Iterator; +import java.util.SortedSet; + +public class AisObjectMenuController extends MenuController { + private AisObject aisObject; + public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointDescription pointDescription, + AisObject aisObject) { + super(new MenuBuilder(mapActivity), pointDescription, mapActivity); + this.aisObject = aisObject; + builder.setShowTitleIfTruncated(false); + builder.setShowNearestPoi(false); + builder.setShowOnlinePhotos(false); + builder.setShowNearestWiki(false); + // TODO: show an icon in the menu + } + + private void addMenuItem(@NonNull String type, @Nullable String value) { + if (value != null) { + if (!value.isEmpty()) { + addPlainMenuItem(0, value, type, false, false, null); + } + } + } + private void addMenuItem(@NonNull String type, @Nullable String value, + @Nullable SortedSet msgTypes, Integer selection[]) { + if (msgTypes != null) { + for (Integer i : selection) { + if (msgTypes.contains(i)) { + addMenuItem(type, value); + break; + } + } + } + } + private void addMenuItemDimension() { + if (((aisObject.getDimensionToBow() != AisObjectConstants.INVALID_DIMENSION) || + (aisObject.getDimensionToStern() != AisObjectConstants.INVALID_DIMENSION)) && + ((aisObject.getDimensionToPort() != AisObjectConstants.INVALID_DIMENSION) || + (aisObject.getDimensionToStarboard() != AisObjectConstants.INVALID_DIMENSION))) { + addMenuItem("Dimension", + Integer.toString(aisObject.getDimensionToBow() + aisObject.getDimensionToStern()) + + "m x " + + Integer.toString(aisObject.getDimensionToPort() + aisObject.getDimensionToStarboard()) + + "m"); + } + } + + @Override + public void addPlainMenuItems(String typeStr, PointDescription pointDescription, LatLon latLon) { + SortedSet msgTypes = aisObject.getMsgTypes(); + Iterator iter = msgTypes.iterator(); + String msgTypesString = ""; + LatLon position = aisObject.getPosition(); + long lastUpdate = (System.currentTimeMillis() - aisObject.getLastUpdate()) / 1000; + + addMenuItem("MMSI", Integer.toString(aisObject.getMmsi())); + if (position != null) { + addMenuItem("Location", + LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); + } + if (msgTypes.contains(21)) { // ATON (aid to navigation) + addMenuItem("ATON Type", aisObject.getAidTypeString()); + addMenuItemDimension(); + } else if (msgTypes.contains(9)) { // SAR aircraft + addMenuItem("Object Type", "SAR Aircraft"); + if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { + addMenuItem("COG", String.valueOf(aisObject.getCog())); + } + if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { + addMenuItem("SOG", String.valueOf(aisObject.getSog())); + } + if (aisObject.getAltitude() != AisObjectConstants.INVALID_ALTITUDE) { + addMenuItem("Altitude", String.valueOf(aisObject.getAltitude())); + } + } else { + addMenuItem("Callsign", aisObject.getCallSign()); + if (aisObject.getImo() != 0 ) { + addMenuItem("IMO", Integer.toString(aisObject.getImo()), msgTypes, new Integer[]{5}); + } + addMenuItem("Shipname", aisObject.getShipName()); + addMenuItem("Shiptype", aisObject.getShipTypeString(), msgTypes, new Integer[]{5, 19, 24}); + if (aisObject.getNavStatus() != AisObjectConstants.INVALID_NAV_STATUS) { + addMenuItem("Navigation Status", aisObject.getNavStatusString()); + } + if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { + addMenuItem("COG", String.valueOf(aisObject.getCog())); + } + if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { + addMenuItem("SOG", String.valueOf(aisObject.getSog()) + " kt"); + } + if (aisObject.getHeading() != AisObjectConstants.INVALID_HEADING) { + addMenuItem("Heading", String.valueOf(aisObject.getHeading())); + } + if (aisObject.getRot() != AisObjectConstants.INVALID_ROT) { + addMenuItem("Rate of Turn", String.valueOf(aisObject.getRot())); + } + addMenuItemDimension(); + if (aisObject.getDraught() != AisObjectConstants.INVALID_DRAUGHT) { + addMenuItem("Draught", String.valueOf(aisObject.getDraught()) + " m"); + } + addMenuItem("Destination", aisObject.getDestination()); + if ((aisObject.getEtaDay() != AisObjectConstants.INVALID_ETA) && + (aisObject.getEtaHour() != AisObjectConstants.INVALID_ETA_HOUR) && + (aisObject.getEtaMin() != AisObjectConstants.INVALID_ETA_MIN) && + (aisObject.getEtaMon() != AisObjectConstants.INVALID_ETA)) { + String eta = new String(aisObject.getEtaDay() + "." + + aisObject.getEtaMon() + ". " + aisObject.getEtaHour() + ":" + + aisObject.getEtaMin()); + addMenuItem("ETA", eta); + // TODO add prepending "0", if needed + } + } + if (lastUpdate > 60) { + addMenuItem("Last Update", (lastUpdate / 60) + + " min " + (lastUpdate % 60) + " sec"); + } else { + addMenuItem("Last Update", lastUpdate + " sec"); + } + boolean hasNext = iter.hasNext(); + while (hasNext) { + msgTypesString = msgTypesString.concat(Integer.toString(iter.next())); + hasNext = iter.hasNext(); + if (hasNext) { msgTypesString = msgTypesString.concat(", "); } + } + addMenuItem("Message Type(s)", msgTypesString); + } + + @Override + protected void setObject(Object object) { + if (object instanceof AisObject) { + this.aisObject = (AisObject) object; + } + } + + @Override + protected Object getObject() { + return aisObject; + } + @Override + public CharSequence getAdditionalInfoStr() { return "Country: " + aisObject.getCountryCode(); } + + @NonNull + @Override + public String getTypeStr() { return "AIS object"; } + + @Override + public boolean needStreetName() { return false; } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java new file mode 100644 index 00000000000..c1478121a32 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -0,0 +1,252 @@ +package net.osmand.plus.plugins.aistracker; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.util.Log; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.core.android.MapRendererView; +import net.osmand.core.jni.PointI; +import net.osmand.data.LatLon; +import net.osmand.data.PointDescription; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.utils.NativeUtilities; +import net.osmand.plus.views.layers.ContextMenuLayer; +import net.osmand.plus.views.layers.base.OsmandMapLayer; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +public class AisTrackerLayer extends OsmandMapLayer implements ContextMenuLayer.IContextMenuProvider { + private static final int START_ZOOM = 10; + private final AisTrackerPlugin plugin; + private Map aisObjectList; + private final int aisObjectListCounterMax = 100; + private final Context context; + private Paint bitmapPaint; + private Timer timer; + private TimerTask taskCheckAisObjectList; + private AisMessageListener listener; + public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugin) { + super(context); + this.plugin = plugin; + this.context = context; + this.listener = null; + + this.aisObjectList = new HashMap<>(); + this.bitmapPaint = new Paint(); + this.bitmapPaint.setAntiAlias(true); + this.bitmapPaint.setFilterBitmap(true); + this.bitmapPaint.setStrokeWidth(4); + this.bitmapPaint.setColor(Color.DKGRAY); + + initTimer(); + startNetworkListener(); + + //initTestObjects(); // for test purposes: + } + + private void initTestObjects() { + AisObject ais1 = new AisObject(12345, 1, 20, 120, 120.0, 4.4, + 37.42421d, -122.08381d, 30, 0,0,0,0); + AisObject ais2 = new AisObject(34567, 3, 20, 320, 320.0, 0.4, + 37.42521d, -122.08481d, 36, 0,0,0,0); + AisObject ais3 = new AisObject(34568, 1, 20, 320, 320.0, 0.4, + 50.738d, 7.099d, 70, 20,40,10,0); + AisObject ais4 = new AisObject(12341, 3, 20, 20, 20.0, 0.4, + 50.737d, 7.098d, 60, 0,0,0,0); + + updateAisObjectList(ais1); + updateAisObjectList(ais2); + removeOldestAisObjectListEntry(); + updateAisObjectList(ais2); + updateAisObjectList(ais3); + updateAisObjectList(ais4); + removeLostAisObjects(); + } + private void initTimer() { + this.taskCheckAisObjectList = new TimerTask() { + @Override + public void run() { + Log.d("AisTrackerLayer", "timer task taskCheckAisObjectList running"); + removeLostAisObjects(); + } + }; + this.timer = new Timer(); + timer.schedule(taskCheckAisObjectList, 20000, 30000); + } + private void startNetworkListener() { + int proto = plugin.AIS_NMEA_PROTOCOL.get(); + if (proto == AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP) { + this.listener = new AisMessageListener(plugin.AIS_NMEA_UDP_PORT.get(), this); + } else if (proto == AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP) { + this.listener = new AisMessageListener(plugin.AIS_NMEA_IP_ADDRESS.get(), plugin.AIS_NMEA_TCP_PORT.get(), this); + } + } + private void stopNetworkListener() { + if (this.listener != null) { + this.listener.stopListener(); + this.listener = null; + } + } + public void restartNetworkListener() { + stopNetworkListener(); + startNetworkListener(); + } + public void cleanup() { + if (this.timer != null) { + this.timer.cancel(); + this.timer.purge(); + this.timer = null; + } + if (this.aisObjectList != null) { + this.aisObjectList.clear(); + this.aisObjectList = null; + } + stopNetworkListener(); + } + private void removeLostAisObjects() { + for (Iterator> iterator = aisObjectList.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + if (entry.getValue().checkObjectAge()) { + Log.d("AisTrackerLayer", "remove AIS object with MMSI " + entry.getValue().getMmsi()); + iterator.remove(); + } + } + // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge()); + } + private void removeOldestAisObjectListEntry() { + Log.d("AisTrackerLayer", "removeOldestAisObjectListEntry() called"); + long oldestTimeStamp = System.currentTimeMillis(); + AisObject oldest = null; + for (AisObject ais : aisObjectList.values()) { + long timeStamp = ais.getLastUpdate(); + if (timeStamp <= oldestTimeStamp) { + oldestTimeStamp = timeStamp; + oldest = ais; + } + } + if (oldest != null) { + Log.d("AisTrackerLayer", "remove AIS object with MMSI " + oldest.getMmsi()); + aisObjectList.remove(oldest.getMmsi(), oldest); + } + } + + /* add new AIS object to list, or (if already exist) update its value */ + public void updateAisObjectList(@NonNull AisObject ais) { + int mmsi = ais.getMmsi(); + AisObject obj = aisObjectList.get(mmsi); + if (obj == null) { + Log.d("AisTrackerLayer", "add AIS object with MMSI " + ais.getMmsi()); + aisObjectList.put(mmsi, new AisObject(ais)); + if (aisObjectList.size() >= this.aisObjectListCounterMax) { + this.removeOldestAisObjectListEntry(); + } + } else { + obj.set(ais); + } + } + + @Nullable + public Bitmap getBitmap(@DrawableRes int drawable) { return getScaledBitmap(drawable); } + + @NonNull + public OsmandApplication getApplication() { + return (OsmandApplication) context.getApplicationContext(); + } + public boolean isLocationVisible(RotatedTileBox tileBox, LatLon coordinates) { + //noinspection SimplifiableIfStatement + if (tileBox == null || coordinates == null) { + return false; + } + return tileBox.containsLatLon(coordinates); + } + + @Override + public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + for (AisObject ais : aisObjectList.values()) { + if (isLocationVisible(tileBox, ais.getPosition())) { + ais.draw(this, bitmapPaint, canvas, tileBox); + } + } + } + + @Override + public boolean drawInScreenPixels() { + return true; + } + + @Override + public void collectObjectsFromPoint(PointF point, RotatedTileBox tileBox, List objects, + boolean unknownLocation, boolean excludeUntouchableObjects) { + if (tileBox.getZoom() >= START_ZOOM) { + getAisObjectsFromPoint(point, tileBox, objects); + } + } + public void getAisObjectsFromPoint(PointF point, RotatedTileBox tileBox, List aisList) { + if (aisObjectList.isEmpty()) { + return; + } + + MapRendererView mapRenderer = getMapRenderer(); + float radius = getScaledTouchRadius(getApplication(), tileBox.getDefaultRadiusPoi()) * TOUCH_RADIUS_MULTIPLIER; + List touchPolygon31 = null; + if (mapRenderer != null) { + touchPolygon31 = NativeUtilities.getPolygon31FromPixelAndRadius(mapRenderer, point, radius); + if (touchPolygon31 == null) { + return; + } + } + + for (AisObject ais : aisObjectList.values()) { + LatLon pos = ais.getPosition(); + if (pos != null) { + double lat = pos.getLatitude(); + double lon = pos.getLongitude(); + + boolean add = mapRenderer != null + ? NativeUtilities.isPointInsidePolygon(lat, lon, touchPolygon31) + : tileBox.isLatLonNearPixel(lat, lon, point.x, point.y, radius); + if (add) { + aisList.add(ais); + } + } + } + } + + @Override + public LatLon getObjectLocation(Object o) { + if (o instanceof AisObject) { + LatLon pos = ((AisObject) o).getPosition(); + if (pos != null) { + return new LatLon(pos.getLatitude(), pos.getLongitude()); + } + } + return null; + } + + @Override + public PointDescription getObjectName(Object o) { + if (o instanceof AisObject) { + AisObject ais = ((AisObject) o); + if (ais.getShipName() != null) { + return new PointDescription("AIS object", ais.getShipName()); + } + return new PointDescription("AIS object", + "AIS object with MMSI " + ais.getMmsi()); + } + return null; + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java new file mode 100644 index 00000000000..d693516614e --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -0,0 +1,152 @@ +package net.osmand.plus.plugins.aistracker; + +//import static net.osmand.aidlapi.OsmAndCustomizationConstants.PLUGIN_AISTRACKER; +import static net.osmand.plus.settings.fragments.SettingsScreenType.AIS_SETTINGS; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.plugins.OsmandPlugin; +import net.osmand.plus.render.RendererRegistry; +import net.osmand.plus.settings.backend.ApplicationMode; +import net.osmand.plus.settings.backend.preferences.CommonPreference; +import net.osmand.plus.settings.fragments.SettingsScreenType; +import net.osmand.plus.views.OsmandMapTileView; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/* +* This plugin receives AIS positions and other AIS data via network (NMEA protocol) +* from an AIS receiver/decoder and displays symbols at the map at the vessel position +*/ +public class AisTrackerPlugin extends OsmandPlugin { + + private AisTrackerLayer aisTrackerLayer = null; + + public static final String COMPONENT = "net.osmand.aistrackerPlugin"; + public final CommonPreference AIS_NMEA_PROTOCOL; + public static final int AIS_NMEA_PROTOCOL_UDP = 0; + public static final int AIS_NMEA_PROTOCOL_TCP = 1; + public final CommonPreference AIS_NMEA_IP_ADDRESS; + private static final String AIS_NMEA_DEFAULT_IP = "192.168.200.16"; + public final CommonPreference AIS_NMEA_TCP_PORT; + public static final Integer AIS_NMEA_DEFAULT_TCP_PORT = 4001; + public final CommonPreference AIS_NMEA_UDP_PORT; + public static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; + + public AisTrackerPlugin(OsmandApplication app) { + super(app); + /* "ais_nmea_protocol" etc. is a reference to the content of ais_settings.xml */ + AIS_NMEA_PROTOCOL = registerIntPreference("ais_nmea_protocol", AIS_NMEA_PROTOCOL_UDP); + AIS_NMEA_IP_ADDRESS = registerStringPreference("ais_address_nmea_server", AIS_NMEA_DEFAULT_IP); + AIS_NMEA_TCP_PORT = registerIntPreference("ais_port_nmea_server", AIS_NMEA_DEFAULT_TCP_PORT); + AIS_NMEA_UDP_PORT = registerIntPreference("ais_port_nmea_local", AIS_NMEA_DEFAULT_UDP_PORT); + + Log.d("AisTrackerPlugin", "constructor"); + } + + @Override + public boolean isMarketPlugin() { + return true; + } + + @Override + public String getComponentId1() { + return COMPONENT; + } + + @Override + public CharSequence getDescription(boolean linksEnabled) { + return app.getString(R.string.plugin_aistracker_description); + } + + @Override + public String getName() { + return app.getString(R.string.plugin_aistracker_name); + } + + @Override + //public int getLogoResourceId() { return R.drawable.ic_plugin_nautical_map; } + public int getLogoResourceId() { + return R.drawable.mm_sport_sailing; + } + + @Override + public Drawable getAssetResourceImage() { + return app.getUIUtilities().getIcon(R.drawable.ais_map); + } + + @Override + public List getAddedAppModes() { + //return Collections.singletonList(ApplicationMode.BOAT); + return Arrays.asList(ApplicationMode.BOAT, ApplicationMode.DEFAULT); + } + + @Override + public List getRendererNames() { + return Collections.singletonList(RendererRegistry.NAUTICAL_RENDER); + } + + @Override + public String getId() { + return "osmand.aistracker"; + } + + @Nullable + @Override + public SettingsScreenType getSettingsScreenType() { + return AIS_SETTINGS; + } + + @Override + public String getPrefsDescription() { + return app.getString(R.string.ais_address_settings_description); + } + + @Override + public void updateLayers(@NonNull Context context, @Nullable MapActivity mapActivity) { + OsmandMapTileView mapView = app.getOsmandMap().getMapView(); + if (isActive()) { + if (aisTrackerLayer == null) { + Log.d("AisTrackerPlugin", "call registerLayers()"); + registerLayers(context, mapActivity); + } + if (!mapView.getLayers().contains(aisTrackerLayer)) { + mapView.addLayer(aisTrackerLayer, 3.5f); + } + } else { + if (aisTrackerLayer != null) { + mapView.removeLayer(aisTrackerLayer); + aisTrackerLayer.cleanup(); + aisTrackerLayer = null; + mapView.refreshMap(); + } + } + } + + @Override + public void registerLayers(@NonNull Context context, @Nullable MapActivity mapActivity) { + if (aisTrackerLayer == null) { + Log.d("AisTrackerPlugin", "new AisTrackerLayer"); + aisTrackerLayer = new AisTrackerLayer(context, this); + app.getOsmandMap().getMapView().addLayer(aisTrackerLayer, 3.5f); + } else { + Log.d("AisTrackerPlugin", "AisTrackerLayer already exists"); + OsmandMapTileView mapView = app.getOsmandMap().getMapView(); + if (!mapView.getLayers().contains(aisTrackerLayer)) { + mapView.addLayer(aisTrackerLayer, 3.5f); + } + } + } + + public AisTrackerLayer getLayer() { return aisTrackerLayer; } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java new file mode 100644 index 00000000000..4c4ae571cb2 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -0,0 +1,152 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP; + +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; + +import net.osmand.plus.R; +import net.osmand.plus.plugins.PluginsHelper; +import net.osmand.plus.settings.fragments.BaseSettingsFragment; +import net.osmand.plus.settings.preferences.EditTextPreferenceEx; +import net.osmand.plus.settings.preferences.ListPreferenceEx; + +public class AisTrackerSettingsFragment extends BaseSettingsFragment { + private AisTrackerPlugin plugin; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + plugin = PluginsHelper.getPlugin(AisTrackerPlugin.class); + } + + @Override + protected void setupPreferences() { + int currentProtocol; + currentProtocol = setupProtocol(); + setupIpAddress(currentProtocol); + setupTcpPort(currentProtocol); + setupUdpPort(currentProtocol); + } + + private int setupProtocol() { + Integer[] entryValues = {AIS_NMEA_PROTOCOL_UDP, AIS_NMEA_PROTOCOL_TCP}; + String[] entries = {"UDP", "TCP"}; + + ListPreferenceEx aisNmeaProtocol = findPreference(plugin.AIS_NMEA_PROTOCOL.getId()); + aisNmeaProtocol.setEntries(entries); + aisNmeaProtocol.setEntryValues(entryValues); + aisNmeaProtocol.setDescription(R.string.ais_nmea_protocol_description); + return (int)aisNmeaProtocol.getValue(); + } + + private void setupIpAddress(int currentProtocol) { + /* + InputFilter[] filters = new InputFilter[1]; + filters[0] = new InputFilter() { + @Override + public CharSequence filter(CharSequence source, int start, int end, + android.text.Spanned dest, int dstart, int dend) { + if (end > start) { + String destTxt = dest.toString(); + String resultingTxt = destTxt.substring(0, dstart) + + source.subSequence(start, end) + + destTxt.substring(dend); + if (!resultingTxt + .matches("^\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3})?)?)?)?)?)?")) { + return ""; + } else { + String[] splits = resultingTxt.split("\\."); + for (int i = 0; i < splits.length; i++) { + if (Integer.valueOf(splits[i]) > 255) { + return ""; + } + } + } + } + return null; + } + }; + */ + //EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); + //Log.d("AisTrackerSettingsFragment","## findPreference()"); + //aisNmeaIpAddress.setOnBindEditTextListener(new androidx.preference.EditTextPreference.OnBindEditTextListener() { + /*aisNmeaIpAddress.setOnBindEditTextListener(new OnBindEditTextListener() { + @Override + public void onBindEditText(@NonNull EditText editText) { + editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); + editText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(10)}); + Log.d("AisTrackerSettingsFragment","## onBindEditText()"); + } + }); + */ + + EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); + if (aisNmeaIpAddress != null) { + aisNmeaIpAddress.setDescription(R.string.ais_address_nmea_server_description); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaIpAddress.setEnabled(false); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaIpAddress.setEnabled(true); + } + } + } + + private void setupTcpPort(int currentProtocol) { + /* EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); + if (aisNmeaPort != null) { + Log.d("AisTrackerSettingsFragment","## setupTcpPort()"); + aisNmeaPort.setOnBindEditTextListener(new OnBindEditTextListener() { + @Override + public void onBindEditText(@NonNull EditText editText) { + Log.d("AisTrackerSettingsFragment","## onBindEditText()"); + editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); + } + }); + aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaPort.setEnabled(false); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaPort.setEnabled(true); + } + } + */ + + EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); + if (aisNmeaPort != null) { + aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaPort.setEnabled(false); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaPort.setEnabled(true); + } + } + } + + private void setupUdpPort(int currentProtocol) { + EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_UDP_PORT.getId()); + if (aisNmeaPort != null) { + aisNmeaPort.setDescription(R.string.ais_port_nmea_local_description); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaPort.setEnabled(true); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaPort.setEnabled(false); + } + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean ret = super.onPreferenceChange(preference, newValue); + AisTrackerLayer layer = plugin.getLayer(); + if (layer != null) { + // layer.restartNetworkListeners(); // TEST + layer.restartNetworkListener(); + } + return ret; + } +} + diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java b/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java index 757ef1bebed..15ec7c498d0 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java @@ -3,6 +3,7 @@ import net.osmand.plus.R; import net.osmand.plus.keyevent.fragments.MainExternalInputDevicesFragment; import net.osmand.plus.plugins.accessibility.AccessibilitySettingsFragment; +import net.osmand.plus.plugins.aistracker.AisTrackerSettingsFragment; import net.osmand.plus.plugins.audionotes.MultimediaNotesFragment; import net.osmand.plus.plugins.development.DevelopmentSettingsFragment; import net.osmand.plus.plugins.externalsensors.ExternalSettingsWriteToTrackSettingsFragment; @@ -49,7 +50,8 @@ public enum SettingsScreenType { WEATHER_SETTINGS(WeatherSettingsFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.weather_settings, R.layout.profile_preference_toolbar), EXTERNAL_SETTINGS_WRITE_TO_TRACK_SETTINGS(ExternalSettingsWriteToTrackSettingsFragment.class.getName(), true, ApplyQueryType.BOTTOM_SHEET, R.xml.external_sensors_write_to_track_settings, R.layout.profile_preference_toolbar), DANGEROUS_GOODS(DangerousGoodsFragment.class.getName(), true, ApplyQueryType.NONE, R.xml.dangerous_goods_parameters, R.layout.global_preference_toolbar), - EXTERNAL_INPUT_DEVICE(MainExternalInputDevicesFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.external_input_device_settings, R.layout.profile_preference_toolbar_with_switch); + EXTERNAL_INPUT_DEVICE(MainExternalInputDevicesFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.external_input_device_settings, R.layout.profile_preference_toolbar_with_switch), + AIS_SETTINGS(AisTrackerSettingsFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.ais_settings, R.layout.profile_preference_toolbar); public final String fragmentName; public final boolean profileDependent; From 9bbd7fa62d10183e8fdea6989d7bdf5fcfff6911 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 16 Jun 2024 17:52:03 +0200 Subject: [PATCH 39/74] new plugin: AIS vessel tracker, initial version --- .../osmand/plus/plugins/PluginsHelper.java | 2 +- .../aistracker/AisObjectConstants.java | 580 +++++++++--------- .../AisTrackerSettingsFragment.java | 11 +- 3 files changed, 298 insertions(+), 295 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java index 6bbf9153e71..69a3cfeee29 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java @@ -564,7 +564,7 @@ public static List onIndexingFiles(@Nullable IProgress progress) { List l = new ArrayList<>(); for (OsmandPlugin plugin : getEnabledPlugins()) { List ls = plugin.indexingFiles(progress); - if (ls != null && ls.size() > 0) { + if (ls != null && !ls.isEmpty()) { l.addAll(ls); } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index 9b3dc923953..d7a84cb3e09 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -40,296 +40,296 @@ public enum AisObjType { AIS_INVALID } public static final Map COUNTRY_CODES = Map.ofEntries( - new AbstractMap.SimpleEntry(201, "Albania"), - new AbstractMap.SimpleEntry(202, "Andorra"), - new AbstractMap.SimpleEntry(203, "Austria"), - new AbstractMap.SimpleEntry(204, "Portugal"), - new AbstractMap.SimpleEntry(205, "Belgium"), - new AbstractMap.SimpleEntry(206, "Belarus"), - new AbstractMap.SimpleEntry(207, "Bulgaria"), - new AbstractMap.SimpleEntry(208, "Vatican"), - new AbstractMap.SimpleEntry(209, "Cyprus"), - new AbstractMap.SimpleEntry(210, "Cyprus"), - new AbstractMap.SimpleEntry(211, "Germany"), - new AbstractMap.SimpleEntry(212, "Cyprus"), - new AbstractMap.SimpleEntry(213, "Georgia"), - new AbstractMap.SimpleEntry(214, "Moldova"), - new AbstractMap.SimpleEntry(215, "Malta"), - new AbstractMap.SimpleEntry(216, "Armenia"), - new AbstractMap.SimpleEntry(218, "Germany"), - new AbstractMap.SimpleEntry(219, "Denmark"), - new AbstractMap.SimpleEntry(220, "Denmark"), - new AbstractMap.SimpleEntry(224, "Spain"), - new AbstractMap.SimpleEntry(225, "Spain"), - new AbstractMap.SimpleEntry(226, "France"), - new AbstractMap.SimpleEntry(227, "France"), - new AbstractMap.SimpleEntry(228, "France"), - new AbstractMap.SimpleEntry(229, "Malta"), - new AbstractMap.SimpleEntry(230, "Finland"), - new AbstractMap.SimpleEntry(231, "Faroe Is"), - new AbstractMap.SimpleEntry(232, "United Kingdom"), - new AbstractMap.SimpleEntry(233, "United Kingdom"), - new AbstractMap.SimpleEntry(234, "United Kingdom"), - new AbstractMap.SimpleEntry(235, "United Kingdom"), - new AbstractMap.SimpleEntry(236, "Gibraltar"), - new AbstractMap.SimpleEntry(237, "Greece"), - new AbstractMap.SimpleEntry(238, "Croatia"), - new AbstractMap.SimpleEntry(239, "Greece"), - new AbstractMap.SimpleEntry(240, "Greece"), - new AbstractMap.SimpleEntry(241, "Greece"), - new AbstractMap.SimpleEntry(242, "Morocco"), - new AbstractMap.SimpleEntry(243, "Hungary"), - new AbstractMap.SimpleEntry(244, "Netherlands"), - new AbstractMap.SimpleEntry(245, "Netherlands"), - new AbstractMap.SimpleEntry(246, "Netherlands"), - new AbstractMap.SimpleEntry(247, "Italy"), - new AbstractMap.SimpleEntry(248, "Malta"), - new AbstractMap.SimpleEntry(249, "Malta"), - new AbstractMap.SimpleEntry(250, "Ireland"), - new AbstractMap.SimpleEntry(251, "Iceland"), - new AbstractMap.SimpleEntry(252, "Liechtenstein"), - new AbstractMap.SimpleEntry(253, "Luxembourg"), - new AbstractMap.SimpleEntry(254, "Monaco"), - new AbstractMap.SimpleEntry(255, "Portugal"), - new AbstractMap.SimpleEntry(256, "Malta"), - new AbstractMap.SimpleEntry(257, "Norway"), - new AbstractMap.SimpleEntry(258, "Norway"), - new AbstractMap.SimpleEntry(259, "Norway"), - new AbstractMap.SimpleEntry(261, "Poland"), - new AbstractMap.SimpleEntry(262, "Montenegro"), - new AbstractMap.SimpleEntry(263, "Portugal"), - new AbstractMap.SimpleEntry(264, "Romania"), - new AbstractMap.SimpleEntry(265, "Sweden"), - new AbstractMap.SimpleEntry(266, "Sweden"), - new AbstractMap.SimpleEntry(267, "Slovakia"), - new AbstractMap.SimpleEntry(268, "San Marino"), - new AbstractMap.SimpleEntry(269, "Switzerland"), - new AbstractMap.SimpleEntry(270, "Czech Republic"), - new AbstractMap.SimpleEntry(271, "Turkey"), - new AbstractMap.SimpleEntry(272, "Ukraine"), - new AbstractMap.SimpleEntry(273, "Russia"), - new AbstractMap.SimpleEntry(274, "FYR Macedonia"), - new AbstractMap.SimpleEntry(275, "Latvia"), - new AbstractMap.SimpleEntry(276, "Estonia"), - new AbstractMap.SimpleEntry(277, "Lithuania"), - new AbstractMap.SimpleEntry(278, "Slovenia"), - new AbstractMap.SimpleEntry(279, "Serbia"), - new AbstractMap.SimpleEntry(301, "Anguilla"), - new AbstractMap.SimpleEntry(303, "USA"), - new AbstractMap.SimpleEntry(304, "Antigua Barbuda"), - new AbstractMap.SimpleEntry(305, "Antigua Barbuda"), - new AbstractMap.SimpleEntry(306, "Curacao"), - new AbstractMap.SimpleEntry(307, "Aruba"), - new AbstractMap.SimpleEntry(308, "Bahamas"), - new AbstractMap.SimpleEntry(309, "Bahamas"), - new AbstractMap.SimpleEntry(310, "Bermuda"), - new AbstractMap.SimpleEntry(311, "Bahamas"), - new AbstractMap.SimpleEntry(312, "Belize"), - new AbstractMap.SimpleEntry(314, "Barbados"), - new AbstractMap.SimpleEntry(316, "Canada"), - new AbstractMap.SimpleEntry(319, "Cayman Is"), - new AbstractMap.SimpleEntry(321, "Costa Rica"), - new AbstractMap.SimpleEntry(323, "Cuba"), - new AbstractMap.SimpleEntry(325, "Dominica"), - new AbstractMap.SimpleEntry(327, "Dominican Rep"), - new AbstractMap.SimpleEntry(329, "Guadeloupe"), - new AbstractMap.SimpleEntry(330, "Grenada"), - new AbstractMap.SimpleEntry(331, "Greenland"), - new AbstractMap.SimpleEntry(332, "Guatemala"), - new AbstractMap.SimpleEntry(334, "Honduras"), - new AbstractMap.SimpleEntry(336, "Haiti"), - new AbstractMap.SimpleEntry(338, "USA"), - new AbstractMap.SimpleEntry(339, "Jamaica"), - new AbstractMap.SimpleEntry(341, "St Kitts Nevis"), - new AbstractMap.SimpleEntry(343, "St Lucia"), - new AbstractMap.SimpleEntry(345, "Mexico"), - new AbstractMap.SimpleEntry(347, "Martinique"), - new AbstractMap.SimpleEntry(348, "Montserrat"), - new AbstractMap.SimpleEntry(350, "Nicaragua"), - new AbstractMap.SimpleEntry(351, "Panama"), - new AbstractMap.SimpleEntry(352, "Panama"), - new AbstractMap.SimpleEntry(353, "Panama"), - new AbstractMap.SimpleEntry(354, "Panama"), - new AbstractMap.SimpleEntry(355, "Panama"), - new AbstractMap.SimpleEntry(356, "Panama"), - new AbstractMap.SimpleEntry(357, "Panama"), - new AbstractMap.SimpleEntry(358, "Puerto Rico"), - new AbstractMap.SimpleEntry(359, "El Salvador"), - new AbstractMap.SimpleEntry(361, "St Pierre Miquelon"), - new AbstractMap.SimpleEntry(362, "Trinidad Tobago"), - new AbstractMap.SimpleEntry(364, "Turks Caicos Is"), - new AbstractMap.SimpleEntry(366, "USA"), - new AbstractMap.SimpleEntry(367, "USA"), - new AbstractMap.SimpleEntry(368, "USA"), - new AbstractMap.SimpleEntry(369, "USA"), - new AbstractMap.SimpleEntry(370, "Panama"), - new AbstractMap.SimpleEntry(371, "Panama"), - new AbstractMap.SimpleEntry(372, "Panama"), - new AbstractMap.SimpleEntry(373, "Panama"), - new AbstractMap.SimpleEntry(374, "Panama"), - new AbstractMap.SimpleEntry(375, "St Vincent Grenadines"), - new AbstractMap.SimpleEntry(376, "St Vincent Grenadines"), - new AbstractMap.SimpleEntry(377, "St Vincent Grenadines"), - new AbstractMap.SimpleEntry(378, "British Virgin Is"), - new AbstractMap.SimpleEntry(379, "US Virgin Is"), - new AbstractMap.SimpleEntry(401, "Afghanistan"), - new AbstractMap.SimpleEntry(403, "Saudi Arabia"), - new AbstractMap.SimpleEntry(405, "Bangladesh"), - new AbstractMap.SimpleEntry(408, "Bahrain"), - new AbstractMap.SimpleEntry(410, "Bhutan"), - new AbstractMap.SimpleEntry(412, "China"), - new AbstractMap.SimpleEntry(413, "China"), - new AbstractMap.SimpleEntry(414, "China"), - new AbstractMap.SimpleEntry(416, "Taiwan"), - new AbstractMap.SimpleEntry(417, "Sri Lanka"), - new AbstractMap.SimpleEntry(419, "India"), - new AbstractMap.SimpleEntry(422, "Iran"), - new AbstractMap.SimpleEntry(423, "Azerbaijan"), - new AbstractMap.SimpleEntry(425, "Iraq"), - new AbstractMap.SimpleEntry(428, "Israel"), - new AbstractMap.SimpleEntry(431, "Japan"), - new AbstractMap.SimpleEntry(432, "Japan"), - new AbstractMap.SimpleEntry(434, "Turkmenistan"), - new AbstractMap.SimpleEntry(436, "Kazakhstan"), - new AbstractMap.SimpleEntry(437, "Uzbekistan"), - new AbstractMap.SimpleEntry(438, "Jordan"), - new AbstractMap.SimpleEntry(440, "Korea"), - new AbstractMap.SimpleEntry(441, "Korea"), - new AbstractMap.SimpleEntry(443, "Palestine"), - new AbstractMap.SimpleEntry(445, "DPR Korea"), - new AbstractMap.SimpleEntry(447, "Kuwait"), - new AbstractMap.SimpleEntry(450, "Lebanon"), - new AbstractMap.SimpleEntry(451, "Kyrgyz Republic"), - new AbstractMap.SimpleEntry(453, "Macao"), - new AbstractMap.SimpleEntry(455, "Maldives"), - new AbstractMap.SimpleEntry(457, "Mongolia"), - new AbstractMap.SimpleEntry(459, "Nepal"), - new AbstractMap.SimpleEntry(461, "Oman"), - new AbstractMap.SimpleEntry(463, "Pakistan"), - new AbstractMap.SimpleEntry(466, "Qatar"), - new AbstractMap.SimpleEntry(468, "Syria"), - new AbstractMap.SimpleEntry(470, "UAE"), - new AbstractMap.SimpleEntry(471, "UAE"), - new AbstractMap.SimpleEntry(472, "Tajikistan"), - new AbstractMap.SimpleEntry(473, "Yemen"), - new AbstractMap.SimpleEntry(475, "Yemen"), - new AbstractMap.SimpleEntry(477, "Hong Kong"), - new AbstractMap.SimpleEntry(478, "Bosnia and Herzegovina"), - new AbstractMap.SimpleEntry(501, "Antarctica"), - new AbstractMap.SimpleEntry(503, "Australia"), - new AbstractMap.SimpleEntry(506, "Myanmar"), - new AbstractMap.SimpleEntry(508, "Brunei"), - new AbstractMap.SimpleEntry(510, "Micronesia"), - new AbstractMap.SimpleEntry(511, "Palau"), - new AbstractMap.SimpleEntry(512, "New Zealand"), - new AbstractMap.SimpleEntry(514, "Cambodia"), - new AbstractMap.SimpleEntry(515, "Cambodia"), - new AbstractMap.SimpleEntry(516, "Christmas Is"), - new AbstractMap.SimpleEntry(518, "Cook Is"), - new AbstractMap.SimpleEntry(520, "Fiji"), - new AbstractMap.SimpleEntry(523, "Cocos Is"), - new AbstractMap.SimpleEntry(525, "Indonesia"), - new AbstractMap.SimpleEntry(529, "Kiribati"), - new AbstractMap.SimpleEntry(531, "Laos"), - new AbstractMap.SimpleEntry(533, "Malaysia"), - new AbstractMap.SimpleEntry(536, "N Mariana Is"), - new AbstractMap.SimpleEntry(538, "Marshall Is"), - new AbstractMap.SimpleEntry(540, "New Caledonia"), - new AbstractMap.SimpleEntry(542, "Niue"), - new AbstractMap.SimpleEntry(544, "Nauru"), - new AbstractMap.SimpleEntry(546, "French Polynesia"), - new AbstractMap.SimpleEntry(548, "Philippines"), - new AbstractMap.SimpleEntry(553, "Papua New Guinea"), - new AbstractMap.SimpleEntry(555, "Pitcairn Is"), - new AbstractMap.SimpleEntry(557, "Solomon Is"), - new AbstractMap.SimpleEntry(559, "American Samoa"), - new AbstractMap.SimpleEntry(561, "Samoa"), - new AbstractMap.SimpleEntry(563, "Singapore"), - new AbstractMap.SimpleEntry(564, "Singapore"), - new AbstractMap.SimpleEntry(565, "Singapore"), - new AbstractMap.SimpleEntry(566, "Singapore"), - new AbstractMap.SimpleEntry(567, "Thailand"), - new AbstractMap.SimpleEntry(570, "Tonga"), - new AbstractMap.SimpleEntry(572, "Tuvalu"), - new AbstractMap.SimpleEntry(574, "Vietnam"), - new AbstractMap.SimpleEntry(576, "Vanuatu"), - new AbstractMap.SimpleEntry(577, "Vanuatu"), - new AbstractMap.SimpleEntry(578, "Wallis Futuna Is"), - new AbstractMap.SimpleEntry(601, "South Africa"), - new AbstractMap.SimpleEntry(603, "Angola"), - new AbstractMap.SimpleEntry(605, "Algeria"), - new AbstractMap.SimpleEntry(607, "St Paul Amsterdam Is"), - new AbstractMap.SimpleEntry(608, "Ascension Is"), - new AbstractMap.SimpleEntry(609, "Burundi"), - new AbstractMap.SimpleEntry(610, "Benin"), - new AbstractMap.SimpleEntry(611, "Botswana"), - new AbstractMap.SimpleEntry(612, "Cen Afr Rep"), - new AbstractMap.SimpleEntry(613, "Cameroon"), - new AbstractMap.SimpleEntry(615, "Congo"), - new AbstractMap.SimpleEntry(616, "Comoros"), - new AbstractMap.SimpleEntry(617, "Cape Verde"), - new AbstractMap.SimpleEntry(618, "Antarctica"), - new AbstractMap.SimpleEntry(619, "Ivory Coast"), - new AbstractMap.SimpleEntry(620, "Comoros"), - new AbstractMap.SimpleEntry(621, "Djibouti"), - new AbstractMap.SimpleEntry(622, "Egypt"), - new AbstractMap.SimpleEntry(624, "Ethiopia"), - new AbstractMap.SimpleEntry(625, "Eritrea"), - new AbstractMap.SimpleEntry(626, "Gabon"), - new AbstractMap.SimpleEntry(627, "Ghana"), - new AbstractMap.SimpleEntry(629, "Gambia"), - new AbstractMap.SimpleEntry(630, "Guinea-Bissau"), - new AbstractMap.SimpleEntry(631, "Equ. Guinea"), - new AbstractMap.SimpleEntry(632, "Guinea"), - new AbstractMap.SimpleEntry(633, "Burkina Faso"), - new AbstractMap.SimpleEntry(634, "Kenya"), - new AbstractMap.SimpleEntry(635, "Antarctica"), - new AbstractMap.SimpleEntry(636, "Liberia"), - new AbstractMap.SimpleEntry(637, "Liberia"), - new AbstractMap.SimpleEntry(642, "Libya"), - new AbstractMap.SimpleEntry(644, "Lesotho"), - new AbstractMap.SimpleEntry(645, "Mauritius"), - new AbstractMap.SimpleEntry(647, "Madagascar"), - new AbstractMap.SimpleEntry(649, "Mali"), - new AbstractMap.SimpleEntry(650, "Mozambique"), - new AbstractMap.SimpleEntry(654, "Mauritania"), - new AbstractMap.SimpleEntry(655, "Malawi"), - new AbstractMap.SimpleEntry(656, "Niger"), - new AbstractMap.SimpleEntry(657, "Nigeria"), - new AbstractMap.SimpleEntry(659, "Namibia"), - new AbstractMap.SimpleEntry(660, "Reunion"), - new AbstractMap.SimpleEntry(661, "Rwanda"), - new AbstractMap.SimpleEntry(662, "Sudan"), - new AbstractMap.SimpleEntry(663, "Senegal"), - new AbstractMap.SimpleEntry(664, "Seychelles"), - new AbstractMap.SimpleEntry(665, "St Helena"), - new AbstractMap.SimpleEntry(666, "Somalia"), - new AbstractMap.SimpleEntry(667, "Sierra Leone"), - new AbstractMap.SimpleEntry(668, "Sao Tome Principe"), - new AbstractMap.SimpleEntry(669, "Swaziland"), - new AbstractMap.SimpleEntry(670, "Chad"), - new AbstractMap.SimpleEntry(671, "Togo"), - new AbstractMap.SimpleEntry(672, "Tunisia"), - new AbstractMap.SimpleEntry(674, "Tanzania"), - new AbstractMap.SimpleEntry(675, "Uganda"), - new AbstractMap.SimpleEntry(676, "DR Congo"), - new AbstractMap.SimpleEntry(677, "Tanzania"), - new AbstractMap.SimpleEntry(678, "Zambia"), - new AbstractMap.SimpleEntry(679, "Zimbabwe"), - new AbstractMap.SimpleEntry(701, "Argentina"), - new AbstractMap.SimpleEntry(710, "Brazil"), - new AbstractMap.SimpleEntry(720, "Bolivia"), - new AbstractMap.SimpleEntry(725, "Chile"), - new AbstractMap.SimpleEntry(730, "Colombia"), - new AbstractMap.SimpleEntry(735, "Ecuador"), - new AbstractMap.SimpleEntry(740, "UK"), - new AbstractMap.SimpleEntry(745, "Guiana"), - new AbstractMap.SimpleEntry(750, "Guyana"), - new AbstractMap.SimpleEntry(755, "Paraguay"), - new AbstractMap.SimpleEntry(760, "Peru"), - new AbstractMap.SimpleEntry(765, "Suriname"), - new AbstractMap.SimpleEntry(770, "Uruguay"), - new AbstractMap.SimpleEntry(775, "Venezuela") + new AbstractMap.SimpleEntry<>(201, "Albania"), + new AbstractMap.SimpleEntry<>(202, "Andorra"), + new AbstractMap.SimpleEntry<>(203, "Austria"), + new AbstractMap.SimpleEntry<>(204, "Portugal"), + new AbstractMap.SimpleEntry<>(205, "Belgium"), + new AbstractMap.SimpleEntry<>(206, "Belarus"), + new AbstractMap.SimpleEntry<>(207, "Bulgaria"), + new AbstractMap.SimpleEntry<>(208, "Vatican"), + new AbstractMap.SimpleEntry<>(209, "Cyprus"), + new AbstractMap.SimpleEntry<>(210, "Cyprus"), + new AbstractMap.SimpleEntry<>(211, "Germany"), + new AbstractMap.SimpleEntry<>(212, "Cyprus"), + new AbstractMap.SimpleEntry<>(213, "Georgia"), + new AbstractMap.SimpleEntry<>(214, "Moldova"), + new AbstractMap.SimpleEntry<>(215, "Malta"), + new AbstractMap.SimpleEntry<>(216, "Armenia"), + new AbstractMap.SimpleEntry<>(218, "Germany"), + new AbstractMap.SimpleEntry<>(219, "Denmark"), + new AbstractMap.SimpleEntry<>(220, "Denmark"), + new AbstractMap.SimpleEntry<>(224, "Spain"), + new AbstractMap.SimpleEntry<>(225, "Spain"), + new AbstractMap.SimpleEntry<>(226, "France"), + new AbstractMap.SimpleEntry<>(227, "France"), + new AbstractMap.SimpleEntry<>(228, "France"), + new AbstractMap.SimpleEntry<>(229, "Malta"), + new AbstractMap.SimpleEntry<>(230, "Finland"), + new AbstractMap.SimpleEntry<>(231, "Faroe Is"), + new AbstractMap.SimpleEntry<>(232, "United Kingdom"), + new AbstractMap.SimpleEntry<>(233, "United Kingdom"), + new AbstractMap.SimpleEntry<>(234, "United Kingdom"), + new AbstractMap.SimpleEntry<>(235, "United Kingdom"), + new AbstractMap.SimpleEntry<>(236, "Gibraltar"), + new AbstractMap.SimpleEntry<>(237, "Greece"), + new AbstractMap.SimpleEntry<>(238, "Croatia"), + new AbstractMap.SimpleEntry<>(239, "Greece"), + new AbstractMap.SimpleEntry<>(240, "Greece"), + new AbstractMap.SimpleEntry<>(241, "Greece"), + new AbstractMap.SimpleEntry<>(242, "Morocco"), + new AbstractMap.SimpleEntry<>(243, "Hungary"), + new AbstractMap.SimpleEntry<>(244, "Netherlands"), + new AbstractMap.SimpleEntry<>(245, "Netherlands"), + new AbstractMap.SimpleEntry<>(246, "Netherlands"), + new AbstractMap.SimpleEntry<>(247, "Italy"), + new AbstractMap.SimpleEntry<>(248, "Malta"), + new AbstractMap.SimpleEntry<>(249, "Malta"), + new AbstractMap.SimpleEntry<>(250, "Ireland"), + new AbstractMap.SimpleEntry<>(251, "Iceland"), + new AbstractMap.SimpleEntry<>(252, "Liechtenstein"), + new AbstractMap.SimpleEntry<>(253, "Luxembourg"), + new AbstractMap.SimpleEntry<>(254, "Monaco"), + new AbstractMap.SimpleEntry<>(255, "Portugal"), + new AbstractMap.SimpleEntry<>(256, "Malta"), + new AbstractMap.SimpleEntry<>(257, "Norway"), + new AbstractMap.SimpleEntry<>(258, "Norway"), + new AbstractMap.SimpleEntry<>(259, "Norway"), + new AbstractMap.SimpleEntry<>(261, "Poland"), + new AbstractMap.SimpleEntry<>(262, "Montenegro"), + new AbstractMap.SimpleEntry<>(263, "Portugal"), + new AbstractMap.SimpleEntry<>(264, "Romania"), + new AbstractMap.SimpleEntry<>(265, "Sweden"), + new AbstractMap.SimpleEntry<>(266, "Sweden"), + new AbstractMap.SimpleEntry<>(267, "Slovakia"), + new AbstractMap.SimpleEntry<>(268, "San Marino"), + new AbstractMap.SimpleEntry<>(269, "Switzerland"), + new AbstractMap.SimpleEntry<>(270, "Czech Republic"), + new AbstractMap.SimpleEntry<>(271, "Turkey"), + new AbstractMap.SimpleEntry<>(272, "Ukraine"), + new AbstractMap.SimpleEntry<>(273, "Russia"), + new AbstractMap.SimpleEntry<>(274, "FYR Macedonia"), + new AbstractMap.SimpleEntry<>(275, "Latvia"), + new AbstractMap.SimpleEntry<>(276, "Estonia"), + new AbstractMap.SimpleEntry<>(277, "Lithuania"), + new AbstractMap.SimpleEntry<>(278, "Slovenia"), + new AbstractMap.SimpleEntry<>(279, "Serbia"), + new AbstractMap.SimpleEntry<>(301, "Anguilla"), + new AbstractMap.SimpleEntry<>(303, "USA"), + new AbstractMap.SimpleEntry<>(304, "Antigua Barbuda"), + new AbstractMap.SimpleEntry<>(305, "Antigua Barbuda"), + new AbstractMap.SimpleEntry<>(306, "Curacao"), + new AbstractMap.SimpleEntry<>(307, "Aruba"), + new AbstractMap.SimpleEntry<>(308, "Bahamas"), + new AbstractMap.SimpleEntry<>(309, "Bahamas"), + new AbstractMap.SimpleEntry<>(310, "Bermuda"), + new AbstractMap.SimpleEntry<>(311, "Bahamas"), + new AbstractMap.SimpleEntry<>(312, "Belize"), + new AbstractMap.SimpleEntry<>(314, "Barbados"), + new AbstractMap.SimpleEntry<>(316, "Canada"), + new AbstractMap.SimpleEntry<>(319, "Cayman Is"), + new AbstractMap.SimpleEntry<>(321, "Costa Rica"), + new AbstractMap.SimpleEntry<>(323, "Cuba"), + new AbstractMap.SimpleEntry<>(325, "Dominica"), + new AbstractMap.SimpleEntry<>(327, "Dominican Rep"), + new AbstractMap.SimpleEntry<>(329, "Guadeloupe"), + new AbstractMap.SimpleEntry<>(330, "Grenada"), + new AbstractMap.SimpleEntry<>(331, "Greenland"), + new AbstractMap.SimpleEntry<>(332, "Guatemala"), + new AbstractMap.SimpleEntry<>(334, "Honduras"), + new AbstractMap.SimpleEntry<>(336, "Haiti"), + new AbstractMap.SimpleEntry<>(338, "USA"), + new AbstractMap.SimpleEntry<>(339, "Jamaica"), + new AbstractMap.SimpleEntry<>(341, "St Kitts Nevis"), + new AbstractMap.SimpleEntry<>(343, "St Lucia"), + new AbstractMap.SimpleEntry<>(345, "Mexico"), + new AbstractMap.SimpleEntry<>(347, "Martinique"), + new AbstractMap.SimpleEntry<>(348, "Montserrat"), + new AbstractMap.SimpleEntry<>(350, "Nicaragua"), + new AbstractMap.SimpleEntry<>(351, "Panama"), + new AbstractMap.SimpleEntry<>(352, "Panama"), + new AbstractMap.SimpleEntry<>(353, "Panama"), + new AbstractMap.SimpleEntry<>(354, "Panama"), + new AbstractMap.SimpleEntry<>(355, "Panama"), + new AbstractMap.SimpleEntry<>(356, "Panama"), + new AbstractMap.SimpleEntry<>(357, "Panama"), + new AbstractMap.SimpleEntry<>(358, "Puerto Rico"), + new AbstractMap.SimpleEntry<>(359, "El Salvador"), + new AbstractMap.SimpleEntry<>(361, "St Pierre Miquelon"), + new AbstractMap.SimpleEntry<>(362, "Trinidad Tobago"), + new AbstractMap.SimpleEntry<>(364, "Turks Caicos Is"), + new AbstractMap.SimpleEntry<>(366, "USA"), + new AbstractMap.SimpleEntry<>(367, "USA"), + new AbstractMap.SimpleEntry<>(368, "USA"), + new AbstractMap.SimpleEntry<>(369, "USA"), + new AbstractMap.SimpleEntry<>(370, "Panama"), + new AbstractMap.SimpleEntry<>(371, "Panama"), + new AbstractMap.SimpleEntry<>(372, "Panama"), + new AbstractMap.SimpleEntry<>(373, "Panama"), + new AbstractMap.SimpleEntry<>(374, "Panama"), + new AbstractMap.SimpleEntry<>(375, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry<>(376, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry<>(377, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry<>(378, "British Virgin Is"), + new AbstractMap.SimpleEntry<>(379, "US Virgin Is"), + new AbstractMap.SimpleEntry<>(401, "Afghanistan"), + new AbstractMap.SimpleEntry<>(403, "Saudi Arabia"), + new AbstractMap.SimpleEntry<>(405, "Bangladesh"), + new AbstractMap.SimpleEntry<>(408, "Bahrain"), + new AbstractMap.SimpleEntry<>(410, "Bhutan"), + new AbstractMap.SimpleEntry<>(412, "China"), + new AbstractMap.SimpleEntry<>(413, "China"), + new AbstractMap.SimpleEntry<>(414, "China"), + new AbstractMap.SimpleEntry<>(416, "Taiwan"), + new AbstractMap.SimpleEntry<>(417, "Sri Lanka"), + new AbstractMap.SimpleEntry<>(419, "India"), + new AbstractMap.SimpleEntry<>(422, "Iran"), + new AbstractMap.SimpleEntry<>(423, "Azerbaijan"), + new AbstractMap.SimpleEntry<>(425, "Iraq"), + new AbstractMap.SimpleEntry<>(428, "Israel"), + new AbstractMap.SimpleEntry<>(431, "Japan"), + new AbstractMap.SimpleEntry<>(432, "Japan"), + new AbstractMap.SimpleEntry<>(434, "Turkmenistan"), + new AbstractMap.SimpleEntry<>(436, "Kazakhstan"), + new AbstractMap.SimpleEntry<>(437, "Uzbekistan"), + new AbstractMap.SimpleEntry<>(438, "Jordan"), + new AbstractMap.SimpleEntry<>(440, "Korea"), + new AbstractMap.SimpleEntry<>(441, "Korea"), + new AbstractMap.SimpleEntry<>(443, "Palestine"), + new AbstractMap.SimpleEntry<>(445, "DPR Korea"), + new AbstractMap.SimpleEntry<>(447, "Kuwait"), + new AbstractMap.SimpleEntry<>(450, "Lebanon"), + new AbstractMap.SimpleEntry<>(451, "Kyrgyz Republic"), + new AbstractMap.SimpleEntry<>(453, "Macao"), + new AbstractMap.SimpleEntry<>(455, "Maldives"), + new AbstractMap.SimpleEntry<>(457, "Mongolia"), + new AbstractMap.SimpleEntry<>(459, "Nepal"), + new AbstractMap.SimpleEntry<>(461, "Oman"), + new AbstractMap.SimpleEntry<>(463, "Pakistan"), + new AbstractMap.SimpleEntry<>(466, "Qatar"), + new AbstractMap.SimpleEntry<>(468, "Syria"), + new AbstractMap.SimpleEntry<>(470, "UAE"), + new AbstractMap.SimpleEntry<>(471, "UAE"), + new AbstractMap.SimpleEntry<>(472, "Tajikistan"), + new AbstractMap.SimpleEntry<>(473, "Yemen"), + new AbstractMap.SimpleEntry<>(475, "Yemen"), + new AbstractMap.SimpleEntry<>(477, "Hong Kong"), + new AbstractMap.SimpleEntry<>(478, "Bosnia and Herzegovina"), + new AbstractMap.SimpleEntry<>(501, "Antarctica"), + new AbstractMap.SimpleEntry<>(503, "Australia"), + new AbstractMap.SimpleEntry<>(506, "Myanmar"), + new AbstractMap.SimpleEntry<>(508, "Brunei"), + new AbstractMap.SimpleEntry<>(510, "Micronesia"), + new AbstractMap.SimpleEntry<>(511, "Palau"), + new AbstractMap.SimpleEntry<>(512, "New Zealand"), + new AbstractMap.SimpleEntry<>(514, "Cambodia"), + new AbstractMap.SimpleEntry<>(515, "Cambodia"), + new AbstractMap.SimpleEntry<>(516, "Christmas Is"), + new AbstractMap.SimpleEntry<>(518, "Cook Is"), + new AbstractMap.SimpleEntry<>(520, "Fiji"), + new AbstractMap.SimpleEntry<>(523, "Cocos Is"), + new AbstractMap.SimpleEntry<>(525, "Indonesia"), + new AbstractMap.SimpleEntry<>(529, "Kiribati"), + new AbstractMap.SimpleEntry<>(531, "Laos"), + new AbstractMap.SimpleEntry<>(533, "Malaysia"), + new AbstractMap.SimpleEntry<>(536, "N Mariana Is"), + new AbstractMap.SimpleEntry<>(538, "Marshall Is"), + new AbstractMap.SimpleEntry<>(540, "New Caledonia"), + new AbstractMap.SimpleEntry<>(542, "Niue"), + new AbstractMap.SimpleEntry<>(544, "Nauru"), + new AbstractMap.SimpleEntry<>(546, "French Polynesia"), + new AbstractMap.SimpleEntry<>(548, "Philippines"), + new AbstractMap.SimpleEntry<>(553, "Papua New Guinea"), + new AbstractMap.SimpleEntry<>(555, "Pitcairn Is"), + new AbstractMap.SimpleEntry<>(557, "Solomon Is"), + new AbstractMap.SimpleEntry<>(559, "American Samoa"), + new AbstractMap.SimpleEntry<>(561, "Samoa"), + new AbstractMap.SimpleEntry<>(563, "Singapore"), + new AbstractMap.SimpleEntry<>(564, "Singapore"), + new AbstractMap.SimpleEntry<>(565, "Singapore"), + new AbstractMap.SimpleEntry<>(566, "Singapore"), + new AbstractMap.SimpleEntry<>(567, "Thailand"), + new AbstractMap.SimpleEntry<>(570, "Tonga"), + new AbstractMap.SimpleEntry<>(572, "Tuvalu"), + new AbstractMap.SimpleEntry<>(574, "Vietnam"), + new AbstractMap.SimpleEntry<>(576, "Vanuatu"), + new AbstractMap.SimpleEntry<>(577, "Vanuatu"), + new AbstractMap.SimpleEntry<>(578, "Wallis Futuna Is"), + new AbstractMap.SimpleEntry<>(601, "South Africa"), + new AbstractMap.SimpleEntry<>(603, "Angola"), + new AbstractMap.SimpleEntry<>(605, "Algeria"), + new AbstractMap.SimpleEntry<>(607, "St Paul Amsterdam Is"), + new AbstractMap.SimpleEntry<>(608, "Ascension Is"), + new AbstractMap.SimpleEntry<>(609, "Burundi"), + new AbstractMap.SimpleEntry<>(610, "Benin"), + new AbstractMap.SimpleEntry<>(611, "Botswana"), + new AbstractMap.SimpleEntry<>(612, "Cen Afr Rep"), + new AbstractMap.SimpleEntry<>(613, "Cameroon"), + new AbstractMap.SimpleEntry<>(615, "Congo"), + new AbstractMap.SimpleEntry<>(616, "Comoros"), + new AbstractMap.SimpleEntry<>(617, "Cape Verde"), + new AbstractMap.SimpleEntry<>(618, "Antarctica"), + new AbstractMap.SimpleEntry<>(619, "Ivory Coast"), + new AbstractMap.SimpleEntry<>(620, "Comoros"), + new AbstractMap.SimpleEntry<>(621, "Djibouti"), + new AbstractMap.SimpleEntry<>(622, "Egypt"), + new AbstractMap.SimpleEntry<>(624, "Ethiopia"), + new AbstractMap.SimpleEntry<>(625, "Eritrea"), + new AbstractMap.SimpleEntry<>(626, "Gabon"), + new AbstractMap.SimpleEntry<>(627, "Ghana"), + new AbstractMap.SimpleEntry<>(629, "Gambia"), + new AbstractMap.SimpleEntry<>(630, "Guinea-Bissau"), + new AbstractMap.SimpleEntry<>(631, "Equ. Guinea"), + new AbstractMap.SimpleEntry<>(632, "Guinea"), + new AbstractMap.SimpleEntry<>(633, "Burkina Faso"), + new AbstractMap.SimpleEntry<>(634, "Kenya"), + new AbstractMap.SimpleEntry<>(635, "Antarctica"), + new AbstractMap.SimpleEntry<>(636, "Liberia"), + new AbstractMap.SimpleEntry<>(637, "Liberia"), + new AbstractMap.SimpleEntry<>(642, "Libya"), + new AbstractMap.SimpleEntry<>(644, "Lesotho"), + new AbstractMap.SimpleEntry<>(645, "Mauritius"), + new AbstractMap.SimpleEntry<>(647, "Madagascar"), + new AbstractMap.SimpleEntry<>(649, "Mali"), + new AbstractMap.SimpleEntry<>(650, "Mozambique"), + new AbstractMap.SimpleEntry<>(654, "Mauritania"), + new AbstractMap.SimpleEntry<>(655, "Malawi"), + new AbstractMap.SimpleEntry<>(656, "Niger"), + new AbstractMap.SimpleEntry<>(657, "Nigeria"), + new AbstractMap.SimpleEntry<>(659, "Namibia"), + new AbstractMap.SimpleEntry<>(660, "Reunion"), + new AbstractMap.SimpleEntry<>(661, "Rwanda"), + new AbstractMap.SimpleEntry<>(662, "Sudan"), + new AbstractMap.SimpleEntry<>(663, "Senegal"), + new AbstractMap.SimpleEntry<>(664, "Seychelles"), + new AbstractMap.SimpleEntry<>(665, "St Helena"), + new AbstractMap.SimpleEntry<>(666, "Somalia"), + new AbstractMap.SimpleEntry<>(667, "Sierra Leone"), + new AbstractMap.SimpleEntry<>(668, "Sao Tome Principe"), + new AbstractMap.SimpleEntry<>(669, "Swaziland"), + new AbstractMap.SimpleEntry<>(670, "Chad"), + new AbstractMap.SimpleEntry<>(671, "Togo"), + new AbstractMap.SimpleEntry<>(672, "Tunisia"), + new AbstractMap.SimpleEntry<>(674, "Tanzania"), + new AbstractMap.SimpleEntry<>(675, "Uganda"), + new AbstractMap.SimpleEntry<>(676, "DR Congo"), + new AbstractMap.SimpleEntry<>(677, "Tanzania"), + new AbstractMap.SimpleEntry<>(678, "Zambia"), + new AbstractMap.SimpleEntry<>(679, "Zimbabwe"), + new AbstractMap.SimpleEntry<>(701, "Argentina"), + new AbstractMap.SimpleEntry<>(710, "Brazil"), + new AbstractMap.SimpleEntry<>(720, "Bolivia"), + new AbstractMap.SimpleEntry<>(725, "Chile"), + new AbstractMap.SimpleEntry<>(730, "Colombia"), + new AbstractMap.SimpleEntry<>(735, "Ecuador"), + new AbstractMap.SimpleEntry<>(740, "UK"), + new AbstractMap.SimpleEntry<>(745, "Guiana"), + new AbstractMap.SimpleEntry<>(750, "Guyana"), + new AbstractMap.SimpleEntry<>(755, "Paraguay"), + new AbstractMap.SimpleEntry<>(760, "Peru"), + new AbstractMap.SimpleEntry<>(765, "Suriname"), + new AbstractMap.SimpleEntry<>(770, "Uruguay"), + new AbstractMap.SimpleEntry<>(775, "Venezuela") ); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 4c4ae571cb2..459bbacb57a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -37,10 +37,13 @@ private int setupProtocol() { String[] entries = {"UDP", "TCP"}; ListPreferenceEx aisNmeaProtocol = findPreference(plugin.AIS_NMEA_PROTOCOL.getId()); - aisNmeaProtocol.setEntries(entries); - aisNmeaProtocol.setEntryValues(entryValues); - aisNmeaProtocol.setDescription(R.string.ais_nmea_protocol_description); - return (int)aisNmeaProtocol.getValue(); + if (aisNmeaProtocol != null) { + aisNmeaProtocol.setEntries(entries); + aisNmeaProtocol.setEntryValues(entryValues); + aisNmeaProtocol.setDescription(R.string.ais_nmea_protocol_description); + return (int)aisNmeaProtocol.getValue(); + } + return 0; } private void setupIpAddress(int currentProtocol) { From 75f91e1f00ae8ca63d0f8b68f910c3d1818c0411 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 16 Jun 2024 19:31:36 +0200 Subject: [PATCH 40/74] insert some AIS objects for test purposes --- .../aistracker/AisObjectMenuController.java | 12 ++--- .../plugins/aistracker/AisTrackerLayer.java | 45 ++++++++++++------- .../plugins/aistracker/AisTrackerPlugin.java | 3 +- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index b50ae77535b..f647af88122 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -2,6 +2,8 @@ import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; +import android.annotation.SuppressLint; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -82,10 +84,10 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("COG", String.valueOf(aisObject.getCog())); } if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { - addMenuItem("SOG", String.valueOf(aisObject.getSog())); + addMenuItem("SOG", String.valueOf(aisObject.getSog()) + " kt"); } if (aisObject.getAltitude() != AisObjectConstants.INVALID_ALTITUDE) { - addMenuItem("Altitude", String.valueOf(aisObject.getAltitude())); + addMenuItem("Altitude", String.valueOf(aisObject.getAltitude()) + " m"); } } else { addMenuItem("Callsign", aisObject.getCallSign()); @@ -118,9 +120,9 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, (aisObject.getEtaHour() != AisObjectConstants.INVALID_ETA_HOUR) && (aisObject.getEtaMin() != AisObjectConstants.INVALID_ETA_MIN) && (aisObject.getEtaMon() != AisObjectConstants.INVALID_ETA)) { - String eta = new String(aisObject.getEtaDay() + "." + - aisObject.getEtaMon() + ". " + aisObject.getEtaHour() + ":" + - aisObject.getEtaMin()); + @SuppressLint("DefaultLocale") String eta = new String(aisObject.getEtaDay() + "." + + aisObject.getEtaMon() + ". " + String.format("%02d", aisObject.getEtaHour()) + ":" + + String.format("%02d", aisObject.getEtaMin())); addMenuItem("ETA", eta); // TODO add prepending "0", if needed } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index c1478121a32..653bf147c05 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -59,22 +59,35 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi } private void initTestObjects() { - AisObject ais1 = new AisObject(12345, 1, 20, 120, 120.0, 4.4, - 37.42421d, -122.08381d, 30, 0,0,0,0); - AisObject ais2 = new AisObject(34567, 3, 20, 320, 320.0, 0.4, - 37.42521d, -122.08481d, 36, 0,0,0,0); - AisObject ais3 = new AisObject(34568, 1, 20, 320, 320.0, 0.4, - 50.738d, 7.099d, 70, 20,40,10,0); - AisObject ais4 = new AisObject(12341, 3, 20, 20, 20.0, 0.4, - 50.737d, 7.098d, 60, 0,0,0,0); - - updateAisObjectList(ais1); - updateAisObjectList(ais2); - removeOldestAisObjectListEntry(); - updateAisObjectList(ais2); - updateAisObjectList(ais3); - updateAisObjectList(ais4); - removeLostAisObjects(); + // passenger ship + AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, + 320.0, 8.4, 50.738d, 7.099d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(34568, 5, 0, "TEST-CALLSIGN1", "TEST-Ship", 60 /* passenger */, 56, + 65, 8, 12, 2, + "Potsdam", 8, 15, 22, 5); + updateAisObjectList(ais); + // sailing boat + ais = new AisObject(454011, 1, 20, 8, 0, 120, + 125.0, 4.4, 50.737d, 7.098d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, + 0, 0, 0, 0, + "", 0, 0, 0, 0); + updateAisObjectList(ais); + // land station + ais = new AisObject(878121, 4, 50.736d, 7.100d); + updateAisObjectList(ais); + // AIDS + ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, + 0, 0, 0, 0); + updateAisObjectList(ais); + // aircraft + ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); + updateAisObjectList(ais); + + //removeOldestAisObjectListEntry(); + //removeLostAisObjects(); } private void initTimer() { this.taskCheckAisObjectList = new TimerTask() { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index d693516614e..f380bc17359 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -33,6 +33,7 @@ public class AisTrackerPlugin extends OsmandPlugin { private AisTrackerLayer aisTrackerLayer = null; public static final String COMPONENT = "net.osmand.aistrackerPlugin"; + public static final String AISTRACKER_ID = "osmand.aistracker"; public final CommonPreference AIS_NMEA_PROTOCOL; public static final int AIS_NMEA_PROTOCOL_UDP = 0; public static final int AIS_NMEA_PROTOCOL_TCP = 1; @@ -98,7 +99,7 @@ public List getRendererNames() { @Override public String getId() { - return "osmand.aistracker"; + return AISTRACKER_ID; } @Nullable From 84aa2eb421d8a272d842534dc68648c72f581892 Mon Sep 17 00:00:00 2001 From: Falk Date: Mon, 17 Jun 2024 23:37:48 +0200 Subject: [PATCH 41/74] added syntax check for IP address and port number in settings dialog --- .../aistracker/AisMessageListener.java | 23 ++++---- .../aistracker/AisObjectMenuController.java | 1 - .../plugins/aistracker/AisTrackerLayer.java | 2 +- .../plugins/aistracker/AisTrackerPlugin.java | 14 +++-- .../AisTrackerSettingsFragment.java | 56 ++++++++++++++++++- 5 files changed, 76 insertions(+), 20 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java index c9a33adf3ee..121475de45b 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -112,15 +112,17 @@ private void initListeners() throws IOException { } } private void removeListeners() { - sentenceReader.stop(); - while (!this.listenerList.isEmpty()) { - SentenceListener listener; - try { - listener = this.listenerList.pop(); - sentenceReader.removeSentenceListener(listener); - Log.d("AisMessageListener", "SentenceListener removed"); - } catch (EmptyStackException e) { - Log.e("AisMessageListener", "stack empty"); + if (sentenceReader != null) { + sentenceReader.stop(); + while (!this.listenerList.isEmpty()) { + SentenceListener listener; + try { + listener = this.listenerList.pop(); + sentenceReader.removeSentenceListener(listener); + Log.d("AisMessageListener", "SentenceListener removed"); + } catch (EmptyStackException e) { + Log.e("AisMessageListener", "stack empty"); + } } } } @@ -132,7 +134,7 @@ public void stopListener() { } removeListeners(); if (tcpSocket != null) { - Log.d("AisMessageListener","stopListener"); + Log.d("AisMessageListener","stopListener (TCP)"); try { if (tcpSocket.isConnected()) { tcpSocket.close(); @@ -143,6 +145,7 @@ public void stopListener() { } catch (Exception ignore) { } } if (udpSocket != null) { + Log.d("AisMessageListener","stopListener (UDP)"); if (udpSocket.isConnected()) { udpSocket.disconnect(); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index f647af88122..01f7340b14c 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -124,7 +124,6 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, aisObject.getEtaMon() + ". " + String.format("%02d", aisObject.getEtaHour()) + ":" + String.format("%02d", aisObject.getEtaMin())); addMenuItem("ETA", eta); - // TODO add prepending "0", if needed } } if (lastUpdate > 60) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 653bf147c05..fa633c9cd2b 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -55,7 +55,7 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi initTimer(); startNetworkListener(); - //initTestObjects(); // for test purposes: + initTestObjects(); // for test purposes: } private void initTestObjects() { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index f380bc17359..a9b50171de4 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -1,6 +1,5 @@ package net.osmand.plus.plugins.aistracker; -//import static net.osmand.aidlapi.OsmAndCustomizationConstants.PLUGIN_AISTRACKER; import static net.osmand.plus.settings.fragments.SettingsScreenType.AIS_SETTINGS; import android.content.Context; @@ -34,6 +33,11 @@ public class AisTrackerPlugin extends OsmandPlugin { public static final String COMPONENT = "net.osmand.aistrackerPlugin"; public static final String AISTRACKER_ID = "osmand.aistracker"; + + public static final String AIS_NMEA_PROTOCOL_ID = "ais_nmea_protocol"; + public static final String AIS_NMEA_IP_ADDRESS_ID = "ais_address_nmea_server"; + public static final String AIS_NMEA_TCP_PORT_ID = "ais_port_nmea_server"; + public static final String AIS_NMEA_UDP_PORT_ID = "ais_port_nmea_local"; public final CommonPreference AIS_NMEA_PROTOCOL; public static final int AIS_NMEA_PROTOCOL_UDP = 0; public static final int AIS_NMEA_PROTOCOL_TCP = 1; @@ -47,10 +51,10 @@ public class AisTrackerPlugin extends OsmandPlugin { public AisTrackerPlugin(OsmandApplication app) { super(app); /* "ais_nmea_protocol" etc. is a reference to the content of ais_settings.xml */ - AIS_NMEA_PROTOCOL = registerIntPreference("ais_nmea_protocol", AIS_NMEA_PROTOCOL_UDP); - AIS_NMEA_IP_ADDRESS = registerStringPreference("ais_address_nmea_server", AIS_NMEA_DEFAULT_IP); - AIS_NMEA_TCP_PORT = registerIntPreference("ais_port_nmea_server", AIS_NMEA_DEFAULT_TCP_PORT); - AIS_NMEA_UDP_PORT = registerIntPreference("ais_port_nmea_local", AIS_NMEA_DEFAULT_UDP_PORT); + AIS_NMEA_PROTOCOL = registerIntPreference(AIS_NMEA_PROTOCOL_ID, AIS_NMEA_PROTOCOL_UDP); + AIS_NMEA_IP_ADDRESS = registerStringPreference(AIS_NMEA_IP_ADDRESS_ID, AIS_NMEA_DEFAULT_IP); + AIS_NMEA_TCP_PORT = registerIntPreference(AIS_NMEA_TCP_PORT_ID, AIS_NMEA_DEFAULT_TCP_PORT); + AIS_NMEA_UDP_PORT = registerIntPreference(AIS_NMEA_UDP_PORT_ID, AIS_NMEA_DEFAULT_UDP_PORT); Log.d("AisTrackerPlugin", "constructor"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 459bbacb57a..824345ee5ff 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -3,9 +3,13 @@ import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP; +import android.content.Context; import android.os.Bundle; +import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import net.osmand.plus.R; @@ -13,6 +17,11 @@ import net.osmand.plus.settings.fragments.BaseSettingsFragment; import net.osmand.plus.settings.preferences.EditTextPreferenceEx; import net.osmand.plus.settings.preferences.ListPreferenceEx; +import net.osmand.plus.utils.UiUtilities; + +import java.text.MessageFormat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class AisTrackerSettingsFragment extends BaseSettingsFragment { private AisTrackerPlugin plugin; @@ -140,16 +149,57 @@ private void setupUdpPort(int currentProtocol) { } } } - @Override public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_IP_ADDRESS_ID)) { + if (!isValidIpV4Address(newValue.toString())) { + showAlertDialog("Only IPv4 address accepted (\"a.b.c.d\", where a,b,c,d in range 0..255)."); + return false; + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_TCP_PORT_ID) || + preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_UDP_PORT_ID)) { + if (!isValidPortNumber(newValue.toString())) { + showAlertDialog("Only numerical values accepted in range 0..65535."); + return false; + } + } boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); if (layer != null) { - // layer.restartNetworkListeners(); // TEST layer.restartNetworkListener(); } return ret; } + private static boolean isValidIpV4Address(@Nullable String value) { + String pattern0to255 = "(\\d{1,2}|(0|1)\\d{2}|2[0-4]\\d|25[0-5])"; + String patternIpV4 = pattern0to255 + "\\." +pattern0to255 + "\\." + + pattern0to255 + "\\." + pattern0to255; + Pattern p = Pattern.compile(patternIpV4); + if (value == null) { + return false; + } + Matcher m = p.matcher(value); + return m.matches(); + } + private static boolean isValidPortNumber(@Nullable String value) { + int i; + if (value == null) { + return false; + } + try { + i = Integer.parseInt(value); + } catch (NumberFormatException e) { + return false; + } + return (i >= 0) && (i <= 65535); + } + private void showAlertDialog(@NonNull String message) { + Context themedContext = UiUtilities.getThemedContext(getActivity(), isNightMode()); + AlertDialog.Builder wrongFormatDialog = new AlertDialog.Builder(themedContext); + wrongFormatDialog.setTitle(MessageFormat.format(getString(R.string.error_message_pattern), + "Unsupported Data Format")); + wrongFormatDialog.setMessage(message); + wrongFormatDialog.setPositiveButton(R.string.shared_string_ok, (dialog, which) -> dismiss()); + wrongFormatDialog.show(); + } } - From cfa015f9173964fd653cbfc2b8b1298ac87cd756 Mon Sep 17 00:00:00 2001 From: Falk Date: Wed, 19 Jun 2024 00:18:19 +0200 Subject: [PATCH 42/74] display distance and bearing in context menu --- .../plus/plugins/aistracker/AisObject.java | 50 +++++++++++++++++++ .../aistracker/AisObjectMenuController.java | 18 +++++++ 2 files changed, 68 insertions(+) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 7143414ecec..06506f99d6e 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -38,13 +38,17 @@ import android.graphics.Color; import android.graphics.LightingColorFilter; import android.graphics.Paint; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import net.osmand.Location; import net.osmand.data.LatLon; import net.osmand.data.RotatedTileBox; +import net.osmand.plus.OsmAndLocationProvider; import net.osmand.plus.R; +import net.osmand.util.MapUtils; import java.util.SortedSet; import java.util.TreeSet; @@ -549,6 +553,14 @@ public LatLon getPosition() { return this.ais_position; } @Nullable + public Location getLocation() { + if (this.ais_position != null) { + return new Location(AisTrackerPlugin.AISTRACKER_ID, + ais_position.getLatitude(), ais_position.getLongitude()); + } + return null; + } + @Nullable public String getCallSign() { return this.ais_callSign; } @@ -799,4 +811,42 @@ public String getAidTypeString() { return(Integer.toString(ais_aidType)); } } + private float getDistanceOrBearing(@Nullable OsmAndLocationProvider locationProvider, + boolean needBearing) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + Location aisLocation = getLocation(); + if ((myLocation != null) && (aisLocation != null)) { + return needBearing ? myLocation.bearingTo(aisLocation) : myLocation.distanceTo(aisLocation); + } else { + Log.e("AisObject", "getDistanceOrBearing(): mylocation -> " + myLocation + + ", aisLocation -> " + aisLocation); + return -500.0f; // invalid + } + } else { + Log.e("AisObject", "getDistanceOrBearing(): locationProvider -> null"); + return -500.0f; // invalid + } + } + /* get bearing from own position to the position of the AIS object */ + public float getBearing(@Nullable OsmAndLocationProvider locationProvider) { + float bearing = getDistanceOrBearing(locationProvider, true); + if ((bearing < 0.0f) && (bearing > -200.0f)) { + while (bearing < 0.0f) { + bearing += 360.0f; + } + } + return bearing; + } + /* get distance from own position to the position of the AIS object in meters */ + public float getDistanceInMeters(@Nullable OsmAndLocationProvider locationProvider) { + return getDistanceOrBearing(locationProvider, false); + } + public float getDistanceInNauticalMiles(@Nullable OsmAndLocationProvider locationProvider) { + float dist = getDistanceInMeters(locationProvider); + if (dist >= 0.0f) { + dist = dist / 1852; + } + return dist; + } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 01f7340b14c..6985be5a691 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -10,6 +10,7 @@ import net.osmand.LocationConvert; import net.osmand.data.LatLon; import net.osmand.data.PointDescription; +import net.osmand.plus.OsmandApplication; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.mapcontextmenu.MenuBuilder; import net.osmand.plus.mapcontextmenu.MenuController; @@ -19,10 +20,12 @@ public class AisObjectMenuController extends MenuController { private AisObject aisObject; + private final OsmandApplication app; public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointDescription pointDescription, AisObject aisObject) { super(new MenuBuilder(mapActivity), pointDescription, mapActivity); this.aisObject = aisObject; + this.app = builder.getApplication(); builder.setShowTitleIfTruncated(false); builder.setShowNearestPoi(false); builder.setShowOnlinePhotos(false); @@ -61,6 +64,7 @@ private void addMenuItemDimension() { } } + @SuppressLint("DefaultLocale") @Override public void addPlainMenuItems(String typeStr, PointDescription pointDescription, LatLon latLon) { SortedSet msgTypes = aisObject.getMsgTypes(); @@ -74,6 +78,20 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Location", LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); + if (this.app != null) { + float distance = aisObject.getDistanceInNauticalMiles(app.getLocationProvider()); + float bearing = aisObject.getBearing(app.getLocationProvider()); + if (distance >= 0.0f) { + try { + addMenuItem("Distance", String.format("%.1f nm", distance)); + } catch (Exception ignore) { } + } + if (bearing >= 0.0f) { + try { + addMenuItem("Bearing", String.format("%.1f", bearing)); + } catch (Exception ignore) { } + } + } } if (msgTypes.contains(21)) { // ATON (aid to navigation) addMenuItem("ATON Type", aisObject.getAidTypeString()); From a439e0b7fe2e9a41f01c077a9e3c155b90e10725 Mon Sep 17 00:00:00 2001 From: Falk Date: Fri, 21 Jun 2024 22:54:39 +0200 Subject: [PATCH 43/74] add 2 settings: AIS_OBJ_LOST_TIMEOUT and AIS_SHIP_LOST_TIMEOUT --- OsmAnd/res/values/strings.xml | 5 + OsmAnd/res/xml/ais_settings.xml | 12 +++ .../plus/plugins/aistracker/AisObject.java | 40 ++++---- .../aistracker/AisObjectConstants.java | 4 - .../aistracker/AisObjectMenuController.java | 2 +- .../plugins/aistracker/AisTrackerLayer.java | 8 +- .../plugins/aistracker/AisTrackerPlugin.java | 18 +++- .../AisTrackerSettingsFragment.java | 98 +++++++------------ 8 files changed, 97 insertions(+), 90 deletions(-) diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index a4143ae64f9..0af5b7d918b 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -1144,6 +1144,11 @@ You need to activate the sensor so OsmAnd can find it. Define TCP port number of the NMEA data source UDP port of local NMEA data receiver Define UPD port where OsmAnd receives NMEA data + Timeout for visibility when object is lost + Set Timeout for visibility of AIS objects: After this time without signal reception, the AIS object will be removed from screen. + Timeout for ship visibility when no signal received + Set timeout for ship visibility: After this time without signal reception, the ship symbol will change its state on screen: It will be crossed out. + Weather Explore Weather forecast. Contours diff --git a/OsmAnd/res/xml/ais_settings.xml b/OsmAnd/res/xml/ais_settings.xml index c3af0a4620b..ebc1d52da1f 100644 --- a/OsmAnd/res/xml/ais_settings.xml +++ b/OsmAnd/res/xml/ais_settings.xml @@ -33,4 +33,16 @@ android:title="@string/ais_port_nmea_local" tools:summary="@string/ais_port_nmea_local_description" /> + + + + \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 06506f99d6e..bbba2314f91 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -30,8 +30,6 @@ import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.maxAgeInMinutes; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.maxVesselAgeInMinutes; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -368,8 +366,8 @@ public void set(@NonNull AisObject ais) { this.bitmapColor = 0; } - private void setBitmap(@NonNull AisTrackerLayer mapLayer) { - if (isLost()) { + private void setBitmap(@NonNull AisTrackerLayer mapLayer, int maxAgeInMin) { + if (isLost(maxAgeInMin)) { if (isMovable()) { this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); } @@ -401,11 +399,11 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { break; } } - this.setColor(); + this.setColor(maxAgeInMin); } - private void setColor() { - if (isLost()) { + private void setColor(int maxAgeInMin) { + if (isLost(maxAgeInMin)) { if (isMovable()) { this.bitmapColor = 0; // black } @@ -436,9 +434,10 @@ private void setColor() { } public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, - @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { - if ((this.bitmap == null) || isLost()) { - this.setBitmap(mapLayer); + @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox, + int maxAgeInMin) { + if ((this.bitmap == null) || isLost(maxAgeInMin)) { + this.setBitmap(mapLayer, maxAgeInMin); } if (this.bitmapColor != 0) { paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); @@ -460,7 +459,7 @@ public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, canvas.rotate(rotation, locationX, locationY); } canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); - if ((speedFactor > 0) && (!isLost())) { + if ((speedFactor > 0) && (!isLost(maxAgeInMin))) { float lineStartX = locationX; float lineLength = (float)this.bitmap.getHeight() * speedFactor; float lineStartY = locationY - this.bitmap.getHeight() / 4.0f; @@ -511,18 +510,15 @@ private boolean needRotation() { return false; } - private boolean isLost(long maxAgeInMin) { + private boolean isLost(int maxAgeInMin) { return ((System.currentTimeMillis() - this.lastUpdate) / 1000 / 60) > maxAgeInMin; } - private boolean isLost() { - return isLost(maxVesselAgeInMinutes); - } /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed * */ - public boolean checkObjectAge() { + public boolean checkObjectAge(int maxAgeInMinutes) { return isLost(maxAgeInMinutes); } public int getMsgType() { return this.ais_msgType; } @@ -555,8 +551,18 @@ public LatLon getPosition() { @Nullable public Location getLocation() { if (this.ais_position != null) { - return new Location(AisTrackerPlugin.AISTRACKER_ID, + Location loc = new Location(AisTrackerPlugin.AISTRACKER_ID, ais_position.getLatitude(), ais_position.getLongitude()); + if (ais_cog != INVALID_COG) { + loc.setBearing((float)ais_cog); + } + if (ais_sog != INVALID_SOG) { + loc.setSpeed((float)ais_sog); + } + if (ais_altitude != INVALID_ALTITUDE) { + loc.setAltitude((float)ais_altitude); + } + return loc; } return null; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index d7a84cb3e09..b965b38c1db 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -4,10 +4,6 @@ import java.util.Map; public final class AisObjectConstants { - /* after this time the object is outdated and can be removed: */ - public final static long maxAgeInMinutes = 7; - /* after this time the (movable) object is lost, the bitmap can be changed: */ - public final static long maxVesselAgeInMinutes = 4; public final static int INVALID_HEADING = 511; public final static int INVALID_NAV_STATUS = 15; public final static int INVALID_MANEUVER_INDICATOR = 0; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 6985be5a691..32a141c5e0a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -75,7 +75,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("MMSI", Integer.toString(aisObject.getMmsi())); if (position != null) { - addMenuItem("Location", + addMenuItem("Position", LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index fa633c9cd2b..6e02da6dd43 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -131,14 +131,15 @@ public void cleanup() { stopNetworkListener(); } private void removeLostAisObjects() { + int maxAge = plugin.AIS_OBJ_LOST_TIMEOUT.get(); for (Iterator> iterator = aisObjectList.entrySet().iterator(); iterator.hasNext(); ) { Map.Entry entry = iterator.next(); - if (entry.getValue().checkObjectAge()) { + if (entry.getValue().checkObjectAge(maxAge)) { Log.d("AisTrackerLayer", "remove AIS object with MMSI " + entry.getValue().getMmsi()); iterator.remove(); } } - // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge()); + // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge(maxAge)); } private void removeOldestAisObjectListEntry() { Log.d("AisTrackerLayer", "removeOldestAisObjectListEntry() called"); @@ -189,9 +190,10 @@ public boolean isLocationVisible(RotatedTileBox tileBox, LatLon coordinates) { @Override public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + int maxAgeInMin = plugin.AIS_SHIP_LOST_TIMEOUT.get(); for (AisObject ais : aisObjectList.values()) { if (isLocationVisible(tileBox, ais.getPosition())) { - ais.draw(this, bitmapPaint, canvas, tileBox); + ais.draw(this, bitmapPaint, canvas, tileBox, maxAgeInMin); } } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index a9b50171de4..2d92fe4c4cb 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -34,10 +34,12 @@ public class AisTrackerPlugin extends OsmandPlugin { public static final String COMPONENT = "net.osmand.aistrackerPlugin"; public static final String AISTRACKER_ID = "osmand.aistracker"; - public static final String AIS_NMEA_PROTOCOL_ID = "ais_nmea_protocol"; - public static final String AIS_NMEA_IP_ADDRESS_ID = "ais_address_nmea_server"; - public static final String AIS_NMEA_TCP_PORT_ID = "ais_port_nmea_server"; - public static final String AIS_NMEA_UDP_PORT_ID = "ais_port_nmea_local"; + public static final String AIS_NMEA_PROTOCOL_ID = "ais_nmea_protocol"; // see xml/ais_settings.xml + public static final String AIS_NMEA_IP_ADDRESS_ID = "ais_address_nmea_server"; // see xml/ais_settings.xml + public static final String AIS_NMEA_TCP_PORT_ID = "ais_port_nmea_server"; // see xml/ais_settings.xml + public static final String AIS_NMEA_UDP_PORT_ID = "ais_port_nmea_local"; // see xml/ais_settings.xml + public static final String AIS_OBJ_LOST_TIMEOUT_ID = "ais_object_lost_timeout"; // see xml/ais_settings.xml + public static final String AIS_SHIP_LOST_TIMEOUT_ID = "ais_ship_lost_timeout"; // see xml/ais_settings.xml public final CommonPreference AIS_NMEA_PROTOCOL; public static final int AIS_NMEA_PROTOCOL_UDP = 0; public static final int AIS_NMEA_PROTOCOL_TCP = 1; @@ -47,14 +49,20 @@ public class AisTrackerPlugin extends OsmandPlugin { public static final Integer AIS_NMEA_DEFAULT_TCP_PORT = 4001; public final CommonPreference AIS_NMEA_UDP_PORT; public static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; + public final CommonPreference AIS_OBJ_LOST_TIMEOUT; + public static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; + public final CommonPreference AIS_SHIP_LOST_TIMEOUT; + public static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; public AisTrackerPlugin(OsmandApplication app) { super(app); - /* "ais_nmea_protocol" etc. is a reference to the content of ais_settings.xml */ + /* "ais_nmea_protocol" etc. is a reference to the content of xml/ais_settings.xml */ AIS_NMEA_PROTOCOL = registerIntPreference(AIS_NMEA_PROTOCOL_ID, AIS_NMEA_PROTOCOL_UDP); AIS_NMEA_IP_ADDRESS = registerStringPreference(AIS_NMEA_IP_ADDRESS_ID, AIS_NMEA_DEFAULT_IP); AIS_NMEA_TCP_PORT = registerIntPreference(AIS_NMEA_TCP_PORT_ID, AIS_NMEA_DEFAULT_TCP_PORT); AIS_NMEA_UDP_PORT = registerIntPreference(AIS_NMEA_UDP_PORT_ID, AIS_NMEA_DEFAULT_UDP_PORT); + AIS_OBJ_LOST_TIMEOUT = registerIntPreference(AIS_OBJ_LOST_TIMEOUT_ID, AIS_OBJ_LOST_DEFAULT_TIMEOUT); + AIS_SHIP_LOST_TIMEOUT = registerIntPreference(AIS_SHIP_LOST_TIMEOUT_ID, AIS_SHIP_LOST_DEFAULT_TIMEOUT); Log.d("AisTrackerPlugin", "constructor"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 824345ee5ff..d67e51ae6c6 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -5,6 +5,7 @@ import android.content.Context; import android.os.Bundle; +import android.text.SpannableStringBuilder; import android.util.Log; import androidx.annotation.NonNull; @@ -39,6 +40,8 @@ protected void setupPreferences() { setupIpAddress(currentProtocol); setupTcpPort(currentProtocol); setupUdpPort(currentProtocol); + setupObjectLostTimeout(); + setupShipLostTimeout(); } private int setupProtocol() { @@ -56,46 +59,6 @@ private int setupProtocol() { } private void setupIpAddress(int currentProtocol) { - /* - InputFilter[] filters = new InputFilter[1]; - filters[0] = new InputFilter() { - @Override - public CharSequence filter(CharSequence source, int start, int end, - android.text.Spanned dest, int dstart, int dend) { - if (end > start) { - String destTxt = dest.toString(); - String resultingTxt = destTxt.substring(0, dstart) - + source.subSequence(start, end) - + destTxt.substring(dend); - if (!resultingTxt - .matches("^\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3})?)?)?)?)?)?")) { - return ""; - } else { - String[] splits = resultingTxt.split("\\."); - for (int i = 0; i < splits.length; i++) { - if (Integer.valueOf(splits[i]) > 255) { - return ""; - } - } - } - } - return null; - } - }; - */ - //EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); - //Log.d("AisTrackerSettingsFragment","## findPreference()"); - //aisNmeaIpAddress.setOnBindEditTextListener(new androidx.preference.EditTextPreference.OnBindEditTextListener() { - /*aisNmeaIpAddress.setOnBindEditTextListener(new OnBindEditTextListener() { - @Override - public void onBindEditText(@NonNull EditText editText) { - editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); - editText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(10)}); - Log.d("AisTrackerSettingsFragment","## onBindEditText()"); - } - }); - */ - EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); if (aisNmeaIpAddress != null) { aisNmeaIpAddress.setDescription(R.string.ais_address_nmea_server_description); @@ -105,28 +68,10 @@ public void onBindEditText(@NonNull EditText editText) { aisNmeaIpAddress.setEnabled(true); } } + // TODO: the current value is not shown in the settings overview dialog(?) } private void setupTcpPort(int currentProtocol) { - /* EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); - if (aisNmeaPort != null) { - Log.d("AisTrackerSettingsFragment","## setupTcpPort()"); - aisNmeaPort.setOnBindEditTextListener(new OnBindEditTextListener() { - @Override - public void onBindEditText(@NonNull EditText editText) { - Log.d("AisTrackerSettingsFragment","## onBindEditText()"); - editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); - } - }); - aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); - if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { - aisNmeaPort.setEnabled(false); - } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { - aisNmeaPort.setEnabled(true); - } - } - */ - EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); if (aisNmeaPort != null) { aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); @@ -136,6 +81,7 @@ public void onBindEditText(@NonNull EditText editText) { aisNmeaPort.setEnabled(true); } } + // TODO: the current value is not shown in the settings overview dialog(?) } private void setupUdpPort(int currentProtocol) { @@ -148,24 +94,56 @@ private void setupUdpPort(int currentProtocol) { aisNmeaPort.setEnabled(false); } } + // TODO: the current value is not shown in the settings overview dialog(?) + } + private void setupObjectLostTimeout() { + Integer[] entryValues = {3, 5, 7, 10, 12, 15, 20}; + String[] entries = new String[entryValues.length]; + for (int i = 0; i < entryValues.length; i++) { + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to ressource file + } + ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_OBJ_LOST_TIMEOUT.getId()); + if (objectLostTimeout != null) { + objectLostTimeout.setEntries(entries); + objectLostTimeout.setEntryValues(entryValues); + objectLostTimeout.setDescription(R.string.ais_object_lost_timeout_description); + } + } + private void setupShipLostTimeout() { + Integer[] entryValues = {2, 3, 4, 5, 7, 10, 15, 100 /* disabled: must be bigger than the biggest value of setupObjectLostTimeout() */}; + String[] entries = new String[entryValues.length]; + for (int i = 0; i < entryValues.length - 1; i++) { + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to ressource file + } + entries[entryValues.length - 1] = "disabled"; // TODO: move to ressource file + + ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_SHIP_LOST_TIMEOUT.getId()); + if (objectLostTimeout != null) { + objectLostTimeout.setEntries(entries); + objectLostTimeout.setEntryValues(entryValues); + objectLostTimeout.setDescription(R.string.ais_ship_lost_timeout_description); + } } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean restartNetworkListener = false; if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_IP_ADDRESS_ID)) { if (!isValidIpV4Address(newValue.toString())) { showAlertDialog("Only IPv4 address accepted (\"a.b.c.d\", where a,b,c,d in range 0..255)."); return false; } + restartNetworkListener = true; } else if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_TCP_PORT_ID) || preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_UDP_PORT_ID)) { if (!isValidPortNumber(newValue.toString())) { showAlertDialog("Only numerical values accepted in range 0..65535."); return false; } + restartNetworkListener = true; } boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); - if (layer != null) { + if ((layer != null) && (restartNetworkListener)) { layer.restartNetworkListener(); } return ret; From 8955967129d61ad39aa090e8c1adb993790b74cd Mon Sep 17 00:00:00 2001 From: Falk Date: Fri, 21 Jun 2024 23:22:52 +0200 Subject: [PATCH 44/74] ship destination consisting of "@" is considered as invalid --- .../src/net/osmand/plus/plugins/aistracker/AisObject.java | 6 +++++- .../net/osmand/plus/plugins/aistracker/AisTrackerLayer.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index bbba2314f91..b7f9e03c210 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -143,7 +143,11 @@ public AisObject(int mmsi, int msgType, int imo, @Nullable String callSign, @Nul this.ais_draught = draught; this.ais_callSign = callSign; this.ais_shipName = shipName; - this.ais_destination = destination; + if (destination != null) { + if (!destination.matches("^@+$")) { // string consisting of only "@" characters is invalid + this.ais_destination = destination; + } + } this.ais_etaMon = etaMon; this.ais_etaDay = etaDay; this.ais_etaHour = etaHour; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 6e02da6dd43..5ea4d3d3679 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -73,7 +73,7 @@ private void initTestObjects() { updateAisObjectList(ais); ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, 0, 0, 0, 0, - "", 0, 0, 0, 0); + "@@@", 0, 0, 0, 0); updateAisObjectList(ais); // land station ais = new AisObject(878121, 4, 50.736d, 7.100d); From 78ceb14fa0d20dbb1a08e16cef0495ee08e648fa Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 22 Jun 2024 18:56:52 +0200 Subject: [PATCH 45/74] improved preference setup dialog for network setting: show current values --- .../aistracker/AisTrackerSettingsFragment.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index d67e51ae6c6..152f3a92082 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -61,40 +61,42 @@ private int setupProtocol() { private void setupIpAddress(int currentProtocol) { EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); if (aisNmeaIpAddress != null) { + String currentValue = plugin.AIS_NMEA_IP_ADDRESS.get(); + if (currentValue == null) { currentValue = ""; } aisNmeaIpAddress.setDescription(R.string.ais_address_nmea_server_description); + aisNmeaIpAddress.setSummary(currentValue); if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { aisNmeaIpAddress.setEnabled(false); } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { aisNmeaIpAddress.setEnabled(true); } } - // TODO: the current value is not shown in the settings overview dialog(?) } - private void setupTcpPort(int currentProtocol) { EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); if (aisNmeaPort != null) { + int currentValue = plugin.AIS_NMEA_TCP_PORT.get(); aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); + aisNmeaPort.setSummary(String.valueOf(currentValue)); if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { aisNmeaPort.setEnabled(false); } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { aisNmeaPort.setEnabled(true); } } - // TODO: the current value is not shown in the settings overview dialog(?) } - private void setupUdpPort(int currentProtocol) { EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_UDP_PORT.getId()); if (aisNmeaPort != null) { + int currentValue = plugin.AIS_NMEA_UDP_PORT.get(); aisNmeaPort.setDescription(R.string.ais_port_nmea_local_description); + aisNmeaPort.setSummary(String.valueOf(currentValue)); if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { aisNmeaPort.setEnabled(true); } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { aisNmeaPort.setEnabled(false); } } - // TODO: the current value is not shown in the settings overview dialog(?) } private void setupObjectLostTimeout() { Integer[] entryValues = {3, 5, 7, 10, 12, 15, 20}; From 7549fbe97bbdb146bed89993649538f49cba657d Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 25 Jun 2024 22:35:42 +0200 Subject: [PATCH 46/74] new class to calculate CPA nd TCPA (not included into GUI yet) --- .../plus/plugins/aistracker/AisObject.java | 2 +- .../aistracker/AisObjectConstants.java | 5 +- .../aistracker/AisObjectMenuController.java | 67 +++++++- .../plugins/aistracker/AisTrackerHelper.java | 159 ++++++++++++++++++ 4 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index b7f9e03c210..5b89d6619c3 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -561,7 +561,7 @@ public Location getLocation() { loc.setBearing((float)ais_cog); } if (ais_sog != INVALID_SOG) { - loc.setSpeed((float)ais_sog); + loc.setSpeed((float)(ais_sog * 3600 / 1852)); // in m/s } if (ais_altitude != INVALID_ALTITUDE) { loc.setAltitude((float)ais_altitude); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index b965b38c1db..c272664d567 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -20,8 +20,11 @@ public final class AisObjectConstants { public final static double INVALID_LON = 181.0; public final static double INVALID_ROT = 128.0; public final static double INVALID_DRAUGHT = 0.0; + public final static double INVALID_TCPA = -10000.0d; + public final static float INVALID_CPA = -1.0f; - public enum AisObjType { + + public static enum AisObjType { AIS_VESSEL, AIS_VESSEL_SPORT, AIS_VESSEL_FAST, diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 32a141c5e0a..f0b5d98dee8 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -7,9 +7,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import net.osmand.Location; import net.osmand.LocationConvert; import net.osmand.data.LatLon; import net.osmand.data.PointDescription; +import net.osmand.plus.OsmAndLocationProvider; import net.osmand.plus.OsmandApplication; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.mapcontextmenu.MenuBuilder; @@ -32,6 +34,39 @@ public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointD builder.setShowNearestWiki(false); // TODO: show an icon in the menu } + private float getOwnSpeed(@Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + if (myLocation.hasSpeed()) { + return myLocation.getSpeed(); + } + } + } + return 0.0f; + } + private float getOwnBearing(@Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + if (myLocation.hasBearing()) { + return myLocation.getBearing(); + } + } + } + return 0.0f; + } + /* + private String getOwnLocationAsString(@Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + return myLocation.toString(); + } + } + return null; + } + */ private void addMenuItem(@NonNull String type, @Nullable String value) { if (value != null) { @@ -79,8 +114,9 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { - float distance = aisObject.getDistanceInNauticalMiles(app.getLocationProvider()); - float bearing = aisObject.getBearing(app.getLocationProvider()); + OsmAndLocationProvider locationProvider = app.getLocationProvider(); + float distance = aisObject.getDistanceInNauticalMiles(locationProvider); + float bearing = aisObject.getBearing(locationProvider); if (distance >= 0.0f) { try { addMenuItem("Distance", String.format("%.1f nm", distance)); @@ -91,6 +127,12 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Bearing", String.format("%.1f", bearing)); } catch (Exception ignore) { } } + /* + // test: + addMenuItem("# loc", getOwnLocationAsString(locationProvider)); + addMenuItem("# ownSpeed", Float.toString(getOwnSpeed(locationProvider))); + addMenuItem("# ownBearing", Float.toString(getOwnBearing(locationProvider))); + */ } } if (msgTypes.contains(21)) { // ATON (aid to navigation) @@ -175,7 +217,26 @@ protected Object getObject() { @NonNull @Override - public String getTypeStr() { return "AIS object"; } + public String getTypeStr() { + String res = ""; + SortedSet msgTypes = aisObject.getMsgTypes(); + for (Integer i : new Integer[]{5, 19, 24}) { + if (msgTypes.contains(i)) { + res += aisObject.getShipTypeString(); + break; + } + } + for (Integer i : new Integer[]{1, 2, 3}) { + if (msgTypes.contains(i)) { + if (res.isEmpty()) { + res = "Vessel"; + } + res += ": " + aisObject.getNavStatusString() + "."; + break; + } + } + return (res.isEmpty() ? "AIS object" : res); + } @Override public boolean needStreetName() { return false; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java new file mode 100644 index 00000000000..bb10c9a4367 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -0,0 +1,159 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_CPA; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_TCPA; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.Location; +import net.osmand.plus.OsmAndLocationProvider; + +public final class AisTrackerHelper { + + private static class Vector { + public double x; + public double y; + public Vector(double a, double b) { + this.x = a; + this.y = b; + } + public Vector(@NonNull Vector a) { + this.x = a.x; + this.y = a.y; + } + @NonNull + public Vector multiply(double a) { + return new Vector(this.x * a, this.y * a); + } + @NonNull + public Vector add(@NonNull Vector a) { + return new Vector(this.x + a.x, this.y + a.y); + } + } + + /* calculate the Time to Closest Point of Approach (TCPA) of two moving objects: + * object 1 at position x and velocity vector vx + * object 2 at position y and velocity vectoy vy, + * For the calculation, cartesian ccordinates are assumed with a cartesian distance metricx + * -> attention: by using sherical coordinates, this will produce an error! */ + private double getTcpa(@NonNull Vector x, @NonNull Vector y, @NonNull Vector vx, @NonNull Vector vy) { + Vector dx = new Vector(y.x - x.x, y.y - x.y); + Vector dv = new Vector(vy.x - vx.x, vy.y - vx.y); + return -(((dx.x * dv.x) + (dx.y * dv.y)) / ((dv.x * dv.x) + (dv.y * dv.y))); // TODO: check for Div/0 + } + + /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and y, + * it is presumed that x and y both contain their position, speed and course */ + public double getTcpa(@NonNull Location x, @NonNull Location y) { + if (checkSpeedAndBearing(x, y)) { + return INVALID_TCPA; + } + Vector vx = courseToVector(x.getBearing(), getSpeedInNodes(x)); + Vector vy = courseToVector(y.getBearing(), getSpeedInNodes(y)); + return getTcpa(locationToVector(x), locationToVector(y), vx, vy); + } + + /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and own location, + * it is presumed that x contains its position, speed and course */ + public double getTcpa(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + return getTcpa(x, myLocation); + } + } + return INVALID_TCPA; + } + @Nullable + private Location getCpa(@NonNull Location x, @NonNull Location y, boolean useFirstAsReference) { + if (checkSpeedAndBearing(x, y)) { + return null; + } + double tcpa = getTcpa(x,y); + Location base = useFirstAsReference ? x : y; + Vector v = courseToVector(base.getBearing(), getSpeedInNodes(base)); + Vector newPos = getNewPosition(locationToVector(base), v, tcpa); + Location newX = new Location(base); + newX.setLongitude(newPos.x); + newX.setLatitude(newPos.y); + return newX; + } + + /* to calculate the Closest Point of Approach (CPA) between the objects x and y, + * it is presumed that x and y both contain their position, speed and course. + * This function returns the position of first object x at time of TCPA */ + @Nullable + public Location getCpa1(@NonNull Location x, @NonNull Location y) { + return getCpa(x, y, true); + } + /* to calculate the Closest Point of Approach (CPA) between the objects x and y, + * it is presumed that x and y both contain their position, speed and course. + * This function returns the position of second object y at time of TCPA */ + @Nullable + public Location getCpa2(@NonNull Location x, @NonNull Location y) { + return getCpa(x, y, false); + } + + /* caluclate the distance between the given objects at their Closest Point of Approach (CPA)*/ + public float getCpaDistance(@NonNull Location x, @NonNull Location y) { + Location cpaX = getCpa1(x,y); + Location cpaY = getCpa2(x,y); + if ((cpaX != null) && (cpaY != null)) { + return cpaX.distanceTo(cpaY); + } else { + return INVALID_CPA; + } + } + + /* caluclate the distance between the given object and own position at their Closest Point of Approach (CPA)*/ + public float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + return getCpaDistance(x, myLocation); + } + } + return INVALID_CPA; + } + + /* calculate the new position of a moving object with coordinates x and velocity vector v + after the given time, + * -> attention: by using sherical coordinates, this will produce an error! */ + @NonNull + private Vector getNewPosition(@NonNull Vector x, @NonNull Vector v, double time) { + return new Vector(x.add(v.multiply(time))); + } + + /* calculate a velocity vector from givem course (COG) and speed (SOG). + COG is given as heading, SOG as scalar */ + @NonNull + private Vector courseToVector(double cog, double sog) { + double alpha = cog + 90.0d; + while (alpha < 0) { alpha += 360.0d; } + while (alpha > 360.0d ) { alpha -= 360.0d; } + alpha = Math.toRadians(alpha); + return new Vector(Math.sin(alpha) * sog, Math.cos(alpha) * sog); + } + + @NonNull + private Vector locationToVector(@NonNull Location loc) { + return new Vector(loc.getLongitude(), loc.getLatitude()); + } + private boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { + if (!x.hasBearing() || !y.hasBearing() || !x.hasSpeed() || !y.hasSpeed()) { + Log.d("AisTrackerHelper", "some input data is missing: x.hasBearing->" + + x.hasBearing() + ", y.hasBearing->" + y.hasBearing() + ", x.hasSpeed->" + + x.hasSpeed() + ", y.hasSpeed" + y.hasSpeed()); + return true; + } else { + return false; + } + } + + private float getSpeedInNodes(@NonNull Location loc) { + return loc.getSpeed() * 1852 / 3600; + } +} From d12c0c51373b70e265be98a485d652aadb285d30 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 30 Jun 2024 17:36:34 +0200 Subject: [PATCH 47/74] update of CPA class after some testing (not included into GUI yet) --- .../aistracker/AisMessageListener.java | 5 + .../plus/plugins/aistracker/AisObject.java | 2 +- .../plugins/aistracker/AisTrackerHelper.java | 199 ++++++++++++++---- .../plugins/aistracker/AisTrackerLayer.java | 133 ++++++++++++ 4 files changed, 292 insertions(+), 47 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java index 121475de45b..49e5d532e9e 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -18,6 +18,7 @@ import net.sf.marineapi.ais.message.AISMessage27; import net.sf.marineapi.nmea.event.SentenceListener; import net.sf.marineapi.nmea.io.SentenceReader; +import net.sf.marineapi.nmea.sentence.SentenceId; import java.io.IOException; import java.io.InputStream; @@ -391,6 +392,10 @@ private void handleAisMessage(int aisType, Object obj) { } private void initEmbeddedLister(int aisType, @NonNull SentenceListener listener) { AisMessageListener.this.sentenceReader.addSentenceListener(listener); + /* + AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDM); + AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDO); + */ AisMessageListener.this.listenerList.push(listener); Log.d("AisMessageListener","Listener Type " + aisType + " started"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 5b89d6619c3..a8af8913ee7 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -561,7 +561,7 @@ public Location getLocation() { loc.setBearing((float)ais_cog); } if (ais_sog != INVALID_SOG) { - loc.setSpeed((float)(ais_sog * 3600 / 1852)); // in m/s + loc.setSpeed((float)(ais_sog * 1852 / 3600)); // in m/s } if (ais_altitude != INVALID_ALTITUDE) { loc.setAltitude((float)ais_altitude); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index bb10c9a4367..03fa6e36725 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -8,14 +8,18 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.jwetherell.openmap.common.LatLonPoint; + import net.osmand.Location; import net.osmand.plus.OsmAndLocationProvider; public final class AisTrackerHelper { - + private static long lastCorrectionUpdate = 0; + private static double correctionFactor = 1.0d; + private static final long maxCorrectionUpdateAgeInMin = 60; private static class Vector { - public double x; - public double y; + public double x; // Latitude (grows in North direction) + public double y; // Longitude (grows in East direction) public Vector(double a, double b) { this.x = a; this.y = b; @@ -25,13 +29,33 @@ public Vector(@NonNull Vector a) { this.y = a.y; } @NonNull - public Vector multiply(double a) { - return new Vector(this.x * a, this.y * a); + public Vector sub(@NonNull Vector a) { + return new Vector(this.x - a.x, this.y - a.y); } - @NonNull - public Vector add(@NonNull Vector a) { - return new Vector(this.x + a.x, this.y + a.y); + public double dot(@NonNull Vector a) { return (this.x * a.x) + (this.y * a.y); } + } + public static class Cpa { + private double tcpa; // in hours + private float cpaDist; // in miles + private Location newPos1; // position of first object at time tcpa + private Location newPos2; // position of first object at time tcpa + public Cpa() { + reset(); + } + public void reset() { + cpaDist = INVALID_CPA; + tcpa = INVALID_TCPA; + newPos1 = null; + newPos2 = null; } + public void setTcpa(double x) { this.tcpa = x; } + public void setCpaDist(float x) { this.cpaDist = x; } + public void setCpaPos1(Location loc) { this.newPos1 = loc; } + public void setCpaPos2(Location loc) { this.newPos2 = loc; } + public double getTcpa() { return tcpa; } + public float getCpaDist() { return cpaDist; } + public Location getCpaPos1() { return newPos1; } + public Location getCpaPos2() { return newPos2; } } /* calculate the Time to Closest Point of Approach (TCPA) of two moving objects: @@ -39,77 +63,89 @@ public Vector add(@NonNull Vector a) { * object 2 at position y and velocity vectoy vy, * For the calculation, cartesian ccordinates are assumed with a cartesian distance metricx * -> attention: by using sherical coordinates, this will produce an error! */ - private double getTcpa(@NonNull Vector x, @NonNull Vector y, @NonNull Vector vx, @NonNull Vector vy) { - Vector dx = new Vector(y.x - x.x, y.y - x.y); - Vector dv = new Vector(vy.x - vx.x, vy.y - vx.y); - return -(((dx.x * dv.x) + (dx.y * dv.y)) / ((dv.x * dv.x) + (dv.y * dv.y))); // TODO: check for Div/0 + private static double getTcpa(@NonNull Vector x, @NonNull Vector y, + @NonNull Vector vx, @NonNull Vector vy, double lonCorrection) { + Vector dx = new Vector( y.sub(x)); + Vector dv = new Vector(vy.sub(vx)); + double divisor = dv.dot(dv); // TODO: check for Div/0 + return -(((dx.x * dv.x) + (dx.y * dv.y / lonCorrection)) / divisor); // TODO: check for Div/0 } /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and y, * it is presumed that x and y both contain their position, speed and course */ - public double getTcpa(@NonNull Location x, @NonNull Location y) { + private static double getTcpa(@NonNull Location x, @NonNull Location y, double lonCorrection) { if (checkSpeedAndBearing(x, y)) { return INVALID_TCPA; } - Vector vx = courseToVector(x.getBearing(), getSpeedInNodes(x)); - Vector vy = courseToVector(y.getBearing(), getSpeedInNodes(y)); - return getTcpa(locationToVector(x), locationToVector(y), vx, vy); + if (lonCorrection < 0.001) { + // in this case the lonCorrection is considered invalid -> new calculation + lonCorrection = getLonCorrection(x); + } + return getTcpa(locationToVector(x), locationToVector(y), + courseToVector(x.getBearing(), getSpeedInKnots(x)), + courseToVector(y.getBearing(), getSpeedInKnots(y)), lonCorrection); + } + + public static double getTcpa(@NonNull Location x, @NonNull Location y) { + return getTcpa(x, y, 0.0d); } /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and own location, * it is presumed that x contains its position, speed and course */ - public double getTcpa(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { + public static double getTcpa(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { if (locationProvider != null) { Location myLocation = locationProvider.getLastKnownLocation(); if (myLocation != null) { - return getTcpa(x, myLocation); + long now = System.currentTimeMillis(); + if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { + lastCorrectionUpdate = now; + correctionFactor = getLonCorrection(myLocation); + } + return getTcpa(x, myLocation, correctionFactor); } } return INVALID_TCPA; } + @Nullable - private Location getCpa(@NonNull Location x, @NonNull Location y, boolean useFirstAsReference) { + private static Location getCpa(@NonNull Location x, @NonNull Location y, boolean useFirstAsReference) { if (checkSpeedAndBearing(x, y)) { return null; } double tcpa = getTcpa(x,y); Location base = useFirstAsReference ? x : y; - Vector v = courseToVector(base.getBearing(), getSpeedInNodes(base)); - Vector newPos = getNewPosition(locationToVector(base), v, tcpa); - Location newX = new Location(base); - newX.setLongitude(newPos.x); - newX.setLatitude(newPos.y); - return newX; + return getNewPosition(base, tcpa); } /* to calculate the Closest Point of Approach (CPA) between the objects x and y, * it is presumed that x and y both contain their position, speed and course. * This function returns the position of first object x at time of TCPA */ @Nullable - public Location getCpa1(@NonNull Location x, @NonNull Location y) { + public static Location getCpa1(@NonNull Location x, @NonNull Location y) { return getCpa(x, y, true); } + /* to calculate the Closest Point of Approach (CPA) between the objects x and y, * it is presumed that x and y both contain their position, speed and course. * This function returns the position of second object y at time of TCPA */ @Nullable - public Location getCpa2(@NonNull Location x, @NonNull Location y) { + public static Location getCpa2(@NonNull Location x, @NonNull Location y) { return getCpa(x, y, false); } - /* caluclate the distance between the given objects at their Closest Point of Approach (CPA)*/ - public float getCpaDistance(@NonNull Location x, @NonNull Location y) { + /* caluclate the distance between the given objects at their Closest Point of Approach (CPA) */ + public static float getCpaDistance(@NonNull Location x, @NonNull Location y) { Location cpaX = getCpa1(x,y); Location cpaY = getCpa2(x,y); if ((cpaX != null) && (cpaY != null)) { - return cpaX.distanceTo(cpaY); + return meterToMiles(cpaX.distanceTo(cpaY)); } else { return INVALID_CPA; } } - /* caluclate the distance between the given object and own position at their Closest Point of Approach (CPA)*/ - public float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { + /* caluclate the distance between the given object and own position at their Closest Point of Approach (CPA) */ + public static float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { if (locationProvider != null) { Location myLocation = locationProvider.getLastKnownLocation(); if (myLocation != null) { @@ -119,30 +155,101 @@ public float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvide return INVALID_CPA; } - /* calculate the new position of a moving object with coordinates x and velocity vector v - after the given time, - * -> attention: by using sherical coordinates, this will produce an error! */ - @NonNull - private Vector getNewPosition(@NonNull Vector x, @NonNull Vector v, double time) { - return new Vector(x.add(v.multiply(time))); + public static void getCpa(@NonNull Location loc1, @NonNull Location loc2, + @NonNull Cpa result) { + if (!checkSpeedAndBearing(loc1, loc2)) { + double tcpa = getTcpa(loc1, loc2); + Location cpaX = getNewPosition(loc1, tcpa); + Location cpaY = getNewPosition(loc2, tcpa); + result.setTcpa(tcpa); + result.setCpaPos1(cpaX); + result.setCpaPos2(cpaY); + if ((cpaX != null) && (cpaY != null)) { + result.setCpaDist(meterToMiles(cpaX.distanceTo(cpaY))); + } + } + } + + public static void getCpa(@NonNull Location loc, @Nullable OsmAndLocationProvider locationProvider, + @NonNull Cpa result) { + if (locationProvider != null) { + Location myLocation = locationProvider.getLastKnownLocation(); + if (myLocation != null) { + getCpa(myLocation, loc, result); + } + } + } + + private static double bearingInRad(float bearingInDegrees) { + double res = bearingInDegrees * 2 * Math.PI / 360.0; + while (res >= Math.PI) { res -= (2 * Math.PI); } + return res; + } + + @Nullable + public static Location getNewPosition(@Nullable Location x, double time) { + if (x != null) { + if (x.hasBearing() && x.hasSpeed()) { + LatLonPoint a = new LatLonPoint(x.getLatitude(), x.getLongitude()); + LatLonPoint b = a.getPoint(x.getSpeed() * time * Math.PI / 5556.0, bearingInRad(x.getBearing())); + Location newX = new Location(x); + newX.setLongitude(b.getLongitude()); + newX.setLatitude(b.getLatitude()); + return newX; + } else { + Log.d("AisTrackerHelper", "getNewPosition(): y.hasBearing->" + + x.hasBearing() + ", x.hasSpeed->" + x.hasSpeed()); + return null; + } + } else { + return null; + } + } + + private static double getLonCorrection(@Nullable Location loc) { + if (loc != null) { + Location x = new Location(loc); + // simulate a "measurement" trio towards East... + x.setSpeed(knotsToMeterPerSecond(1.0f)); // speed -> 1 kn + x.setBearing(90.0f); // course -> east + Location yEast = getNewPosition(x, 1.0); // new position after 1 hour + + if (yEast != null) { + double diffLon = yEast.getLongitude() - x.getLongitude(); + return diffLon * 60.0; // correction factor for longitude + } + } + return 1.0f; // fallback + } + + public static float knotsToMeterPerSecond(float speed) { + return speed * 1852 / 3600; + } + public static float meterPerSecondToKnots(float speed) { + return speed * 3600 / 1852; + } + + public static float meterToMiles(float x) { + return x / 1852.0f; } /* calculate a velocity vector from givem course (COG) and speed (SOG). COG is given as heading, SOG as scalar */ @NonNull - private Vector courseToVector(double cog, double sog) { - double alpha = cog + 90.0d; + private static Vector courseToVector(double cog, double sog) { + double alpha = 450.0d - cog; while (alpha < 0) { alpha += 360.0d; } - while (alpha > 360.0d ) { alpha -= 360.0d; } + while (alpha >= 360.0d ) { alpha -= 360.0d; } alpha = Math.toRadians(alpha); return new Vector(Math.sin(alpha) * sog, Math.cos(alpha) * sog); } @NonNull - private Vector locationToVector(@NonNull Location loc) { - return new Vector(loc.getLongitude(), loc.getLatitude()); + private static Vector locationToVector(@NonNull Location loc) { + return new Vector(loc.getLatitude() * 60.0, loc.getLongitude() * 60.0); } - private boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { + + private static boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { if (!x.hasBearing() || !y.hasBearing() || !x.hasSpeed() || !y.hasSpeed()) { Log.d("AisTrackerHelper", "some input data is missing: x.hasBearing->" + x.hasBearing() + ", y.hasBearing->" + y.hasBearing() + ", x.hasSpeed->" @@ -153,7 +260,7 @@ private boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { } } - private float getSpeedInNodes(@NonNull Location loc) { - return loc.getSpeed() * 1852 / 3600; + private static float getSpeedInKnots(@NonNull Location loc) { + return meterPerSecondToKnots(loc.getSpeed()); } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 5ea4d3d3679..f4f86c046dd 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -1,5 +1,10 @@ package net.osmand.plus.plugins.aistracker; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.knotsToMeterPerSecond; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.meterToMiles; +import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -12,6 +17,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import net.osmand.Location; +import net.osmand.LocationConvert; import net.osmand.core.android.MapRendererView; import net.osmand.core.jni.PointI; import net.osmand.data.LatLon; @@ -86,6 +93,132 @@ private void initTestObjects() { ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); updateAisObjectList(ais); + // here some tests for the geo (CPA) calculation + // define 3 (vessel) objects + Location x1 = new Location("test", 49.5d, -1.0d); // 49°30'N, 1°00'W + Location x2 = new Location("test", 49.916667d, 0.416667d); // 49°55'N, 0°25'E + Location x3 = new Location("test", 49.666667d, -0.75d); // 49°40'N, 0°45'W + Location y1, y2, y3; + Log.d("AisTrackerLayer", "# test0: position 1 after 0 hours: " + + LocationConvert.convertLatitude(x1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 2 after 0 hours: " + + LocationConvert.convertLatitude(x2.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x2.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 3 after 0 hours: " + + LocationConvert.convertLatitude(x3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x3.getLongitude(), FORMAT_MINUTES, true)); + + // use case: x1: course 0°, speed 5kn, x3: course 270°, speed 10kn, time: 1h, 1.5h + x1.setSpeed(knotsToMeterPerSecond(5.0f)); + x1.setBearing(0.0f); + x3.setSpeed(knotsToMeterPerSecond(10.0f)); + x3.setBearing(270.0f); + AisTrackerHelper.Cpa cpa1 = new AisTrackerHelper.Cpa(); + getCpa(x1, x3, cpa1); + Log.d("AisTrackerLayer", "# test1: tcpa(x1, x3): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test1: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test1: dist0: " + meterToMiles(x1.distanceTo(x3))); + y1 = AisTrackerHelper.getNewPosition(x1, 1.0); + y3 = AisTrackerHelper.getNewPosition(x3, 1.0); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test1: position 1 after 1 hour: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: position 3 after 1 hour: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 1.18); + y3 = AisTrackerHelper.getNewPosition(x3, 1.18); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test1: position 1 after 1.18 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: position 3 after 1.18 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 1.5); + y3 = AisTrackerHelper.getNewPosition(x3, 1.5); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test1: position 1 after 1.5 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: position 3 after 1.5 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + + // use case: x1: course 0°, speed 5kn, x3: course 270°, speed 5kn, time 1h, 1.5h, 2h + x1.setSpeed(knotsToMeterPerSecond(5.0f)); + x1.setBearing(0.0f); + x3.setSpeed(knotsToMeterPerSecond(5.0f)); + x3.setBearing(270.0f); + cpa1.reset(); + getCpa(x1, x3, cpa1); + Log.d("AisTrackerLayer", "# test2: tcpa(x1, x3): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test2: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test2: dist0: " + meterToMiles(x1.distanceTo(x3))); + y1 = AisTrackerHelper.getNewPosition(x1, 1.0); + y3 = AisTrackerHelper.getNewPosition(x3, 1.0); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test2: position 1 after 1 hour: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: position 3 after 1 hour: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 1.5); + y3 = AisTrackerHelper.getNewPosition(x3, 1.5); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test2: position 1 after 1.5 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: position 3 after 1.5 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 2.0); + y3 = AisTrackerHelper.getNewPosition(x3, 2.0); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test2: position 1 after 2 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: position 3 after 2 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + + // use case: x2: course 270°, speed 5kn, x3: course 45°, speed 5kn, time 5h + x2.setSpeed(knotsToMeterPerSecond(5.0f)); + x2.setBearing(270.0f); + x3.setSpeed(knotsToMeterPerSecond(5.0f)); + x3.setBearing(45.0f); + cpa1.reset(); + getCpa(x2, x3, cpa1); + Log.d("AisTrackerLayer", "# test3: tcpa(x1, x3): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test3: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test3: dist0: " + meterToMiles(x2.distanceTo(x3))); + y2 = AisTrackerHelper.getNewPosition(x2, 5.0); + y3 = AisTrackerHelper.getNewPosition(x3, 5.0); + if ((y2 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test3: position 2 after 5 hours: " + + LocationConvert.convertLatitude(y2.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y2.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test3: position 3 after 5 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test3: dist1: " + meterToMiles(y2.distanceTo(y3))); + } + //removeOldestAisObjectListEntry(); //removeLostAisObjects(); } From 3362c2a16242876cdac85bd9f0d55db584ded7dc Mon Sep 17 00:00:00 2001 From: Falk Date: Mon, 1 Jul 2024 23:38:37 +0200 Subject: [PATCH 48/74] use CPA data request in AIS object context menu to display CPA, TCPA --- .../aistracker/AisObjectMenuController.java | 36 ++++++ .../plugins/aistracker/AisTrackerHelper.java | 44 ++++--- .../plugins/aistracker/AisTrackerLayer.java | 114 ++++++++++++------ 3 files changed, 146 insertions(+), 48 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index f0b5d98dee8..1b2d5a136d9 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -1,7 +1,10 @@ package net.osmand.plus.plugins.aistracker; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; +import static java.lang.Math.ceil; + import android.annotation.SuppressLint; import androidx.annotation.NonNull; @@ -67,6 +70,38 @@ private String getOwnLocationAsString(@Nullable OsmAndLocationProvider locationP return null; } */ + @SuppressLint("DefaultLocale") + private void addCpaInfo(@NonNull SortedSet msgTypes, + @Nullable OsmAndLocationProvider locationProvider) { + if (msgTypes.contains(21) || msgTypes.contains(9)) { + return; + } + if ((aisObject.getCog() != AisObjectConstants.INVALID_COG) && + (aisObject.getSog() != AisObjectConstants.INVALID_SOG)) { + AisTrackerHelper.Cpa cpa = new AisTrackerHelper.Cpa(); + Location aisLocation = aisObject.getLocation(); + if (aisLocation != null) { + getCpa(aisLocation, locationProvider, cpa); + if (cpa.isValid()) { + double cpaTime = cpa.getTcpa(); + double hours = ceil(cpaTime); + double minutes = (cpaTime - hours) * 60.0; + addMenuItem("CPA", String.format("%.1f nm", cpa.getCpaDist())); + if (cpaTime > 0.0) { + if (hours >= 2.0) { + addMenuItem("TCPA", String.format("%.0f hours %.0f min", hours, minutes)); + } else if (hours >= 1.0) { + addMenuItem("TCPA", String.format("%.0f hour %.0f min", hours, minutes)); + } else { + addMenuItem("TCPA", String.format("%.0f min", minutes)); + } + } else { + addMenuItem("TCPA", String.format("%.1f hours", cpaTime)); + } + } + } + } + } private void addMenuItem(@NonNull String type, @Nullable String value) { if (value != null) { @@ -127,6 +162,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Bearing", String.format("%.1f", bearing)); } catch (Exception ignore) { } } + addCpaInfo(msgTypes, locationProvider); /* // test: addMenuItem("# loc", getOwnLocationAsString(locationProvider)); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index 03fa6e36725..228bf491934 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -39,6 +39,7 @@ public static class Cpa { private float cpaDist; // in miles private Location newPos1; // position of first object at time tcpa private Location newPos2; // position of first object at time tcpa + private boolean valid; public Cpa() { reset(); } @@ -47,6 +48,7 @@ public void reset() { tcpa = INVALID_TCPA; newPos1 = null; newPos2 = null; + valid = false; } public void setTcpa(double x) { this.tcpa = x; } public void setCpaDist(float x) { this.cpaDist = x; } @@ -56,6 +58,8 @@ public void reset() { public float getCpaDist() { return cpaDist; } public Location getCpaPos1() { return newPos1; } public Location getCpaPos2() { return newPos2; } + public void validate() { valid = true; } + public boolean isValid() { return valid; } } /* calculate the Time to Closest Point of Approach (TCPA) of two moving objects: @@ -67,8 +71,12 @@ private static double getTcpa(@NonNull Vector x, @NonNull Vector y, @NonNull Vector vx, @NonNull Vector vy, double lonCorrection) { Vector dx = new Vector( y.sub(x)); Vector dv = new Vector(vy.sub(vx)); - double divisor = dv.dot(dv); // TODO: check for Div/0 - return -(((dx.x * dv.x) + (dx.y * dv.y / lonCorrection)) / divisor); // TODO: check for Div/0 + double divisor = dv.dot(dv); + if ((Math.abs(divisor) < 1.0E-10f) || (lonCorrection < 1.0E-10f)) { + // avoid div by 0 or invalid lonCorrection + return INVALID_TCPA; + } + return -(((dx.x * dv.x) + (dx.y * dv.y / lonCorrection)) / divisor); } /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and y, @@ -77,7 +85,7 @@ private static double getTcpa(@NonNull Location x, @NonNull Location y, double l if (checkSpeedAndBearing(x, y)) { return INVALID_TCPA; } - if (lonCorrection < 0.001) { + if (lonCorrection < 1.0E-10f) { // in this case the lonCorrection is considered invalid -> new calculation lonCorrection = getLonCorrection(x); } @@ -113,8 +121,12 @@ private static Location getCpa(@NonNull Location x, @NonNull Location y, boolean return null; } double tcpa = getTcpa(x,y); - Location base = useFirstAsReference ? x : y; - return getNewPosition(base, tcpa); + if (tcpa == INVALID_TCPA) { + return null; + } else { + Location base = useFirstAsReference ? x : y; + return getNewPosition(base, tcpa); + } } /* to calculate the Closest Point of Approach (CPA) between the objects x and y, @@ -159,13 +171,16 @@ public static void getCpa(@NonNull Location loc1, @NonNull Location loc2, @NonNull Cpa result) { if (!checkSpeedAndBearing(loc1, loc2)) { double tcpa = getTcpa(loc1, loc2); - Location cpaX = getNewPosition(loc1, tcpa); - Location cpaY = getNewPosition(loc2, tcpa); - result.setTcpa(tcpa); - result.setCpaPos1(cpaX); - result.setCpaPos2(cpaY); - if ((cpaX != null) && (cpaY != null)) { - result.setCpaDist(meterToMiles(cpaX.distanceTo(cpaY))); + if (tcpa != INVALID_TCPA) { + Location cpaX = getNewPosition(loc1, tcpa); + Location cpaY = getNewPosition(loc2, tcpa); + result.setTcpa(tcpa); + result.setCpaPos1(cpaX); + result.setCpaPos2(cpaY); + if ((cpaX != null) && (cpaY != null)) { + result.setCpaDist(meterToMiles(cpaX.distanceTo(cpaY))); + result.validate(); + } } } } @@ -191,7 +206,8 @@ public static Location getNewPosition(@Nullable Location x, double time) { if (x != null) { if (x.hasBearing() && x.hasSpeed()) { LatLonPoint a = new LatLonPoint(x.getLatitude(), x.getLongitude()); - LatLonPoint b = a.getPoint(x.getSpeed() * time * Math.PI / 5556.0, bearingInRad(x.getBearing())); + LatLonPoint b = a.getPoint(x.getSpeed() * time * Math.PI / 5556.0, + bearingInRad(x.getBearing())); Location newX = new Location(x); newX.setLongitude(b.getLongitude()); newX.setLatitude(b.getLatitude()); @@ -209,7 +225,7 @@ public static Location getNewPosition(@Nullable Location x, double time) { private static double getLonCorrection(@Nullable Location loc) { if (loc != null) { Location x = new Location(loc); - // simulate a "measurement" trio towards East... + // simulate a "measurement" trip towards East... x.setSpeed(knotsToMeterPerSecond(1.0f)); // speed -> 1 kn x.setBearing(90.0f); // course -> east Location yEast = getNewPosition(x, 1.0); // new position after 1 hour diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index f4f86c046dd..a46f63f00b9 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -62,43 +62,22 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi initTimer(); startNetworkListener(); - initTestObjects(); // for test purposes: + // for test purposes: remove later... + initTestObjects(); + testCpa(); } - private void initTestObjects() { - // passenger ship - AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, - 320.0, 8.4, 50.738d, 7.099d, 0.0); - updateAisObjectList(ais); - ais = new AisObject(34568, 5, 0, "TEST-CALLSIGN1", "TEST-Ship", 60 /* passenger */, 56, - 65, 8, 12, 2, - "Potsdam", 8, 15, 22, 5); - updateAisObjectList(ais); - // sailing boat - ais = new AisObject(454011, 1, 20, 8, 0, 120, - 125.0, 4.4, 50.737d, 7.098d, 0.0); - updateAisObjectList(ais); - ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, - 0, 0, 0, 0, - "@@@", 0, 0, 0, 0); - updateAisObjectList(ais); - // land station - ais = new AisObject(878121, 4, 50.736d, 7.100d); - updateAisObjectList(ais); - // AIDS - ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, - 0, 0, 0, 0); - updateAisObjectList(ais); - // aircraft - ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); - updateAisObjectList(ais); - + private void testCpa() { // here some tests for the geo (CPA) calculation // define 3 (vessel) objects + // for coordinate transformation see https://www.koordinaten-umrechner.de Location x1 = new Location("test", 49.5d, -1.0d); // 49°30'N, 1°00'W Location x2 = new Location("test", 49.916667d, 0.416667d); // 49°55'N, 0°25'E Location x3 = new Location("test", 49.666667d, -0.75d); // 49°40'N, 0°45'W - Location y1, y2, y3; + Location x4 = new Location("test", 49.5d, -4.0d); // 49°30'N, 4°00'W + Location x5 = new Location("test", 50.0d, -3.75d); // 50°00'N, 3°45'W + // taken from marine chart: distances: x1 - x3: 13.8 nm, x2 - x3: 47,2 nm, x4 - x5: 31.4 nm + Location y1, y2, y3, y4, y5; Log.d("AisTrackerLayer", "# test0: position 1 after 0 hours: " + LocationConvert.convertLatitude(x1.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(x1.getLongitude(), FORMAT_MINUTES, true)); @@ -108,8 +87,17 @@ private void initTestObjects() { Log.d("AisTrackerLayer", "# test0: position 3 after 0 hours: " + LocationConvert.convertLatitude(x3.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(x3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 4 after 0 hours: " + + LocationConvert.convertLatitude(x4.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x4.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 5 after 0 hours: " + + LocationConvert.convertLatitude(x5.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x5.getLongitude(), FORMAT_MINUTES, true)); - // use case: x1: course 0°, speed 5kn, x3: course 270°, speed 10kn, time: 1h, 1.5h + // test case: x1: course 0°, speed 5kn, x3: course 270°, speed 10kn, time: 1h, 1.5h + // taken from marine chart: + // position after 1h: x1: 49°35'N, 1°00'W, x3: 49°40'N, 1°0.5'W, distance: 5.0nm + // position after 1.5h: x1: 49°37.5'N, 1°00'W, x3: 49°40'N, 1°8.5'W, distance: 6.0nm x1.setSpeed(knotsToMeterPerSecond(5.0f)); x1.setBearing(0.0f); x3.setSpeed(knotsToMeterPerSecond(10.0f)); @@ -124,7 +112,7 @@ private void initTestObjects() { if ((y1 != null) && (y3 != null)) { Log.d("AisTrackerLayer", "# test1: position 1 after 1 hour: " + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) - + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); Log.d("AisTrackerLayer", "# test1: position 3 after 1 hour: " + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); @@ -153,7 +141,11 @@ private void initTestObjects() { Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); } - // use case: x1: course 0°, speed 5kn, x3: course 270°, speed 5kn, time 1h, 1.5h, 2h + // test case: x1: course 0°, speed 5kn, x3: course 270°, speed 5kn, time 1h, 1.5h, 2h + // taken from marine chart: + // position after 1h: x1: 49°35'N, 1°00'W, x3: 49°40'N, 0°52.7'W, distance: 6.8nm + // position after 1.5h: x1: 49°37.5'N, 1°00'W, x3: 49°40'N, 0°56.7'W, distance: 3.1nm + // position after 2h: x1: 49°40'N, 1°00'W, x3: 49°40'N, 1°0.5'W, distance: 0.3nm x1.setSpeed(knotsToMeterPerSecond(5.0f)); x1.setBearing(0.0f); x3.setSpeed(knotsToMeterPerSecond(5.0f)); @@ -197,7 +189,9 @@ private void initTestObjects() { Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); } - // use case: x2: course 270°, speed 5kn, x3: course 45°, speed 5kn, time 5h + // test case: x2: course 270°, speed 5kn, x3: course 45°, speed 5kn, time 5h + // taken from marine chart: + // position after 5h: x2: 49°55'N, 0°14.1'W, x3: 49°57.8'N, 0°17.5'W, distance: 3.5nm x2.setSpeed(knotsToMeterPerSecond(5.0f)); x2.setBearing(270.0f); x3.setSpeed(knotsToMeterPerSecond(5.0f)); @@ -219,6 +213,58 @@ private void initTestObjects() { Log.d("AisTrackerLayer", "# test3: dist1: " + meterToMiles(y2.distanceTo(y3))); } + // test case: x4: course 45°, speed 10kn, x5: course 70°, speed 5kn, time 6h + // taken from marine chart: + // position after 6h: x4: 50°12.1'N, 2°54.4'W, x5: 50°10.1'N, 3°1.5'W, distance: 5nm + x4.setSpeed(knotsToMeterPerSecond(10.0f)); + x4.setBearing(45.0f); + x5.setSpeed(knotsToMeterPerSecond(5.0f)); + x5.setBearing(70.0f); + cpa1.reset(); + getCpa(x4, x5, cpa1); + Log.d("AisTrackerLayer", "# test4: tcpa(x4, x5): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test4: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test4: dist0: " + meterToMiles(x4.distanceTo(x5))); + y4 = AisTrackerHelper.getNewPosition(x4, 6.0); + y5 = AisTrackerHelper.getNewPosition(x5, 6.0); + if ((y4 != null) && (y5 != null)) { + Log.d("AisTrackerLayer", "# test4: position 4 after 6 hours: " + + LocationConvert.convertLatitude(y4.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y4.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test4: position 5 after 6 hours: " + + LocationConvert.convertLatitude(y5.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y5.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test4: dist1: " + meterToMiles(y4.distanceTo(y5))); + } + } + private void initTestObjects() { + // passenger ship + AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, + 320.0, 8.4, 50.738d, 7.099d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(34568, 5, 0, "TEST-CALLSIGN1", "TEST-Ship", 60 /* passenger */, 56, + 65, 8, 12, 2, + "Potsdam", 8, 15, 22, 5); + updateAisObjectList(ais); + // sailing boat + ais = new AisObject(454011, 1, 20, 8, 0, 120, + 125.0, 4.4, 50.737d, 7.098d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, + 0, 0, 0, 0, + "@@@", 0, 0, 0, 0); + updateAisObjectList(ais); + // land station + ais = new AisObject(878121, 4, 50.736d, 7.100d); + updateAisObjectList(ais); + // AIDS + ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, + 0, 0, 0, 0); + updateAisObjectList(ais); + // aircraft + ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); + updateAisObjectList(ais); + //removeOldestAisObjectListEntry(); //removeLostAisObjects(); } From 7660bbb1405519de4f3e06e0c4a2f804c5b94d54 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 2 Jul 2024 21:05:25 +0200 Subject: [PATCH 49/74] some code refactoring --- .../plus/plugins/aistracker/AisObject.java | 33 +++----- .../aistracker/AisObjectMenuController.java | 17 ++-- .../plugins/aistracker/AisTrackerHelper.java | 78 +++++-------------- 3 files changed, 42 insertions(+), 86 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index a8af8913ee7..e6169e44f28 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -44,9 +44,7 @@ import net.osmand.Location; import net.osmand.data.LatLon; import net.osmand.data.RotatedTileBox; -import net.osmand.plus.OsmAndLocationProvider; import net.osmand.plus.R; -import net.osmand.util.MapUtils; import java.util.SortedSet; import java.util.TreeSet; @@ -821,26 +819,19 @@ public String getAidTypeString() { return(Integer.toString(ais_aidType)); } } - private float getDistanceOrBearing(@Nullable OsmAndLocationProvider locationProvider, - boolean needBearing) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - Location aisLocation = getLocation(); - if ((myLocation != null) && (aisLocation != null)) { - return needBearing ? myLocation.bearingTo(aisLocation) : myLocation.distanceTo(aisLocation); - } else { - Log.e("AisObject", "getDistanceOrBearing(): mylocation -> " + myLocation + - ", aisLocation -> " + aisLocation); - return -500.0f; // invalid - } + private float getDistanceOrBearing(@Nullable Location ownLocation, boolean needBearing) { + Location aisLocation = getLocation(); + if ((ownLocation != null) && (aisLocation != null)) { + return needBearing ? ownLocation.bearingTo(aisLocation) : ownLocation.distanceTo(aisLocation); } else { - Log.e("AisObject", "getDistanceOrBearing(): locationProvider -> null"); + Log.e("AisObject", "getDistanceOrBearing(): ownLocation -> " + ownLocation + + ", aisLocation -> " + aisLocation); return -500.0f; // invalid } } /* get bearing from own position to the position of the AIS object */ - public float getBearing(@Nullable OsmAndLocationProvider locationProvider) { - float bearing = getDistanceOrBearing(locationProvider, true); + public float getBearing(@Nullable Location ownLocation) { + float bearing = getDistanceOrBearing(ownLocation, true); if ((bearing < 0.0f) && (bearing > -200.0f)) { while (bearing < 0.0f) { bearing += 360.0f; @@ -849,11 +840,11 @@ public float getBearing(@Nullable OsmAndLocationProvider locationProvider) { return bearing; } /* get distance from own position to the position of the AIS object in meters */ - public float getDistanceInMeters(@Nullable OsmAndLocationProvider locationProvider) { - return getDistanceOrBearing(locationProvider, false); + public float getDistanceInMeters(@Nullable Location ownLocation) { + return getDistanceOrBearing(ownLocation, false); } - public float getDistanceInNauticalMiles(@Nullable OsmAndLocationProvider locationProvider) { - float dist = getDistanceInMeters(locationProvider); + public float getDistanceInNauticalMiles(@Nullable Location ownLocation) { + float dist = getDistanceInMeters(ownLocation); if (dist >= 0.0f) { dist = dist / 1852; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 1b2d5a136d9..0e78b207ea8 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -71,8 +71,7 @@ private String getOwnLocationAsString(@Nullable OsmAndLocationProvider locationP } */ @SuppressLint("DefaultLocale") - private void addCpaInfo(@NonNull SortedSet msgTypes, - @Nullable OsmAndLocationProvider locationProvider) { + private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet msgTypes) { if (msgTypes.contains(21) || msgTypes.contains(9)) { return; } @@ -80,8 +79,8 @@ private void addCpaInfo(@NonNull SortedSet msgTypes, (aisObject.getSog() != AisObjectConstants.INVALID_SOG)) { AisTrackerHelper.Cpa cpa = new AisTrackerHelper.Cpa(); Location aisLocation = aisObject.getLocation(); - if (aisLocation != null) { - getCpa(aisLocation, locationProvider, cpa); + if ((aisLocation != null) && (myLocation != null)) { + getCpa(myLocation, aisLocation, cpa); if (cpa.isValid()) { double cpaTime = cpa.getTcpa(); double hours = ceil(cpaTime); @@ -150,8 +149,12 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { OsmAndLocationProvider locationProvider = app.getLocationProvider(); - float distance = aisObject.getDistanceInNauticalMiles(locationProvider); - float bearing = aisObject.getBearing(locationProvider); + Location ownLocation = null; + if (locationProvider != null) { + ownLocation = locationProvider.getLastKnownLocation(); + } + float distance = aisObject.getDistanceInNauticalMiles(ownLocation); + float bearing = aisObject.getBearing(ownLocation); if (distance >= 0.0f) { try { addMenuItem("Distance", String.format("%.1f nm", distance)); @@ -162,7 +165,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Bearing", String.format("%.1f", bearing)); } catch (Exception ignore) { } } - addCpaInfo(msgTypes, locationProvider); + addCpaInfo(ownLocation, msgTypes); /* // test: addMenuItem("# loc", getOwnLocationAsString(locationProvider)); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index 228bf491934..2acb79af94c 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -85,34 +85,18 @@ private static double getTcpa(@NonNull Location x, @NonNull Location y, double l if (checkSpeedAndBearing(x, y)) { return INVALID_TCPA; } - if (lonCorrection < 1.0E-10f) { - // in this case the lonCorrection is considered invalid -> new calculation - lonCorrection = getLonCorrection(x); - } return getTcpa(locationToVector(x), locationToVector(y), courseToVector(x.getBearing(), getSpeedInKnots(x)), courseToVector(y.getBearing(), getSpeedInKnots(y)), lonCorrection); } - public static double getTcpa(@NonNull Location x, @NonNull Location y) { - return getTcpa(x, y, 0.0d); - } - - /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and own location, - * it is presumed that x contains its position, speed and course */ - public static double getTcpa(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - long now = System.currentTimeMillis(); - if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { - lastCorrectionUpdate = now; - correctionFactor = getLonCorrection(myLocation); - } - return getTcpa(x, myLocation, correctionFactor); - } + public static double getTcpa(@NonNull Location ownLocation, @NonNull Location otherLocation) { + long now = System.currentTimeMillis(); + if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { + correctionFactor = getLonCorrection(ownLocation); + lastCorrectionUpdate = now; } - return INVALID_TCPA; + return getTcpa(ownLocation, otherLocation, correctionFactor); } @Nullable @@ -156,24 +140,13 @@ public static float getCpaDistance(@NonNull Location x, @NonNull Location y) { } } - /* caluclate the distance between the given object and own position at their Closest Point of Approach (CPA) */ - public static float getCpaDistance(@NonNull Location x, @Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - return getCpaDistance(x, myLocation); - } - } - return INVALID_CPA; - } - - public static void getCpa(@NonNull Location loc1, @NonNull Location loc2, + public static void getCpa(@NonNull Location ownLocation, @NonNull Location otherLocation, @NonNull Cpa result) { - if (!checkSpeedAndBearing(loc1, loc2)) { - double tcpa = getTcpa(loc1, loc2); + if (!checkSpeedAndBearing(ownLocation, otherLocation)) { + double tcpa = getTcpa(ownLocation, otherLocation); if (tcpa != INVALID_TCPA) { - Location cpaX = getNewPosition(loc1, tcpa); - Location cpaY = getNewPosition(loc2, tcpa); + Location cpaX = getNewPosition(ownLocation, tcpa); + Location cpaY = getNewPosition(otherLocation, tcpa); result.setTcpa(tcpa); result.setCpaPos1(cpaX); result.setCpaPos2(cpaY); @@ -185,16 +158,6 @@ public static void getCpa(@NonNull Location loc1, @NonNull Location loc2, } } - public static void getCpa(@NonNull Location loc, @Nullable OsmAndLocationProvider locationProvider, - @NonNull Cpa result) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - getCpa(myLocation, loc, result); - } - } - } - private static double bearingInRad(float bearingInDegrees) { double res = bearingInDegrees * 2 * Math.PI / 360.0; while (res >= Math.PI) { res -= (2 * Math.PI); } @@ -202,19 +165,19 @@ private static double bearingInRad(float bearingInDegrees) { } @Nullable - public static Location getNewPosition(@Nullable Location x, double time) { - if (x != null) { - if (x.hasBearing() && x.hasSpeed()) { - LatLonPoint a = new LatLonPoint(x.getLatitude(), x.getLongitude()); - LatLonPoint b = a.getPoint(x.getSpeed() * time * Math.PI / 5556.0, - bearingInRad(x.getBearing())); - Location newX = new Location(x); + public static Location getNewPosition(@Nullable Location loc, double time) { + if (loc != null) { + if (loc.hasBearing() && loc.hasSpeed()) { + LatLonPoint a = new LatLonPoint(loc.getLatitude(), loc.getLongitude()); + LatLonPoint b = a.getPoint(loc.getSpeed() * time * Math.PI / 5556.0, + bearingInRad(loc.getBearing())); + Location newX = new Location(loc); newX.setLongitude(b.getLongitude()); newX.setLatitude(b.getLatitude()); return newX; } else { - Log.d("AisTrackerHelper", "getNewPosition(): y.hasBearing->" - + x.hasBearing() + ", x.hasSpeed->" + x.hasSpeed()); + Log.d("AisTrackerHelper", "getNewPosition(): loc.hasBearing->" + + loc.hasBearing() + ", loc.hasSpeed->" + loc.hasSpeed()); return null; } } else { @@ -244,7 +207,6 @@ public static float knotsToMeterPerSecond(float speed) { public static float meterPerSecondToKnots(float speed) { return speed * 3600 / 1852; } - public static float meterToMiles(float x) { return x / 1852.0f; } From fa213854941b14abcd2cc1467f9a498c33b29fc0 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 2 Jul 2024 23:14:45 +0200 Subject: [PATCH 50/74] additional preferences regarding CPA (not used in the application yet) --- OsmAnd/res/values/strings.xml | 9 +++- OsmAnd/res/xml/ais_settings.xml | 22 ++++++++++ .../aistracker/AisObjectMenuController.java | 18 +------- .../plugins/aistracker/AisTrackerPlugin.java | 19 +++++--- .../AisTrackerSettingsFragment.java | 44 +++++++++++++++++-- 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index 0af5b7d918b..e4be8d1f530 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -1144,11 +1144,18 @@ You need to activate the sensor so OsmAnd can find it. Define TCP port number of the NMEA data source UDP port of local NMEA data receiver Define UPD port where OsmAnd receives NMEA data + Timeout settings for AIS signal reception + Set timeout values to identify lost AIS objects if no signal was received for a specific time. Timeout for visibility when object is lost Set Timeout for visibility of AIS objects: After this time without signal reception, the AIS object will be removed from screen. Timeout for ship visibility when no signal received Set timeout for ship visibility: After this time without signal reception, the ship symbol will change its state on screen: It will be crossed out. - + Settings related to CPA + These values define the presentation of AIS objects that may come too close to the own position. + Warning time to reach the Closest Point of Approach (CPA) + If the TCPA (time to reach the CPA with another vessel) is less than this value, the vessel is marked with red color. + Warning distance for the Closes Point of Approach (CPA) + Vessels are marked with red color if the CPA is less than this value and the CPA is reached in the near future (see setting "Warning time to reach the CPA"). Weather Explore Weather forecast. Contours diff --git a/OsmAnd/res/xml/ais_settings.xml b/OsmAnd/res/xml/ais_settings.xml index ebc1d52da1f..0498714b169 100644 --- a/OsmAnd/res/xml/ais_settings.xml +++ b/OsmAnd/res/xml/ais_settings.xml @@ -33,6 +33,11 @@ android:title="@string/ais_port_nmea_local" tools:summary="@string/ais_port_nmea_local_description" /> + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 0e78b207ea8..fe81c96e945 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -59,17 +59,7 @@ private float getOwnBearing(@Nullable OsmAndLocationProvider locationProvider) { } return 0.0f; } - /* - private String getOwnLocationAsString(@Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - return myLocation.toString(); - } - } - return null; - } - */ + @SuppressLint("DefaultLocale") private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet msgTypes) { if (msgTypes.contains(21) || msgTypes.contains(9)) { @@ -166,12 +156,6 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, } catch (Exception ignore) { } } addCpaInfo(ownLocation, msgTypes); - /* - // test: - addMenuItem("# loc", getOwnLocationAsString(locationProvider)); - addMenuItem("# ownSpeed", Float.toString(getOwnSpeed(locationProvider))); - addMenuItem("# ownBearing", Float.toString(getOwnBearing(locationProvider))); - */ } } if (msgTypes.contains(21)) { // ATON (aid to navigation) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index 2d92fe4c4cb..a0f1983c768 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -31,28 +31,33 @@ public class AisTrackerPlugin extends OsmandPlugin { private AisTrackerLayer aisTrackerLayer = null; - public static final String COMPONENT = "net.osmand.aistrackerPlugin"; + private static final String COMPONENT = "net.osmand.aistrackerPlugin"; public static final String AISTRACKER_ID = "osmand.aistracker"; - public static final String AIS_NMEA_PROTOCOL_ID = "ais_nmea_protocol"; // see xml/ais_settings.xml public static final String AIS_NMEA_IP_ADDRESS_ID = "ais_address_nmea_server"; // see xml/ais_settings.xml public static final String AIS_NMEA_TCP_PORT_ID = "ais_port_nmea_server"; // see xml/ais_settings.xml public static final String AIS_NMEA_UDP_PORT_ID = "ais_port_nmea_local"; // see xml/ais_settings.xml public static final String AIS_OBJ_LOST_TIMEOUT_ID = "ais_object_lost_timeout"; // see xml/ais_settings.xml public static final String AIS_SHIP_LOST_TIMEOUT_ID = "ais_ship_lost_timeout"; // see xml/ais_settings.xml + public static final String AIS_CPA_WARNING_TIME_ID = "ais_cpa_warning_time"; // see xml/ais_settings.xml + public static final String AIS_CPA_WARNING_DISTANCE_ID = "ais_cpa_warning_distance"; // see xml/ais_settings.xml public final CommonPreference AIS_NMEA_PROTOCOL; public static final int AIS_NMEA_PROTOCOL_UDP = 0; public static final int AIS_NMEA_PROTOCOL_TCP = 1; public final CommonPreference AIS_NMEA_IP_ADDRESS; private static final String AIS_NMEA_DEFAULT_IP = "192.168.200.16"; public final CommonPreference AIS_NMEA_TCP_PORT; - public static final Integer AIS_NMEA_DEFAULT_TCP_PORT = 4001; + private static final Integer AIS_NMEA_DEFAULT_TCP_PORT = 4001; public final CommonPreference AIS_NMEA_UDP_PORT; - public static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; + private static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; public final CommonPreference AIS_OBJ_LOST_TIMEOUT; - public static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; + private static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; public final CommonPreference AIS_SHIP_LOST_TIMEOUT; - public static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; + private static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; + public final CommonPreference AIS_CPA_WARNING_TIME; + private static final Integer AIS_CPA_DEFAULT_WARNING_TIME = 0; + public final CommonPreference AIS_CPA_WARNING_DISTANCE; + private static final Float AIS_CPA_WARNING_DEFAULT_DISTANCE = 1.0f; public AisTrackerPlugin(OsmandApplication app) { super(app); @@ -63,6 +68,8 @@ public AisTrackerPlugin(OsmandApplication app) { AIS_NMEA_UDP_PORT = registerIntPreference(AIS_NMEA_UDP_PORT_ID, AIS_NMEA_DEFAULT_UDP_PORT); AIS_OBJ_LOST_TIMEOUT = registerIntPreference(AIS_OBJ_LOST_TIMEOUT_ID, AIS_OBJ_LOST_DEFAULT_TIMEOUT); AIS_SHIP_LOST_TIMEOUT = registerIntPreference(AIS_SHIP_LOST_TIMEOUT_ID, AIS_SHIP_LOST_DEFAULT_TIMEOUT); + AIS_CPA_WARNING_TIME = registerIntPreference(AIS_CPA_WARNING_TIME_ID, AIS_CPA_DEFAULT_WARNING_TIME); + AIS_CPA_WARNING_DISTANCE = registerFloatPreference(AIS_CPA_WARNING_DISTANCE_ID, AIS_CPA_WARNING_DEFAULT_DISTANCE); Log.d("AisTrackerPlugin", "constructor"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 152f3a92082..f5980905b81 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -3,10 +3,11 @@ import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP; +import static java.lang.Math.ceil; + +import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; -import android.text.SpannableStringBuilder; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -36,12 +37,15 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Override protected void setupPreferences() { int currentProtocol; + boolean cpaWarningEnabled; currentProtocol = setupProtocol(); setupIpAddress(currentProtocol); setupTcpPort(currentProtocol); setupUdpPort(currentProtocol); setupObjectLostTimeout(); setupShipLostTimeout(); + cpaWarningEnabled = setupCpaWarningTime(); + setupCpaWarningDistance(cpaWarningEnabled); } private int setupProtocol() { @@ -57,7 +61,6 @@ private int setupProtocol() { } return 0; } - private void setupIpAddress(int currentProtocol) { EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); if (aisNmeaIpAddress != null) { @@ -126,6 +129,41 @@ private void setupShipLostTimeout() { objectLostTimeout.setDescription(R.string.ais_ship_lost_timeout_description); } } + private boolean setupCpaWarningTime() { + Integer[] entryValues = {0, 1, 5, 10, 20, 30, 60}; + String[] entries = new String[entryValues.length]; + entries[0] = "disabled"; + for (int i = 1; i < entryValues.length; i++) { + entries[i] = entryValues[i] + " "; + entries[i] += entryValues[i].equals(1) ? "minute" : "minutes"; // TODO: move to ressource file + } + ListPreferenceEx cpaWarningTime = findPreference(plugin.AIS_CPA_WARNING_TIME.getId()); + if (cpaWarningTime != null) { + cpaWarningTime.setEntries(entries); + cpaWarningTime.setEntryValues(entryValues); + cpaWarningTime.setDescription(R.string.ais_cpa_warning_time_description); + return !cpaWarningTime.getValue().equals(0); + } + return false; + } + @SuppressLint("DefaultLocale") + private void setupCpaWarningDistance(boolean enabled) { + Float[] entryValues = {0.5f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + String[] entries = new String[entryValues.length]; + for (int i = 0; i < entryValues.length; i++) { + entries[i] = (ceil(entryValues[i]) == entryValues[i]) ? + String.format("%.0f ", entryValues[i]) : String.format("%.1f ", entryValues[i]); + entries[i] += entryValues[i].equals(1.0f) ? "nautical mile" : "nautical miles"; // TODO: move to ressource file + } + ListPreferenceEx cpaWarningDistance = findPreference(plugin.AIS_CPA_WARNING_DISTANCE.getId()); + if (cpaWarningDistance != null) { + cpaWarningDistance.setEntries(entries); + cpaWarningDistance.setEntryValues(entryValues); + cpaWarningDistance.setDescription(R.string.ais_cpa_warning_distance_description); + cpaWarningDistance.setEnabled(enabled); + } + } + @Override public boolean onPreferenceChange(Preference preference, Object newValue) { boolean restartNetworkListener = false; From 92eb0dc3430905228b3aa04bb314ce34ae5e1e7e Mon Sep 17 00:00:00 2001 From: Falk Date: Wed, 3 Jul 2024 00:03:51 +0200 Subject: [PATCH 51/74] code refactoring regarding maxObjectAgeInMinutes, vesselLostTimeoutInMinutes --- .../plus/plugins/aistracker/AisObject.java | 34 +++++++++++-------- .../plugins/aistracker/AisTrackerLayer.java | 8 ++--- .../plugins/aistracker/AisTrackerPlugin.java | 4 +-- .../AisTrackerSettingsFragment.java | 10 ++++++ 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index e6169e44f28..ca4ff3086e3 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -30,6 +30,8 @@ import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_OBJ_LOST_DEFAULT_TIMEOUT; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_SHIP_LOST_DEFAULT_TIMEOUT; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -82,8 +84,10 @@ public class AisObject { private String countryCode = null; private SortedSet msgTypes = null; private long lastUpdate = 0; - /* after this time the object is outdated and can be removed: */ - + /* after this time of missing AIS signal the object is outdated and can be removed: */ + private static int maxObjectAgeInMinutes = AIS_OBJ_LOST_DEFAULT_TIMEOUT; + /* after this time of missing AIS signal the vessel symbol can change to mark "lost": */ + private static int vesselLostTimeoutInMinutes = AIS_SHIP_LOST_DEFAULT_TIMEOUT; private AisObjType objectClass; private Bitmap bitmap = null; private int bitmapColor; @@ -368,8 +372,8 @@ public void set(@NonNull AisObject ais) { this.bitmapColor = 0; } - private void setBitmap(@NonNull AisTrackerLayer mapLayer, int maxAgeInMin) { - if (isLost(maxAgeInMin)) { + private void setBitmap(@NonNull AisTrackerLayer mapLayer) { + if (isLost(vesselLostTimeoutInMinutes)) { if (isMovable()) { this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); } @@ -401,11 +405,11 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer, int maxAgeInMin) { break; } } - this.setColor(maxAgeInMin); + this.setColor(); } - private void setColor(int maxAgeInMin) { - if (isLost(maxAgeInMin)) { + private void setColor() { + if (isLost(vesselLostTimeoutInMinutes)) { if (isMovable()) { this.bitmapColor = 0; // black } @@ -436,10 +440,9 @@ private void setColor(int maxAgeInMin) { } public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, - @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox, - int maxAgeInMin) { - if ((this.bitmap == null) || isLost(maxAgeInMin)) { - this.setBitmap(mapLayer, maxAgeInMin); + @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { + if ((this.bitmap == null) || isLost(vesselLostTimeoutInMinutes)) { + this.setBitmap(mapLayer); } if (this.bitmapColor != 0) { paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); @@ -461,7 +464,7 @@ public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, canvas.rotate(rotation, locationX, locationY); } canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); - if ((speedFactor > 0) && (!isLost(maxAgeInMin))) { + if ((speedFactor > 0) && (!isLost(vesselLostTimeoutInMinutes))) { float lineStartX = locationX; float lineLength = (float)this.bitmap.getHeight() * speedFactor; float lineStartY = locationY - this.bitmap.getHeight() / 4.0f; @@ -515,13 +518,14 @@ private boolean needRotation() { private boolean isLost(int maxAgeInMin) { return ((System.currentTimeMillis() - this.lastUpdate) / 1000 / 60) > maxAgeInMin; } - + public static void setMaxObjectAge(int timeInMinutes) { maxObjectAgeInMinutes = timeInMinutes; } + public static void setVesselLostTimeout(int timeInMinutes) { vesselLostTimeoutInMinutes = timeInMinutes; } /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed * */ - public boolean checkObjectAge(int maxAgeInMinutes) { - return isLost(maxAgeInMinutes); + public boolean checkObjectAge() { + return isLost(maxObjectAgeInMinutes); } public int getMsgType() { return this.ais_msgType; } public SortedSet getMsgTypes() { return this.msgTypes; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index a46f63f00b9..30da3e1a6f1 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -310,15 +310,14 @@ public void cleanup() { stopNetworkListener(); } private void removeLostAisObjects() { - int maxAge = plugin.AIS_OBJ_LOST_TIMEOUT.get(); for (Iterator> iterator = aisObjectList.entrySet().iterator(); iterator.hasNext(); ) { Map.Entry entry = iterator.next(); - if (entry.getValue().checkObjectAge(maxAge)) { + if (entry.getValue().checkObjectAge()) { Log.d("AisTrackerLayer", "remove AIS object with MMSI " + entry.getValue().getMmsi()); iterator.remove(); } } - // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge(maxAge)); + // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge()); } private void removeOldestAisObjectListEntry() { Log.d("AisTrackerLayer", "removeOldestAisObjectListEntry() called"); @@ -369,10 +368,9 @@ public boolean isLocationVisible(RotatedTileBox tileBox, LatLon coordinates) { @Override public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { - int maxAgeInMin = plugin.AIS_SHIP_LOST_TIMEOUT.get(); for (AisObject ais : aisObjectList.values()) { if (isLocationVisible(tileBox, ais.getPosition())) { - ais.draw(this, bitmapPaint, canvas, tileBox, maxAgeInMin); + ais.draw(this, bitmapPaint, canvas, tileBox); } } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index a0f1983c768..28944481736 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -51,9 +51,9 @@ public class AisTrackerPlugin extends OsmandPlugin { public final CommonPreference AIS_NMEA_UDP_PORT; private static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; public final CommonPreference AIS_OBJ_LOST_TIMEOUT; - private static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; + public static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; public final CommonPreference AIS_SHIP_LOST_TIMEOUT; - private static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; + public static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; public final CommonPreference AIS_CPA_WARNING_TIME; private static final Integer AIS_CPA_DEFAULT_WARNING_TIME = 0; public final CommonPreference AIS_CPA_WARNING_DISTANCE; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index f5980905b81..87844758a01 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -112,6 +112,7 @@ private void setupObjectLostTimeout() { objectLostTimeout.setEntries(entries); objectLostTimeout.setEntryValues(entryValues); objectLostTimeout.setDescription(R.string.ais_object_lost_timeout_description); + AisObject.setMaxObjectAge((Integer) objectLostTimeout.getValue()); } } private void setupShipLostTimeout() { @@ -127,6 +128,7 @@ private void setupShipLostTimeout() { objectLostTimeout.setEntries(entries); objectLostTimeout.setEntryValues(entryValues); objectLostTimeout.setDescription(R.string.ais_ship_lost_timeout_description); + AisObject.setVesselLostTimeout((Integer) objectLostTimeout.getValue()); } } private boolean setupCpaWarningTime() { @@ -180,6 +182,14 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { return false; } restartNetworkListener = true; + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_OBJ_LOST_TIMEOUT_ID)) { + if (newValue instanceof Integer) { + AisObject.setMaxObjectAge((Integer) newValue); + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_SHIP_LOST_TIMEOUT_ID)) { + if (newValue instanceof Integer) { + AisObject.setVesselLostTimeout((Integer) newValue); + } } boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); From 5ac23cfa691da8db84fc48b9456a189aa116b73b Mon Sep 17 00:00:00 2001 From: Falk Date: Thu, 4 Jul 2024 23:15:42 +0200 Subject: [PATCH 52/74] add CPA check into vessel visualisation --- .../plus/plugins/aistracker/AisObject.java | 83 ++++++++++++++++--- .../aistracker/AisObjectConstants.java | 2 +- .../aistracker/AisObjectMenuController.java | 43 +++++----- .../plugins/aistracker/AisTrackerHelper.java | 9 +- .../plugins/aistracker/AisTrackerLayer.java | 6 +- .../plugins/aistracker/AisTrackerPlugin.java | 4 +- .../AisTrackerSettingsFragment.java | 10 ++- 7 files changed, 111 insertions(+), 46 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index ca4ff3086e3..e29e06f6d86 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -30,6 +30,10 @@ import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.CPA_UPDATE_TIMEOUT_IN_SECONDS; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_DEFAULT_WARNING_TIME; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_WARNING_DEFAULT_DISTANCE; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_OBJ_LOST_DEFAULT_TIMEOUT; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_SHIP_LOST_DEFAULT_TIMEOUT; @@ -88,9 +92,14 @@ public class AisObject { private static int maxObjectAgeInMinutes = AIS_OBJ_LOST_DEFAULT_TIMEOUT; /* after this time of missing AIS signal the vessel symbol can change to mark "lost": */ private static int vesselLostTimeoutInMinutes = AIS_SHIP_LOST_DEFAULT_TIMEOUT; + private static int cpaWarningTime = AIS_CPA_DEFAULT_WARNING_TIME; // in minutes + private static float cpaWarningDistance = AIS_CPA_WARNING_DEFAULT_DISTANCE; // in miles + private static Location ownPosition = null; // used to calculate distances, CPA etc. private AisObjType objectClass; private Bitmap bitmap = null; private int bitmapColor; + private AisTrackerHelper.Cpa cpa; + private long lastCpaUpdate = 0; public AisObject(int mmsi, int msgType, double lat, double lon) { initObj(mmsi, msgType); @@ -185,6 +194,7 @@ private String getCountryCode(Integer mmsi) { /* to be called only by a contructor! */ private void initObj(int mmsi, int msgType) { this.msgTypes = new TreeSet<>(); + this.cpa = new AisTrackerHelper.Cpa(); this.ais_mmsi = mmsi; this.ais_msgType = msgType; this.countryCode = getCountryCode(this.ais_mmsi); @@ -366,8 +376,10 @@ public void set(@NonNull AisObject ais) { this.msgTypes = new TreeSet<>(); } this.msgTypes.add(ais_msgType); + if (this.cpa == null) { + cpa = new AisTrackerHelper.Cpa(); + } this.initObjectClass(); - //this.objectClass = ais.getObjectClass(); // test only... remove later this.bitmap = null; this.bitmapColor = 0; } @@ -442,7 +454,12 @@ private void setColor() { public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { if ((this.bitmap == null) || isLost(vesselLostTimeoutInMinutes)) { - this.setBitmap(mapLayer); + setBitmap(mapLayer); + } + if (checkCpaWarning()) { + activateCpaWarning(); + } else { + deactivateCpaWarning(); } if (this.bitmapColor != 0) { paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); @@ -515,11 +532,53 @@ private boolean needRotation() { return false; } + /* return true if the vessel gets too close with the own position in the future + * (danger of collusion) */ + private boolean checkCpaWarning() { + if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0)) { + if (checkForCpaTimeout() && (ownPosition != null)) { + Location aisPosition = getLocation(); + if (aisPosition != null) { + getCpa(ownPosition, getLocation(), cpa); + lastCpaUpdate = System.currentTimeMillis(); + } + } + if (cpa.isValid()) { + double tcpa = cpa.getTcpa(); + if (tcpa > 0.0f) { + return ((tcpa * 60.0d) <= cpaWarningTime) && (cpa.getCpaDist() <= cpaWarningDistance); + } + } + } + return false; + } + + private void activateCpaWarning() { + bitmapColor = Color.RED; + } + private void deactivateCpaWarning() { + if (bitmapColor == Color.RED) { + setColor(); + } + } private boolean isLost(int maxAgeInMin) { return ((System.currentTimeMillis() - this.lastUpdate) / 1000 / 60) > maxAgeInMin; } + private boolean checkForCpaTimeout() { + return ((System.currentTimeMillis() - this.lastCpaUpdate) / 1000) > CPA_UPDATE_TIMEOUT_IN_SECONDS; + } public static void setMaxObjectAge(int timeInMinutes) { maxObjectAgeInMinutes = timeInMinutes; } public static void setVesselLostTimeout(int timeInMinutes) { vesselLostTimeoutInMinutes = timeInMinutes; } + public static void setCpaWarningTime(int warningTime) { cpaWarningTime = warningTime; } + public static void setCpaWarningDistance(float warningDistance) { cpaWarningDistance = warningDistance; } + //public static void setOwnPosition(Location position) { ownPosition = position; } + public static void setOwnPosition(Location position) { + ownPosition = position; + if (ownPosition != null) { + ownPosition.setBearing(180.0f); // test + ownPosition.setSpeed(0.1f); // test (m/s) + } + } /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed @@ -823,19 +882,19 @@ public String getAidTypeString() { return(Integer.toString(ais_aidType)); } } - private float getDistanceOrBearing(@Nullable Location ownLocation, boolean needBearing) { + private float getDistanceOrBearing(boolean needBearing) { Location aisLocation = getLocation(); - if ((ownLocation != null) && (aisLocation != null)) { - return needBearing ? ownLocation.bearingTo(aisLocation) : ownLocation.distanceTo(aisLocation); + if ((ownPosition != null) && (aisLocation != null)) { + return needBearing ? ownPosition.bearingTo(aisLocation) : ownPosition.distanceTo(aisLocation); } else { - Log.e("AisObject", "getDistanceOrBearing(): ownLocation -> " + ownLocation + + Log.e("AisObject", "getDistanceOrBearing(): ownLocation -> " + ownPosition + ", aisLocation -> " + aisLocation); return -500.0f; // invalid } } /* get bearing from own position to the position of the AIS object */ - public float getBearing(@Nullable Location ownLocation) { - float bearing = getDistanceOrBearing(ownLocation, true); + public float getBearing() { + float bearing = getDistanceOrBearing(true); if ((bearing < 0.0f) && (bearing > -200.0f)) { while (bearing < 0.0f) { bearing += 360.0f; @@ -844,11 +903,11 @@ public float getBearing(@Nullable Location ownLocation) { return bearing; } /* get distance from own position to the position of the AIS object in meters */ - public float getDistanceInMeters(@Nullable Location ownLocation) { - return getDistanceOrBearing(ownLocation, false); + public float getDistanceInMeters() { + return getDistanceOrBearing(false); } - public float getDistanceInNauticalMiles(@Nullable Location ownLocation) { - float dist = getDistanceInMeters(ownLocation); + public float getDistanceInNauticalMiles() { + float dist = getDistanceInMeters(); if (dist >= 0.0f) { dist = dist / 1852; } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index c272664d567..afb42fa526d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -22,7 +22,7 @@ public final class AisObjectConstants { public final static double INVALID_DRAUGHT = 0.0; public final static double INVALID_TCPA = -10000.0d; public final static float INVALID_CPA = -1.0f; - + public final static int CPA_UPDATE_TIMEOUT_IN_SECONDS = 10; public static enum AisObjType { AIS_VESSEL, diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index fe81c96e945..ddfc32a1fd8 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -6,6 +6,7 @@ import static java.lang.Math.ceil; import android.annotation.SuppressLint; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -73,19 +74,24 @@ private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet 0.0) { - if (hours >= 2.0) { - addMenuItem("TCPA", String.format("%.0f hours %.0f min", hours, minutes)); - } else if (hours >= 1.0) { - addMenuItem("TCPA", String.format("%.0f hour %.0f min", hours, minutes)); - } else { - addMenuItem("TCPA", String.format("%.0f min", minutes)); + boolean isPositive = cpaTime >= 0; + cpaTime = Math.abs(cpaTime); + if (cpaTime < Long.MAX_VALUE) { + if (isPositive) { + long hours = (long)cpaTime; + double minutes = (cpaTime % 1 - hours) * 60.0; + addMenuItem("CPA", String.format("%.1f nm", cpa.getCpaDist())); + if (hours >= 2.0) { + addMenuItem("TCPA", String.format("%d hours %.0f min", hours, minutes)); + } else if (hours >= 1.0) { + addMenuItem("TCPA", String.format("%d hour %.0f min", hours, minutes)); + } else { + addMenuItem("TCPA", String.format("%.0f min", minutes)); + } + } else { // remove this later: don't show negative values... + addMenuItem("CPA", String.format("%.1f nm", cpa.getCpaDist())); + addMenuItem("TCPA", String.format("-%.1f hours", cpaTime)); } - } else { - addMenuItem("TCPA", String.format("%.1f hours", cpaTime)); } } } @@ -138,13 +144,10 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { - OsmAndLocationProvider locationProvider = app.getLocationProvider(); - Location ownLocation = null; - if (locationProvider != null) { - ownLocation = locationProvider.getLastKnownLocation(); - } - float distance = aisObject.getDistanceInNauticalMiles(ownLocation); - float bearing = aisObject.getBearing(ownLocation); + Location ownPosition = app.getLocationProvider().getLastKnownLocation(); + AisObject.setOwnPosition(ownPosition); + float distance = aisObject.getDistanceInNauticalMiles(); + float bearing = aisObject.getBearing(); if (distance >= 0.0f) { try { addMenuItem("Distance", String.format("%.1f nm", distance)); @@ -155,7 +158,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Bearing", String.format("%.1f", bearing)); } catch (Exception ignore) { } } - addCpaInfo(ownLocation, msgTypes); + addCpaInfo(ownPosition, msgTypes); } } if (msgTypes.contains(21)) { // ATON (aid to navigation) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index 2acb79af94c..1495d582608 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -228,14 +228,7 @@ private static Vector locationToVector(@NonNull Location loc) { } private static boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { - if (!x.hasBearing() || !y.hasBearing() || !x.hasSpeed() || !y.hasSpeed()) { - Log.d("AisTrackerHelper", "some input data is missing: x.hasBearing->" - + x.hasBearing() + ", y.hasBearing->" + y.hasBearing() + ", x.hasSpeed->" - + x.hasSpeed() + ", y.hasSpeed" + y.hasSpeed()); - return true; - } else { - return false; - } + return !x.hasBearing() || !y.hasBearing() || !x.hasSpeed() || !y.hasSpeed(); } private static float getSpeedInKnots(@NonNull Location loc) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 30da3e1a6f1..8cf38adae8d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -59,12 +59,15 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi this.bitmapPaint.setStrokeWidth(4); this.bitmapPaint.setColor(Color.DKGRAY); + AisObject.setCpaWarningTime(plugin.AIS_CPA_WARNING_TIME.get()); + AisObject.setCpaWarningDistance(plugin.AIS_CPA_WARNING_DISTANCE.get()); + initTimer(); startNetworkListener(); // for test purposes: remove later... initTestObjects(); - testCpa(); + //testCpa(); } private void testCpa() { @@ -368,6 +371,7 @@ public boolean isLocationVisible(RotatedTileBox tileBox, LatLon coordinates) { @Override public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + AisObject.setOwnPosition(getApplication().getLocationProvider().getLastKnownLocation()); for (AisObject ais : aisObjectList.values()) { if (isLocationVisible(tileBox, ais.getPosition())) { ais.draw(this, bitmapPaint, canvas, tileBox); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index 28944481736..224cf159e1e 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -55,9 +55,9 @@ public class AisTrackerPlugin extends OsmandPlugin { public final CommonPreference AIS_SHIP_LOST_TIMEOUT; public static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; public final CommonPreference AIS_CPA_WARNING_TIME; - private static final Integer AIS_CPA_DEFAULT_WARNING_TIME = 0; + public static final Integer AIS_CPA_DEFAULT_WARNING_TIME = 0; public final CommonPreference AIS_CPA_WARNING_DISTANCE; - private static final Float AIS_CPA_WARNING_DEFAULT_DISTANCE = 1.0f; + public static final Float AIS_CPA_WARNING_DEFAULT_DISTANCE = 1.0f; public AisTrackerPlugin(OsmandApplication app) { super(app); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 87844758a01..e46bcb72c57 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -112,7 +112,6 @@ private void setupObjectLostTimeout() { objectLostTimeout.setEntries(entries); objectLostTimeout.setEntryValues(entryValues); objectLostTimeout.setDescription(R.string.ais_object_lost_timeout_description); - AisObject.setMaxObjectAge((Integer) objectLostTimeout.getValue()); } } private void setupShipLostTimeout() { @@ -128,7 +127,6 @@ private void setupShipLostTimeout() { objectLostTimeout.setEntries(entries); objectLostTimeout.setEntryValues(entryValues); objectLostTimeout.setDescription(R.string.ais_ship_lost_timeout_description); - AisObject.setVesselLostTimeout((Integer) objectLostTimeout.getValue()); } } private boolean setupCpaWarningTime() { @@ -190,6 +188,14 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { if (newValue instanceof Integer) { AisObject.setVesselLostTimeout((Integer) newValue); } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_CPA_WARNING_TIME_ID)) { + if (newValue instanceof Integer) { + AisObject.setCpaWarningTime((Integer) newValue); + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_CPA_WARNING_DISTANCE_ID)) { + if (newValue instanceof Float) { + AisObject.setCpaWarningDistance((Float) newValue); + } } boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); From 68f8d805bd9494a42ed68ece60b33d9daf37caeb Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 6 Jul 2024 23:22:29 +0200 Subject: [PATCH 53/74] adjust layout of context menu entries --- .../plus/plugins/aistracker/AisObject.java | 5 +- .../aistracker/AisObjectMenuBuilder.java | 155 ++++++++++++++++++ .../aistracker/AisObjectMenuController.java | 43 ++--- 3 files changed, 168 insertions(+), 35 deletions(-) create mode 100644 OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index e29e06f6d86..68b5c260dc0 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -571,14 +571,15 @@ private boolean checkForCpaTimeout() { public static void setVesselLostTimeout(int timeInMinutes) { vesselLostTimeoutInMinutes = timeInMinutes; } public static void setCpaWarningTime(int warningTime) { cpaWarningTime = warningTime; } public static void setCpaWarningDistance(float warningDistance) { cpaWarningDistance = warningDistance; } - //public static void setOwnPosition(Location position) { ownPosition = position; } - public static void setOwnPosition(Location position) { + public static void setOwnPosition(Location position) { ownPosition = position; } +/* public static void setOwnPosition(Location position) { ownPosition = position; if (ownPosition != null) { ownPosition.setBearing(180.0f); // test ownPosition.setSpeed(0.1f); // test (m/s) } } + */ /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java new file mode 100644 index 00000000000..6bdfef83a3d --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java @@ -0,0 +1,155 @@ +package net.osmand.plus.plugins.aistracker; + + +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.helpers.FontCache; +import net.osmand.plus.mapcontextmenu.CollapsableView; +import net.osmand.plus.mapcontextmenu.MenuBuilder; +import net.osmand.plus.utils.AndroidUtils; +import net.osmand.plus.utils.ColorUtilities; +import net.osmand.plus.widgets.TextViewEx; +import net.osmand.util.Algorithms; + +public class AisObjectMenuBuilder extends MenuBuilder { + + public AisObjectMenuBuilder(@NonNull MapActivity mapActivity) { + super(mapActivity); + } + + public View buildRow(View view, Drawable icon, String buttonText, String textPrefix, String text, + int textColor, String secondaryText, boolean collapsable, CollapsableView collapsableView, boolean needLinks, + int textLinesLimit, boolean isUrl, boolean isNumber, boolean isEmail, View.OnClickListener onClickListener, boolean matchWidthDivider) { + + return buildAisRow(view,null, text, textColor, buttonText,null, textLinesLimit, matchWidthDivider); + + /* + return super.buildRow(view, icon, buttonText, textPrefix, text, textColor, secondaryText, collapsable, collapsableView, needLinks, + textLinesLimit, isUrl, isNumber, isEmail, onClickListener, matchWidthDivider); + */ + /* + return super.buildRow(view, icon, null, textPrefix, text, textColor, buttonText, collapsable, collapsableView, needLinks, + textLinesLimit, isUrl, isNumber, isEmail, onClickListener, matchWidthDivider); + */ + } + + private View buildAisRow(View view, String prefixText, String aisType, int aisTypeColor, String aisValue, + String suffixText, int textLinesLimit, boolean matchWidthDivider) { + + if (!isFirstRow()) { + buildRowDivider(view); + } + + LinearLayout baseView = new LinearLayout(view.getContext()); + baseView.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams llBaseViewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + baseView.setLayoutParams(llBaseViewParams); + + LinearLayout ll = new LinearLayout(view.getContext()); + ll.setOrientation(LinearLayout.HORIZONTAL); + LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + ll.setLayoutParams(llParams); + ll.setBackgroundResource(AndroidUtils.resolveAttribute(view.getContext(), android.R.attr.selectableItemBackground)); + ll.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + String textToCopy = Algorithms.isEmpty(prefixText) ? aisType : prefixText + ": " + aisType; + copyToClipboard(textToCopy, view.getContext()); + return true; + } + }); + + baseView.addView(ll); + + // prefixText + LinearLayout llText = new LinearLayout(view.getContext()); + llText.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams llTextViewParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT); + llTextViewParams.weight = 1f; + AndroidUtils.setMargins(llTextViewParams, 0, 0, dpToPx(10f), 0); + llTextViewParams.gravity = Gravity.CENTER_VERTICAL; + llText.setLayoutParams(llTextViewParams); + ll.addView(llText); + + TextViewEx textPrefixView = null; + if (!Algorithms.isEmpty(prefixText)) { + textPrefixView = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams llTextParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + AndroidUtils.setMargins(llTextParams, dpToPx(16f), dpToPx(8f), 0, 0); + textPrefixView.setLayoutParams(llTextParams); + textPrefixView.setTypeface(FontCache.getRobotoRegular(view.getContext())); + textPrefixView.setTextSize(12); + textPrefixView.setTextColor(ColorUtilities.getSecondaryTextColor(app, !light)); + textPrefixView.setMinLines(1); + textPrefixView.setMaxLines(1); + textPrefixView.setText(prefixText); + llText.addView(textPrefixView); + } + + // aisType + TextViewEx textView = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams llTextParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + AndroidUtils.setMargins(llTextParams, + dpToPx(16f), dpToPx(textPrefixView != null ? 2f : (suffixText != null ? 10f : 8f)), 0, dpToPx(suffixText != null ? 6f : 8f)); + textView.setLayoutParams(llTextParams); + textView.setTypeface(FontCache.getRobotoRegular(view.getContext())); + textView.setTextSize(16); + textView.setTextColor(ColorUtilities.getPrimaryTextColor(app, !light)); + textView.setText(aisType); + + if (textLinesLimit > 0) { + textView.setMinLines(1); + textView.setMaxLines(textLinesLimit); + textView.setEllipsize(TextUtils.TruncateAt.END); + } + if (aisTypeColor > 0) { + textView.setTextColor(getColor(aisTypeColor)); + } + llText.addView(textView); + + // suffixText + if (!TextUtils.isEmpty(suffixText)) { + TextViewEx textViewSecondary = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams llTextSecondaryParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + AndroidUtils.setMargins(llTextSecondaryParams, dpToPx(16f), 0, 0, dpToPx(6f)); + textViewSecondary.setLayoutParams(llTextSecondaryParams); + textViewSecondary.setTypeface(FontCache.getRobotoRegular(view.getContext())); + textViewSecondary.setTextSize(14); + textViewSecondary.setTextColor(ColorUtilities.getSecondaryTextColor(app, !light)); + textViewSecondary.setText(suffixText); + llText.addView(textViewSecondary); + } + + // aisValue + if (!TextUtils.isEmpty(aisValue)) { + TextViewEx buttonTextView = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams buttonTextViewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + buttonTextViewParams.gravity = Gravity.CENTER_VERTICAL; + AndroidUtils.setMargins(buttonTextViewParams, dpToPx(8), 0, dpToPx(8), 0); + buttonTextView.setLayoutParams(buttonTextViewParams); + buttonTextView.setTypeface(FontCache.getRobotoMedium(view.getContext())); + buttonTextView.setTextSize(16); + buttonTextView.setTextColor(ContextCompat.getColor(view.getContext(), !light ? R.color.ctx_menu_controller_button_text_color_dark_n : R.color.ctx_menu_controller_button_text_color_light_n)); + buttonTextView.setText(aisValue); + ll.addView(buttonTextView); + } + + ((LinearLayout) view).addView(baseView); + + rowBuilt(); + + setDividerWidth(matchWidthDivider); + + return ll; + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index ddfc32a1fd8..39f998700de 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -3,8 +3,6 @@ import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; -import static java.lang.Math.ceil; - import android.annotation.SuppressLint; import android.util.Log; @@ -15,10 +13,8 @@ import net.osmand.LocationConvert; import net.osmand.data.LatLon; import net.osmand.data.PointDescription; -import net.osmand.plus.OsmAndLocationProvider; import net.osmand.plus.OsmandApplication; import net.osmand.plus.activities.MapActivity; -import net.osmand.plus.mapcontextmenu.MenuBuilder; import net.osmand.plus.mapcontextmenu.MenuController; import java.util.Iterator; @@ -29,7 +25,9 @@ public class AisObjectMenuController extends MenuController { private final OsmandApplication app; public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointDescription pointDescription, AisObject aisObject) { - super(new MenuBuilder(mapActivity), pointDescription, mapActivity); + //super(new MenuBuilder(mapActivity), pointDescription, mapActivity); + super(new AisObjectMenuBuilder(mapActivity), pointDescription, mapActivity); + this.aisObject = aisObject; this.app = builder.getApplication(); builder.setShowTitleIfTruncated(false); @@ -38,28 +36,6 @@ public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointD builder.setShowNearestWiki(false); // TODO: show an icon in the menu } - private float getOwnSpeed(@Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - if (myLocation.hasSpeed()) { - return myLocation.getSpeed(); - } - } - } - return 0.0f; - } - private float getOwnBearing(@Nullable OsmAndLocationProvider locationProvider) { - if (locationProvider != null) { - Location myLocation = locationProvider.getLastKnownLocation(); - if (myLocation != null) { - if (myLocation.hasBearing()) { - return myLocation.getBearing(); - } - } - } - return 0.0f; - } @SuppressLint("DefaultLocale") private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet msgTypes) { @@ -88,7 +64,7 @@ private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet= 0.0f) { try { - addMenuItem("Bearing", String.format("%.1f", bearing)); + addMenuItem("Bearing", String.format("%.0f", bearing)); } catch (Exception ignore) { } } addCpaInfo(ownPosition, msgTypes); @@ -167,10 +143,10 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, } else if (msgTypes.contains(9)) { // SAR aircraft addMenuItem("Object Type", "SAR Aircraft"); if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { - addMenuItem("COG", String.valueOf(aisObject.getCog())); + addMenuItem("COG", String.format("%.0f", aisObject.getCog())); } if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { - addMenuItem("SOG", String.valueOf(aisObject.getSog()) + " kt"); + addMenuItem("SOG", String.format("%.1f kts", aisObject.getSog())); } if (aisObject.getAltitude() != AisObjectConstants.INVALID_ALTITUDE) { addMenuItem("Altitude", String.valueOf(aisObject.getAltitude()) + " m"); @@ -186,10 +162,10 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, addMenuItem("Navigation Status", aisObject.getNavStatusString()); } if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { - addMenuItem("COG", String.valueOf(aisObject.getCog())); + addMenuItem("COG", String.format("%.0f", aisObject.getCog())); } if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { - addMenuItem("SOG", String.valueOf(aisObject.getSog()) + " kt"); + addMenuItem("SOG", String.format("%.1f kts", aisObject.getSog())); } if (aisObject.getHeading() != AisObjectConstants.INVALID_HEADING) { addMenuItem("Heading", String.valueOf(aisObject.getHeading())); @@ -225,6 +201,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, if (hasNext) { msgTypesString = msgTypesString.concat(", "); } } addMenuItem("Message Type(s)", msgTypesString); + super.addPlainMenuItems(typeStr, pointDescription, latLon); } @Override From 1ab832edd88abe98e72375ad8b0b0b354e0a2209 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 7 Jul 2024 22:41:17 +0200 Subject: [PATCH 54/74] add disclaimer in the plugin description --- OsmAnd/res/values/strings.xml | 1 + .../osmand/plus/plugins/aistracker/AisTrackerPlugin.java | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index e4be8d1f530..331d51fcbe6 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -4251,6 +4251,7 @@ Download tile maps directly, or copy them as SQLite database files to OsmAnd\'s Create paths by tapping the map, or by using or modifying existing GPX files, to plan a trip and measure the distance between points. The result can be saved as a GPX file to use later for guidance. AIS vessel tracker Display AIS positions and information about surrounding vessels. The AIS data is received via network from an external AIS receiver. + DISCLAIMER\n\nThis plugin is a hobby project and not designed for reliability and correctness. DO NOT rely upon this software in any way including for navigation and/or safety of life. Accessibility Makes the device\'s accessibility features directly available in OsmAnd. This facilitates e.g. adjusting the speech rate for text-to-speech voices, configuring D-pad navigation, using a trackball for zoom control, or text-to-speech feedback, for example to auto-announce your position. OpenStreetMap editing diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index 224cf159e1e..ad3545afd5a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -79,6 +79,11 @@ public boolean isMarketPlugin() { return true; } + @Override + public int getVersion() { + return -1; + } + @Override public String getComponentId1() { return COMPONENT; @@ -86,7 +91,7 @@ public String getComponentId1() { @Override public CharSequence getDescription(boolean linksEnabled) { - return app.getString(R.string.plugin_aistracker_description); + return app.getString(R.string.plugin_aistracker_description).concat("\n\n").concat(app.getString(R.string.plugin_aistracker_disclaimer)); } @Override From 8a65f7e4ae021a05dd8a4a4373d1d10cbe568692 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 21 Jul 2024 22:08:41 +0200 Subject: [PATCH 55/74] address some concurrency issue in AIS object list --- .../aistracker/AisMessageListener.java | 31 +++++----- .../plus/plugins/aistracker/AisObject.java | 59 ++++++++++--------- .../aistracker/AisObjectMenuController.java | 3 +- .../plugins/aistracker/AisTrackerLayer.java | 19 +++--- 4 files changed, 57 insertions(+), 55 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java index 49e5d532e9e..785907fb816 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -34,7 +34,6 @@ public class AisMessageListener { private AisTrackerLayer aisLayer; private Timer timer; - private TimerTask taskCheckNetworkConnection; private DatagramSocket udpSocket; private Socket tcpSocket; private InputStream tcpStream; @@ -54,6 +53,7 @@ public AisMessageListener(int port, @NonNull AisTrackerLayer aisLayer) { } } public AisMessageListener(@NonNull String serverIp, int serverPort, @NonNull AisTrackerLayer aisLayer) { + TimerTask taskCheckNetworkConnection; initMembers(aisLayer); taskCheckNetworkConnection = new TimerTask() { @Override @@ -186,9 +186,8 @@ private void handleAisMessage(int aisType, Object obj) { switch (aisType) { case 1: AISMessage01 aisMsg01 = (AISMessage01)obj; // position report class A Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg01.getMMSI() - + " Type: " + aisMsg01.getMessageType() - + " ROT: " + aisMsg01.getRateOfTurn()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg01); + + " Type: " + aisMsg01.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg01); mmsi = aisMsg01.getMMSI(); msgType = aisMsg01.getMessageType(); navStatus = aisMsg01.getNavigationalStatus(); @@ -207,7 +206,7 @@ private void handleAisMessage(int aisType, Object obj) { case 2: AISMessage02 aisMsg02 = (AISMessage02)obj; // position report class A Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg02.getMMSI() + " Type: " + aisMsg02.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg02); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg02); mmsi = aisMsg02.getMMSI(); msgType = aisMsg02.getMessageType(); navStatus = aisMsg02.getNavigationalStatus(); @@ -226,7 +225,7 @@ private void handleAisMessage(int aisType, Object obj) { case 3: AISMessage03 aisMsg03 = (AISMessage03)obj; // position report class A Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg03.getMMSI() + " Type: " + aisMsg03.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg03); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg03); mmsi = aisMsg03.getMMSI(); msgType = aisMsg03.getMessageType(); navStatus = aisMsg03.getNavigationalStatus(); @@ -245,7 +244,7 @@ private void handleAisMessage(int aisType, Object obj) { case 4: AISMessage04 aisMsg04 = (AISMessage04)obj; // base station report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg04.getMMSI() + " Type: " + aisMsg04.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg04); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg04); mmsi = aisMsg04.getMMSI(); msgType = aisMsg04.getMessageType(); if (aisMsg04.hasLatitude()) { lat = aisMsg04.getLatitudeInDegrees(); } @@ -256,7 +255,7 @@ private void handleAisMessage(int aisType, Object obj) { case 5: AISMessage05 aisMsg05 = (AISMessage05)obj; // static and voyage related data Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg05.getMMSI() + " Type: " + aisMsg05.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg05); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg05); mmsi = aisMsg05.getMMSI(); msgType = aisMsg05.getMessageType(); imo = aisMsg05.getIMONumber(); @@ -281,7 +280,7 @@ private void handleAisMessage(int aisType, Object obj) { case 9: AISMessage09 aisMsg09 = (AISMessage09)obj; // SAR aircraft position report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg09.getMMSI() + " Type: " + aisMsg09.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg09); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg09); mmsi = aisMsg09.getMMSI(); msgType = aisMsg09.getMessageType(); timeStamp = aisMsg09.getTimeStamp(); @@ -296,7 +295,7 @@ private void handleAisMessage(int aisType, Object obj) { case 18: AISMessage18 aisMsg18 = (AISMessage18)obj; // basic class B position report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg18.getMMSI() + " Type: " + aisMsg18.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg18); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg18); mmsi = aisMsg18.getMMSI(); msgType = aisMsg18.getMessageType(); if (aisMsg18.hasTimeStamp()) { timeStamp = aisMsg18.getTimeStamp(); } @@ -312,7 +311,7 @@ private void handleAisMessage(int aisType, Object obj) { case 19: AISMessage19 aisMsg19 = (AISMessage19)obj; // extended class B position report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg19.getMMSI() + " Type: " + aisMsg19.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg19); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg19); mmsi = aisMsg19.getMMSI(); msgType = aisMsg19.getMessageType(); shipType = aisMsg19.getTypeOfShipAndCargoType(); @@ -333,7 +332,7 @@ private void handleAisMessage(int aisType, Object obj) { case 21: AISMessage21 aisMsg21 = (AISMessage21)obj; // aid-to-navigation report Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg21.getMMSI() + " Type: " + aisMsg21.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg21); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg21); mmsi = aisMsg21.getMMSI(); msgType = aisMsg21.getMessageType(); dimensionToBow = aisMsg21.getBow(); @@ -350,7 +349,7 @@ private void handleAisMessage(int aisType, Object obj) { case 24: AISMessage24 aisMsg24 = (AISMessage24)obj; // static data report (like type 5) Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg24.getMMSI() + " Type: " + aisMsg24.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg24); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg24); mmsi = aisMsg24.getMMSI(); msgType = aisMsg24.getMessageType(); callSign = aisMsg24.getCallSign(); @@ -368,7 +367,7 @@ private void handleAisMessage(int aisType, Object obj) { case 27: AISMessage27 aisMsg27 = (AISMessage27)obj; // long range broadcast message Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg27.getMMSI() + " Type: " + aisMsg27.getMessageType()); - Log.d("AisMessageListener","handleAisMessage() " + aisMsg27); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg27); mmsi = aisMsg27.getMMSI(); msgType = aisMsg27.getMessageType(); navStatus = aisMsg27.getNavigationalStatus(); @@ -391,11 +390,9 @@ private void handleAisMessage(int aisType, Object obj) { aisLayer.updateAisObjectList(ais); } private void initEmbeddedLister(int aisType, @NonNull SentenceListener listener) { - AisMessageListener.this.sentenceReader.addSentenceListener(listener); - /* + //AisMessageListener.this.sentenceReader.addSentenceListener(listener); // listen to all (!) NMEA messages AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDM); AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDO); - */ AisMessageListener.this.listenerList.push(listener); Log.d("AisMessageListener","Listener Type " + aisType + " started"); } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 68b5c260dc0..3a0e6183648 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -97,6 +97,7 @@ public class AisObject { private static Location ownPosition = null; // used to calculate distances, CPA etc. private AisObjType objectClass; private Bitmap bitmap = null; + private boolean bitmapValid = false; private int bitmapColor; private AisTrackerHelper.Cpa cpa; private long lastCpaUpdate = 0; @@ -340,6 +341,10 @@ private void initObjectClass() { } } + private void invalidateBitmap() { + this.bitmapValid = false; + } + public void set(@NonNull AisObject ais) { this.ais_mmsi = ais.getMmsi(); this.ais_msgType = ais.getMsgType(); @@ -380,14 +385,16 @@ public void set(@NonNull AisObject ais) { cpa = new AisTrackerHelper.Cpa(); } this.initObjectClass(); - this.bitmap = null; + this.invalidateBitmap(); this.bitmapColor = 0; } private void setBitmap(@NonNull AisTrackerLayer mapLayer) { + invalidateBitmap(); if (isLost(vesselLostTimeoutInMinutes)) { if (isMovable()) { this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); + this.bitmapValid = true; } } else { switch (this.objectClass) { @@ -399,21 +406,27 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { case AIS_VESSEL_COMMERCIAL: case AIS_INVALID: this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel); + this.bitmapValid = true; break; case AIS_LANDSTATION: this.bitmap = mapLayer.getBitmap(R.drawable.ais_land); + this.bitmapValid = true; break; case AIS_AIRPLANE: this.bitmap = mapLayer.getBitmap(R.drawable.ais_plane); + this.bitmapValid = true; break; case AIS_SART: this.bitmap = mapLayer.getBitmap(R.drawable.ais_sar); + this.bitmapValid = true; break; case AIS_ATON: this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton); + this.bitmapValid = true; break; case AIS_ATON_VIRTUAL: this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton_virt); + this.bitmapValid = true; break; } } @@ -453,7 +466,7 @@ private void setColor() { public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { - if ((this.bitmap == null) || isLost(vesselLostTimeoutInMinutes)) { + if ((!this.bitmapValid) || isLost(vesselLostTimeoutInMinutes)) { setBitmap(mapLayer); } if (checkCpaWarning()) { @@ -572,14 +585,6 @@ private boolean checkForCpaTimeout() { public static void setCpaWarningTime(int warningTime) { cpaWarningTime = warningTime; } public static void setCpaWarningDistance(float warningDistance) { cpaWarningDistance = warningDistance; } public static void setOwnPosition(Location position) { ownPosition = position; } -/* public static void setOwnPosition(Location position) { - ownPosition = position; - if (ownPosition != null) { - ownPosition.setBearing(180.0f); // test - ownPosition.setSpeed(0.1f); // test (m/s) - } - } - */ /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed @@ -663,18 +668,6 @@ public String getShipTypeString() { return("WIG, Hazardous category C"); case 24: return("WIG, Hazardous category D"); - case 40: - return("High Speed Craft (HSC)"); - case 41: - return("HSC, Hazardous category A"); - case 42: - return("HSC, Hazardous category B"); - case 43: - return("HSC, Hazardous category C"); - case 44: - return("HSC, Hazardous category D"); - case 49: // HSC, No additional information - return("High Speed Craft (HSC)"); case 30: return("Fishing"); case 31: @@ -687,6 +680,22 @@ public String getShipTypeString() { return("Diving ops"); case 35: return("Military ops"); + case 36: + return("Sailing"); + case 37: + return("Pleasure Craft"); + case 40: + return("High Speed Craft (HSC)"); + case 41: + return("HSC, Hazardous category A"); + case 42: + return("HSC, Hazardous category B"); + case 43: + return("HSC, Hazardous category C"); + case 44: + return("HSC, Hazardous category D"); + case 49: // HSC, No additional information + return("High Speed Craft (HSC)"); case 50: return("Pilot Vessel"); case 51: @@ -707,10 +716,6 @@ public String getShipTypeString() { return("Medical Transport"); case 59: return("Noncombatant ship according to RR Resolution No. 18"); - case 36: - return("Sailing"); - case 37: - return("Pleasure Craft"); case 60: return("Passenger"); case 61: @@ -722,7 +727,7 @@ public String getShipTypeString() { case 64: return("Passenger, Hazardous category D"); case 69: // Passenger, No additional information - return("Passenger"); + return("Passenger/Cruise/Ferry"); case 70: // Cargo, all ships of this type return("Cargo"); case 71: diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 39f998700de..6406bfeffd1 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -4,7 +4,6 @@ import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; import android.annotation.SuppressLint; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -118,7 +117,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, if (position != null) { addMenuItem("Position", LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + - ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); if (this.app != null) { Location ownPosition = app.getLocationProvider().getLastKnownLocation(); AisObject.setOwnPosition(ownPosition); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 8cf38adae8d..12c355436f3 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -29,22 +29,22 @@ import net.osmand.plus.views.layers.ContextMenuLayer; import net.osmand.plus.views.layers.base.OsmandMapLayer; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; public class AisTrackerLayer extends OsmandMapLayer implements ContextMenuLayer.IContextMenuProvider { private static final int START_ZOOM = 10; private final AisTrackerPlugin plugin; - private Map aisObjectList; - private final int aisObjectListCounterMax = 100; + private ConcurrentMap aisObjectList; + private static final int aisObjectListCounterMax = 100; private final Context context; - private Paint bitmapPaint; + private final Paint bitmapPaint; private Timer timer; - private TimerTask taskCheckAisObjectList; private AisMessageListener listener; public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugin) { super(context); @@ -52,7 +52,7 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi this.context = context; this.listener = null; - this.aisObjectList = new HashMap<>(); + this.aisObjectList = new ConcurrentHashMap<>(); this.bitmapPaint = new Paint(); this.bitmapPaint.setAntiAlias(true); this.bitmapPaint.setFilterBitmap(true); @@ -66,7 +66,7 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi startNetworkListener(); // for test purposes: remove later... - initTestObjects(); + //initTestObjects(); //testCpa(); } @@ -272,7 +272,8 @@ private void initTestObjects() { //removeLostAisObjects(); } private void initTimer() { - this.taskCheckAisObjectList = new TimerTask() { + TimerTask taskCheckAisObjectList; + taskCheckAisObjectList = new TimerTask() { @Override public void run() { Log.d("AisTrackerLayer", "timer task taskCheckAisObjectList running"); @@ -346,7 +347,7 @@ public void updateAisObjectList(@NonNull AisObject ais) { if (obj == null) { Log.d("AisTrackerLayer", "add AIS object with MMSI " + ais.getMmsi()); aisObjectList.put(mmsi, new AisObject(ais)); - if (aisObjectList.size() >= this.aisObjectListCounterMax) { + if (aisObjectList.size() >= aisObjectListCounterMax) { this.removeOldestAisObjectListEntry(); } } else { From 823aafce5f5ceabb5b7f69c49ebc628f73ae0b7e Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 21 Jul 2024 23:09:10 +0200 Subject: [PATCH 56/74] use getCurrentLocation() instead of getLocation() for CPA calculations --- .../plus/plugins/aistracker/AisObject.java | 18 ++++++++++++++++-- .../aistracker/AisObjectMenuController.java | 2 +- .../plugins/aistracker/AisTrackerHelper.java | 4 ++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 3a0e6183648..6583c90670b 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -32,6 +32,7 @@ import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; import static net.osmand.plus.plugins.aistracker.AisObjectConstants.CPA_UPDATE_TIMEOUT_IN_SECONDS; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getNewPosition; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_DEFAULT_WARNING_TIME; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_WARNING_DEFAULT_DISTANCE; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_OBJ_LOST_DEFAULT_TIMEOUT; @@ -550,9 +551,9 @@ private boolean needRotation() { private boolean checkCpaWarning() { if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0)) { if (checkForCpaTimeout() && (ownPosition != null)) { - Location aisPosition = getLocation(); + Location aisPosition = getCurrentLocation(); if (aisPosition != null) { - getCpa(ownPosition, getLocation(), cpa); + getCpa(ownPosition, aisPosition, cpa); lastCpaUpdate = System.currentTimeMillis(); } } @@ -637,6 +638,19 @@ public Location getLocation() { } return null; } + /* in contrast to getLocation(), this method considers the timestamp of the creation + * of the AIS object and adjusts the received position using the time difference + * between now and the timestamp (assuming that course and speed is constant) */ + @Nullable + public Location getCurrentLocation() { + Location loc = getLocation(); + Location newLocation = null; + if (loc != null) { + double ageInHours = (System.currentTimeMillis() - this.lastUpdate) / 1000.0 / 3600.0; + newLocation = getNewPosition(loc, ageInHours); + } + return newLocation; + } @Nullable public String getCallSign() { return this.ais_callSign; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 6406bfeffd1..37d745cb436 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -44,7 +44,7 @@ private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet Date: Tue, 23 Jul 2024 22:49:14 +0200 Subject: [PATCH 57/74] add TCP connection reset after mapActivityResume in special situations --- .../plugins/aistracker/AisMessageListener.java | 5 +++++ .../plus/plugins/aistracker/AisObject.java | 11 +++++++++++ .../plugins/aistracker/AisTrackerLayer.java | 17 +++++++++++++++++ .../plugins/aistracker/AisTrackerPlugin.java | 12 +++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java index 785907fb816..9a37998c8be 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -153,6 +153,11 @@ public void stopListener() { udpSocket.close(); } } + + public boolean checkTcpSocket() { + return (tcpSocket != null) && (tcpStream != null); + } + private void handleAisMessage(int aisType, Object obj) { AisObject ais = null; int msgType = 0; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 6583c90670b..e8c4bd9e32a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -88,7 +88,10 @@ public class AisObject { private String ais_destination = null; private String countryCode = null; private SortedSet msgTypes = null; + /* timestamp of last AIS message received for the current instance: */ private long lastUpdate = 0; + /* timestamp of last AIS message received for all instances: */ + private static long lastMessageReceived = 0; /* after this time of missing AIS signal the object is outdated and can be removed: */ private static int maxObjectAgeInMinutes = AIS_OBJ_LOST_DEFAULT_TIMEOUT; /* after this time of missing AIS signal the vessel symbol can change to mark "lost": */ @@ -202,6 +205,7 @@ private void initObj(int mmsi, int msgType) { this.countryCode = getCountryCode(this.ais_mmsi); this.msgTypes.add(ais_msgType); this.lastUpdate = System.currentTimeMillis(); + lastMessageReceived = this.lastUpdate; } private void initLatLon(double lat, double lon) { if ((lat != INVALID_LAT) && (lon != INVALID_LON)) { @@ -378,6 +382,7 @@ public void set(@NonNull AisObject ais) { /* this method does not produce an exact copy of the given object, here are the differences: */ this.lastUpdate = System.currentTimeMillis(); + lastMessageReceived = this.lastUpdate; if (this.msgTypes == null) { this.msgTypes = new TreeSet<>(); } @@ -667,6 +672,12 @@ public String getDestination() { public String getCountryCode() { return this.countryCode; } public AisObjType getObjectClass() { return this.objectClass; } public long getLastUpdate() { return this.lastUpdate; } + public static long getLastMessageReceived() { return lastMessageReceived; } + public static long getAndUpdateLastMessageReceived() { + long timestamp = getLastMessageReceived(); + lastMessageReceived = System.currentTimeMillis(); + return timestamp; + } @NonNull public String getShipTypeString() { switch (this.ais_shipType) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 12c355436f3..76e5a124132 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -297,6 +297,23 @@ private void stopNetworkListener() { this.listener = null; } } + + /* this method restarts the TCP listeners after a "resume" event (the smartphone resumed + * from sleep or from switched off state): in this case the TCP connection might be broken, + * but the sockets are still (logically) open. + * as additional indication of a broken TCP connection it is checked whether any AIS message + * was received in the last 20 seconds */ + public void checkTcpConnection() { + if (listener != null) { + if (listener.checkTcpSocket()) { + if (((System.currentTimeMillis() - AisObject.getAndUpdateLastMessageReceived()) / 1000) > 20) { + Log.d("AisTrackerLayer", "checkTcpConnection(): restart TCP socket"); + restartNetworkListener(); + } + } + } + } + public void restartNetworkListener() { stopNetworkListener(); startNetworkListener(); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java index ad3545afd5a..86574c602fa 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -88,7 +88,10 @@ public int getVersion() { public String getComponentId1() { return COMPONENT; } - + @Override + public String getComponentId2() { + return "net.osmand.dev"; // for test purposes to enable logcat at adb connected physical device + } @Override public CharSequence getDescription(boolean linksEnabled) { return app.getString(R.string.plugin_aistracker_description).concat("\n\n").concat(app.getString(R.string.plugin_aistracker_disclaimer)); @@ -137,6 +140,13 @@ public String getPrefsDescription() { return app.getString(R.string.ais_address_settings_description); } + @Override + public void mapActivityResume(@NonNull MapActivity activity) { + if (aisTrackerLayer != null) { + aisTrackerLayer.checkTcpConnection(); + } + } + @Override public void updateLayers(@NonNull Context context, @Nullable MapActivity mapActivity) { OsmandMapTileView mapView = app.getOsmandMap().getMapView(); From 562da8623fa7b80bf6a416fc3d33b2153f882e12 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 23 Jul 2024 23:21:07 +0200 Subject: [PATCH 58/74] added icon in the top area of the context menu --- .../aistracker/AisObjectMenuController.java | 9 ++++++- .../plugins/aistracker/AisTrackerLayer.java | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 37d745cb436..65dd219306e 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -13,6 +13,7 @@ import net.osmand.data.LatLon; import net.osmand.data.PointDescription; import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.mapcontextmenu.MenuController; @@ -33,7 +34,13 @@ public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointD builder.setShowNearestPoi(false); builder.setShowOnlinePhotos(false); builder.setShowNearestWiki(false); - // TODO: show an icon in the menu + } + + @Override + public int getRightIconId() { return R.drawable.ic_plugin_nautical_map; } + @Override + public boolean isBigRightIcon() { + return true; } @SuppressLint("DefaultLocale") diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 76e5a124132..8a33c9203a2 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -66,7 +66,10 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi startNetworkListener(); // for test purposes: remove later... - //initTestObjects(); + //initTestObject1(); + //initTestObject2(); + //initTestObject3(); + //initTestObject4(); //testCpa(); } @@ -240,7 +243,8 @@ private void testCpa() { Log.d("AisTrackerLayer", "# test4: dist1: " + meterToMiles(y4.distanceTo(y5))); } } - private void initTestObjects() { + + private void initTestObject1() { // passenger ship AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, 320.0, 8.4, 50.738d, 7.099d, 0.0); @@ -249,28 +253,32 @@ private void initTestObjects() { 65, 8, 12, 2, "Potsdam", 8, 15, 22, 5); updateAisObjectList(ais); + } + private void initTestObject2() { // sailing boat - ais = new AisObject(454011, 1, 20, 8, 0, 120, + AisObject ais = new AisObject(454011, 1, 20, 8, 0, 120, 125.0, 4.4, 50.737d, 7.098d, 0.0); updateAisObjectList(ais); ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, 0, 0, 0, 0, "@@@", 0, 0, 0, 0); updateAisObjectList(ais); + } + private void initTestObject3() { // land station - ais = new AisObject(878121, 4, 50.736d, 7.100d); + AisObject ais = new AisObject(878121, 4, 50.736d, 7.100d); updateAisObjectList(ais); // AIDS ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, 0, 0, 0, 0); updateAisObjectList(ais); + } + private void initTestObject4() { // aircraft - ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); + AisObject ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); updateAisObjectList(ais); - - //removeOldestAisObjectListEntry(); - //removeLostAisObjects(); } + private void initTimer() { TimerTask taskCheckAisObjectList; taskCheckAisObjectList = new TimerTask() { From 8467b79f6d2576c923d1d29c3e0001193974a40a Mon Sep 17 00:00:00 2001 From: Falk Date: Mon, 29 Jul 2024 20:01:13 +0200 Subject: [PATCH 59/74] do not show negative CPA times in context menu --- .../plus/plugins/aistracker/AisObjectMenuController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index 65dd219306e..cc9a084a783 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -70,10 +70,12 @@ private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet Date: Wed, 31 Jul 2024 23:01:04 +0200 Subject: [PATCH 60/74] change bitmap+color handling --- .../plus/plugins/aistracker/AisObject.java | 103 +++++++++--------- .../aistracker/AisObjectConstants.java | 1 + 2 files changed, 51 insertions(+), 53 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index e8c4bd9e32a..1523cf5a2fb 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -395,6 +395,51 @@ public void set(@NonNull AisObject ais) { this.bitmapColor = 0; } + public static int selectBitmap(AisObjType objType) { + switch (objType) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + case AIS_INVALID: + return R.drawable.ais_vessel; + case AIS_VESSEL_LOST: + return R.drawable.ais_vessel_cross; + case AIS_LANDSTATION: + return R.drawable.ais_land; + case AIS_AIRPLANE: + return R.drawable.ais_plane; + case AIS_SART: + return R.drawable.ais_sar; + case AIS_ATON: + return R.drawable.ais_aton; + case AIS_ATON_VIRTUAL: + return R.drawable.ais_aton_virt; + } + return -1; + } + + public static int selectColor(AisObjType objType) { + switch (objType) { + case AIS_VESSEL: + return Color.GREEN; + case AIS_VESSEL_SPORT: + return Color.YELLOW; + case AIS_VESSEL_FAST: + return Color.BLUE; + case AIS_VESSEL_PASSENGER: + return Color.CYAN; + case AIS_VESSEL_FREIGHT: + return Color.GRAY; + case AIS_VESSEL_COMMERCIAL: + return Color.LTGRAY; + default: + return 0; // black + } + } + private void setBitmap(@NonNull AisTrackerLayer mapLayer) { invalidateBitmap(); if (isLost(vesselLostTimeoutInMinutes)) { @@ -403,37 +448,10 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { this.bitmapValid = true; } } else { - switch (this.objectClass) { - case AIS_VESSEL: - case AIS_VESSEL_SPORT: - case AIS_VESSEL_FAST: - case AIS_VESSEL_PASSENGER: - case AIS_VESSEL_FREIGHT: - case AIS_VESSEL_COMMERCIAL: - case AIS_INVALID: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel); - this.bitmapValid = true; - break; - case AIS_LANDSTATION: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_land); - this.bitmapValid = true; - break; - case AIS_AIRPLANE: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_plane); - this.bitmapValid = true; - break; - case AIS_SART: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_sar); - this.bitmapValid = true; - break; - case AIS_ATON: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton); - this.bitmapValid = true; - break; - case AIS_ATON_VIRTUAL: - this.bitmap = mapLayer.getBitmap(R.drawable.ais_aton_virt); - this.bitmapValid = true; - break; + int bitmapId = selectBitmap(this.objectClass); + if (bitmapId >= 0) { + this.bitmap = mapLayer.getBitmap(bitmapId); + this.bitmapValid = true; } } this.setColor(); @@ -445,28 +463,7 @@ private void setColor() { this.bitmapColor = 0; // black } } else { - switch (this.objectClass) { - case AIS_VESSEL: - this.bitmapColor = Color.GREEN; - break; - case AIS_VESSEL_SPORT: - this.bitmapColor = Color.YELLOW; - break; - case AIS_VESSEL_FAST: - this.bitmapColor = Color.BLUE; - break; - case AIS_VESSEL_PASSENGER: - this.bitmapColor = Color.CYAN; - break; - case AIS_VESSEL_FREIGHT: - this.bitmapColor = Color.GRAY; - break; - case AIS_VESSEL_COMMERCIAL: - this.bitmapColor = Color.LTGRAY; - break; - default: - this.bitmapColor = 0; // black - } + this.bitmapColor = selectColor(this.objectClass); } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index afb42fa526d..7eb76b0b8b7 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -31,6 +31,7 @@ public static enum AisObjType { AIS_VESSEL_PASSENGER, AIS_VESSEL_FREIGHT, AIS_VESSEL_COMMERCIAL, + AIS_VESSEL_LOST, // only dummy value, not assigned by a real vessel AIS_LANDSTATION, AIS_AIRPLANE, AIS_SART, From 30ca7cce0f562398b2cf5f4d1dc97abf41172396 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 4 Aug 2024 18:51:19 +0200 Subject: [PATCH 61/74] adjust logic for bitmap/color selection --- .../plus/plugins/aistracker/AisObject.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 1523cf5a2fb..3953d58c049 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -467,21 +467,29 @@ private void setColor() { } } - public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, - @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { - if ((!this.bitmapValid) || isLost(vesselLostTimeoutInMinutes)) { + private void updateBitmap(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint) { + if (isLost(vesselLostTimeoutInMinutes)) { setBitmap(mapLayer); - } - if (checkCpaWarning()) { - activateCpaWarning(); } else { - deactivateCpaWarning(); + if (!this.bitmapValid) { + setBitmap(mapLayer); + } + if (checkCpaWarning()) { + activateCpaWarning(); + } else { + deactivateCpaWarning(); + } } if (this.bitmapColor != 0) { paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); } else { paint.setColorFilter(null); } + } + + public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, + @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { + updateBitmap(mapLayer, paint); if (this.bitmap != null) { canvas.save(); canvas.rotate(tileBox.getRotate(), (float)tileBox.getCenterPixelX(), (float)tileBox.getCenterPixelY()); From 0e72e91071bc3642b13f72b45e0773e5b4ed9224 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 11 Aug 2024 16:39:56 +0200 Subject: [PATCH 62/74] adjustments after merge --- OsmAnd-java/build.gradle | 4 ++++ OsmAnd/res/values/strings.xml | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/OsmAnd-java/build.gradle b/OsmAnd-java/build.gradle index dfa6ff47976..382feccc252 100644 --- a/OsmAnd-java/build.gradle +++ b/OsmAnd-java/build.gradle @@ -6,6 +6,10 @@ configurations { android } +test { + exclude '**/*' +} + tasks.withType(JavaCompile).configureEach { sourceCompatibility = "17" targetCompatibility = "17" diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index 331d51fcbe6..f105b095af5 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -278,7 +278,6 @@ You will be able to pair this scanner again at any time. A toggle to show or hide %1$s on the map. Hugerock Promo for %1$s months Free access to features including unlimited map downloads, 3D relief etc. for %1$s month - UK and similar India Keep left Keep right @@ -306,7 +305,6 @@ You will be able to pair this scanner again at any time. Terrain color scheme Start point Destination - Next destination point To My Location Map to the left Map to the right @@ -322,9 +320,6 @@ You will be able to pair this scanner again at any time. Simulate Display position always in center Terrain colorization type - Underlay - Overlay - Map style First intermediate Audio note Video note From a9da1554c39ab4b95b6a5d83d7134191e49c4949 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 13 Aug 2024 23:19:01 +0200 Subject: [PATCH 63/74] restart network listeners in cast of protocol change (UDP/TCP) --- .../plus/plugins/aistracker/AisTrackerSettingsFragment.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index e46bcb72c57..812a54f42de 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -8,6 +8,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -196,7 +197,10 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { if (newValue instanceof Float) { AisObject.setCpaWarningDistance((Float) newValue); } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_PROTOCOL_ID)) { + restartNetworkListener = true; } + boolean ret = super.onPreferenceChange(preference, newValue); AisTrackerLayer layer = plugin.getLayer(); if ((layer != null) && (restartNetworkListener)) { From 0cf6b9f4193f608bc1e7e0982faf48ac9dccaca6 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 13 Aug 2024 23:20:53 +0200 Subject: [PATCH 64/74] increase max number of AIS objects to 200 --- .../src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 8a33c9203a2..e78a8452ed7 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -41,7 +41,7 @@ public class AisTrackerLayer extends OsmandMapLayer implements ContextMenuLayer. private static final int START_ZOOM = 10; private final AisTrackerPlugin plugin; private ConcurrentMap aisObjectList; - private static final int aisObjectListCounterMax = 100; + private static final int aisObjectListCounterMax = 200; private final Context context; private final Paint bitmapPaint; private Timer timer; From 06586cbfdb178064128bef0a9b5f631f99092c63 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 24 Sep 2024 23:56:23 +0200 Subject: [PATCH 65/74] adjusted CPA warning indication: add new condition: examine time when the own course crosses the course line of the other vessel --- .../plus/plugins/aistracker/AisObject.java | 22 +++- .../plugins/aistracker/AisTrackerHelper.java | 93 ++++++++++--- .../plugins/aistracker/AisTrackerLayer.java | 122 +++++++++++++++++- 3 files changed, 214 insertions(+), 23 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 3953d58c049..b12ec861c0a 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -99,6 +99,7 @@ public class AisObject { private static int cpaWarningTime = AIS_CPA_DEFAULT_WARNING_TIME; // in minutes private static float cpaWarningDistance = AIS_CPA_WARNING_DEFAULT_DISTANCE; // in miles private static Location ownPosition = null; // used to calculate distances, CPA etc. + private static boolean ownPositionFaked = false; // used for test purposes to fake own position private AisObjType objectClass; private Bitmap bitmap = null; private boolean bitmapValid = false; @@ -557,7 +558,15 @@ private boolean needRotation() { } /* return true if the vessel gets too close with the own position in the future - * (danger of collusion) */ + * (danger of collusion); + * this situation occurs if all of the following conditions hold: + * (1) the calculated TCPA is in the future (>0) + * (2) the calculated CPA is not bigger than the configured warning distance + * (3) the calculated TCPA is not bigger than the configured warning time + * (4) the time when the own course crosses the course of the other vessel + * is not in the past + * (5) the time when the course of the other vessel crosses the own course + * is not in the past */ private boolean checkCpaWarning() { if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0)) { if (checkForCpaTimeout() && (ownPosition != null)) { @@ -570,7 +579,10 @@ private boolean checkCpaWarning() { if (cpa.isValid()) { double tcpa = cpa.getTcpa(); if (tcpa > 0.0f) { - return ((tcpa * 60.0d) <= cpaWarningTime) && (cpa.getCpaDist() <= cpaWarningDistance); + return ((cpa.getCpaDist() <= cpaWarningDistance) && + ((tcpa * 60.0d) <= cpaWarningTime) && + (cpa.getCrossingTime1() >= 0.0d) && + (cpa.getCrossingTime2() >= 0.0d)); } } } @@ -595,7 +607,11 @@ private boolean checkForCpaTimeout() { public static void setVesselLostTimeout(int timeInMinutes) { vesselLostTimeoutInMinutes = timeInMinutes; } public static void setCpaWarningTime(int warningTime) { cpaWarningTime = warningTime; } public static void setCpaWarningDistance(float warningDistance) { cpaWarningDistance = warningDistance; } - public static void setOwnPosition(Location position) { ownPosition = position; } + public static void setOwnPosition(Location position) { if (!ownPositionFaked) { ownPosition = position; }} + public static void fakeOwnPosition(Location fakePosition) { // used for test purposes + ownPosition = fakePosition; + ownPositionFaked = fakePosition != null; + } /* * this function checks the age of the object (check lastUpdate against its limit) * and returns true if the object is outdated and can be removed diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java index 96452cfa5d8..bd9abd11d7c 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pair; import com.jwetherell.openmap.common.LatLonPoint; @@ -17,9 +18,10 @@ public final class AisTrackerHelper { private static long lastCorrectionUpdate = 0; private static double correctionFactor = 1.0d; private static final long maxCorrectionUpdateAgeInMin = 60; + private static class Vector { - public double x; // Latitude (grows in North direction) - public double y; // Longitude (grows in East direction) + public double x; // Longitude (grows in East direction) + public double y; // Latitude (grows in North direction) public Vector(double a, double b) { this.x = a; this.y = b; @@ -39,6 +41,8 @@ public static class Cpa { private float cpaDist; // in miles private Location newPos1; // position of first object at time tcpa private Location newPos2; // position of first object at time tcpa + private double t1 = 0.0d; // time for object 1 to cross the course of object 2 + private double t2 = 0.0d; // time for object 2 to cross the course of object 1 private boolean valid; public Cpa() { reset(); @@ -49,11 +53,19 @@ public void reset() { newPos1 = null; newPos2 = null; valid = false; + t1 = t2 = 0.0d; } public void setTcpa(double x) { this.tcpa = x; } public void setCpaDist(float x) { this.cpaDist = x; } public void setCpaPos1(Location loc) { this.newPos1 = loc; } public void setCpaPos2(Location loc) { this.newPos2 = loc; } + public void setCrossingTimes(@Nullable Pair t) { + if (t != null) { + t1 = t.first; t2 = t.second; + } + }; + public double getCrossingTime1() { return t1; } + public double getCrossingTime2() { return t2; } public double getTcpa() { return tcpa; } public float getCpaDist() { return cpaDist; } public Location getCpaPos1() { return newPos1; } @@ -64,9 +76,9 @@ public void reset() { /* calculate the Time to Closest Point of Approach (TCPA) of two moving objects: * object 1 at position x and velocity vector vx - * object 2 at position y and velocity vectoy vy, - * For the calculation, cartesian ccordinates are assumed with a cartesian distance metricx - * -> attention: by using sherical coordinates, this will produce an error! */ + * object 2 at position y and velocity vector vy, + * For the calculation, cartesian coordinates are assumed with a cartesian distance metric + * -> attention: by using spherical coordinates, this will produce an error! */ private static double getTcpa(@NonNull Vector x, @NonNull Vector y, @NonNull Vector vx, @NonNull Vector vy, double lonCorrection) { Vector dx = new Vector( y.sub(x)); @@ -76,7 +88,7 @@ private static double getTcpa(@NonNull Vector x, @NonNull Vector y, // avoid div by 0 or invalid lonCorrection return INVALID_TCPA; } - return -(((dx.x * dv.x) + (dx.y * dv.y / lonCorrection)) / divisor); + return -(((dx.x * dv.x / lonCorrection) + (dx.y * dv.y)) / divisor); } /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and y, @@ -91,12 +103,7 @@ private static double getTcpa(@NonNull Location x, @NonNull Location y, double l } public static double getTcpa(@NonNull Location ownLocation, @NonNull Location otherLocation) { - long now = System.currentTimeMillis(); - if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { - correctionFactor = getLonCorrection(ownLocation); - lastCorrectionUpdate = now; - } - return getTcpa(ownLocation, otherLocation, correctionFactor); + return getTcpa(ownLocation, otherLocation, getLonCorrection(ownLocation)); } @Nullable @@ -129,7 +136,7 @@ public static Location getCpa2(@NonNull Location x, @NonNull Location y) { return getCpa(x, y, false); } - /* caluclate the distance between the given objects at their Closest Point of Approach (CPA) */ + /* calculate the distance between the given objects at their Closest Point of Approach (CPA) */ public static float getCpaDistance(@NonNull Location x, @NonNull Location y) { Location cpaX = getCpa1(x,y); Location cpaY = getCpa2(x,y); @@ -147,6 +154,8 @@ public static void getCpa(@NonNull Location ownLocation, @NonNull Location other if (tcpa != INVALID_TCPA) { Location cpaX = getNewPosition(ownLocation, tcpa); Location cpaY = getNewPosition(otherLocation, tcpa); + PaircrossingTimes = getCrossingTimes(ownLocation, otherLocation); + result.setCrossingTimes(crossingTimes); result.setTcpa(tcpa); result.setCpaPos1(cpaX); result.setCpaPos2(cpaY); @@ -164,6 +173,37 @@ private static double bearingInRad(float bearingInDegrees) { return res; } + /* This method takes the two locations (including position, course and speed) + and calculates the time when the two objects reach the location where the course lines + are crossing. + for each object, the time may be different or even in the past, hence a pair of two + times is returned + in error case or if the courses do not cross each other, Null is returned + * */ + @Nullable + private static Pair getCrossingTimes(@NonNull Location x, @NonNull Location y) { + double lonCorrection = getLonCorrection(x); + Vector vX = locationToVector(x, lonCorrection); // position 1 at time t0 + Vector vY = locationToVector(y, lonCorrection); // position 2 at time t0 + Vector vVX = courseToVector(x.getBearing(), getSpeedInKnots(x)); // velocity vector 1 + Vector vVY = courseToVector(y.getBearing(), getSpeedInKnots(y)); // velocity vector 2 + Vector vDXY = vX.sub(vY); // position difference at time t0 + double divisor = vVX.x * vVY.y - vVX.y * vVY.x; + if ((Math.abs(divisor) < 1.0E-10f) || (lonCorrection < 1.0E-10f)) { + // avoid div by 0 or invalid lonCorrection + Log.d("AisTrackerHelper", "getCollisionTimes(): Division by 0: divisor->" + + divisor + ", lonCorrection->" + lonCorrection); + return null; + } + Pair result = new Pair((vVY.x * vDXY.y - vVY.y * vDXY.x) / divisor, + (vVX.x * vDXY.y - vVX.y * vDXY.x) / divisor); + /* Log.d("AisTrackerHelper", "getCollisionTimes(): t1->" + + result.first.toString() + ", t2->" + result.second.toString()); + */ + + return result; + } + @Nullable public static Location getNewPosition(@Nullable Location loc, double timeInHours) { if (loc != null) { @@ -176,8 +216,10 @@ public static Location getNewPosition(@Nullable Location loc, double timeInHours newX.setLatitude(b.getLatitude()); return newX; } else { - Log.d("AisTrackerHelper", "getNewPosition(): loc.hasBearing->" - + loc.hasBearing() + ", loc.hasSpeed->" + loc.hasSpeed()); + /* Log.d("AisTrackerHelper", "getNewPosition(): loc.hasBearing->" + + loc.hasBearing() + ", loc.hasSpeed->" + loc.hasSpeed() + + ", speed->" + loc.getSpeed()); + */ return null; } } else { @@ -185,7 +227,7 @@ public static Location getNewPosition(@Nullable Location loc, double timeInHours } } - private static double getLonCorrection(@Nullable Location loc) { + private static double calculateLonCorrection(@Nullable Location loc) { if (loc != null) { Location x = new Location(loc); // simulate a "measurement" trip towards East... @@ -201,6 +243,15 @@ private static double getLonCorrection(@Nullable Location loc) { return 1.0f; // fallback } + private static double getLonCorrection(@Nullable Location loc) { + long now = System.currentTimeMillis(); + if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { + correctionFactor = calculateLonCorrection(loc); + lastCorrectionUpdate = now; + } + return correctionFactor; + } + public static float knotsToMeterPerSecond(float speed) { return speed * 1852 / 3600; } @@ -211,7 +262,7 @@ public static float meterToMiles(float x) { return x / 1852.0f; } - /* calculate a velocity vector from givem course (COG) and speed (SOG). + /* calculate a velocity vector from given course (COG) and speed (SOG). COG is given as heading, SOG as scalar */ @NonNull private static Vector courseToVector(double cog, double sog) { @@ -219,12 +270,16 @@ private static Vector courseToVector(double cog, double sog) { while (alpha < 0) { alpha += 360.0d; } while (alpha >= 360.0d ) { alpha -= 360.0d; } alpha = Math.toRadians(alpha); - return new Vector(Math.sin(alpha) * sog, Math.cos(alpha) * sog); + return new Vector(Math.cos(alpha) * sog, Math.sin(alpha) * sog); } @NonNull private static Vector locationToVector(@NonNull Location loc) { - return new Vector(loc.getLatitude() * 60.0, loc.getLongitude() * 60.0); + return new Vector(loc.getLongitude() * 60.0, loc.getLatitude() * 60.0); + } + + private static Vector locationToVector(@NonNull Location loc, double lonCorrection) { + return new Vector(loc.getLongitude() * 60.0 / lonCorrection, loc.getLatitude() * 60.0); } private static boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index e78a8452ed7..638a7bcd3e2 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -65,12 +65,110 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi initTimer(); startNetworkListener(); - // for test purposes: remove later... + // for test purposes: remove/disable later... //initTestObject1(); //initTestObject2(); //initTestObject3(); //initTestObject4(); + //testCrossingTimes(); //testCpa(); + //initFakePosition(); + } + + private void testCrossingTimes() { + // here some tests for the geo (CPA) calculation + // intention is to test the function to calculate times of two objects with crossing courses + // define 12 positions on two courses: position a1 ... a6 at course line A (course 90°) + // and position b1 ... b6 at course line B (course 45°) + // the positions are taken from a (paper) map + // for coordinate transformation see https://www.koordinaten-umrechner.de + AisTrackerHelper.Cpa cpa = new AisTrackerHelper.Cpa(); + Location a1 = new Location("test", 49.5d, -3.266667d); // 49°30'N, 3°16'W + Location a2 = new Location("test", 49.5d, -3.166667d); // 49°30'N, 3°10'W + Location a3 = new Location("test", 49.5d, -3.116667d); // 49°30'N, 3°7'W + Location a4 = new Location("test", 49.5d, -3.093333d); // 49°30'N, 3°5.6'W + Location a5 = new Location("test", 49.5d, -3.05d); // 49°30'N, 3°3'W + Location a6 = new Location("test", 49.5d, -3.016667d); // 49°30'N, 3°1'W + Location b1 = new Location("test", 49.395d, -3.25d); // 49°23.7'N, 3°15'W + Location b2 = new Location("test", 49.441667d, -3.183333d); // 49°26.5'N, 3°11'W + Location b3 = new Location("test", 49.47d, -3.133333d); // 49°28.2'N, 3°8'W + Location b4 = new Location("test", 49.5d, -3.093333d); // 49°30'N, 3°5.6'W + Location b5 = new Location("test", 49.513333d, -3.066667d); // 49°30.8'N, 3°4'W + Location b6 = new Location("test", 49.538333d, -3.033333d); // 49°32.3'N, 3°2'W + a1.setSpeed(knotsToMeterPerSecond(1.0f)); a1.setBearing(90.0f); + a2.setSpeed(knotsToMeterPerSecond(1.0f)); a2.setBearing(90.0f); + a3.setSpeed(knotsToMeterPerSecond(1.0f)); a3.setBearing(90.0f); + a4.setSpeed(knotsToMeterPerSecond(1.0f)); a4.setBearing(90.0f); + a5.setSpeed(knotsToMeterPerSecond(1.0f)); a5.setBearing(90.0f); + a6.setSpeed(knotsToMeterPerSecond(1.0f)); a6.setBearing(90.0f); + b1.setSpeed(knotsToMeterPerSecond(1.0f)); b1.setBearing(45.0f); + b2.setSpeed(knotsToMeterPerSecond(1.0f)); b2.setBearing(45.0f); + b3.setSpeed(knotsToMeterPerSecond(1.0f)); b3.setBearing(45.0f); + b4.setSpeed(knotsToMeterPerSecond(1.0f)); b4.setBearing(45.0f); + b5.setSpeed(knotsToMeterPerSecond(1.0f)); b5.setBearing(45.0f); + b6.setSpeed(knotsToMeterPerSecond(1.0f)); b6.setBearing(45.0f); + // now trigger the calculations: + cpa.reset(); getCpa(a3, b2, cpa); // expected: t1>0, t2>0 + Log.d("AisTrackerLayer", "# test(a3, b2): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b3, cpa); // expected: t1>0, t2>0 + Log.d("AisTrackerLayer", "# test(a3, b3): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b4, cpa); // expected: t1>0, t2->0 + Log.d("AisTrackerLayer", "# test(a3, b4): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b5, cpa); // expected: t1>0, t2<0 + Log.d("AisTrackerLayer", "# test(a3, b5): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b6, cpa); // expected: t1>0, t2<0 + Log.d("AisTrackerLayer", "# test(a3, b6): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b2, cpa); // expected: t1->0, t2>0 + Log.d("AisTrackerLayer", "# test(a4, b2): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b3, cpa); // expected: t1->0, t2>0 + Log.d("AisTrackerLayer", "# test(a4, b3): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b4, cpa); // expected: t1->0, t2->0 + Log.d("AisTrackerLayer", "# test(a4, b4): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b5, cpa); // expected: t1->0, t2<0 + Log.d("AisTrackerLayer", "# test(a4, b5): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b6, cpa); // expected: t1->0, t2<0 + Log.d("AisTrackerLayer", "# test(a4, b6): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b2, cpa); // expected: t1<0, t2>0 + Log.d("AisTrackerLayer", "# test(a5, b2): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b3, cpa); // expected: t1<0, t2>0 + Log.d("AisTrackerLayer", "# test(a5, b3): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b4, cpa); // expected: t1<0, t2->0 + Log.d("AisTrackerLayer", "# test(a5, b4): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b5, cpa); // expected: t1<0, t2<0 + Log.d("AisTrackerLayer", "# test(a5, b5): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b6, cpa); // expected: t1<0, t2<0 + Log.d("AisTrackerLayer", "# test(a5, b6): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + } private void testCpa() { @@ -244,6 +342,28 @@ private void testCpa() { } } + private void initFakePosition() { + // fake the own position, course and speed to a (fixed) hard coded value + double fakeLat = 50.76077d; + double fakeLon = 7.08747d; + float fakeCOG = 340.0f; + //float fakeCOG = 100.0f; + float fakeSOG = 3.0f; + Location fake = new Location("test", fakeLat, fakeLon); + fake.setBearing(fakeCOG); + fake.setSpeed(knotsToMeterPerSecond(fakeSOG)); + AisObject.fakeOwnPosition(fake); + Log.d("AisTrackerLayer", "initFakePosition: fake: " + fake.toString()); + // in order to visualize this faked (own) position on the map, create an AIS object at this location... + AisObject ais = new AisObject(324578, 1, 20, 0, 1, (int)fakeCOG, + fakeCOG, fakeSOG, fakeLat, fakeLon, 0.0); + updateAisObjectList(ais); + ais = new AisObject(324578, 5, 0, "own-position", "fake", 60 /* passenger */, 56, + 65, 8, 12, 2, + "home", 8, 15, 22, 5); + updateAisObjectList(ais); + } + private void initTestObject1() { // passenger ship AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, From 457d61bb571b26a23a48137f9aef181e63e58c42 Mon Sep 17 00:00:00 2001 From: Falk Date: Wed, 25 Sep 2024 00:24:07 +0200 Subject: [PATCH 66/74] change the available set for configurable CPA warning distances (now offer lower distances) --- .../aistracker/AisTrackerSettingsFragment.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java index 812a54f42de..e29a91d6d78 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -106,7 +106,7 @@ private void setupObjectLostTimeout() { Integer[] entryValues = {3, 5, 7, 10, 12, 15, 20}; String[] entries = new String[entryValues.length]; for (int i = 0; i < entryValues.length; i++) { - entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to ressource file + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to resource file } ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_OBJ_LOST_TIMEOUT.getId()); if (objectLostTimeout != null) { @@ -119,9 +119,9 @@ private void setupShipLostTimeout() { Integer[] entryValues = {2, 3, 4, 5, 7, 10, 15, 100 /* disabled: must be bigger than the biggest value of setupObjectLostTimeout() */}; String[] entries = new String[entryValues.length]; for (int i = 0; i < entryValues.length - 1; i++) { - entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to ressource file + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to resource file } - entries[entryValues.length - 1] = "disabled"; // TODO: move to ressource file + entries[entryValues.length - 1] = "disabled"; // TODO: move to resource file ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_SHIP_LOST_TIMEOUT.getId()); if (objectLostTimeout != null) { @@ -136,7 +136,7 @@ private boolean setupCpaWarningTime() { entries[0] = "disabled"; for (int i = 1; i < entryValues.length; i++) { entries[i] = entryValues[i] + " "; - entries[i] += entryValues[i].equals(1) ? "minute" : "minutes"; // TODO: move to ressource file + entries[i] += entryValues[i].equals(1) ? "minute" : "minutes"; // TODO: move to resource file } ListPreferenceEx cpaWarningTime = findPreference(plugin.AIS_CPA_WARNING_TIME.getId()); if (cpaWarningTime != null) { @@ -149,12 +149,13 @@ private boolean setupCpaWarningTime() { } @SuppressLint("DefaultLocale") private void setupCpaWarningDistance(boolean enabled) { - Float[] entryValues = {0.5f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + Float[] entryValues = {0.02f, 0.05f, 0.1f, 0.2f, 0.5f, 1.0f, 2.0f}; String[] entries = new String[entryValues.length]; for (int i = 0; i < entryValues.length; i++) { entries[i] = (ceil(entryValues[i]) == entryValues[i]) ? - String.format("%.0f ", entryValues[i]) : String.format("%.1f ", entryValues[i]); - entries[i] += entryValues[i].equals(1.0f) ? "nautical mile" : "nautical miles"; // TODO: move to ressource file + String.format("%.0f ", entryValues[i]) : + ((entryValues[i] < 0.1f) ? String.format("%.2f ", entryValues[i]) : String.format("%.1f ", entryValues[i])); + entries[i] += entryValues[i].equals(1.0f) ? "nautical mile" : "nautical miles"; // TODO: move to resource file } ListPreferenceEx cpaWarningDistance = findPreference(plugin.AIS_CPA_WARNING_DISTANCE.getId()); if (cpaWarningDistance != null) { From 0f6716dfa45b3196dcce36369f42b98fd15748f8 Mon Sep 17 00:00:00 2001 From: Falk Date: Thu, 26 Sep 2024 21:55:15 +0200 Subject: [PATCH 67/74] fixed wrong visualisation for AIS message type 18 --- .../plus/plugins/aistracker/AisObject.java | 6 ++++-- .../plugins/aistracker/AisTrackerLayer.java | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index b12ec861c0a..dbac7c5a79d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -315,6 +315,8 @@ private void initObjectClass() { default: this.objectClass = AIS_ATON; } + } else if (msgTypes.contains(18)) { + this.objectClass = AIS_VESSEL; } else { switch (ais_navStatus) { // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a @@ -437,7 +439,7 @@ public static int selectColor(AisObjType objType) { case AIS_VESSEL_COMMERCIAL: return Color.LTGRAY; default: - return 0; // black + return 0; // transparent } } @@ -568,7 +570,7 @@ private boolean needRotation() { * (5) the time when the course of the other vessel crosses the own course * is not in the past */ private boolean checkCpaWarning() { - if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0)) { + if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0) && (ais_sog > 0.0d)) { if (checkForCpaTimeout() && (ownPosition != null)) { Location aisPosition = getCurrentLocation(); if (aisPosition != null) { diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 638a7bcd3e2..1d743f987b9 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -355,13 +355,22 @@ private void initFakePosition() { AisObject.fakeOwnPosition(fake); Log.d("AisTrackerLayer", "initFakePosition: fake: " + fake.toString()); // in order to visualize this faked (own) position on the map, create an AIS object at this location... - AisObject ais = new AisObject(324578, 1, 20, 0, 1, (int)fakeCOG, - fakeCOG, fakeSOG, fakeLat, fakeLon, 0.0); + AisObject ais = new AisObject(324578, 18, 20, AisObjectConstants.INVALID_NAV_STATUS, + AisObjectConstants.INVALID_MANEUVER_INDICATOR, + (int)fakeCOG, fakeCOG, fakeSOG, fakeLat, fakeLon, AisObjectConstants.INVALID_ROT); updateAisObjectList(ais); - ais = new AisObject(324578, 5, 0, "own-position", "fake", 60 /* passenger */, 56, - 65, 8, 12, 2, - "home", 8, 15, 22, 5); + ais = new AisObject(324578, 24, 0, "callsign", "fake", 60, 56, + 65, 8, 12, AisObjectConstants.INVALID_DRAUGHT, + "home", AisObjectConstants.INVALID_ETA, AisObjectConstants.INVALID_ETA, + AisObjectConstants.INVALID_ETA_HOUR, AisObjectConstants.INVALID_ETA_MIN); updateAisObjectList(ais); + //AisObject ais = new AisObject(324578, 1, 20, 0, 1, (int)fakeCOG, + // fakeCOG, fakeSOG, fakeLat, fakeLon, 0.0); + //updateAisObjectList(ais); + //ais = new AisObject(324578, 5, 0, "own-position", "fake", 60 /* passenger */, 56, + // 65, 8, 12, 2, + // "home", 8, 15, 22, 5); + //updateAisObjectList(ais); } private void initTestObject1() { From f1b03c50084bb63ce03198a1c5cf757be1209b3b Mon Sep 17 00:00:00 2001 From: Falk Date: Thu, 26 Sep 2024 23:38:18 +0200 Subject: [PATCH 68/74] adjusted/extended object description in the context menu --- .../plus/plugins/aistracker/AisObject.java | 56 +++---------------- .../aistracker/AisObjectConstants.java | 1 - .../aistracker/AisObjectMenuController.java | 24 +++++--- .../plugins/aistracker/AisTrackerLayer.java | 20 ++++++- 4 files changed, 42 insertions(+), 59 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index dbac7c5a79d..f5ddce6dd60 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -1,36 +1,7 @@ package net.osmand.plus.plugins.aistracker; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_AIRPLANE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON_VIRTUAL; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_INVALID; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_LANDSTATION; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_SART; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_COMMERCIAL; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_FAST; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_FREIGHT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_PASSENGER; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_VESSEL_SPORT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.COUNTRY_CODES; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ALTITUDE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_COG; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_DIMENSION; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_DRAUGHT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA_HOUR; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ETA_MIN; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_HEADING; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_LAT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_LON; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_MANEUVER_INDICATOR; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_NAV_STATUS; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_ROT; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SHIP_TYPE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_SOG; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; -import static net.osmand.plus.plugins.aistracker.AisObjectConstants.CPA_UPDATE_TIMEOUT_IN_SECONDS; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.*; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.*; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getNewPosition; import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_DEFAULT_WARNING_TIME; @@ -408,8 +379,6 @@ public static int selectBitmap(AisObjType objType) { case AIS_VESSEL_COMMERCIAL: case AIS_INVALID: return R.drawable.ais_vessel; - case AIS_VESSEL_LOST: - return R.drawable.ais_vessel_cross; case AIS_LANDSTATION: return R.drawable.ais_land; case AIS_AIRPLANE: @@ -463,7 +432,7 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { private void setColor() { if (isLost(vesselLostTimeoutInMinutes)) { if (isMovable()) { - this.bitmapColor = 0; // black + this.bitmapColor = 0; // transparent } } else { this.bitmapColor = selectColor(this.objectClass); @@ -679,20 +648,10 @@ public Location getCurrentLocation() { } return newLocation; } - @Nullable - public String getCallSign() { - return this.ais_callSign; - } - @Nullable - public String getShipName() { - return this.ais_shipName; - } - @Nullable - public String getDestination() { - return this.ais_destination; - } - @NonNull - public String getCountryCode() { return this.countryCode; } + @Nullable public String getCallSign() { return this.ais_callSign; } + @Nullable public String getShipName() { return this.ais_shipName; } + @Nullable public String getDestination() { return this.ais_destination; } + @NonNull public String getCountryCode() { return this.countryCode; } public AisObjType getObjectClass() { return this.objectClass; } public long getLastUpdate() { return this.lastUpdate; } public static long getLastMessageReceived() { return lastMessageReceived; } @@ -967,4 +926,5 @@ public float getDistanceInNauticalMiles() { } return dist; } + public boolean getSignalLostState() { return (isLost(vesselLostTimeoutInMinutes) && isMovable()); } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index 7eb76b0b8b7..afb42fa526d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -31,7 +31,6 @@ public static enum AisObjType { AIS_VESSEL_PASSENGER, AIS_VESSEL_FREIGHT, AIS_VESSEL_COMMERCIAL, - AIS_VESSEL_LOST, // only dummy value, not assigned by a real vessel AIS_LANDSTATION, AIS_AIRPLANE, AIS_SART, diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java index cc9a084a783..a5119df39e4 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -1,5 +1,8 @@ package net.osmand.plus.plugins.aistracker; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON_VIRTUAL; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; @@ -146,7 +149,7 @@ public void addPlainMenuItems(String typeStr, PointDescription pointDescription, } } if (msgTypes.contains(21)) { // ATON (aid to navigation) - addMenuItem("ATON Type", aisObject.getAidTypeString()); + addMenuItem("Aid Type", aisObject.getAidTypeString()); addMenuItemDimension(); } else if (msgTypes.contains(9)) { // SAR aircraft addMenuItem("Object Type", "SAR Aircraft"); @@ -229,24 +232,31 @@ protected Object getObject() { @NonNull @Override public String getTypeStr() { - String res = ""; + String result = ""; SortedSet msgTypes = aisObject.getMsgTypes(); + AisObjectConstants.AisObjType objectClass = aisObject.getObjectClass(); for (Integer i : new Integer[]{5, 19, 24}) { if (msgTypes.contains(i)) { - res += aisObject.getShipTypeString(); + result += aisObject.getShipTypeString(); break; } } for (Integer i : new Integer[]{1, 2, 3}) { if (msgTypes.contains(i)) { - if (res.isEmpty()) { - res = "Vessel"; + if (result.isEmpty()) { + result = "Vessel"; } - res += ": " + aisObject.getNavStatusString() + "."; + result += ": " + aisObject.getNavStatusString() + "."; break; } } - return (res.isEmpty() ? "AIS object" : res); + if ((objectClass == AIS_ATON) || (objectClass == AIS_ATON_VIRTUAL)) { + int aidType = aisObject.getAidType(); + if (aidType != UNSPECIFIED_AID_TYPE) { + result = aisObject.getAidTypeString(); + } + } + return (result.isEmpty() ? "AIS object" : result); } @Override diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 1d743f987b9..ba88b01cd3d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -3,6 +3,7 @@ import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.knotsToMeterPerSecond; import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.meterToMiles; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.*; import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; import android.content.Context; @@ -32,6 +33,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.SortedSet; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; @@ -398,7 +400,7 @@ private void initTestObject3() { AisObject ais = new AisObject(878121, 4, 50.736d, 7.100d); updateAisObjectList(ais); // AIDS - ais = new AisObject( 521077, 21, 50.735d, 7.101d, 2, + ais = new AisObject( 521077, 21, 50.735d, 7.101d, 1, 0, 0, 0, 0); updateAisObjectList(ais); } @@ -592,11 +594,23 @@ public LatLon getObjectLocation(Object o) { public PointDescription getObjectName(Object o) { if (o instanceof AisObject) { AisObject ais = ((AisObject) o); + AisObjectConstants.AisObjType objectClass = ais.getObjectClass(); if (ais.getShipName() != null) { - return new PointDescription("AIS object", ais.getShipName()); + return new PointDescription("AIS object", ais.getShipName() + + (ais.getSignalLostState() ? " (signal lost)" : "")); + } else if (objectClass == AIS_LANDSTATION) { + return new PointDescription("AIS object", "Land Station with MMSI " + ais.getMmsi()); + } else if (objectClass == AIS_AIRPLANE) { + return new PointDescription("AIS object", "Airplane with MMSI " + + ais.getMmsi() + (ais.getSignalLostState() ? " (signal lost)" : "")); + } else if ((objectClass == AIS_ATON) || (objectClass == AIS_ATON_VIRTUAL)) { + return new PointDescription("AIS object", "Aid to Navigation"); + } else if (objectClass == AIS_SART) { + return new PointDescription("AIS object", "SART (Search and Rescue Transmitter)"); } return new PointDescription("AIS object", - "AIS object with MMSI " + ais.getMmsi()); + "AIS object with MMSI " + ais.getMmsi() + + (ais.getSignalLostState() ? " (signal lost)" : "")); } return null; } From 39978beb5ba638d98ad91e5c1fea832a66af3446 Mon Sep 17 00:00:00 2001 From: Falk Date: Fri, 27 Sep 2024 22:59:55 +0200 Subject: [PATCH 69/74] adjust visualisation of moored vessels (vessels at rest): draw a circle instead of a bitmap --- .../plus/plugins/aistracker/AisObject.java | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index f5ddce6dd60..7fc90c46a32 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -77,6 +77,7 @@ public class AisObject { private int bitmapColor; private AisTrackerHelper.Cpa cpa; private long lastCpaUpdate = 0; + private boolean vesselAtRest = false; // if true, draw a circle instead of a bitmap public AisObject(int mmsi, int msgType, double lat, double lon) { initObj(mmsi, msgType); @@ -414,7 +415,8 @@ public static int selectColor(AisObjType objType) { private void setBitmap(@NonNull AisTrackerLayer mapLayer) { invalidateBitmap(); - if (isLost(vesselLostTimeoutInMinutes)) { + vesselAtRest = isVesselAtRest(); + if (isLost(vesselLostTimeoutInMinutes) && !vesselAtRest) { if (isMovable()) { this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); this.bitmapValid = true; @@ -430,7 +432,7 @@ private void setBitmap(@NonNull AisTrackerLayer mapLayer) { } private void setColor() { - if (isLost(vesselLostTimeoutInMinutes)) { + if (isLost(vesselLostTimeoutInMinutes) && !vesselAtRest) { if (isMovable()) { this.bitmapColor = 0; // transparent } @@ -459,6 +461,15 @@ private void updateBitmap(@NonNull AisTrackerLayer mapLayer, @NonNull Paint pain } } + private void drawCircle(float locationX, float locationY, + @NonNull Paint paint, @NonNull Canvas canvas) { + Paint localPaint = new Paint(paint); + localPaint.setColor(Color.DKGRAY); + canvas.drawCircle(locationX, locationY, 22.0f, localPaint); + localPaint.setColor(this.bitmapColor); + canvas.drawCircle(locationX, locationY, 18.0f, localPaint); + } + public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { updateBitmap(mapLayer, paint); @@ -470,14 +481,18 @@ public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, int locationY = tileBox.getPixYFromLatNoRot(this.ais_position.getLatitude()); float fx = locationX - this.bitmap.getWidth() / 2.0f; float fy = locationY - this.bitmap.getHeight() / 2.0f; - if (this.needRotation()) { + if (!vesselAtRest && this.needRotation()) { float rotation = 0; if (this.ais_cog != INVALID_COG) { rotation = (float)this.ais_cog; } else if (this.ais_heading != INVALID_HEADING ) { rotation = this.ais_heading; } canvas.rotate(rotation, locationX, locationY); } - canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); - if ((speedFactor > 0) && (!isLost(vesselLostTimeoutInMinutes))) { + if (vesselAtRest) { + drawCircle(locationX, locationY, paint, canvas); + } else { + canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); + } + if ((speedFactor > 0) && (!isLost(vesselLostTimeoutInMinutes)) && !vesselAtRest) { float lineStartX = locationX; float lineLength = (float)this.bitmap.getHeight() * speedFactor; float lineStartY = locationY - this.bitmap.getHeight() / 4.0f; @@ -527,6 +542,32 @@ private boolean needRotation() { } return false; } + /* return true if a vessel is moored etc. and needs to be drawn as a circle */ + private boolean isVesselAtRest() { + switch (this.objectClass) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + switch (this.ais_navStatus) { + case 5: // moored + return true; + default: + if (msgTypes.contains(18) || msgTypes.contains(24) + || msgTypes.contains(1) || msgTypes.contains(3)) { + if ((ais_cog == INVALID_COG /* maybe remove this condition */) + && (ais_sog == 0.0d)) { + return true; + } + } + return false; + } + default: + return false; + } + } /* return true if the vessel gets too close with the own position in the future * (danger of collusion); @@ -926,5 +967,7 @@ public float getDistanceInNauticalMiles() { } return dist; } - public boolean getSignalLostState() { return (isLost(vesselLostTimeoutInMinutes) && isMovable()); } + public boolean getSignalLostState() { + return (isLost(vesselLostTimeoutInMinutes) && isMovable() && !vesselAtRest); + } } From 46fb12275eb9b094311eba807091ffc6812a0c00 Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 28 Sep 2024 16:39:24 +0200 Subject: [PATCH 70/74] added two new AIS object types: AIS_VESSEL_AUTHORITIES and AIS_VESSEL_SAR with individual colors in visualisation --- .../plus/plugins/aistracker/AisObject.java | 24 +++++++++++++++---- .../aistracker/AisObjectConstants.java | 2 ++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 7fc90c46a32..4b0ef01e36d 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -218,20 +218,26 @@ private void initObjectClass() { case 32: // Towing case 33: // Dredging case 34: // Diving ops - case 35: // Military ops case 50: // Pilot Vessel - case 51: // Search and Rescue vessel case 52: // Tug case 53: // Port Tender case 54: // Anti-pollution equipment - case 55: // Law Enforcement case 56: // Spare - Local Vessel case 57: // Spare - Local Vessel - case 58: // Medical Transport case 59: // Noncombatant ship according to RR Resolution No. 18 this.objectClass = AIS_VESSEL_COMMERCIAL; break; + case 35: // Military ops + case 55: // Law Enforcement + this.objectClass = AIS_VESSEL_AUTHORITIES; + break; + + case 51: // Search and Rescue vessel + case 58: // Medical Transport + this.objectClass = AIS_VESSEL_SAR; + break; + case 36: // Sailing case 37: // Pleasure Craft this.objectClass = AIS_VESSEL_SPORT; @@ -378,6 +384,8 @@ public static int selectBitmap(AisObjType objType) { case AIS_VESSEL_PASSENGER: case AIS_VESSEL_FREIGHT: case AIS_VESSEL_COMMERCIAL: + case AIS_VESSEL_AUTHORITIES: + case AIS_VESSEL_SAR: case AIS_INVALID: return R.drawable.ais_vessel; case AIS_LANDSTATION: @@ -408,6 +416,10 @@ public static int selectColor(AisObjType objType) { return Color.GRAY; case AIS_VESSEL_COMMERCIAL: return Color.LTGRAY; + case AIS_VESSEL_AUTHORITIES: + return 0x556b2f; // darkolivegreen + case AIS_VESSEL_SAR: + return 0xfa8072; // salmon default: return 0; // transparent } @@ -511,6 +523,8 @@ public boolean isMovable() { case AIS_VESSEL_PASSENGER: case AIS_VESSEL_FREIGHT: case AIS_VESSEL_COMMERCIAL: + case AIS_VESSEL_AUTHORITIES: + case AIS_VESSEL_SAR: case AIS_AIRPLANE: return true; default: @@ -551,6 +565,8 @@ private boolean isVesselAtRest() { case AIS_VESSEL_PASSENGER: case AIS_VESSEL_FREIGHT: case AIS_VESSEL_COMMERCIAL: + case AIS_VESSEL_AUTHORITIES: + case AIS_VESSEL_SAR: switch (this.ais_navStatus) { case 5: // moored return true; diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index afb42fa526d..374a18b8534 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -31,6 +31,8 @@ public static enum AisObjType { AIS_VESSEL_PASSENGER, AIS_VESSEL_FREIGHT, AIS_VESSEL_COMMERCIAL, + AIS_VESSEL_AUTHORITIES, + AIS_VESSEL_SAR, AIS_LANDSTATION, AIS_AIRPLANE, AIS_SART, From 1c3cd3276f8c7a9d5601e0eb2956c00eb50df36f Mon Sep 17 00:00:00 2001 From: Falk Date: Fri, 18 Oct 2024 22:44:06 +0200 Subject: [PATCH 71/74] correct wrong color definition in AisObject.selectColor() --- .../plus/plugins/aistracker/AisObject.java | 5 ++-- .../plugins/aistracker/AisTrackerLayer.java | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 4b0ef01e36d..c3960624240 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -417,9 +417,9 @@ public static int selectColor(AisObjType objType) { case AIS_VESSEL_COMMERCIAL: return Color.LTGRAY; case AIS_VESSEL_AUTHORITIES: - return 0x556b2f; // darkolivegreen + return Color.argb(0xff, 0x55, 0x6b, 0x2f); // 0x556b2f: darkolivegreen case AIS_VESSEL_SAR: - return 0xfa8072; // salmon + return Color.argb(0xff, 0xfa, 0x80, 0x72); // 0xfa8072: salmon default: return 0; // transparent } @@ -476,6 +476,7 @@ private void updateBitmap(@NonNull AisTrackerLayer mapLayer, @NonNull Paint pain private void drawCircle(float locationX, float locationY, @NonNull Paint paint, @NonNull Canvas canvas) { Paint localPaint = new Paint(paint); + localPaint.setColorFilter(null); localPaint.setColor(Color.DKGRAY); canvas.drawCircle(locationX, locationY, 22.0f, localPaint); localPaint.setColor(this.bitmapColor); diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index ba88b01cd3d..47a25d029bc 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -72,6 +72,7 @@ public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugi //initTestObject2(); //initTestObject3(); //initTestObject4(); + //initTestObject5(); //testCrossingTimes(); //testCpa(); //initFakePosition(); @@ -387,12 +388,14 @@ private void initTestObject1() { } private void initTestObject2() { // sailing boat - AisObject ais = new AisObject(454011, 1, 20, 8, 0, 120, - 125.0, 4.4, 50.737d, 7.098d, 0.0); + AisObject ais = new AisObject(454011, 18, 20, AisObjectConstants.INVALID_NAV_STATUS, + AisObjectConstants.INVALID_MANEUVER_INDICATOR, + 125, 125.0, 4.4, 50.737d, 7.098d, AisObjectConstants.INVALID_ROT); updateAisObjectList(ais); - ais = new AisObject(454011, 5, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, - 0, 0, 0, 0, - "@@@", 0, 0, 0, 0); + ais = new AisObject(454011, 24, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, + 0, 0, 0, AisObjectConstants.INVALID_DRAUGHT, + "home", AisObjectConstants.INVALID_ETA, AisObjectConstants.INVALID_ETA, + AisObjectConstants.INVALID_ETA_HOUR, AisObjectConstants.INVALID_ETA_MIN); updateAisObjectList(ais); } private void initTestObject3() { @@ -409,6 +412,17 @@ private void initTestObject4() { AisObject ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); updateAisObjectList(ais); } + private void initTestObject5() { + // law enforcement + AisObject ais = new AisObject(34569, 1, 20, 5, 1, 15, + 25.0, 8.4, 50.739d, 7.0931d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(34569, 5, 0, "TEST-CALLSIGN3", + "Mecklenburg Vorpommern", 55 /* law enforcement */, 26, + 5, 8, 4, 1, + "Potsdam", 8, 15, 22, 5); + updateAisObjectList(ais); + } private void initTimer() { TimerTask taskCheckAisObjectList; From dcd9967f99c91448aa0cb4526cfe2e0a4a066987 Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 19 Oct 2024 22:54:47 +0200 Subject: [PATCH 72/74] allow status change from VALID to INVALID for some AIS attributes --- .../plus/plugins/aistracker/AisObject.java | 36 +++++++++++++------ .../plugins/aistracker/AisTrackerLayer.java | 2 +- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index c3960624240..3e5520f0fd1 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -24,6 +24,8 @@ import net.osmand.data.RotatedTileBox; import net.osmand.plus.R; +import java.util.Arrays; +import java.util.List; import java.util.SortedSet; import java.util.TreeSet; @@ -332,13 +334,11 @@ private void invalidateBitmap() { } public void set(@NonNull AisObject ais) { + /* attention: this method does not produce an exact copy of the given object */ this.ais_mmsi = ais.getMmsi(); this.ais_msgType = ais.getMsgType(); if (ais.getTimestamp() != 0) { this.ais_timeStamp = ais.getTimestamp(); } if (ais.getImo() != 0 ) { this.ais_imo = ais.getImo(); } - if (ais.getHeading() != INVALID_HEADING ) { this.ais_heading = ais.getHeading(); } - if (ais.getNavStatus() != INVALID_NAV_STATUS ) { this.ais_navStatus = ais.getNavStatus(); } - if (ais.getManInd() != INVALID_MANEUVER_INDICATOR ) { this.ais_manInd = ais.getManInd(); } if (ais.getShipType() != INVALID_SHIP_TYPE ) { this.ais_shipType = ais.getShipType(); } if (ais.getDimensionToBow() != INVALID_DIMENSION ) { this.ais_dimensionToBow = ais.getDimensionToBow(); } if (ais.getDimensionToStern() != INVALID_DIMENSION ) { this.ais_dimensionToStern = ais.getDimensionToStern(); } @@ -351,19 +351,32 @@ public void set(@NonNull AisObject ais) { if (ais.getAltitude() != INVALID_ALTITUDE) { this.ais_altitude = ais.getAltitude(); } if (ais.getAidType() != UNSPECIFIED_AID_TYPE) { this.ais_aidType = ais.getAidType(); } if (ais.getDraught() != INVALID_DRAUGHT) { this.ais_draught = ais.getDraught(); } - if (ais.getCog() != INVALID_COG) { this.ais_cog = ais.getCog(); } - if (ais.getSog() != INVALID_SOG) { this.ais_sog = ais.getSog(); } - if (ais.getRot() != INVALID_ROT) { this.ais_rot = ais.getRot(); } if (ais.getPosition() != null) { this.ais_position = ais.getPosition(); } if (ais.getCallSign() != null) { this.ais_callSign = ais.getCallSign(); } if (ais.getShipName() != null) { this.ais_shipName = ais.getShipName(); } if (ais.getDestination() != null ) { this.ais_destination = ais.getDestination(); } - this.countryCode = ais.getCountryCode(); + /* the following values may change its value from VALID to INVALID, + hence overwriting with INVALID is accepted in some cases... */ + final List msgListHeading = Arrays.asList(1, 2, 3, 18, 19, 27); + final List msgListStatus = Arrays.asList(1, 2, 3, 27); + final List msgListCourse = Arrays.asList(1, 2, 3, 9, 18, 19, 27); + if (msgListHeading.contains(ais_msgType)) { + this.ais_heading = ais.getHeading(); + } + if (msgListStatus.contains(ais_msgType)) { + this.ais_navStatus = ais.getNavStatus(); + this.ais_manInd = ais.getManInd(); + this.ais_rot = ais.getRot(); + } + if (msgListCourse.contains(ais_msgType)) { + this.ais_cog = ais.getCog(); + this.ais_sog = ais.getSog(); + } - /* this method does not produce an exact copy of the given object, here are the differences: */ + this.countryCode = ais.getCountryCode(); this.lastUpdate = System.currentTimeMillis(); - lastMessageReceived = this.lastUpdate; + lastMessageReceived = this.lastUpdate; // lastMessageReceived is a static variable for the entire AisObject class if (this.msgTypes == null) { this.msgTypes = new TreeSet<>(); } @@ -570,12 +583,13 @@ private boolean isVesselAtRest() { case AIS_VESSEL_SAR: switch (this.ais_navStatus) { case 5: // moored - return true; + /* sometimes the ais_navStatus is wrong and contradicts other data... */ + return (ais_cog == INVALID_COG) || (ais_sog < 0.2d); default: if (msgTypes.contains(18) || msgTypes.contains(24) || msgTypes.contains(1) || msgTypes.contains(3)) { if ((ais_cog == INVALID_COG /* maybe remove this condition */) - && (ais_sog == 0.0d)) { + && (ais_sog < 0.2d)) { return true; } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java index 47a25d029bc..fbc2e2016f9 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -414,7 +414,7 @@ private void initTestObject4() { } private void initTestObject5() { // law enforcement - AisObject ais = new AisObject(34569, 1, 20, 5, 1, 15, + AisObject ais = new AisObject(34569, 1, 20, 5 /* moored */, 1, 15, 25.0, 8.4, 50.739d, 7.0931d, 0.0); updateAisObjectList(ais); ais = new AisObject(34569, 5, 0, "TEST-CALLSIGN3", From e5dcee3a6c6f9de8557d85adf78728b3cb3597ad Mon Sep 17 00:00:00 2001 From: Falk Date: Sat, 19 Oct 2024 23:25:33 +0200 Subject: [PATCH 73/74] created new AisObjectType: AIS_VESSEL_OTHER with individual color --- .../src/net/osmand/plus/plugins/aistracker/AisObject.java | 7 ++++++- .../osmand/plus/plugins/aistracker/AisObjectConstants.java | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 3e5520f0fd1..1e885885680 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -276,7 +276,7 @@ private void initObjectClass() { case 94: // Other Type, Hazardous category D case 99: // Other Type, no additional information default: - this.objectClass = AIS_VESSEL; + this.objectClass = AIS_VESSEL_OTHER; break; } /* for the case that no ship type was transmitted... */ @@ -399,6 +399,7 @@ public static int selectBitmap(AisObjType objType) { case AIS_VESSEL_COMMERCIAL: case AIS_VESSEL_AUTHORITIES: case AIS_VESSEL_SAR: + case AIS_VESSEL_OTHER: case AIS_INVALID: return R.drawable.ais_vessel; case AIS_LANDSTATION: @@ -433,6 +434,8 @@ public static int selectColor(AisObjType objType) { return Color.argb(0xff, 0x55, 0x6b, 0x2f); // 0x556b2f: darkolivegreen case AIS_VESSEL_SAR: return Color.argb(0xff, 0xfa, 0x80, 0x72); // 0xfa8072: salmon + case AIS_VESSEL_OTHER: + return Color.argb(0xff, 0x00, 0xbf, 0xff); // 0x00bfff: deepskyblue default: return 0; // transparent } @@ -539,6 +542,7 @@ public boolean isMovable() { case AIS_VESSEL_COMMERCIAL: case AIS_VESSEL_AUTHORITIES: case AIS_VESSEL_SAR: + case AIS_VESSEL_OTHER: case AIS_AIRPLANE: return true; default: @@ -581,6 +585,7 @@ private boolean isVesselAtRest() { case AIS_VESSEL_COMMERCIAL: case AIS_VESSEL_AUTHORITIES: case AIS_VESSEL_SAR: + case AIS_VESSEL_OTHER: switch (this.ais_navStatus) { case 5: // moored /* sometimes the ais_navStatus is wrong and contradicts other data... */ diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java index 374a18b8534..2df28c2f327 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -33,6 +33,7 @@ public static enum AisObjType { AIS_VESSEL_COMMERCIAL, AIS_VESSEL_AUTHORITIES, AIS_VESSEL_SAR, + AIS_VESSEL_OTHER, AIS_LANDSTATION, AIS_AIRPLANE, AIS_SART, From e310ec1bec82b580b64af13ff7a43451d01b9b57 Mon Sep 17 00:00:00 2001 From: Falk Date: Sun, 20 Oct 2024 20:53:30 +0200 Subject: [PATCH 74/74] special handling for objectClass = AIS_INVALID: might be moveable --- OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java index 1e885885680..f0f43bb1975 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -545,6 +545,8 @@ public boolean isMovable() { case AIS_VESSEL_OTHER: case AIS_AIRPLANE: return true; + case AIS_INVALID: + return (this.ais_sog != INVALID_SOG) && (this.ais_sog > 0.0d); default: return false; }

TjyYFPs}Nok@<4 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%0A!lcrGprSHHJDH8k+GKZvR8pNeSDJlr^gNSMi-8a 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 GNBke$V>V|1 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xhdpi/ais_plane.png b/OsmAnd/res/drawable-xhdpi/ais_plane.png new file mode 100644 index 0000000000000000000000000000000000000000..df3f68e4c38235b5d7c38a416de2f6bfb2b3d3ea GIT binary patch literal 4348 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

TjyYFPs}Nok@<4 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%0A!lcrGprSHHJDH8k+GKZvR8pNeSDJlr^gNSMi-8a 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 GNBke$V>V|1 literal 0 HcmV?d00001 diff --git a/OsmAnd/res/drawable-xhdpi/ais_plane.png b/OsmAnd/res/drawable-xhdpi/ais_plane.png new file mode 100644 index 0000000000000000000000000000000000000000..df3f68e4c38235b5d7c38a416de2f6bfb2b3d3ea GIT binary patch literal 4348 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